Unverified Commit d8da0917 authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Android 12 overscroll stretch effect (#87839)

parent 8d89632f
......@@ -15,6 +15,17 @@
version: 1
transforms:
# Changes made in https://github.com/flutter/flutter/pull/87839
- title: "Migrate to 'disallowIndicator'"
date: 2021-08-06
element:
uris: [ 'material.dart', 'widgets.dart', 'cupertino.dart' ]
method: 'disallowGlow'
inClass: 'OverscrollIndicatorNotification'
changes:
- kind: 'rename'
newName: 'disallowIndicator'
# Changes made in https://github.com/flutter/flutter/pull/87281
- title: "Remove 'fixTextFieldOutlineLabel'"
date: 2021-04-30
......
......@@ -703,7 +703,9 @@ class MaterialScrollBehavior extends ScrollBehavior {
/// Creates a MaterialScrollBehavior that decorates [Scrollable]s with
/// [GlowingOverscrollIndicator]s and [Scrollbar]s based on the current
/// platform and provided [ScrollableDetails].
const MaterialScrollBehavior();
const MaterialScrollBehavior({
AndroidOverscrollIndicator? androidOverscrollIndicator,
}) : super(androidOverscrollIndicator: androidOverscrollIndicator);
@override
TargetPlatform getPlatform(BuildContext context) => Theme.of(context).platform;
......@@ -743,6 +745,16 @@ class MaterialScrollBehavior extends ScrollBehavior {
case TargetPlatform.windows:
return child;
case TargetPlatform.android:
switch (androidOverscrollIndicator) {
case AndroidOverscrollIndicator.stretch:
return StretchingOverscrollIndicator(
axisDirection: details.direction,
child: child,
);
case AndroidOverscrollIndicator.glow:
continue glow;
}
glow:
case TargetPlatform.fuchsia:
return GlowingOverscrollIndicator(
axisDirection: details.direction,
......
......@@ -185,7 +185,7 @@ class NotificationListener<T extends Notification> extends StatelessWidget {
/// Called when a notification of the appropriate type arrives at this
/// location in the tree.
///
/// Return true to cancel the notification bubbling. Return false (or null) to
/// Return true to cancel the notification bubbling. Return false to
/// allow the notification to continue to be dispatched to further ancestors.
///
/// The notification's [Notification.visitAncestor] method is called for each
......
......@@ -6,7 +6,7 @@ import 'dart:async' show Timer;
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/physics.dart' show nearEqual, Tolerance;
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
......@@ -15,6 +15,7 @@ import 'framework.dart';
import 'notification_listener.dart';
import 'scroll_notification.dart';
import 'ticker_provider.dart';
import 'transitions.dart';
/// A visual indication that a scroll view has overscrolled.
///
......@@ -116,9 +117,10 @@ import 'ticker_provider.dart';
/// See also:
///
/// * [OverscrollIndicatorNotification], which can be used to manipulate the
/// glow position or prevent the glow from being painted at all
/// glow position or prevent the glow from being painted at all.
/// * [NotificationListener], to listen for the
/// [OverscrollIndicatorNotification]
/// [OverscrollIndicatorNotification].
/// * [StretchingOverscrollIndicator], a Material Design overscroll indicator.
class GlowingOverscrollIndicator extends StatefulWidget {
/// Creates a visual indication that a scroll view has overscrolled.
///
......@@ -165,22 +167,28 @@ class GlowingOverscrollIndicator extends StatefulWidget {
/// viewport.
final bool showTrailing;
/// {@template flutter.overscroll.axisDirection}
/// The direction of positive scroll offsets in the [Scrollable] whose
/// overscrolls are to be visualized.
/// {@endtemplate}
final AxisDirection axisDirection;
/// {@template flutter.overscroll.axis}
/// The axis along which scrolling occurs in the [Scrollable] whose
/// overscrolls are to be visualized.
/// {@endtemplate}
Axis get axis => axisDirectionToAxis(axisDirection);
/// The color of the glow. The alpha channel is ignored.
final Color color;
/// {@template flutter.overscroll.notificationPredicate}
/// A check that specifies whether a [ScrollNotification] should be
/// handled by this widget.
///
/// By default, checks whether `notification.depth == 0`. Set it to something
/// else for more complicated layouts.
/// else for more complicated layouts, such as nested [ScrollView]s.
/// {@endtemplate}
final ScrollNotificationPredicate notificationPredicate;
/// The widget below this widget in the tree.
......@@ -271,7 +279,7 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
assert(false);
}
final bool isLeading = controller == _leadingController;
if (_lastNotificationType != OverscrollNotification) {
if (_lastNotificationType is! OverscrollNotification) {
final OverscrollIndicatorNotification confirmationNotification = OverscrollIndicatorNotification(leading: isLeading);
confirmationNotification.dispatch(context);
_accepted[isLeading] = confirmationNotification._accepted;
......@@ -637,18 +645,285 @@ class _GlowingOverscrollIndicatorPainter extends CustomPainter {
}
}
/// A notification that an [GlowingOverscrollIndicator] will start showing an
/// overscroll indication.
/// A Material Design visual indication that a scroll view has overscrolled.
///
/// To prevent the indicator from showing the indication, call [disallowGlow] on
/// the notification.
/// A [StretchingOverscrollIndicator] listens for [ScrollNotification]s in order
/// to stretch the content of the [Scrollable]. These notifications are typically
/// generated by a [ScrollView], such as a [ListView] or a [GridView].
///
/// When triggered, the [StretchingOverscrollIndicator] generates an
/// [OverscrollIndicatorNotification] before showing an overscroll indication.
/// To prevent the indicator from showing the indication, call
/// [OverscrollIndicatorNotification.disallowIndicator] on the notification.
///
/// Created by [ScrollBehavior.buildOverscrollIndicator] on platforms
/// (e.g., Android) that commonly use this type of overscroll indication when
/// [ScrollBehavior.androidOverscrollIndicator] is
/// [AndroidOverscrollIndicator.stretch]. Otherwise, the default
/// [GlowingOverscrollIndicator] is applied.
///
/// See also:
///
/// * [GlowingOverscrollIndicator], which generates this type of notification.
/// * [OverscrollIndicatorNotification], which can be used to prevent the stretch
/// effect from being applied at all.
/// * [NotificationListener], to listen for the
/// [OverscrollIndicatorNotification].
/// * [GlowingOverscrollIndicator], the default overscroll indicator for
/// [TargetPlatform.android] and [TargetPlatform.fuchsia].
class StretchingOverscrollIndicator extends StatefulWidget {
/// Creates a visual indication that a scroll view has overscrolled by
/// applying a stretch transformation to the content.
///
/// In order for this widget to display an overscroll indication, the [child]
/// widget must contain a widget that generates a [ScrollNotification], such
/// as a [ListView] or a [GridView].
///
/// The [axisDirection] and [notificationPredicate] arguments must not be null.
const StretchingOverscrollIndicator({
Key? key,
required this.axisDirection,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.child,
}) : assert(axisDirection != null),
assert(notificationPredicate != null),
super(key: key);
/// {@macro flutter.overscroll.axisDirection}
final AxisDirection axisDirection;
/// {@macro flutter.overscroll.axis}
Axis get axis => axisDirectionToAxis(axisDirection);
/// {@macro flutter.overscroll.notificationPredicate}
final ScrollNotificationPredicate notificationPredicate;
/// The widget below this widget in the tree.
///
/// The overscroll indicator will apply a stretch effect to this child. This
/// child (and its subtree) should include a source of [ScrollNotification]
/// notifications.
///
/// Typically a [StretchingOverscrollIndicator] is created by a
/// [ScrollBehavior.buildOverscrollIndicator] method when opted-in using the
/// [ScrollBehavior.androidOverscrollIndicator] flag. In this case
/// the child is usually the one provided as an argument to that method.
final Widget? child;
@override
State<StretchingOverscrollIndicator> createState() => _StretchingOverscrollIndicatorState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(EnumProperty<AxisDirection>('axisDirection', axisDirection));
}
}
class _StretchingOverscrollIndicatorState extends State<StretchingOverscrollIndicator> with TickerProviderStateMixin {
late final _StretchController _stretchController = _StretchController(vsync: this);
ScrollNotification? _lastNotification;
OverscrollNotification? _lastOverscrollNotification;
bool _accepted = true;
bool _handleScrollNotification(ScrollNotification notification) {
if (!widget.notificationPredicate(notification))
return false;
if (notification is OverscrollNotification) {
_lastOverscrollNotification = notification;
if (_lastNotification.runtimeType is! OverscrollNotification) {
final OverscrollIndicatorNotification confirmationNotification = OverscrollIndicatorNotification(leading: notification.overscroll < 0.0);
confirmationNotification.dispatch(context);
_accepted = confirmationNotification._accepted;
}
assert(notification.metrics.axis == widget.axis);
if (_accepted) {
if (notification.velocity != 0.0) {
assert(notification.dragDetails == null);
_stretchController.absorbImpact(notification.velocity.abs());
} else {
assert(notification.overscroll != 0.0);
if (notification.dragDetails != null) {
_stretchController.pull(notification.overscroll.abs() / notification.metrics.viewportDimension);
}
}
}
} else if (notification is ScrollEndNotification && notification.dragDetails != null
|| notification is ScrollUpdateNotification && notification.dragDetails != null) {
_stretchController.scrollEnd();
}
_lastNotification = notification;
return false;
}
@override
void dispose() {
_stretchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: AnimatedBuilder(
animation: _stretchController,
builder: (BuildContext context, Widget? child) {
final double stretch = _stretchController.value;
double x = 1.0;
double y = 1.0;
final AlignmentDirectional alignment;
switch (widget.axis) {
case Axis.horizontal:
x += stretch;
alignment = (_lastOverscrollNotification?.overscroll ?? 0) > 0
? AlignmentDirectional.centerEnd
: AlignmentDirectional.centerStart;
break;
case Axis.vertical:
y += stretch;
alignment = (_lastOverscrollNotification?.overscroll ?? 0) > 0
? AlignmentDirectional.bottomCenter
: AlignmentDirectional.topCenter;
break;
}
return Transform(
alignment: alignment,
transform: Matrix4.diagonal3Values(x, y, 1.0),
child: widget.child,
);
},
),
);
}
}
enum _StretchState {
idle,
absorb,
pull,
recede,
}
class _StretchController extends ChangeNotifier {
_StretchController({ required TickerProvider vsync }) {
_stretchController = AnimationController(vsync: vsync)
..addStatusListener(_changePhase);
final Animation<double> decelerator = CurvedAnimation(
parent: _stretchController,
curve: Curves.decelerate,
)..addListener(notifyListeners);
_stretchSize = decelerator.drive(_stretchSizeTween);
}
late final AnimationController _stretchController;
late final Animation<double> _stretchSize;
final Tween<double> _stretchSizeTween = Tween<double>(begin: 0.0, end: 0.0);
_StretchState _state = _StretchState.idle;
double _pullDistance = 0.0;
// Constants from Android.
static const double _exponentialScalar = math.e / 0.33;
static const double _stretchIntensity = 0.016;
static const double _flingFriction = 1.01;
static const Duration _stretchDuration = Duration(milliseconds: 400);
double get value => _stretchSize.value;
/// Handle a fling to the edge of the viewport at a particular velocity.
///
/// The velocity must be positive.
void absorbImpact(double velocity) {
assert(velocity >= 0.0);
velocity = velocity.clamp(1, 10000);
_stretchSizeTween.begin = _stretchSize.value;
_stretchSizeTween.end = math.min(_stretchIntensity + (_flingFriction / velocity), 1.0);
_stretchController.duration = Duration(milliseconds: (velocity * 0.02).round());
_stretchController.forward(from: 0.0);
_state = _StretchState.absorb;
}
/// Handle a user-driven overscroll.
///
/// The `normalizedOverscroll` argument should be the absolute value of the
/// scroll distance in logical pixels, divided by the extent of the viewport
/// in the main axis.
void pull(double normalizedOverscroll) {
assert(normalizedOverscroll >= 0.0);
_pullDistance = normalizedOverscroll + _pullDistance;
_stretchSizeTween.begin = _stretchSize.value;
final double linearIntensity =_stretchIntensity * _pullDistance;
final double exponentialIntensity = _stretchIntensity * (1 - math.exp(-_pullDistance * _exponentialScalar));
_stretchSizeTween.end = linearIntensity + exponentialIntensity;
_stretchController.duration = _stretchDuration;
if (_state != _StretchState.pull) {
_stretchController.forward(from: 0.0);
_state = _StretchState.pull;
} else {
if (!_stretchController.isAnimating) {
assert(_stretchController.value == 1.0);
notifyListeners();
}
}
}
void scrollEnd() {
if (_state == _StretchState.pull)
_recede(_stretchDuration);
}
void _changePhase(AnimationStatus status) {
if (status != AnimationStatus.completed)
return;
switch (_state) {
case _StretchState.absorb:
_recede(_stretchDuration);
break;
case _StretchState.recede:
_state = _StretchState.idle;
_pullDistance = 0.0;
break;
case _StretchState.pull:
case _StretchState.idle:
break;
}
}
void _recede(Duration duration) {
if (_state == _StretchState.recede || _state == _StretchState.idle)
return;
_stretchSizeTween.begin = _stretchSize.value;
_stretchSizeTween.end = 0.0;
_stretchController.duration = duration;
_stretchController.forward(from: 0.0);
_state = _StretchState.recede;
}
@override
void dispose() {
_stretchController.dispose();
super.dispose();
}
}
/// A notification that either a [GlowingOverscrollIndicator] or a
/// [StretchingOverscrollIndicator] will start showing an overscroll indication.
///
/// To prevent the indicator from showing the indication, call
/// [disallowIndicator] on the notification.
///
/// See also:
///
/// * [GlowingOverscrollIndicator], which generates this type of notification
/// by painting an indicator over the child content.
/// * [StretchingOverscrollIndicator], which generates this type of
/// notification by applying a stretch transformation to the child content.
class OverscrollIndicatorNotification extends Notification with ViewportNotificationMixin {
/// Creates a notification that an [GlowingOverscrollIndicator] will start
/// showing an overscroll indication.
/// Creates a notification that an [GlowingOverscrollIndicator] or a
/// [StretchingOverscrollIndicator] will start showing an overscroll indication.
///
/// The [leading] argument must not be null.
OverscrollIndicatorNotification({
......@@ -659,7 +934,7 @@ class OverscrollIndicatorNotification extends Notification with ViewportNotifica
/// view.
final bool leading;
/// Controls at which offset the glow should be drawn.
/// Controls at which offset a [GlowingOverscrollIndicator] draws.
///
/// A positive offset will move the glow away from its edge,
/// i.e. for a vertical, [leading] indicator, a [paintOffset] of 100.0 will
......@@ -669,15 +944,27 @@ class OverscrollIndicatorNotification extends Notification with ViewportNotifica
///
/// A negative [paintOffset] is generally not useful, since the glow will be
/// clipped.
///
/// This has no effect on a [StretchingOverscrollIndicator].
double paintOffset = 0.0;
bool _accepted = true;
/// Call this method if the glow should be prevented.
/// Call this method if the glow should be prevented. This method is
/// deprecated in favor of [disallowIndicator].
@Deprecated(
'Use disallowIndicator instead. '
'This feature was deprecated after v2.5.0-6.0.pre.',
)
void disallowGlow() {
_accepted = false;
}
/// Call this method if the overscroll indicator should be prevented.
void disallowIndicator() {
_accepted = false;
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
......
......@@ -21,6 +21,21 @@ const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
PointerDeviceKind.invertedStylus,
};
/// The default overscroll indicator applied on [TargetPlatform.android].
// TODO(Piinks): Complete migration to stretch by default.
const AndroidOverscrollIndicator _kDefaultAndroidOverscrollIndicator = AndroidOverscrollIndicator.glow;
/// Types of overscroll indicators supported by [TargetPlatform.android].
enum AndroidOverscrollIndicator {
/// Utilizes a [StretchingOverscrollIndicator], which transforms the contents
/// of a [ScrollView] when overscrolled.
stretch,
/// Utilizes a [GlowingOverscrollIndicator], painting a glowing semi circle on
/// top of the [ScrollView] in response to oversfcrolling.
glow,
}
/// Describes how [Scrollable] widgets should behave.
///
/// {@template flutter.widgets.scrollBehavior}
......@@ -46,7 +61,13 @@ const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
@immutable
class ScrollBehavior {
/// Creates a description of how [Scrollable] widgets should behave.
const ScrollBehavior();
const ScrollBehavior({
AndroidOverscrollIndicator? androidOverscrollIndicator,
}): _androidOverscrollIndicator = androidOverscrollIndicator;
/// Specifies which overscroll indicatpr to use on [TargetPlatform.android].
AndroidOverscrollIndicator get androidOverscrollIndicator => _androidOverscrollIndicator ?? _kDefaultAndroidOverscrollIndicator;
final AndroidOverscrollIndicator? _androidOverscrollIndicator;
/// Creates a copy of this ScrollBehavior, making it possible to
/// easily toggle `scrollbar` and `overscrollIndicator` effects.
......@@ -106,6 +127,16 @@ class ScrollBehavior {
case TargetPlatform.windows:
return child;
case TargetPlatform.android:
switch (androidOverscrollIndicator) {
case AndroidOverscrollIndicator.stretch:
return StretchingOverscrollIndicator(
axisDirection: axisDirection,
child: child,
);
case AndroidOverscrollIndicator.glow:
continue glow;
}
glow:
case TargetPlatform.fuchsia:
return GlowingOverscrollIndicator(
axisDirection: axisDirection,
......@@ -230,6 +261,11 @@ class _WrappedScrollBehavior implements ScrollBehavior {
@override
Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices;
@override
AndroidOverscrollIndicator get androidOverscrollIndicator => delegate.androidOverscrollIndicator;
@override
AndroidOverscrollIndicator? get _androidOverscrollIndicator => throw UnimplementedError();
@override
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
if (overscrollIndicator)
......@@ -256,6 +292,7 @@ class _WrappedScrollBehavior implements ScrollBehavior {
ScrollPhysics? physics,
TargetPlatform? platform,
Set<PointerDeviceKind>? dragDevices,
AndroidOverscrollIndicator? androidOverscrollIndicator
}) {
return delegate.copyWith(
scrollbars: scrollbars,
......
......@@ -1070,6 +1070,42 @@ void main() {
expect(scrollBehavior.getScrollPhysics(capturedContext).runtimeType, NeverScrollableScrollPhysics);
});
testWidgets('ScrollBehavior default android overscroll indicator', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
scrollBehavior: const MaterialScrollBehavior(),
home: ListView(
children: const <Widget>[
SizedBox(
height: 1000.0,
width: 1000.0,
child: Text('Test'),
)
]
)
));
expect(find.byType(StretchingOverscrollIndicator), findsNothing);
expect(find.byType(GlowingOverscrollIndicator), findsOneWidget);
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
testWidgets('ScrollBehavior stretch android overscroll indicator', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
scrollBehavior: const MaterialScrollBehavior(androidOverscrollIndicator: AndroidOverscrollIndicator.stretch),
home: ListView(
children: const <Widget>[
SizedBox(
height: 1000.0,
width: 1000.0,
child: Text('Test'),
)
]
)
));
expect(find.byType(StretchingOverscrollIndicator), findsOneWidget);
expect(find.byType(GlowingOverscrollIndicator), findsNothing);
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
testWidgets('When `useInheritedMediaQuery` is true an existing MediaQuery is used if one is available', (WidgetTester tester) async {
late BuildContext capturedContext;
final UniqueKey uniqueKey = UniqueKey();
......
// 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.
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
Widget buildTest(
Key box1Key,
Key box2Key,
Key box3Key,
ScrollController controller, {
Axis? axis,
}) {
return Directionality(
textDirection: TextDirection.ltr,
child: ScrollConfiguration(
behavior: const ScrollBehavior().copyWith(overscroll: false),
child: StretchingOverscrollIndicator(
axisDirection: axis == null ? AxisDirection.down : AxisDirection.right,
child: CustomScrollView(
scrollDirection: axis ?? Axis.vertical,
controller: controller,
slivers: <Widget>[
SliverToBoxAdapter(child: Container(
color: const Color(0xD0FF0000),
key: box1Key,
height: 250.0,
width: 300.0,
)),
SliverToBoxAdapter(child: Container(
color: const Color(0xFFFFFF00),
key: box2Key,
height: 250.0,
width: 300.0,
)),
SliverToBoxAdapter(child: Container(
color: const Color(0xFF6200EA),
key: box3Key,
height: 250.0,
width: 300.0,
)),
],
),
),
),
);
}
testWidgets('Stretch overscroll vertically', (WidgetTester tester) async {
final Key box1Key = UniqueKey();
final Key box2Key = UniqueKey();
final Key box3Key = UniqueKey();
final ScrollController controller = ScrollController();
await tester.pumpWidget(
buildTest(box1Key, box2Key, box3Key, controller),
);
expect(find.byType(StretchingOverscrollIndicator), findsOneWidget);
expect(find.byType(GlowingOverscrollIndicator), findsNothing);
final RenderBox box1 = tester.renderObject(find.byKey(box1Key));
final RenderBox box2 = tester.renderObject(find.byKey(box2Key));
final RenderBox box3 = tester.renderObject(find.byKey(box3Key));
expect(controller.offset, 0.0);
expect(box1.localToGlobal(Offset.zero), Offset.zero);
expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0));
expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0));
await expectLater(
find.byType(CustomScrollView),
matchesGoldenFile('overscroll_stretch.vertical.start.png'),
);
TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView)));
// Overscroll the start
await gesture.moveBy(const Offset(0.0, 200.0));
await tester.pumpAndSettle();
expect(box1.localToGlobal(Offset.zero), Offset.zero);
expect(box2.localToGlobal(Offset.zero).dy, greaterThan(255.0));
expect(box3.localToGlobal(Offset.zero).dy, greaterThan(510.0));
await expectLater(
find.byType(CustomScrollView),
matchesGoldenFile('overscroll_stretch.vertical.top.png'),
);
await gesture.up();
await tester.pumpAndSettle();
// Stretch released back to the start
expect(box1.localToGlobal(Offset.zero), Offset.zero);
expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0));
expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0));
// Jump to end of the list
controller.jumpTo(controller.position.maxScrollExtent);
expect(controller.offset, 150.0);
await expectLater(
find.byType(CustomScrollView),
matchesGoldenFile('overscroll_stretch.vertical.end.png'),
);
gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView)));
// Overscroll the end
await gesture.moveBy(const Offset(0.0, -200.0));
await tester.pumpAndSettle();
expect(box1.localToGlobal(Offset.zero).dy, lessThan(-165));
expect(box2.localToGlobal(Offset.zero).dy, lessThan(90.0));
expect(box3.localToGlobal(Offset.zero).dy, lessThan(350.0));
await expectLater(
find.byType(CustomScrollView),
matchesGoldenFile('overscroll_stretch.vertical.bottom.png'),
);
});
testWidgets('Stretch overscroll horizontally', (WidgetTester tester) async {
final Key box1Key = UniqueKey();
final Key box2Key = UniqueKey();
final Key box3Key = UniqueKey();
final ScrollController controller = ScrollController();
await tester.pumpWidget(
buildTest(box1Key, box2Key, box3Key, controller, axis: Axis.horizontal)
);
expect(find.byType(StretchingOverscrollIndicator), findsOneWidget);
expect(find.byType(GlowingOverscrollIndicator), findsNothing);
final RenderBox box1 = tester.renderObject(find.byKey(box1Key));
final RenderBox box2 = tester.renderObject(find.byKey(box2Key));
final RenderBox box3 = tester.renderObject(find.byKey(box3Key));
expect(controller.offset, 0.0);
expect(box1.localToGlobal(Offset.zero), Offset.zero);
expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0));
expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0));
await expectLater(
find.byType(CustomScrollView),
matchesGoldenFile('overscroll_stretch.horizontal.start.png'),
);
TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView)));
// Overscroll the start
await gesture.moveBy(const Offset(200.0, 0.0));
await tester.pumpAndSettle();
expect(box1.localToGlobal(Offset.zero), Offset.zero);
expect(box2.localToGlobal(Offset.zero).dx, greaterThan(305.0));
expect(box3.localToGlobal(Offset.zero).dx, greaterThan(610.0));
await expectLater(
find.byType(CustomScrollView),
matchesGoldenFile('overscroll_stretch.horizontal.left.png'),
);
await gesture.up();
await tester.pumpAndSettle();
// Stretch released back to the start
expect(box1.localToGlobal(Offset.zero), Offset.zero);
expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0));
expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0));
// Jump to end of the list
controller.jumpTo(controller.position.maxScrollExtent);
expect(controller.offset, 100.0);
await expectLater(
find.byType(CustomScrollView),
matchesGoldenFile('overscroll_stretch.horizontal.end.png'),
);
gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView)));
// Overscroll the end
await gesture.moveBy(const Offset(-200.0, 0.0));
await tester.pumpAndSettle();
expect(box1.localToGlobal(Offset.zero).dx, lessThan(-116.0));
expect(box2.localToGlobal(Offset.zero).dx, lessThan(190.0));
expect(box3.localToGlobal(Offset.zero).dx, lessThan(500.0));
await expectLater(
find.byType(CustomScrollView),
matchesGoldenFile('overscroll_stretch.horizontal.right.png'),
);
});
testWidgets('Disallow stretching overscroll', (WidgetTester tester) async {
final Key box1Key = UniqueKey();
final Key box2Key = UniqueKey();
final Key box3Key = UniqueKey();
final ScrollController controller = ScrollController();
double indicatorNotification =0;
await tester.pumpWidget(
NotificationListener<OverscrollIndicatorNotification>(
onNotification: (OverscrollIndicatorNotification notification) {
notification.disallowIndicator();
indicatorNotification += 1;
return false;
},
child: buildTest(box1Key, box2Key, box3Key, controller),
)
);
expect(find.byType(StretchingOverscrollIndicator), findsOneWidget);
expect(find.byType(GlowingOverscrollIndicator), findsNothing);
final RenderBox box1 = tester.renderObject(find.byKey(box1Key));
final RenderBox box2 = tester.renderObject(find.byKey(box2Key));
final RenderBox box3 = tester.renderObject(find.byKey(box3Key));
expect(indicatorNotification, 0.0);
expect(controller.offset, 0.0);
expect(box1.localToGlobal(Offset.zero), Offset.zero);
expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0));
expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0));
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView)));
// Overscroll the start, should not stretch
await gesture.moveBy(const Offset(0.0, 200.0));
await tester.pumpAndSettle();
expect(indicatorNotification, 1.0);
expect(box1.localToGlobal(Offset.zero), Offset.zero);
expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0));
expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0));
await gesture.up();
await tester.pumpAndSettle();
});
}
......@@ -80,4 +80,46 @@ void main() {
expect(metrics.extentAfter, equals(400.0));
expect(metrics.viewportDimension, equals(600.0));
});
testWidgets('ScrollBehavior default android overscroll indicator', (WidgetTester tester) async {
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: ScrollConfiguration(
behavior: const ScrollBehavior(),
child: ListView(
children: const <Widget>[
SizedBox(
height: 1000.0,
width: 1000.0,
child: Text('Test'),
)
]
)
),
));
expect(find.byType(StretchingOverscrollIndicator), findsNothing);
expect(find.byType(GlowingOverscrollIndicator), findsOneWidget);
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
testWidgets('ScrollBehavior stretch android overscroll indicator', (WidgetTester tester) async {
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: ScrollConfiguration(
behavior: const ScrollBehavior(androidOverscrollIndicator: AndroidOverscrollIndicator.stretch),
child: ListView(
children: const <Widget>[
SizedBox(
height: 1000.0,
width: 1000.0,
child: Text('Test'),
)
]
)
),
));
expect(find.byType(StretchingOverscrollIndicator), findsOneWidget);
expect(find.byType(GlowingOverscrollIndicator), findsNothing);
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
}
......@@ -181,4 +181,8 @@ void main() {
listWheelViewport = ListWheelViewport(clipToSize: true);
listWheelViewport = ListWheelViewport(clipToSize: false);
listWheelViewport.clipToSize;
// Changes made in https://github.com/flutter/flutter/pull/87839
final OverscrollIndicatorNotification notification = OverscrollIndicatorNotification(leading: true);
notification.disallowGlow();
}
......@@ -181,4 +181,8 @@ void main() {
listWheelViewport = ListWheelViewport(clipBehavior: Clip.hardEdge);
listWheelViewport = ListWheelViewport(clipBehavior: Clip.none);
listWheelViewport.clipBehavior;
// Changes made in https://github.com/flutter/flutter/pull/87839
final OverscrollIndicatorNotification notification = OverscrollIndicatorNotification(leading: true);
notification.disallowIndicator();
}
......@@ -394,4 +394,8 @@ void main() {
themeData = ThemeData.raw(fixTextFieldOutlineLabel: true);
themeData = themeData.copyWith(fixTextFieldOutlineLabel: true);
themeData.fixTextFieldOutlineLabel; // Removing field reference not supported.
// Changes made in https://github.com/flutter/flutter/pull/87839
final OverscrollIndicatorNotification notification = OverscrollIndicatorNotification(leading: true);
notification.disallowGlow();
}
......@@ -366,4 +366,8 @@ void main() {
themeData = ThemeData.raw();
themeData = themeData.copyWith();
themeData.fixTextFieldOutlineLabel; // Removing field reference not supported.
// Changes made in https://github.com/flutter/flutter/pull/87839
final OverscrollIndicatorNotification notification = OverscrollIndicatorNotification(leading: true);
notification.disallowIndicator();
}
......@@ -149,4 +149,8 @@ void main() {
listWheelViewport = ListWheelViewport(clipToSize: true);
listWheelViewport = ListWheelViewport(clipToSize: false);
listWheelViewport.clipToSize;
// Changes made in https://github.com/flutter/flutter/pull/87839
final OverscrollIndicatorNotification notification = OverscrollIndicatorNotification(leading: true);
notification.disallowGlow();
}
......@@ -149,4 +149,8 @@ void main() {
listWheelViewport = ListWheelViewport(clipBehavior: Clip.hardEdge);
listWheelViewport = ListWheelViewport(clipBehavior: Clip.none);
listWheelViewport.clipBehavior;
// Changes made in https://github.com/flutter/flutter/pull/87839
final OverscrollIndicatorNotification notification = OverscrollIndicatorNotification(leading: true);
notification.disallowIndicator();
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment