revert moving scroll function out of scaling gesture detector (rebuild perf issue)
monitor scrolling for thumbnail loading
This commit is contained in:
parent
2dc4cd6fe9
commit
e9d12ed3f3
6 changed files with 184 additions and 104 deletions
|
@ -3,8 +3,8 @@ import 'dart:math';
|
|||
import 'package:aves/model/collection_lens.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/widgets/album/grid/header_generic.dart';
|
||||
import 'package:aves/widgets/album/grid/list_sliver.dart';
|
||||
import 'package:aves/widgets/album/grid/tile_extent_manager.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
@ -13,12 +13,14 @@ class SectionedListLayoutProvider extends StatelessWidget {
|
|||
final int columnCount;
|
||||
final double scrollableWidth;
|
||||
final double tileExtent;
|
||||
final Widget Function(ImageEntry entry) thumbnailBuilder;
|
||||
final Widget child;
|
||||
|
||||
SectionedListLayoutProvider({
|
||||
@required this.collection,
|
||||
@required this.scrollableWidth,
|
||||
@required this.tileExtent,
|
||||
@required this.thumbnailBuilder,
|
||||
@required this.child,
|
||||
}) : columnCount = max((scrollableWidth / tileExtent).round(), TileExtentManager.columnCountMin);
|
||||
|
||||
|
@ -89,22 +91,12 @@ class SectionedListLayoutProvider extends StatelessWidget {
|
|||
|
||||
final minEntryIndex = sectionChildIndex * columnCount;
|
||||
final maxEntryIndex = min(sectionEntryCount, minEntryIndex + columnCount);
|
||||
final ids = <int>[];
|
||||
final children = <Widget>[];
|
||||
for (var i = minEntryIndex; i < maxEntryIndex; i++) {
|
||||
final entry = section[i];
|
||||
final id = entry.contentId;
|
||||
ids.add(id);
|
||||
children.add(GridThumbnail(
|
||||
key: ValueKey(id),
|
||||
collection: collection,
|
||||
index: i,
|
||||
entry: entry,
|
||||
tileExtent: tileExtent,
|
||||
));
|
||||
children.add(thumbnailBuilder(entry));
|
||||
}
|
||||
return Row(
|
||||
key: ValueKey(ids.join('-')),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
);
|
||||
|
@ -142,6 +134,25 @@ class SectionedListLayout {
|
|||
final top = sectionLayout.indexToLayoutOffset(listIndex);
|
||||
return Rect.fromLTWH(left, top, tileExtent, tileExtent);
|
||||
}
|
||||
|
||||
int rowIndex(dynamic sectionKey, List<int> builtIds) {
|
||||
if (!collection.sections.containsKey(sectionKey)) return null;
|
||||
|
||||
final section = collection.sections[sectionKey];
|
||||
final firstId = builtIds.first;
|
||||
final firstIndexInSection = section.indexWhere((entry) => entry.contentId == firstId);
|
||||
if (firstIndexInSection % columnCount != 0) return null;
|
||||
|
||||
final collectionIds = section.skip(firstIndexInSection).take(builtIds.length).map((entry) => entry.contentId);
|
||||
final eq = const IterableEquality().equals;
|
||||
if (eq(builtIds, collectionIds)) {
|
||||
final sectionLayout = sectionLayouts.firstWhere((section) => section.sectionKey == sectionKey, orElse: () => null);
|
||||
if (sectionLayout == null) return null;
|
||||
return sectionLayout.firstIndex + 1 + firstIndexInSection ~/ columnCount;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class SectionLayout {
|
||||
|
|
|
@ -35,18 +35,16 @@ class CollectionListSliver extends StatelessWidget {
|
|||
|
||||
class GridThumbnail extends StatelessWidget {
|
||||
final CollectionLens collection;
|
||||
final int index;
|
||||
final ImageEntry entry;
|
||||
final double tileExtent;
|
||||
final GestureTapCallback onTap;
|
||||
final ValueNotifier<bool> isScrollingNotifier;
|
||||
|
||||
const GridThumbnail({
|
||||
Key key,
|
||||
this.collection,
|
||||
this.index,
|
||||
this.entry,
|
||||
this.tileExtent,
|
||||
this.onTap,
|
||||
@required this.entry,
|
||||
@required this.tileExtent,
|
||||
this.isScrollingNotifier,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -66,11 +64,12 @@ class GridThumbnail extends StatelessWidget {
|
|||
}
|
||||
},
|
||||
child: MetaData(
|
||||
metaData: ThumbnailMetadata(index, entry),
|
||||
metaData: ThumbnailMetadata(entry),
|
||||
child: DecoratedThumbnail(
|
||||
entry: entry,
|
||||
extent: tileExtent,
|
||||
heroTag: collection.heroTag(entry),
|
||||
collection: collection,
|
||||
isScrollingNotifier: isScrollingNotifier,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -91,8 +90,7 @@ class GridThumbnail extends StatelessWidget {
|
|||
|
||||
// metadata to identify entry from RenderObject hit test during collection scaling
|
||||
class ThumbnailMetadata {
|
||||
final int index;
|
||||
final ImageEntry entry;
|
||||
|
||||
const ThumbnailMetadata(this.index, this.entry);
|
||||
const ThumbnailMetadata(this.entry);
|
||||
}
|
||||
|
|
|
@ -2,27 +2,29 @@ import 'dart:math';
|
|||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/widgets/album/grid/list_section_layout.dart';
|
||||
import 'package:aves/widgets/album/grid/list_sliver.dart';
|
||||
import 'package:aves/widgets/album/grid/tile_extent_manager.dart';
|
||||
import 'package:aves/widgets/album/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class GridScaleGestureDetector extends StatefulWidget {
|
||||
final GlobalKey scrollableKey;
|
||||
final ValueNotifier<double> appBarHeightNotifier;
|
||||
final ValueNotifier<double> extentNotifier;
|
||||
final Size mqSize;
|
||||
final double mqHorizontalPadding;
|
||||
final void Function(ImageEntry entry) onScaled;
|
||||
final Widget child;
|
||||
|
||||
const GridScaleGestureDetector({
|
||||
this.scrollableKey,
|
||||
@required this.appBarHeightNotifier,
|
||||
@required this.extentNotifier,
|
||||
@required this.mqSize,
|
||||
@required this.mqHorizontalPadding,
|
||||
@required this.onScaled,
|
||||
@required this.child,
|
||||
});
|
||||
|
||||
|
@ -110,7 +112,7 @@ class _GridScaleGestureDetectorState extends State<GridScaleGestureDetector> {
|
|||
} else {
|
||||
// scroll to show the focal point thumbnail at its new position
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onScaled(_metadata.entry);
|
||||
_scrollToEntry(_metadata.entry);
|
||||
_applyingScale = false;
|
||||
});
|
||||
}
|
||||
|
@ -118,6 +120,23 @@ class _GridScaleGestureDetectorState extends State<GridScaleGestureDetector> {
|
|||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
// about scrolling & offset retrieval:
|
||||
// `Scrollable.ensureVisible` only works on already rendered objects
|
||||
// `RenderViewport.showOnScreen` can find any `RenderSliver`, but not always a `RenderMetadata`
|
||||
// `RenderViewport.scrollOffsetOf` is a good alternative
|
||||
void _scrollToEntry(ImageEntry entry) {
|
||||
final scrollableContext = widget.scrollableKey.currentContext;
|
||||
final scrollableHeight = (scrollableContext.findRenderObject() as RenderBox).size.height;
|
||||
final sectionedListLayout = Provider.of<SectionedListLayout>(context, listen: false);
|
||||
final tileRect = sectionedListLayout.getTileRect(entry) ?? Rect.zero;
|
||||
// most of the time the app bar will be scrolled away after scaling,
|
||||
// so we compensate for it to center the focal point thumbnail
|
||||
final appBarHeight = widget.appBarHeightNotifier.value;
|
||||
final scrollOffset = tileRect.top + (tileRect.height - scrollableHeight) / 2 + appBarHeight;
|
||||
|
||||
PrimaryScrollController.of(context)?.jumpTo(max(.0, scrollOffset));
|
||||
}
|
||||
}
|
||||
|
||||
class ScaleOverlay extends StatefulWidget {
|
||||
|
|
|
@ -8,6 +8,7 @@ class DecoratedThumbnail extends StatelessWidget {
|
|||
final ImageEntry entry;
|
||||
final double extent;
|
||||
final Object heroTag;
|
||||
final ValueNotifier<bool> isScrollingNotifier;
|
||||
final bool showOverlay;
|
||||
|
||||
static final Color borderColor = Colors.grey.shade700;
|
||||
|
@ -18,11 +19,42 @@ class DecoratedThumbnail extends StatelessWidget {
|
|||
@required this.entry,
|
||||
@required this.extent,
|
||||
this.heroTag,
|
||||
this.isScrollingNotifier,
|
||||
this.showOverlay = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final child = Stack(
|
||||
children: [
|
||||
entry.isSvg
|
||||
? ThumbnailVectorImage(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
heroTag: heroTag,
|
||||
)
|
||||
: ThumbnailRasterImage(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
isScrollingNotifier: isScrollingNotifier,
|
||||
heroTag: heroTag,
|
||||
),
|
||||
if (showOverlay)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
child: ThumbnailEntryOverlay(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
),
|
||||
),
|
||||
if (showOverlay)
|
||||
ThumbnailSelectionOverlay(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
),
|
||||
],
|
||||
);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
|
@ -32,35 +64,7 @@ class DecoratedThumbnail extends StatelessWidget {
|
|||
),
|
||||
width: extent,
|
||||
height: extent,
|
||||
child: Stack(
|
||||
children: [
|
||||
entry.isSvg
|
||||
? ThumbnailVectorImage(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
heroTag: heroTag,
|
||||
)
|
||||
: ThumbnailRasterImage(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
heroTag: heroTag,
|
||||
),
|
||||
if (showOverlay)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
child: ThumbnailEntryOverlay(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
),
|
||||
),
|
||||
if (showOverlay)
|
||||
ThumbnailSelectionOverlay(
|
||||
entry: entry,
|
||||
extent: extent,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,12 +8,14 @@ import 'package:flutter/material.dart';
|
|||
class ThumbnailRasterImage extends StatefulWidget {
|
||||
final ImageEntry entry;
|
||||
final double extent;
|
||||
final ValueNotifier<bool> isScrollingNotifier;
|
||||
final Object heroTag;
|
||||
|
||||
const ThumbnailRasterImage({
|
||||
Key key,
|
||||
@required this.entry,
|
||||
@required this.extent,
|
||||
this.isScrollingNotifier,
|
||||
this.heroTag,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -53,7 +55,15 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
|||
|
||||
void _initProvider() => _imageProvider = ThumbnailProvider(entry: entry, extent: Constants.thumbnailCacheExtent);
|
||||
|
||||
void _pauseProvider() => _imageProvider?.pause();
|
||||
void _pauseProvider() {
|
||||
final isScrolling = widget.isScrollingNotifier?.value ?? false;
|
||||
// when the user is scrolling faster than we can retrieve the thumbnails,
|
||||
// the retrieval task queue can pile up for thumbnails that got disposed
|
||||
// in this case we pause the image retrieval task to get it out of the queue
|
||||
if (isScrolling) {
|
||||
_imageProvider?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import 'dart:math';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/collection_lens.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/mime_types.dart';
|
||||
import 'package:aves/widgets/album/app_bar.dart';
|
||||
import 'package:aves/widgets/album/empty.dart';
|
||||
|
@ -21,6 +20,8 @@ import 'package:tuple/tuple.dart';
|
|||
class ThumbnailCollection extends StatelessWidget {
|
||||
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
|
||||
final ValueNotifier<double> _tileExtentNotifier = ValueNotifier(0);
|
||||
final ValueNotifier<bool> _isScrollingNotifier = ValueNotifier(false);
|
||||
final GlobalKey _scrollableKey = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -36,9 +37,25 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
// so that view updates on collection filter changes
|
||||
return Consumer<CollectionLens>(
|
||||
builder: (context, collection, child) {
|
||||
final appBar = CollectionAppBar(
|
||||
appBarHeightNotifier: _appBarHeightNotifier,
|
||||
final scrollView = CollectionScrollView(
|
||||
scrollableKey: _scrollableKey,
|
||||
collection: collection,
|
||||
appBar: CollectionAppBar(
|
||||
appBarHeightNotifier: _appBarHeightNotifier,
|
||||
collection: collection,
|
||||
),
|
||||
appBarHeightNotifier: _appBarHeightNotifier,
|
||||
isScrollingNotifier: _isScrollingNotifier,
|
||||
scrollController: PrimaryScrollController.of(context),
|
||||
);
|
||||
|
||||
final scaler = GridScaleGestureDetector(
|
||||
scrollableKey: _scrollableKey,
|
||||
appBarHeightNotifier: _appBarHeightNotifier,
|
||||
extentNotifier: _tileExtentNotifier,
|
||||
mqSize: mqSize,
|
||||
mqHorizontalPadding: mqHorizontalPadding,
|
||||
child: scrollView,
|
||||
);
|
||||
|
||||
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
|
||||
|
@ -47,14 +64,14 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
collection: collection,
|
||||
scrollableWidth: mqSize.width - mqHorizontalPadding,
|
||||
tileExtent: tileExtent,
|
||||
child: _ScalableThumbnailCollection(
|
||||
appBarHeightNotifier: _appBarHeightNotifier,
|
||||
tileExtentNotifier: _tileExtentNotifier,
|
||||
thumbnailBuilder: (entry) => GridThumbnail(
|
||||
key: ValueKey(entry.contentId),
|
||||
collection: collection,
|
||||
mqSize: mqSize,
|
||||
mqHorizontalPadding: mqHorizontalPadding,
|
||||
appBar: appBar,
|
||||
entry: entry,
|
||||
tileExtent: tileExtent,
|
||||
isScrollingNotifier: _isScrollingNotifier,
|
||||
),
|
||||
child: scaler,
|
||||
),
|
||||
);
|
||||
return sectionedListLayoutProvider;
|
||||
|
@ -66,42 +83,67 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _ScalableThumbnailCollection extends StatelessWidget {
|
||||
class CollectionScrollView extends StatefulWidget {
|
||||
final GlobalKey scrollableKey;
|
||||
final CollectionLens collection;
|
||||
final ValueNotifier<double> appBarHeightNotifier;
|
||||
final ValueNotifier<double> tileExtentNotifier;
|
||||
final Size mqSize;
|
||||
final double mqHorizontalPadding;
|
||||
final Widget appBar;
|
||||
final ValueNotifier<double> appBarHeightNotifier;
|
||||
final ValueNotifier<bool> isScrollingNotifier;
|
||||
final ScrollController scrollController;
|
||||
|
||||
final GlobalKey _scrollableKey = GlobalKey();
|
||||
|
||||
_ScalableThumbnailCollection({
|
||||
@required this.appBarHeightNotifier,
|
||||
@required this.tileExtentNotifier,
|
||||
const CollectionScrollView({
|
||||
@required this.scrollableKey,
|
||||
@required this.collection,
|
||||
@required this.mqSize,
|
||||
@required this.mqHorizontalPadding,
|
||||
@required this.appBar,
|
||||
@required this.appBarHeightNotifier,
|
||||
@required this.isScrollingNotifier,
|
||||
@required this.scrollController,
|
||||
});
|
||||
|
||||
@override
|
||||
_CollectionScrollViewState createState() => _CollectionScrollViewState();
|
||||
}
|
||||
|
||||
class _CollectionScrollViewState extends State<CollectionScrollView> {
|
||||
Timer _scrollMonitoringTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registerWidget(widget);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CollectionScrollView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_unregisterWidget(oldWidget);
|
||||
_registerWidget(widget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unregisterWidget(widget);
|
||||
_stopScrollMonitoringTimer();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _registerWidget(CollectionScrollView widget) {
|
||||
widget.scrollController.addListener(_onScrollChange);
|
||||
}
|
||||
|
||||
void _unregisterWidget(CollectionScrollView widget) {
|
||||
widget.scrollController.removeListener(_onScrollChange);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scrollView = _buildScrollView(appBar, collection);
|
||||
final draggable = _buildDraggableScrollView(scrollView);
|
||||
return GridScaleGestureDetector(
|
||||
scrollableKey: _scrollableKey,
|
||||
extentNotifier: tileExtentNotifier,
|
||||
mqSize: mqSize,
|
||||
mqHorizontalPadding: mqHorizontalPadding,
|
||||
onScaled: (entry) => _scrollToEntry(context, entry),
|
||||
child: draggable,
|
||||
);
|
||||
final scrollView = _buildScrollView(widget.appBar, widget.collection);
|
||||
return _buildDraggableScrollView(scrollView);
|
||||
}
|
||||
|
||||
ScrollView _buildScrollView(Widget appBar, CollectionLens collection) {
|
||||
return CustomScrollView(
|
||||
key: _scrollableKey,
|
||||
key: widget.scrollableKey,
|
||||
primary: true,
|
||||
// workaround to prevent scrolling the app bar away
|
||||
// when there is no content and we use `SliverFillRemaining`
|
||||
|
@ -128,7 +170,7 @@ class _ScalableThumbnailCollection extends StatelessWidget {
|
|||
|
||||
Widget _buildDraggableScrollView(ScrollView scrollView) {
|
||||
return ValueListenableBuilder<double>(
|
||||
valueListenable: appBarHeightNotifier,
|
||||
valueListenable: widget.appBarHeightNotifier,
|
||||
builder: (context, appBarHeight, child) => Selector<MediaQueryData, double>(
|
||||
selector: (context, mq) => mq.viewInsets.bottom,
|
||||
builder: (context, mqViewInsetsBottom, child) => DraggableScrollbar(
|
||||
|
@ -138,7 +180,7 @@ class _ScalableThumbnailCollection extends StatelessWidget {
|
|||
height: avesScrollThumbHeight,
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
controller: PrimaryScrollController.of(context),
|
||||
controller: widget.scrollController,
|
||||
padding: EdgeInsets.only(
|
||||
// padding to keep scroll thumb between app bar above and nav bar below
|
||||
top: appBarHeight,
|
||||
|
@ -164,20 +206,16 @@ class _ScalableThumbnailCollection extends StatelessWidget {
|
|||
: const EmptyContent();
|
||||
}
|
||||
|
||||
// about scrolling & offset retrieval:
|
||||
// `Scrollable.ensureVisible` only works on already rendered objects
|
||||
// `RenderViewport.showOnScreen` can find any `RenderSliver`, but not always a `RenderMetadata`
|
||||
// `RenderViewport.scrollOffsetOf` is a good alternative
|
||||
void _scrollToEntry(BuildContext context, ImageEntry entry) {
|
||||
final scrollableContext = _scrollableKey.currentContext;
|
||||
final scrollableHeight = (scrollableContext.findRenderObject() as RenderBox).size.height;
|
||||
final sectionLayout = Provider.of<SectionedListLayout>(context, listen: false);
|
||||
final tileRect = sectionLayout.getTileRect(entry) ?? Rect.zero;
|
||||
// most of the time the app bar will be scrolled away after scaling,
|
||||
// so we compensate for it to center the focal point thumbnail
|
||||
final appBarHeight = appBarHeightNotifier.value;
|
||||
final scrollOffset = tileRect.top + (tileRect.height - scrollableHeight) / 2 + appBarHeight;
|
||||
void _onScrollChange() {
|
||||
widget.isScrollingNotifier.value = true;
|
||||
_stopScrollMonitoringTimer();
|
||||
_scrollMonitoringTimer = Timer(const Duration(milliseconds: 100), () {
|
||||
debugPrint('$runtimeType _onScrollChange is scrolling false');
|
||||
widget.isScrollingNotifier.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
PrimaryScrollController.of(context)?.jumpTo(max(.0, scrollOffset));
|
||||
void _stopScrollMonitoringTimer() {
|
||||
_scrollMonitoringTimer?.cancel();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue