blob: eb0e45f5d2a0d561c1b8c64121d6478b06be87cf [file]
// Copyright 2020 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
// ignore_for_file: avoid-explicit-type-declaration, forked code from Flutter framework.
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
/// A [ScrollView] that uses a single child layout model and allows for custom
/// handling of [PointerSignalEvent]s.
///
/// This class is copied from the Flutter framework [BoxScrollView] and
/// overrides [ScrollView.build] to use [CustomPointerScrollable] in place of
/// [Scrollable].
abstract class CustomPointerScrollView extends BoxScrollView {
/// Creates a [ScrollView] uses a single child layout model.
///
/// If the [primary] argument is true, the [controller] must be null.
const CustomPointerScrollView({
super.key,
super.scrollDirection,
super.reverse,
super.controller,
super.primary,
super.physics,
super.shrinkWrap,
super.padding,
super.cacheExtent,
super.semanticChildCount,
super.dragStartBehavior,
this.customPointerSignalHandler,
}) : _primary =
primary ??
controller == null && identical(scrollDirection, Axis.vertical);
final void Function(PointerSignalEvent event)? customPointerSignalHandler;
// TODO(Piinks): Restore once PSC changes have landed, or keep to maintain
// original primary behavior.
final bool _primary;
@override
Widget build(BuildContext context) {
final List<Widget> slivers = buildSlivers(context);
final AxisDirection axisDirection = getDirection(context);
final ScrollController? scrollController = _primary
? PrimaryScrollController.of(context)
: controller;
assert(
scrollController != null,
'No ScrollController has been provided to the CustomPointerScrollView.',
);
final CustomPointerScrollable scrollable = CustomPointerScrollable(
dragStartBehavior: dragStartBehavior,
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
semanticChildCount: semanticChildCount,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return buildViewport(context, offset, axisDirection, slivers);
},
customPointerSignalHandler: customPointerSignalHandler,
);
return _primary
? PrimaryScrollController.none(child: scrollable)
: scrollable;
}
}
/// A widget that scrolls and allows custom pointer signal event handling.
///
/// This widget is a copy of [Scrollable] with additional support for custom
/// pointer signal event handling via [customPointerSignalHandler].
class CustomPointerScrollable extends StatefulWidget {
const CustomPointerScrollable({
super.key,
this.axisDirection = AxisDirection.down,
this.controller,
this.physics,
required this.viewportBuilder,
this.incrementCalculator,
this.excludeFromSemantics = false,
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.customPointerSignalHandler,
}) : assert(semanticChildCount == null || semanticChildCount >= 0);
/// 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;
/// An optional function that will be called to calculate the distance to
/// scroll when the scrollable is asked to scroll via the keyboard using a
/// [ScrollAction].
///
/// If not supplied, the [Scrollable] will scroll a default amount when a
/// keyboard navigation key is pressed (e.g. pageUp/pageDown, control-upArrow,
/// etc.), or otherwise invoked by a [ScrollAction].
///
/// If [incrementCalculator] is null, the default for
/// [ScrollIncrementType.page] is 80% of the size of the scroll window, and
/// for [ScrollIncrementType.line], 50 logical pixels.
final ScrollIncrementCalculator? incrementCalculator;
/// 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 [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);
final void Function(PointerSignalEvent event)? customPointerSignalHandler;
@override
State<CustomPointerScrollable> createState() =>
CustomPointerScrollableState();
@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
/// _CustomPointerScrollableState scrollable = Scrollable.of(context);
/// ```
///
/// Calling this method will create a dependency on the closest [Scrollable]
/// in the [context], if there is one.
static CustomPointerScrollableState? 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>>[];
CustomPointerScrollableState? scrollable = CustomPointerScrollable.of(
context,
);
while (scrollable != null) {
futures.add(
scrollable.position!.ensureVisible(
context.findRenderObject()!,
alignment: alignment,
duration: duration,
curve: curve,
alignmentPolicy: alignmentPolicy,
),
);
context = scrollable.context;
scrollable = CustomPointerScrollable.of(context);
}
if (futures.isEmpty || duration == Duration.zero) {
return Future<void>.value();
}
if (futures.length == 1) return futures.single;
return futures.wait.then<void>((List<void> _) => null);
}
}
/// State object for [CustomPointerScrollable].
///
/// This state object is a copy of [ScrollableState] and replaces the handler
/// [ScrollableState._receivedPointerSignal] with
/// [CustomPointerScrollable.customPointerSignalHandler] when non-null.
class CustomPointerScrollableState extends State<CustomPointerScrollable>
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;
@override
double get devicePixelRatio => _devicePixelRatio;
late double _devicePixelRatio;
late 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 saveOffset(double offset) {
assert(debugIsSerializableForRestoration(offset));
// TODO(goderbauer): enable state restoration once the framework is stable.
}
@override
void didChangeDependencies() {
_devicePixelRatio =
MediaQuery.maybeDevicePixelRatioOf(context) ??
View.of(context).devicePixelRatio;
_updatePosition();
super.didChangeDependencies();
}
bool _shouldUpdatePosition(CustomPointerScrollable 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(CustomPointerScrollable 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;
}),
};
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()
as RenderIgnorePointer;
renderBox.ignoring = _shouldIgnorePointer;
}
}
@override
BuildContext? get notificationContext => _gestureDetectorKey.currentContext;
@override
BuildContext get storageContext => context;
// TOUCH HANDLERS
Drag? _drag;
ScrollHoldController? _hold;
void _handleDragDown(DragDownDetails _) {
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;
}
// 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 as PointerScrollEvent,
);
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 _CustomPointerScrollableState
// by doing the following:
//
// ScrollNotification notification;
// CustomPointerScrollable.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:
widget.customPointerSignalHandler ?? _receivedPointerSignal,
child: RawGestureDetector(
key: _gestureDetectorKey,
gestures: _gestureRecognizers,
behavior: HitTestBehavior.opaque,
excludeFromSemantics: widget.excludeFromSemantics,
child: Semantics(
explicitChildNodes: !widget.excludeFromSemantics,
child: IgnorePointer(
key: _ignorePointerKey,
ignoring: _shouldIgnorePointer,
child: widget.viewportBuilder(context, position!),
),
),
),
),
);
if (!widget.excludeFromSemantics) {
result = _ScrollSemantics(
key: _scrollSemanticsKey,
position: position!,
allowImplicitScrolling:
widget.physics?.allowImplicitScrolling ??
_physics!.allowImplicitScrolling,
semanticChildCount: widget.semanticChildCount,
child: result,
);
}
// In contrast to scrollable.dart, _configuration.buildScrollbar is not
// called since scrollbars are added manually where needed.
return _configuration.buildOverscrollIndicator(
context,
result,
ScrollableDetails(
controller: widget.controller!,
direction: axisDirection,
),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ScrollPosition>('position', position));
}
}
// The following classes were copied from the Flutter framework with minor
// changes to use with [CustomPointerScrollable] instead of [Scrollable]. See
// flutter/lib/src/widgets/scrollable.dart for original classes.
// Enable Scrollable.of() to work as if _CustomPointerScrollableState was an
// inherited widget. _CustomPointerScrollableState.build() always rebuilds its
// _ScrollableScope.
class _ScrollableScope extends InheritedWidget {
const _ScrollableScope({
required this.scrollable,
required this.position,
required super.child,
});
final CustomPointerScrollableState scrollable;
final ScrollPosition position;
@override
bool updateShouldNotify(_ScrollableScope old) {
return position != old.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({
super.key,
required this.position,
required this.allowImplicitScrolling,
required this.semanticChildCount,
super.child,
}) : assert(semanticChildCount == null || semanticChildCount >= 0);
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,
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) {
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)).rect = node.rect;
int? firstVisibleIndex;
final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode!];
final List<SemanticsNode> included = <SemanticsNode>[];
for (final child in children) {
assert(child.isTagged(RenderViewport.useTwoPaneSemantics));
if (child.isTagged(RenderViewport.excludeFromScrolling)) {
excluded.add(child);
} else {
if (!child.flagsCollection.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;
}
}