diff --git a/lib/common/widgets/flutter/page/page_view.dart b/lib/common/widgets/flutter/page/page_view.dart index 4a2a86ccb..175f774a9 100644 --- a/lib/common/widgets/flutter/page/page_view.dart +++ b/lib/common/widgets/flutter/page/page_view.dart @@ -11,7 +11,8 @@ library; import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart'; -import 'package:flutter/gestures.dart' show DragStartBehavior; +import 'package:flutter/gestures.dart' + show DragStartBehavior, HorizontalDragGestureRecognizer; import 'package:flutter/material.dart' hide Scrollable, ScrollableState; import 'package:flutter/rendering.dart'; @@ -41,18 +42,18 @@ const PageScrollPhysics _kPagePhysics = PageScrollPhysics(); /// /// 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 [CustomPageView], a [PageController] also lets you control the offset in terms +/// 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 -/// [CustomPageView] is first constructed, and the [PageController.viewportFraction], +/// [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 [CustomPageView]. It creates a centered [Text] in each of the three pages +/// 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 ** @@ -61,7 +62,7 @@ const PageScrollPhysics _kPagePhysics = PageScrollPhysics(); /// ## Persisting the scroll position during a session /// /// Scroll views attempt to persist their scroll position using [PageStorage]. -/// For a [CustomPageView], this can be disabled by setting [PageController.keepPage] +/// 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. @@ -74,7 +75,8 @@ const PageScrollPhysics _kPagePhysics = PageScrollPhysics(); /// * [GridView], for a scrollable grid of boxes. /// * [ScrollNotification] and [NotificationListener], which can be used to watch /// the scroll position without using a [ScrollController]. -class CustomPageView extends StatefulWidget { +class PageView + extends StatefulWidget { /// Creates a scrollable list that works page by page from an explicit [List] /// of widgets. /// @@ -88,12 +90,12 @@ class CustomPageView extends StatefulWidget { /// See the documentation at [SliverChildListDelegate.children] for more details. /// /// {@template flutter.widgets.PageView.allowImplicitScrolling} - /// If [allowImplicitScrolling] is true, the [CustomPageView] will participate in + /// 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 - /// [CustomPageView]. + /// [PageView]. /// {@endtemplate} - CustomPageView({ + PageView({ super.key, this.scrollDirection = Axis.horizontal, this.reverse = false, @@ -109,12 +111,10 @@ class CustomPageView extends StatefulWidget { this.hitTestBehavior = HitTestBehavior.opaque, this.scrollBehavior, this.padEnds = true, - this.header, - this.bgColor = Colors.transparent, + required this.horizontalDragGestureRecognizer, }) : childrenDelegate = SliverChildListDelegate(children); - final Widget? header; - final Color bgColor; + final T horizontalDragGestureRecognizer; /// Creates a scrollable list that works page by page using widgets that are /// created on demand. @@ -123,7 +123,7 @@ class CustomPageView extends StatefulWidget { /// number of children because the builder is called only for those children /// that are actually visible. /// - /// Providing a non-null [itemCount] lets the [CustomPageView] compute the maximum + /// 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 @@ -141,7 +141,7 @@ class CustomPageView extends StatefulWidget { /// {@endtemplate} /// /// {@macro flutter.widgets.PageView.allowImplicitScrolling} - CustomPageView.builder({ + PageView.builder({ super.key, this.scrollDirection = Axis.horizontal, this.reverse = false, @@ -159,8 +159,7 @@ class CustomPageView extends StatefulWidget { this.hitTestBehavior = HitTestBehavior.opaque, this.scrollBehavior, this.padEnds = true, - this.header, - this.bgColor = Colors.transparent, + required this.horizontalDragGestureRecognizer, }) : childrenDelegate = SliverChildBuilderDelegate( itemBuilder, findChildIndexCallback: findChildIndexCallback, @@ -171,14 +170,14 @@ class CustomPageView extends StatefulWidget { /// model. /// /// {@tool dartpad} - /// This example shows a [CustomPageView] that uses a custom [SliverChildBuilderDelegate] to support child + /// 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} - const CustomPageView.custom({ + const PageView.custom({ super.key, this.scrollDirection = Axis.horizontal, this.reverse = false, @@ -194,8 +193,7 @@ class CustomPageView extends StatefulWidget { this.hitTestBehavior = HitTestBehavior.opaque, this.scrollBehavior, this.padEnds = true, - this.header, - this.bgColor = Colors.transparent, + required this.horizontalDragGestureRecognizer, }); /// Controls whether the widget's pages will respond to @@ -265,10 +263,10 @@ class CustomPageView extends StatefulWidget { /// Called whenever the page in the center of the viewport changes. final ValueChanged? onPageChanged; - /// A delegate that provides the children for the [CustomPageView]. + /// A delegate that provides the children for the [PageView]. /// /// The [PageView.custom] constructor lets you specify this delegate - /// explicitly. The [CustomPageView] and [PageView.builder] constructors create a + /// explicitly. The [PageView] and [PageView.builder] constructors create a /// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder], /// respectively. final SliverChildDelegate childrenDelegate; @@ -304,10 +302,11 @@ class CustomPageView extends StatefulWidget { final bool padEnds; @override - State createState() => _CustomPageViewState(); + State> createState() => _PageViewState(); } -class _CustomPageViewState extends State { +class _PageViewState + extends State> { int _lastReportedPage = 0; late PageController _controller; @@ -332,7 +331,7 @@ class _CustomPageViewState extends State { } @override - void didUpdateWidget(CustomPageView oldWidget) { + void didUpdateWidget(PageView oldWidget) { if (oldWidget.controller != widget.controller) { if (oldWidget.controller == null) { _controller.dispose(); @@ -388,9 +387,7 @@ class _CustomPageViewState extends State { } return false; }, - child: CustomScrollable( - header: widget.header, - bgColor: widget.bgColor, + child: Scrollable( dragStartBehavior: widget.dragStartBehavior, axisDirection: axisDirection, controller: _controller, @@ -419,6 +416,7 @@ class _CustomPageViewState extends State { ], ); }, + horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer, ), ); } diff --git a/lib/common/widgets/flutter/page/scrollable.dart b/lib/common/widgets/flutter/page/scrollable.dart index 7c13b568c..3d5ec7439 100644 --- a/lib/common/widgets/flutter/page/scrollable.dart +++ b/lib/common/widgets/flutter/page/scrollable.dart @@ -28,32 +28,30 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -export 'package:flutter/physics.dart' show Tolerance; - // 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>, CustomScrollableState); +typedef _EnsureVisibleResults = (List>, ScrollableState); /// A widget that manages scrolling in one dimension and informs the [Viewport] /// through which the content is viewed. /// -/// [CustomScrollable] implements the interaction model for a scrollable widget, +/// [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 [CustomScrollable] directly. Instead, consider [ListView] +/// 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 [CustomScrollable.of] and [CustomScrollable.ensureVisible] functions are -/// often used to interact with the [CustomScrollable] widget inside a [ListView] or +/// 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 [CustomScrollable]: +/// 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 @@ -63,7 +61,7 @@ typedef _EnsureVisibleResults = (List>, CustomScrollableState); /// 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 [CustomScrollable] resizes. +/// that keeps the same page visible when the [Scrollable] resizes. /// /// ## Persisting the scroll position during a session /// @@ -71,7 +69,7 @@ typedef _EnsureVisibleResults = (List>, CustomScrollableState); /// 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 [CustomScrollable]s from each other. +/// recommended to help disambiguate different [Scrollable]s from each other. /// /// See also: /// @@ -87,9 +85,10 @@ typedef _EnsureVisibleResults = (List>, CustomScrollableState); /// child. /// * [ScrollNotification] and [NotificationListener], which can be used to watch /// the scroll position without using a [ScrollController]. -class CustomScrollable extends StatefulWidget { +class Scrollable + extends StatefulWidget { /// Creates a widget that scrolls. - const CustomScrollable({ + const Scrollable({ super.key, this.axisDirection = AxisDirection.down, this.controller, @@ -103,19 +102,15 @@ class CustomScrollable extends StatefulWidget { this.scrollBehavior, this.clipBehavior = Clip.hardEdge, this.hitTestBehavior = HitTestBehavior.opaque, - this.enableSlide, - this.header, - this.bgColor = Colors.transparent, + required this.horizontalDragGestureRecognizer, }) : assert(semanticChildCount == null || semanticChildCount >= 0); - final Widget? header; - final bool? enableSlide; - final Color bgColor; + final T horizontalDragGestureRecognizer; /// {@template flutter.widgets.Scrollable.axisDirection} /// The direction in which this widget scrolls. /// - /// For example, if the [CustomScrollable.axisDirection] is [AxisDirection.down], + /// 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 @@ -138,12 +133,12 @@ class CustomScrollable extends StatefulWidget { /// scroll position (see [ScrollController.offset]), or change it (see /// [ScrollController.animateTo]). /// - /// If null, a [ScrollController] will be created internally by [CustomScrollable] + /// If null, a [ScrollController] will be created internally by [Scrollable] /// in order to create and manage the [ScrollPosition]. /// /// See also: /// - /// * [CustomScrollable.ensureVisible], which animates the scroll position to + /// * [Scrollable.ensureVisible], which animates the scroll position to /// reveal a given [BuildContext]. /// {@endtemplate} final ScrollController? controller; @@ -158,8 +153,8 @@ class CustomScrollable extends StatefulWidget { /// the ambient [ScrollConfiguration]. /// /// If an explicit [ScrollBehavior] is provided to - /// [CustomScrollable.scrollBehavior], the [ScrollPhysics] provided by that behavior - /// will take precedence after [CustomScrollable.physics]. + /// [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 @@ -194,7 +189,7 @@ class CustomScrollable extends StatefulWidget { /// scroll when the scrollable is asked to scroll via the keyboard using a /// [ScrollAction]. /// - /// If not supplied, the [CustomScrollable] will scroll a default amount when a + /// 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]. /// @@ -205,7 +200,7 @@ class CustomScrollable extends StatefulWidget { final ScrollIncrementCalculator? incrementCalculator; /// {@template flutter.widgets.scrollable.excludeFromSemantics} - /// Whether the scroll actions introduced by this [CustomScrollable] are exposed + /// 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 @@ -220,10 +215,10 @@ class CustomScrollable extends StatefulWidget { final bool excludeFromSemantics; /// {@template flutter.widgets.scrollable.hitTestBehavior} - /// Defines the behavior of gesture detector used in this [CustomScrollable]. + /// Defines the behavior of gesture detector used in this [Scrollable]. /// /// This defaults to [HitTestBehavior.opaque] which means it prevents targets - /// behind this [CustomScrollable] from receiving events. + /// behind this [Scrollable] from receiving events. /// {@endtemplate} /// /// See also: @@ -305,7 +300,7 @@ class CustomScrollable extends StatefulWidget { /// Defaults to [Clip.hardEdge]. /// /// This is passed to decorators in [ScrollableDetails], and does not directly affect - /// clipping of the [CustomScrollable]. This reflects the same [Clip] that is provided + /// clipping of the [Scrollable]. This reflects the same [Clip] that is provided /// to [ScrollView.clipBehavior] and is supplied to the [Viewport]. final Clip clipBehavior; @@ -315,7 +310,7 @@ class CustomScrollable extends StatefulWidget { Axis get axis => axisDirectionToAxis(axisDirection); @override - CustomScrollableState createState() => CustomScrollableState(); + ScrollableState createState() => ScrollableState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -335,31 +330,31 @@ class CustomScrollable extends StatefulWidget { /// ScrollableState? scrollable = Scrollable.maybeOf(context); /// ``` /// - /// Calling this method will create a dependency on the [CustomScrollableState] + /// Calling this method will create a dependency on the [ScrollableState] /// that is returned, if there is one. This is typically the closest - /// [CustomScrollable], but may be a more distant ancestor if [axis] is used to - /// target a specific [CustomScrollable]. + /// [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 [CustomScrollable] is not the closest instance. When [axis] is provided, - /// the nearest enclosing [CustomScrollableState] in that [Axis] is returned, or + /// 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_ [CustomScrollable] of the `context`. This - /// means that if the `context` is that of a [CustomScrollable], it will _not_ find - /// _that_ [CustomScrollable]. + /// 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: /// - /// * [CustomScrollable.of], which is similar to this method, but asserts - /// if no [CustomScrollable] ancestor is found. - static CustomScrollableState? maybeOf(BuildContext context, {Axis? axis}) { + /// * [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 BuildContext originalContext = context; InheritedElement? element = context .getElementForInheritedWidgetOfExactType<_ScrollableScope>(); while (element != null) { - final CustomScrollableState scrollable = + final ScrollableState scrollable = (element.widget as _ScrollableScope).scrollable; if (axis == null || axisDirectionToAxis(scrollable.axisDirection) == axis) { @@ -383,28 +378,28 @@ class CustomScrollable extends StatefulWidget { /// ScrollableState scrollable = Scrollable.of(context); /// ``` /// - /// Calling this method will create a dependency on the [CustomScrollableState] + /// Calling this method will create a dependency on the [ScrollableState] /// that is returned, if there is one. This is typically the closest - /// [CustomScrollable], but may be a more distant ancestor if [axis] is used to - /// target a specific [CustomScrollable]. + /// [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 [CustomScrollable] is not the closest instance. When [axis] is provided, - /// the nearest enclosing [CustomScrollableState] in that [Axis] is returned. + /// 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_ [CustomScrollable] of the `context`. This - /// means that if the `context` is that of a [CustomScrollable], it will _not_ find - /// _that_ [CustomScrollable]. + /// 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 [CustomScrollable] ancestor is found, then this method will assert in + /// If no [Scrollable] ancestor is found, then this method will assert in /// debug mode, and throw an exception in release mode. /// /// See also: /// - /// * [CustomScrollable.maybeOf], which is similar to this method, but returns null - /// if no [CustomScrollable] ancestor is found. - static CustomScrollableState of(BuildContext context, {Axis? axis}) { - final CustomScrollableState? scrollableState = maybeOf(context, axis: axis); + /// * [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([ @@ -440,16 +435,16 @@ class CustomScrollable extends StatefulWidget { /// 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 [CustomScrollable] + /// 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 [CustomScrollable] of that + /// 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 - /// [CustomScrollable] in that [Axis]. + /// [Scrollable] in that [Axis]. /// - /// If there is no [CustomScrollable] in the widget tree above the [context], this + /// If there is no [Scrollable] in the widget tree above the [context], this /// method returns false. static bool recommendDeferredLoadingForContext( BuildContext context, { @@ -471,7 +466,7 @@ class CustomScrollable extends StatefulWidget { /// Scrolls all scrollables that enclose the given context so as to make the /// given context visible. /// - /// If a [CustomScrollable] enclosing the provided [BuildContext] is a + /// 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( @@ -492,7 +487,7 @@ class CustomScrollable extends StatefulWidget { // // Also see https://github.com/flutter/flutter/issues/65100 RenderObject? targetRenderObject; - CustomScrollableState? scrollable = CustomScrollable.maybeOf(context); + ScrollableState? scrollable = Scrollable.maybeOf(context); while (scrollable != null) { final List> newFutures; (newFutures, scrollable) = scrollable._performEnsureVisible( @@ -507,7 +502,7 @@ class CustomScrollable extends StatefulWidget { targetRenderObject ??= context.findRenderObject(); context = scrollable.context; - scrollable = CustomScrollable.maybeOf(context); + scrollable = Scrollable.maybeOf(context); } if (futures.isEmpty || duration == Duration.zero) { @@ -529,7 +524,7 @@ class _ScrollableScope extends InheritedWidget { required super.child, }); - final CustomScrollableState scrollable; + final ScrollableState scrollable; final ScrollPosition position; @override @@ -538,30 +533,31 @@ class _ScrollableScope extends InheritedWidget { } } -/// State object for a [CustomScrollable] widget. +/// State object for a [Scrollable] widget. /// -/// To manipulate a [CustomScrollable] widget's scroll position, use the object +/// To manipulate a [Scrollable] widget's scroll position, use the object /// obtained from the [position] property. /// -/// To be informed of when a [CustomScrollable] widget is scrolling, use a +/// 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 -/// [CustomScrollable], provide it with a [ScrollPhysics]. -class CustomScrollableState extends State +/// [Scrollable], provide it with a [ScrollPhysics]. +class ScrollableState + extends State> with TickerProviderStateMixin, RestorationMixin implements ScrollContext { // GETTERS - /// The manager for this [CustomScrollable] widget's viewport position. + /// The manager for this [Scrollable] widget's viewport position. /// - /// To control what kind of [ScrollPosition] is created for a [CustomScrollable], + /// 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 [CustomScrollableState]. + /// The resolved [ScrollPhysics] of the [ScrollableState]. ScrollPhysics? get resolvedPhysics => _physics; ScrollPhysics? _physics; @@ -661,10 +657,6 @@ class CustomScrollableState extends State _fallbackScrollController = ScrollController(); } super.initState(); - _animController = AnimationController( - vsync: this, - reverseDuration: const Duration(milliseconds: 500), - ); } @protected @@ -678,7 +670,7 @@ class CustomScrollableState extends State super.didChangeDependencies(); } - bool _shouldUpdatePosition(CustomScrollable oldWidget) { + bool _shouldUpdatePosition(Scrollable oldWidget) { if ((widget.scrollBehavior == null) != (oldWidget.scrollBehavior == null)) { return true; } @@ -705,7 +697,7 @@ class CustomScrollableState extends State @protected @override - void didUpdateWidget(CustomScrollable oldWidget) { + void didUpdateWidget(Scrollable oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { @@ -747,8 +739,6 @@ class CustomScrollableState extends State position.dispose(); _persistedScrollOffset.dispose(); - - _animController.dispose(); super.dispose(); } @@ -778,12 +768,6 @@ class CustomScrollableState extends State bool? _lastCanDrag; Axis? _lastAxisDirection; - late bool _isRTL = false; - Offset? _downPos; - bool? _isSliding; - - late AnimationController _animController; - @override @protected void setCanDrag(bool value) { @@ -830,32 +814,27 @@ class CustomScrollableState extends State }; case Axis.horizontal: _gestureRecognizers = { - HorizontalDragGestureRecognizer: - GestureRecognizerFactoryWithHandlers< - HorizontalDragGestureRecognizer - >( - () => HorizontalDragGestureRecognizer( - supportedDevices: _configuration.dragDevices, - ), - (HorizontalDragGestureRecognizer 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; - }, - ), + 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; + }, + ), }; } } @@ -889,65 +868,12 @@ class CustomScrollableState extends State ScrollHoldController? _hold; void _handleDragDown(DragDownDetails details) { - final dx = details.localPosition.dx; - const offset = 30; - final isLTR = dx <= offset; - final isRTL = dx >= _maxWidth - offset; - if (isLTR || isRTL) { - _isRTL = isRTL; - _downPos = details.localPosition; - return; - } assert(_drag == null); assert(_hold == null); _hold = position.hold(_disposeHold); } - void _onPan(Offset localPosition) { - if (_isSliding == false) { - return; - } else if (_isSliding == null) { - if (_downPos != null) { - Offset cumulativeDelta = localPosition - _downPos!; - if (cumulativeDelta.dx.abs() >= cumulativeDelta.dy.abs()) { - _downPos = localPosition; - _isSliding = true; - } else { - _downPos = null; - _isSliding = false; - } - } else { - _downPos = null; - _isSliding = false; - } - } else if (_isSliding == true) { - final from = _downPos!.dx; - final to = localPosition.dx; - _animController.value = - math.max(0, _isRTL ? from - to : to - from) / _maxWidth; - } - } - - void _onDismiss() { - if (_isSliding == true) { - final dx = _downPos!.dx; - if (_animController.value * _maxWidth + - (_isRTL ? (_maxWidth - dx) : dx) >= - 100) { - Navigator.pop(context); - } else { - _animController.reverse(); - } - } - _downPos = null; - _isSliding = null; - } - void _handleDragStart(DragStartDetails details) { - if (_downPos != null) { - _onPan(details.localPosition); - return; - } // 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. @@ -961,20 +887,12 @@ class CustomScrollableState extends State } void _handleDragUpdate(DragUpdateDetails details) { - if (_downPos != null) { - _onPan(details.localPosition); - return; - } // _drag might be null if the drag activity ended and called _disposeDrag. assert(_hold == null || _drag == null); _drag?.update(details); } void _handleDragEnd(DragEndDetails details) { - if (_downPos != null) { - _onDismiss(); - return; - } // _drag might be null if the drag activity ended and called _disposeDrag. assert(_hold == null || _drag == null); _drag?.end(details); @@ -982,10 +900,6 @@ class CustomScrollableState extends State } void _handleDragCancel() { - if (_downPos != null) { - _onDismiss(); - return; - } 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 @@ -1099,8 +1013,6 @@ class CustomScrollableState extends State return false; } - late double _maxWidth; - Widget _buildChrome(BuildContext context, Widget child) { final ScrollableDetails details = ScrollableDetails( direction: widget.axisDirection, @@ -1178,32 +1090,7 @@ class CustomScrollableState extends State ); } - return LayoutBuilder( - builder: (context, constraints) { - _maxWidth = constraints.maxWidth; - return AnimatedBuilder( - animation: _animController, - builder: (context, child) { - return Align( - alignment: AlignmentDirectional.topStart, - heightFactor: 1 - _animController.value, - child: child, - ); - }, - child: Material( - color: widget.bgColor, - child: widget.header != null - ? Column( - children: [ - widget.header!, - Expanded(child: result), - ], - ) - : result, - ), - ); - }, - ); + return result; } // Returns the Future from calling ensureVisible for the ScrollPosition, as @@ -1251,7 +1138,7 @@ class _ScrollableSelectionHandler extends StatefulWidget { required this.child, }); - final CustomScrollableState state; + final ScrollableState state; final ScrollPosition position; final Widget child; final SelectionRegistrar registrar; @@ -1324,7 +1211,7 @@ class _ScrollableSelectionContainerDelegate // An eye-balled value for a smooth scrolling speed. static const double _kDefaultSelectToScrollVelocityScalar = 30; - final CustomScrollableState state; + final ScrollableState state; final EdgeDraggingAutoScroller _autoScroller; bool _scheduledLayoutChange = false; Offset? _currentDragStartRelatedToOrigin; @@ -1776,7 +1663,7 @@ class _ScrollableSelectionContainerDelegate } } -Offset _getDeltaToScrollOrigin(CustomScrollableState scrollableState) { +Offset _getDeltaToScrollOrigin(ScrollableState scrollableState) { return switch (scrollableState.axisDirection) { AxisDirection.up => Offset(0, -scrollableState.position.pixels), AxisDirection.down => Offset(0, scrollableState.position.pixels), @@ -1795,7 +1682,7 @@ Offset _getDeltaToScrollOrigin(CustomScrollableState scrollableState) { /// [RenderObject.describeSemanticsConfiguration]. /// /// If the tag [RenderViewport.useTwoPaneSemantics] is present on the viewport, -/// two semantics nodes will be used to represent the [CustomScrollable]: The outer +/// 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. diff --git a/lib/common/widgets/flutter/page/scrollable_helpers.dart b/lib/common/widgets/flutter/page/scrollable_helpers.dart index 0ba394c79..895de8852 100644 --- a/lib/common/widgets/flutter/page/scrollable_helpers.dart +++ b/lib/common/widgets/flutter/page/scrollable_helpers.dart @@ -13,7 +13,7 @@ import 'dart:math' as math; import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide ScrollableState; /// An auto scroller that scrolls the [scrollable] if a drag gesture drags close /// to its edge. @@ -30,7 +30,7 @@ class EdgeDraggingAutoScroller { }); /// The [CustomScrollable] this auto scroller is scrolling. - final CustomScrollableState scrollable; + final ScrollableState scrollable; /// Called when a scroll view is scrolled. /// diff --git a/lib/common/widgets/flutter/page/tabs.dart b/lib/common/widgets/flutter/page/tabs.dart index 60cd0c4ad..133d7f34e 100644 --- a/lib/common/widgets/flutter/page/tabs.dart +++ b/lib/common/widgets/flutter/page/tabs.dart @@ -2,12 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show SemanticsRole; - import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart'; import 'package:flutter/foundation.dart' show clampDouble; -import 'package:flutter/gestures.dart' show DragStartBehavior; -import 'package:flutter/material.dart' hide TabBarView, PageView; +import 'package:flutter/gestures.dart' + show DragStartBehavior, HorizontalDragGestureRecognizer; +import 'package:flutter/material.dart' hide PageView; /// A page view that displays the widget which corresponds to the currently /// selected tab. @@ -23,11 +22,12 @@ import 'package:flutter/material.dart' hide TabBarView, PageView; /// [children] list and the length of the [TabBar.tabs] list. /// /// To see a sample implementation, visit the [TabController] documentation. -class CustomTabBarView extends StatefulWidget { +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 CustomTabBarView({ + const TabBarView({ super.key, required this.children, this.controller, @@ -35,13 +35,10 @@ class CustomTabBarView extends StatefulWidget { this.dragStartBehavior = DragStartBehavior.start, this.viewportFraction = 1.0, this.clipBehavior = Clip.hardEdge, - this.scrollDirection = Axis.horizontal, - this.header, - this.bgColor = Colors.transparent, + required this.horizontalDragGestureRecognizer, }); - final Widget? header; - final Color bgColor; + final T horizontalDragGestureRecognizer; /// This widget's selection and animation state. /// @@ -77,13 +74,12 @@ class CustomTabBarView extends StatefulWidget { /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; - final Axis scrollDirection; - @override - State createState() => _CustomTabBarViewState(); + State> createState() => _TabBarViewState(); } -class _CustomTabBarViewState extends State { +class _TabBarViewState + extends State> { TabController? _controller; PageController? _pageController; late List _childrenWithKey; @@ -168,7 +164,7 @@ class _CustomTabBarViewState extends State { } @override - void didUpdateWidget(CustomTabBarView oldWidget) { + void didUpdateWidget(TabBarView oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { _updateTabController(); @@ -203,7 +199,7 @@ class _CustomTabBarViewState extends State { void _updateChildren() { _childrenWithKey = KeyedSubtree.ensureUniqueKeysForList( widget.children.map((Widget child) { - return Semantics(role: SemanticsRole.tabPanel, child: child); + return Semantics(role: .tabPanel, child: child); }).toList(), ); } @@ -361,16 +357,14 @@ class _CustomTabBarViewState extends State { return NotificationListener( onNotification: _handleScrollNotification, - child: CustomPageView( - scrollDirection: widget.scrollDirection, + child: PageView( dragStartBehavior: widget.dragStartBehavior, clipBehavior: widget.clipBehavior, controller: _pageController, physics: widget.physics == null ? const PageScrollPhysics().applyTo(const ClampingScrollPhysics()) : const PageScrollPhysics().applyTo(widget.physics), - header: widget.header, - bgColor: widget.bgColor, + horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer, children: _childrenWithKey, ), ); diff --git a/lib/common/widgets/flutter/tabs.dart b/lib/common/widgets/flutter/tabs.dart index cf3807362..1a9dc0152 100644 --- a/lib/common/widgets/flutter/tabs.dart +++ b/lib/common/widgets/flutter/tabs.dart @@ -4,9 +4,11 @@ import 'dart:ui' show SemanticsRole; +import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart'; +import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/gestures.dart' show DragStartBehavior; -import 'package:flutter/material.dart' hide TabBarView; +import 'package:flutter/material.dart' hide TabBarView, PageView; /// A page view that displays the widget which corresponds to the currently /// selected tab. @@ -355,7 +357,7 @@ class _CustomTabBarViewState extends State { return NotificationListener( onNotification: _handleScrollNotification, - child: PageView( + child: PageView( scrollDirection: widget.scrollDirection, dragStartBehavior: widget.dragStartBehavior, clipBehavior: widget.clipBehavior, @@ -363,6 +365,8 @@ class _CustomTabBarViewState extends State { physics: widget.physics == null ? const PageScrollPhysics().applyTo(const ClampingScrollPhysics()) : const PageScrollPhysics().applyTo(widget.physics), + horizontalDragGestureRecognizer: + CustomHorizontalDragGestureRecognizer(), children: _childrenWithKey, ), ); diff --git a/lib/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart b/lib/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart new file mode 100644 index 000000000..e79cdef08 --- /dev/null +++ b/lib/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart @@ -0,0 +1,52 @@ +import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:flutter/gestures.dart'; + +class CustomHorizontalDragGestureRecognizer + extends HorizontalDragGestureRecognizer { + CustomHorizontalDragGestureRecognizer({ + super.debugOwner, + super.supportedDevices, + super.allowedButtonsFilter, + }); + + Offset? _initialPosition; + + @override + void addAllowedPointer(PointerDownEvent event) { + super.addAllowedPointer(event); + _initialPosition = event.position; + } + + @override + bool hasSufficientGlobalDistanceToAccept( + PointerDeviceKind pointerDeviceKind, + double? deviceTouchSlop, + ) { + return globalDistanceMoved.abs() > _computeHitSlop(pointerDeviceKind) && + _cacl(_initialPosition!, lastPosition.global, gestureSettings); + } + + static bool _cacl( + Offset initialPosition, + Offset lastPosition, + DeviceGestureSettings? gestureSettings, + ) { + final offset = lastPosition - initialPosition; + return offset.dx.abs() > offset.dy.abs() * 3; + } +} + +double touchSlopH = Pref.touchSlopH; + +double _computeHitSlop(PointerDeviceKind kind) { + switch (kind) { + case PointerDeviceKind.mouse: + return kPrecisePointerHitSlop; + case PointerDeviceKind.stylus: + case PointerDeviceKind.invertedStylus: + case PointerDeviceKind.unknown: + case PointerDeviceKind.touch: + case PointerDeviceKind.trackpad: + return touchSlopH; + } +} diff --git a/lib/common/widgets/scroll_physics.dart b/lib/common/widgets/scroll_physics.dart index f4dbb5fae..60f402afe 100644 --- a/lib/common/widgets/scroll_physics.dart +++ b/lib/common/widgets/scroll_physics.dart @@ -1,23 +1,25 @@ +import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart'; +import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide TabBarView; Widget videoTabBarView({ required List children, TabController? controller, -}) => TabBarView( - physics: const CustomTabBarViewScrollPhysics( - parent: ClampingScrollPhysics(), - ), +}) => TabBarView( controller: controller, + physics: const CustomTabBarViewScrollPhysics(parent: ClampingScrollPhysics()), + horizontalDragGestureRecognizer: CustomHorizontalDragGestureRecognizer(), children: children, ); Widget tabBarView({ required List children, TabController? controller, -}) => TabBarView( +}) => TabBarView( physics: const CustomTabBarViewScrollPhysics(), controller: controller, + horizontalDragGestureRecognizer: CustomHorizontalDragGestureRecognizer(), children: children, ); diff --git a/lib/pages/common/slide/common_slide_page.dart b/lib/pages/common/slide/common_slide_page.dart index b4bf3df76..a991e9906 100644 --- a/lib/pages/common/slide/common_slide_page.dart +++ b/lib/pages/common/slide/common_slide_page.dart @@ -1,5 +1,6 @@ import 'dart:math' show max; +import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/gestures.dart' show HorizontalDragGestureRecognizer; import 'package:flutter/material.dart'; @@ -12,8 +13,10 @@ abstract class CommonSlidePage extends StatefulWidget { } mixin CommonSlideMixin on State, TickerProvider { + static const double offset = 30.0; double? _downDx; late double _maxWidth; + double get maxWidth => _maxWidth; late bool _isRTL = false; late final bool enableSlide; late final AnimationController _animController; @@ -33,7 +36,6 @@ mixin CommonSlideMixin on State, TickerProvider { _slideDragGestureRecognizer = SlideDragGestureRecognizer( isDxAllowed: (double dx) { - const offset = 30; final isLTR = dx <= offset; final isRTL = dx >= _maxWidth - offset; if (isLTR || isRTL) { @@ -111,6 +113,8 @@ mixin CommonSlideMixin on State, TickerProvider { ); } +typedef IsDxAllowed = bool Function(double dx); + class SlideDragGestureRecognizer extends HorizontalDragGestureRecognizer { SlideDragGestureRecognizer({ super.debugOwner, @@ -119,7 +123,24 @@ class SlideDragGestureRecognizer extends HorizontalDragGestureRecognizer { required this.isDxAllowed, }); - final bool Function(double dx) isDxAllowed; + final IsDxAllowed isDxAllowed; + + @override + bool isPointerAllowed(PointerEvent event) { + return isDxAllowed(event.localPosition.dx) && super.isPointerAllowed(event); + } +} + +class TabBarDragGestureRecognizer + extends CustomHorizontalDragGestureRecognizer { + TabBarDragGestureRecognizer({ + super.debugOwner, + super.supportedDevices, + super.allowedButtonsFilter, + required this.isDxAllowed, + }); + + final IsDxAllowed isDxAllowed; @override bool isPointerAllowed(PointerEvent event) { diff --git a/lib/pages/episode_panel/view.dart b/lib/pages/episode_panel/view.dart index e1cdd9552..8bb88e3c8 100644 --- a/lib/pages/episode_panel/view.dart +++ b/lib/pages/episode_panel/view.dart @@ -7,7 +7,6 @@ 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/stat/stat.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -205,30 +204,42 @@ class _EpisodePanelState extends State super.dispose(); } + late final _isMulti = + widget.type == EpisodeType.season && widget.list.length > 1; + @override Widget buildPage(ThemeData theme) { - final isMulti = widget.type == EpisodeType.season && widget.list.length > 1; - - Widget tabBar() => TabBar( - controller: _tabController, - padding: const EdgeInsets.only(right: 60), - isScrollable: true, - tabs: widget.list.map((item) => Tab(text: item.title)).toList(), - dividerHeight: 1, - dividerColor: theme.dividerColor.withValues(alpha: 0.1), + return Material( + color: showTitle ? theme.colorScheme.surface : null, + type: showTitle ? MaterialType.canvas : MaterialType.transparency, + child: Column( + children: [ + _buildToolbar(theme), + if (_isMulti) + TabBar( + controller: _tabController, + padding: const EdgeInsets.only(right: 60), + isScrollable: true, + tabs: widget.list.map((item) => Tab(text: item.title)).toList(), + dividerHeight: 1, + dividerColor: theme.dividerColor.withValues(alpha: 0.1), + ), + Expanded(child: enableSlide ? slideList(theme) : buildList(theme)), + ], + ), ); + } - if (isMulti && enableSlide) { - return CustomTabBarView( + @override + Widget buildList(ThemeData theme) { + if (_isMulti) { + return TabBarView( controller: _tabController, - physics: const CustomTabBarViewScrollPhysics(), - bgColor: theme.colorScheme.surface, - header: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _buildToolbar(theme), - tabBar(), - ], + horizontalDragGestureRecognizer: TabBarDragGestureRecognizer( + isDxAllowed: (double dx) => enableSlide + ? dx > CommonSlideMixin.offset && + dx < maxWidth - CommonSlideMixin.offset + : true, ), children: List.generate( widget.list.length, @@ -240,37 +251,6 @@ class _EpisodePanelState extends State ), ); } - - return Material( - color: showTitle ? theme.colorScheme.surface : null, - type: showTitle ? MaterialType.canvas : MaterialType.transparency, - child: Column( - children: [ - _buildToolbar(theme), - if (isMulti) ...[ - tabBar(), - Expanded( - child: tabBarView( - controller: _tabController, - children: List.generate( - widget.list.length, - (index) => _buildBody( - theme, - index, - widget.list[index].episodes, - ), - ), - ), - ), - ] else - Expanded(child: enableSlide ? slideList(theme) : buildList(theme)), - ], - ), - ); - } - - @override - Widget buildList(ThemeData theme) { return _buildBody(theme, 0, _getCurrEpisodes); } diff --git a/lib/pages/history/view.dart b/lib/pages/history/view.dart index 15fac0121..5a81b5268 100644 --- a/lib/pages/history/view.dart +++ b/lib/pages/history/view.dart @@ -1,5 +1,7 @@ import 'package:PiliPlus/common/widgets/appbar/appbar.dart'; +import 'package:PiliPlus/common/widgets/flutter/page/tabs.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'; @@ -10,7 +12,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'; +import 'package:flutter/material.dart' hide TabBarView; import 'package:get/get.dart'; class HistoryPage extends StatefulWidget { @@ -130,6 +132,8 @@ class _HistoryPageState extends State ? const NeverScrollableScrollPhysics() : const CustomTabBarViewScrollPhysics(), controller: _historyController.tabController, + horizontalDragGestureRecognizer: + CustomHorizontalDragGestureRecognizer(), children: [ KeepAliveWrapper(builder: (context) => child), ..._historyController.tabs.map( diff --git a/lib/pages/later/view.dart b/lib/pages/later/view.dart index 88d4a56af..dfb0bd094 100644 --- a/lib/pages/later/view.dart +++ b/lib/pages/later/view.dart @@ -1,4 +1,6 @@ import 'package:PiliPlus/common/widgets/appbar/appbar.dart'; +import 'package:PiliPlus/common/widgets/flutter/page/tabs.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/view_safe_area.dart'; import 'package:PiliPlus/models/common/later_view_type.dart'; @@ -11,7 +13,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'; +import 'package:flutter/material.dart' hide TabBarView; import 'package:get/get.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @@ -137,6 +139,8 @@ class _LaterPageState extends State ? const NeverScrollableScrollPhysics() : const CustomTabBarViewScrollPhysics(), controller: _tabController, + horizontalDragGestureRecognizer: + CustomHorizontalDragGestureRecognizer(), children: LaterViewType.values .map((item) => item.page) .toList(), diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index 79ad395d0..f7e72a28d 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -5,7 +5,9 @@ import 'dart:ui'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.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/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/scroll_physics.dart'; @@ -41,7 +43,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:floating/floating.dart'; import 'package:flutter/foundation.dart' show kDebugMode; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide PageView; 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'; @@ -721,7 +723,7 @@ class _LiveRoomPageState extends State return Padding( padding: EdgeInsets.only(bottom: 12, top: isPortrait ? 12 : 0), child: _liveRoomController.showSuperChat - ? PageView( + ? PageView( key: pageKey, controller: _liveRoomController.pageController, physics: const CustomTabBarViewScrollPhysics( @@ -729,6 +731,8 @@ class _LiveRoomPageState extends State ), onPageChanged: (value) => _liveRoomController.pageIndex.value = value, + horizontalDragGestureRecognizer: + CustomHorizontalDragGestureRecognizer(), children: [ KeepAliveWrapper(builder: (context) => chat()), SuperChatPanel( diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index c6f66d8dd..87a636d91 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -1,8 +1,10 @@ import 'dart:io'; import 'package:PiliPlus/common/constants.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/tabs.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/models/common/nav_bar_config.dart'; import 'package:PiliPlus/pages/home/view.dart'; @@ -18,7 +20,7 @@ import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide PageView; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:tray_manager/tray_manager.dart'; @@ -393,9 +395,11 @@ class _MainAppState extends PopScopeState children: _mainController.navigationBars.map((i) => i.page).toList(), ); } else { - child = PageView( + child = PageView( physics: const NeverScrollableScrollPhysics(), controller: _mainController.controller, + horizontalDragGestureRecognizer: + CustomHorizontalDragGestureRecognizer(), children: _mainController.navigationBars.map((i) => i.page).toList(), ); } diff --git a/lib/pages/setting/models/extra_settings.dart b/lib/pages/setting/models/extra_settings.dart index b10491f8a..5ad9f7268 100644 --- a/lib/pages/setting/models/extra_settings.dart +++ b/lib/pages/setting/models/extra_settings.dart @@ -3,6 +3,8 @@ import 'dart:math' show pi, max; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart' + show touchSlopH; import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart' show CustomGridView, ImageModel; import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; @@ -187,11 +189,9 @@ List get extraSettings => [ leading: const Icon(Icons.notifications_none), setKey: SettingBoxKey.checkDynamic, defaultVal: true, - onChanged: (value) { - Get.find().checkDynamic = value; - }, + onChanged: (value) => Get.find().checkDynamic = value, onTap: (context) { - int dynamicPeriod = Pref.dynamicPeriod; + String dynamicPeriod = Pref.dynamicPeriod.toString(); showDialog( context: context, builder: (context) { @@ -199,11 +199,9 @@ List get extraSettings => [ title: const Text('检查周期'), content: TextFormField( autofocus: true, - initialValue: dynamicPeriod.toString(), + initialValue: dynamicPeriod, keyboardType: TextInputType.number, - onChanged: (value) { - dynamicPeriod = int.tryParse(value) ?? 5; - }, + onChanged: (value) => dynamicPeriod = value, inputFormatters: [FilteringTextInputFormatter.digitsOnly], decoration: const InputDecoration(suffixText: 'min'), ), @@ -219,13 +217,14 @@ List get extraSettings => [ ), TextButton( onPressed: () { - Get.back(); - GStorage.setting.put( - SettingBoxKey.dynamicPeriod, - dynamicPeriod, - ); - Get.find().dynamicPeriod = - dynamicPeriod * 60 * 1000; + try { + final val = int.parse(dynamicPeriod); + Get.back(); + GStorage.setting.put(SettingBoxKey.dynamicPeriod, val); + Get.find().dynamicPeriod = val * 60 * 1000; + } catch (e) { + SmartDialog.showToast(e.toString()); + } }, child: const Text('确定'), ), @@ -312,9 +311,7 @@ List get extraSettings => [ autofocus: true, initialValue: replyLengthLimit, keyboardType: TextInputType.number, - onChanged: (value) { - replyLengthLimit = value; - }, + onChanged: (value) => replyLengthLimit = value, inputFormatters: [FilteringTextInputFormatter.digitsOnly], decoration: const InputDecoration(suffixText: '行'), ), @@ -365,12 +362,8 @@ List get extraSettings => [ content: TextFormField( autofocus: true, initialValue: danmakuLineHeight, - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - ), - onChanged: (value) { - danmakuLineHeight = value; - }, + keyboardType: const .numberWithOptions(decimal: true), + onChanged: (value) => danmakuLineHeight = value, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')), ], @@ -460,6 +453,56 @@ List get extraSettings => [ setKey: SettingBoxKey.openInBrowser, defaultVal: false, ), + NormalModel( + title: '横向滑动阈值', + getSubtitle: () => '当前:「${Pref.touchSlopH}」', + onTap: (context, setState) { + String initialValue = Pref.touchSlopH.toString(); + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('横向滑动阈值'), + content: TextFormField( + autofocus: true, + initialValue: initialValue, + keyboardType: const .numberWithOptions(decimal: true), + onChanged: (value) => initialValue = value, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')), + ], + ), + actions: [ + TextButton( + onPressed: Get.back, + child: Text( + '取消', + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + TextButton( + onPressed: () async { + try { + final val = double.parse(initialValue); + Get.back(); + touchSlopH = val; + await GStorage.setting.put(SettingBoxKey.touchSlopH, val); + setState(); + } catch (e) { + SmartDialog.showToast(e.toString()); + } + }, + child: const Text('确定'), + ), + ], + ); + }, + ); + }, + leading: const Icon(Icons.pan_tool_alt_outlined), + ), NormalModel( title: '刷新滑动距离', leading: const Icon(Icons.refresh), @@ -687,9 +730,7 @@ List get extraSettings => [ ), setKey: SettingBoxKey.antiGoodsDyn, defaultVal: false, - onChanged: (value) { - DynamicsDataModel.antiGoodsDyn = value; - }, + onChanged: (value) => DynamicsDataModel.antiGoodsDyn = value, ), SwitchModel( title: '屏蔽带货评论', @@ -703,9 +744,7 @@ List get extraSettings => [ ), setKey: SettingBoxKey.antiGoodsReply, defaultVal: false, - onChanged: (value) { - ReplyGrpc.antiGoodsReply = value; - }, + onChanged: (value) => ReplyGrpc.antiGoodsReply = value, ), SwitchModel( title: '侧滑关闭二级页面', @@ -715,9 +754,7 @@ List get extraSettings => [ ), setKey: SettingBoxKey.slideDismissReplyPage, defaultVal: Platform.isIOS, - onChanged: (value) { - CommonSlideMixin.slideDismissReplyPage = value; - }, + onChanged: (value) => CommonSlideMixin.slideDismissReplyPage = value, ), const SwitchModel( title: '启用双指缩小视频', @@ -856,9 +893,7 @@ List get extraSettings => [ leading: const Icon(Icons.search_outlined), setKey: SettingBoxKey.enableWordRe, defaultVal: false, - onChanged: (value) { - ReplyItemGrpc.enableWordRe = value; - }, + onChanged: (value) => ReplyItemGrpc.enableWordRe = value, ), const SwitchModel( title: '启用AI总结', diff --git a/lib/pages/video/introduction/pgc/widgets/intro_detail.dart b/lib/pages/video/introduction/pgc/widgets/intro_detail.dart index 1b5cd9368..d04547e35 100644 --- a/lib/pages/video/introduction/pgc/widgets/intro_detail.dart +++ b/lib/pages/video/introduction/pgc/widgets/intro_detail.dart @@ -21,7 +21,7 @@ class PgcIntroPanel extends CommonSlidePage { const PgcIntroPanel({ super.key, required this.item, - super.enableSlide = false, + super.enableSlide, this.videoTags, }); @@ -50,42 +50,61 @@ class _IntroDetailState extends State @override Widget buildPage(ThemeData theme) { - return CustomTabBarView( - controller: _tabController, - physics: const CustomTabBarViewScrollPhysics(), - bgColor: theme.colorScheme.surface, - header: Row( + return Material( + color: theme.colorScheme.surface, + child: Column( children: [ + Row( + children: [ + Expanded( + child: TabBar( + controller: _tabController, + dividerHeight: 0, + isScrollable: true, + tabAlignment: TabAlignment.start, + dividerColor: Colors.transparent, + tabs: const [ + Tab(text: '详情'), + Tab(text: '点评'), + ], + onTap: (index) { + if (!_tabController.indexIsChanging) { + if (index == 0) { + _controller.animToTop(); + } + } + }, + ), + ), + IconButton( + tooltip: '关闭', + icon: const Icon(Icons.close, size: 20), + onPressed: Get.back, + ), + const SizedBox(width: 2), + ], + ), Expanded( - child: TabBar( - controller: _tabController, - dividerHeight: 0, - isScrollable: true, - tabAlignment: TabAlignment.start, - dividerColor: Colors.transparent, - tabs: const [ - Tab(text: '详情'), - Tab(text: '点评'), - ], - onTap: (index) { - if (!_tabController.indexIsChanging) { - if (index == 0) { - _controller.animToTop(); - } - } - }, - ), + child: enableSlide ? slideList(theme) : buildList(theme), ), - IconButton( - tooltip: '关闭', - icon: const Icon(Icons.close, size: 20), - onPressed: Get.back, - ), - const SizedBox(width: 2), ], ), + ); + } + + @override + Widget buildList(ThemeData theme) { + return TabBarView( + controller: _tabController, + physics: const CustomTabBarViewScrollPhysics(), + horizontalDragGestureRecognizer: TabBarDragGestureRecognizer( + isDxAllowed: (double dx) => enableSlide + ? dx > CommonSlideMixin.offset && + dx < maxWidth - CommonSlideMixin.offset + : true, + ), children: [ - KeepAliveWrapper(builder: (context) => buildList(theme)), + KeepAliveWrapper(builder: (context) => _buildInfo(theme)), PgcReviewPage( name: widget.item.title!, mediaId: widget.item.mediaId, @@ -94,8 +113,7 @@ class _IntroDetailState extends State ); } - @override - Widget buildList(ThemeData theme) { + Widget _buildInfo(ThemeData theme) { final TextStyle smallTitle = TextStyle( fontSize: 12, color: theme.colorScheme.onSurface, diff --git a/lib/utils/storage_key.dart b/lib/utils/storage_key.dart index d365aaaf0..d2148e75d 100644 --- a/lib/utils/storage_key.dart +++ b/lib/utils/storage_key.dart @@ -146,7 +146,8 @@ abstract final class SettingBoxKey { downloadPath = 'downloadPath', followOrderType = 'followOrderType', enableImgMenu = 'enableImgMenu', - showDynDispute = 'showDynDispute'; + showDynDispute = 'showDynDispute', + touchSlopH = 'touchSlopH'; static const String minimizeOnExit = 'minimizeOnExit', windowSize = 'windowSize', diff --git a/lib/utils/storage_pref.dart b/lib/utils/storage_pref.dart index c7a03f129..99b197c32 100644 --- a/lib/utils/storage_pref.dart +++ b/lib/utils/storage_pref.dart @@ -947,4 +947,7 @@ abstract final class Pref { static bool get showDynDispute => _setting.get(SettingBoxKey.showDynDispute, defaultValue: false); + + static double get touchSlopH => + _setting.get(SettingBoxKey.touchSlopH, defaultValue: 24.0); }