revert moving scroll function out of scaling gesture detector (rebuild perf issue)

monitor scrolling for thumbnail loading
This commit is contained in:
Thibault Deckers 2020-05-08 17:51:59 +09:00
parent 2dc4cd6fe9
commit e9d12ed3f3
6 changed files with 184 additions and 104 deletions

View file

@ -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 {

View file

@ -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);
}

View file

@ -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 {

View file

@ -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,
);
}
}

View file

@ -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) {

View file

@ -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();
}
}