Created
December 2, 2019 10:12
-
-
Save mikepyts/195263ee630b1add69d2f53af3fb9e0a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'dart:math' as math; | |
import 'dart:ui'; | |
import 'package:flutter/gestures.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart'; | |
import 'package:flutter/scheduler.dart'; | |
import 'package:flutter/painting.dart'; | |
export 'package:flutter/physics.dart' show Tolerance; | |
/// Signature used by [Scrollable] to build the viewport through which the | |
/// scrollable content is displayed. | |
typedef ViewportBuilder = Widget Function(BuildContext context, ViewportOffset position); | |
/// A widget that scrolls. | |
/// | |
/// [Scrollable] implements the interaction model for a scrollable widget, | |
/// including gesture recognition, but does not have an opinion about how the | |
/// viewport, which actually displays the children, is constructed. | |
/// | |
/// It's rare to construct a [Scrollable] directly. Instead, consider [ListView] | |
/// or [GridView], which combine scrolling, viewporting, and a layout model. To | |
/// combine layout models (or to use a custom layout mode), consider using | |
/// [CustomScrollView]. | |
/// | |
/// The static [Scrollable.of] and [Scrollable.ensureVisible] functions are | |
/// often used to interact with the [Scrollable] widget inside a [ListView] or | |
/// a [GridView]. | |
/// | |
/// To further customize scrolling behavior with a [Scrollable]: | |
/// | |
/// 1. You can provide a [viewportBuilder] to customize the child model. For | |
/// example, [SingleChildScrollView] uses a viewport that displays a single | |
/// box child whereas [CustomScrollView] uses a [Viewport] or a | |
/// [ShrinkWrappingViewport], both of which display a list of slivers. | |
/// | |
/// 2. You can provide a custom [ScrollController] that creates a custom | |
/// [ScrollPosition] subclass. For example, [PageView] uses a | |
/// [PageController], which creates a page-oriented scroll position subclass | |
/// that keeps the same page visible when the [Scrollable] resizes. | |
/// | |
/// See also: | |
/// | |
/// * [ListView], which is a commonly used [ScrollView] that displays a | |
/// scrolling, linear list of child widgets. | |
/// * [PageView], which is a scrolling list of child widgets that are each the | |
/// size of the viewport. | |
/// * [GridView], which is a [ScrollView] that displays a scrolling, 2D array | |
/// of child widgets. | |
/// * [CustomScrollView], which is a [ScrollView] that creates custom scroll | |
/// effects using slivers. | |
/// * [SingleChildScrollView], which is a scrollable widget that has a single | |
/// child. | |
/// * [ScrollNotification] and [NotificationListener], which can be used to watch | |
/// the scroll position without using a [ScrollController]. | |
class Scrollable extends StatefulWidget { | |
/// Creates a widget that scrolls. | |
/// | |
/// The [axisDirection] and [viewportBuilder] arguments must not be null. | |
const Scrollable({ | |
Key key, | |
this.axisDirection = AxisDirection.down, | |
this.controller, | |
this.physics, | |
@required this.viewportBuilder, | |
this.excludeFromSemantics = false, | |
this.semanticChildCount, | |
this.dragStartBehavior = DragStartBehavior.start, | |
}) : assert(axisDirection != null), | |
assert(dragStartBehavior != null), | |
assert(viewportBuilder != null), | |
assert(excludeFromSemantics != null), | |
assert(semanticChildCount == null || semanticChildCount >= 0), | |
super (key: key); | |
/// 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. | |
/// | |
/// Defaults to [AxisDirection.down]. | |
final AxisDirection axisDirection; | |
/// An object that can be used to control the position to which this widget is | |
/// scrolled. | |
/// | |
/// A [ScrollController] serves several purposes. It can be used to control | |
/// the initial scroll position (see [ScrollController.initialScrollOffset]). | |
/// It can be used to control whether the scroll view should automatically | |
/// save and restore its scroll position in the [PageStorage] (see | |
/// [ScrollController.keepScrollOffset]). It can be used to read the current | |
/// scroll position (see [ScrollController.offset]), or change it (see | |
/// [ScrollController.animateTo]). | |
/// | |
/// See also: | |
/// | |
/// * [ensureVisible], which animates the scroll position to reveal a given | |
/// [BuildContext]. | |
final ScrollController controller; | |
/// How the widgets should respond to user input. | |
/// | |
/// For example, determines how the widget continues to animate after the | |
/// user stops dragging the scroll view. | |
/// | |
/// Defaults to matching platform conventions via the physics provided from | |
/// the ambient [ScrollConfiguration]. | |
/// | |
/// The physics can be changed dynamically, but new physics will only take | |
/// effect if the _class_ of the provided object changes. Merely constructing | |
/// a new instance with a different configuration is insufficient to cause the | |
/// physics to be reapplied. (This is because the final object used is | |
/// generated dynamically, which can be relatively expensive, and it would be | |
/// inefficient to speculatively create this object each frame to see if the | |
/// physics should be updated.) | |
/// | |
/// See also: | |
/// | |
/// * [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. | |
final ScrollPhysics physics; | |
/// Builds the viewport through which the scrollable content is displayed. | |
/// | |
/// A typical viewport uses the given [ViewportOffset] to determine which part | |
/// of its content is actually visible through the viewport. | |
/// | |
/// See also: | |
/// | |
/// * [Viewport], which is a viewport that displays a list of slivers. | |
/// * [ShrinkWrappingViewport], which is a viewport that displays a list of | |
/// slivers and sizes itself based on the size of the slivers. | |
final ViewportBuilder viewportBuilder; | |
/// Whether the scroll actions introduced by this [Scrollable] are exposed | |
/// in the semantics tree. | |
/// | |
/// Text fields with an overflow are usually scrollable to make sure that the | |
/// user can get to the beginning/end of the entered text. However, these | |
/// scrolling actions are generally not exposed to the semantics layer. | |
/// | |
/// See also: | |
/// | |
/// * [GestureDetector.excludeFromSemantics], which is used to accomplish the | |
/// exclusion. | |
final bool excludeFromSemantics; | |
/// The number of children that will contribute semantic information. | |
/// | |
/// The value will be null if the number of children is unknown or unbounded. | |
/// | |
/// Some subtypes of [ScrollView] can infer this value automatically. For | |
/// example [ListView] will use the number of widgets in the child list, | |
/// while the [new ListView.separated] constructor will use half that amount. | |
/// | |
/// For [CustomScrollView] and other types which do not receive a builder | |
/// or list of widgets, the child count must be explicitly provided. | |
/// | |
/// See also: | |
/// | |
/// * [CustomScrollView], for an explanation of scroll semantics. | |
/// * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property. | |
final int semanticChildCount; | |
// TODO(jslavitz): Set the DragStartBehavior default to be start across all widgets. | |
/// {@template flutter.widgets.scrollable.dragStartBehavior} | |
/// Determines the way that drag start behavior is handled. | |
/// | |
/// If set to [DragStartBehavior.start], scrolling drag behavior will | |
/// begin upon the detection of a drag gesture. If set to | |
/// [DragStartBehavior.down] it will begin when a down event is first detected. | |
/// | |
/// In general, setting this to [DragStartBehavior.start] will make drag | |
/// animation smoother and setting it to [DragStartBehavior.down] will make | |
/// drag behavior feel slightly more reactive. | |
/// | |
/// By default, the drag start behavior is [DragStartBehavior.start]. | |
/// | |
/// See also: | |
/// | |
/// * [DragGestureRecognizer.dragStartBehavior], which gives an example for | |
/// the different behaviors. | |
/// | |
/// {@endtemplate} | |
final DragStartBehavior dragStartBehavior; | |
/// The axis along which the scroll view scrolls. | |
/// | |
/// Determined by the [axisDirection]. | |
Axis get axis => axisDirectionToAxis(axisDirection); | |
@override | |
ScrollableState createState() => ScrollableState(); | |
@override | |
void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
super.debugFillProperties(properties); | |
properties.add(EnumProperty<AxisDirection>('axisDirection', axisDirection)); | |
properties.add(DiagnosticsProperty<ScrollPhysics>('physics', physics)); | |
} | |
/// The state from the closest instance of this class that encloses the given context. | |
/// | |
/// Typical usage is as follows: | |
/// | |
/// ```dart | |
/// ScrollableState scrollable = Scrollable.of(context); | |
/// ``` | |
static ScrollableState of(BuildContext context) { | |
final _ScrollableScope widget = context.dependOnInheritedWidgetOfExactType<_ScrollableScope>(); | |
return widget?.scrollable; | |
} | |
/// Scrolls the scrollables that enclose the given context so as to make the | |
/// given context visible. | |
static Future<void> ensureVisible( | |
BuildContext context, { | |
double alignment = 0.0, | |
Duration duration = Duration.zero, | |
Curve curve = Curves.ease, | |
ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit, | |
}) { | |
final List<Future<void>> futures = <Future<void>>[]; | |
ScrollableState scrollable = Scrollable.of(context); | |
while (scrollable != null) { | |
futures.add(scrollable.position.ensureVisible( | |
context.findRenderObject(), | |
alignment: alignment, | |
duration: duration, | |
curve: curve, | |
alignmentPolicy: alignmentPolicy, | |
)); | |
context = scrollable.context; | |
scrollable = Scrollable.of(context); | |
} | |
if (futures.isEmpty || duration == Duration.zero) { | |
return Future<void>.value(); | |
} | |
if (futures.length == 1) { | |
return futures.single; | |
} | |
return Future.wait<void>(futures).then<void>((List<void> _) => null); | |
} | |
} | |
// Enable Scrollable.of() to work as if ScrollableState was an inherited widget. | |
// ScrollableState.build() always rebuilds its _ScrollableScope. | |
class _ScrollableScope extends InheritedWidget { | |
const _ScrollableScope({ | |
Key key, | |
@required this.scrollable, | |
@required this.position, | |
@required Widget child, | |
}) : assert(scrollable != null), | |
assert(child != null), | |
super(key: key, child: child); | |
final ScrollableState scrollable; | |
final ScrollPosition position; | |
@override | |
bool updateShouldNotify(_ScrollableScope old) { | |
return position != old.position; | |
} | |
} | |
/// State object for a [Scrollable] widget. | |
/// | |
/// To manipulate a [Scrollable] widget's scroll position, use the object | |
/// obtained from the [position] property. | |
/// | |
/// To be informed of when a [Scrollable] widget is scrolling, use a | |
/// [NotificationListener] to listen for [ScrollNotification] notifications. | |
/// | |
/// This class is not intended to be subclassed. To specialize the behavior of a | |
/// [Scrollable], provide it with a [ScrollPhysics]. | |
class ScrollableState extends State<Scrollable> with TickerProviderStateMixin | |
implements ScrollContext { | |
/// The manager for this [Scrollable] widget's viewport position. | |
/// | |
/// To control what kind of [ScrollPosition] is created for a [Scrollable], | |
/// provide it with custom [ScrollController] that creates the appropriate | |
/// [ScrollPosition] in its [ScrollController.createScrollPosition] method. | |
ScrollPosition get position => _position; | |
ScrollPosition _position; | |
@override | |
AxisDirection get axisDirection => widget.axisDirection; | |
ScrollBehavior _configuration; | |
ScrollPhysics _physics; | |
// Only call this from places that will definitely trigger a rebuild. | |
void _updatePosition() { | |
_configuration = ScrollConfiguration.of(context); | |
_physics = _configuration.getScrollPhysics(context); | |
if (widget.physics != null) { | |
_physics = widget.physics.applyTo(_physics); | |
} | |
final ScrollController controller = widget.controller; | |
final ScrollPosition oldPosition = position; | |
if (oldPosition != null) { | |
controller?.detach(oldPosition); | |
// It's important that we not dispose the old position until after the | |
// viewport has had a chance to unregister its listeners from the old | |
// position. So, schedule a microtask to do it. | |
scheduleMicrotask(oldPosition.dispose); | |
} | |
_position = controller?.createScrollPosition(_physics, this, oldPosition) | |
?? ScrollPositionWithSingleContext(physics: _physics, context: this, oldPosition: oldPosition); | |
assert(position != null); | |
controller?.attach(position); | |
} | |
@override | |
void didChangeDependencies() { | |
super.didChangeDependencies(); | |
_updatePosition(); | |
} | |
bool _shouldUpdatePosition(Scrollable oldWidget) { | |
ScrollPhysics newPhysics = widget.physics; | |
ScrollPhysics oldPhysics = oldWidget.physics; | |
do { | |
if (newPhysics?.runtimeType != oldPhysics?.runtimeType) { | |
return true; | |
} | |
newPhysics = newPhysics?.parent; | |
oldPhysics = oldPhysics?.parent; | |
} while (newPhysics != null || oldPhysics != null); | |
return widget.controller?.runtimeType != oldWidget.controller?.runtimeType; | |
} | |
@override | |
void didUpdateWidget(Scrollable oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (widget.controller != oldWidget.controller) { | |
oldWidget.controller?.detach(position); | |
widget.controller?.attach(position); | |
} | |
if (_shouldUpdatePosition(oldWidget)) { | |
_updatePosition(); | |
} | |
} | |
@override | |
void dispose() { | |
widget.controller?.detach(position); | |
position.dispose(); | |
super.dispose(); | |
} | |
// SEMANTICS | |
final GlobalKey _scrollSemanticsKey = GlobalKey(); | |
@override | |
@protected | |
void setSemanticsActions(Set<SemanticsAction> actions) { | |
if (_gestureDetectorKey.currentState != null) { | |
_gestureDetectorKey.currentState.replaceSemanticsActions(actions); | |
} | |
} | |
// GESTURE RECOGNITION AND POINTER IGNORING | |
final GlobalKey<RawGestureDetectorState> _gestureDetectorKey = GlobalKey<RawGestureDetectorState>(); | |
final GlobalKey _ignorePointerKey = GlobalKey(); | |
// This field is set during layout, and then reused until the next time it is set. | |
Map<Type, GestureRecognizerFactory> _gestureRecognizers = const <Type, GestureRecognizerFactory>{}; | |
bool _shouldIgnorePointer = false; | |
bool _lastCanDrag; | |
Axis _lastAxisDirection; | |
@override | |
@protected | |
void setCanDrag(bool canDrag) { | |
if (canDrag == _lastCanDrag && (!canDrag || widget.axis == _lastAxisDirection)) { | |
return; | |
} | |
if (!canDrag) { | |
_gestureRecognizers = const <Type, GestureRecognizerFactory>{}; | |
} else { | |
switch (widget.axis) { | |
case Axis.vertical: | |
_gestureRecognizers = <Type, GestureRecognizerFactory>{ | |
VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>( | |
() => VerticalDragGestureRecognizer(), | |
(VerticalDragGestureRecognizer instance) { | |
instance | |
..onDown = _handleDragDown | |
..onStart = _handleDragStart | |
..onUpdate = _handleDragUpdate | |
..onEnd = _handleDragEnd | |
..onCancel = _handleDragCancel | |
..minFlingDistance = _physics?.minFlingDistance | |
..minFlingVelocity = _physics?.minFlingVelocity | |
..maxFlingVelocity = _physics?.maxFlingVelocity | |
..dragStartBehavior = widget.dragStartBehavior; | |
}, | |
), | |
ScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<ScaleGestureRecognizer>( | |
() => ScaleGestureRecognizer(), | |
(ScaleGestureRecognizer instance) { | |
instance | |
..onStart = (_) {_handleScaleStart(_);} | |
..onUpdate = (_) {_handleScaleUpdate(_);}; | |
}, | |
), | |
}; | |
break; | |
case Axis.horizontal: | |
_gestureRecognizers = <Type, GestureRecognizerFactory>{ | |
HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>( | |
() => HorizontalDragGestureRecognizer(), | |
(HorizontalDragGestureRecognizer instance) { | |
instance | |
..onDown = _handleDragDown | |
..onStart = _handleDragStart | |
..onUpdate = _handleDragUpdate | |
..onEnd = _handleDragEnd | |
..onCancel = _handleDragCancel | |
..minFlingDistance = _physics?.minFlingDistance | |
..minFlingVelocity = _physics?.minFlingVelocity | |
..maxFlingVelocity = _physics?.maxFlingVelocity | |
..dragStartBehavior = widget.dragStartBehavior; | |
}, | |
), | |
}; | |
break; | |
} | |
} | |
_lastCanDrag = canDrag; | |
_lastAxisDirection = widget.axis; | |
if (_gestureDetectorKey.currentState != null) { | |
_gestureDetectorKey.currentState.replaceGestureRecognizers(_gestureRecognizers); | |
} | |
} | |
@override | |
TickerProvider get vsync => this; | |
@override | |
@protected | |
void setIgnorePointer(bool value) { | |
if (_shouldIgnorePointer == value) { | |
return; | |
} | |
_shouldIgnorePointer = value; | |
if (_ignorePointerKey.currentContext != null) { | |
final RenderIgnorePointer renderBox = _ignorePointerKey.currentContext.findRenderObject(); | |
renderBox.ignoring = _shouldIgnorePointer; | |
} | |
} | |
@override | |
BuildContext get notificationContext => _gestureDetectorKey.currentContext; | |
@override | |
BuildContext get storageContext => context; | |
// TOUCH HANDLERS | |
Drag _drag; | |
ScrollHoldController _hold; | |
void _handleDragDown(DragDownDetails details) { | |
assert(_drag == null); | |
assert(_hold == null); | |
_hold = position.hold(_disposeHold); | |
} | |
void _handleDragStart(DragStartDetails details) { | |
// It's possible for _hold to become null between _handleDragDown and | |
// _handleDragStart, for example if some user code calls jumpTo or otherwise | |
// triggers a new activity to begin. | |
assert(_drag == null); | |
_drag = position.drag(details, _disposeDrag); | |
assert(_drag != null); | |
assert(_hold == null); | |
} | |
void _handleDragUpdate(DragUpdateDetails details) { | |
// _drag might be null if the drag activity ended and called _disposeDrag. | |
assert(_hold == null || _drag == null); | |
_drag?.update(details); | |
} | |
void _handleDragEnd(DragEndDetails details) { | |
// _drag might be null if the drag activity ended and called _disposeDrag. | |
assert(_hold == null || _drag == null); | |
_drag?.end(details); | |
assert(_drag == null); | |
} | |
void _handleDragCancel() { | |
// _hold might be null if the drag started. | |
// _drag might be null if the drag activity ended and called _disposeDrag. | |
assert(_hold == null || _drag == null); | |
_hold?.cancel(); | |
_drag?.cancel(); | |
assert(_hold == null); | |
assert(_drag == null); | |
} | |
void _disposeHold() { | |
_hold = null; | |
} | |
void _disposeDrag() { | |
_drag = null; | |
} | |
// SCALE GESTURE | |
double _scaleStartPosition; | |
void _handleScaleStart(ScaleStartDetails details) { | |
_scaleStartPosition = details.focalPoint.dy; | |
} | |
void _handleScaleUpdate(ScaleUpdateDetails details) { | |
double currentPosition = details.focalPoint.dy; | |
position.jumpTo(position.pixels + (_scaleStartPosition - currentPosition)); | |
_scaleStartPosition = details.focalPoint.dy; | |
} | |
// SCROLL WHEEL | |
// Returns the offset that should result from applying [event] to the current | |
// position, taking min/max scroll extent into account. | |
double _targetScrollOffsetForPointerScroll(PointerScrollEvent event) { | |
double delta = widget.axis == Axis.horizontal | |
? event.scrollDelta.dx | |
: event.scrollDelta.dy; | |
if (axisDirectionIsReversed(widget.axisDirection)) { | |
delta *= -1; | |
} | |
return math.min(math.max(position.pixels + delta, position.minScrollExtent), | |
position.maxScrollExtent); | |
} | |
void _receivedPointerSignal(PointerSignalEvent event) { | |
if (event is PointerScrollEvent && position != null) { | |
final double targetScrollOffset = _targetScrollOffsetForPointerScroll(event); | |
// Only express interest in the event if it would actually result in a scroll. | |
if (targetScrollOffset != position.pixels) { | |
GestureBinding.instance.pointerSignalResolver.register(event, _handlePointerScroll); | |
} | |
} | |
} | |
void _handlePointerScroll(PointerEvent event) { | |
assert(event is PointerScrollEvent); | |
if (_physics != null && !_physics.shouldAcceptUserOffset(position)) { | |
return; | |
} | |
final double targetScrollOffset = _targetScrollOffsetForPointerScroll(event); | |
if (targetScrollOffset != position.pixels) { | |
position.jumpTo(targetScrollOffset); | |
} | |
} | |
// DESCRIPTION | |
@override | |
Widget build(BuildContext context) { | |
assert(position != null); | |
// _ScrollableScope must be placed above the BuildContext returned by notificationContext | |
// so that we can get this ScrollableState by doing the following: | |
// | |
// ScrollNotification notification; | |
// Scrollable.of(notification.context) | |
// | |
// Since notificationContext is pointing to _gestureDetectorKey.context, _ScrollableScope | |
// must be placed above the widget using it: RawGestureDetector | |
Widget result = _ScrollableScope( | |
scrollable: this, | |
position: position, | |
// TODO(ianh): Having all these global keys is sad. | |
child: Listener( | |
onPointerSignal: _receivedPointerSignal, | |
child: RawGestureDetector( | |
key: _gestureDetectorKey, | |
gestures: _gestureRecognizers, | |
behavior: HitTestBehavior.opaque, | |
excludeFromSemantics: widget.excludeFromSemantics, | |
child: Semantics( | |
explicitChildNodes: !widget.excludeFromSemantics, | |
child: IgnorePointer( | |
key: _ignorePointerKey, | |
ignoring: _shouldIgnorePointer, | |
ignoringSemantics: false, | |
child: widget.viewportBuilder(context, position), | |
), | |
), | |
), | |
), | |
); | |
if (!widget.excludeFromSemantics) { | |
result = _ScrollSemantics( | |
key: _scrollSemanticsKey, | |
child: result, | |
position: position, | |
allowImplicitScrolling: widget?.physics?.allowImplicitScrolling ?? _physics.allowImplicitScrolling, | |
semanticChildCount: widget.semanticChildCount, | |
); | |
} | |
return _configuration.buildViewportChrome(context, result, widget.axisDirection); | |
} | |
@override | |
void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
super.debugFillProperties(properties); | |
properties.add(DiagnosticsProperty<ScrollPosition>('position', position)); | |
} | |
} | |
/// With [_ScrollSemantics] certain child [SemanticsNode]s can be | |
/// excluded from the scrollable area for semantics purposes. | |
/// | |
/// Nodes, that are to be excluded, have to be tagged with | |
/// [RenderViewport.excludeFromScrolling] and the [RenderAbstractViewport] in | |
/// use has to add the [RenderViewport.useTwoPaneSemantics] tag to its | |
/// [SemanticsConfiguration] by overriding | |
/// [RenderObject.describeSemanticsConfiguration]. | |
/// | |
/// If the tag [RenderViewport.useTwoPaneSemantics] is present on the viewport, | |
/// two semantics nodes will be used to represent the [Scrollable]: The outer | |
/// node will contain all children, that are excluded from scrolling. The inner | |
/// node, which is annotated with the scrolling actions, will house the | |
/// scrollable children. | |
class _ScrollSemantics extends SingleChildRenderObjectWidget { | |
const _ScrollSemantics({ | |
Key key, | |
@required this.position, | |
@required this.allowImplicitScrolling, | |
@required this.semanticChildCount, | |
Widget child, | |
}) : assert(position != null), | |
assert(semanticChildCount == null || semanticChildCount >= 0), | |
super(key: key, child: child); | |
final ScrollPosition position; | |
final bool allowImplicitScrolling; | |
final int semanticChildCount; | |
@override | |
_RenderScrollSemantics createRenderObject(BuildContext context) { | |
return _RenderScrollSemantics( | |
position: position, | |
allowImplicitScrolling: allowImplicitScrolling, | |
semanticChildCount: semanticChildCount, | |
); | |
} | |
@override | |
void updateRenderObject(BuildContext context, _RenderScrollSemantics renderObject) { | |
renderObject | |
..allowImplicitScrolling = allowImplicitScrolling | |
..position = position | |
..semanticChildCount = semanticChildCount; | |
} | |
} | |
class _RenderScrollSemantics extends RenderProxyBox { | |
_RenderScrollSemantics({ | |
@required ScrollPosition position, | |
@required bool allowImplicitScrolling, | |
@required int semanticChildCount, | |
RenderBox child, | |
}) : _position = position, | |
_allowImplicitScrolling = allowImplicitScrolling, | |
_semanticChildCount = semanticChildCount, | |
assert(position != null), | |
super(child) { | |
position.addListener(markNeedsSemanticsUpdate); | |
} | |
/// Whether this render object is excluded from the semantic tree. | |
ScrollPosition get position => _position; | |
ScrollPosition _position; | |
set position(ScrollPosition value) { | |
assert(value != null); | |
if (value == _position) { | |
return; | |
} | |
_position.removeListener(markNeedsSemanticsUpdate); | |
_position = value; | |
_position.addListener(markNeedsSemanticsUpdate); | |
markNeedsSemanticsUpdate(); | |
} | |
/// Whether this node can be scrolled implicitly. | |
bool get allowImplicitScrolling => _allowImplicitScrolling; | |
bool _allowImplicitScrolling; | |
set allowImplicitScrolling(bool value) { | |
if (value == _allowImplicitScrolling) { | |
return; | |
} | |
_allowImplicitScrolling = value; | |
markNeedsSemanticsUpdate(); | |
} | |
int get semanticChildCount => _semanticChildCount; | |
int _semanticChildCount; | |
set semanticChildCount(int value) { | |
if (value == semanticChildCount) { | |
return; | |
} | |
_semanticChildCount = value; | |
markNeedsSemanticsUpdate(); | |
} | |
@override | |
void describeSemanticsConfiguration(SemanticsConfiguration config) { | |
super.describeSemanticsConfiguration(config); | |
config.isSemanticBoundary = true; | |
if (position.haveDimensions) { | |
config | |
..hasImplicitScrolling = allowImplicitScrolling | |
..scrollPosition = _position.pixels | |
..scrollExtentMax = _position.maxScrollExtent | |
..scrollExtentMin = _position.minScrollExtent | |
..scrollChildCount = semanticChildCount; | |
} | |
} | |
SemanticsNode _innerNode; | |
@override | |
void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) { | |
if (children.isEmpty || !children.first.isTagged(RenderViewport.useTwoPaneSemantics)) { | |
super.assembleSemanticsNode(node, config, children); | |
return; | |
} | |
_innerNode ??= SemanticsNode(showOnScreen: showOnScreen); | |
_innerNode | |
..isMergedIntoParent = node.isPartOfNodeMerging | |
..rect = Offset.zero & node.rect.size; | |
int firstVisibleIndex; | |
final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode]; | |
final List<SemanticsNode> included = <SemanticsNode>[]; | |
for (SemanticsNode child in children) { | |
assert(child.isTagged(RenderViewport.useTwoPaneSemantics)); | |
if (child.isTagged(RenderViewport.excludeFromScrolling)) { | |
excluded.add(child); | |
} else { | |
if (!child.hasFlag(SemanticsFlag.isHidden)) { | |
firstVisibleIndex ??= child.indexInParent; | |
} | |
included.add(child); | |
} | |
} | |
config.scrollIndex = firstVisibleIndex; | |
node.updateWith(config: null, childrenInInversePaintOrder: excluded); | |
_innerNode.updateWith(config: config, childrenInInversePaintOrder: included); | |
} | |
@override | |
void clearSemantics() { | |
super.clearSemantics(); | |
_innerNode = null; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment