diff --git a/lib/widgets/album/grid/list_section_layout.dart b/lib/widgets/album/grid/list_section_layout.dart index de6a2ce04..58e004c88 100644 --- a/lib/widgets/album/grid/list_section_layout.dart +++ b/lib/widgets/album/grid/list_section_layout.dart @@ -89,11 +89,14 @@ class SectionedListLayoutProvider extends StatelessWidget { final minEntryIndex = sectionChildIndex * columnCount; final maxEntryIndex = min(sectionEntryCount, minEntryIndex + columnCount); + final ids = []; final children = []; for (var i = minEntryIndex; i < maxEntryIndex; i++) { final entry = section[i]; + final id = entry.contentId; + ids.add(id); children.add(GridThumbnail( - key: ValueKey(entry.contentId), + key: ValueKey(id), collection: collection, index: i, entry: entry, @@ -101,6 +104,7 @@ class SectionedListLayoutProvider extends StatelessWidget { )); } return Row( + key: ValueKey(ids.join('-')), mainAxisSize: MainAxisSize.min, children: children, ); @@ -136,7 +140,6 @@ class SectionedListLayout { final left = tileExtent * column; final top = sectionLayout.indexToLayoutOffset(listIndex); - debugPrint('TLAD getTileRect sectionKey=$sectionKey sectionOffset=${sectionLayout.minOffset} top=$top row=$row column=$column for title=${entry.bestTitle}'); return Rect.fromLTWH(left, top, tileExtent, tileExtent); } } diff --git a/lib/widgets/album/grid/scaling.dart b/lib/widgets/album/grid/scaling.dart index 3169b57c7..4f26fcd63 100644 --- a/lib/widgets/album/grid/scaling.dart +++ b/lib/widgets/album/grid/scaling.dart @@ -2,29 +2,27 @@ 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 appBarHeightNotifier; final ValueNotifier 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, }); @@ -38,7 +36,6 @@ class _GridScaleGestureDetectorState extends State { ValueNotifier _scaledExtentNotifier; OverlayEntry _overlayEntry; ThumbnailMetadata _metadata; - RenderViewport _renderViewport; ValueNotifier get tileExtentNotifier => widget.extentNotifier; @@ -64,7 +61,6 @@ class _GridScaleGestureDetectorState extends State { final renderMetaData = firstOf(result); // abort if we cannot find an image to show on overlay if (renderMetaData == null) return; - _renderViewport = firstOf(result); _metadata = renderMetaData.metaData; _startExtent = tileExtentNotifier.value; _scaledExtentNotifier = ValueNotifier(_startExtent); @@ -113,21 +109,8 @@ class _GridScaleGestureDetectorState extends State { _applyingScale = false; } else { // scroll to show the focal point thumbnail at its new position - final viewportClosure = _renderViewport; WidgetsBinding.instance.addPostFrameCallback((_) { - // 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 - final scrollableContext = widget.scrollableKey.currentContext; - final gridSize = (scrollableContext.findRenderObject() as RenderBox).size; - final sectionLayout = Provider.of(context, listen: false); - final tileRect = sectionLayout.getTileRect(_metadata.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 - gridSize.height) / 2 + appBarHeight; - viewportClosure.offset.jumpTo(max(.0, scrollOffset)); + widget.onScaled(_metadata.entry); _applyingScale = false; }); } diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index 901039601..5700996a9 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -1,6 +1,9 @@ +import 'dart:math'; + 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'; @@ -18,7 +21,6 @@ import 'package:tuple/tuple.dart'; class ThumbnailCollection extends StatelessWidget { final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); final ValueNotifier _tileExtentNotifier = ValueNotifier(0); - final GlobalKey _scrollableKey = GlobalKey(); @override Widget build(BuildContext context) { @@ -34,23 +36,25 @@ class ThumbnailCollection extends StatelessWidget { // so that view updates on collection filter changes return Consumer( builder: (context, collection, child) { - final scrollView = _buildScrollView(collection); - final draggable = _buildDraggableScrollView(scrollView); - final scaler = GridScaleGestureDetector( - scrollableKey: _scrollableKey, + final appBar = CollectionAppBar( appBarHeightNotifier: _appBarHeightNotifier, - extentNotifier: _tileExtentNotifier, - mqSize: mqSize, - mqHorizontalPadding: mqHorizontalPadding, - child: draggable, + collection: collection, ); + final sectionedListLayoutProvider = ValueListenableBuilder( valueListenable: _tileExtentNotifier, builder: (context, tileExtent, child) => SectionedListLayoutProvider( collection: collection, scrollableWidth: mqSize.width - mqHorizontalPadding, tileExtent: tileExtent, - child: scaler, + child: _ScalableThumbnailCollection( + appBarHeightNotifier: _appBarHeightNotifier, + tileExtentNotifier: _tileExtentNotifier, + collection: collection, + mqSize: mqSize, + mqHorizontalPadding: mqHorizontalPadding, + appBar: appBar, + ), ), ); return sectionedListLayoutProvider; @@ -60,8 +64,42 @@ class ThumbnailCollection extends StatelessWidget { ), ); } +} - ScrollView _buildScrollView(CollectionLens collection) { +class _ScalableThumbnailCollection extends StatelessWidget { + final CollectionLens collection; + final ValueNotifier appBarHeightNotifier; + final ValueNotifier tileExtentNotifier; + final Size mqSize; + final double mqHorizontalPadding; + final Widget appBar; + + final GlobalKey _scrollableKey = GlobalKey(); + + _ScalableThumbnailCollection({ + @required this.appBarHeightNotifier, + @required this.tileExtentNotifier, + @required this.collection, + @required this.mqSize, + @required this.mqHorizontalPadding, + @required this.appBar, + }); + + @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, + ); + } + + ScrollView _buildScrollView(Widget appBar, CollectionLens collection) { return CustomScrollView( key: _scrollableKey, primary: true, @@ -69,10 +107,7 @@ class ThumbnailCollection extends StatelessWidget { // when there is no content and we use `SliverFillRemaining` physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : null, slivers: [ - CollectionAppBar( - appBarHeightNotifier: _appBarHeightNotifier, - collection: collection, - ), + appBar, collection.isEmpty ? SliverFillRemaining( child: _buildEmptyCollectionPlaceholder(collection), @@ -93,7 +128,7 @@ class ThumbnailCollection extends StatelessWidget { Widget _buildDraggableScrollView(ScrollView scrollView) { return ValueListenableBuilder( - valueListenable: _appBarHeightNotifier, + valueListenable: appBarHeightNotifier, builder: (context, appBarHeight, child) => Selector( selector: (context, mq) => mq.viewInsets.bottom, builder: (context, mqViewInsetsBottom, child) => DraggableScrollbar( @@ -128,4 +163,21 @@ class ThumbnailCollection 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(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; + + PrimaryScrollController.of(context)?.jumpTo(max(.0, scrollOffset)); + } }