mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Android 12 overscroll stretch effect (#87839)
This commit is contained in:
parent
8d89632fdd
commit
d8da091751
@ -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();
|
||||
|
@ -0,0 +1,223 @@
|
||||
// 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();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user