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
......
...@@ -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