Unverified Commit 4cac07b7 authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Update ScrollableDetails for 2D scrolling (#122555)

Update ScrollableDetails for 2D scrolling
parent dfab19c1
......@@ -23,6 +23,29 @@
# * ListWheelScrollView: fix_list_wheel_scroll_view.yaml
version: 1
transforms:
# Changes made in https://github.com/flutter/flutter/pull/122555
- title: "Migrate to 'decorationClipBehavior'"
date: 2023-03-13
element:
uris: [ 'widgets.dart', 'material.dart', 'cupertino.dart' ]
field: 'clipBehavior'
inClass: 'ScrollableDetails'
changes:
- kind: 'rename'
newName: 'decorationClipBehavior'
# Changes made in https://github.com/flutter/flutter/pull/122555
- title: "Migrate to 'decorationClipBehavior'"
date: 2023-03-13
element:
uris: [ 'widgets.dart', 'material.dart', 'cupertino.dart' ]
constructor: ''
inClass: 'ScrollableDetails'
changes:
- kind: 'renameParameter'
oldName: 'clipBehavior'
newName: 'decorationClipBehavior'
# Changes made in https://github.com/flutter/flutter/pull/119647
- title: "Migrate to 'fromView'"
date: 2022-10-28
......
......@@ -460,6 +460,7 @@ class CupertinoScrollBehavior extends ScrollBehavior {
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
assert(details.controller != null);
return CupertinoScrollbar(
controller: details.controller,
child: child,
......
......@@ -825,6 +825,7 @@ class MaterialScrollBehavior extends ScrollBehavior {
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
assert(details.controller != null);
return Scrollbar(
controller: details.controller,
child: child,
......
......@@ -163,6 +163,7 @@ class ScrollBehavior {
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
assert(details.controller != null);
return RawScrollbar(
controller: details.controller,
child: child,
......
......@@ -99,18 +99,21 @@ class Scrollable extends StatefulWidget {
this.clipBehavior = Clip.hardEdge,
}) : assert(semanticChildCount == null || semanticChildCount >= 0);
/// {@template flutter.widgets.Scrollable.axisDirection}
/// The direction in which this widget scrolls.
///
/// For example, if the [axisDirection] is [AxisDirection.down], increasing
/// the scroll position will cause content below the bottom of the viewport to
/// become visible through the viewport. Similarly, if [axisDirection] is
/// [AxisDirection.right], increasing the scroll position will cause content
/// beyond the right edge of the viewport to become visible through the
/// viewport.
/// For example, if the [Scrollable.axisDirection] is [AxisDirection.down],
/// increasing the scroll position will cause content below the bottom of the
/// viewport to become visible through the viewport. Similarly, if the
/// axisDirection is [AxisDirection.right], increasing the scroll position
/// will cause content beyond the right edge of the viewport to become visible
/// through the viewport.
///
/// Defaults to [AxisDirection.down].
/// {@endtemplate}
final AxisDirection axisDirection;
/// {@template flutter.widgets.Scrollable.controller}
/// An object that can be used to control the position to which this widget is
/// scrolled.
///
......@@ -122,12 +125,17 @@ class Scrollable extends StatefulWidget {
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
///
/// If null, a [ScrollController] will be created internally by [Scrollable]
/// in order to create and manage the [ScrollPosition].
///
/// See also:
///
/// * [ensureVisible], which animates the scroll position to reveal a given
/// [BuildContext].
/// * [Scrollable.ensureVisible], which animates the scroll position to
/// reveal a given [BuildContext].
/// {@endtemplate}
final ScrollController? controller;
/// {@template flutter.widgets.Scrollable.physics}
/// How the widgets should respond to user input.
///
/// For example, determines how the widget continues to animate after the
......@@ -136,9 +144,9 @@ class Scrollable extends StatefulWidget {
/// Defaults to matching platform conventions via the physics provided from
/// the ambient [ScrollConfiguration].
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
/// If an explicit [ScrollBehavior] is provided to
/// [Scrollable.scrollBehavior], the [ScrollPhysics] provided by that behavior
/// will take precedence after [Scrollable.physics].
///
/// The physics can be changed dynamically, but new physics will only take
/// effect if the _class_ of the provided object changes. Merely constructing
......@@ -153,6 +161,7 @@ class Scrollable extends StatefulWidget {
/// * [AlwaysScrollableScrollPhysics], which can be used to indicate that the
/// scrollable should react to scroll requests (and possible overscroll)
/// even if the scrollable's contents fit without scrolling being necessary.
/// {@endtemplate}
final ScrollPhysics? physics;
/// Builds the viewport through which the scrollable content is displayed.
......@@ -902,7 +911,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
final ScrollableDetails details = ScrollableDetails(
direction: widget.axisDirection,
controller: _effectiveScrollController,
clipBehavior: widget.clipBehavior,
decorationClipBehavior: widget.clipBehavior,
);
result = _configuration.buildScrollbar(
......
......@@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'actions.dart';
......@@ -15,12 +16,16 @@ import 'primary_scroll_controller.dart';
import 'scroll_configuration.dart';
import 'scroll_controller.dart';
import 'scroll_metrics.dart';
import 'scroll_physics.dart';
import 'scrollable.dart';
export 'package:flutter/physics.dart' show Tolerance;
/// Describes the aspects of a Scrollable widget to inform inherited widgets
/// like [ScrollBehavior] for decorating.
// TODO(Piinks): Fix doc with 2DScrollable change.
// or enumerate the properties of combined
// Scrollables, such as [TwoDimensionalScrollable].
///
/// Decorations like [GlowingOverscrollIndicator]s and [Scrollbar]s require
/// information about the Scrollable in order to be initialized.
......@@ -30,28 +35,118 @@ class ScrollableDetails {
/// cannot be null.
const ScrollableDetails({
required this.direction,
required this.controller,
this.clipBehavior,
});
/// The direction in which this widget scrolls.
///
/// Cannot be null.
this.controller,
this.physics,
@Deprecated(
'Migrate to decorationClipBehavior. '
'This property was deprecated so that its application is clearer. This clip '
'applies to decorators, and does not directly clip a scroll view. '
'This feature was deprecated after v3.9.0-1.0.pre.'
)
Clip? clipBehavior,
Clip? decorationClipBehavior,
}) : decorationClipBehavior = clipBehavior ?? decorationClipBehavior;
/// A constructor specific to a [Scrollable] with an [Axis.vertical].
const ScrollableDetails.vertical({
bool reverse = false,
this.controller,
this.physics,
this.decorationClipBehavior,
}) : direction = reverse ? AxisDirection.up : AxisDirection.down;
/// A constructor specific to a [Scrollable] with an [Axis.horizontal].
const ScrollableDetails.horizontal({
bool reverse = false,
this.controller,
this.physics,
this.decorationClipBehavior,
}) : direction = reverse ? AxisDirection.left : AxisDirection.right;
/// {@macro flutter.widgets.Scrollable.axisDirection}
final AxisDirection direction;
/// A [ScrollController] that can be used to control the position of the
/// [Scrollable] widget.
///
/// This can be used by [ScrollBehavior] to apply a [Scrollbar] to the associated
/// [Scrollable].
final ScrollController controller;
/// {@macro flutter.widgets.Scrollable.controller}
final ScrollController? controller;
/// {@macro flutter.widgets.Scrollable.physics}
final ScrollPhysics? physics;
/// {@macro flutter.material.Material.clipBehavior}
///
/// This can be used by [MaterialScrollBehavior] to clip [StretchingOverscrollIndicator].
/// This can be used by [MaterialScrollBehavior] to clip a
/// [StretchingOverscrollIndicator].
///
/// This [Clip] does not affect the [Viewport.clipBehavior], but is rather
/// passed from the same value by [Scrollable] so that decorators like
/// [StretchingOverscrollIndicator] honor the same clip.
///
/// Defaults to null.
final Clip? clipBehavior;
final Clip? decorationClipBehavior;
/// Deprecated getter for [decorationClipBehavior].
@Deprecated(
'Migrate to decorationClipBehavior. '
'This property was deprecated so that its application is clearer. This clip '
'applies to decorators, and does not directly clip a scroll view. '
'This feature was deprecated after v3.9.0-1.0.pre.'
)
Clip? get clipBehavior => decorationClipBehavior;
/// Copy the current [ScrollableDetails] with the given values replacing the
/// current values.
ScrollableDetails copyWith({
AxisDirection? direction,
ScrollController? controller,
ScrollPhysics? physics,
Clip? decorationClipBehavior,
}) {
return ScrollableDetails(
direction: direction ?? this.direction,
controller: controller ?? this.controller,
physics: physics ?? this.physics,
decorationClipBehavior: decorationClipBehavior ?? this.decorationClipBehavior,
);
}
@override
String toString() {
final List<String> description = <String>[];
description.add('axisDirection: $direction');
void addIfNonNull(String prefix, Object? value) {
if (value != null) {
description.add(prefix + value.toString());
}
}
addIfNonNull('scroll controller: ', controller);
addIfNonNull('scroll physics: ', physics);
addIfNonNull('decorationClipBehavior: ', decorationClipBehavior);
return '${describeIdentity(this)}(${description.join(", ")})';
}
@override
int get hashCode => Object.hash(
direction,
controller,
physics,
decorationClipBehavior,
);
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is ScrollableDetails
&& other.direction == direction
&& other.controller == controller
&& other.physics == physics
&& other.decorationClipBehavior == decorationClipBehavior;
}
}
/// An auto scroller that scrolls the [scrollable] if a drag gesture drags close
......
......@@ -424,6 +424,49 @@ void main() {
debugBrightnessOverride = null;
});
testWidgets('Assert in buildScrollbar that controller != null when using it', (WidgetTester tester) async {
const ScrollBehavior defaultBehavior = CupertinoScrollBehavior();
late BuildContext capturedContext;
await tester.pumpWidget(ScrollConfiguration(
// Avoid the default ones here.
behavior: const CupertinoScrollBehavior().copyWith(scrollbars: false),
child: SingleChildScrollView(
child: Builder(
builder: (BuildContext context) {
capturedContext = context;
return Container(height: 1000.0);
},
),
),
));
const ScrollableDetails details = ScrollableDetails(direction: AxisDirection.down);
final Widget child = Container();
switch(defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
// Does not throw if we aren't using it.
defaultBehavior.buildScrollbar(capturedContext, child, details);
break;
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
expect(
() {
defaultBehavior.buildScrollbar(capturedContext, child, details);
},
throwsA(
isA<AssertionError>().having((AssertionError error) => error.toString(),
'description', contains('details.controller != null')),
),
);
break;
}
}, variant: TargetPlatformVariant.all());
}
class MockScrollBehavior extends ScrollBehavior {
......
......@@ -1445,6 +1445,92 @@ void main() {
);
expect(capturedContext.dependOnInheritedWidgetOfExactType<MediaQuery>()?.key, uniqueKey);
});
testWidgets('Assert in buildScrollbar that controller != null when using it (vertical)', (WidgetTester tester) async {
const ScrollBehavior defaultBehavior = MaterialScrollBehavior();
late BuildContext capturedContext;
await tester.pumpWidget(MaterialApp(
home: ScrollConfiguration(
// Avoid the default ones here.
behavior: const MaterialScrollBehavior().copyWith(scrollbars: false),
child: SingleChildScrollView(
child: Builder(
builder: (BuildContext context) {
capturedContext = context;
return Container(height: 1000.0);
},
),
),
),
));
const ScrollableDetails details = ScrollableDetails(
direction: AxisDirection.down,
);
final Widget child = Container();
switch(defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
// Does not throw if we aren't using it.
defaultBehavior.buildScrollbar(capturedContext, child, details);
break;
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
expect(
() {
defaultBehavior.buildScrollbar(capturedContext, child, details);
},
throwsA(
isA<AssertionError>().having((AssertionError error) => error.toString(),
'description', contains('details.controller != null')),
),
);
break;
}
}, variant: TargetPlatformVariant.all());
testWidgets('Assert in buildScrollbar that controller != null when using it (horizontal)', (WidgetTester tester) async {
const ScrollBehavior defaultBehavior = MaterialScrollBehavior();
late BuildContext capturedContext;
await tester.pumpWidget(MaterialApp(
home: ScrollConfiguration(
// Avoid the default ones here.
behavior: const MaterialScrollBehavior().copyWith(scrollbars: false),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Builder(
builder: (BuildContext context) {
capturedContext = context;
return Container(height: 1000.0);
},
),
),
),
));
const ScrollableDetails details = ScrollableDetails(
direction: AxisDirection.left,
);
final Widget child = Container();
switch(defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
// Does not throw if we aren't using it.
// Horizontal axis gets no scrollbars for all platforms.
defaultBehavior.buildScrollbar(capturedContext, child, details);
break;
}
}, variant: TargetPlatformVariant.all());
}
class MockScrollBehavior extends ScrollBehavior {
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -32,6 +33,49 @@ class TestScrollBehavior extends ScrollBehavior {
}
void main() {
testWidgets('Assert in buildScrollbar that controller != null when using it', (WidgetTester tester) async {
const ScrollBehavior defaultBehavior = ScrollBehavior();
late BuildContext capturedContext;
await tester.pumpWidget(ScrollConfiguration(
// Avoid the default ones here.
behavior: const ScrollBehavior().copyWith(scrollbars: false),
child: SingleChildScrollView(
child: Builder(
builder: (BuildContext context) {
capturedContext = context;
return Container(height: 1000.0);
},
),
),
));
const ScrollableDetails details = ScrollableDetails(direction: AxisDirection.down);
final Widget child = Container();
switch(defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
// Does not throw if we aren't using it.
defaultBehavior.buildScrollbar(capturedContext, child, details);
break;
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
expect(
() {
defaultBehavior.buildScrollbar(capturedContext, child, details);
},
throwsA(
isA<AssertionError>().having((AssertionError error) => error.toString(),
'description', contains('details.controller != null')),
),
);
break;
}
}, variant: TargetPlatformVariant.all());
// Regression test for https://github.com/flutter/flutter/issues/89681
testWidgets('_WrappedScrollBehavior shouldNotify test', (WidgetTester tester) async {
final ScrollBehavior behavior1 = const ScrollBehavior().copyWith();
......
......@@ -12,6 +12,80 @@ final LogicalKeyboardKey modifierKey = defaultTargetPlatform == TargetPlatform.m
: LogicalKeyboardKey.controlLeft;
void main() {
group('ScrollableDetails', (){
final ScrollController controller = ScrollController();
test('copyWith / == / hashCode', () {
final ScrollableDetails details = ScrollableDetails(
direction: AxisDirection.down,
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
decorationClipBehavior: Clip.hardEdge,
);
ScrollableDetails copiedDetails = details.copyWith();
expect(details, copiedDetails);
expect(details.hashCode, copiedDetails.hashCode);
copiedDetails = details.copyWith(
direction: AxisDirection.left,
physics: const ClampingScrollPhysics(),
decorationClipBehavior: Clip.none,
);
expect(
copiedDetails,
ScrollableDetails(
direction: AxisDirection.left,
controller: controller,
physics: const ClampingScrollPhysics(),
decorationClipBehavior: Clip.none,
),
);
});
test('toString', (){
const ScrollableDetails bareDetails = ScrollableDetails(
direction: AxisDirection.right,
);
expect(
bareDetails.toString(),
equalsIgnoringHashCodes(
'ScrollableDetails#00000(axisDirection: AxisDirection.right)'
),
);
final ScrollableDetails fullDetails = ScrollableDetails(
direction: AxisDirection.down,
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
decorationClipBehavior: Clip.hardEdge,
);
expect(
fullDetails.toString(),
equalsIgnoringHashCodes(
'ScrollableDetails#00000('
'axisDirection: AxisDirection.down, '
'scroll controller: ScrollController#00000(no clients), '
'scroll physics: AlwaysScrollableScrollPhysics, '
'decorationClipBehavior: Clip.hardEdge)'
),
);
});
test('deprecated clipBehavior is backwards compatible', (){
const ScrollableDetails deprecatedClip = ScrollableDetails(
direction: AxisDirection.right,
clipBehavior: Clip.hardEdge,
);
expect(deprecatedClip.clipBehavior, Clip.hardEdge);
expect(deprecatedClip.decorationClipBehavior, Clip.hardEdge);
const ScrollableDetails newClip = ScrollableDetails(
direction: AxisDirection.right,
decorationClipBehavior: Clip.hardEdge,
);
expect(newClip.clipBehavior, Clip.hardEdge);
expect(newClip.decorationClipBehavior, Clip.hardEdge);
});
});
testWidgets("Keyboard scrolling doesn't happen if scroll physics are set to NeverScrollableScrollPhysics", (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(
......
......@@ -238,4 +238,11 @@ void main() {
// Changes made in https://github.com/flutter/flutter/pull/114459
MediaQuery.boldTextOverride(context);
// Changes made in https://github.com/flutter/flutter/pull/122555
final ScrollableDetails details = ScrollableDetails(
direction: AxisDirection.down,
clipBehavior: Clip.none,
);
final Clip clip = details.clipBehavior;
}
......@@ -238,4 +238,11 @@ void main() {
// Changes made in https://github.com/flutter/flutter/pull/114459
MediaQuery.boldTextOf(context);
// Changes made in https://github.com/flutter/flutter/pull/122555
final ScrollableDetails details = ScrollableDetails(
direction: AxisDirection.down,
decorationClipBehavior: Clip.none,
);
final Clip clip = details.decorationClipBehavior;
}
......@@ -310,4 +310,11 @@ void main() {
// Changes made in https://github.com/flutter/flutter/pull/114459
MediaQuery.boldTextOverride(context);
// Changes made in https://github.com/flutter/flutter/pull/122555
final ScrollableDetails details = ScrollableDetails(
direction: AxisDirection.down,
clipBehavior: Clip.none,
);
final Clip clip = details.clipBehavior;
}
......@@ -306,4 +306,11 @@ void main() {
// Changes made in https://github.com/flutter/flutter/pull/114459
MediaQuery.boldTextOf(context);
// Changes made in https://github.com/flutter/flutter/pull/122555
final ScrollableDetails details = ScrollableDetails(
direction: AxisDirection.down,
decorationClipBehavior: Clip.none,
);
final Clip clip = details.decorationClipBehavior;
}
......@@ -160,4 +160,11 @@ void main() {
// Changes made in https://github.com/flutter/flutter/pull/114459
MediaQuery.boldTextOverride(context);
// Changes made in https://github.com/flutter/flutter/pull/122555
final ScrollableDetails details = ScrollableDetails(
direction: AxisDirection.down,
clipBehavior: Clip.none,
);
final Clip clip = details.clipBehavior;
}
......@@ -160,4 +160,11 @@ void main() {
// Changes made in https://github.com/flutter/flutter/pull/114459
MediaQuery.boldTextOf(context);
// Changes made in https://github.com/flutter/flutter/pull/122555
final ScrollableDetails details = ScrollableDetails(
direction: AxisDirection.down,
decorationClipBehavior: Clip.none,
);
final Clip clip = details.decorationClipBehavior;
}
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