collection: snap to header/row when fast scrolling long content

This commit is contained in:
Thibault Deckers 2023-03-12 22:58:57 +01:00
parent 5677dab9fb
commit 954853643a
2 changed files with 30 additions and 3 deletions

View file

@ -45,6 +45,7 @@ import 'package:aves/widgets/common/thumbnail/notifications.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart'; import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
@ -500,6 +501,8 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
return Selector<SectionedListLayout<AvesEntry>, List<SectionLayout>>( return Selector<SectionedListLayout<AvesEntry>, List<SectionLayout>>(
selector: (context, layout) => layout.sectionLayouts, selector: (context, layout) => layout.sectionLayouts,
builder: (context, sectionLayouts, child) { builder: (context, sectionLayouts, child) {
final scrollController = widget.scrollController;
final offsetIncrementSnapThreshold = context.select<TileExtentController, double>((v) => (v.extentNotifier.value + v.spacing) / 4);
return DraggableScrollbar( return DraggableScrollbar(
backgroundColor: Colors.white, backgroundColor: Colors.white,
scrollThumbSize: Size(avesScrollThumbWidth, avesScrollThumbHeight), scrollThumbSize: Size(avesScrollThumbWidth, avesScrollThumbHeight),
@ -507,7 +510,23 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
height: avesScrollThumbHeight, height: avesScrollThumbHeight,
backgroundColor: Colors.white, backgroundColor: Colors.white,
), ),
controller: widget.scrollController, controller: scrollController,
dragOffsetSnapper: (scrollOffset, offsetIncrement) {
if (offsetIncrement > offsetIncrementSnapThreshold && scrollOffset < scrollController.position.maxScrollExtent) {
final section = sectionLayouts.firstWhereOrNull((section) => section.hasChildAtOffset(scrollOffset));
if (section != null) {
if (section.maxOffset - section.minOffset < scrollController.position.viewportDimension) {
// snap to section header
return section.minOffset;
} else {
// snap to content row
final index = section.getMinChildIndexForScrollOffset(scrollOffset);
return section.indexToLayoutOffset(index);
}
}
}
return scrollOffset;
},
crumbsBuilder: () => _getCrumbs(sectionLayouts), crumbsBuilder: () => _getCrumbs(sectionLayouts),
padding: EdgeInsets.only( padding: EdgeInsets.only(
// padding to keep scroll thumb between app bar above and nav bar below // padding to keep scroll thumb between app bar above and nav bar below

View file

@ -59,6 +59,8 @@ class DraggableScrollbar extends StatefulWidget {
/// The ScrollController for the BoxScrollView /// The ScrollController for the BoxScrollView
final ScrollController controller; final ScrollController controller;
final double Function(double scrollOffset, double offsetIncrement)? dragOffsetSnapper;
/// The view that will be scrolled with the scroll thumb /// The view that will be scrolled with the scroll thumb
final ScrollView child; final ScrollView child;
@ -68,6 +70,7 @@ class DraggableScrollbar extends StatefulWidget {
required this.scrollThumbSize, required this.scrollThumbSize,
required this.scrollThumbBuilder, required this.scrollThumbBuilder,
required this.controller, required this.controller,
this.dragOffsetSnapper,
this.crumbsBuilder, this.crumbsBuilder,
this.padding = EdgeInsets.zero, this.padding = EdgeInsets.zero,
this.scrollbarAnimationDuration = const Duration(milliseconds: 300), this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
@ -114,6 +117,7 @@ class DraggableScrollbar extends StatefulWidget {
class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProviderStateMixin { class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProviderStateMixin {
final ValueNotifier<double> _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0); final ValueNotifier<double> _thumbOffsetNotifier = ValueNotifier(0), _viewOffsetNotifier = ValueNotifier(0);
bool _isDragInProcess = false; bool _isDragInProcess = false;
double _boundlessThumbOffset = 0, _offsetIncrement = 0;
late Offset _longPressLastGlobalPosition; late Offset _longPressLastGlobalPosition;
late AnimationController _thumbAnimationController; late AnimationController _thumbAnimationController;
@ -281,6 +285,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
void _onVerticalDragStart() { void _onVerticalDragStart() {
const DraggableScrollbarNotification(DraggableScrollbarEvent.dragStart).dispatch(context); const DraggableScrollbarNotification(DraggableScrollbarEvent.dragStart).dispatch(context);
_boundlessThumbOffset = _thumbOffsetNotifier.value;
_offsetIncrement = 1 / thumbMaxScrollExtent * controller.position.maxScrollExtent;
_labelAnimationController.forward(); _labelAnimationController.forward();
_fadeoutTimer?.cancel(); _fadeoutTimer?.cancel();
_showThumb(); _showThumb();
@ -292,12 +298,14 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
_showThumb(); _showThumb();
if (_isDragInProcess) { if (_isDragInProcess) {
// thumb offset // thumb offset
_thumbOffsetNotifier.value = (_thumbOffsetNotifier.value + deltaY).clamp(thumbMinScrollExtent, thumbMaxScrollExtent); _boundlessThumbOffset += deltaY;
_thumbOffsetNotifier.value = _boundlessThumbOffset.clamp(thumbMinScrollExtent, thumbMaxScrollExtent);
// scroll offset // scroll offset
final min = controller.position.minScrollExtent; final min = controller.position.minScrollExtent;
final max = controller.position.maxScrollExtent; final max = controller.position.maxScrollExtent;
controller.jumpTo((_thumbOffsetNotifier.value / thumbMaxScrollExtent * max).clamp(min, max)); final scrollOffset = _thumbOffsetNotifier.value / thumbMaxScrollExtent * max;
controller.jumpTo((widget.dragOffsetSnapper?.call(scrollOffset, _offsetIncrement) ?? scrollOffset).clamp(min, max));
} }
} }