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 @@ ...@@ -15,6 +15,17 @@
version: 1 version: 1
transforms: 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 # Changes made in https://github.com/flutter/flutter/pull/87281
- title: "Remove 'fixTextFieldOutlineLabel'" - title: "Remove 'fixTextFieldOutlineLabel'"
date: 2021-04-30 date: 2021-04-30
......
...@@ -703,7 +703,9 @@ class MaterialScrollBehavior extends ScrollBehavior { ...@@ -703,7 +703,9 @@ class MaterialScrollBehavior extends ScrollBehavior {
/// Creates a MaterialScrollBehavior that decorates [Scrollable]s with /// Creates a MaterialScrollBehavior that decorates [Scrollable]s with
/// [GlowingOverscrollIndicator]s and [Scrollbar]s based on the current /// [GlowingOverscrollIndicator]s and [Scrollbar]s based on the current
/// platform and provided [ScrollableDetails]. /// platform and provided [ScrollableDetails].
const MaterialScrollBehavior(); const MaterialScrollBehavior({
AndroidOverscrollIndicator? androidOverscrollIndicator,
}) : super(androidOverscrollIndicator: androidOverscrollIndicator);
@override @override
TargetPlatform getPlatform(BuildContext context) => Theme.of(context).platform; TargetPlatform getPlatform(BuildContext context) => Theme.of(context).platform;
...@@ -743,6 +745,16 @@ class MaterialScrollBehavior extends ScrollBehavior { ...@@ -743,6 +745,16 @@ class MaterialScrollBehavior extends ScrollBehavior {
case TargetPlatform.windows: case TargetPlatform.windows:
return child; return child;
case TargetPlatform.android: case TargetPlatform.android:
switch (androidOverscrollIndicator) {
case AndroidOverscrollIndicator.stretch:
return StretchingOverscrollIndicator(
axisDirection: details.direction,
child: child,
);
case AndroidOverscrollIndicator.glow:
continue glow;
}
glow:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
return GlowingOverscrollIndicator( return GlowingOverscrollIndicator(
axisDirection: details.direction, axisDirection: details.direction,
......
...@@ -185,7 +185,7 @@ class NotificationListener<T extends Notification> extends StatelessWidget { ...@@ -185,7 +185,7 @@ class NotificationListener<T extends Notification> extends StatelessWidget {
/// Called when a notification of the appropriate type arrives at this /// Called when a notification of the appropriate type arrives at this
/// location in the tree. /// 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. /// allow the notification to continue to be dispatched to further ancestors.
/// ///
/// The notification's [Notification.visitAncestor] method is called for each /// The notification's [Notification.visitAncestor] method is called for each
......
...@@ -6,7 +6,7 @@ import 'dart:async' show Timer; ...@@ -6,7 +6,7 @@ import 'dart:async' show Timer;
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/foundation.dart'; 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/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
...@@ -15,6 +15,7 @@ import 'framework.dart'; ...@@ -15,6 +15,7 @@ import 'framework.dart';
import 'notification_listener.dart'; import 'notification_listener.dart';
import 'scroll_notification.dart'; import 'scroll_notification.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
import 'transitions.dart';
/// A visual indication that a scroll view has overscrolled. /// A visual indication that a scroll view has overscrolled.
/// ///
...@@ -116,9 +117,10 @@ import 'ticker_provider.dart'; ...@@ -116,9 +117,10 @@ import 'ticker_provider.dart';
/// See also: /// See also:
/// ///
/// * [OverscrollIndicatorNotification], which can be used to manipulate the /// * [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 /// * [NotificationListener], to listen for the
/// [OverscrollIndicatorNotification] /// [OverscrollIndicatorNotification].
/// * [StretchingOverscrollIndicator], a Material Design overscroll indicator.
class GlowingOverscrollIndicator extends StatefulWidget { class GlowingOverscrollIndicator extends StatefulWidget {
/// Creates a visual indication that a scroll view has overscrolled. /// Creates a visual indication that a scroll view has overscrolled.
/// ///
...@@ -165,22 +167,28 @@ class GlowingOverscrollIndicator extends StatefulWidget { ...@@ -165,22 +167,28 @@ class GlowingOverscrollIndicator extends StatefulWidget {
/// viewport. /// viewport.
final bool showTrailing; final bool showTrailing;
/// {@template flutter.overscroll.axisDirection}
/// The direction of positive scroll offsets in the [Scrollable] whose /// The direction of positive scroll offsets in the [Scrollable] whose
/// overscrolls are to be visualized. /// overscrolls are to be visualized.
/// {@endtemplate}
final AxisDirection axisDirection; final AxisDirection axisDirection;
/// {@template flutter.overscroll.axis}
/// The axis along which scrolling occurs in the [Scrollable] whose /// The axis along which scrolling occurs in the [Scrollable] whose
/// overscrolls are to be visualized. /// overscrolls are to be visualized.
/// {@endtemplate}
Axis get axis => axisDirectionToAxis(axisDirection); Axis get axis => axisDirectionToAxis(axisDirection);
/// The color of the glow. The alpha channel is ignored. /// The color of the glow. The alpha channel is ignored.
final Color color; final Color color;
/// {@template flutter.overscroll.notificationPredicate}
/// A check that specifies whether a [ScrollNotification] should be /// A check that specifies whether a [ScrollNotification] should be
/// handled by this widget. /// handled by this widget.
/// ///
/// By default, checks whether `notification.depth == 0`. Set it to something /// 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; final ScrollNotificationPredicate notificationPredicate;
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
...@@ -271,7 +279,7 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator> ...@@ -271,7 +279,7 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
assert(false); assert(false);
} }
final bool isLeading = controller == _leadingController; final bool isLeading = controller == _leadingController;
if (_lastNotificationType != OverscrollNotification) { if (_lastNotificationType is! OverscrollNotification) {
final OverscrollIndicatorNotification confirmationNotification = OverscrollIndicatorNotification(leading: isLeading); final OverscrollIndicatorNotification confirmationNotification = OverscrollIndicatorNotification(leading: isLeading);
confirmationNotification.dispatch(context); confirmationNotification.dispatch(context);
_accepted[isLeading] = confirmationNotification._accepted; _accepted[isLeading] = confirmationNotification._accepted;
...@@ -637,18 +645,285 @@ class _GlowingOverscrollIndicatorPainter extends CustomPainter { ...@@ -637,18 +645,285 @@ class _GlowingOverscrollIndicatorPainter extends CustomPainter {
} }
} }
/// A notification that an [GlowingOverscrollIndicator] will start showing an /// A Material Design visual indication that a scroll view has overscrolled.
/// overscroll indication.
/// ///
/// To prevent the indicator from showing the indication, call [disallowGlow] on /// A [StretchingOverscrollIndicator] listens for [ScrollNotification]s in order
/// the notification. /// 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: /// 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 { class OverscrollIndicatorNotification extends Notification with ViewportNotificationMixin {
/// Creates a notification that an [GlowingOverscrollIndicator] will start /// Creates a notification that an [GlowingOverscrollIndicator] or a
/// showing an overscroll indication. /// [StretchingOverscrollIndicator] will start showing an overscroll indication.
/// ///
/// The [leading] argument must not be null. /// The [leading] argument must not be null.
OverscrollIndicatorNotification({ OverscrollIndicatorNotification({
...@@ -659,7 +934,7 @@ class OverscrollIndicatorNotification extends Notification with ViewportNotifica ...@@ -659,7 +934,7 @@ class OverscrollIndicatorNotification extends Notification with ViewportNotifica
/// view. /// view.
final bool leading; 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, /// A positive offset will move the glow away from its edge,
/// i.e. for a vertical, [leading] indicator, a [paintOffset] of 100.0 will /// i.e. for a vertical, [leading] indicator, a [paintOffset] of 100.0 will
...@@ -669,15 +944,27 @@ class OverscrollIndicatorNotification extends Notification with ViewportNotifica ...@@ -669,15 +944,27 @@ class OverscrollIndicatorNotification extends Notification with ViewportNotifica
/// ///
/// A negative [paintOffset] is generally not useful, since the glow will be /// A negative [paintOffset] is generally not useful, since the glow will be
/// clipped. /// clipped.
///
/// This has no effect on a [StretchingOverscrollIndicator].
double paintOffset = 0.0; double paintOffset = 0.0;
bool _accepted = true; 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() { void disallowGlow() {
_accepted = false; _accepted = false;
} }
/// Call this method if the overscroll indicator should be prevented.
void disallowIndicator() {
_accepted = false;
}
@override @override
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description); super.debugFillDescription(description);
......
...@@ -21,6 +21,21 @@ const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{ ...@@ -21,6 +21,21 @@ const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
PointerDeviceKind.invertedStylus, 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. /// Describes how [Scrollable] widgets should behave.
/// ///
/// {@template flutter.widgets.scrollBehavior} /// {@template flutter.widgets.scrollBehavior}
...@@ -46,7 +61,13 @@ const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{ ...@@ -46,7 +61,13 @@ const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
@immutable @immutable
class ScrollBehavior { class ScrollBehavior {
/// Creates a description of how [Scrollable] widgets should behave. /// 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 /// Creates a copy of this ScrollBehavior, making it possible to
/// easily toggle `scrollbar` and `overscrollIndicator` effects. /// easily toggle `scrollbar` and `overscrollIndicator` effects.
...@@ -106,6 +127,16 @@ class ScrollBehavior { ...@@ -106,6 +127,16 @@ class ScrollBehavior {
case TargetPlatform.windows: case TargetPlatform.windows:
return child; return child;
case TargetPlatform.android: case TargetPlatform.android:
switch (androidOverscrollIndicator) {
case AndroidOverscrollIndicator.stretch:
return StretchingOverscrollIndicator(
axisDirection: axisDirection,
child: child,
);
case AndroidOverscrollIndicator.glow:
continue glow;
}
glow:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
return GlowingOverscrollIndicator( return GlowingOverscrollIndicator(
axisDirection: axisDirection, axisDirection: axisDirection,
...@@ -230,6 +261,11 @@ class _WrappedScrollBehavior implements ScrollBehavior { ...@@ -230,6 +261,11 @@ class _WrappedScrollBehavior implements ScrollBehavior {
@override @override
Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices; Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices;
@override
AndroidOverscrollIndicator get androidOverscrollIndicator => delegate.androidOverscrollIndicator;
@override
AndroidOverscrollIndicator? get _androidOverscrollIndicator => throw UnimplementedError();
@override @override
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) { Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
if (overscrollIndicator) if (overscrollIndicator)
...@@ -256,6 +292,7 @@ class _WrappedScrollBehavior implements ScrollBehavior { ...@@ -256,6 +292,7 @@ class _WrappedScrollBehavior implements ScrollBehavior {
ScrollPhysics? physics, ScrollPhysics? physics,
TargetPlatform? platform, TargetPlatform? platform,
Set<PointerDeviceKind>? dragDevices, Set<PointerDeviceKind>? dragDevices,
AndroidOverscrollIndicator? androidOverscrollIndicator
}) { }) {
return delegate.copyWith( return delegate.copyWith(
scrollbars: scrollbars, scrollbars: scrollbars,
......
...@@ -1070,6 +1070,42 @@ void main() { ...@@ -1070,6 +1070,42 @@ void main() {
expect(scrollBehavior.getScrollPhysics(capturedContext).runtimeType, NeverScrollableScrollPhysics); 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 { testWidgets('When `useInheritedMediaQuery` is true an existing MediaQuery is used if one is available', (WidgetTester tester) async {
late BuildContext capturedContext; late BuildContext capturedContext;
final UniqueKey uniqueKey = UniqueKey(); 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() { ...@@ -80,4 +80,46 @@ void main() {
expect(metrics.extentAfter, equals(400.0)); expect(metrics.extentAfter, equals(400.0));
expect(metrics.viewportDimension, equals(600.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() { ...@@ -181,4 +181,8 @@ void main() {
listWheelViewport = ListWheelViewport(clipToSize: true); listWheelViewport = ListWheelViewport(clipToSize: true);
listWheelViewport = ListWheelViewport(clipToSize: false); listWheelViewport = ListWheelViewport(clipToSize: false);
listWheelViewport.clipToSize; 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() { ...@@ -181,4 +181,8 @@ void main() {
listWheelViewport = ListWheelViewport(clipBehavior: Clip.hardEdge); listWheelViewport = ListWheelViewport(clipBehavior: Clip.hardEdge);
listWheelViewport = ListWheelViewport(clipBehavior: Clip.none); listWheelViewport = ListWheelViewport(clipBehavior: Clip.none);
listWheelViewport.clipBehavior; 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() { ...@@ -394,4 +394,8 @@ void main() {
themeData = ThemeData.raw(fixTextFieldOutlineLabel: true); themeData = ThemeData.raw(fixTextFieldOutlineLabel: true);
themeData = themeData.copyWith(fixTextFieldOutlineLabel: true); themeData = themeData.copyWith(fixTextFieldOutlineLabel: true);
themeData.fixTextFieldOutlineLabel; // Removing field reference not supported. 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() { ...@@ -366,4 +366,8 @@ void main() {
themeData = ThemeData.raw(); themeData = ThemeData.raw();
themeData = themeData.copyWith(); themeData = themeData.copyWith();
themeData.fixTextFieldOutlineLabel; // Removing field reference not supported. 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() { ...@@ -149,4 +149,8 @@ void main() {
listWheelViewport = ListWheelViewport(clipToSize: true); listWheelViewport = ListWheelViewport(clipToSize: true);
listWheelViewport = ListWheelViewport(clipToSize: false); listWheelViewport = ListWheelViewport(clipToSize: false);
listWheelViewport.clipToSize; 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() { ...@@ -149,4 +149,8 @@ void main() {
listWheelViewport = ListWheelViewport(clipBehavior: Clip.hardEdge); listWheelViewport = ListWheelViewport(clipBehavior: Clip.hardEdge);
listWheelViewport = ListWheelViewport(clipBehavior: Clip.none); listWheelViewport = ListWheelViewport(clipBehavior: Clip.none);
listWheelViewport.clipBehavior; 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