diff --git a/lib/ui/layout/layout.dart b/lib/ui/layout/layout.dart index ae0a31a..fbe395b 100644 --- a/lib/ui/layout/layout.dart +++ b/lib/ui/layout/layout.dart @@ -6,11 +6,11 @@ import 'package:annix/ui/page/playing/playing_mobile_blur.dart'; import 'package:annix/ui/route/delegate.dart'; import 'package:annix/ui/bottom_player/bottom_player.dart'; import 'package:annix/ui/route/page.dart'; +import 'package:annix/ui/widgets/slide_up.dart'; import 'package:annix/utils/context_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:sliding_up_panel2/sliding_up_panel2.dart'; import 'package:annix/i18n/strings.g.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -97,38 +97,29 @@ class AnnixLayout extends HookConsumerWidget { child: child!, ), if (showMiniPlayer) - LayoutBuilder( - builder: (context, constraints) => MediaQuery( - data: MediaQuery.of(context).copyWith( - size: Size(constraints.maxWidth, constraints.maxHeight)), - child: SlidingUpPanel( - controller: router.panelController, - renderPanelSheet: false, - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - panelBuilder: () { - return panel; - }, - collapsed: GestureDetector( - onTap: router.openPanel, - child: SlotLayout( - config: { - Breakpoints.small: SlotLayout.from( - key: const Key('Bottom Player Small'), - builder: (context) => const MobileBottomPlayer(), - ), - Breakpoints.mediumAndUp: SlotLayout.from( - key: const Key('Bottom Player Medium'), - builder: (context) => const DesktopBottomPlayer(), - ), - }, + SlidingUpPanel( + controller: router.panelController, + borderRadius: BorderRadius.circular(8), + panel: panel, + isDraggable: context.isMobileOrPortrait, + collapsed: GestureDetector( + onTap: router.openPanel, + child: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('Bottom Player Small'), + builder: (context) => const MobileBottomPlayer(), ), - ), - minHeight: miniPlayerHeight, - maxHeight: panelMaxSize, - onPanelSlide: (pos) => positionState.value = pos, + Breakpoints.mediumAndUp: SlotLayout.from( + key: const Key('Bottom Player Medium'), + builder: (context) => const DesktopBottomPlayer(), + ), + }, ), ), + minHeight: miniPlayerHeight, + maxHeight: panelMaxSize, + onPanelSlide: (pos) => positionState.value = pos, ) ], ); @@ -136,6 +127,7 @@ class AnnixLayout extends HookConsumerWidget { child: child, ); final root = AdaptiveLayout( + internalAnimations: false, primaryNavigation: SlotLayout(config: { Breakpoints.mediumAndUp: SlotLayout.from( key: const Key('Primary Navigation Medium'), diff --git a/lib/ui/route/delegate.dart b/lib/ui/route/delegate.dart index 5098b28..1218f4d 100644 --- a/lib/ui/route/delegate.dart +++ b/lib/ui/route/delegate.dart @@ -19,9 +19,9 @@ import 'package:annix/services/metadata/metadata_model.dart'; import 'package:annix/ui/page/home/home.dart'; import 'package:annix/ui/page/search.dart'; import 'package:annix/ui/route/page.dart'; +import 'package:annix/ui/widgets/slide_up.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:sliding_up_panel2/sliding_up_panel2.dart'; class AnnixRouterDelegate extends RouterDelegate> with ChangeNotifier, PopNavigatorRouterDelegateMixin> { diff --git a/lib/ui/widgets/lyric.dart b/lib/ui/widgets/lyric.dart index 0e62873..02021e8 100644 --- a/lib/ui/widgets/lyric.dart +++ b/lib/ui/widgets/lyric.dart @@ -1,12 +1,12 @@ import 'package:annix/providers.dart'; import 'package:annix/services/metadata/metadata_model.dart'; import 'package:annix/services/playback/playback.dart'; +import 'package:annix/ui/widgets/slide_up.dart'; import 'package:annix/utils/context_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_lyric/lyrics_reader.dart'; import 'package:annix/i18n/strings.g.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:sliding_up_panel2/sliding_up_panel2.dart'; extension on LyricAlign { TextAlign get textAlign { diff --git a/lib/ui/widgets/slide_up.dart b/lib/ui/widgets/slide_up.dart new file mode 100644 index 0000000..dfe664c --- /dev/null +++ b/lib/ui/widgets/slide_up.dart @@ -0,0 +1,557 @@ +/* +Name: Zotov Vladimir +Date: 18/06/22 +Purpose: Defines the package: sliding_up_panel2 +Copyright: © 2022, Zotov Vladimir. All rights reserved. +Licensing: More information can be found here: https://github.com/Zotov-VD/sliding_up_panel/blob/master/LICENSE + +This product includes software developed by Akshath Jain (https://akshathjain.com) +*/ + +import 'dart:math'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/physics.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +enum PanelState { open, closed } + +class SlidingUpPanel extends StatefulHookConsumerWidget { + /// Returns the Widget that slides into view. When the + /// panel is collapsed and if [collapsed] is null, + /// then top portion of this Widget will be displayed; + /// otherwise, [collapsed] will be displayed overtop + /// of this Widget. + final Widget panel; + + /// The Widget displayed overtop the [panel] when collapsed. + /// This fades out as the panel is opened. + final Widget collapsed; + + /// The height of the sliding panel when fully collapsed. + final double minHeight; + + /// The height of the sliding panel when fully open. + final double maxHeight; + + /// A point between [minHeight] and [maxHeight] that the panel snaps to + /// while animating. A fast swipe on the panel will disregard this point + /// and go directly to the open/close position. This value is represented as a + /// percentage of the total animation distance ([maxHeight] - [minHeight]), + /// so it must be between 0.0 and 1.0, exclusive. + final double? snapPoint; + + /// If non-null, the corners of the sliding panel sheet are rounded by this [BorderRadiusGeometry]. + final BorderRadiusGeometry? borderRadius; + + /// Set to false to disable the panel from snapping open or closed. + final bool panelSnapping; + + /// If non-null, this can be used to control the state of the panel. + final PanelController? controller; + + /// If non-null, this callback + /// is called as the panel slides around with the + /// current position of the panel. The position is a double + /// between 0.0 and 1.0 where 0.0 is fully collapsed and 1.0 is fully open. + final void Function(double position)? onPanelSlide; + + /// Allows toggling of the draggability of the SlidingUpPanel. + /// Set this to false to prevent the user from being able to drag + /// the panel up and down. Defaults to true. + final bool isDraggable; + + /// The default state of the panel; either PanelState.OPEN or PanelState.CLOSED. + /// This value defaults to PanelState.CLOSED which indicates that the panel is + /// in the closed position and must be opened. PanelState.OPEN indicates that + /// by default the Panel is open and must be swiped closed by the user. + final PanelState defaultPanelState; + + const SlidingUpPanel( + {Key? key, + required this.collapsed, + this.minHeight = 100.0, + this.maxHeight = 500.0, + this.snapPoint, + this.borderRadius, + this.panelSnapping = true, + this.controller, + this.onPanelSlide, + this.isDraggable = true, + this.defaultPanelState = PanelState.closed, + required this.panel}) + : assert(snapPoint == null || 0 < snapPoint && snapPoint < 1.0), + super(key: key); + + @override + ConsumerState createState() => _SlidingUpPanelState(); +} + +class _SlidingUpPanelState extends ConsumerState { + late AnimationController _ac; + final ScrollController _sc = ScrollController(); + + bool _scrollingEnabled = false; + final _vt = VelocityTracker.withKind(PointerDeviceKind.touch); + + bool _isPanelVisible = true; + + @override + void initState() { + super.initState(); + + // prevent the panel content from being scrolled only if the widget is + // draggable and panel scrolling is enabled + _sc.addListener(() { + if (widget.isDraggable && + (!_scrollingEnabled || _panelPosition < 1) && + widget.controller?._forceScrollChange != true) { + _sc.jumpTo(_scMinffset); + } + }); + + widget.controller?._addState(this); + } + + @override + Widget build(BuildContext context) { + final animationController = useAnimationController( + duration: const Duration(milliseconds: 300), + initialValue: widget.defaultPanelState == PanelState.closed ? 0.0 : 1.0, + ); + useEffect(() { + if (widget.onPanelSlide != null) { + animationController.addListener(() => widget.onPanelSlide!(_ac.value)); + } + _ac = animationController; + + return animationController.dispose; + }, [animationController]); + + return Stack( + alignment: Alignment.bottomCenter, + children: [ + Container(), + _isPanelVisible + ? _gestureHandler( + child: AnimatedBuilder( + animation: animationController, + builder: (context, child) { + return SizedBox( + height: animationController.value * + (widget.maxHeight - widget.minHeight) + + widget.minHeight, + child: child, + ); + }, + child: Stack( + children: [ + // open panel + Positioned( + top: 0, + width: MediaQuery.of(context).size.width, + child: SizedBox( + height: widget.maxHeight, + child: FadeTransition( + opacity: TweenSequence([ + TweenSequenceItem( + tween: Tween(begin: 0, end: 1), + weight: 10, + ), + TweenSequenceItem( + tween: Tween(begin: 1, end: 1), + weight: 90, + ), + ]).animate(animationController), + child: widget.panel, + ), + ), + ), + + // collapsed panel + SizedBox( + height: widget.minHeight, + child: FadeTransition( + opacity: Tween(begin: 1.0, end: 0.0) + .animate(animationController), + + // if the panel is open ignore pointers (touch events) on the collapsed + // child so that way touch events go through to whatever is underneath + child: IgnorePointer( + ignoring: _isPanelOpen, + child: widget.collapsed, + ), + ), + ), + ], + ), + ), + ) + : Container() + ], + ); + } + + bool _ignoreScrollable = false; + + // returns a gesture detector if panel is used + // and a listener if panel is used. + // this is because the listener is designed only for use with linking the scrolling of + // panels and using it for panels that don't want to linked scrolling yields odd results + Widget _gestureHandler({required Widget child}) { + if (!widget.isDraggable) return child; + + return Listener( + onPointerDown: (PointerDownEvent e) { + final rb = context.findRenderObject() as RenderBox; + final result = BoxHitTestResult(); + rb.hitTest(result, position: e.position); + + if (_panelPosition == 1) { + _scMinffset = 0.0; + } + if (result.path.any((entry) => + entry.target.runtimeType == + _IgnoreDraggableWidgetWidgetRenderBox)) { + _ignoreScrollable = true; + return; + } + _ignoreScrollable = false; + _vt.addPosition(e.timeStamp, e.position); + }, + onPointerMove: (PointerMoveEvent e) { + if (_ignoreScrollable) return; + // add current position for velocity tracking + _vt.addPosition(e.timeStamp, e.position); + _onGestureSlide(e.delta.dy); + }, + onPointerUp: (PointerUpEvent e) { + if (_ignoreScrollable) return; + _onGestureEnd(_vt.getVelocity()); + }, + child: child, + ); + } + + double _scMinffset = 0.0; + + // handles the sliding gesture + void _onGestureSlide(double dy) { + if ((!_scrollingEnabled) || _panelPosition < 1) { + _ac.value -= dy / (widget.maxHeight - widget.minHeight); + } + + // if the panel is open and the user hasn't scrolled, we need to determine + // whether to enable scrolling if the user swipes up, or disable closing and + // begin to close the panel if the user swipes down + if (_isPanelOpen && _sc.hasClients && _sc.offset <= _scMinffset) { + if (dy < 0) { + _scrollingEnabled = true; + } else { + _scrollingEnabled = false; + } + } + } + + // handles when user stops sliding + void _onGestureEnd(Velocity v) { + const minFlingVelocity = 365.0; + const kSnap = 8; + + //let the current animation finish before starting a new one + if (_ac.isAnimating) return; + + // if scrolling is allowed and the panel is open, we don't want to close + // the panel if they swipe up on the scrollable + if (_isPanelOpen && _scrollingEnabled) return; + + //check if the velocity is sufficient to constitute fling to end + final visualVelocity = + -v.pixelsPerSecond.dy / (widget.maxHeight - widget.minHeight); + + // get minimum distances to figure out where the panel is at + final double d2Close = _ac.value; + final double d2Open = 1 - _ac.value; + final double d2Snap = ((widget.snapPoint ?? 3) - _ac.value) + .abs(); // large value if null results in not every being the min + final double minDistance = min(d2Close, min(d2Snap, d2Open)); + + // check if velocity is sufficient for a fling + if (v.pixelsPerSecond.dy.abs() >= minFlingVelocity) { + // snapPoint exists + if (widget.panelSnapping && widget.snapPoint != null) { + if (v.pixelsPerSecond.dy.abs() >= kSnap * minFlingVelocity || + minDistance == d2Snap) { + _ac.fling(velocity: visualVelocity); + } else { + _flingPanelToPosition(widget.snapPoint!, visualVelocity); + } + + // no snap point exists + } else if (widget.panelSnapping) { + _ac.fling(velocity: visualVelocity); + + // panel snapping disabled + } else { + _ac.animateTo( + _ac.value + visualVelocity * 0.16, + duration: const Duration(milliseconds: 410), + curve: Curves.decelerate, + ); + } + + return; + } + + // check if the controller is already halfway there + if (widget.panelSnapping) { + if (minDistance == d2Close) { + _close(); + } else if (minDistance == d2Snap) { + _flingPanelToPosition(widget.snapPoint!, visualVelocity); + } else { + _open(); + } + } + } + + void _flingPanelToPosition(double targetPos, double velocity) { + final Simulation simulation = SpringSimulation( + SpringDescription.withDampingRatio( + mass: 1.0, + stiffness: 500.0, + ratio: 1.0, + ), + _ac.value, + targetPos, + velocity); + + _ac.animateWith(simulation); + } + + //--------------------------------- + //PanelController related functions + //--------------------------------- + + //close the panel + Future _close() { + return _ac.fling(velocity: -1.0); + } + + //open the panel + Future _open() { + return _ac.fling(velocity: 1.0); + } + + //hide the panel (completely offscreen) + Future _hide() { + return _ac.fling(velocity: -1.0).then((x) { + setState(() { + _isPanelVisible = false; + }); + }); + } + + //show the panel (in collapsed mode) + Future _show() { + return _ac.fling(velocity: -1.0).then((x) { + setState(() { + _isPanelVisible = true; + }); + }); + } + + //animate the panel position to value - must + //be between 0.0 and 1.0 + Future _animatePanelToPosition(double value, + {Duration? duration, Curve curve = Curves.linear}) { + assert(0.0 <= value && value <= 1.0); + return _ac.animateTo(value, duration: duration, curve: curve); + } + + //animate the panel position to the snap point + //REQUIRES that widget.snapPoint != null + Future _animatePanelToSnapPoint( + {Duration? duration, Curve curve = Curves.linear}) { + assert(widget.snapPoint != null); + return _ac.animateTo(widget.snapPoint!, duration: duration, curve: curve); + } + + //set the panel position to value - must + //be between 0.0 and 1.0 + set _panelPosition(double value) { + assert(0.0 <= value && value <= 1.0); + _ac.value = value; + } + + //get the current panel position + //returns the % offset from collapsed state + //as a decimal between 0.0 and 1.0 + double get _panelPosition => _ac.value; + + //returns whether or not + //the panel is still animating + bool get _isPanelAnimating => _ac.isAnimating; + + //returns whether or not the + //panel is open + bool get _isPanelOpen => _ac.value == 1.0; + + //returns whether or not the + //panel is closed + bool get _isPanelClosed => _ac.value == 0.0; + + //returns whether or not the + //panel is shown/hidden + bool get _isPanelShown => _isPanelVisible; +} + +class PanelController { + _SlidingUpPanelState? _panelState; + + void _addState(_SlidingUpPanelState panelState) { + _panelState = panelState; + } + + bool _forceScrollChange = false; + + /// use this function when scroll change in func + /// Example: + /// panelController.forseScrollChange(scrollController.animateTo(100, duration: Duration(milliseconds: 400), curve: Curves.ease)) + Future forseScrollChange(Future func) async { + _forceScrollChange = true; + _panelState!._scrollingEnabled = true; + await func; + // if (_panelState!._sc.offset == 0) { + // _panelState!._scrollingEnabled = true; + // } + if (panelPosition < 1) { + _panelState!._scMinffset = _panelState!._sc.offset; + } + _forceScrollChange = false; + } + + /// Determine if the panelController is attached to an instance + /// of the SlidingUpPanel (this property must return true before any other + /// functions can be used) + bool get isAttached => _panelState != null; + + /// Closes the sliding panel to its collapsed state (i.e. to the minHeight) + Future close() { + assert(isAttached, 'PanelController must be attached to a SlidingUpPanel'); + return _panelState!._close(); + } + + /// Opens the sliding panel fully + /// (i.e. to the maxHeight) + Future open() { + assert(isAttached, 'PanelController must be attached to a SlidingUpPanel'); + return _panelState!._open(); + } + + /// Hides the sliding panel (i.e. is invisible) + Future hide() { + assert(isAttached, 'PanelController must be attached to a SlidingUpPanel'); + return _panelState!._hide(); + } + + /// Shows the sliding panel in its collapsed state + /// (i.e. "un-hide" the sliding panel) + Future show() { + assert(isAttached, 'PanelController must be attached to a SlidingUpPanel'); + return _panelState!._show(); + } + + /// Animates the panel position to the value. + /// The value must between 0.0 and 1.0 + /// where 0.0 is fully collapsed and 1.0 is completely open. + /// (optional) duration specifies the time for the animation to complete + /// (optional) curve specifies the easing behavior of the animation. + Future animatePanelToPosition(double value, + {Duration? duration, Curve curve = Curves.linear}) { + assert(isAttached, 'PanelController must be attached to a SlidingUpPanel'); + assert(0.0 <= value && value <= 1.0); + return _panelState! + ._animatePanelToPosition(value, duration: duration, curve: curve); + } + + /// Animates the panel position to the snap point + /// Requires that the SlidingUpPanel snapPoint property is not null + /// (optional) duration specifies the time for the animation to complete + /// (optional) curve specifies the easing behavior of the animation. + Future animatePanelToSnapPoint( + {Duration? duration, Curve curve = Curves.linear}) { + assert(isAttached, 'PanelController must be attached to a SlidingUpPanel'); + assert(_panelState!.widget.snapPoint != null, + 'SlidingUpPanel snapPoint property must not be null'); + return _panelState! + ._animatePanelToSnapPoint(duration: duration, curve: curve); + } + + /// Sets the panel position (without animation). + /// The value must between 0.0 and 1.0 + /// where 0.0 is fully collapsed and 1.0 is completely open. + set panelPosition(double value) { + assert(isAttached, 'PanelController must be attached to a SlidingUpPanel'); + assert(0.0 <= value && value <= 1.0); + _panelState!._panelPosition = value; + } + + /// Gets the current panel position. + /// Returns the % offset from collapsed state + /// to the open state + /// as a decimal between 0.0 and 1.0 + /// where 0.0 is fully collapsed and + /// 1.0 is full open. + double get panelPosition { + assert(isAttached, 'PanelController must be attached to a SlidingUpPanel'); + return _panelState!._panelPosition; + } + + /// Returns whether or not the panel is + /// currently animating. + bool get isPanelAnimating { + assert(isAttached, 'PanelController must be attached to a SlidingUpPanel'); + return _panelState!._isPanelAnimating; + } + + /// Returns whether or not the + /// panel is open. + bool get isPanelOpen { + assert(isAttached, 'PanelController must be attached to a SlidingUpPanel'); + return _panelState!._isPanelOpen; + } + + /// Returns whether or not the + /// panel is closed. + bool get isPanelClosed { + assert(isAttached, 'PanelController must be attached to a SlidingUpPanel'); + return _panelState!._isPanelClosed; + } + + /// Returns whether or not the + /// panel is shown/hidden. + bool get isPanelShown { + assert(isAttached, 'PanelController must be attached to a SlidingUpPanel'); + return _panelState!._isPanelShown; + } +} + +/// if you want to prevent the panel from being dragged using the widget, +/// wrap the widget with this +class IgnoreDraggableWidget extends SingleChildRenderObjectWidget { + const IgnoreDraggableWidget({super.key, required super.child}); + + @override + RenderObject createRenderObject(BuildContext context) { + return _IgnoreDraggableWidgetWidgetRenderBox(); + } +} + +class _IgnoreDraggableWidgetWidgetRenderBox extends RenderPointerListener { + @override + HitTestBehavior get behavior => HitTestBehavior.opaque; +} diff --git a/pubspec.lock b/pubspec.lock index 3c3eea3..bbd7c28 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1115,14 +1115,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.31.0" - sliding_up_panel2: - dependency: "direct main" - description: - name: sliding_up_panel2 - sha256: "7c2aac81c03e74fcd070799c5e2011f1c5de7026bd22a76164e81e23a49f2bdb" - url: "https://pub.dev" - source: hosted - version: "3.3.0+1" source_gen: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ed6033c..ed0e7a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,6 @@ dependencies: flutter_linkify: ^6.0.0 flutter_json_view: ^1.1.5 - sliding_up_panel2: ^3.3.0+1 flutter_adaptive_scaffold: ^0.1.12 animations: ^2.0.11 crypto: ^3.0.3