diff --git a/lib/widgets/album/grid/header_generic.dart b/lib/widgets/album/grid/header_generic.dart index 311c66732..a2bfddf8e 100644 --- a/lib/widgets/album/grid/header_generic.dart +++ b/lib/widgets/album/grid/header_generic.dart @@ -2,7 +2,6 @@ import 'dart:math'; import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/collection_source.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/album/grid/header_album.dart'; @@ -14,13 +13,11 @@ import 'package:flutter/rendering.dart'; class SectionHeader extends StatelessWidget { final CollectionLens collection; - final Map> sections; final dynamic sectionKey; const SectionHeader({ Key key, @required this.collection, - @required this.sections, @required this.sectionKey, }) : super(key: key); diff --git a/lib/widgets/album/grid/list_section_layout.dart b/lib/widgets/album/grid/list_section_layout.dart index 0292397d2..42e981131 100644 --- a/lib/widgets/album/grid/list_section_layout.dart +++ b/lib/widgets/album/grid/list_section_layout.dart @@ -1,4 +1,109 @@ +import 'dart:math'; + +import 'package:aves/model/collection_lens.dart'; +import 'package:aves/widgets/album/grid/header_generic.dart'; +import 'package:aves/widgets/album/grid/list_sliver.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class SectionedListLayoutProvider extends StatelessWidget { + final Widget child; + + final CollectionLens collection; + final int columnCount; + final double scrollableWidth; + final double tileExtent; + + SectionedListLayoutProvider({ + @required this.collection, + @required this.scrollableWidth, + @required this.tileExtent, + @required this.child, + }) : columnCount = (scrollableWidth / tileExtent).round(); + + @override + Widget build(BuildContext context) { + return ProxyProvider0( + update: (context, __) => _updateLayouts(context), + child: child, + ); + } + + SectionedListLayout _updateLayouts(BuildContext context) { + debugPrint('$runtimeType _updateLayouts entries=${collection.entryCount} columnCount=$columnCount tileExtent=$tileExtent'); + final sectionLayouts = []; + final showHeaders = collection.showHeaders; + final source = collection.source; + final sections = collection.sections; + final sectionKeys = sections.keys.toList(); + var currentIndex = 0, currentOffset = 0.0; + sectionKeys.forEach((sectionKey) { + final sectionEntryCount = sections[sectionKey].length; + final sectionChildCount = 1 + (sectionEntryCount / columnCount).ceil(); + + final headerExtent = showHeaders ? SectionHeader.computeHeaderHeight(source, sectionKey, scrollableWidth) : 0.0; + + final sectionFirstIndex = currentIndex; + currentIndex += sectionChildCount; + final sectionLastIndex = currentIndex - 1; + + final sectionMinOffset = currentOffset; + currentOffset += headerExtent + tileExtent * (sectionChildCount - 1); + final sectionMaxOffset = currentOffset; + + sectionLayouts.add( + SectionLayout( + sectionKey: sectionKey, + firstIndex: sectionFirstIndex, + lastIndex: sectionLastIndex, + minOffset: sectionMinOffset, + maxOffset: sectionMaxOffset, + headerExtent: headerExtent, + tileExtent: tileExtent, + builder: (context, listIndex) => _buildInSection(listIndex - sectionFirstIndex, collection, sectionKey), + ), + ); + }); + return SectionedListLayout(sectionLayouts); + } + + Widget _buildInSection(int sectionChildIndex, CollectionLens collection, dynamic sectionKey) { + if (sectionChildIndex == 0) { + return collection.showHeaders + ? SectionHeader( + collection: collection, + sectionKey: sectionKey, + ) + : const SizedBox.shrink(); + } + sectionChildIndex--; + + final section = collection.sections[sectionKey]; + final sectionEntryCount = section.length; + + final minEntryIndex = sectionChildIndex * columnCount; + final maxEntryIndex = min(sectionEntryCount, minEntryIndex + columnCount); + final children = []; + for (var i = minEntryIndex; i < maxEntryIndex; i++) { + children.add(GridThumbnail( + collection: collection, + index: i, + entry: section[i], + tileExtent: tileExtent, + )); + } + return Row( + mainAxisSize: MainAxisSize.min, + children: children, + ); + } +} + +class SectionedListLayout { + final List sectionLayouts; + + const SectionedListLayout(this.sectionLayouts); +} class SectionLayout { final dynamic sectionKey; diff --git a/lib/widgets/album/grid/list_sliver.dart b/lib/widgets/album/grid/list_sliver.dart index 3cb88f5bf..cb1dfc8cf 100644 --- a/lib/widgets/album/grid/list_sliver.dart +++ b/lib/widgets/album/grid/list_sliver.dart @@ -1,111 +1,36 @@ -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_known_extent.dart'; import 'package:aves/widgets/album/grid/list_section_layout.dart'; import 'package:aves/widgets/album/thumbnail.dart'; import 'package:aves/widgets/album/transparent_material_page_route.dart'; import 'package:aves/widgets/fullscreen/fullscreen_page.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; +import 'package:provider/provider.dart'; // use a `SliverList` instead of multiple `SliverGrid` because having one `SliverGrid` per section does not scale up // with the multiple `SliverGrid` solution, thumbnails at the beginning of each sections are built even though they are offscreen // because of `RenderSliverMultiBoxAdaptor.addInitialChild` called by `RenderSliverGrid.performLayout` (line 547), as of Flutter v1.17.0 class CollectionListSliver extends StatelessWidget { - final CollectionLens collection; - final bool showHeader; - final double scrollableWidth; - final int columnCount; - final double tileExtent; - - CollectionListSliver({ - Key key, - @required this.collection, - @required this.showHeader, - @required this.scrollableWidth, - @required this.tileExtent, - }) : columnCount = (scrollableWidth / tileExtent).round(), - super(key: key); - @override Widget build(BuildContext context) { - final sectionLayouts = []; - final source = collection.source; - final sections = collection.sections; - final sectionKeys = sections.keys.toList(); - var currentIndex = 0, currentOffset = 0.0; - sectionKeys.forEach((sectionKey) { - final sectionEntryCount = sections[sectionKey].length; - final sectionChildCount = 1 + (sectionEntryCount / columnCount).ceil(); - - final headerExtent = showHeader ? SectionHeader.computeHeaderHeight(source, sectionKey, scrollableWidth) : 0.0; - - final sectionFirstIndex = currentIndex; - currentIndex += sectionChildCount; - final sectionLastIndex = currentIndex - 1; - - final sectionMinOffset = currentOffset; - currentOffset += headerExtent + tileExtent * (sectionChildCount - 1); - final sectionMaxOffset = currentOffset; - - sectionLayouts.add( - SectionLayout( - sectionKey: sectionKey, - firstIndex: sectionFirstIndex, - lastIndex: sectionLastIndex, - minOffset: sectionMinOffset, - maxOffset: sectionMaxOffset, - headerExtent: headerExtent, - tileExtent: tileExtent, - builder: (context, listIndex) { - listIndex -= sectionFirstIndex; - if (listIndex == 0) { - return showHeader - ? SectionHeader( - collection: collection, - sections: sections, - sectionKey: sectionKey, - ) - : const SizedBox.shrink(); - } - listIndex--; - - final section = sections[sectionKey]; - final minEntryIndex = listIndex * columnCount; - final maxEntryIndex = min(sectionEntryCount, minEntryIndex + columnCount); - final children = []; - for (var i = minEntryIndex; i < maxEntryIndex; i++) { - children.add(GridThumbnail( - collection: collection, - index: i, - entry: section[i], - tileExtent: tileExtent, - )); - } - return Row( - mainAxisSize: MainAxisSize.min, - children: children, - ); - }, - ), - ); - }); - final childCount = currentIndex; - - return SliverKnownExtentList( - sectionLayouts: sectionLayouts, - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index >= childCount) return null; - final sectionLayout = sectionLayouts.firstWhere((section) => section.hasChild(index), orElse: () => null); - return sectionLayout?.builder(context, index) ?? const SizedBox.shrink(); - }, - childCount: childCount, - addAutomaticKeepAlives: false, - ), + return Consumer( + builder: (context, sectionedListLayout, child) { + final sectionLayouts = sectionedListLayout.sectionLayouts; + final childCount = sectionLayouts.last.lastIndex + 1; + return SliverKnownExtentList( + sectionLayouts: sectionLayouts, + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index >= childCount) return null; + final sectionLayout = sectionLayouts.firstWhere((section) => section.hasChild(index), orElse: () => null); + return sectionLayout?.builder(context, index) ?? const SizedBox.shrink(); + }, + childCount: childCount, + addAutomaticKeepAlives: false, + ), + ); + }, ); } } diff --git a/lib/widgets/album/grid/scaling.dart b/lib/widgets/album/grid/scaling.dart index f31807d60..cd74a3d03 100644 --- a/lib/widgets/album/grid/scaling.dart +++ b/lib/widgets/album/grid/scaling.dart @@ -13,14 +13,14 @@ class GridScaleGestureDetector extends StatefulWidget { final GlobalKey scrollableKey; final ValueNotifier extentNotifier; final Size mqSize; - final EdgeInsets mqPadding; + final double mqHorizontalPadding; final Widget child; const GridScaleGestureDetector({ this.scrollableKey, @required this.extentNotifier, @required this.mqSize, - @required this.mqPadding, + @required this.mqHorizontalPadding, @required this.child, }); @@ -96,7 +96,7 @@ class _GridScaleGestureDetectorState extends State { // sanitize and update grid layout if necessary final newExtent = TileExtentManager.applyTileExtent( widget.mqSize, - widget.mqPadding, + widget.mqHorizontalPadding, tileExtentNotifier, newExtent: _scaledExtentNotifier.value, ); diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index a252f854b..4ac9a84f5 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -5,6 +5,7 @@ import 'package:aves/model/mime_types.dart'; import 'package:aves/widgets/album/app_bar.dart'; import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/album/empty.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/scaling.dart'; import 'package:aves/widgets/album/tile_extent_manager.dart'; @@ -30,84 +31,33 @@ class ThumbnailCollection extends StatelessWidget { @override Widget build(BuildContext context) { return SafeArea( - child: Selector>( - selector: (c, mq) => Tuple3(mq.size, mq.padding, mq.viewInsets.bottom), - builder: (c, mq, child) { + child: Selector>( + selector: (context, mq) => Tuple2(mq.size, mq.padding.horizontal), + builder: (context, mq, child) { final mqSize = mq.item1; - final mqPadding = mq.item2; - final mqViewInsetsBottom = mq.item3; - TileExtentManager.applyTileExtent(mqSize, mqPadding, _tileExtentNotifier); + final mqHorizontalPadding = mq.item2; + TileExtentManager.applyTileExtent(mqSize, mqHorizontalPadding, _tileExtentNotifier); return Consumer( builder: (context, collection, child) { -// debugPrint('$runtimeType collection builder entries=${collection.entryCount}'); - final showHeaders = collection.showHeaders; - return GridScaleGestureDetector( + final scrollView = _buildScrollView(collection); + final draggable = _buildDraggableScrollView(scrollView); + final sectionedListLayoutProvider = ValueListenableBuilder( + valueListenable: _tileExtentNotifier, + builder: (context, tileExtent, child) => SectionedListLayoutProvider( + collection: collection, + scrollableWidth: mqSize.width - mqHorizontalPadding, + tileExtent: tileExtent, + child: draggable, + ), + ); + final scaler = GridScaleGestureDetector( scrollableKey: _scrollableKey, extentNotifier: _tileExtentNotifier, mqSize: mqSize, - mqPadding: mqPadding, - child: ValueListenableBuilder( - valueListenable: _tileExtentNotifier, - builder: (context, tileExtent, child) { - debugPrint('$runtimeType tileExtent builder entries=${collection.entryCount} tileExtent=$tileExtent'); - final scrollView = CustomScrollView( - key: _scrollableKey, - primary: true, - // workaround to prevent scrolling the app bar away - // when there is no content and we use `SliverFillRemaining` - physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : null, - slivers: [ - CollectionAppBar( - stateNotifier: stateNotifier, - appBarHeightNotifier: _appBarHeightNotifier, - collection: collection, - ), - collection.isEmpty - ? SliverFillRemaining( - child: _buildEmptyCollectionPlaceholder(collection), - hasScrollBody: false, - ) - : CollectionListSliver( - collection: collection, - showHeader: showHeaders, - scrollableWidth: mqSize.width - mqPadding.horizontal, - tileExtent: tileExtent, - ), - SliverToBoxAdapter( - child: Selector( - selector: (c, mq) => mq.viewInsets.bottom, - builder: (c, mqViewInsetsBottom, child) { - return SizedBox(height: mqViewInsetsBottom); - }, - ), - ), - ], - ); - - return ValueListenableBuilder( - valueListenable: _appBarHeightNotifier, - builder: (context, appBarHeight, child) { - return DraggableScrollbar( - heightScrollThumb: avesScrollThumbHeight, - backgroundColor: Colors.white, - scrollThumbBuilder: avesScrollThumbBuilder( - height: avesScrollThumbHeight, - backgroundColor: Colors.white, - ), - controller: PrimaryScrollController.of(context), - padding: EdgeInsets.only( - // padding to keep scroll thumb between app bar above and nav bar below - top: appBarHeight, - bottom: mqViewInsetsBottom, - ), - child: child, - ); - }, - child: scrollView, - ); - }, - ), + mqHorizontalPadding: mqHorizontalPadding, + child: sectionedListLayoutProvider, ); + return scaler; }, ); }, @@ -115,6 +65,62 @@ class ThumbnailCollection extends StatelessWidget { ); } + ScrollView _buildScrollView(CollectionLens collection) { + return CustomScrollView( + key: _scrollableKey, + primary: true, + // workaround to prevent scrolling the app bar away + // when there is no content and we use `SliverFillRemaining` + physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : null, + slivers: [ + CollectionAppBar( + stateNotifier: stateNotifier, + appBarHeightNotifier: _appBarHeightNotifier, + collection: collection, + ), + collection.isEmpty + ? SliverFillRemaining( + child: _buildEmptyCollectionPlaceholder(collection), + hasScrollBody: false, + ) + : CollectionListSliver(), + SliverToBoxAdapter( + child: Selector( + selector: (context, mq) => mq.viewInsets.bottom, + builder: (context, mqViewInsetsBottom, child) { + return SizedBox(height: mqViewInsetsBottom); + }, + ), + ), + ], + ); + } + + Widget _buildDraggableScrollView(ScrollView scrollView) { + return ValueListenableBuilder( + valueListenable: _appBarHeightNotifier, + builder: (context, appBarHeight, child) => Selector( + selector: (context, mq) => mq.viewInsets.bottom, + builder: (context, mqViewInsetsBottom, child) => DraggableScrollbar( + heightScrollThumb: avesScrollThumbHeight, + backgroundColor: Colors.white, + scrollThumbBuilder: avesScrollThumbBuilder( + height: avesScrollThumbHeight, + backgroundColor: Colors.white, + ), + controller: PrimaryScrollController.of(context), + padding: EdgeInsets.only( + // padding to keep scroll thumb between app bar above and nav bar below + top: appBarHeight, + bottom: mqViewInsetsBottom, + ), + child: scrollView, + ), + child: child, + ), + ); + } + Widget _buildEmptyCollectionPlaceholder(CollectionLens collection) { return collection.filters.any((filter) => filter is FavouriteFilter) ? const EmptyContent( diff --git a/lib/widgets/album/tile_extent_manager.dart b/lib/widgets/album/tile_extent_manager.dart index ce10d2358..cc6fbefc8 100644 --- a/lib/widgets/album/tile_extent_manager.dart +++ b/lib/widgets/album/tile_extent_manager.dart @@ -8,8 +8,8 @@ class TileExtentManager { static const int columnCountDefault = 4; static const double tileExtentMin = 46.0; - static double applyTileExtent(Size mqSize, EdgeInsets mqPadding, ValueNotifier extentNotifier, {double newExtent}) { - final availableWidth = mqSize.width - mqPadding.horizontal; + static double applyTileExtent(Size mqSize, double mqHorizontalPadding, ValueNotifier extentNotifier, {double newExtent}) { + final availableWidth = mqSize.width - mqHorizontalPadding; var numColumns; if ((newExtent ?? 0) == 0) { newExtent = extentNotifier.value; @@ -20,7 +20,6 @@ class TileExtentManager { if ((newExtent ?? 0) == 0) { numColumns = columnCountDefault; } else { - debugPrint('TODO TLAD tileExtentMin=$tileExtentMin mqSize=$mqSize'); newExtent = newExtent.clamp(tileExtentMin, extentMaxForSize(mqSize)); numColumns = max(columnCountMin, (availableWidth / newExtent).round()); }