#226 collection: fast-scrolling shows breadcrumbs from groups
This commit is contained in:
parent
839f19a141
commit
987a7dfe70
7 changed files with 250 additions and 54 deletions
|
@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
- Bottom navigation bar
|
||||
- Collection: thumbnail overlay tag icon
|
||||
- Collection: fast-scrolling shows breadcrumbs from groups
|
||||
- Settings: search
|
||||
- `huawei` app flavor (Petal Maps, no Crashlytics)
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:aves/model/filters/mime.dart';
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/model/source/section_keys.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
|
@ -21,8 +22,10 @@ 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';
|
||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||
import 'package:aves/widgets/common/grid/draggable_thumb_label.dart';
|
||||
import 'package:aves/widgets/common/grid/item_tracker.dart';
|
||||
import 'package:aves/widgets/common/grid/scaling.dart';
|
||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||
import 'package:aves/widgets/common/grid/selector.dart';
|
||||
import 'package:aves/widgets/common/grid/sliver.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
|
@ -34,6 +37,7 @@ import 'package:aves/widgets/common/tile_extent_controller.dart';
|
|||
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
|
@ -347,26 +351,34 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> {
|
|||
valueListenable: widget.appBarHeightNotifier,
|
||||
builder: (context, appBarHeight, child) => Selector<MediaQueryData, double>(
|
||||
selector: (context, mq) => mq.effectiveBottomPadding,
|
||||
builder: (context, mqPaddingBottom, child) => DraggableScrollbar(
|
||||
backgroundColor: Colors.white,
|
||||
scrollThumbHeight: avesScrollThumbHeight,
|
||||
scrollThumbBuilder: avesScrollThumbBuilder(
|
||||
height: avesScrollThumbHeight,
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
controller: widget.scrollController,
|
||||
padding: EdgeInsets.only(
|
||||
// padding to keep scroll thumb between app bar above and nav bar below
|
||||
top: appBarHeight,
|
||||
bottom: mqPaddingBottom,
|
||||
),
|
||||
labelTextBuilder: (offsetY) => CollectionDraggableThumbLabel(
|
||||
collection: collection,
|
||||
offsetY: offsetY,
|
||||
),
|
||||
child: scrollView,
|
||||
),
|
||||
child: child,
|
||||
builder: (context, mqPaddingBottom, child) {
|
||||
return Selector<SectionedListLayout<AvesEntry>, List<SectionLayout>>(
|
||||
selector: (context, layout) => layout.sectionLayouts,
|
||||
builder: (context, sectionLayouts, child) {
|
||||
return DraggableScrollbar(
|
||||
backgroundColor: Colors.white,
|
||||
scrollThumbSize: Size(avesScrollThumbWidth, avesScrollThumbHeight),
|
||||
scrollThumbBuilder: avesScrollThumbBuilder(
|
||||
height: avesScrollThumbHeight,
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
controller: widget.scrollController,
|
||||
crumbsBuilder: () => _getCrumbs(sectionLayouts),
|
||||
padding: EdgeInsets.only(
|
||||
// padding to keep scroll thumb between app bar above and nav bar below
|
||||
top: appBarHeight,
|
||||
bottom: mqPaddingBottom,
|
||||
),
|
||||
labelTextBuilder: (offsetY) => CollectionDraggableThumbLabel(
|
||||
collection: collection,
|
||||
offsetY: offsetY,
|
||||
),
|
||||
crumbTextBuilder: (label) => DraggableCrumbLabel(label: label),
|
||||
child: scrollView,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -433,4 +445,64 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> {
|
|||
void _stopScrollMonitoringTimer() {
|
||||
_scrollMonitoringTimer?.cancel();
|
||||
}
|
||||
|
||||
Map<double, String> _getCrumbs(List<SectionLayout> sectionLayouts) {
|
||||
final crumbs = <double, String>{};
|
||||
if (sectionLayouts.length <= 1) return crumbs;
|
||||
|
||||
void addAlbums(CollectionLens collection, List<SectionLayout> sectionLayouts, Map<double, String> crumbs) {
|
||||
final source = collection.source;
|
||||
sectionLayouts.forEach((section) {
|
||||
final directory = (section.sectionKey as EntryAlbumSectionKey).directory;
|
||||
if (directory != null) {
|
||||
final label = source.getAlbumDisplayName(context, directory);
|
||||
crumbs[section.minOffset] = label;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final collection = widget.collection;
|
||||
switch (collection.sortFactor) {
|
||||
case EntrySortFactor.date:
|
||||
switch (collection.sectionFactor) {
|
||||
case EntryGroupFactor.album:
|
||||
addAlbums(collection, sectionLayouts, crumbs);
|
||||
break;
|
||||
case EntryGroupFactor.month:
|
||||
case EntryGroupFactor.day:
|
||||
final firstKey = sectionLayouts.first.sectionKey;
|
||||
final lastKey = sectionLayouts.last.sectionKey;
|
||||
if (firstKey is EntryDateSectionKey && lastKey is EntryDateSectionKey) {
|
||||
final newest = firstKey.date;
|
||||
final oldest = lastKey.date;
|
||||
if (newest != null && oldest != null) {
|
||||
final localeName = context.l10n.localeName;
|
||||
final dateFormat = newest.difference(oldest).inDays > 365 ? DateFormat.y(localeName) : DateFormat.MMM(localeName);
|
||||
String? lastLabel;
|
||||
sectionLayouts.forEach((section) {
|
||||
final date = (section.sectionKey as EntryDateSectionKey).date;
|
||||
if (date != null) {
|
||||
final label = dateFormat.format(date);
|
||||
if (label != lastLabel) {
|
||||
crumbs[section.minOffset] = label;
|
||||
lastLabel = label;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EntryGroupFactor.none:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case EntrySortFactor.name:
|
||||
addAlbums(collection, sectionLayouts, crumbs);
|
||||
break;
|
||||
case EntrySortFactor.rating:
|
||||
case EntrySortFactor.size:
|
||||
break;
|
||||
}
|
||||
return crumbs;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,8 @@ typedef ScrollThumbBuilder = Widget Function(
|
|||
});
|
||||
|
||||
/// Build a Text widget using the current scroll offset
|
||||
typedef LabelTextBuilder = Widget Function(double offsetY);
|
||||
typedef OffsetLabelBuilder = Widget Function(double offsetY);
|
||||
typedef TextLabelBuilder = Widget Function(String label);
|
||||
|
||||
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
|
||||
/// for quick navigation of the BoxScrollView.
|
||||
|
@ -32,14 +33,15 @@ class DraggableScrollbar extends StatefulWidget {
|
|||
/// The background color of the label and thumb
|
||||
final Color backgroundColor;
|
||||
|
||||
/// The height of the scroll thumb
|
||||
final double scrollThumbHeight;
|
||||
final Map<double, String> Function()? crumbsBuilder;
|
||||
|
||||
final Size scrollThumbSize;
|
||||
|
||||
/// A function that builds a thumb using the current configuration
|
||||
final ScrollThumbBuilder scrollThumbBuilder;
|
||||
|
||||
/// The amount of padding that should surround the thumb
|
||||
final EdgeInsets? padding;
|
||||
final EdgeInsets padding;
|
||||
|
||||
/// Determines how quickly the scrollbar will animate in and out
|
||||
final Duration scrollbarAnimationDuration;
|
||||
|
@ -48,7 +50,9 @@ class DraggableScrollbar extends StatefulWidget {
|
|||
final Duration scrollbarTimeToFade;
|
||||
|
||||
/// Build a Text widget from the current offset in the BoxScrollView
|
||||
final LabelTextBuilder? labelTextBuilder;
|
||||
final OffsetLabelBuilder labelTextBuilder;
|
||||
|
||||
final TextLabelBuilder crumbTextBuilder;
|
||||
|
||||
/// The ScrollController for the BoxScrollView
|
||||
final ScrollController controller;
|
||||
|
@ -59,13 +63,15 @@ class DraggableScrollbar extends StatefulWidget {
|
|||
DraggableScrollbar({
|
||||
Key? key,
|
||||
required this.backgroundColor,
|
||||
required this.scrollThumbHeight,
|
||||
required this.scrollThumbSize,
|
||||
required this.scrollThumbBuilder,
|
||||
required this.controller,
|
||||
this.padding,
|
||||
this.crumbsBuilder,
|
||||
this.padding = EdgeInsets.zero,
|
||||
this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
|
||||
this.scrollbarTimeToFade = const Duration(milliseconds: 1000),
|
||||
this.labelTextBuilder,
|
||||
required this.labelTextBuilder,
|
||||
required this.crumbTextBuilder,
|
||||
required this.child,
|
||||
}) : assert(child.scrollDirection == Axis.vertical),
|
||||
super(key: key);
|
||||
|
@ -73,6 +79,8 @@ class DraggableScrollbar extends StatefulWidget {
|
|||
@override
|
||||
State<DraggableScrollbar> createState() => _DraggableScrollbarState();
|
||||
|
||||
static const double labelThumbPadding = 16;
|
||||
|
||||
static Widget buildScrollThumbAndLabel({
|
||||
required Widget scrollThumb,
|
||||
required Color backgroundColor,
|
||||
|
@ -91,7 +99,7 @@ class DraggableScrollbar extends StatefulWidget {
|
|||
backgroundColor: backgroundColor,
|
||||
child: labelText,
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
const SizedBox(width: labelThumbPadding),
|
||||
scrollThumb,
|
||||
],
|
||||
);
|
||||
|
@ -141,6 +149,11 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||
late AnimationController _labelAnimationController;
|
||||
late Animation<double> _labelAnimation;
|
||||
Timer? _fadeoutTimer;
|
||||
Map<double, String>? _modelCrumbs;
|
||||
final List<_Crumb> _viewportCrumbs = [];
|
||||
|
||||
static const crumbPadding = 30.0;
|
||||
static const crumbOffsetRatioThreshold = 10;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -167,6 +180,15 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant DraggableScrollbar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.crumbsBuilder != widget.crumbsBuilder) {
|
||||
_modelCrumbs = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_thumbAnimationController.dispose();
|
||||
|
@ -177,7 +199,9 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||
|
||||
ScrollController get controller => widget.controller;
|
||||
|
||||
double get thumbMaxScrollExtent => context.size!.height - widget.scrollThumbHeight - (widget.padding?.vertical ?? 0.0);
|
||||
double get scrollBarHeight => context.size!.height - widget.padding.vertical;
|
||||
|
||||
double get thumbMaxScrollExtent => scrollBarHeight - widget.scrollThumbSize.height;
|
||||
|
||||
double get thumbMinScrollExtent => 0.0;
|
||||
|
||||
|
@ -193,6 +217,27 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||
RepaintBoundary(
|
||||
child: widget.child,
|
||||
),
|
||||
if (_isDragInProcess)
|
||||
..._viewportCrumbs.map((crumb) {
|
||||
return Positioned.directional(
|
||||
textDirection: Directionality.of(context),
|
||||
top: crumb.labelOffset,
|
||||
end: DraggableScrollbar.labelThumbPadding + widget.scrollThumbSize.width,
|
||||
child: Padding(
|
||||
padding: widget.padding,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: widget.scrollThumbSize.height),
|
||||
child: Center(
|
||||
child: ScrollLabel(
|
||||
animation: kAlwaysCompleteAnimation,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
child: widget.crumbTextBuilder(crumb.label),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
RepaintBoundary(
|
||||
child: GestureDetector(
|
||||
onLongPressStart: (details) {
|
||||
|
@ -212,16 +257,16 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||
valueListenable: _thumbOffsetNotifier,
|
||||
builder: (context, thumbOffset, child) => Container(
|
||||
alignment: AlignmentDirectional.topEnd,
|
||||
padding: EdgeInsets.only(top: thumbOffset) + (widget.padding ?? EdgeInsets.zero),
|
||||
padding: EdgeInsets.only(top: thumbOffset) + widget.padding,
|
||||
child: widget.scrollThumbBuilder(
|
||||
widget.backgroundColor,
|
||||
_thumbAnimation,
|
||||
_labelAnimation,
|
||||
widget.scrollThumbHeight,
|
||||
labelText: (widget.labelTextBuilder != null && _isDragInProcess)
|
||||
widget.scrollThumbSize.height,
|
||||
labelText: _isDragInProcess
|
||||
? ValueListenableBuilder<double>(
|
||||
valueListenable: _viewOffsetNotifier,
|
||||
builder: (context, viewOffset, child) => widget.labelTextBuilder!.call(viewOffset + thumbOffset),
|
||||
builder: (context, viewOffset, child) => widget.labelTextBuilder(viewOffset + thumbOffset),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
@ -261,6 +306,7 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||
_labelAnimationController.forward();
|
||||
_fadeoutTimer?.cancel();
|
||||
_showThumb();
|
||||
_updateViewportCrumbs();
|
||||
setState(() => _isDragInProcess = true);
|
||||
}
|
||||
|
||||
|
@ -296,6 +342,50 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
|
|||
_fadeoutTimer = null;
|
||||
});
|
||||
}
|
||||
|
||||
void _updateViewportCrumbs() {
|
||||
_viewportCrumbs.clear();
|
||||
final crumbsBuilder = widget.crumbsBuilder;
|
||||
if (crumbsBuilder != null) {
|
||||
final position = controller.position;
|
||||
final contentHeight = position.maxScrollExtent + thumbMaxScrollExtent + position.viewportDimension;
|
||||
final ratio = contentHeight / scrollBarHeight;
|
||||
if (ratio > crumbOffsetRatioThreshold) {
|
||||
final maxLabelOffset = scrollBarHeight - widget.scrollThumbSize.height;
|
||||
double lastLabelOffset = -crumbPadding;
|
||||
_modelCrumbs ??= crumbsBuilder();
|
||||
_modelCrumbs!.entries.forEach((kv) {
|
||||
final viewOffset = kv.key;
|
||||
final label = kv.value;
|
||||
final labelOffset = (viewOffset / ratio).roundToDouble();
|
||||
if (labelOffset >= lastLabelOffset + crumbPadding && labelOffset < maxLabelOffset) {
|
||||
lastLabelOffset = labelOffset;
|
||||
_viewportCrumbs.add(_Crumb(
|
||||
viewOffset: viewOffset,
|
||||
labelOffset: labelOffset,
|
||||
label: label,
|
||||
));
|
||||
}
|
||||
});
|
||||
// hide lonesome crumb, whether it is because of a single section,
|
||||
// or because multiple sections collapsed to a single crumb
|
||||
if (_viewportCrumbs.length == 1) {
|
||||
_viewportCrumbs.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _Crumb {
|
||||
final double viewOffset, labelOffset;
|
||||
final String label;
|
||||
|
||||
const _Crumb({
|
||||
required this.viewOffset,
|
||||
required this.labelOffset,
|
||||
required this.label,
|
||||
});
|
||||
}
|
||||
|
||||
///This cut 2 lines in arrow shape
|
||||
|
|
|
@ -5,6 +5,26 @@ import 'package:flutter/material.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DraggableCrumbLabel extends StatelessWidget {
|
||||
final String label;
|
||||
|
||||
const DraggableCrumbLabel({
|
||||
Key? key,
|
||||
required this.label,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: _crumbLabelMaxWidth),
|
||||
child: Padding(
|
||||
padding: _padding,
|
||||
child: _buildText(label, isCrumb: true),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DraggableThumbLabel<T> extends StatelessWidget {
|
||||
final double offsetY;
|
||||
final List<String> Function(BuildContext context, T item) lineBuilder;
|
||||
|
@ -30,30 +50,20 @@ class DraggableThumbLabel<T> extends StatelessWidget {
|
|||
if (lines.isEmpty) return const SizedBox();
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 140),
|
||||
constraints: const BoxConstraints(maxWidth: _thumbLabelMaxWidth),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
padding: _padding,
|
||||
child: lines.length > 1
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: lines.map(_buildText).toList(),
|
||||
children: lines.map((v) => _buildText(v, isCrumb: false)).toList(),
|
||||
)
|
||||
: _buildText(lines.first),
|
||||
: _buildText(lines.first, isCrumb: false),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildText(String text) => Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
),
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
);
|
||||
|
||||
static String formatMonthThumbLabel(BuildContext context, DateTime? date) {
|
||||
final l10n = context.l10n;
|
||||
if (date == null) return l10n.sectionUnknown;
|
||||
|
@ -66,3 +76,18 @@ class DraggableThumbLabel<T> extends StatelessWidget {
|
|||
return formatDay(date, l10n.localeName);
|
||||
}
|
||||
}
|
||||
|
||||
const double _crumbLabelMaxWidth = 96;
|
||||
const double _thumbLabelMaxWidth = 144;
|
||||
const EdgeInsets _padding = EdgeInsets.symmetric(vertical: 4, horizontal: 8);
|
||||
|
||||
Widget _buildText(String text, {required bool isCrumb}) => Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: isCrumb ? 10 : 14,
|
||||
),
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
);
|
||||
|
|
|
@ -15,12 +15,12 @@ ScrollThumbBuilder avesScrollThumbBuilder({
|
|||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
height: height,
|
||||
margin: const EdgeInsetsDirectional.only(end: 1),
|
||||
padding: const EdgeInsets.all(2),
|
||||
margin: _margin,
|
||||
padding: _padding,
|
||||
child: ClipPath(
|
||||
clipper: ArrowClipper(),
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
width: _width,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
|
@ -38,3 +38,9 @@ ScrollThumbBuilder avesScrollThumbBuilder({
|
|||
);
|
||||
};
|
||||
}
|
||||
|
||||
const _margin = EdgeInsetsDirectional.only(end: 1);
|
||||
const _padding = EdgeInsets.all(2);
|
||||
const _width = 20.0;
|
||||
|
||||
double get avesScrollThumbWidth => _width + _padding.horizontal + _margin.horizontal;
|
||||
|
|
|
@ -197,11 +197,12 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
|
|||
}
|
||||
|
||||
void onTap(TapUpDetails details) {
|
||||
if (widget.onTap == null) return;
|
||||
final onTap = widget.onTap;
|
||||
if (onTap == null) return;
|
||||
|
||||
final viewportTapPosition = details.localPosition;
|
||||
final childTapPosition = scaleBoundaries.viewportToChildPosition(controller, viewportTapPosition);
|
||||
widget.onTap!.call(context, details, controller.currentState, childTapPosition);
|
||||
onTap(context, details, controller.currentState, childTapPosition);
|
||||
}
|
||||
|
||||
void onDoubleTap(TapDownDetails details) {
|
||||
|
|
|
@ -503,7 +503,7 @@ class _FilterScrollView<T extends CollectionFilter> extends StatelessWidget {
|
|||
selector: (context, mq) => mq.effectiveBottomPadding,
|
||||
builder: (context, mqPaddingBottom, child) => DraggableScrollbar(
|
||||
backgroundColor: Colors.white,
|
||||
scrollThumbHeight: avesScrollThumbHeight,
|
||||
scrollThumbSize: Size(avesScrollThumbWidth, avesScrollThumbHeight),
|
||||
scrollThumbBuilder: avesScrollThumbBuilder(
|
||||
height: avesScrollThumbHeight,
|
||||
backgroundColor: Colors.white,
|
||||
|
@ -518,6 +518,7 @@ class _FilterScrollView<T extends CollectionFilter> extends StatelessWidget {
|
|||
sortFactor: sortFactor,
|
||||
offsetY: offsetY,
|
||||
),
|
||||
crumbTextBuilder: (offsetY) => const SizedBox(),
|
||||
child: scrollView,
|
||||
),
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue