From 1ded09f246d707f42ebb70db7130897f2be7dc7d Mon Sep 17 00:00:00 2001 From: dom Date: Fri, 31 Jul 2026 15:34:50 +0800 Subject: [PATCH] opt scrollable gesture Signed-off-by: dom --- .../widgets/flutter/page/page_view.dart | 524 ----- .../widgets/flutter/page/scrollable.dart | 1857 ----------------- .../flutter/page/scrollable_helpers.dart | 208 -- lib/common/widgets/flutter/page/tabs.dart | 374 ---- .../horizontal_drag_gesture_recognizer.dart | 3 + .../widgets/image_viewer/gallery_viewer.dart | 12 +- lib/common/widgets/scroll_physics.dart | 25 +- lib/pages/article/view.dart | 10 +- lib/pages/bubble/view.dart | 2 +- lib/pages/contact/view.dart | 2 +- lib/pages/danmaku_block/view.dart | 2 +- lib/pages/dynamics/view.dart | 2 +- lib/pages/dynamics_detail/view.dart | 10 +- lib/pages/episode_panel/view.dart | 12 +- lib/pages/fav/view.dart | 2 +- lib/pages/follow/view.dart | 2 +- lib/pages/history/view.dart | 10 +- lib/pages/home/view.dart | 2 +- lib/pages/later/view.dart | 10 +- lib/pages/live_area/view.dart | 2 +- lib/pages/live_area_detail/view.dart | 2 +- lib/pages/live_dm_block/view.dart | 3 +- .../live_room/contribution_rank/view.dart | 2 +- lib/pages/live_room/view.dart | 13 +- lib/pages/live_search/view.dart | 7 +- lib/pages/login/view.dart | 2 +- lib/pages/main/view.dart | 2 - lib/pages/member/view.dart | 3 +- lib/pages/member/widget/user_info_card.dart | 5 +- lib/pages/member_search/view.dart | 7 +- lib/pages/member_upower_rank/view.dart | 2 +- lib/pages/member_video/controller.dart | 2 +- lib/pages/member_video/view.dart | 3 +- lib/pages/member_video_web/base/view.dart | 3 +- lib/pages/pgc/view.dart | 2 +- lib/pages/popular_series/view.dart | 3 +- lib/pages/search_result/view.dart | 2 +- lib/pages/setting/models/style_settings.dart | 9 +- lib/pages/video/ai_conclusion/view.dart | 12 +- .../pgc/widgets/intro_detail.dart | 10 +- .../video/introduction/ugc/controller.dart | 2 +- lib/pages/video/note/view.dart | 42 +- lib/pages/video/pay_coins/view.dart | 5 +- lib/pages/video/post_panel/view.dart | 28 +- lib/pages/video/reply/view.dart | 10 +- lib/pages/video/reply_reply/view.dart | 6 +- lib/pages/video/reply_search_item/view.dart | 2 +- lib/pages/video/view.dart | 14 +- lib/pages/video/view_point/view.dart | 12 +- lib/scripts/patch.ps1 | 4 +- lib/scripts/scrollable_gesture.patch | 318 +++ lib/scripts/tabs.patch | 24 - 52 files changed, 460 insertions(+), 3172 deletions(-) delete mode 100644 lib/common/widgets/flutter/page/page_view.dart delete mode 100644 lib/common/widgets/flutter/page/scrollable.dart delete mode 100644 lib/common/widgets/flutter/page/scrollable_helpers.dart delete mode 100644 lib/common/widgets/flutter/page/tabs.dart create mode 100644 lib/scripts/scrollable_gesture.patch delete mode 100644 lib/scripts/tabs.patch diff --git a/lib/common/widgets/flutter/page/page_view.dart b/lib/common/widgets/flutter/page/page_view.dart deleted file mode 100644 index 18f15dbcb..000000000 --- a/lib/common/widgets/flutter/page/page_view.dart +++ /dev/null @@ -1,524 +0,0 @@ -// 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. - -// ignore_for_file: prefer_initializing_formals - -import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart'; -import 'package:flutter/gestures.dart' - show DragStartBehavior, HorizontalDragGestureRecognizer; -import 'package:flutter/material.dart' - hide PageView, Scrollable, ScrollableState; -import 'package:flutter/rendering.dart'; - -class _ForceImplicitScrollPhysics extends ScrollPhysics { - const _ForceImplicitScrollPhysics({ - required this.allowImplicitScrolling, - super.parent, - }); - - @override - _ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) { - return _ForceImplicitScrollPhysics( - allowImplicitScrolling: allowImplicitScrolling, - parent: buildParent(ancestor), - ); - } - - @override - final bool allowImplicitScrolling; -} - -const PageScrollPhysics _kPagePhysics = PageScrollPhysics(); - -/// A scrollable list that works page by page. -/// -/// Each child of a page view is forced to be the same size as the viewport. -/// -/// You can use a [PageController] to control which page is visible in the view. -/// In addition to being able to control the pixel offset of the content inside -/// the [PageView], a [PageController] also lets you control the offset in terms -/// of pages, which are increments of the viewport size. -/// -/// The [PageController] can also be used to control the -/// [PageController.initialPage], which determines which page is shown when the -/// [PageView] is first constructed, and the [PageController.viewportFraction], -/// which determines the size of the pages as a fraction of the viewport size. -/// -/// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A} -/// -/// {@tool dartpad} -/// Here is an example of [PageView]. It creates a centered [Text] in each of the three pages -/// which scroll horizontally. -/// -/// ** See code in examples/api/lib/widgets/page_view/page_view.0.dart ** -/// {@end-tool} -/// -/// ## Persisting the scroll position during a session -/// -/// Scroll views attempt to persist their scroll position using [PageStorage]. -/// For a [PageView], this can be disabled by setting [PageController.keepPage] -/// to false on the [controller]. If it is enabled, using a [PageStorageKey] for -/// the [key] of this widget is recommended to help disambiguate different -/// scroll views from each other. -/// -/// See also: -/// -/// * [PageController], which controls which page is visible in the view. -/// * [SingleChildScrollView], when you need to make a single child scrollable. -/// * [ListView], for a scrollable list of boxes. -/// * [GridView], for a scrollable grid of boxes. -/// * [ScrollNotification] and [NotificationListener], which can be used to watch -/// the scroll position without using a [ScrollController]. -class PageView - extends StatefulWidget { - /// Creates a scrollable list that works page by page from an explicit [List] - /// of widgets. - /// - /// This constructor is appropriate for page views with a small number of - /// children because constructing the [List] requires doing work for every - /// child that could possibly be displayed in the page view, instead of just - /// those children that are actually visible. - /// - /// Like other widgets in the framework, this widget expects that - /// the [children] list will not be mutated after it has been passed in here. - /// See the documentation at [SliverChildListDelegate.children] for more details. - /// - /// {@template flutter.widgets.PageView.allowImplicitScrolling} - /// If [allowImplicitScrolling] is true, the [PageView] will participate in - /// accessibility scrolling more like a [ListView], where implicit scroll - /// actions will move to the next page rather than into the contents of the - /// [PageView]. - /// {@endtemplate} - PageView({ - super.key, - this.scrollDirection = Axis.horizontal, - this.reverse = false, - this.controller, - this.physics, - this.pageSnapping = true, - this.onPageChanged, - List children = const [], - this.dragStartBehavior = DragStartBehavior.start, - this.allowImplicitScrolling = false, - ScrollCacheExtent? scrollCacheExtent, - this.restorationId, - this.clipBehavior = Clip.hardEdge, - this.hitTestBehavior = HitTestBehavior.opaque, - this.scrollBehavior, - this.padEnds = true, - required this.horizontalDragGestureRecognizer, - }) : assert( - scrollCacheExtent == null || - (scrollCacheExtent.value > 0.0) == allowImplicitScrolling, - 'scrollCacheExtent and allowImplicitScrolling must be consistent: ' - 'scrollCacheExtent must be greater than 0.0 when allowImplicitScrolling is true, ' - 'and must be 0.0 when allowImplicitScrolling is false.', - ), - scrollCacheExtent = - scrollCacheExtent ?? - ScrollCacheExtent.viewport(allowImplicitScrolling ? 1.0 : 0.0), - childrenDelegate = SliverChildListDelegate(children); - - final GestureRecognizerFactoryConstructor horizontalDragGestureRecognizer; - - /// Creates a scrollable list that works page by page using widgets that are - /// created on demand. - /// - /// This constructor is appropriate for page views with a large (or infinite) - /// number of children because the builder is called only for those children - /// that are actually visible. - /// - /// Providing a non-null [itemCount] lets the [PageView] compute the maximum - /// scroll extent. - /// - /// [itemBuilder] will be called only with indices greater than or equal to - /// zero and less than [itemCount]. - /// - /// {@macro flutter.widgets.ListView.builder.itemBuilder} - /// - /// {@template flutter.widgets.PageView.findChildIndexCallback} - /// The [findChildIndexCallback] corresponds to the - /// [SliverChildBuilderDelegate.findChildIndexCallback] property. If null, - /// a child widget may not map to its existing [RenderObject] when the order - /// of children returned from the children builder changes. - /// This may result in state-loss. This callback needs to be implemented if - /// the order of the children may change at a later time. - /// {@endtemplate} - /// - /// {@macro flutter.widgets.PageView.allowImplicitScrolling} - PageView.builder({ - super.key, - this.scrollDirection = Axis.horizontal, - this.reverse = false, - this.controller, - this.physics, - this.pageSnapping = true, - this.onPageChanged, - required NullableIndexedWidgetBuilder itemBuilder, - ChildIndexGetter? findChildIndexCallback, - int? itemCount, - this.dragStartBehavior = DragStartBehavior.start, - this.allowImplicitScrolling = false, - ScrollCacheExtent? scrollCacheExtent, - this.restorationId, - this.clipBehavior = Clip.hardEdge, - this.hitTestBehavior = HitTestBehavior.opaque, - this.scrollBehavior, - this.padEnds = true, - required this.horizontalDragGestureRecognizer, - }) : assert( - scrollCacheExtent == null || - (scrollCacheExtent.value > 0.0) == allowImplicitScrolling, - 'scrollCacheExtent and allowImplicitScrolling must be consistent: ' - 'scrollCacheExtent must be greater than 0.0 when allowImplicitScrolling is true, ' - 'and must be 0.0 when allowImplicitScrolling is false.', - ), - scrollCacheExtent = - scrollCacheExtent ?? - ScrollCacheExtent.viewport(allowImplicitScrolling ? 1.0 : 0.0), - childrenDelegate = SliverChildBuilderDelegate( - itemBuilder, - findChildIndexCallback: findChildIndexCallback, - childCount: itemCount, - ); - - /// Creates a scrollable list that works page by page with a custom child - /// model. - /// - /// {@tool dartpad} - /// This example shows a [PageView] that uses a custom [SliverChildBuilderDelegate] to support child - /// reordering. - /// - /// ** See code in examples/api/lib/widgets/page_view/page_view.1.dart ** - /// {@end-tool} - /// - /// {@macro flutter.widgets.PageView.allowImplicitScrolling} - PageView.custom({ - super.key, - this.scrollDirection = Axis.horizontal, - this.reverse = false, - this.controller, - this.physics, - this.pageSnapping = true, - this.onPageChanged, - required this.childrenDelegate, - this.dragStartBehavior = DragStartBehavior.start, - this.allowImplicitScrolling = false, - ScrollCacheExtent? scrollCacheExtent, - this.restorationId, - this.clipBehavior = Clip.hardEdge, - this.hitTestBehavior = HitTestBehavior.opaque, - this.scrollBehavior, - this.padEnds = true, - required this.horizontalDragGestureRecognizer, - }) : assert( - scrollCacheExtent == null || - (scrollCacheExtent.value > 0.0) == allowImplicitScrolling, - 'scrollCacheExtent and allowImplicitScrolling must be consistent: ' - 'scrollCacheExtent must be greater than 0.0 when allowImplicitScrolling is true, ' - 'and must be 0.0 when allowImplicitScrolling is false.', - ), - scrollCacheExtent = - scrollCacheExtent ?? - ScrollCacheExtent.viewport(allowImplicitScrolling ? 1.0 : 0.0); - - /// {@template flutter.widgets.PageView.allowImplicitScrolling} - /// Controls whether the widget's pages will respond to - /// [RenderObject.showOnScreen], which will allow for implicit accessibility - /// scrolling. - /// - /// With this flag set to false, when accessibility focus reaches the end of - /// the current page and the user attempts to move it to the next element, the - /// focus will traverse to the next widget outside of the page view. - /// - /// With this flag set to true, when accessibility focus reaches the end of - /// the current page and user attempts to move it to the next element, focus - /// will traverse to the next page in the page view. - /// {@endtemplate} - final bool allowImplicitScrolling; - - /// {@macro flutter.rendering.RenderViewportBase.scrollCacheExtent} - /// - /// In [PageView], the default [scrollCacheExtent] uses - /// [ScrollCacheExtent.viewport], where the value represents the number of - /// viewport lengths to cache beyond the visible area. - /// - /// When [PageController.viewportFraction] is 1.0 (the default), this is - /// equivalent to the number of pages. For example, - /// `ScrollCacheExtent.viewport(2.0)` caches 2 pages before and after the - /// visible page. - /// - /// When [PageController.viewportFraction] is less than 1.0, multiple pages - /// may be visible in a single viewport, so `ScrollCacheExtent.viewport(1.0)` - /// may cache more than one additional page in each direction. - /// - /// [ScrollCacheExtent.pixels] can also be used to specify the cache extent - /// in logical pixels instead of viewport sizes. - /// - /// If [scrollCacheExtent] is specified, its value must be consistent with - /// [allowImplicitScrolling]: the value must be greater than 0.0 when - /// [allowImplicitScrolling] is true, and must be 0.0 when - /// [allowImplicitScrolling] is false. - /// - /// Defaults to `ScrollCacheExtent.viewport(1.0)` if - /// [allowImplicitScrolling] is true, and `ScrollCacheExtent.viewport(0.0)` if - /// [allowImplicitScrolling] is false. - final ScrollCacheExtent scrollCacheExtent; - - /// {@macro flutter.widgets.scrollable.restorationId} - final String? restorationId; - - /// The [Axis] along which the scroll view's offset increases with each page. - /// - /// For the direction in which active scrolling may be occurring, see - /// [ScrollDirection]. - /// - /// Defaults to [Axis.horizontal]. - final Axis scrollDirection; - - /// Whether the page view scrolls in the reading direction. - /// - /// For example, if the reading direction is left-to-right and - /// [scrollDirection] is [Axis.horizontal], then the page view scrolls from - /// left to right when [reverse] is false and from right to left when - /// [reverse] is true. - /// - /// Similarly, if [scrollDirection] is [Axis.vertical], then the page view - /// scrolls from top to bottom when [reverse] is false and from bottom to top - /// when [reverse] is true. - /// - /// Defaults to false. - final bool reverse; - - /// An object that can be used to control the position to which this page - /// view is scrolled. - final PageController? controller; - - /// How the page view should respond to user input. - /// - /// For example, determines how the page view continues to animate after the - /// user stops dragging the page view. - /// - /// The physics are modified to snap to page boundaries using - /// [PageScrollPhysics] prior to being used. - /// - /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the - /// [ScrollPhysics] provided by that behavior will take precedence after - /// [physics]. - /// - /// Defaults to matching platform conventions. - final ScrollPhysics? physics; - - /// Set to false to disable page snapping, useful for custom scroll behavior. - /// - /// If the [padEnds] is false and [PageController.viewportFraction] < 1.0, - /// the page will snap to the beginning of the viewport; otherwise, the page - /// will snap to the center of the viewport. - final bool pageSnapping; - - /// Called whenever the page in the center of the viewport changes. - final ValueChanged? onPageChanged; - - /// A delegate that provides the children for the [PageView]. - /// - /// The [PageView.custom] constructor lets you specify this delegate - /// explicitly. The [PageView] and [PageView.builder] constructors create a - /// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder], - /// respectively. - final SliverChildDelegate childrenDelegate; - - /// {@macro flutter.widgets.scrollable.dragStartBehavior} - final DragStartBehavior dragStartBehavior; - - /// {@macro flutter.material.Material.clipBehavior} - /// - /// Defaults to [Clip.hardEdge]. - final Clip clipBehavior; - - /// {@macro flutter.widgets.scrollable.hitTestBehavior} - /// - /// Defaults to [HitTestBehavior.opaque]. - final HitTestBehavior hitTestBehavior; - - /// {@macro flutter.widgets.scrollable.scrollBehavior} - /// - /// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be - /// modified by default to not apply a [Scrollbar]. - final ScrollBehavior? scrollBehavior; - - /// Whether to add padding to both ends of the list. - /// - /// If this is set to true and [PageController.viewportFraction] < 1.0, padding will be added - /// such that the first and last child slivers will be in the center of - /// the viewport when scrolled all the way to the start or end, respectively. - /// - /// If [PageController.viewportFraction] >= 1.0, this property has no effect. - /// - /// This property defaults to true. - final bool padEnds; - - @override - State> createState() => _PageViewState(); -} - -class _PageViewState - extends State> { - int _lastReportedPage = 0; - - late PageController _controller; - - @override - void initState() { - super.initState(); - _initController(); - _lastReportedPage = _controller.initialPage; - } - - @override - void dispose() { - if (widget.controller == null) { - _controller.dispose(); - } - super.dispose(); - } - - void _initController() { - _controller = widget.controller ?? PageController(); - } - - @override - void didUpdateWidget(PageView oldWidget) { - if (oldWidget.controller != widget.controller) { - if (oldWidget.controller == null) { - _controller.dispose(); - } - _initController(); - } - super.didUpdateWidget(oldWidget); - } - - AxisDirection _getDirection(BuildContext context) { - switch (widget.scrollDirection) { - case Axis.horizontal: - assert(debugCheckHasDirectionality(context)); - final TextDirection textDirection = Directionality.of(context); - final AxisDirection axisDirection = textDirectionToAxisDirection( - textDirection, - ); - return widget.reverse - ? flipAxisDirection(axisDirection) - : axisDirection; - case Axis.vertical: - return widget.reverse ? AxisDirection.up : AxisDirection.down; - } - } - - @override - Widget build(BuildContext context) { - final AxisDirection axisDirection = _getDirection(context); - final ScrollPhysics physics = - _ForceImplicitScrollPhysics( - allowImplicitScrolling: widget.allowImplicitScrolling, - ).applyTo( - widget.pageSnapping - ? _kPagePhysics.applyTo( - widget.physics ?? - widget.scrollBehavior?.getScrollPhysics(context), - ) - : widget.physics ?? - widget.scrollBehavior?.getScrollPhysics(context), - ); - - final child = Scrollable( - dragStartBehavior: widget.dragStartBehavior, - axisDirection: axisDirection, - controller: _controller, - physics: physics, - restorationId: widget.restorationId, - hitTestBehavior: widget.hitTestBehavior, - scrollBehavior: - widget.scrollBehavior ?? - ScrollConfiguration.of(context).copyWith(scrollbars: false), - viewportBuilder: (BuildContext context, ViewportOffset position) { - return Viewport( - scrollCacheExtent: widget.scrollCacheExtent, - axisDirection: axisDirection, - offset: position, - clipBehavior: widget.clipBehavior, - slivers: [ - SliverFillViewport( - viewportFraction: _controller.viewportFraction, - delegate: widget.childrenDelegate, - padEnds: widget.padEnds, - allowImplicitScrolling: widget.allowImplicitScrolling, - ), - ], - ); - }, - horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer, - ); - if (widget.onPageChanged != null) { - return NotificationListener( - onNotification: (notification) { - if (notification.depth == 0) { - final metrics = notification.metrics as PageMetrics; - final int currentPage = metrics.page!.round(); - if (currentPage != _lastReportedPage) { - _lastReportedPage = currentPage; - widget.onPageChanged!(currentPage); - } - } - return false; - }, - child: child, - ); - } - return child; - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder description) { - super.debugFillProperties(description); - description - ..add(EnumProperty('scrollDirection', widget.scrollDirection)) - ..add(FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed')) - ..add( - DiagnosticsProperty( - 'controller', - _controller, - showName: false, - ), - ) - ..add( - DiagnosticsProperty( - 'physics', - widget.physics, - showName: false, - ), - ) - ..add( - FlagProperty( - 'pageSnapping', - value: widget.pageSnapping, - ifFalse: 'snapping disabled', - ), - ) - ..add( - FlagProperty( - 'allowImplicitScrolling', - value: widget.allowImplicitScrolling, - ifTrue: 'allow implicit scrolling', - ), - ) - ..add( - DiagnosticsProperty( - 'scrollCacheExtent', - widget.scrollCacheExtent, - defaultValue: ScrollCacheExtent.viewport( - widget.allowImplicitScrolling ? 1.0 : 0.0, - ), - ), - ); - } -} diff --git a/lib/common/widgets/flutter/page/scrollable.dart b/lib/common/widgets/flutter/page/scrollable.dart deleted file mode 100644 index 1abd487d0..000000000 --- a/lib/common/widgets/flutter/page/scrollable.dart +++ /dev/null @@ -1,1857 +0,0 @@ -// 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. - -// ignore_for_file: prefer_initializing_formals - -import 'dart:async'; -import 'dart:math' as math; - -import 'package:PiliPlus/common/widgets/flutter/page/scrollable_helpers.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart' - hide Scrollable, ScrollableState, EdgeDraggingAutoScroller; -import 'package:flutter/rendering.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; - -// The return type of _performEnsureVisible. -// -// The list of futures represents each pending ScrollPosition call to -// ensureVisible. The returned ScrollableState's context is used to find the -// next potential ancestor Scrollable. -typedef _EnsureVisibleResults = (List>, ScrollableState); - -/// A widget that manages scrolling in one dimension and informs the [Viewport] -/// through which the content is viewed. -/// -/// [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. -/// -/// ## Persisting the scroll position during a session -/// -/// Scrollables attempt to persist their scroll position using [PageStorage]. -/// This can be disabled by setting [ScrollController.keepScrollOffset] to false -/// on the [controller]. If it is enabled, using a [PageStorageKey] for the -/// [key] of this widget (or one of its ancestors, e.g. a [ScrollView]) is -/// recommended to help disambiguate different [Scrollable]s from each other. -/// -/// 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. - const Scrollable({ - 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.restorationId, - this.scrollBehavior, - this.clipBehavior = Clip.hardEdge, - this.hitTestBehavior = HitTestBehavior.opaque, - required this.horizontalDragGestureRecognizer, - }) : assert(semanticChildCount == null || semanticChildCount >= 0); - - final GestureRecognizerFactoryConstructor horizontalDragGestureRecognizer; - - /// {@template flutter.widgets.Scrollable.axisDirection} - /// The direction in which this widget scrolls. - /// - /// 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. - /// - /// 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]). - /// - /// If null, a [ScrollController] will be created internally by [Scrollable] - /// in order to create and manage the [ScrollPosition]. - /// - /// See also: - /// - /// * [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 - /// user stops dragging the scroll view. - /// - /// Defaults to matching platform conventions via the physics provided from - /// the ambient [ScrollConfiguration]. - /// - /// 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 - /// 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. - /// {@endtemplate} - 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; - - /// {@template flutter.widgets.Scrollable.incrementCalculator} - /// 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. - /// {@endtemplate} - final ScrollIncrementCalculator? incrementCalculator; - - /// {@template flutter.widgets.scrollable.excludeFromSemantics} - /// 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. - /// {@endtemplate} - /// - /// See also: - /// - /// * [GestureDetector.excludeFromSemantics], which is used to accomplish the - /// exclusion. - final bool excludeFromSemantics; - - /// {@template flutter.widgets.scrollable.hitTestBehavior} - /// Defines the behavior of gesture detector used in this [Scrollable]. - /// - /// This defaults to [HitTestBehavior.opaque] which means it prevents targets - /// behind this [Scrollable] from receiving events. - /// {@endtemplate} - /// - /// See also: - /// - /// * [HitTestBehavior], for an explanation on different behaviors. - final HitTestBehavior hitTestBehavior; - - /// 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 at the position where the drag gesture won the arena. If set to - /// [DragStartBehavior.down] it will begin at the position where 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; - - /// {@template flutter.widgets.scrollable.restorationId} - /// Restoration ID to save and restore the scroll offset of the scrollable. - /// - /// If a restoration id is provided, the scrollable will persist its current - /// scroll offset and restore it during state restoration. - /// - /// The scroll offset is persisted in a [RestorationBucket] claimed from - /// the surrounding [RestorationScope] using the provided restoration ID. - /// - /// See also: - /// - /// * [RestorationManager], which explains how state restoration works in - /// Flutter. - /// {@endtemplate} - final String? restorationId; - - /// {@template flutter.widgets.scrollable.scrollBehavior} - /// A [ScrollBehavior] that will be applied to this widget individually. - /// - /// Defaults to null, wherein the inherited [ScrollBehavior] is copied and - /// modified to alter the viewport decoration, like [Scrollbar]s. - /// - /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit - /// [ScrollPhysics] is provided in [physics], it will take precedence, - /// followed by [scrollBehavior], and then the inherited ancestor - /// [ScrollBehavior]. - /// {@endtemplate} - final ScrollBehavior? scrollBehavior; - - /// {@macro flutter.material.Material.clipBehavior} - /// - /// Defaults to [Clip.hardEdge]. - /// - /// This is passed to decorators in [ScrollableDetails], and does not directly affect - /// clipping of the [Scrollable]. This reflects the same [Clip] that is provided - /// to [ScrollView.clipBehavior] and is supplied to the [Viewport]. - final Clip clipBehavior; - - /// 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)) - ..add(DiagnosticsProperty('physics', physics)) - ..add(StringProperty('restorationId', restorationId)); - } - - /// The state from the closest instance of this class that encloses the given - /// context, or null if none is found. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// ScrollableState? scrollable = Scrollable.maybeOf(context); - /// ``` - /// - /// Calling this method will create a dependency on the [ScrollableState] - /// that is returned, if there is one. This is typically the closest - /// [Scrollable], but may be a more distant ancestor if [axis] is used to - /// target a specific [Scrollable]. - /// - /// Using the optional [Axis] is useful when Scrollables are nested and the - /// target [Scrollable] is not the closest instance. When [axis] is provided, - /// the nearest enclosing [ScrollableState] in that [Axis] is returned, or - /// null if there is none. - /// - /// This finds the nearest _ancestor_ [Scrollable] of the `context`. This - /// means that if the `context` is that of a [Scrollable], it will _not_ find - /// _that_ [Scrollable]. - /// - /// See also: - /// - /// * [Scrollable.of], which is similar to this method, but asserts - /// if no [Scrollable] ancestor is found. - static ScrollableState? maybeOf(BuildContext context, {Axis? axis}) { - // This is the context that will need to establish the dependency. - final originalContext = context; - InheritedElement? element = context - .getElementForInheritedWidgetOfExactType<_ScrollableScope>(); - while (element != null) { - final ScrollableState scrollable = - (element.widget as _ScrollableScope).scrollable; - if (axis == null || - axisDirectionToAxis(scrollable.axisDirection) == axis) { - // Establish the dependency on the correct context. - originalContext.dependOnInheritedElement(element); - return scrollable; - } - context = scrollable.context; - element = context - .getElementForInheritedWidgetOfExactType<_ScrollableScope>(); - } - return null; - } - - /// 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); - /// ``` - /// - /// Calling this method will create a dependency on the [ScrollableState] - /// that is returned, if there is one. This is typically the closest - /// [Scrollable], but may be a more distant ancestor if [axis] is used to - /// target a specific [Scrollable]. - /// - /// Using the optional [Axis] is useful when Scrollables are nested and the - /// target [Scrollable] is not the closest instance. When [axis] is provided, - /// the nearest enclosing [ScrollableState] in that [Axis] is returned. - /// - /// This finds the nearest _ancestor_ [Scrollable] of the `context`. This - /// means that if the `context` is that of a [Scrollable], it will _not_ find - /// _that_ [Scrollable]. - /// - /// If no [Scrollable] ancestor is found, then this method will assert in - /// debug mode, and throw an exception in release mode. - /// - /// See also: - /// - /// * [Scrollable.maybeOf], which is similar to this method, but returns null - /// if no [Scrollable] ancestor is found. - static ScrollableState of(BuildContext context, {Axis? axis}) { - final ScrollableState? scrollableState = maybeOf(context, axis: axis); - assert(() { - if (scrollableState == null) { - throw FlutterError.fromParts([ - ErrorSummary( - 'Scrollable.of() was called with a context that does not contain a ' - 'Scrollable widget.', - ), - ErrorDescription( - 'No Scrollable widget ancestor could be found ' - '${axis == null ? '' : 'for the provided Axis: $axis '}' - 'starting from the context that was passed to Scrollable.of(). This ' - 'can happen because you are using a widget that looks for a Scrollable ' - 'ancestor, but no such ancestor exists.\n' - 'The context used was:\n' - ' $context', - ), - if (axis != null) - ErrorHint( - 'When specifying an axis, this method will only look for a Scrollable ' - 'that matches the given Axis.', - ), - ]); - } - return true; - }()); - return scrollableState!; - } - - /// Provides a heuristic to determine if expensive frame-bound tasks should be - /// deferred for the [context] at a specific point in time. - /// - /// Calling this method does _not_ create a dependency on any other widget. - /// This also means that the value returned is only good for the point in time - /// when it is called, and callers will not get updated if the value changes. - /// - /// The heuristic used is determined by the [physics] of this [Scrollable] - /// via [ScrollPhysics.recommendDeferredLoading]. That method is called with - /// the current [ScrollPosition.activity]'s [ScrollActivity.velocity]. - /// - /// The optional [Axis] allows targeting of a specific [Scrollable] of that - /// axis, useful when Scrollables are nested. When [axis] is provided, - /// [ScrollPosition.recommendDeferredLoading] is called for the nearest - /// [Scrollable] in that [Axis]. - /// - /// If there is no [Scrollable] in the widget tree above the [context], this - /// method returns false. - static bool recommendDeferredLoadingForContext( - BuildContext context, { - Axis? axis, - }) { - _ScrollableScope? widget = context - .getInheritedWidgetOfExactType<_ScrollableScope>(); - while (widget != null) { - if (axis == null || - axisDirectionToAxis(widget.scrollable.axisDirection) == axis) { - return widget.position.recommendDeferredLoading(context); - } - context = widget.scrollable.context; - widget = context.getInheritedWidgetOfExactType<_ScrollableScope>(); - } - return false; - } - - /// Scrolls all scrollables that enclose the given context so as to make the - /// given context visible. - /// - /// If a [Scrollable] enclosing the provided [BuildContext] is a - /// [TwoDimensionalScrollable], both vertical and horizontal axes will ensure - /// the target is made visible. - static Future ensureVisible( - BuildContext context, { - double alignment = 0.0, - Duration duration = Duration.zero, - Curve curve = Curves.ease, - ScrollPositionAlignmentPolicy alignmentPolicy = - ScrollPositionAlignmentPolicy.explicit, - }) { - final futures = >[]; - - // The targetRenderObject is used to record the first target renderObject. - // If there are multiple scrollable widgets nested, the targetRenderObject - // is made to be as visible as possible to improve the user experience. If - // the targetRenderObject is already visible, then let the outer - // renderObject be as visible as possible. - // - // Also see https://github.com/flutter/flutter/issues/65100 - RenderObject? targetRenderObject; - ScrollableState? scrollable = Scrollable.maybeOf(context); - while (scrollable != null) { - final List> newFutures; - (newFutures, scrollable) = scrollable._performEnsureVisible( - context.findRenderObject()!, - alignment: alignment, - duration: duration, - curve: curve, - alignmentPolicy: alignmentPolicy, - targetRenderObject: targetRenderObject, - ); - futures.addAll(newFutures); - - targetRenderObject ??= context.findRenderObject(); - context = scrollable.context; - scrollable = Scrollable.maybeOf(context); - } - - if (futures.isEmpty || duration == Duration.zero) { - return Future.value(); - } - if (futures.length == 1) { - return futures.single; - } - return Future.wait(futures).then((List _) => 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({ - required this.scrollable, - required this.position, - required super.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> - with TickerProviderStateMixin, RestorationMixin - implements ScrollContext { - // GETTERS - - /// 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; - - /// The resolved [ScrollPhysics] of the [ScrollableState]. - ScrollPhysics? get resolvedPhysics => _physics; - ScrollPhysics? _physics; - - /// An [Offset] that represents the absolute distance from the origin, or 0, - /// of the [ScrollPosition] expressed in the associated [Axis]. - /// - /// Used by [EdgeDraggingAutoScroller] to progress the position forward when a - /// drag gesture reaches the edge of the [Viewport]. - Offset get deltaToScrollOrigin => switch (axisDirection) { - AxisDirection.up => Offset(0, -position.pixels), - AxisDirection.down => Offset(0, position.pixels), - AxisDirection.left => Offset(-position.pixels, 0), - AxisDirection.right => Offset(position.pixels, 0), - }; - - ScrollController get _effectiveScrollController => - widget.controller ?? _fallbackScrollController!; - - @override - AxisDirection get axisDirection => widget.axisDirection; - - @override - TickerProvider get vsync => this; - - @override - double get devicePixelRatio => _devicePixelRatio; - late double _devicePixelRatio; - - @override - BuildContext? get notificationContext => _gestureDetectorKey.currentContext; - - @override - BuildContext get storageContext => context; - - @override - String? get restorationId => widget.restorationId; - final _RestorableScrollOffset _persistedScrollOffset = - _RestorableScrollOffset(); - - late ScrollBehavior _configuration; - ScrollController? _fallbackScrollController; - DeviceGestureSettings? _mediaQueryGestureSettings; - - // Only call this from places that will definitely trigger a rebuild. - void _updatePosition() { - _configuration = widget.scrollBehavior ?? ScrollConfiguration.of(context); - final ScrollPhysics? physicsFromWidget = - widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context); - _physics = _configuration.getScrollPhysics(context); - _physics = physicsFromWidget?.applyTo(_physics) ?? _physics; - - final ScrollPosition? oldPosition = _position; - if (oldPosition != null) { - _effectiveScrollController.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 = _effectiveScrollController.createScrollPosition( - _physics!, - this, - oldPosition, - ); - assert(_position != null); - _effectiveScrollController.attach(position); - } - - @protected - @override - void restoreState(RestorationBucket? oldBucket, bool initialRestore) { - registerForRestoration(_persistedScrollOffset, 'offset'); - assert(_position != null); - if (_persistedScrollOffset.value != null) { - position.restoreOffset( - _persistedScrollOffset.value!, - initialRestore: initialRestore, - ); - } - } - - @protected - @override - void saveOffset(double offset) { - assert(debugIsSerializableForRestoration(offset)); - _persistedScrollOffset.value = offset; - // [saveOffset] is called after a scrolling ends and it is usually not - // followed by a frame. Therefore, manually flush restoration data. - ServicesBinding.instance.restorationManager.flushData(); - } - - @protected - @override - void initState() { - if (widget.controller == null) { - _fallbackScrollController = ScrollController(); - } - super.initState(); - } - - @protected - @override - void didChangeDependencies() { - _mediaQueryGestureSettings = MediaQuery.maybeGestureSettingsOf(context); - _devicePixelRatio = - MediaQuery.maybeDevicePixelRatioOf(context) ?? - View.of(context).devicePixelRatio; - _updatePosition(); - super.didChangeDependencies(); - } - - bool _shouldUpdatePosition(Scrollable oldWidget) { - if ((widget.scrollBehavior == null) != (oldWidget.scrollBehavior == null)) { - return true; - } - if (widget.scrollBehavior != null && - oldWidget.scrollBehavior != null && - widget.scrollBehavior!.shouldNotify(oldWidget.scrollBehavior!)) { - return true; - } - ScrollPhysics? newPhysics = - widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context); - ScrollPhysics? oldPhysics = - oldWidget.physics ?? - oldWidget.scrollBehavior?.getScrollPhysics(context); - 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; - } - - @protected - @override - void didUpdateWidget(Scrollable oldWidget) { - super.didUpdateWidget(oldWidget); - - if (widget.controller != oldWidget.controller) { - if (oldWidget.controller == null) { - // The old controller was null, meaning the fallback cannot be null. - // Dispose of the fallback. - assert(_fallbackScrollController != null); - assert(widget.controller != null); - _fallbackScrollController!.detach(position); - _fallbackScrollController!.dispose(); - _fallbackScrollController = null; - } else { - // The old controller was not null, detach. - oldWidget.controller?.detach(position); - if (widget.controller == null) { - // If the new controller is null, we need to set up the fallback - // ScrollController. - _fallbackScrollController = ScrollController(); - } - } - // Attach the updated effective scroll controller. - _effectiveScrollController.attach(position); - } - - if (_shouldUpdatePosition(oldWidget)) { - _updatePosition(); - } - } - - @protected - @override - void dispose() { - if (widget.controller != null) { - widget.controller!.detach(position); - } else { - _fallbackScrollController?.detach(position); - _fallbackScrollController?.dispose(); - } - - position.dispose(); - _persistedScrollOffset.dispose(); - super.dispose(); - } - - // SEMANTICS - - final GlobalKey _scrollSemanticsKey = GlobalKey(); - - @override - @protected - void setSemanticsActions(Set actions) { - if (_gestureDetectorKey.currentState != null) { - _gestureDetectorKey.currentState!.replaceSemanticsActions(actions); - } - } - - // GESTURE RECOGNITION AND POINTER IGNORING - - final GlobalKey _gestureDetectorKey = - GlobalKey(); - final GlobalKey _ignorePointerKey = GlobalKey(); - - // This field is set during layout, and then reused until the next time it is set. - Map _gestureRecognizers = - const {}; - bool _shouldIgnorePointer = false; - - bool? _lastCanDrag; - Axis? _lastAxisDirection; - - @override - @protected - void setCanDrag(bool value) { - if (value == _lastCanDrag && - (!value || widget.axis == _lastAxisDirection)) { - return; - } - if (!value) { - _gestureRecognizers = const {}; - // Cancel the active hold/drag (if any) because the gesture recognizers - // will soon be disposed by our RawGestureDetector, and we won't be - // receiving pointer up events to cancel the hold/drag. - _handleDragCancel(); - } else { - switch (widget.axis) { - case Axis.vertical: - _gestureRecognizers = { - VerticalDragGestureRecognizer: - GestureRecognizerFactoryWithHandlers< - VerticalDragGestureRecognizer - >( - () => VerticalDragGestureRecognizer( - supportedDevices: _configuration.dragDevices, - ), - (VerticalDragGestureRecognizer instance) { - instance - ..onDown = _handleDragDown - ..onStart = _handleDragStart - ..onUpdate = _handleDragUpdate - ..onEnd = _handleDragEnd - ..onCancel = _handleDragCancel - ..minFlingDistance = _physics?.minFlingDistance - ..minFlingVelocity = _physics?.minFlingVelocity - ..maxFlingVelocity = _physics?.maxFlingVelocity - ..velocityTrackerBuilder = _configuration - .velocityTrackerBuilder(context) - ..dragStartBehavior = widget.dragStartBehavior - ..multitouchDragStrategy = _configuration - .getMultitouchDragStrategy(context) - ..gestureSettings = _mediaQueryGestureSettings - ..supportedDevices = _configuration.dragDevices; - }, - ), - }; - case Axis.horizontal: - _gestureRecognizers = { - T: GestureRecognizerFactoryWithHandlers( - widget.horizontalDragGestureRecognizer, - (T instance) { - instance - ..onDown = _handleDragDown - ..onStart = _handleDragStart - ..onUpdate = _handleDragUpdate - ..onEnd = _handleDragEnd - ..onCancel = _handleDragCancel - ..minFlingDistance = _physics?.minFlingDistance - ..minFlingVelocity = _physics?.minFlingVelocity - ..maxFlingVelocity = _physics?.maxFlingVelocity - ..velocityTrackerBuilder = _configuration - .velocityTrackerBuilder(context) - ..dragStartBehavior = widget.dragStartBehavior - ..multitouchDragStrategy = _configuration - .getMultitouchDragStrategy(context) - ..gestureSettings = _mediaQueryGestureSettings - ..supportedDevices = _configuration.dragDevices; - }, - ), - }; - } - } - _lastCanDrag = value; - _lastAxisDirection = widget.axis; - if (_gestureDetectorKey.currentState != null) { - _gestureDetectorKey.currentState!.replaceGestureRecognizers( - _gestureRecognizers, - ); - } - } - - @override - @protected - void setIgnorePointer(bool value) { - if (_shouldIgnorePointer == value) { - return; - } - _shouldIgnorePointer = value; - if (_ignorePointerKey.currentContext != null) { - final renderBox = - _ignorePointerKey.currentContext!.findRenderObject()! - as RenderIgnorePointer; - renderBox.ignoring = _shouldIgnorePointer; - } - } - - // 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); - // _hold might be non-null if the scroll position is currently animating. - if (_hold != null) { - _disposeHold(); - } - } - - 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() { - if (_gestureDetectorKey.currentContext == null) { - // The cancel was caused by the GestureDetector getting disposed, which - // means we will get disposed momentarily as well and shouldn't do - // any work. - return; - } - // _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(double delta) { - return math.min( - math.max(position.pixels + delta, position.minScrollExtent), - position.maxScrollExtent, - ); - } - - // Returns the delta that should result from applying [event] with axis, - // direction, and any modifiers specified by the ScrollBehavior taken into - // account. - double _pointerSignalEventDelta(PointerScrollEvent event) { - final Set pressed = - HardwareKeyboard.instance.logicalKeysPressed; - final bool flipAxes = - pressed.any(_configuration.pointerAxisModifiers.contains) && - // Axes are only flipped for physical mouse wheel input. - // On some platforms, like web, trackpad input is handled through pointer - // signals, but should not be included in this axis modifying behavior. - // This is because on a trackpad, all directional axes are available to - // the user, while mouse scroll wheels typically are restricted to one - // axis. - event.kind == PointerDeviceKind.mouse; - - final Axis axis = flipAxes ? flipAxis(widget.axis) : widget.axis; - final double delta = switch (axis) { - Axis.horizontal => event.scrollDelta.dx, - Axis.vertical => event.scrollDelta.dy, - }; - - return axisDirectionIsReversed(widget.axisDirection) ? -delta : delta; - } - - void _receivedPointerSignal(PointerSignalEvent event) { - if (event is PointerScrollEvent && _position != null) { - if (_physics != null && !_physics!.shouldAcceptUserOffset(position)) { - return; - } - final double delta = _pointerSignalEventDelta(event); - final double targetScrollOffset = _targetScrollOffsetForPointerScroll( - delta, - ); - // Only express interest in the event if it would actually result in a scroll. - if (delta != 0.0 && targetScrollOffset != position.pixels) { - GestureBinding.instance.pointerSignalResolver.register( - event, - _handlePointerScroll, - ); - return; - } - } else if (event is PointerScrollInertiaCancelEvent) { - position.pointerScroll(0); - // Don't use the pointer signal resolver, all hit-tested scrollables should stop. - } - } - - void _handlePointerScroll(PointerEvent event) { - assert(event is PointerScrollEvent); - final scrollEvent = event as PointerScrollEvent; - final double delta = _pointerSignalEventDelta(scrollEvent); - final double targetScrollOffset = _targetScrollOffsetForPointerScroll( - delta, - ); - if (delta != 0.0 && targetScrollOffset != position.pixels) { - position.pointerScroll(delta); - // Tell engine this scrollable handled the event. - // This prevents parent page from scrolling when nested scrollables exist. - scrollEvent.respond(allowPlatformDefault: false); - } - } - - bool _handleScrollMetricsNotification( - ScrollMetricsNotification notification, - ) { - if (notification.depth == 0) { - final RenderObject? scrollSemanticsRenderObject = _scrollSemanticsKey - .currentContext - ?.findRenderObject(); - if (scrollSemanticsRenderObject != null) { - scrollSemanticsRenderObject.markNeedsSemanticsUpdate(); - } - } - return false; - } - - Widget _buildChrome(BuildContext context, Widget child) { - final details = ScrollableDetails( - direction: widget.axisDirection, - controller: _effectiveScrollController, - decorationClipBehavior: widget.clipBehavior, - ); - - return _configuration.buildScrollbar( - context, - _configuration.buildOverscrollIndicator(context, child, details), - details, - ); - } - - // DESCRIPTION - - @protected - @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, - 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), - ), - ), - ), - ), - ); - - if (!widget.excludeFromSemantics) { - result = NotificationListener( - onNotification: _handleScrollMetricsNotification, - child: _ScrollSemantics( - key: _scrollSemanticsKey, - position: position, - allowImplicitScrolling: _physics!.allowImplicitScrolling, - axis: widget.axis, - semanticChildCount: widget.semanticChildCount, - child: result, - ), - ); - } - - result = _buildChrome(context, result); - - // Selection is only enabled when there is a parent registrar. - final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context); - if (registrar != null) { - result = _ScrollableSelectionHandler( - state: this, - position: position, - registrar: registrar, - child: result, - ); - } - - return result; - } - - // Returns the Future from calling ensureVisible for the ScrollPosition, as - // as well as this ScrollableState instance so its context can be used to - // check for other ancestor Scrollables in executing ensureVisible. - _EnsureVisibleResults _performEnsureVisible( - RenderObject object, { - double alignment = 0.0, - Duration duration = Duration.zero, - Curve curve = Curves.ease, - ScrollPositionAlignmentPolicy alignmentPolicy = - ScrollPositionAlignmentPolicy.explicit, - RenderObject? targetRenderObject, - }) { - final Future ensureVisibleFuture = position.ensureVisible( - object, - alignment: alignment, - duration: duration, - curve: curve, - alignmentPolicy: alignmentPolicy, - targetRenderObject: targetRenderObject, - ); - return (>[ensureVisibleFuture], this); - } - - @protected - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('position', _position)) - ..add(DiagnosticsProperty('effective physics', _physics)); - } -} - -/// A widget to handle selection for a scrollable. -/// -/// This widget registers itself to the [registrar] and uses -/// [SelectionContainer] to collect selectables from its subtree. -class _ScrollableSelectionHandler extends StatefulWidget { - const _ScrollableSelectionHandler({ - required this.state, - required this.position, - required this.registrar, - required this.child, - }); - - final ScrollableState state; - final ScrollPosition position; - final Widget child; - final SelectionRegistrar registrar; - - @override - _ScrollableSelectionHandlerState createState() => - _ScrollableSelectionHandlerState(); -} - -class _ScrollableSelectionHandlerState - extends State<_ScrollableSelectionHandler> { - late _ScrollableSelectionContainerDelegate _selectionDelegate; - - @override - void initState() { - super.initState(); - _selectionDelegate = _ScrollableSelectionContainerDelegate( - state: widget.state, - position: widget.position, - ); - } - - @override - void didUpdateWidget(_ScrollableSelectionHandler oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.position != widget.position) { - _selectionDelegate.position = widget.position; - } - } - - @override - void dispose() { - _selectionDelegate.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SelectionContainer( - registrar: widget.registrar, - delegate: _selectionDelegate, - child: widget.child, - ); - } -} - -/// This updater handles the case where the selectables change frequently, and -/// it optimizes toward scrolling updates. -/// -/// It keeps track of the drag start offset relative to scroll origin for every -/// selectable. The records are used to determine whether the selection is up to -/// date with the scroll position when it sends the drag update event to a -/// selectable. -class _ScrollableSelectionContainerDelegate - extends MultiSelectableSelectionContainerDelegate { - _ScrollableSelectionContainerDelegate({ - required this.state, - required ScrollPosition position, - }) : _position = position, - _autoScroller = EdgeDraggingAutoScroller( - state, - velocityScalar: _kDefaultSelectToScrollVelocityScalar, - ) { - _position.addListener(_scheduleLayoutChange); - } - - // Pointer drag is a single point, it should not have a size. - static const double _kDefaultDragTargetSize = 0; - - // An eye-balled value for a smooth scrolling speed. - static const double _kDefaultSelectToScrollVelocityScalar = 30; - - final ScrollableState state; - final EdgeDraggingAutoScroller _autoScroller; - bool _scheduledLayoutChange = false; - Offset? _currentDragStartRelatedToOrigin; - Offset? _currentDragEndRelatedToOrigin; - - // The scrollable only auto scrolls if the selection starts in the scrollable. - bool _selectionStartsInScrollable = false; - - ScrollPosition get position => _position; - ScrollPosition _position; - set position(ScrollPosition other) { - if (other == _position) { - return; - } - _position.removeListener(_scheduleLayoutChange); - _position = other; - _position.addListener(_scheduleLayoutChange); - } - - // The layout will only be updated a frame later than position changes. - // Schedule PostFrameCallback to capture the accurate layout. - void _scheduleLayoutChange() { - if (_scheduledLayoutChange) { - return; - } - _scheduledLayoutChange = true; - SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { - if (!_scheduledLayoutChange) { - return; - } - _scheduledLayoutChange = false; - layoutDidChange(); - }, debugLabel: 'ScrollableSelectionContainer.layoutDidChange'); - } - - /// Stores the scroll offset when a scrollable receives the last - /// [SelectionEdgeUpdateEvent]. - /// - /// The stored scroll offset may be null if a scrollable never receives a - /// [SelectionEdgeUpdateEvent]. - /// - /// When a new [SelectionEdgeUpdateEvent] is dispatched to a selectable, this - /// updater checks the current scroll offset against the one stored in these - /// records. If the scroll offset is different, it synthesizes an opposite - /// [SelectionEdgeUpdateEvent] and dispatches the event before dispatching the - /// new event. - /// - /// For example, if a selectable receives an end [SelectionEdgeUpdateEvent] - /// and its scroll offset in the records is different from the current value, - /// it synthesizes a start [SelectionEdgeUpdateEvent] and dispatches it before - /// dispatching the original end [SelectionEdgeUpdateEvent]. - final Map _selectableStartEdgeUpdateRecords = - {}; - final Map _selectableEndEdgeUpdateRecords = - {}; - - @override - void didChangeSelectables() { - final Set selectableSet = selectables.toSet(); - _selectableStartEdgeUpdateRecords.removeWhere( - (Selectable key, double value) => !selectableSet.contains(key), - ); - _selectableEndEdgeUpdateRecords.removeWhere( - (Selectable key, double value) => !selectableSet.contains(key), - ); - super.didChangeSelectables(); - } - - @override - SelectionResult handleClearSelection(ClearSelectionEvent event) { - _selectableStartEdgeUpdateRecords.clear(); - _selectableEndEdgeUpdateRecords.clear(); - _currentDragStartRelatedToOrigin = null; - _currentDragEndRelatedToOrigin = null; - _selectionStartsInScrollable = false; - return super.handleClearSelection(event); - } - - @override - SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) { - if (_currentDragEndRelatedToOrigin == null && - _currentDragStartRelatedToOrigin == null) { - assert(!_selectionStartsInScrollable); - _selectionStartsInScrollable = _globalPositionInScrollable( - event.globalPosition, - ); - } - final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); - if (event.type == SelectionEventType.endEdgeUpdate) { - _currentDragEndRelatedToOrigin = _inferPositionRelatedToOrigin( - event.globalPosition, - ); - final Offset endOffset = _currentDragEndRelatedToOrigin!.translate( - -deltaToOrigin.dx, - -deltaToOrigin.dy, - ); - event = SelectionEdgeUpdateEvent.forEnd( - globalPosition: endOffset, - granularity: event.granularity, - ); - } else { - _currentDragStartRelatedToOrigin = _inferPositionRelatedToOrigin( - event.globalPosition, - ); - final Offset startOffset = _currentDragStartRelatedToOrigin!.translate( - -deltaToOrigin.dx, - -deltaToOrigin.dy, - ); - event = SelectionEdgeUpdateEvent.forStart( - globalPosition: startOffset, - granularity: event.granularity, - ); - } - final SelectionResult result = super.handleSelectionEdgeUpdate(event); - - // Result may be pending if one of the selectable child is also a scrollable. - // In that case, the parent scrollable needs to wait for the child to finish - // scrolling. - if (result == SelectionResult.pending) { - _autoScroller.stopAutoScroll(); - return result; - } - if (_selectionStartsInScrollable) { - _autoScroller.startAutoScrollIfNecessary(_dragTargetFromEvent(event)); - if (_autoScroller.scrolling) { - return SelectionResult.pending; - } - } - return result; - } - - Offset _inferPositionRelatedToOrigin(Offset globalPosition) { - final box = state.context.findRenderObject()! as RenderBox; - final Offset localPosition = box.globalToLocal(globalPosition); - if (!_selectionStartsInScrollable) { - // If the selection starts outside of the scrollable, selecting across the - // scrollable boundary will act as selecting the entire content in the - // scrollable. This logic move the offset to the 0.0 or infinity to cover - // the entire content if the input position is outside of the scrollable. - if (localPosition.dy < 0 || localPosition.dx < 0) { - return box.localToGlobal(Offset.zero); - } - if (localPosition.dy > box.size.height || - localPosition.dx > box.size.width) { - return Offset.infinite; - } - } - final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); - return box.localToGlobal( - localPosition.translate(deltaToOrigin.dx, deltaToOrigin.dy), - ); - } - - /// Infers the [_currentDragStartRelatedToOrigin] and - /// [_currentDragEndRelatedToOrigin] from the geometry. - /// - /// This method is called after a select word and select all event where the - /// selection is triggered by none drag events. The - /// [_currentDragStartRelatedToOrigin] and [_currentDragEndRelatedToOrigin] - /// are essential to handle future [SelectionEdgeUpdateEvent]s. - void _updateDragLocationsFromGeometries({ - bool forceUpdateStart = true, - bool forceUpdateEnd = true, - }) { - final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); - final box = state.context.findRenderObject()! as RenderBox; - final Matrix4 transform = box.getTransformTo(null); - if (currentSelectionStartIndex != -1 && - (_currentDragStartRelatedToOrigin == null || forceUpdateStart)) { - final SelectionGeometry geometry = - selectables[currentSelectionStartIndex].value; - assert(geometry.hasSelection); - final SelectionPoint start = geometry.startSelectionPoint!; - final Matrix4 childTransform = selectables[currentSelectionStartIndex] - .getTransformTo(box); - final Offset localDragStart = MatrixUtils.transformPoint( - childTransform, - start.localPosition + Offset(0, -start.lineHeight / 2), - ); - _currentDragStartRelatedToOrigin = MatrixUtils.transformPoint( - transform, - localDragStart + deltaToOrigin, - ); - } - if (currentSelectionEndIndex != -1 && - (_currentDragEndRelatedToOrigin == null || forceUpdateEnd)) { - final SelectionGeometry geometry = - selectables[currentSelectionEndIndex].value; - assert(geometry.hasSelection); - final SelectionPoint end = geometry.endSelectionPoint!; - final Matrix4 childTransform = selectables[currentSelectionEndIndex] - .getTransformTo(box); - final Offset localDragEnd = MatrixUtils.transformPoint( - childTransform, - end.localPosition + Offset(0, -end.lineHeight / 2), - ); - _currentDragEndRelatedToOrigin = MatrixUtils.transformPoint( - transform, - localDragEnd + deltaToOrigin, - ); - } - } - - @override - SelectionResult handleSelectAll(SelectAllSelectionEvent event) { - assert(!_selectionStartsInScrollable); - final SelectionResult result = super.handleSelectAll(event); - assert( - (currentSelectionStartIndex == -1) == (currentSelectionEndIndex == -1), - ); - if (currentSelectionStartIndex != -1) { - _updateDragLocationsFromGeometries(); - } - return result; - } - - @override - SelectionResult handleSelectWord(SelectWordSelectionEvent event) { - _selectionStartsInScrollable = _globalPositionInScrollable( - event.globalPosition, - ); - final SelectionResult result = super.handleSelectWord(event); - _updateDragLocationsFromGeometries(); - return result; - } - - @override - SelectionResult handleGranularlyExtendSelection( - GranularlyExtendSelectionEvent event, - ) { - final SelectionResult result = super.handleGranularlyExtendSelection(event); - // The selection geometry may not have the accurate offset for the edges - // that are outside of the viewport whose transform may not be valid. Only - // the edge this event is updating is sure to be accurate. - _updateDragLocationsFromGeometries( - forceUpdateStart: !event.isEnd, - forceUpdateEnd: event.isEnd, - ); - if (_selectionStartsInScrollable) { - _jumpToEdge(event.isEnd); - } - return result; - } - - @override - SelectionResult handleDirectionallyExtendSelection( - DirectionallyExtendSelectionEvent event, - ) { - final SelectionResult result = super.handleDirectionallyExtendSelection( - event, - ); - // The selection geometry may not have the accurate offset for the edges - // that are outside of the viewport whose transform may not be valid. Only - // the edge this event is updating is sure to be accurate. - _updateDragLocationsFromGeometries( - forceUpdateStart: !event.isEnd, - forceUpdateEnd: event.isEnd, - ); - if (_selectionStartsInScrollable) { - _jumpToEdge(event.isEnd); - } - return result; - } - - void _jumpToEdge(bool isExtent) { - final Selectable selectable; - final double? lineHeight; - final SelectionPoint? edge; - if (isExtent) { - selectable = selectables[currentSelectionEndIndex]; - edge = selectable.value.endSelectionPoint; - lineHeight = selectable.value.endSelectionPoint!.lineHeight; - } else { - selectable = selectables[currentSelectionStartIndex]; - edge = selectable.value.startSelectionPoint; - lineHeight = selectable.value.startSelectionPoint?.lineHeight; - } - if (lineHeight == null || edge == null) { - return; - } - final scrollableBox = state.context.findRenderObject()! as RenderBox; - final Matrix4 transform = selectable.getTransformTo(scrollableBox); - final Offset edgeOffsetInScrollableCoordinates = MatrixUtils.transformPoint( - transform, - edge.localPosition, - ); - final scrollableRect = Rect.fromLTRB( - 0, - 0, - scrollableBox.size.width, - scrollableBox.size.height, - ); - switch (state.axisDirection) { - case AxisDirection.up: - final double edgeBottom = edgeOffsetInScrollableCoordinates.dy; - final double edgeTop = - edgeOffsetInScrollableCoordinates.dy - lineHeight; - if (edgeBottom >= scrollableRect.bottom && - edgeTop <= scrollableRect.top) { - return; - } - if (edgeBottom > scrollableRect.bottom) { - position.jumpTo(position.pixels + scrollableRect.bottom - edgeBottom); - return; - } - if (edgeTop < scrollableRect.top) { - position.jumpTo(position.pixels + scrollableRect.top - edgeTop); - } - return; - case AxisDirection.right: - final double edge = edgeOffsetInScrollableCoordinates.dx; - if (edge >= scrollableRect.right && edge <= scrollableRect.left) { - return; - } - if (edge > scrollableRect.right) { - position.jumpTo(position.pixels + edge - scrollableRect.right); - return; - } - if (edge < scrollableRect.left) { - position.jumpTo(position.pixels + edge - scrollableRect.left); - } - return; - case AxisDirection.down: - final double edgeBottom = edgeOffsetInScrollableCoordinates.dy; - final double edgeTop = - edgeOffsetInScrollableCoordinates.dy - lineHeight; - if (edgeBottom >= scrollableRect.bottom && - edgeTop <= scrollableRect.top) { - return; - } - if (edgeBottom > scrollableRect.bottom) { - position.jumpTo(position.pixels + edgeBottom - scrollableRect.bottom); - return; - } - if (edgeTop < scrollableRect.top) { - position.jumpTo(position.pixels + edgeTop - scrollableRect.top); - } - return; - case AxisDirection.left: - final double edge = edgeOffsetInScrollableCoordinates.dx; - if (edge >= scrollableRect.right && edge <= scrollableRect.left) { - return; - } - if (edge > scrollableRect.right) { - position.jumpTo(position.pixels + scrollableRect.right - edge); - return; - } - if (edge < scrollableRect.left) { - position.jumpTo(position.pixels + scrollableRect.left - edge); - } - return; - } - } - - bool _globalPositionInScrollable(Offset globalPosition) { - final box = state.context.findRenderObject()! as RenderBox; - final Offset localPosition = box.globalToLocal(globalPosition); - final rect = Rect.fromLTRB(0, 0, box.size.width, box.size.height); - return rect.contains(localPosition); - } - - Rect _dragTargetFromEvent(SelectionEdgeUpdateEvent event) { - return Rect.fromCenter( - center: event.globalPosition, - width: _kDefaultDragTargetSize, - height: _kDefaultDragTargetSize, - ); - } - - @override - SelectionResult dispatchSelectionEventToChild( - Selectable selectable, - SelectionEvent event, - ) { - switch (event.type) { - case SelectionEventType.startEdgeUpdate: - _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels; - ensureChildUpdated(selectable); - case SelectionEventType.endEdgeUpdate: - _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels; - ensureChildUpdated(selectable); - case SelectionEventType.granularlyExtendSelection: - case SelectionEventType.directionallyExtendSelection: - ensureChildUpdated(selectable); - _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels; - _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels; - case SelectionEventType.clear: - _selectableEndEdgeUpdateRecords.remove(selectable); - _selectableStartEdgeUpdateRecords.remove(selectable); - case SelectionEventType.selectAll: - case SelectionEventType.selectWord: - case SelectionEventType.selectParagraph: - _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels; - _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels; - } - return super.dispatchSelectionEventToChild(selectable, event); - } - - @override - void ensureChildUpdated(Selectable selectable) { - final double newRecord = state.position.pixels; - final double? previousStartRecord = - _selectableStartEdgeUpdateRecords[selectable]; - if (_currentDragStartRelatedToOrigin != null && - (previousStartRecord == null || - (newRecord - previousStartRecord).abs() > - precisionErrorTolerance)) { - // Make sure the selectable has up to date events. - final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); - final Offset startOffset = _currentDragStartRelatedToOrigin!.translate( - -deltaToOrigin.dx, - -deltaToOrigin.dy, - ); - selectable.dispatchSelectionEvent( - SelectionEdgeUpdateEvent.forStart(globalPosition: startOffset), - ); - // Make sure we track that we have synthesized a start event for this selectable, - // so we don't synthesize events unnecessarily. - _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels; - } - final double? previousEndRecord = - _selectableEndEdgeUpdateRecords[selectable]; - if (_currentDragEndRelatedToOrigin != null && - (previousEndRecord == null || - (newRecord - previousEndRecord).abs() > precisionErrorTolerance)) { - // Make sure the selectable has up to date events. - final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); - final Offset endOffset = _currentDragEndRelatedToOrigin!.translate( - -deltaToOrigin.dx, - -deltaToOrigin.dy, - ); - selectable.dispatchSelectionEvent( - SelectionEdgeUpdateEvent.forEnd(globalPosition: endOffset), - ); - // Make sure we track that we have synthesized an end event for this selectable, - // so we don't synthesize events unnecessarily. - _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels; - } - } - - @override - void dispose() { - _selectableStartEdgeUpdateRecords.clear(); - _selectableEndEdgeUpdateRecords.clear(); - _scheduledLayoutChange = false; - _autoScroller.stopAutoScroll(); - super.dispose(); - } -} - -Offset _getDeltaToScrollOrigin(ScrollableState scrollableState) { - return switch (scrollableState.axisDirection) { - AxisDirection.up => Offset(0, -scrollableState.position.pixels), - AxisDirection.down => Offset(0, scrollableState.position.pixels), - AxisDirection.left => Offset(-scrollableState.position.pixels, 0), - AxisDirection.right => Offset(scrollableState.position.pixels, 0), - }; -} - -/// 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.axis, - required this.semanticChildCount, - super.child, - }) : assert(semanticChildCount == null || semanticChildCount >= 0); - - final ScrollPosition position; - final bool allowImplicitScrolling; - final int? semanticChildCount; - final Axis axis; - - @override - _RenderScrollSemantics createRenderObject(BuildContext context) { - return _RenderScrollSemantics( - position: position, - allowImplicitScrolling: allowImplicitScrolling, - semanticChildCount: semanticChildCount, - axis: axis, - ); - } - - @override - void updateRenderObject( - BuildContext context, - _RenderScrollSemantics renderObject, - ) { - renderObject - ..allowImplicitScrolling = allowImplicitScrolling - ..axis = axis - ..position = position - ..semanticChildCount = semanticChildCount; - } -} - -class _RenderScrollSemantics extends RenderProxyBox { - _RenderScrollSemantics({ - required ScrollPosition position, - required bool allowImplicitScrolling, - required this.axis, - 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(); - } - - Axis axis; - - int? get semanticChildCount => _semanticChildCount; - int? _semanticChildCount; - set semanticChildCount(int? value) { - if (value == semanticChildCount) { - return; - } - _semanticChildCount = value; - markNeedsSemanticsUpdate(); - } - - void _onScrollToOffset(Offset targetOffset) { - final double offset = switch (axis) { - Axis.horizontal => targetOffset.dx, - Axis.vertical => targetOffset.dy, - }; - _position.jumpTo(offset); - } - - @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; - if (position.maxScrollExtent > position.minScrollExtent && - allowImplicitScrolling) { - config.onScrollToOffset = _onScrollToOffset; - } - } - } - - SemanticsNode? _innerNode; - - @override - void assembleSemanticsNode( - SemanticsNode node, - SemanticsConfiguration config, - Iterable children, - ) { - if (children.isEmpty || - !children.first.isTagged(RenderViewport.useTwoPaneSemantics)) { - _innerNode = null; - super.assembleSemanticsNode(node, config, children); - return; - } - - (_innerNode ??= SemanticsNode(showOnScreen: showOnScreen)).rect = node.rect; - - int? firstVisibleIndex; - final excluded = [_innerNode!]; - final included = []; - 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; - } -} - -// Not using a RestorableDouble because we want to allow null values and override -// [enabled]. -class _RestorableScrollOffset extends RestorableValue { - @override - double? createDefaultValue() => null; - - @override - void didUpdateValue(double? oldValue) { - notifyListeners(); - } - - @override - double fromPrimitives(Object? data) { - return data! as double; - } - - @override - Object? toPrimitives() { - return value; - } - - @override - bool get enabled => value != null; -} diff --git a/lib/common/widgets/flutter/page/scrollable_helpers.dart b/lib/common/widgets/flutter/page/scrollable_helpers.dart deleted file mode 100644 index b68809be3..000000000 --- a/lib/common/widgets/flutter/page/scrollable_helpers.dart +++ /dev/null @@ -1,208 +0,0 @@ -// 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. - -// ignore_for_file: dangling_library_doc_comments, prefer_initializing_formals - -/// @docImport 'package:flutter/material.dart'; -/// -/// @docImport 'overscroll_indicator.dart'; -/// @docImport 'viewport.dart'; - -import 'dart:math' as math; - -import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart' - hide EdgeDraggingAutoScroller, Scrollable, ScrollableState; - -/// 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. - void startAutoScrollIfNecessary(Rect dragTarget) { - 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 _scroll() async { - final scrollRenderBox = scrollable.context.findRenderObject()! as RenderBox; - final Matrix4 transform = scrollRenderBox.getTransformTo(null); - final Rect globalRect = MatrixUtils.transformRect( - transform, - Rect.fromLTRB( - 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(); - } - } -} diff --git a/lib/common/widgets/flutter/page/tabs.dart b/lib/common/widgets/flutter/page/tabs.dart deleted file mode 100644 index cd71c0ee4..000000000 --- a/lib/common/widgets/flutter/page/tabs.dart +++ /dev/null @@ -1,374 +0,0 @@ -// 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. - -// ignore_for_file: prefer_initializing_formals - -import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart'; -import 'package:flutter/foundation.dart' show clampDouble; -import 'package:flutter/gestures.dart' - show DragStartBehavior, HorizontalDragGestureRecognizer; -import 'package:flutter/material.dart' hide TabBarView, PageView; - -/// A page view that displays the widget which corresponds to the currently -/// selected tab. -/// -/// This widget is typically used in conjunction with a [TabBar]. -/// -/// {@youtube 560 315 https://www.youtube.com/watch?v=POtoEH-5l40} -/// -/// If a [TabController] is not provided, then there must be a [DefaultTabController] -/// ancestor. -/// -/// The tab controller's [TabController.length] must equal the length of the -/// [children] list and the length of the [TabBar.tabs] list. -/// -/// To see a sample implementation, visit the [TabController] documentation. -class TabBarView - extends StatefulWidget { - /// Creates a page view with one child per tab. - /// - /// The length of [children] must be the same as the [controller]'s length. - const TabBarView({ - super.key, - required this.children, - this.controller, - this.physics, - this.dragStartBehavior = DragStartBehavior.start, - this.viewportFraction = 1.0, - this.clipBehavior = Clip.hardEdge, - required this.horizontalDragGestureRecognizer, - }); - - final GestureRecognizerFactoryConstructor horizontalDragGestureRecognizer; - - /// This widget's selection and animation state. - /// - /// If [TabController] is not provided, then the value of [DefaultTabController.of] - /// will be used. - final TabController? controller; - - /// One widget per tab. - /// - /// Its length must match the length of the [TabBar.tabs] - /// list, as well as the [controller]'s [TabController.length]. - final List children; - - /// How the page view should respond to user input. - /// - /// For example, determines how the page view continues to animate after the - /// user stops dragging the page view. - /// - /// The physics are modified to snap to page boundaries using - /// [PageScrollPhysics] prior to being used. - /// - /// Defaults to matching platform conventions. - final ScrollPhysics? physics; - - /// {@macro flutter.widgets.scrollable.dragStartBehavior} - final DragStartBehavior dragStartBehavior; - - /// {@macro flutter.widgets.pageview.viewportFraction} - final double viewportFraction; - - /// {@macro flutter.material.Material.clipBehavior} - /// - /// Defaults to [Clip.hardEdge]. - final Clip clipBehavior; - - @override - State> createState() => _TabBarViewState(); -} - -class _TabBarViewState - extends State> { - TabController? _controller; - PageController? _pageController; - late List _childrenWithKey; - int? _currentIndex; - int _warpUnderwayCount = 0; - int _scrollUnderwayCount = 0; - bool _debugHasScheduledValidChildrenCountCheck = false; - - // If the TabBarView is rebuilt with a new tab controller, the caller should - // dispose the old one. In that case the old controller's animation will be - // null and should not be accessed. - bool get _controllerIsValid => _controller?.animation != null; - - void _updateTabController() { - final TabController? newController = - widget.controller ?? DefaultTabController.maybeOf(context); - assert(() { - if (newController == null) { - throw FlutterError( - 'No TabController for ${widget.runtimeType}.\n' - 'When creating a ${widget.runtimeType}, you must either provide an explicit ' - 'TabController using the "controller" property, or you must ensure that there ' - 'is a DefaultTabController above the ${widget.runtimeType}.\n' - 'In this case, there was neither an explicit controller nor a default controller.', - ); - } - return true; - }()); - - if (newController == _controller) { - return; - } - - if (_controllerIsValid) { - _controller!.animation!.removeListener(_handleTabControllerAnimationTick); - } - _controller = newController; - if (_controller != null) { - _controller!.animation!.addListener(_handleTabControllerAnimationTick); - } - } - - void _jumpToPage(int page) { - _warpUnderwayCount += 1; - _pageController!.jumpToPage(page); - _warpUnderwayCount -= 1; - } - - Future _animateToPage( - int page, { - required Duration duration, - required Curve curve, - }) async { - _warpUnderwayCount += 1; - await _pageController!.animateToPage( - page, - duration: duration, - curve: curve, - ); - _warpUnderwayCount -= 1; - } - - @override - void initState() { - super.initState(); - _updateChildren(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _updateTabController(); - _currentIndex = _controller!.index; - if (_pageController == null) { - _pageController = PageController( - initialPage: _currentIndex!, - viewportFraction: widget.viewportFraction, - ); - } else { - _pageController!.jumpToPage(_currentIndex!); - } - } - - @override - void didUpdateWidget(TabBarView oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.controller != oldWidget.controller) { - _updateTabController(); - _currentIndex = _controller!.index; - _jumpToPage(_currentIndex!); - } - if (widget.viewportFraction != oldWidget.viewportFraction) { - _pageController?.dispose(); - _pageController = PageController( - initialPage: _currentIndex!, - viewportFraction: widget.viewportFraction, - ); - } - // While a warp is under way, we stop updating the tab page contents. - // This is tracked in https://github.com/flutter/flutter/issues/31269. - if (widget.children != oldWidget.children && _warpUnderwayCount == 0) { - _updateChildren(); - } - } - - @override - void dispose() { - if (_controllerIsValid) { - _controller!.animation!.removeListener(_handleTabControllerAnimationTick); - } - _controller = null; - _pageController?.dispose(); - // We don't own the _controller Animation, so it's not disposed here. - super.dispose(); - } - - void _updateChildren() { - _childrenWithKey = KeyedSubtree.ensureUniqueKeysForList( - widget.children.map((Widget child) { - return Semantics(role: .tabPanel, child: child); - }).toList(), - ); - } - - void _handleTabControllerAnimationTick() { - if (_scrollUnderwayCount > 0 || !_controller!.indexIsChanging) { - return; - } // This widget is driving the controller's animation. - - if (_controller!.index != _currentIndex) { - _currentIndex = _controller!.index; - _warpToCurrentIndex(); - } - } - - void _warpToCurrentIndex() { - if (!mounted || _pageController!.page == _currentIndex!.toDouble()) { - return; - } - - final adjacentDestination = - (_currentIndex! - _controller!.previousIndex).abs() == 1; - if (adjacentDestination) { - _warpToAdjacentTab(_controller!.animationDuration); - } else { - _warpToNonAdjacentTab(_controller!.animationDuration); - } - } - - Future _warpToAdjacentTab(Duration duration) async { - if (duration == Duration.zero) { - _jumpToPage(_currentIndex!); - } else { - await _animateToPage( - _currentIndex!, - duration: duration, - curve: Curves.ease, - ); - } - if (mounted) { - setState(_updateChildren); - } - return Future.value(); - } - - Future _warpToNonAdjacentTab(Duration duration) async { - final int previousIndex = _controller!.previousIndex; - assert((_currentIndex! - previousIndex).abs() > 1); - - // initialPage defines which page is shown when starting the animation. - // This page is adjacent to the destination page. - final int initialPage = _currentIndex! > previousIndex - ? _currentIndex! - 1 - : _currentIndex! + 1; - - setState(() { - // Needed for `RenderSliverMultiBoxAdaptor.move` and kept alive children. - // For motivation, see https://github.com/flutter/flutter/pull/29188 and - // https://github.com/flutter/flutter/issues/27010#issuecomment-486475152. - _childrenWithKey = List.of(_childrenWithKey, growable: false); - final Widget temp = _childrenWithKey[initialPage]; - _childrenWithKey[initialPage] = _childrenWithKey[previousIndex]; - _childrenWithKey[previousIndex] = temp; - }); - - // Make a first jump to the adjacent page. - _jumpToPage(initialPage); - - // Jump or animate to the destination page. - if (duration == Duration.zero) { - _jumpToPage(_currentIndex!); - } else { - await _animateToPage( - _currentIndex!, - duration: duration, - curve: Curves.ease, - ); - } - - if (mounted) { - setState(_updateChildren); - } - } - - void _syncControllerOffset() { - _controller!.offset = clampDouble( - _pageController!.page! - _controller!.index, - -1.0, - 1.0, - ); - } - - // Called when the PageView scrolls - bool _handleScrollNotification(ScrollNotification notification) { - if (_warpUnderwayCount > 0 || _scrollUnderwayCount > 0) { - return false; - } - - if (notification.depth != 0) { - return false; - } - - if (!_controllerIsValid) { - return false; - } - - _scrollUnderwayCount += 1; - final double page = _pageController!.page!; - if (notification is ScrollUpdateNotification && - !_controller!.indexIsChanging) { - final bool pageChanged = (page - _controller!.index).abs() > 1.0; - if (pageChanged) { - _controller!.index = page.round(); - _currentIndex = _controller!.index; - } - _syncControllerOffset(); - } else if (notification is ScrollEndNotification) { - _controller!.index = page.round(); - _currentIndex = _controller!.index; - if (!_controller!.indexIsChanging) { - _syncControllerOffset(); - } - } - _scrollUnderwayCount -= 1; - - return false; - } - - bool _debugScheduleCheckHasValidChildrenCount() { - if (_debugHasScheduledValidChildrenCountCheck) { - return true; - } - WidgetsBinding.instance.addPostFrameCallback((Duration duration) { - _debugHasScheduledValidChildrenCountCheck = false; - if (!mounted) { - return; - } - assert(() { - if (_controller!.length != widget.children.length) { - throw FlutterError( - "Controller's length property (${_controller!.length}) does not match the " - "number of children (${widget.children.length}) present in TabBarView's children property.", - ); - } - return true; - }()); - }, debugLabel: 'TabBarView.validChildrenCountCheck'); - _debugHasScheduledValidChildrenCountCheck = true; - return true; - } - - @override - Widget build(BuildContext context) { - assert(_debugScheduleCheckHasValidChildrenCount()); - - return NotificationListener( - onNotification: _handleScrollNotification, - child: PageView( - dragStartBehavior: widget.dragStartBehavior, - clipBehavior: widget.clipBehavior, - controller: _pageController, - physics: widget.physics == null - ? const PageScrollPhysics().applyTo(const ClampingScrollPhysics()) - : const PageScrollPhysics().applyTo(widget.physics), - horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer, - children: _childrenWithKey, - ), - ); - } -} diff --git a/lib/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart b/lib/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart index fe7c2da03..d4051302d 100644 --- a/lib/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart +++ b/lib/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart @@ -39,6 +39,9 @@ class CustomHorizontalDragGestureRecognizer lastPosition.global, ); } + + @override + Type get runtimeType => HorizontalDragGestureRecognizer; } double touchSlopH = Pref.touchSlopH; diff --git a/lib/common/widgets/image_viewer/gallery_viewer.dart b/lib/common/widgets/image_viewer/gallery_viewer.dart index 94ca0261f..8f3a5b61d 100644 --- a/lib/common/widgets/image_viewer/gallery_viewer.dart +++ b/lib/common/widgets/image_viewer/gallery_viewer.dart @@ -19,12 +19,12 @@ import 'dart:io' show File, Platform; import 'package:PiliPlus/common/widgets/colored_box_transition.dart'; import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; -import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart'; import 'package:PiliPlus/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/image.dart'; import 'package:PiliPlus/common/widgets/image_viewer/loading_indicator.dart'; import 'package:PiliPlus/common/widgets/image_viewer/viewer.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show tabBarScrollPhysics; import 'package:PiliPlus/main.dart' show tmpPadding; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; @@ -40,7 +40,7 @@ import 'package:PiliPlus/utils/utils.dart'; import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart' hide Image, PageView; +import 'package:flutter/material.dart' hide Image; import 'package:flutter/services.dart' show HapticFeedback; import 'package:get/get.dart'; import 'package:media_kit/media_kit.dart'; @@ -331,11 +331,11 @@ class _GalleryViewerState extends State alignment: .topLeft, animation: _animateController, onTransform: _onTransform, - child: PageView.builder( + child: PageView.builder( controller: _pageController, onPageChanged: _onPageChanged, - physics: const CustomTabBarViewScrollPhysics( - parent: AlwaysScrollableScrollPhysics(), + physics: const AlwaysScrollableScrollPhysics( + parent: tabBarScrollPhysics, ), itemCount: widget.sources.length, itemBuilder: _itemBuilder, diff --git a/lib/common/widgets/scroll_physics.dart b/lib/common/widgets/scroll_physics.dart index 823ba3640..5fbb900ff 100644 --- a/lib/common/widgets/scroll_physics.dart +++ b/lib/common/widgets/scroll_physics.dart @@ -1,20 +1,21 @@ -import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart'; import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; -import 'package:flutter/material.dart' hide TabBarView; +import 'package:flutter/material.dart'; Widget tabBarView({ required List children, TabController? controller, -}) => TabBarView( + HitTestBehavior hitTestBehavior = .opaque, +}) => TabBarView( controller: controller, - physics: clampingScrollPhysics, + physics: tabBarScrollPhysics, + hitTestBehavior: hitTestBehavior, horizontalDragGestureRecognizer: CustomHorizontalDragGestureRecognizer.new, children: children, ); -final _springDescription = _customSpringDescription(); +SpringDescription kSpringDescription = _customSpringDescription(); SpringDescription _customSpringDescription() { final List springDescription = Pref.springDescription; @@ -25,20 +26,18 @@ SpringDescription _customSpringDescription() { ); } -const clampingScrollPhysics = CustomTabBarViewScrollPhysics( - parent: ClampingScrollPhysics(), -); +const tabBarScrollPhysics = _TabBarViewScrollPhysics(); -class CustomTabBarViewScrollPhysics extends ScrollPhysics { - const CustomTabBarViewScrollPhysics({super.parent}); +class _TabBarViewScrollPhysics extends ClampingScrollPhysics { + const _TabBarViewScrollPhysics({super.parent}); @override - CustomTabBarViewScrollPhysics applyTo(ScrollPhysics? ancestor) { - return CustomTabBarViewScrollPhysics(parent: buildParent(ancestor)); + _TabBarViewScrollPhysics applyTo(ScrollPhysics? ancestor) { + return _TabBarViewScrollPhysics(parent: buildParent(ancestor)); } @override - SpringDescription get spring => _springDescription; + SpringDescription get spring => kSpringDescription; } mixin ReloadMixin { diff --git a/lib/pages/article/view.dart b/lib/pages/article/view.dart index 33bc02725..60fbdb463 100644 --- a/lib/pages/article/view.dart +++ b/lib/pages/article/view.dart @@ -2,11 +2,11 @@ import 'dart:math'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; -import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show tabBarScrollPhysics; import 'package:PiliPlus/common/widgets/sliver/sliver_to_box_adapter.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/models/dynamics/article_content_model.dart' show Pic; @@ -27,7 +27,7 @@ import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/share_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:cached_network_image_ce/cached_network_image.dart'; -import 'package:flutter/material.dart' hide PageView; +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -501,8 +501,8 @@ class _ArticlePageState extends CommonDynPageState { height: height, width: maxWidth, margin: const .only(bottom: 10), - child: PageView.builder( - physics: clampingScrollPhysics, + child: PageView.builder( + physics: tabBarScrollPhysics, horizontalDragGestureRecognizer: CustomHorizontalDragGestureRecognizer.new, onPageChanged: controller.topIndex.call, diff --git a/lib/pages/bubble/view.dart b/lib/pages/bubble/view.dart index d3940c341..8e2954514 100644 --- a/lib/pages/bubble/view.dart +++ b/lib/pages/bubble/view.dart @@ -3,7 +3,7 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/bubble/dyn_list.dart'; import 'package:PiliPlus/pages/bubble/controller.dart'; diff --git a/lib/pages/contact/view.dart b/lib/pages/contact/view.dart index 4f31c8f7b..95cb0c7e7 100644 --- a/lib/pages/contact/view.dart +++ b/lib/pages/contact/view.dart @@ -1,4 +1,4 @@ -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/pages/fan/view.dart'; import 'package:PiliPlus/pages/follow/child/child_view.dart'; import 'package:PiliPlus/pages/follow_search/view.dart'; diff --git a/lib/pages/danmaku_block/view.dart b/lib/pages/danmaku_block/view.dart index 6e4e349de..9e8d92bbd 100644 --- a/lib/pages/danmaku_block/view.dart +++ b/lib/pages/danmaku_block/view.dart @@ -2,7 +2,7 @@ import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/models/common/dm_block_type.dart'; import 'package:PiliPlus/models/user/danmaku_block.dart'; import 'package:PiliPlus/models/user/danmaku_rule.dart'; diff --git a/lib/pages/dynamics/view.dart b/lib/pages/dynamics/view.dart index 1b06398e1..1f2abe4a8 100644 --- a/lib/pages/dynamics/view.dart +++ b/lib/pages/dynamics/view.dart @@ -1,4 +1,4 @@ -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/dynamic/dynamics_type.dart'; import 'package:PiliPlus/models/common/dynamic/up_panel_position.dart'; diff --git a/lib/pages/dynamics_detail/view.dart b/lib/pages/dynamics_detail/view.dart index cf4754fa9..bf729cc2e 100644 --- a/lib/pages/dynamics_detail/view.dart +++ b/lib/pages/dynamics_detail/view.dart @@ -4,10 +4,12 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; +import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/scroll_behavior.dart' show NoOverscrollIndicator; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show ReloadScrollPhysics; import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_to_box_adapter.dart'; import 'package:PiliPlus/common/widgets/tap_region_surface.dart'; @@ -361,8 +363,12 @@ class _DynamicDetailPageState Obx(() => replyList(controller.loadingState.value)), ], ); - final child = tabBarView( + final child = TabBarView( controller: tabController, + hitTestBehavior: .translucent, + physics: const NeverScrollableScrollPhysics(), + horizontalDragGestureRecognizer: + CustomHorizontalDragGestureRecognizer.new, children: [ isPortrait ? reply diff --git a/lib/pages/episode_panel/view.dart b/lib/pages/episode_panel/view.dart index 72526f0f5..81ff25913 100644 --- a/lib/pages/episode_panel/view.dart +++ b/lib/pages/episode_panel/view.dart @@ -4,11 +4,11 @@ import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; -import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show tabBarScrollPhysics; import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -32,8 +32,8 @@ import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart' hide TabBarView; +import 'package:flutter/foundation.dart' show kDebugMode; +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @@ -235,9 +235,9 @@ class _EpisodePanelState extends State @override Widget buildList(ThemeData theme) { if (_isMulti) { - return TabBarView( + return TabBarView( controller: _tabController, - physics: clampingScrollPhysics, + physics: tabBarScrollPhysics, horizontalDragGestureRecognizer: () => TabBarDragGestureRecognizer(isDxAllowed: isDxAllowed), children: List.generate( diff --git a/lib/pages/fav/view.dart b/lib/pages/fav/view.dart index 0124257e5..90288627e 100644 --- a/lib/pages/fav/view.dart +++ b/lib/pages/fav/view.dart @@ -1,4 +1,4 @@ -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/fav_type.dart'; diff --git a/lib/pages/follow/view.dart b/lib/pages/follow/view.dart index 2b0fefa8f..c4d235f61 100644 --- a/lib/pages/follow/view.dart +++ b/lib/pages/follow/view.dart @@ -1,7 +1,7 @@ import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/member/tags.dart'; diff --git a/lib/pages/history/view.dart b/lib/pages/history/view.dart index d8ba6540c..2f9d237b0 100644 --- a/lib/pages/history/view.dart +++ b/lib/pages/history/view.dart @@ -1,11 +1,11 @@ import 'package:PiliPlus/common/widgets/appbar/appbar.dart'; -import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart'; import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show tabBarScrollPhysics; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/history/list.dart'; import 'package:PiliPlus/pages/history/base_controller.dart'; @@ -13,7 +13,7 @@ import 'package:PiliPlus/pages/history/controller.dart'; import 'package:PiliPlus/pages/history/widgets/item.dart'; import 'package:PiliPlus/utils/extension/scroll_controller_ext.dart'; import 'package:PiliPlus/utils/grid.dart'; -import 'package:flutter/material.dart' hide TabBarView; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; class HistoryPage extends StatefulWidget { @@ -133,10 +133,10 @@ class _HistoryPageState extends State ], ), Expanded( - child: TabBarView( + child: TabBarView( physics: enableMultiSelect ? const NeverScrollableScrollPhysics() - : clampingScrollPhysics, + : tabBarScrollPhysics, controller: _historyController.tabController, horizontalDragGestureRecognizer: CustomHorizontalDragGestureRecognizer.new, diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index 68faa8a90..187d6869f 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -1,7 +1,7 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/custom_height_widget.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/pages/common/common_page.dart'; import 'package:PiliPlus/pages/home/controller.dart'; import 'package:PiliPlus/pages/main/controller.dart'; diff --git a/lib/pages/later/view.dart b/lib/pages/later/view.dart index db37be02a..44d526393 100644 --- a/lib/pages/later/view.dart +++ b/lib/pages/later/view.dart @@ -1,8 +1,8 @@ import 'package:PiliPlus/common/widgets/appbar/appbar.dart'; -import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart'; import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart'; import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show tabBarScrollPhysics; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/models/common/later_view_type.dart'; import 'package:PiliPlus/models_new/later/list.dart'; @@ -14,7 +14,7 @@ import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/extension/get_ext.dart'; import 'package:PiliPlus/utils/extension/scroll_controller_ext.dart'; import 'package:PiliPlus/utils/request_utils.dart'; -import 'package:flutter/material.dart' hide TabBarView; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @@ -131,10 +131,10 @@ class _LaterPageState extends State }, ), Expanded( - child: TabBarView( + child: TabBarView( physics: enableMultiSelect ? const NeverScrollableScrollPhysics() - : clampingScrollPhysics, + : tabBarScrollPhysics, controller: _tabController, horizontalDragGestureRecognizer: CustomHorizontalDragGestureRecognizer.new, diff --git a/lib/pages/live_area/view.dart b/lib/pages/live_area/view.dart index 2bec49271..404048e1b 100644 --- a/lib/pages/live_area/view.dart +++ b/lib/pages/live_area/view.dart @@ -2,7 +2,7 @@ import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models_new/live/live_area_list/area_item.dart'; diff --git a/lib/pages/live_area_detail/view.dart b/lib/pages/live_area_detail/view.dart index 3624e49c4..56043997e 100644 --- a/lib/pages/live_area_detail/view.dart +++ b/lib/pages/live_area_detail/view.dart @@ -1,6 +1,6 @@ import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models_new/live/live_area_list/area_item.dart'; diff --git a/lib/pages/live_dm_block/view.dart b/lib/pages/live_dm_block/view.dart index 7e55891ce..49783b376 100644 --- a/lib/pages/live_dm_block/view.dart +++ b/lib/pages/live_dm_block/view.dart @@ -3,7 +3,7 @@ import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/scroll_behavior.dart' show NoOverscrollIndicator; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; import 'package:PiliPlus/models/common/live/live_dm_silent_type.dart'; import 'package:PiliPlus/models_new/live/live_dm_block/shield_user_list.dart'; @@ -46,6 +46,7 @@ class _LiveDmBlockPageState extends State { ); Widget view = tabBarView( + hitTestBehavior: .translucent, controller: _controller.tabController, children: [ KeepAliveWrapper( diff --git a/lib/pages/live_room/contribution_rank/view.dart b/lib/pages/live_room/contribution_rank/view.dart index 8a64d5c55..695d56c5c 100644 --- a/lib/pages/live_room/contribution_rank/view.dart +++ b/lib/pages/live_room/contribution_rank/view.dart @@ -2,7 +2,7 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/live/live_contribution_rank_type.dart'; diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index a44d9cd73..6cfe5cf44 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -7,15 +7,14 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/extra_hittest_stack.dart'; -import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart'; import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/route_aware_mixin.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; -import 'package:PiliPlus/models/common/image_type.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show tabBarScrollPhysics; import 'package:PiliPlus/models/common/live/live_contribution_rank_type.dart'; import 'package:PiliPlus/models_new/live/live_room_info_h5/data.dart'; import 'package:PiliPlus/models_new/live/live_superchat/item.dart'; @@ -53,7 +52,7 @@ import 'package:PiliPlus/utils/utils.dart'; import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:canvas_danmaku/danmaku_screen.dart'; import 'package:flutter/foundation.dart' show kDebugMode; -import 'package:flutter/material.dart' hide PageView; +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:screen_brightness_platform_interface/screen_brightness_platform_interface.dart'; @@ -572,7 +571,7 @@ class _LiveRoomPageState extends State NetworkImgLayer( width: 34, height: 34, - type: ImageType.avatar, + type: .avatar, src: roomInfoH5.anchorInfo!.baseInfo!.face, ), Flexible( @@ -766,10 +765,10 @@ class _LiveRoomPageState extends State return Padding( padding: .only(bottom: 12, top: isPortrait ? 12 : 0), child: _liveRoomController.showSuperChat - ? PageView( + ? PageView( key: pageKey, controller: _liveRoomController.pageController, - physics: clampingScrollPhysics, + physics: tabBarScrollPhysics, onPageChanged: _liveRoomController.pageIndex.call, horizontalDragGestureRecognizer: CustomHorizontalDragGestureRecognizer.new, diff --git a/lib/pages/live_search/view.dart b/lib/pages/live_search/view.dart index 0ff89aff1..cf3229ce5 100644 --- a/lib/pages/live_search/view.dart +++ b/lib/pages/live_search/view.dart @@ -1,6 +1,5 @@ -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; -import 'package:PiliPlus/models/common/live/live_search_type.dart'; import 'package:PiliPlus/pages/live_search/child/view.dart'; import 'package:PiliPlus/pages/live_search/controller.dart'; import 'package:PiliPlus/utils/utils.dart'; @@ -95,11 +94,11 @@ class _LiveSearchPageState extends State { children: [ LiveSearchChildPage( controller: _controller.roomCtr, - searchType: LiveSearchType.room, + searchType: .room, ), LiveSearchChildPage( controller: _controller.userCtr, - searchType: LiveSearchType.user, + searchType: .user, ), ], ), diff --git a/lib/pages/login/view.dart b/lib/pages/login/view.dart index b4cc05df4..f0e6383c2 100644 --- a/lib/pages/login/view.dart +++ b/lib/pages/login/view.dart @@ -2,7 +2,7 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/dial_prefix.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/pages/login/controller.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index c0d961c53..3802c0cb2 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -442,8 +442,6 @@ class _MainAppState extends PopScopeState child = TabBarView( controller: _mainController.controller, physics: const NeverScrollableScrollPhysics(), - - /// apply `lib/scripts/tabs.patch` scrollDirection: _mainController.useBottomNav ? .horizontal : .vertical, children: _mainController.navigationBars.map((i) => i.page).toList(), ); diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index 9a958a760..6047c5005 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -9,7 +9,7 @@ import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/scroll_behavior.dart' show NoOverscrollIndicator; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/http/live.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/user.dart'; @@ -571,6 +571,7 @@ class _MemberPageState extends State { ]; Widget get _buildBody => tabBarView( + hitTestBehavior: .translucent, controller: _userController.tabController, children: _userController.tab2!.map((item) { return switch (item.param!) { diff --git a/lib/pages/member/widget/user_info_card.dart b/lib/pages/member/widget/user_info_card.dart index 945531a9c..993bde3f5 100644 --- a/lib/pages/member/widget/user_info_card.dart +++ b/lib/pages/member/widget/user_info_card.dart @@ -3,7 +3,8 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/avatars.dart'; import 'package:PiliPlus/common/widgets/image_viewer/hero.dart'; import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show tabBarScrollPhysics; import 'package:PiliPlus/common/widgets/selection_text.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; @@ -635,7 +636,7 @@ class UserInfoCard extends StatelessWidget { child: PageView.builder( controller: controller, itemCount: imgUrls.length, - physics: clampingScrollPhysics, + physics: tabBarScrollPhysics, itemBuilder: (context, index) { final img = imgUrls[index]; return fromHero( diff --git a/lib/pages/member_search/view.dart b/lib/pages/member_search/view.dart index c3467e4c4..b4c5c1c50 100644 --- a/lib/pages/member_search/view.dart +++ b/lib/pages/member_search/view.dart @@ -1,6 +1,5 @@ -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; -import 'package:PiliPlus/models/common/member/search_type.dart'; import 'package:PiliPlus/pages/member_search/child/view.dart'; import 'package:PiliPlus/pages/member_search/controller.dart'; import 'package:PiliPlus/utils/utils.dart'; @@ -98,11 +97,11 @@ class _MemberSearchPageState extends State { children: [ MemberSearchChildPage( controller: _controller.arcCtr, - searchType: MemberSearchType.archive, + searchType: .archive, ), MemberSearchChildPage( controller: _controller.dynCtr, - searchType: MemberSearchType.dynamic, + searchType: .dynamic, ), ], ), diff --git a/lib/pages/member_upower_rank/view.dart b/lib/pages/member_upower_rank/view.dart index a10f2e2fd..17d679961 100644 --- a/lib/pages/member_upower_rank/view.dart +++ b/lib/pages/member_upower_rank/view.dart @@ -4,7 +4,7 @@ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models_new/upower_rank/rank_info.dart'; diff --git a/lib/pages/member_video/controller.dart b/lib/pages/member_video/controller.dart index e7d95fc06..838fd92dc 100644 --- a/lib/pages/member_video/controller.dart +++ b/lib/pages/member_video/controller.dart @@ -1,4 +1,4 @@ -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show ReloadMixin; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/member.dart'; import 'package:PiliPlus/http/search.dart'; diff --git a/lib/pages/member_video/view.dart b/lib/pages/member_video/view.dart index 0b1a1ea79..66e4bb82a 100644 --- a/lib/pages/member_video/view.dart +++ b/lib/pages/member_video/view.dart @@ -1,7 +1,8 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show ReloadScrollPhysics; import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/member/contribute_type.dart'; diff --git a/lib/pages/member_video_web/base/view.dart b/lib/pages/member_video_web/base/view.dart index 56abe1cc4..2025aa56b 100644 --- a/lib/pages/member_video_web/base/view.dart +++ b/lib/pages/member_video_web/base/view.dart @@ -2,7 +2,8 @@ import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show ReloadScrollPhysics; import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart'; import 'package:PiliPlus/http/loading_state.dart'; diff --git a/lib/pages/pgc/view.dart b/lib/pages/pgc/view.dart index d56e5fc4f..bbf19be81 100644 --- a/lib/pages/pgc/view.dart +++ b/lib/pages/pgc/view.dart @@ -5,7 +5,7 @@ import 'package:PiliPlus/common/widgets/button/more_btn.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/fav_type.dart'; diff --git a/lib/pages/popular_series/view.dart b/lib/pages/popular_series/view.dart index 937e9aa1b..da16edfd1 100644 --- a/lib/pages/popular_series/view.dart +++ b/lib/pages/popular_series/view.dart @@ -2,7 +2,8 @@ import 'dart:math'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show ReloadScrollPhysics; import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart'; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; diff --git a/lib/pages/search_result/view.dart b/lib/pages/search_result/view.dart index a5b3fff93..300bb4cdb 100644 --- a/lib/pages/search_result/view.dart +++ b/lib/pages/search_result/view.dart @@ -1,4 +1,4 @@ -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/models/common/search/search_type.dart'; import 'package:PiliPlus/pages/search/controller.dart'; diff --git a/lib/pages/setting/models/style_settings.dart b/lib/pages/setting/models/style_settings.dart index 5be370a61..24623de93 100644 --- a/lib/pages/setting/models/style_settings.dart +++ b/lib/pages/setting/models/style_settings.dart @@ -6,6 +6,8 @@ import 'package:PiliPlus/common/widgets/custom_toast.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/scale_app.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show kSpringDescription; import 'package:PiliPlus/common/widgets/stateful_builder.dart'; import 'package:PiliPlus/models/common/bar_hide_type.dart'; import 'package:PiliPlus/models/common/dynamic/dynamic_badge_mode.dart'; @@ -619,7 +621,12 @@ void _showSpringDialog(BuildContext context, _) { final res = springDescription.map(double.parse).toList(); Get.back(); GStorage.setting.put(SettingBoxKey.springDescription, res); - SmartDialog.showToast('设置成功,重启生效'); + kSpringDescription = SpringDescription( + mass: res[0], + stiffness: res[1], + damping: res[2], + ); + SmartDialog.showToast('设置成功'); } catch (e) { SmartDialog.showToast(e.toString()); } diff --git a/lib/pages/video/ai_conclusion/view.dart b/lib/pages/video/ai_conclusion/view.dart index 5b4744b84..6da95d6e5 100644 --- a/lib/pages/video/ai_conclusion/view.dart +++ b/lib/pages/video/ai_conclusion/view.dart @@ -3,7 +3,6 @@ import 'package:PiliPlus/models_new/video/video_ai_conclusion/model_result.dart' import 'package:PiliPlus/pages/common/slide/common_slide_page.dart'; import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; -import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -166,30 +165,21 @@ class _AiDetailState extends State } late Key _key; - late bool _isNested; @override void didChangeDependencies() { super.didChangeDependencies(); final controller = PrimaryScrollController.of(context); - _isNested = controller is ExtendedNestedScrollController; _key = ValueKey(controller.hashCode); } @override Widget buildList(ThemeData theme) { - final child = AiConclusionPanel.buildContent( + return AiConclusionPanel.buildContent( context, theme, widget.item, key: _key, ); - if (_isNested) { - return ExtendedVisibilityDetector( - uniqueKey: const Key('ai-conclusion'), - child: child, - ); - } - return child; } } diff --git a/lib/pages/video/introduction/pgc/widgets/intro_detail.dart b/lib/pages/video/introduction/pgc/widgets/intro_detail.dart index 45acf0ad3..4e38fcf11 100644 --- a/lib/pages/video/introduction/pgc/widgets/intro_detail.dart +++ b/lib/pages/video/introduction/pgc/widgets/intro_detail.dart @@ -1,6 +1,6 @@ -import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show tabBarScrollPhysics; import 'package:PiliPlus/common/widgets/selection_text.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/models/common/stat_type.dart'; @@ -11,7 +11,7 @@ import 'package:PiliPlus/pages/pgc_review/view.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart'; import 'package:PiliPlus/utils/extension/scroll_controller_ext.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:flutter/material.dart' hide TabBarView; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; class PgcIntroPanel extends CommonSlidePage { @@ -94,9 +94,9 @@ class _IntroDetailState extends State @override Widget buildList(ThemeData theme) { - return TabBarView( + return TabBarView( controller: _tabController, - physics: clampingScrollPhysics, + physics: tabBarScrollPhysics, horizontalDragGestureRecognizer: () => TabBarDragGestureRecognizer(isDxAllowed: isDxAllowed), children: [ diff --git a/lib/pages/video/introduction/ugc/controller.dart b/lib/pages/video/introduction/ugc/controller.dart index ada200d55..6ba149825 100644 --- a/lib/pages/video/introduction/ugc/controller.dart +++ b/lib/pages/video/introduction/ugc/controller.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:math'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show ReloadMixin; import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/init.dart'; diff --git a/lib/pages/video/note/view.dart b/lib/pages/video/note/view.dart index 444265542..e83874b4f 100644 --- a/lib/pages/video/note/view.dart +++ b/lib/pages/video/note/view.dart @@ -11,7 +11,6 @@ import 'package:PiliPlus/pages/webview/view.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/bili_utils.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; -import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -94,43 +93,36 @@ class _NoteListPageState extends State } late Key _key; - late bool _isNested; @override void didChangeDependencies() { super.didChangeDependencies(); final controller = PrimaryScrollController.of(context); - _isNested = controller is ExtendedNestedScrollController; _key = ValueKey(controller.hashCode); } @override Widget buildList(ThemeData theme) { - Widget child = refreshIndicator( - onRefresh: _controller.onRefresh, - child: CustomScrollView( - key: _key, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverPadding( - padding: const EdgeInsets.only(bottom: 100), - sliver: Obx( - () => _buildBody(theme, _controller.loadingState.value), - ), - ), - ], - ), - ); - if (_isNested) { - child = ExtendedVisibilityDetector( - uniqueKey: const ValueKey(NoteListPage), - child: child, - ); - } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Expanded(child: child), + Expanded( + child: refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + key: _key, + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverPadding( + padding: const .only(bottom: 100), + sliver: Obx( + () => _buildBody(theme, _controller.loadingState.value), + ), + ), + ], + ), + ), + ), Container( padding: EdgeInsets.only( left: 12, diff --git a/lib/pages/video/pay_coins/view.dart b/lib/pages/video/pay_coins/view.dart index 1b5861a53..4d763fba4 100644 --- a/lib/pages/video/pay_coins/view.dart +++ b/lib/pages/video/pay_coins/view.dart @@ -3,7 +3,8 @@ import 'dart:math' as math; import 'dart:math' show max; import 'package:PiliPlus/common/assets.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show tabBarScrollPhysics; import 'package:PiliPlus/pages/common/publish/publish_route.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; @@ -303,7 +304,7 @@ class _PayCoinsPageState extends State child: SizedBox( height: 100, child: PageView( - physics: clampingScrollPhysics, + physics: tabBarScrollPhysics, controller: _controller, onPageChanged: (index) { _scale(); diff --git a/lib/pages/video/post_panel/view.dart b/lib/pages/video/post_panel/view.dart index 2380ba5b0..ae6e1e287 100644 --- a/lib/pages/video/post_panel/view.dart +++ b/lib/pages/video/post_panel/view.dart @@ -14,7 +14,6 @@ import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/post_panel/popup_menu_text.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; -import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show FilteringTextInputFormatter; @@ -235,13 +234,11 @@ class _PostPanelState extends State } late Key _key; - late bool _isNested; @override void didChangeDependencies() { super.didChangeDependencies(); final controller = PrimaryScrollController.of(context); - _isNested = controller is ExtendedNestedScrollController; _key = ValueKey(controller.hashCode); } @@ -251,25 +248,18 @@ class _PostPanelState extends State return scrollableError; } final bottom = MediaQuery.viewPaddingOf(context).bottom; - Widget child = ListView.builder( - key: _key, - physics: const AlwaysScrollableScrollPhysics(), - padding: EdgeInsets.only(bottom: 88 + bottom), - itemCount: list.length, - itemBuilder: (context, index) { - return _buildItem(theme, index, list[index]); - }, - ); - if (_isNested) { - child = ExtendedVisibilityDetector( - uniqueKey: const ValueKey(PostPanel), - child: child, - ); - } return Stack( clipBehavior: Clip.none, children: [ - child, + ListView.builder( + key: _key, + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.only(bottom: 88 + bottom), + itemCount: list.length, + itemBuilder: (context, index) { + return _buildItem(theme, index, list[index]); + }, + ), Positioned( right: kFloatingActionButtonMargin, bottom: kFloatingActionButtonMargin + bottom, diff --git a/lib/pages/video/reply/view.dart b/lib/pages/video/reply/view.dart index 382c433f0..1885c7a83 100644 --- a/lib/pages/video/reply/view.dart +++ b/lib/pages/video/reply/view.dart @@ -13,7 +13,6 @@ import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart'; import 'package:PiliPlus/pages/video/reply_reply/view.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:easy_debounce/easy_throttle.dart'; -import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -68,7 +67,7 @@ class _VideoReplyPanelState extends State @override Widget build(BuildContext context) { super.build(context); - final child = fabAnimWrapper( + return fabAnimWrapper( child: refreshIndicator( onRefresh: _videoReplyController.onRefresh, isClampingScrollPhysics: widget.isNested, @@ -149,13 +148,6 @@ class _VideoReplyPanelState extends State ), ), ); - if (widget.isNested) { - return ExtendedVisibilityDetector( - uniqueKey: const ValueKey(VideoReplyPanel), - child: child, - ); - } - return child; } Widget _buildBody(LoadingState?> loadingState) { diff --git a/lib/pages/video/reply_reply/view.dart b/lib/pages/video/reply_reply/view.dart index 31db3f28c..a40a83faa 100644 --- a/lib/pages/video/reply_reply/view.dart +++ b/lib/pages/video/reply_reply/view.dart @@ -183,7 +183,7 @@ class _VideoReplyReplyPanelState extends State @override Widget buildList(ThemeData theme) { - final child = refreshIndicator( + return refreshIndicator( onRefresh: _controller.onRefresh, isClampingScrollPhysics: widget.isNested, child: CustomScrollView( @@ -211,10 +211,6 @@ class _VideoReplyReplyPanelState extends State ], ), ); - if (widget.isNested) { - return ExtendedVisibilityDetector(uniqueKey: Key(_tag), child: child); - } - return child; } Widget _header(ThemeData theme, ReplyInfo firstFloor) { diff --git a/lib/pages/video/reply_search_item/view.dart b/lib/pages/video/reply_search_item/view.dart index 7120a608e..20c0f2b0a 100644 --- a/lib/pages/video/reply_search_item/view.dart +++ b/lib/pages/video/reply_search_item/view.dart @@ -1,4 +1,4 @@ -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/models/common/reply/reply_search_type.dart'; import 'package:PiliPlus/pages/video/reply_search_item/child/view.dart'; diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart index fe7077b48..9f5f60407 100644 --- a/lib/pages/video/view.dart +++ b/lib/pages/video/view.dart @@ -587,13 +587,10 @@ class _VideoDetailPageVState extends State buildTabBar(onTap: videoDetailController.animToTop), Expanded( child: tabBarView( + hitTestBehavior: .translucent, controller: videoDetailController.tabCtr, children: [ - videoIntro( - isHorizontal: false, - needCtr: false, - isNested: true, - ), + videoIntro(isHorizontal: false, needCtr: false), if (videoDetailController.showReply) videoReplyPanel(isNested: true), if (_shouldShowSeasonPanel) seasonPanel, @@ -1648,7 +1645,6 @@ class _VideoDetailPageVState extends State bool? isHorizontal, bool needRelated = true, bool needCtr = true, - bool isNested = false, }) { if (videoDetailController.isFileSource) { return localIntroPanel(needCtr: needCtr); @@ -1708,12 +1704,6 @@ class _VideoDetailPageVState extends State ), ], ); - if (isNested) { - child = ExtendedVisibilityDetector( - uniqueKey: const Key('intro-panel'), - child: child, - ); - } return KeepAliveWrapper(child: child); } diff --git a/lib/pages/video/view_point/view.dart b/lib/pages/video/view_point/view.dart index 3b3e3e500..072e8e3db 100644 --- a/lib/pages/video/view_point/view.dart +++ b/lib/pages/video/view_point/view.dart @@ -6,7 +6,6 @@ import 'package:PiliPlus/pages/common/slide/common_slide_page.dart'; import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; -import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -78,19 +77,17 @@ class _ViewPointsPageState extends State } late Key _key; - late bool _isNested; @override void didChangeDependencies() { super.didChangeDependencies(); final controller = PrimaryScrollController.of(context); - _isNested = controller is ExtendedNestedScrollController; _key = ValueKey(controller.hashCode); } @override Widget buildList(ThemeData theme) { - final child = ListView.builder( + return ListView.builder( key: _key, physics: const AlwaysScrollableScrollPhysics(), padding: EdgeInsets.only( @@ -112,13 +109,6 @@ class _ViewPointsPageState extends State return _buildItem(theme.colorScheme, segment, isCurr); }, ); - if (_isNested) { - return ExtendedVisibilityDetector( - uniqueKey: const ValueKey(ViewPointsPage), - child: child, - ); - } - return child; } Widget _buildItem( diff --git a/lib/scripts/patch.ps1 b/lib/scripts/patch.ps1 index 304a2d170..88d6f2718 100644 --- a/lib/scripts/patch.ps1 +++ b/lib/scripts/patch.ps1 @@ -67,7 +67,7 @@ $ScrollPositionPatch = "lib/scripts/scroll_position.patch" # expose `_shouldIgnorePointer` $ScrollablePatch = "lib/scripts/scrollable.patch" -$TabsPatch = "lib/scripts/tabs.patch" +$ScrollableGesturePatch = "lib/scripts/scrollable_gesture.patch" # TODO: remove # https://github.com/flutter/flutter/issues/124078 @@ -103,7 +103,7 @@ $patches = @($ModalBarrierPatch, $TextSelectionPatch, $MouseCursorPatch, $ImageAnimPatch, $LayoutBuilderPatch, $NavigationDrawerPatch, $PopupMenuPatch, $FABPatch, $NullSafetySelectableRegionPatch, $SelectableRegionPatch, $EditableTextPatch, $TextFieldPatch, - $ScrollPositionPatch, $ScrollablePatch, $TabsPatch) + $ScrollPositionPatch, $ScrollablePatch, $ScrollableGesturePatch) switch ($platform.ToLower()) { "android" { diff --git a/lib/scripts/scrollable_gesture.patch b/lib/scripts/scrollable_gesture.patch new file mode 100644 index 000000000..3d3911e55 --- /dev/null +++ b/lib/scripts/scrollable_gesture.patch @@ -0,0 +1,318 @@ +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) { diff --git a/lib/scripts/tabs.patch b/lib/scripts/tabs.patch deleted file mode 100644 index a60fe5113..000000000 --- a/lib/scripts/tabs.patch +++ /dev/null @@ -1,24 +0,0 @@ -diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart -index 87a611a6f60..acf799edbd3 100644 ---- a/packages/flutter/lib/src/material/tabs.dart -+++ b/packages/flutter/lib/src/material/tabs.dart -@@ -2222,8 +2222,11 @@ class TabBarView extends StatefulWidget { - this.dragStartBehavior = DragStartBehavior.start, - this.viewportFraction = 1.0, - this.clipBehavior = Clip.hardEdge, -+ this.scrollDirection = Axis.horizontal, - }); - -+ final Axis scrollDirection; -+ - /// This widget's selection and animation state. - /// - /// If [TabController] is not provided, then the value of [DefaultTabController.of] -@@ -2522,6 +2525,7 @@ class _TabBarViewState extends State { - return NotificationListener( - onNotification: _handleScrollNotification, - child: PageView( -+ scrollDirection: widget.scrollDirection, - dragStartBehavior: widget.dragStartBehavior, - clipBehavior: widget.clipBehavior, - controller: _pageController,