diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 87a611a6f60..843a05d15e6 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -9,7 +9,7 @@ import 'dart:math' as math; import 'dart:ui' show SemanticsRole, lerpDouble; import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart' show DragStartBehavior; +import 'package:flutter/gestures.dart' show DragStartBehavior, HorizontalDragGestureRecognizer; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -2222,8 +2222,17 @@ class TabBarView extends StatefulWidget { this.dragStartBehavior = DragStartBehavior.start, this.viewportFraction = 1.0, this.clipBehavior = Clip.hardEdge, + this.scrollDirection = Axis.horizontal, + this.hitTestBehavior = HitTestBehavior.opaque, + this.horizontalDragGestureRecognizer = HorizontalDragGestureRecognizer.new, }); + final Axis scrollDirection; + + final HitTestBehavior hitTestBehavior; + + final ValueGetter horizontalDragGestureRecognizer; + /// This widget's selection and animation state. /// /// If [TabController] is not provided, then the value of [DefaultTabController.of] @@ -2525,6 +2534,9 @@ class _TabBarViewState extends State { dragStartBehavior: widget.dragStartBehavior, clipBehavior: widget.clipBehavior, controller: _pageController, + scrollDirection: widget.scrollDirection, + hitTestBehavior: widget.hitTestBehavior, + horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer, physics: widget.physics == null ? const PageScrollPhysics().applyTo(const ClampingScrollPhysics()) : const PageScrollPhysics().applyTo(widget.physics), diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index 158e7c321ed..b0d1205895f 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -1430,7 +1430,7 @@ class RawGestureDetector extends StatefulWidget { } /// State for a [RawGestureDetector]. -class RawGestureDetectorState extends State { +class RawGestureDetectorState extends State { Map? _recognizers = const {}; SemanticsGestureDelegate? _semantics; @@ -1444,7 +1444,7 @@ class RawGestureDetectorState extends State { @protected @override - void didUpdateWidget(RawGestureDetector oldWidget) { + void didUpdateWidget(T oldWidget) { super.didUpdateWidget(oldWidget); if (!(oldWidget.semantics == null && widget.semantics == null)) { _semantics = widget.semantics ?? _DefaultSemanticsGestureDelegate(this); @@ -1636,6 +1636,109 @@ class RawGestureDetectorState extends State { } } +class NestedRawGestureDetector extends RawGestureDetector { + const NestedRawGestureDetector({ + super.key, + super.child, + super.gestures, + super.behavior, + super.excludeFromSemantics, + super.semantics, + required this.ignoring, + }); + + final ValueGetter ignoring; + + @override + RawGestureDetectorState createState() => NestedRawGestureDetectorState(); +} + +class NestedRawGestureDetectorState extends RawGestureDetectorState { + @override + Widget build(BuildContext context) { + Widget result = NestedListener( + onPointerDown: _handlePointerDown, + onPointerPanZoomStart: _handlePointerPanZoomStart, + behavior: widget.behavior ?? _defaultBehavior, + ignoring: widget.ignoring, + child: widget.child, + ); + if (!widget.excludeFromSemantics) { + result = _GestureSemantics( + behavior: widget.behavior ?? _defaultBehavior, + assignSemantics: _updateSemanticsForRenderObject, + child: result, + ); + } + return result; + } +} + +class NestedListener extends Listener { + const NestedListener({ + super.key, + super.onPointerDown, + super.onPointerMove, + super.onPointerUp, + super.onPointerHover, + super.onPointerCancel, + super.onPointerPanZoomStart, + super.onPointerPanZoomUpdate, + super.onPointerPanZoomEnd, + super.onPointerSignal, + super.behavior, + required this.ignoring, + super.child, + }); + + final ValueGetter ignoring; + + @override + RenderPointerListener createRenderObject(BuildContext context) { + return _RenderPointerListener( + onPointerDown: onPointerDown, + onPointerMove: onPointerMove, + onPointerUp: onPointerUp, + onPointerHover: onPointerHover, + onPointerCancel: onPointerCancel, + onPointerPanZoomStart: onPointerPanZoomStart, + onPointerPanZoomUpdate: onPointerPanZoomUpdate, + onPointerPanZoomEnd: onPointerPanZoomEnd, + onPointerSignal: onPointerSignal, + behavior: behavior, + ignoring: ignoring, + ); + } + + @override + void updateRenderObject(BuildContext context, _RenderPointerListener renderObject) { + super.updateRenderObject(context, renderObject); + renderObject.ignoring = ignoring; + } +} + +class _RenderPointerListener extends RenderPointerListener { + _RenderPointerListener({ + super.onPointerDown, + super.onPointerMove, + super.onPointerUp, + super.onPointerHover, + super.onPointerCancel, + super.onPointerPanZoomStart, + super.onPointerPanZoomUpdate, + super.onPointerPanZoomEnd, + super.onPointerSignal, + super.behavior, + required this.ignoring, + super.child, + }); + + ValueGetter ignoring; + + @override + bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque && ignoring(); +} + typedef _AssignSemantics = void Function(RenderSemanticsGestureHandler); class _GestureSemantics extends SingleChildRenderObjectWidget { diff --git a/packages/flutter/lib/src/widgets/page_view.dart b/packages/flutter/lib/src/widgets/page_view.dart index 59fa2544abe..43c57ad5f56 100644 --- a/packages/flutter/lib/src/widgets/page_view.dart +++ b/packages/flutter/lib/src/widgets/page_view.dart @@ -11,7 +11,7 @@ library; import 'dart:math' as math; import 'package:flutter/foundation.dart' show clampDouble, precisionErrorTolerance; -import 'package:flutter/gestures.dart' show DragStartBehavior; +import 'package:flutter/gestures.dart' show DragStartBehavior, HorizontalDragGestureRecognizer; import 'package:flutter/rendering.dart'; import 'basic.dart'; @@ -697,6 +697,7 @@ class PageView extends StatefulWidget { this.hitTestBehavior = HitTestBehavior.opaque, this.scrollBehavior, this.padEnds = true, + this.horizontalDragGestureRecognizer = HorizontalDragGestureRecognizer.new, }) : assert( scrollCacheExtent == null || (scrollCacheExtent.value > 0.0) == allowImplicitScrolling, 'scrollCacheExtent and allowImplicitScrolling must be consistent: ' @@ -751,6 +752,7 @@ class PageView extends StatefulWidget { this.hitTestBehavior = HitTestBehavior.opaque, this.scrollBehavior, this.padEnds = true, + this.horizontalDragGestureRecognizer = HorizontalDragGestureRecognizer.new, }) : assert( scrollCacheExtent == null || (scrollCacheExtent.value > 0.0) == allowImplicitScrolling, 'scrollCacheExtent and allowImplicitScrolling must be consistent: ' @@ -793,6 +795,7 @@ class PageView extends StatefulWidget { this.hitTestBehavior = HitTestBehavior.opaque, this.scrollBehavior, this.padEnds = true, + this.horizontalDragGestureRecognizer = HorizontalDragGestureRecognizer.new, }) : assert( scrollCacheExtent == null || (scrollCacheExtent.value > 0.0) == allowImplicitScrolling, 'scrollCacheExtent and allowImplicitScrolling must be consistent: ' @@ -802,6 +805,8 @@ class PageView extends StatefulWidget { scrollCacheExtent = scrollCacheExtent ?? ScrollCacheExtent.viewport(allowImplicitScrolling ? 1.0 : 0.0); + final ValueGetter horizontalDragGestureRecognizer; + /// {@template flutter.widgets.PageView.allowImplicitScrolling} /// Controls whether the widget's pages will respond to /// [RenderObject.showOnScreen], which will allow for implicit accessibility @@ -1021,6 +1026,7 @@ class _PageViewState extends State { physics: physics, restorationId: widget.restorationId, hitTestBehavior: widget.hitTestBehavior, + horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer, scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false), viewportBuilder: (BuildContext context, ViewportOffset position) { diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index 9df2d633f16..001565fbb00 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -45,6 +45,8 @@ import 'viewport.dart'; export 'package:flutter/physics.dart' show Tolerance; +const _nestedOuterDebugLabel = 'outer'; + // Examples can assume: // late BuildContext context; @@ -134,8 +136,11 @@ class Scrollable extends StatefulWidget { this.scrollBehavior, this.clipBehavior = Clip.hardEdge, this.hitTestBehavior = HitTestBehavior.opaque, + this.horizontalDragGestureRecognizer = HorizontalDragGestureRecognizer.new, }) : assert(semanticChildCount == null || semanticChildCount >= 0); + final ValueGetter horizontalDragGestureRecognizer; + /// {@template flutter.widgets.Scrollable.axisDirection} /// The direction in which this widget scrolls. /// @@ -811,8 +816,7 @@ class ScrollableState extends State _gestureRecognizers = { HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => - HorizontalDragGestureRecognizer(supportedDevices: _configuration.dragDevices), + widget.horizontalDragGestureRecognizer, (HorizontalDragGestureRecognizer instance) { instance ..onDown = _handleDragDown @@ -1020,26 +1024,37 @@ class ScrollableState extends State // // Since notificationContext is pointing to _gestureDetectorKey.context, _ScrollableScope // must be placed above the widget using it: RawGestureDetector - Widget result = _ScrollableScope( + Widget result = Semantics( + explicitChildNodes: !widget.excludeFromSemantics, + child: IgnorePointer( + key: _ignorePointerKey, + ignoring: _shouldIgnorePointer, + child: widget.viewportBuilder(context, position), + ), + ); + if (_effectiveScrollController.debugLabel == _nestedOuterDebugLabel) { + result = NestedRawGestureDetector( + key: _gestureDetectorKey, + gestures: _gestureRecognizers, + behavior: widget.hitTestBehavior, + excludeFromSemantics: widget.excludeFromSemantics, + ignoring: () => _shouldIgnorePointer, + child: result, + ); + } else { + result = RawGestureDetector( + key: _gestureDetectorKey, + gestures: _gestureRecognizers, + behavior: widget.hitTestBehavior, + excludeFromSemantics: widget.excludeFromSemantics, + child: result, + ); + } + + result = _ScrollableScope( scrollable: this, position: position, - child: Listener( - onPointerSignal: _receivedPointerSignal, - child: RawGestureDetector( - key: _gestureDetectorKey, - gestures: _gestureRecognizers, - behavior: widget.hitTestBehavior, - excludeFromSemantics: widget.excludeFromSemantics, - child: Semantics( - explicitChildNodes: !widget.excludeFromSemantics, - child: IgnorePointer( - key: _ignorePointerKey, - ignoring: _shouldIgnorePointer, - child: widget.viewportBuilder(context, position), - ), - ), - ), - ), + child: Listener(onPointerSignal: _receivedPointerSignal, child: result), ); if (!widget.excludeFromSemantics) {