blob: e8f41291aa1a29c9f6565e7040baf911a07b3524 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This file contains a wacky demonstration of creating a custom ScrollPosition
// setup. It's testing that we don't regress the factoring of the
// ScrollPosition/ScrollActivity logic into a state where you can no longer
// implement this, e.g. by oversimplifying it or overfitting it to the features
// built into the framework itself.
import 'dart:collection';
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class LinkedScrollController extends ScrollController {
LinkedScrollController({ this.before, this.after });
LinkedScrollController before;
LinkedScrollController after;
ScrollController _parent;
void setParent(ScrollController newParent) {
if (_parent != null) {
positions.forEach(_parent.detach);
}
_parent = newParent;
if (_parent != null) {
positions.forEach(_parent.attach);
}
}
@override
void attach(ScrollPosition position) {
assert(position is LinkedScrollPosition, 'A LinkedScrollController must only be used with LinkedScrollPositions.');
final LinkedScrollPosition linkedPosition = position as LinkedScrollPosition;
assert(linkedPosition.owner == this, 'A LinkedScrollPosition cannot change controllers once created.');
super.attach(position);
_parent?.attach(position);
}
@override
void detach(ScrollPosition position) {
super.detach(position);
_parent?.detach(position);
}
@override
void dispose() {
if (_parent != null) {
positions.forEach(_parent.detach);
}
super.dispose();
}
@override
LinkedScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
return LinkedScrollPosition(
this,
physics: physics,
context: context,
initialPixels: initialScrollOffset,
oldPosition: oldPosition,
);
}
bool get canLinkWithBefore => before != null && before.hasClients;
bool get canLinkWithAfter => after != null && after.hasClients;
Iterable<LinkedScrollActivity> linkWithBefore(LinkedScrollPosition driver) {
assert(canLinkWithBefore);
return before.link(driver);
}
Iterable<LinkedScrollActivity> linkWithAfter(LinkedScrollPosition driver) {
assert(canLinkWithAfter);
return after.link(driver);
}
Iterable<LinkedScrollActivity> link(LinkedScrollPosition driver) sync* {
assert(hasClients);
for (final LinkedScrollPosition position in positions.cast<LinkedScrollPosition>())
yield position.link(driver);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (before != null && after != null) {
description.add('links: ⬌');
} else if (before != null) {
description.add('links: ⬅');
} else if (after != null) {
description.add('links: ➡');
} else {
description.add('links: none');
}
}
}
class LinkedScrollPosition extends ScrollPositionWithSingleContext {
LinkedScrollPosition(
this.owner, {
ScrollPhysics physics,
ScrollContext context,
double initialPixels,
ScrollPosition oldPosition,
}) : assert(owner != null),
super(
physics: physics,
context: context,
initialPixels: initialPixels,
oldPosition: oldPosition,
);
final LinkedScrollController owner;
Set<LinkedScrollActivity> _beforeActivities;
Set<LinkedScrollActivity> _afterActivities;
@override
void beginActivity(ScrollActivity newActivity) {
if (newActivity == null)
return;
if (_beforeActivities != null) {
for (final LinkedScrollActivity activity in _beforeActivities)
activity.unlink(this);
_beforeActivities.clear();
}
if (_afterActivities != null) {
for (final LinkedScrollActivity activity in _afterActivities)
activity.unlink(this);
_afterActivities.clear();
}
super.beginActivity(newActivity);
}
@override
void applyUserOffset(double delta) {
updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
final double value = pixels - physics.applyPhysicsToUserOffset(this, delta);
if (value == pixels)
return;
double beforeOverscroll = 0.0;
if (owner.canLinkWithBefore && (value < minScrollExtent)) {
final double delta = value - minScrollExtent;
_beforeActivities ??= HashSet<LinkedScrollActivity>();
_beforeActivities.addAll(owner.linkWithBefore(this));
for (final LinkedScrollActivity activity in _beforeActivities)
beforeOverscroll = math.min(activity.moveBy(delta), beforeOverscroll);
assert(beforeOverscroll <= 0.0);
}
double afterOverscroll = 0.0;
if (owner.canLinkWithAfter && (value > maxScrollExtent)) {
final double delta = value - maxScrollExtent;
_afterActivities ??= HashSet<LinkedScrollActivity>();
_afterActivities.addAll(owner.linkWithAfter(this));
for (final LinkedScrollActivity activity in _afterActivities)
afterOverscroll = math.max(activity.moveBy(delta), afterOverscroll);
assert(afterOverscroll >= 0.0);
}
assert(beforeOverscroll == 0.0 || afterOverscroll == 0.0);
final double localOverscroll = setPixels(value.clamp(
owner.canLinkWithBefore ? minScrollExtent : -double.infinity,
owner.canLinkWithAfter ? maxScrollExtent : double.infinity,
) as double);
assert(localOverscroll == 0.0 || (beforeOverscroll == 0.0 && afterOverscroll == 0.0));
}
void _userMoved(ScrollDirection direction) {
updateUserScrollDirection(direction);
}
LinkedScrollActivity link(LinkedScrollPosition driver) {
if (this.activity is! LinkedScrollActivity)
beginActivity(LinkedScrollActivity(this));
final LinkedScrollActivity activity = this.activity as LinkedScrollActivity;
activity.link(driver);
return activity;
}
void unlink(LinkedScrollActivity activity) {
if (_beforeActivities != null)
_beforeActivities.remove(activity);
if (_afterActivities != null)
_afterActivities.remove(activity);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('owner: $owner');
}
}
class LinkedScrollActivity extends ScrollActivity {
LinkedScrollActivity(
LinkedScrollPosition delegate,
) : super(delegate);
@override
LinkedScrollPosition get delegate => super.delegate as LinkedScrollPosition;
final Set<LinkedScrollPosition> drivers = HashSet<LinkedScrollPosition>();
void link(LinkedScrollPosition driver) {
drivers.add(driver);
}
void unlink(LinkedScrollPosition driver) {
drivers.remove(driver);
if (drivers.isEmpty)
delegate?.goIdle();
}
@override
bool get shouldIgnorePointer => true;
@override
bool get isScrolling => true;
// LinkedScrollActivity is not self-driven but moved by calls to the [moveBy]
// method.
@override
double get velocity => 0.0;
double moveBy(double delta) {
assert(drivers.isNotEmpty);
ScrollDirection commonDirection;
for (final LinkedScrollPosition driver in drivers) {
commonDirection ??= driver.userScrollDirection;
if (driver.userScrollDirection != commonDirection)
commonDirection = ScrollDirection.idle;
}
delegate._userMoved(commonDirection);
return delegate.setPixels(delegate.pixels + delta);
}
@override
void dispose() {
for (final LinkedScrollPosition driver in drivers)
driver.unlink(this);
super.dispose();
}
}
class Test extends StatefulWidget {
const Test({ Key key }) : super(key: key);
@override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
LinkedScrollController _beforeController;
LinkedScrollController _afterController;
@override
void initState() {
super.initState();
_beforeController = LinkedScrollController();
_afterController = LinkedScrollController(before: _beforeController);
_beforeController.after = _afterController;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_beforeController.setParent(PrimaryScrollController.of(context));
_afterController.setParent(PrimaryScrollController.of(context));
}
@override
void dispose() {
_beforeController.dispose();
_afterController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Column(
children: <Widget>[
Expanded(
child: ListView(
controller: _beforeController,
children: <Widget>[
Container(
margin: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
height: 250.0,
color: const Color(0xFF90F090),
child: const Center(child: Text('Hello A')),
),
Container(
margin: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
height: 250.0,
color: const Color(0xFF90F090),
child: const Center(child: Text('Hello B')),
),
Container(
margin: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
height: 250.0,
color: const Color(0xFF90F090),
child: const Center(child: Text('Hello C')),
),
Container(
margin: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
height: 250.0,
color: const Color(0xFF90F090),
child: const Center(child: Text('Hello D')),
),
],
),
),
const Divider(),
Expanded(
child: ListView(
controller: _afterController,
children: <Widget>[
Container(
margin: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
height: 250.0,
color: const Color(0xFF9090F0),
child: const Center(child: Text('Hello 1')),
),
Container(
margin: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
height: 250.0,
color: const Color(0xFF9090F0),
child: const Center(child: Text('Hello 2')),
),
Container(
margin: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
height: 250.0,
color: const Color(0xFF9090F0),
child: const Center(child: Text('Hello 3')),
),
Container(
margin: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
height: 250.0,
color: const Color(0xFF9090F0),
child: const Center(child: Text('Hello 4')),
),
],
),
),
],
),
);
}
}
void main() {
testWidgets('LinkedScrollController - 1', (WidgetTester tester) async {
await tester.pumpWidget(const Test());
expect(find.text('Hello A'), findsOneWidget);
expect(find.text('Hello 1'), findsOneWidget);
expect(find.text('Hello D'), findsNothing);
expect(find.text('Hello 4'), findsNothing);
await tester.pump(const Duration(seconds: 2));
await tester.fling(find.text('Hello A'), const Offset(0.0, -50.0), 10000.0);
await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 2));
expect(find.text('Hello A'), findsNothing);
expect(find.text('Hello 1'), findsOneWidget);
expect(find.text('Hello D'), findsOneWidget);
expect(find.text('Hello 4'), findsNothing);
await tester.pump(const Duration(seconds: 2));
await tester.drag(find.text('Hello D'), const Offset(0.0, -10000.0));
await tester.pump(const Duration(seconds: 2));
expect(find.text('Hello A'), findsNothing);
expect(find.text('Hello 1'), findsNothing);
expect(find.text('Hello D'), findsOneWidget);
expect(find.text('Hello 4'), findsOneWidget);
await tester.pump(const Duration(seconds: 2));
await tester.drag(find.text('Hello D'), const Offset(0.0, -10000.0));
await tester.pump(const Duration(seconds: 2));
expect(find.text('Hello A'), findsNothing);
expect(find.text('Hello 1'), findsNothing);
expect(find.text('Hello D'), findsOneWidget);
expect(find.text('Hello 4'), findsOneWidget);
await tester.pump(const Duration(seconds: 2));
await tester.drag(find.text('Hello 4'), const Offset(0.0, -10000.0));
await tester.pump(const Duration(seconds: 2));
expect(find.text('Hello A'), findsNothing);
expect(find.text('Hello 1'), findsNothing);
expect(find.text('Hello D'), findsOneWidget);
expect(find.text('Hello 4'), findsOneWidget);
await tester.pump(const Duration(seconds: 2));
await tester.drag(find.text('Hello D'), const Offset(0.0, 10000.0));
await tester.pump(const Duration(seconds: 2));
expect(find.text('Hello A'), findsOneWidget);
expect(find.text('Hello 1'), findsNothing);
expect(find.text('Hello D'), findsNothing);
expect(find.text('Hello 4'), findsOneWidget);
await tester.pump(const Duration(seconds: 2));
await tester.drag(find.text('Hello A'), const Offset(0.0, 10000.0));
await tester.pump(const Duration(seconds: 2));
expect(find.text('Hello A'), findsOneWidget);
expect(find.text('Hello 1'), findsNothing);
expect(find.text('Hello D'), findsNothing);
expect(find.text('Hello 4'), findsOneWidget);
await tester.pump(const Duration(seconds: 2));
await tester.drag(find.text('Hello A'), const Offset(0.0, -10000.0));
await tester.pump(const Duration(seconds: 2));
expect(find.text('Hello A'), findsNothing);
expect(find.text('Hello 1'), findsNothing);
expect(find.text('Hello D'), findsOneWidget);
expect(find.text('Hello 4'), findsOneWidget);
await tester.pump(const Duration(seconds: 2));
await tester.drag(find.text('Hello 4'), const Offset(0.0, 10000.0));
await tester.pump(const Duration(seconds: 2));
expect(find.text('Hello A'), findsOneWidget);
expect(find.text('Hello 1'), findsOneWidget);
expect(find.text('Hello D'), findsNothing);
expect(find.text('Hello 4'), findsNothing);
await tester.pump(const Duration(seconds: 2));
await tester.drag(find.text('Hello 1'), const Offset(0.0, 10000.0));
await tester.pump(const Duration(seconds: 2));
expect(find.text('Hello A'), findsOneWidget);
expect(find.text('Hello 1'), findsOneWidget);
expect(find.text('Hello D'), findsNothing);
expect(find.text('Hello 4'), findsNothing);
await tester.pump(const Duration(seconds: 2));
await tester.drag(find.text('Hello 1'), const Offset(0.0, -10000.0));
await tester.pump(const Duration(seconds: 2));
expect(find.text('Hello A'), findsOneWidget);
expect(find.text('Hello 1'), findsNothing);
expect(find.text('Hello D'), findsNothing);
expect(find.text('Hello 4'), findsOneWidget);
});
testWidgets('LinkedScrollController - 2', (WidgetTester tester) async {
await tester.pumpWidget(const Test());
expect(find.text('Hello A'), findsOneWidget);
expect(find.text('Hello B'), findsOneWidget);
expect(find.text('Hello C'), findsNothing);
expect(find.text('Hello D'), findsNothing);
expect(find.text('Hello 1'), findsOneWidget);
expect(find.text('Hello 2'), findsOneWidget);
expect(find.text('Hello 3'), findsNothing);
expect(find.text('Hello 4'), findsNothing);
final TestGesture gestureTop = await tester.startGesture(const Offset(200.0, 150.0));
final TestGesture gestureBottom = await tester.startGesture(const Offset(600.0, 450.0));
await tester.pump(const Duration(seconds: 1));
await gestureTop.moveBy(const Offset(0.0, -270.0));
await tester.pump(const Duration(seconds: 1));
expect(find.text('Hello A'), findsNothing);
expect(find.text('Hello B'), findsOneWidget);
expect(find.text('Hello C'), findsOneWidget);
expect(find.text('Hello D'), findsNothing);
expect(find.text('Hello 1'), findsOneWidget);
expect(find.text('Hello 2'), findsOneWidget);
expect(find.text('Hello 3'), findsNothing);
expect(find.text('Hello 4'), findsNothing);
await gestureBottom.moveBy(const Offset(0.0, -270.0));
await tester.pump(const Duration(seconds: 1));
expect(find.text('Hello A'), findsNothing);
expect(find.text('Hello B'), findsOneWidget);
expect(find.text('Hello C'), findsOneWidget);
expect(find.text('Hello D'), findsNothing);
expect(find.text('Hello 1'), findsNothing);
expect(find.text('Hello 2'), findsOneWidget);
expect(find.text('Hello 3'), findsOneWidget);
expect(find.text('Hello 4'), findsNothing);
await gestureTop.moveBy(const Offset(0.0, -270.0));
await gestureBottom.moveBy(const Offset(0.0, -270.0));
await tester.pump(const Duration(seconds: 1));
expect(find.text('Hello A'), findsNothing);
expect(find.text('Hello B'), findsNothing);
expect(find.text('Hello C'), findsOneWidget);
expect(find.text('Hello D'), findsOneWidget);
expect(find.text('Hello 1'), findsNothing);
expect(find.text('Hello 2'), findsNothing);
expect(find.text('Hello 3'), findsOneWidget);
expect(find.text('Hello 4'), findsOneWidget);
await gestureTop.moveBy(const Offset(0.0, 270.0));
await gestureBottom.moveBy(const Offset(0.0, 270.0));
await tester.pump(const Duration(seconds: 1));
expect(find.text('Hello A'), findsNothing);
expect(find.text('Hello B'), findsOneWidget);
expect(find.text('Hello C'), findsOneWidget);
expect(find.text('Hello D'), findsNothing);
expect(find.text('Hello 1'), findsNothing);
expect(find.text('Hello 2'), findsOneWidget);
expect(find.text('Hello 3'), findsOneWidget);
expect(find.text('Hello 4'), findsNothing);
await gestureBottom.moveBy(const Offset(0.0, 270.0));
await tester.pump(const Duration(seconds: 1));
expect(find.text('Hello A'), findsNothing);
expect(find.text('Hello B'), findsOneWidget);
expect(find.text('Hello C'), findsOneWidget);
expect(find.text('Hello D'), findsNothing);
expect(find.text('Hello 1'), findsOneWidget);
expect(find.text('Hello 2'), findsOneWidget);
expect(find.text('Hello 3'), findsNothing);
expect(find.text('Hello 4'), findsNothing);
await gestureBottom.moveBy(const Offset(0.0, 50.0));
await tester.pump(const Duration(seconds: 1));
expect(find.text('Hello A'), findsOneWidget);
expect(find.text('Hello B'), findsOneWidget);
expect(find.text('Hello C'), findsNothing);
expect(find.text('Hello D'), findsNothing);
expect(find.text('Hello 1'), findsOneWidget);
expect(find.text('Hello 2'), findsOneWidget);
expect(find.text('Hello 3'), findsNothing);
expect(find.text('Hello 4'), findsNothing);
await gestureBottom.moveBy(const Offset(0.0, 50.0));
await tester.pump(const Duration(seconds: 1));
expect(find.text('Hello A'), findsOneWidget);
expect(find.text('Hello B'), findsOneWidget);
expect(find.text('Hello C'), findsNothing);
expect(find.text('Hello D'), findsNothing);
expect(find.text('Hello 1'), findsOneWidget);
expect(find.text('Hello 2'), findsOneWidget);
expect(find.text('Hello 3'), findsNothing);
expect(find.text('Hello 4'), findsNothing);
await gestureBottom.moveBy(const Offset(0.0, 50.0));
await tester.pump(const Duration(seconds: 1));
expect(find.text('Hello A'), findsOneWidget);
expect(find.text('Hello B'), findsOneWidget);
expect(find.text('Hello C'), findsNothing);
expect(find.text('Hello D'), findsNothing);
expect(find.text('Hello 1'), findsOneWidget);
expect(find.text('Hello 2'), findsOneWidget);
expect(find.text('Hello 3'), findsNothing);
expect(find.text('Hello 4'), findsNothing);
await gestureTop.moveBy(const Offset(0.0, -270.0));
expect(find.text('Hello A'), findsOneWidget);
expect(find.text('Hello B'), findsOneWidget);
expect(find.text('Hello C'), findsNothing);
expect(find.text('Hello D'), findsNothing);
expect(find.text('Hello 1'), findsOneWidget);
expect(find.text('Hello 2'), findsOneWidget);
expect(find.text('Hello 3'), findsNothing);
expect(find.text('Hello 4'), findsNothing);
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 60));
});
}