-
Notifications
You must be signed in to change notification settings - Fork 30.5k
Expand file tree
/
Copy pathscrollable_helpers.dart
More file actions
518 lines (468 loc) · 19 KB
/
Copy pathscrollable_helpers.dart
File metadata and controls
518 lines (468 loc) · 19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
// 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.
/// @docImport 'package:flutter/material.dart';
///
/// @docImport 'overscroll_indicator.dart';
/// @docImport 'viewport.dart';
library;
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'actions.dart';
import 'basic.dart';
import 'framework.dart';
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 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.
@immutable
class ScrollableDetails {
/// Creates a set of details describing the [Scrollable].
const ScrollableDetails({
required this.direction,
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;
/// {@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 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? 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 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
/// to its edge.
///
/// The scroll velocity is controlled by the [velocityScalar]:
///
/// velocity = (distance of overscroll) * [velocityScalar].
class EdgeDraggingAutoScroller {
/// Creates a auto scroller that scrolls the [scrollable].
EdgeDraggingAutoScroller(
this.scrollable, {
this.onScrollViewScrolled,
required this.velocityScalar,
});
/// The [Scrollable] this auto scroller is scrolling.
final ScrollableState scrollable;
/// Called when a scroll view is scrolled.
///
/// The scroll view may be scrolled multiple times in a row until the drag
/// target no longer triggers the auto scroll. This callback will be called
/// in between each scroll.
final VoidCallback? onScrollViewScrolled;
/// {@template flutter.widgets.EdgeDraggingAutoScroller.velocityScalar}
/// The velocity scalar per pixel over scroll.
///
/// It represents how the velocity scale with the over scroll distance. The
/// auto-scroll velocity = (distance of overscroll) * velocityScalar.
/// {@endtemplate}
final double velocityScalar;
late Rect _dragTargetRelatedToScrollOrigin;
/// Whether the auto scroll is in progress.
bool get scrolling => _scrolling;
bool _scrolling = false;
double _offsetExtent(Offset offset, Axis scrollDirection) {
return switch (scrollDirection) {
Axis.horizontal => offset.dx,
Axis.vertical => offset.dy,
};
}
double _sizeExtent(Size size, Axis scrollDirection) {
return switch (scrollDirection) {
Axis.horizontal => size.width,
Axis.vertical => size.height,
};
}
AxisDirection get _axisDirection => scrollable.axisDirection;
Axis get _scrollDirection => axisDirectionToAxis(_axisDirection);
/// Starts the auto scroll if the [dragTarget] is close to the edge.
///
/// The scroll starts to scroll the [scrollable] if the target rect is close
/// to the edge of the [scrollable]; otherwise, it remains stationary.
///
/// If the scrollable is already scrolling, calling this method updates the
/// previous dragTarget to the new value and continues scrolling if necessary.
///
/// If the [scrollable]'s [ScrollableState.resolvedPhysics] refuses
/// user-driven scrolling (for example [NeverScrollableScrollPhysics]), no
/// auto scroll is started and any in-flight auto scroll is stopped.
void startAutoScrollIfNecessary(Rect dragTarget) {
final ScrollPhysics? physics = scrollable.resolvedPhysics;
if (physics != null && !physics.shouldAcceptUserOffset(scrollable.position)) {
stopAutoScroll();
return;
}
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
_dragTargetRelatedToScrollOrigin = dragTarget.translate(deltaToOrigin.dx, deltaToOrigin.dy);
if (_scrolling) {
// The change will be picked up in the next scroll.
return;
}
assert(!_scrolling);
_scroll();
}
/// Stop any ongoing auto scrolling.
void stopAutoScroll() {
_scrolling = false;
}
Future<void> _scroll() async {
final scrollRenderBox = scrollable.context.findRenderObject()! as RenderBox;
final Matrix4 transform = scrollRenderBox.getTransformTo(null);
final Rect globalRect = MatrixUtils.transformRect(
transform,
Rect.fromLTWH(0, 0, scrollRenderBox.size.width, scrollRenderBox.size.height),
);
final Rect transformedDragTarget = MatrixUtils.transformRect(
transform,
_dragTargetRelatedToScrollOrigin,
);
assert(
(globalRect.size.width + precisionErrorTolerance) >= transformedDragTarget.size.width &&
(globalRect.size.height + precisionErrorTolerance) >= transformedDragTarget.size.height,
'Drag target size is larger than scrollable size, which may cause bouncing',
);
_scrolling = true;
double? newOffset;
const overDragMax = 20.0;
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
final Offset viewportOrigin = globalRect.topLeft.translate(deltaToOrigin.dx, deltaToOrigin.dy);
final double viewportStart = _offsetExtent(viewportOrigin, _scrollDirection);
final double viewportEnd = viewportStart + _sizeExtent(globalRect.size, _scrollDirection);
final double proxyStart = _offsetExtent(
_dragTargetRelatedToScrollOrigin.topLeft,
_scrollDirection,
);
final double proxyEnd = _offsetExtent(
_dragTargetRelatedToScrollOrigin.bottomRight,
_scrollDirection,
);
switch (_axisDirection) {
case AxisDirection.up:
case AxisDirection.left:
if (proxyEnd > viewportEnd &&
scrollable.position.pixels > scrollable.position.minScrollExtent) {
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
newOffset = math.max(
scrollable.position.minScrollExtent,
scrollable.position.pixels - overDrag,
);
} else if (proxyStart < viewportStart &&
scrollable.position.pixels < scrollable.position.maxScrollExtent) {
final double overDrag = math.min(viewportStart - proxyStart, overDragMax);
newOffset = math.min(
scrollable.position.maxScrollExtent,
scrollable.position.pixels + overDrag,
);
}
case AxisDirection.right:
case AxisDirection.down:
if (proxyStart < viewportStart &&
scrollable.position.pixels > scrollable.position.minScrollExtent) {
final double overDrag = math.min(viewportStart - proxyStart, overDragMax);
newOffset = math.max(
scrollable.position.minScrollExtent,
scrollable.position.pixels - overDrag,
);
} else if (proxyEnd > viewportEnd &&
scrollable.position.pixels < scrollable.position.maxScrollExtent) {
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
newOffset = math.min(
scrollable.position.maxScrollExtent,
scrollable.position.pixels + overDrag,
);
}
}
if (newOffset == null || (newOffset - scrollable.position.pixels).abs() < 1.0) {
// Drag should not trigger scroll.
_scrolling = false;
return;
}
final duration = Duration(milliseconds: (1000 / velocityScalar).round());
await scrollable.position.animateTo(newOffset, duration: duration, curve: Curves.linear);
onScrollViewScrolled?.call();
if (_scrolling) {
await _scroll();
}
}
}
/// A typedef for a function that can calculate the offset for a type of scroll
/// increment given a [ScrollIncrementDetails].
///
/// This function is used as the type for [Scrollable.incrementCalculator],
/// which is called from a [ScrollAction].
typedef ScrollIncrementCalculator = double Function(ScrollIncrementDetails details);
/// Describes the type of scroll increment that will be performed by a
/// [ScrollAction] on a [Scrollable].
///
/// This is used to configure a [ScrollIncrementDetails] object to pass to a
/// [ScrollIncrementCalculator] function on a [Scrollable].
///
/// {@template flutter.widgets.ScrollIncrementType.intent}
/// This indicates the *intent* of the scroll, not necessarily the size. Not all
/// scrollable areas will have the concept of a "line" or "page", but they can
/// respond to the different standard key bindings that cause scrolling, which
/// are bound to keys that people use to indicate a "line" scroll (e.g.
/// control-arrowDown keys) or a "page" scroll (e.g. pageDown key). It is
/// recommended that at least the relative magnitudes of the scrolls match
/// expectations.
/// {@endtemplate}
enum ScrollIncrementType {
/// Indicates that the [ScrollIncrementCalculator] should return the scroll
/// distance it should move when the user requests to scroll by a "line".
///
/// The distance a "line" scrolls refers to what should happen when the key
/// binding for "scroll down/up by a line" is triggered. It's up to the
/// [ScrollIncrementCalculator] function to decide what that means for a
/// particular scrollable.
line,
/// Indicates that the [ScrollIncrementCalculator] should return the scroll
/// distance it should move when the user requests to scroll by a "page".
///
/// The distance a "page" scrolls refers to what should happen when the key
/// binding for "scroll down/up by a page" is triggered. It's up to the
/// [ScrollIncrementCalculator] function to decide what that means for a
/// particular scrollable.
page,
}
/// A details object that describes the type of scroll increment being requested
/// of a [ScrollIncrementCalculator] function, as well as the current metrics
/// for the scrollable.
class ScrollIncrementDetails {
/// A const constructor for a [ScrollIncrementDetails].
const ScrollIncrementDetails({required this.type, required this.metrics});
/// The type of scroll this is (e.g. line, page, etc.).
///
/// {@macro flutter.widgets.ScrollIncrementType.intent}
final ScrollIncrementType type;
/// The current metrics of the scrollable that is being scrolled.
final ScrollMetrics metrics;
}
/// An [Intent] that represents scrolling the nearest scrollable by an amount
/// appropriate for the [type] specified.
///
/// The actual amount of the scroll is determined by the
/// [Scrollable.incrementCalculator], or by its defaults if that is not
/// specified.
class ScrollIntent extends Intent {
/// Creates a const [ScrollIntent] that requests scrolling in the given
/// [direction], with the given [type].
const ScrollIntent({required this.direction, this.type = ScrollIncrementType.line});
/// The direction in which to scroll the scrollable containing the focused
/// widget.
final AxisDirection direction;
/// The type of scrolling that is intended.
final ScrollIncrementType type;
}
/// An [Action] that scrolls the relevant [Scrollable] by the amount configured
/// in the [ScrollIntent] given to it.
///
/// If a Scrollable cannot be found above the given [BuildContext], the
/// [PrimaryScrollController] will be considered for default handling of
/// [ScrollAction]s.
///
/// If [Scrollable.incrementCalculator] is null for the scrollable, the default
/// for a [ScrollIntent.type] set to [ScrollIncrementType.page] is 80% of the
/// size of the scroll window, and for [ScrollIncrementType.line], 50 logical
/// pixels.
class ScrollAction extends ContextAction<ScrollIntent> {
@override
bool isEnabled(ScrollIntent intent, [BuildContext? context]) {
if (context == null) {
return false;
}
if (Scrollable.maybeOf(context) != null) {
return true;
}
final ScrollController? primaryScrollController = PrimaryScrollController.maybeOf(context);
return (primaryScrollController != null) && primaryScrollController.hasClients;
}
/// Returns the scroll increment for a single scroll request, for use when
/// scrolling using a hardware keyboard.
///
/// Must not be called when the position is null, or when any of the position
/// metrics (pixels, viewportDimension, maxScrollExtent, minScrollExtent) are
/// null. The widget must have already been laid out so that the position
/// fields are valid.
static double _calculateScrollIncrement(
ScrollableState state, {
ScrollIncrementType type = ScrollIncrementType.line,
}) {
assert(state.position.hasPixels);
assert(
state.resolvedPhysics == null ||
state.resolvedPhysics!.shouldAcceptUserOffset(state.position),
);
if (state.widget.incrementCalculator != null) {
return state.widget.incrementCalculator!(
ScrollIncrementDetails(type: type, metrics: state.position),
);
}
return switch (type) {
ScrollIncrementType.line => 50.0,
ScrollIncrementType.page => 0.8 * state.position.viewportDimension,
};
}
/// Find out how much of an increment to move by, taking the different
/// directions into account.
static double getDirectionalIncrement(ScrollableState state, ScrollIntent intent) {
if (axisDirectionToAxis(intent.direction) == axisDirectionToAxis(state.axisDirection)) {
final double increment = _calculateScrollIncrement(state, type: intent.type);
return intent.direction == state.axisDirection ? increment : -increment;
}
return 0.0;
}
@override
void invoke(ScrollIntent intent, [BuildContext? context]) {
assert(context != null, 'Cannot scroll without a context.');
ScrollableState? state = Scrollable.maybeOf(context!);
if (state == null) {
final ScrollController primaryScrollController = PrimaryScrollController.of(context);
assert(() {
if (primaryScrollController.positions.length != 1) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'A ScrollAction was invoked with the PrimaryScrollController, but '
'more than one ScrollPosition is attached.',
),
ErrorDescription(
'Only one ScrollPosition can be manipulated by a ScrollAction at '
'a time.',
),
ErrorHint(
'The PrimaryScrollController can be inherited automatically by '
'descendant ScrollViews based on the TargetPlatform and scroll '
'direction. By default, the PrimaryScrollController is '
'automatically inherited on mobile platforms for vertical '
'ScrollViews. ScrollView.primary can also override this behavior.',
),
]);
}
return true;
}());
final BuildContext? notificationContext =
primaryScrollController.position.context.notificationContext;
if (notificationContext != null) {
state = Scrollable.maybeOf(notificationContext);
}
if (state == null) {
return;
}
}
assert(
state.position.hasPixels,
'Scrollable must be laid out before it can be scrolled via a ScrollAction',
);
// Don't do anything if the user isn't allowed to scroll.
if (state.resolvedPhysics != null &&
!state.resolvedPhysics!.shouldAcceptUserOffset(state.position)) {
return;
}
final double increment = getDirectionalIncrement(state, intent);
if (increment == 0.0) {
return;
}
state.position.moveTo(
state.position.pixels + increment,
duration: const Duration(milliseconds: 100),
curve: Curves.easeInOut,
);
}
}