diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index b31208632..51597f019 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -89,12 +89,6 @@ class Constants { licenseUrl: 'https://github.com/benPesso/flutter_decorated_icon/blob/master/LICENSE', sourceUrl: 'https://github.com/benPesso/flutter_decorated_icon', ), - Dependency( - name: 'Draggable Scrollbar', - license: 'MIT', - licenseUrl: 'https://github.com/fluttercommunity/flutter-draggable-scrollbar/blob/master/LICENSE', - sourceUrl: 'https://github.com/fluttercommunity/flutter-draggable-scrollbar', - ), Dependency( name: 'Event Bus', license: 'MIT', diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index ccc9791dc..d9a123b67 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -16,6 +16,7 @@ import 'package:aves/widgets/collection/grid/section_layout.dart'; import 'package:aves/widgets/collection/grid/selector.dart'; import 'package:aves/widgets/collection/grid/thumbnail.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; +import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -26,7 +27,6 @@ import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/common/scaling.dart'; import 'package:aves/widgets/common/tile_extent_manager.dart'; -import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -237,8 +237,8 @@ class _CollectionScrollViewState extends State { builder: (context, appBarHeight, child) => Selector( selector: (context, mq) => mq.effectiveBottomPadding, builder: (context, mqPaddingBottom, child) => DraggableScrollbar( - heightScrollThumb: avesScrollThumbHeight, backgroundColor: Colors.white, + scrollThumbHeight: avesScrollThumbHeight, scrollThumbBuilder: avesScrollThumbBuilder( height: avesScrollThumbHeight, backgroundColor: Colors.white, diff --git a/lib/widgets/common/basic/draggable_scrollbar.dart b/lib/widgets/common/basic/draggable_scrollbar.dart new file mode 100644 index 000000000..c2e8d3292 --- /dev/null +++ b/lib/widgets/common/basic/draggable_scrollbar.dart @@ -0,0 +1,388 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/* + This is derived from `draggable_scrollbar` package v0.0.4: + - removed default thumb builders + - allow any `ScrollView` as child + - allow any `Widget` as label content + - moved out constraints responsibility + - various extent & thumb positioning fixes + */ + +/// Build the Scroll Thumb and label using the current configuration +typedef ScrollThumbBuilder = Widget Function( + Color backgroundColor, + Animation thumbAnimation, + Animation labelAnimation, + double height, { + Widget labelText, +}); + +/// Build a Text widget using the current scroll offset +typedef LabelTextBuilder = Widget Function(double offsetY); + +/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged +/// for quick navigation of the BoxScrollView. +class DraggableScrollbar extends StatefulWidget { + /// The background color of the label and thumb + final Color backgroundColor; + + /// The height of the scroll thumb + final double scrollThumbHeight; + + /// A function that builds a thumb using the current configuration + final ScrollThumbBuilder scrollThumbBuilder; + + /// The amount of padding that should surround the thumb + final EdgeInsetsGeometry padding; + + /// Determines how quickly the scrollbar will animate in and out + final Duration scrollbarAnimationDuration; + + /// How long should the thumb be visible before fading out + final Duration scrollbarTimeToFade; + + /// Build a Text widget from the current offset in the BoxScrollView + final LabelTextBuilder labelTextBuilder; + + /// The ScrollController for the BoxScrollView + final ScrollController controller; + + /// The view that will be scrolled with the scroll thumb + final ScrollView child; + + DraggableScrollbar({ + Key key, + @required this.backgroundColor, + @required this.scrollThumbHeight, + @required this.scrollThumbBuilder, + @required this.controller, + this.padding, + this.scrollbarAnimationDuration = const Duration(milliseconds: 300), + this.scrollbarTimeToFade = const Duration(milliseconds: 600), + this.labelTextBuilder, + @required this.child, + }) : assert(controller != null), + assert(scrollThumbBuilder != null), + assert(child.scrollDirection == Axis.vertical), + super(key: key); + + @override + _DraggableScrollbarState createState() => _DraggableScrollbarState(); + + static Widget buildScrollThumbAndLabel({ + @required Widget scrollThumb, + @required Color backgroundColor, + @required Animation thumbAnimation, + @required Animation labelAnimation, + @required Widget labelText, + }) { + final scrollThumbAndLabel = labelText == null + ? scrollThumb + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ScrollLabel( + animation: labelAnimation, + child: labelText, + backgroundColor: backgroundColor, + ), + scrollThumb, + ], + ); + return SlideFadeTransition( + animation: thumbAnimation, + child: scrollThumbAndLabel, + ); + } +} + +class ScrollLabel extends StatelessWidget { + final Animation animation; + final Color backgroundColor; + final Widget child; + + const ScrollLabel({ + Key key, + @required this.child, + @required this.animation, + @required this.backgroundColor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: animation, + child: Container( + margin: EdgeInsets.only(right: 12.0), + child: Material( + elevation: 4.0, + color: backgroundColor, + borderRadius: BorderRadius.all(Radius.circular(16.0)), + child: child, + ), + ), + ); + } +} + +class _DraggableScrollbarState extends State with TickerProviderStateMixin { + final ValueNotifier _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0); + bool _isDragInProcess = false; + + AnimationController _thumbAnimationController; + Animation _thumbAnimation; + AnimationController _labelAnimationController; + Animation _labelAnimation; + Timer _fadeoutTimer; + + @override + void initState() { + super.initState(); + + _thumbAnimationController = AnimationController( + vsync: this, + duration: widget.scrollbarAnimationDuration, + ); + + _thumbAnimation = CurvedAnimation( + parent: _thumbAnimationController, + curve: Curves.fastOutSlowIn, + ); + + _labelAnimationController = AnimationController( + vsync: this, + duration: widget.scrollbarAnimationDuration, + ); + + _labelAnimation = CurvedAnimation( + parent: _labelAnimationController, + curve: Curves.fastOutSlowIn, + ); + } + + @override + void dispose() { + _thumbAnimationController.dispose(); + _labelAnimationController.dispose(); + _fadeoutTimer?.cancel(); + super.dispose(); + } + + ScrollController get controller => widget.controller; + + double get thumbMaxScrollExtent => context.size.height - widget.scrollThumbHeight - (widget.padding?.vertical ?? 0.0); + + double get thumbMinScrollExtent => 0.0; + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: (notification) { + _onScrollNotification(notification); + return false; + }, + child: Stack( + children: [ + RepaintBoundary( + child: widget.child, + ), + RepaintBoundary( + child: GestureDetector( + onVerticalDragStart: _onVerticalDragStart, + onVerticalDragUpdate: _onVerticalDragUpdate, + onVerticalDragEnd: _onVerticalDragEnd, + child: ValueListenableBuilder( + valueListenable: _thumbOffsetNotifier, + builder: (context, thumbOffset, child) => Container( + alignment: AlignmentDirectional.topEnd, + padding: EdgeInsets.only(top: thumbOffset) + widget.padding, + child: widget.scrollThumbBuilder( + widget.backgroundColor, + _thumbAnimation, + _labelAnimation, + widget.scrollThumbHeight, + labelText: (widget.labelTextBuilder != null && _isDragInProcess) + ? ValueListenableBuilder( + valueListenable: _viewOffsetNotifier, + builder: (context, viewOffset, child) => widget.labelTextBuilder(viewOffset + thumbOffset), + ) + : null, + ), + ), + ), + ), + ), + ], + ), + ); + } + + void _onScrollNotification(ScrollNotification notification) { + final scrollMetrics = notification.metrics; + + // do not update the thumb if we cannot actually scroll + if (scrollMetrics.minScrollExtent >= scrollMetrics.maxScrollExtent) return; + + _viewOffsetNotifier.value = scrollMetrics.pixels; + + // we update the thumb position from the scrolled offset + // when the user is not dragging the thumb + if (!_isDragInProcess) { + if (notification is ScrollUpdateNotification) { + _thumbOffsetNotifier.value = (scrollMetrics.pixels / scrollMetrics.maxScrollExtent * thumbMaxScrollExtent).clamp(thumbMinScrollExtent, thumbMaxScrollExtent); + } + + if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { + _showThumb(); + _scheduleFadeout(); + } + } + } + + void _onVerticalDragStart(DragStartDetails details) { + _labelAnimationController.forward(); + _fadeoutTimer?.cancel(); + setState(() => _isDragInProcess = true); + } + + void _onVerticalDragUpdate(DragUpdateDetails details) { + _showThumb(); + if (_isDragInProcess) { + // thumb offset + _thumbOffsetNotifier.value = (_thumbOffsetNotifier.value + details.delta.dy).clamp(thumbMinScrollExtent, thumbMaxScrollExtent); + + // scroll offset + final min = controller.position.minScrollExtent; + final max = controller.position.maxScrollExtent; + controller.jumpTo((_thumbOffsetNotifier.value / thumbMaxScrollExtent * max).clamp(min, max)); + } + } + + void _onVerticalDragEnd(DragEndDetails details) { + _scheduleFadeout(); + setState(() => _isDragInProcess = false); + } + + void _showThumb() { + if (_thumbAnimationController.status != AnimationStatus.forward) { + _thumbAnimationController.forward(); + } + } + + void _scheduleFadeout() { + _fadeoutTimer?.cancel(); + _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { + _thumbAnimationController.reverse(); + _labelAnimationController.reverse(); + _fadeoutTimer = null; + }); + } +} + +/// Draws 2 triangles like arrow up and arrow down +class ArrowCustomPainter extends CustomPainter { + Color color; + + ArrowCustomPainter(this.color); + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + const width = 12.0; + const height = 8.0; + final baseX = size.width / 2; + final baseY = size.height / 2; + + canvas.drawPath( + _trianglePath(Offset(baseX, baseY - 2.0), width, height, true), + paint, + ); + canvas.drawPath( + _trianglePath(Offset(baseX, baseY + 2.0), width, height, false), + paint, + ); + } + + static Path _trianglePath(Offset o, double width, double height, bool isUp) { + return Path() + ..moveTo(o.dx, o.dy) + ..lineTo(o.dx + width, o.dy) + ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height) + ..close(); + } +} + +///This cut 2 lines in arrow shape +class ArrowClipper extends CustomClipper { + @override + Path getClip(Size size) { + final path = Path(); + path.lineTo(0.0, size.height); + path.lineTo(size.width, size.height); + path.lineTo(size.width, 0.0); + path.lineTo(0.0, 0.0); + path.close(); + + const arrowWidth = 8.0; + final startPointX = (size.width - arrowWidth) / 2; + var startPointY = size.height / 2 - arrowWidth / 2; + path.moveTo(startPointX, startPointY); + path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2); + path.lineTo(startPointX + arrowWidth, startPointY); + path.lineTo(startPointX + arrowWidth, startPointY + 1.0); + path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0); + path.lineTo(startPointX, startPointY + 1.0); + path.close(); + + startPointY = size.height / 2 + arrowWidth / 2; + path.moveTo(startPointX + arrowWidth, startPointY); + path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2); + path.lineTo(startPointX, startPointY); + path.lineTo(startPointX, startPointY - 1.0); + path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0); + path.lineTo(startPointX + arrowWidth, startPointY - 1.0); + path.close(); + + return path; + } + + @override + bool shouldReclip(CustomClipper oldClipper) => false; +} + +class SlideFadeTransition extends StatelessWidget { + final Animation animation; + final Widget child; + + const SlideFadeTransition({ + Key key, + @required this.animation, + @required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: animation, + builder: (context, child) => animation.value == 0.0 ? Container() : child, + child: SlideTransition( + position: Tween( + begin: Offset(0.3, 0.0), + end: Offset(0.0, 0.0), + ).animate(animation), + child: FadeTransition( + opacity: animation, + child: child, + ), + ), + ); + } +} diff --git a/lib/widgets/common/identity/scroll_thumb.dart b/lib/widgets/common/identity/scroll_thumb.dart index de907a8b3..268874983 100644 --- a/lib/widgets/common/identity/scroll_thumb.dart +++ b/lib/widgets/common/identity/scroll_thumb.dart @@ -1,4 +1,4 @@ -import 'package:draggable_scrollbar/draggable_scrollbar.dart'; +import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; const double avesScrollThumbHeight = 48; @@ -35,7 +35,6 @@ ScrollThumbBuilder avesScrollThumbBuilder({ thumbAnimation: thumbAnimation, labelAnimation: labelAnimation, labelText: labelText, - alwaysVisibleScrollThumb: false, ); }; } diff --git a/lib/widgets/common/magnifier/magnifier.dart b/lib/widgets/common/magnifier/magnifier.dart index e73bebbde..22c7d83d6 100644 --- a/lib/widgets/common/magnifier/magnifier.dart +++ b/lib/widgets/common/magnifier/magnifier.dart @@ -6,16 +6,18 @@ import 'package:aves/widgets/common/magnifier/scale/scale_level.dart'; import 'package:aves/widgets/common/magnifier/scale/state.dart'; import 'package:flutter/material.dart'; -/// `Magnifier` is derived from `photo_view` package v0.9.2: -/// - removed image related aspects to focus on a general purpose pan/scale viewer (à la `InteractiveViewer`) -/// - removed rotation and many customization parameters -/// - removed ignorable/ignoring partial notifiers -/// - formatted, renamed and reorganized -/// - fixed gesture recognizers when used inside a scrollable widget like `PageView` -/// - fixed corner hit detection when in containers scrollable in both axes -/// - fixed corner hit detection issues due to imprecise double comparisons -/// - added single & double tap position feedback -/// - fixed focus when scaling by double-tap/pinch +/* + `Magnifier` is derived from `photo_view` package v0.9.2: + - removed image related aspects to focus on a general purpose pan/scale viewer (à la `InteractiveViewer`) + - removed rotation and many customization parameters + - removed ignorable/ignoring partial notifiers + - formatted, renamed and reorganized + - fixed gesture recognizers when used inside a scrollable widget like `PageView` + - fixed corner hit detection when in containers scrollable in both axes + - fixed corner hit detection issues due to imprecise double comparisons + - added single & double tap position feedback + - fixed focus when scaling by double-tap/pinch + */ class Magnifier extends StatefulWidget { const Magnifier({ Key key, @@ -33,16 +35,16 @@ class Magnifier extends StatefulWidget { final Widget child; - /// The size of the custom [child]. This value is used to compute the relation between the child and the container's size to calculate the scale value. + // The size of the custom [child]. This value is used to compute the relation between the child and the container's size to calculate the scale value. final Size childSize; - /// Defines the maximum size in which the image will be allowed to assume, it is proportional to the original image size. + // Defines the maximum size in which the image will be allowed to assume, it is proportional to the original image size. final ScaleLevel maxScale; - /// Defines the minimum size in which the image will be allowed to assume, it is proportional to the original image size. + // Defines the minimum size in which the image will be allowed to assume, it is proportional to the original image size. final ScaleLevel minScale; - /// Defines the size the image will assume when the component is initialized, it is proportional to the original image size. + // Defines the size the image will assume when the component is initialized, it is proportional to the original image size. final ScaleLevel initialScale; final MagnifierController controller; diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 97104b603..4d866db2b 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -4,6 +4,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/highlight.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -20,7 +21,6 @@ import 'package:aves/widgets/drawer/app_drawer.dart'; import 'package:aves/widgets/filter_grids/common/decorated_filter_chip.dart'; import 'package:aves/widgets/filter_grids/common/section_keys.dart'; import 'package:aves/widgets/filter_grids/common/section_layout.dart'; -import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -192,8 +192,8 @@ class FilterGridPage extends StatelessWidget { return Selector( selector: (context, mq) => mq.effectiveBottomPadding, builder: (context, mqPaddingBottom, child) => DraggableScrollbar( - heightScrollThumb: avesScrollThumbHeight, backgroundColor: Colors.white, + scrollThumbHeight: avesScrollThumbHeight, scrollThumbBuilder: avesScrollThumbBuilder( height: avesScrollThumbHeight, backgroundColor: Colors.white, diff --git a/pubspec.lock b/pubspec.lock index fdff631b9..edd985531 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -148,15 +148,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" - draggable_scrollbar: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "3b823ae0a9def4edec62771f18e6348312bfce15" - url: "git://github.com/deckerst/flutter-draggable-scrollbar.git" - source: git - version: "0.0.4" event_bus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 60130769b..63fee4179 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,10 +34,6 @@ dependencies: charts_flutter: collection: decorated_icon: - draggable_scrollbar: -# path: ../flutter-draggable-scrollbar - git: - url: git://github.com/deckerst/flutter-draggable-scrollbar.git event_bus: expansion_tile_card: # path: ../expansion_tile_card