Assert for valid ScrollController in Scrollbar gestures (#81278)
This commit is contained in:
parent
8603ed995d
commit
e9542ec646
@ -278,6 +278,9 @@ class CupertinoAlertDialog extends StatelessWidget {
|
|||||||
/// section when there are many actions.
|
/// section when there are many actions.
|
||||||
final ScrollController? scrollController;
|
final ScrollController? scrollController;
|
||||||
|
|
||||||
|
ScrollController get _effectiveScrollController =>
|
||||||
|
scrollController ?? ScrollController();
|
||||||
|
|
||||||
/// A scroll controller that can be used to control the scrolling of the
|
/// A scroll controller that can be used to control the scrolling of the
|
||||||
/// actions in the dialog.
|
/// actions in the dialog.
|
||||||
///
|
///
|
||||||
@ -289,6 +292,9 @@ class CupertinoAlertDialog extends StatelessWidget {
|
|||||||
/// section when it is long.
|
/// section when it is long.
|
||||||
final ScrollController? actionScrollController;
|
final ScrollController? actionScrollController;
|
||||||
|
|
||||||
|
ScrollController get _effectiveActionScrollController =>
|
||||||
|
actionScrollController ?? ScrollController();
|
||||||
|
|
||||||
/// {@macro flutter.material.dialog.insetAnimationDuration}
|
/// {@macro flutter.material.dialog.insetAnimationDuration}
|
||||||
final Duration insetAnimationDuration;
|
final Duration insetAnimationDuration;
|
||||||
|
|
||||||
@ -305,7 +311,7 @@ class CupertinoAlertDialog extends StatelessWidget {
|
|||||||
child: _CupertinoAlertContentSection(
|
child: _CupertinoAlertContentSection(
|
||||||
title: title,
|
title: title,
|
||||||
message: content,
|
message: content,
|
||||||
scrollController: scrollController,
|
scrollController: _effectiveScrollController,
|
||||||
titlePadding: EdgeInsets.only(
|
titlePadding: EdgeInsets.only(
|
||||||
left: _kDialogEdgePadding,
|
left: _kDialogEdgePadding,
|
||||||
right: _kDialogEdgePadding,
|
right: _kDialogEdgePadding,
|
||||||
@ -344,7 +350,7 @@ class CupertinoAlertDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
if (actions.isNotEmpty) {
|
if (actions.isNotEmpty) {
|
||||||
actionSection = _CupertinoAlertActionSection(
|
actionSection = _CupertinoAlertActionSection(
|
||||||
scrollController: actionScrollController,
|
scrollController: _effectiveActionScrollController,
|
||||||
isActionSheet: false,
|
isActionSheet: false,
|
||||||
children: actions,
|
children: actions,
|
||||||
);
|
);
|
||||||
@ -591,12 +597,18 @@ class CupertinoActionSheet extends StatelessWidget {
|
|||||||
/// short.
|
/// short.
|
||||||
final ScrollController? messageScrollController;
|
final ScrollController? messageScrollController;
|
||||||
|
|
||||||
|
ScrollController get _effectiveMessageScrollController =>
|
||||||
|
messageScrollController ?? ScrollController();
|
||||||
|
|
||||||
/// A scroll controller that can be used to control the scrolling of the
|
/// A scroll controller that can be used to control the scrolling of the
|
||||||
/// [actions] in the action sheet.
|
/// [actions] in the action sheet.
|
||||||
///
|
///
|
||||||
/// This attribute is typically not needed.
|
/// This attribute is typically not needed.
|
||||||
final ScrollController? actionScrollController;
|
final ScrollController? actionScrollController;
|
||||||
|
|
||||||
|
ScrollController get _effectiveActionScrollController =>
|
||||||
|
actionScrollController ?? ScrollController();
|
||||||
|
|
||||||
/// The optional cancel button that is grouped separately from the other
|
/// The optional cancel button that is grouped separately from the other
|
||||||
/// actions.
|
/// actions.
|
||||||
///
|
///
|
||||||
@ -609,7 +621,7 @@ class CupertinoActionSheet extends StatelessWidget {
|
|||||||
final Widget titleSection = _CupertinoAlertContentSection(
|
final Widget titleSection = _CupertinoAlertContentSection(
|
||||||
title: title,
|
title: title,
|
||||||
message: message,
|
message: message,
|
||||||
scrollController: messageScrollController,
|
scrollController: _effectiveMessageScrollController,
|
||||||
titlePadding: const EdgeInsets.only(
|
titlePadding: const EdgeInsets.only(
|
||||||
left: _kActionSheetContentHorizontalPadding,
|
left: _kActionSheetContentHorizontalPadding,
|
||||||
right: _kActionSheetContentHorizontalPadding,
|
right: _kActionSheetContentHorizontalPadding,
|
||||||
@ -650,7 +662,7 @@ class CupertinoActionSheet extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return _CupertinoAlertActionSection(
|
return _CupertinoAlertActionSection(
|
||||||
scrollController: actionScrollController,
|
scrollController: _effectiveActionScrollController,
|
||||||
hasCancelButton: cancelButton != null,
|
hasCancelButton: cancelButton != null,
|
||||||
isActionSheet: true,
|
isActionSheet: true,
|
||||||
children: actions!,
|
children: actions!,
|
||||||
@ -1501,6 +1513,7 @@ class _CupertinoAlertContentSection extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return CupertinoScrollbar(
|
return CupertinoScrollbar(
|
||||||
|
controller: scrollController,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -1565,6 +1578,7 @@ class _CupertinoAlertActionSectionState
|
|||||||
}
|
}
|
||||||
|
|
||||||
return CupertinoScrollbar(
|
return CupertinoScrollbar(
|
||||||
|
controller: widget.scrollController,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
controller: widget.scrollController,
|
controller: widget.scrollController,
|
||||||
child: _CupertinoDialogActionsRenderWidget(
|
child: _CupertinoDialogActionsRenderWidget(
|
||||||
|
@ -1026,19 +1026,43 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
|
|||||||
// A scroll event is required in order to paint the thumb.
|
// A scroll event is required in order to paint the thumb.
|
||||||
void _maybeTriggerScrollbar() {
|
void _maybeTriggerScrollbar() {
|
||||||
WidgetsBinding.instance!.addPostFrameCallback((Duration duration) {
|
WidgetsBinding.instance!.addPostFrameCallback((Duration duration) {
|
||||||
|
final ScrollController? scrollController = widget.controller ?? PrimaryScrollController.of(context);
|
||||||
if (showScrollbar) {
|
if (showScrollbar) {
|
||||||
_fadeoutTimer?.cancel();
|
_fadeoutTimer?.cancel();
|
||||||
// Wait one frame and cause an empty scroll event. This allows the
|
// Wait one frame and cause an empty scroll event. This allows the
|
||||||
// thumb to show immediately when isAlwaysShown is true. A scroll
|
// thumb to show immediately when isAlwaysShown is true. A scroll
|
||||||
// event is required in order to paint the thumb.
|
// event is required in order to paint the thumb.
|
||||||
|
_checkForValidScrollPosition();
|
||||||
|
scrollController!.position.didUpdateScrollPositionBy(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interactive scrollbars need to be properly configured.
|
||||||
|
// If there is no scroll controller, there will not be gestures at all.
|
||||||
|
if (scrollController != null && enableGestures) {
|
||||||
|
_checkForValidScrollPosition();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _checkForValidScrollPosition() {
|
||||||
final ScrollController? scrollController = widget.controller ?? PrimaryScrollController.of(context);
|
final ScrollController? scrollController = widget.controller ?? PrimaryScrollController.of(context);
|
||||||
final bool tryPrimary = widget.controller == null;
|
final bool tryPrimary = widget.controller == null;
|
||||||
final String controllerForError = tryPrimary
|
final String controllerForError = tryPrimary
|
||||||
? 'provided ScrollController'
|
? 'provided ScrollController'
|
||||||
: 'PrimaryScrollController';
|
: 'PrimaryScrollController';
|
||||||
|
|
||||||
|
String when = '';
|
||||||
|
if (showScrollbar) {
|
||||||
|
when = 'Scrollbar.isAlwaysShown is true';
|
||||||
|
} else if (enableGestures) {
|
||||||
|
when = 'the scrollbar is interactive';
|
||||||
|
} else {
|
||||||
|
when = 'using the Scrollbar';
|
||||||
|
}
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
scrollController != null,
|
scrollController != null,
|
||||||
'A ScrollController is required when Scrollbar.isAlwaysShown is true. '
|
'A ScrollController is required when $when. '
|
||||||
'${tryPrimary ? 'The Scrollbar was not provided a ScrollController, '
|
'${tryPrimary ? 'The Scrollbar was not provided a ScrollController, '
|
||||||
'and attempted to use the PrimaryScrollController, but none was found.' :''}',
|
'and attempted to use the PrimaryScrollController, but none was found.' :''}',
|
||||||
);
|
);
|
||||||
@ -1083,7 +1107,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
|
|||||||
'The Scrollbar requires a single ScrollPosition in order to be painted.',
|
'The Scrollbar requires a single ScrollPosition in order to be painted.',
|
||||||
),
|
),
|
||||||
ErrorHint(
|
ErrorHint(
|
||||||
'When Scrollbar.isAlwaysShown is true, the associated Scrollable '
|
'When $when, the associated Scrollable '
|
||||||
'widgets must have unique ScrollControllers. '
|
'widgets must have unique ScrollControllers. '
|
||||||
'${tryPrimary
|
'${tryPrimary
|
||||||
? 'The PrimaryScrollController is used by default for '
|
? 'The PrimaryScrollController is used by default for '
|
||||||
@ -1099,9 +1123,6 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
scrollController!.position.didUpdateScrollPositionBy(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This method is responsible for configuring the [scrollbarPainter]
|
/// This method is responsible for configuring the [scrollbarPainter]
|
||||||
@ -1191,6 +1212,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
|
|||||||
@protected
|
@protected
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
void handleThumbPress() {
|
void handleThumbPress() {
|
||||||
|
_checkForValidScrollPosition();
|
||||||
if (getScrollbarDirection() == null) {
|
if (getScrollbarDirection() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1203,6 +1225,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
|
|||||||
@protected
|
@protected
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
void handleThumbPressStart(Offset localPosition) {
|
void handleThumbPressStart(Offset localPosition) {
|
||||||
|
_checkForValidScrollPosition();
|
||||||
_currentController = widget.controller ?? PrimaryScrollController.of(context);
|
_currentController = widget.controller ?? PrimaryScrollController.of(context);
|
||||||
final Axis? direction = getScrollbarDirection();
|
final Axis? direction = getScrollbarDirection();
|
||||||
if (direction == null) {
|
if (direction == null) {
|
||||||
@ -1219,6 +1242,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
|
|||||||
@protected
|
@protected
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
void handleThumbPressUpdate(Offset localPosition) {
|
void handleThumbPressUpdate(Offset localPosition) {
|
||||||
|
_checkForValidScrollPosition();
|
||||||
final Axis? direction = getScrollbarDirection();
|
final Axis? direction = getScrollbarDirection();
|
||||||
if (direction == null) {
|
if (direction == null) {
|
||||||
return;
|
return;
|
||||||
@ -1231,9 +1255,11 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
|
|||||||
@protected
|
@protected
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
void handleThumbPressEnd(Offset localPosition, Velocity velocity) {
|
void handleThumbPressEnd(Offset localPosition, Velocity velocity) {
|
||||||
|
_checkForValidScrollPosition();
|
||||||
final Axis? direction = getScrollbarDirection();
|
final Axis? direction = getScrollbarDirection();
|
||||||
if (direction == null)
|
if (direction == null) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
_maybeStartFadeoutTimer();
|
_maybeStartFadeoutTimer();
|
||||||
_dragScrollbarAxisOffset = null;
|
_dragScrollbarAxisOffset = null;
|
||||||
_currentController = null;
|
_currentController = null;
|
||||||
@ -1241,6 +1267,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
|
|||||||
|
|
||||||
void _handleTrackTapDown(TapDownDetails details) {
|
void _handleTrackTapDown(TapDownDetails details) {
|
||||||
// The Scrollbar should page towards the position of the tap on the track.
|
// The Scrollbar should page towards the position of the tap on the track.
|
||||||
|
_checkForValidScrollPosition();
|
||||||
_currentController = widget.controller ?? PrimaryScrollController.of(context);
|
_currentController = widget.controller ?? PrimaryScrollController.of(context);
|
||||||
|
|
||||||
double scrollIncrement;
|
double scrollIncrement;
|
||||||
|
@ -389,6 +389,36 @@ void main() {
|
|||||||
expect(tester.getSize(find.byType(CupertinoActionSheet)).height, screenHeight);
|
expect(tester.getSize(find.byType(CupertinoActionSheet)).height, screenHeight);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('CupertinoActionSheet scrollbars controllers should be different', (WidgetTester tester) async {
|
||||||
|
// https://github.com/flutter/flutter/pull/81278
|
||||||
|
await tester.pumpWidget(
|
||||||
|
createAppWithButtonThatLaunchesActionSheet(
|
||||||
|
CupertinoActionSheet(
|
||||||
|
title: const Text('The title'),
|
||||||
|
message: Text('Very long content' * 200),
|
||||||
|
actions: <Widget>[
|
||||||
|
CupertinoActionSheetAction(
|
||||||
|
child: const Text('One'),
|
||||||
|
onPressed: () { },
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final List<CupertinoScrollbar> scrollbars =
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(CupertinoActionSheet),
|
||||||
|
matching: find.byType(CupertinoScrollbar),
|
||||||
|
).evaluate().map((Element e) => e.widget as CupertinoScrollbar).toList();
|
||||||
|
|
||||||
|
expect(scrollbars.length, 2);
|
||||||
|
expect(scrollbars[0].controller != scrollbars[1].controller, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Tap on button calls onPressed', (WidgetTester tester) async {
|
testWidgets('Tap on button calls onPressed', (WidgetTester tester) async {
|
||||||
bool wasPressed = false;
|
bool wasPressed = false;
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
|
@ -1286,6 +1286,32 @@ void main() {
|
|||||||
// one for the content.
|
// one for the content.
|
||||||
expect(find.byType(CupertinoScrollbar), findsNWidgets(2));
|
expect(find.byType(CupertinoScrollbar), findsNWidgets(2));
|
||||||
}, variant: TargetPlatformVariant.all());
|
}, variant: TargetPlatformVariant.all());
|
||||||
|
|
||||||
|
testWidgets('CupertinoAlertDialog scrollbars controllers should be different', (WidgetTester tester) async {
|
||||||
|
// https://github.com/flutter/flutter/pull/81278
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
home: MediaQuery(
|
||||||
|
data: MediaQueryData(viewInsets: EdgeInsets.zero),
|
||||||
|
child: CupertinoAlertDialog(
|
||||||
|
actions: <Widget>[
|
||||||
|
CupertinoDialogAction(child: Text('OK')),
|
||||||
|
],
|
||||||
|
content: Placeholder(fallbackHeight: 200.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<CupertinoScrollbar> scrollbars =
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(CupertinoAlertDialog),
|
||||||
|
matching: find.byType(CupertinoScrollbar),
|
||||||
|
).evaluate().map((Element e) => e.widget as CupertinoScrollbar).toList();
|
||||||
|
|
||||||
|
expect(scrollbars.length, 2);
|
||||||
|
expect(scrollbars[0].controller != scrollbars[1].controller, isTrue);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderBox findActionButtonRenderBoxByTitle(WidgetTester tester, String title) {
|
RenderBox findActionButtonRenderBoxByTitle(WidgetTester tester, String title) {
|
||||||
|
@ -934,6 +934,41 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Interactive scrollbars should have a valid scroll controller', (WidgetTester tester) async {
|
||||||
|
final ScrollController primaryScrollController = ScrollController();
|
||||||
|
final ScrollController scrollController = ScrollController();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: MediaQuery(
|
||||||
|
data: const MediaQueryData(),
|
||||||
|
child: PrimaryScrollController(
|
||||||
|
controller: primaryScrollController,
|
||||||
|
child: CupertinoScrollbar(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: scrollController,
|
||||||
|
child: const SizedBox(
|
||||||
|
height: 1000.0,
|
||||||
|
width: 1000.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final dynamic exception = tester.takeException();
|
||||||
|
expect(exception, isAssertionError);
|
||||||
|
expect(
|
||||||
|
(exception as AssertionError).message,
|
||||||
|
contains("The Scrollbar's ScrollController has no ScrollPosition attached."),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Simultaneous dragging and pointer scrolling does not cause a crash', (WidgetTester tester) async {
|
testWidgets('Simultaneous dragging and pointer scrolling does not cause a crash', (WidgetTester tester) async {
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/70105
|
// Regression test for https://github.com/flutter/flutter/issues/70105
|
||||||
final ScrollController scrollController = ScrollController();
|
final ScrollController scrollController = ScrollController();
|
||||||
|
@ -1051,8 +1051,9 @@ void main() {
|
|||||||
),
|
),
|
||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
child: const SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: SizedBox(width: 4000.0, height: 4000.0),
|
controller: controller,
|
||||||
|
child: const SizedBox(width: 4000.0, height: 4000.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1171,6 +1171,41 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Interactive scrollbars should have a valid scroll controller', (WidgetTester tester) async {
|
||||||
|
final ScrollController primaryScrollController = ScrollController();
|
||||||
|
final ScrollController scrollController = ScrollController();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: MediaQuery(
|
||||||
|
data: const MediaQueryData(),
|
||||||
|
child: PrimaryScrollController(
|
||||||
|
controller: primaryScrollController,
|
||||||
|
child: RawScrollbar(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: scrollController,
|
||||||
|
child: const SizedBox(
|
||||||
|
height: 1000.0,
|
||||||
|
width: 1000.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final dynamic exception = tester.takeException();
|
||||||
|
expect(exception, isAssertionError);
|
||||||
|
expect(
|
||||||
|
(exception as AssertionError).message,
|
||||||
|
contains("The Scrollbar's ScrollController has no ScrollPosition attached."),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Simultaneous dragging and pointer scrolling does not cause a crash', (WidgetTester tester) async {
|
testWidgets('Simultaneous dragging and pointer scrolling does not cause a crash', (WidgetTester tester) async {
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/70105
|
// Regression test for https://github.com/flutter/flutter/issues/70105
|
||||||
final ScrollController scrollController = ScrollController();
|
final ScrollController scrollController = ScrollController();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user