From 75143cf56b01864b042d19282d9c35cb7c88acb8 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 9 Apr 2020 18:03:16 +0900 Subject: [PATCH] collection: modify tile extent, not column count --- lib/model/settings.dart | 5 ++ lib/widgets/album/collection_scaling.dart | 77 ++++++++++++--------- lib/widgets/album/collection_section.dart | 28 +++----- lib/widgets/album/thumbnail_collection.dart | 28 +++++--- lib/widgets/album/tile_extent_manager.dart | 39 +++++++++++ lib/widgets/debug_page.dart | 16 +++-- 6 files changed, 130 insertions(+), 63 deletions(-) create mode 100644 lib/widgets/album/tile_extent_manager.dart diff --git a/lib/model/settings.dart b/lib/model/settings.dart index 524a78f57..c95f70559 100644 --- a/lib/model/settings.dart +++ b/lib/model/settings.dart @@ -17,6 +17,7 @@ class Settings { // preferences static const collectionGroupFactorKey = 'collection_group_factor'; static const collectionSortFactorKey = 'collection_sort_factor'; + static const collectionTileExtentKey = 'collection_tile_extent'; static const infoMapZoomKey = 'info_map_zoom'; static const catalogTimeZoneKey = 'catalog_time_zone'; @@ -64,6 +65,10 @@ class Settings { set collectionSortFactor(SortFactor newValue) => setAndNotify(collectionSortFactorKey, newValue.toString()); + double get collectionTileExtent => _prefs.getDouble(collectionTileExtentKey) ?? 0; + + set collectionTileExtent(double newValue) => setAndNotify(collectionTileExtentKey, newValue); + // convenience methods bool getBoolOrDefault(String key, bool defaultValue) => _prefs.getKeys().contains(key) ? _prefs.getBool(key) : defaultValue; diff --git a/lib/widgets/album/collection_scaling.dart b/lib/widgets/album/collection_scaling.dart index f1f47e839..770051598 100644 --- a/lib/widgets/album/collection_scaling.dart +++ b/lib/widgets/album/collection_scaling.dart @@ -4,19 +4,25 @@ import 'dart:ui' as ui; import 'package:aves/model/image_entry.dart'; import 'package:aves/widgets/album/collection_section.dart'; import 'package:aves/widgets/album/thumbnail.dart'; +import 'package:aves/widgets/album/tile_extent_manager.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:flutter_sticky_header/flutter_sticky_header.dart'; +import 'package:tuple/tuple.dart'; class GridScaleGestureDetector extends StatefulWidget { final GlobalKey scrollableKey; - final ValueNotifier columnCountNotifier; + final ValueNotifier extentNotifier; + final Size mqSize; + final EdgeInsets mqPadding; final Widget child; const GridScaleGestureDetector({ this.scrollableKey, - @required this.columnCountNotifier, + @required this.extentNotifier, + @required this.mqSize, + @required this.mqPadding, @required this.child, }); @@ -25,17 +31,21 @@ class GridScaleGestureDetector extends StatefulWidget { } class _GridScaleGestureDetectorState extends State { - int _start; - ValueNotifier _scaledCountNotifier; + Tuple2 _extentMinMax; + double _startExtent; + ValueNotifier _scaledExtentNotifier; OverlayEntry _overlayEntry; ThumbnailMetadata _metadata; RenderSliver _renderSliver; RenderViewport _renderViewport; - ValueNotifier get countNotifier => widget.columnCountNotifier; + ValueNotifier get tileExtentNotifier => widget.extentNotifier; - static const columnCountMin = 2.0; - static const columnCountMax = 8.0; + @override + void initState() { + super.initState(); + _extentMinMax = TileExtentManager.extentBoundsForSize(widget.mqSize); + } @override Widget build(BuildContext context) { @@ -54,11 +64,12 @@ class _GridScaleGestureDetectorState extends State { _renderSliver = firstOf(result) ?? firstOf(result); _renderViewport = firstOf(result); _metadata = renderMetaData.metaData; - _start = countNotifier.value; - _scaledCountNotifier = ValueNotifier(_start.toDouble()); + _startExtent = tileExtentNotifier.value; + _scaledExtentNotifier = ValueNotifier(_startExtent); + // not the same as `MediaQuery.size.width`, because of screen insets/padding final gridWidth = scrollableBox.size.width; - final halfExtent = gridWidth / _start / 2; + final halfExtent = _startExtent / 2; final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfExtent, halfExtent)); _overlayEntry = OverlayEntry( builder: (context) { @@ -66,30 +77,34 @@ class _GridScaleGestureDetectorState extends State { imageEntry: _metadata.entry, center: thumbnailCenter, gridWidth: gridWidth, - scaledCountNotifier: _scaledCountNotifier, + scaledExtentNotifier: _scaledExtentNotifier, ); }, ); Overlay.of(scrollableContext).insert(_overlayEntry); }, onScaleUpdate: (details) { - if (_scaledCountNotifier == null) return; + if (_scaledExtentNotifier == null) return; final s = details.scale; - _scaledCountNotifier.value = (_start / s).clamp(columnCountMin, columnCountMax); + _scaledExtentNotifier.value = (_startExtent * s).clamp(_extentMinMax.item1, _extentMinMax.item2); }, onScaleEnd: (details) { if (_overlayEntry != null) { _overlayEntry.remove(); _overlayEntry = null; } - if (_scaledCountNotifier == null) return; + if (_scaledExtentNotifier == null) return; - final newColumnCount = _scaledCountNotifier.value.round(); - _scaledCountNotifier = null; - if (newColumnCount == countNotifier.value) return; - - // update grid layout - countNotifier.value = newColumnCount; + final oldExtent = tileExtentNotifier.value; + // sanitize and update grid layout if necessary + final newExtent = TileExtentManager.applyTileExtent( + widget.mqSize, + widget.mqPadding, + tileExtentNotifier, + newExtent: _scaledExtentNotifier.value, + ); + _scaledExtentNotifier = null; + if (newExtent == oldExtent) return; // scroll to show the focal point thumbnail at its new position final sliverClosure = _renderSliver; @@ -98,7 +113,7 @@ class _GridScaleGestureDetectorState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { final scrollableContext = widget.scrollableKey.currentContext; final gridSize = (scrollableContext.findRenderObject() as RenderBox).size; - final newExtent = gridSize.width / newColumnCount; + final newColumnCount = gridSize.width / newExtent; final row = index ~/ newColumnCount; // `Scrollable.ensureVisible` only works on already rendered objects // `RenderViewport.showOnScreen` can find any `RenderSliver`, but not always a `RenderMetadata` @@ -115,13 +130,13 @@ class ScaleOverlay extends StatefulWidget { final ImageEntry imageEntry; final Offset center; final double gridWidth; - final ValueNotifier scaledCountNotifier; + final ValueNotifier scaledExtentNotifier; const ScaleOverlay({ @required this.imageEntry, @required this.center, @required this.gridWidth, - @required this.scaledCountNotifier, + @required this.scaledExtentNotifier, }); @override @@ -168,16 +183,16 @@ class _ScaleOverlayState extends State { ), duration: const Duration(milliseconds: 200), child: ValueListenableBuilder( - valueListenable: widget.scaledCountNotifier, - builder: (context, columnCount, child) { - final extent = gridWidth / columnCount; - + valueListenable: widget.scaledExtentNotifier, + builder: (context, extent, child) { // keep scaled thumbnail within the screen + final xMin = MediaQuery.of(context).padding.left; + final xMax = xMin + gridWidth; var dx = .0; - if (center.dx - extent / 2 < 0) { - dx = extent / 2 - center.dx; - } else if (center.dx + extent / 2 > gridWidth) { - dx = gridWidth - (center.dx + extent / 2); + if (center.dx - extent / 2 < xMin) { + dx = xMin - (center.dx - extent / 2); + } else if (center.dx + extent / 2 > xMax) { + dx = xMax - (center.dx + extent / 2); } final clampedCenter = center.translate(dx, 0); diff --git a/lib/widgets/album/collection_section.dart b/lib/widgets/album/collection_section.dart index 4a24bcd27..db079e153 100644 --- a/lib/widgets/album/collection_section.dart +++ b/lib/widgets/album/collection_section.dart @@ -8,19 +8,18 @@ import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/fullscreen_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; -import 'package:provider/provider.dart'; class SectionSliver extends StatelessWidget { final CollectionLens collection; final dynamic sectionKey; - final int columnCount; + final double tileExtent; final bool showHeader; const SectionSliver({ Key key, @required this.collection, @required this.sectionKey, - @required this.columnCount, + @required this.tileExtent, @required this.showHeader, }) : super(key: key); @@ -40,14 +39,14 @@ class SectionSliver extends StatelessWidget { collection: collection, index: index, entry: sectionEntries[index], - columnCount: columnCount, + tileExtent: tileExtent, ) : null, childCount: childCount, addAutomaticKeepAlives: false, ), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: columnCount, + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: tileExtent, ), ); @@ -69,7 +68,7 @@ class GridThumbnail extends StatelessWidget { final CollectionLens collection; final int index; final ImageEntry entry; - final int columnCount; + final double tileExtent; final GestureTapCallback onTap; const GridThumbnail({ @@ -77,7 +76,7 @@ class GridThumbnail extends StatelessWidget { this.collection, this.index, this.entry, - this.columnCount, + this.tileExtent, this.onTap, }) : super(key: key); @@ -88,15 +87,10 @@ class GridThumbnail extends StatelessWidget { onTap: () => _goToFullscreen(context), child: MetaData( metaData: ThumbnailMetadata(index, entry), - child: Selector( - selector: (c, mq) => mq.size.width, - builder: (c, mqWidth, child) { - return Thumbnail( - entry: entry, - extent: mqWidth / columnCount, - heroTag: collection.heroTag(entry), - ); - }, + child: Thumbnail( + entry: entry, + extent: tileExtent, + heroTag: collection.heroTag(entry), ), ), ); diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index c361133aa..bc88cb0e3 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -7,17 +7,19 @@ import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/album/collection_scaling.dart'; import 'package:aves/widgets/album/collection_section.dart'; import 'package:aves/widgets/album/empty.dart'; +import 'package:aves/widgets/album/tile_extent_manager.dart'; import 'package:aves/widgets/common/scroll_thumb.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; import 'package:outline_material_icons/outline_material_icons.dart'; import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; class ThumbnailCollection extends StatelessWidget { final ValueNotifier stateNotifier; final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); - final ValueNotifier _columnCountNotifier = ValueNotifier(4); + final ValueNotifier _tileExtentNotifier = ValueNotifier(0); final GlobalKey _scrollableKey = GlobalKey(); ThumbnailCollection({ @@ -28,9 +30,13 @@ class ThumbnailCollection extends StatelessWidget { @override Widget build(BuildContext context) { return SafeArea( - child: Selector( - selector: (c, mq) => mq.viewInsets.bottom, - builder: (c, mqViewInsetsBottom, child) { + child: Selector>( + selector: (c, mq) => Tuple3(mq.size, mq.padding, mq.viewInsets.bottom), + builder: (c, mq, child) { + final mqSize = mq.item1; + final mqPadding = mq.item2; + final mqViewInsetsBottom = mq.item3; + TileExtentManager.applyTileExtent(mqSize, mqPadding, _tileExtentNotifier); return Consumer( builder: (context, collection, child) { // debugPrint('$runtimeType collection builder entries=${collection.entryCount}'); @@ -38,11 +44,13 @@ class ThumbnailCollection extends StatelessWidget { final showHeaders = collection.showHeaders; return GridScaleGestureDetector( scrollableKey: _scrollableKey, - columnCountNotifier: _columnCountNotifier, - child: ValueListenableBuilder( - valueListenable: _columnCountNotifier, - builder: (context, columnCount, child) { - debugPrint('$runtimeType columnCount builder entries=${collection.entryCount} columnCount=$columnCount'); + 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, @@ -63,7 +71,7 @@ class ThumbnailCollection extends StatelessWidget { ...sectionKeys.map((sectionKey) => SectionSliver( collection: collection, sectionKey: sectionKey, - columnCount: columnCount, + tileExtent: tileExtent, showHeader: showHeaders, )), SliverToBoxAdapter( diff --git a/lib/widgets/album/tile_extent_manager.dart b/lib/widgets/album/tile_extent_manager.dart new file mode 100644 index 000000000..945d550fc --- /dev/null +++ b/lib/widgets/album/tile_extent_manager.dart @@ -0,0 +1,39 @@ +import 'package:aves/model/settings.dart'; +import 'package:flutter/widgets.dart'; +import 'package:tuple/tuple.dart'; + +class TileExtentManager { + static const columnCountMin = 2; + static const columnCountDefault = 4; + static const columnCountMax = 8; + + static double applyTileExtent(Size mqSize, EdgeInsets mqPadding, ValueNotifier extentNotifier, {double newExtent}) { + final availableWidth = mqSize.width - mqPadding.horizontal; + var numColumns; + if ((newExtent ?? 0) == 0) { + newExtent = extentNotifier.value; + } + if ((newExtent ?? 0) == 0) { + newExtent = settings.collectionTileExtent; + } + if ((newExtent ?? 0) == 0) { + numColumns = columnCountDefault; + } else { + final minMax = extentBoundsForSize(mqSize); + newExtent = newExtent.clamp(minMax.item1, minMax.item2); + numColumns = (availableWidth / newExtent).round().clamp(columnCountMin, columnCountMax); + } + newExtent = availableWidth / numColumns; + if (extentNotifier.value != newExtent) { + settings.collectionTileExtent = newExtent; + extentNotifier.value = newExtent; + } + return newExtent; + } + + static Tuple2 extentBoundsForSize(Size mqSize) { + final min = mqSize.shortestSide / columnCountMax; + final max = mqSize.shortestSide / columnCountMin; + return Tuple2(min, max); + } +} diff --git a/lib/widgets/debug_page.dart b/lib/widgets/debug_page.dart index a0dd00ca6..050de997d 100644 --- a/lib/widgets/debug_page.dart +++ b/lib/widgets/debug_page.dart @@ -48,13 +48,19 @@ class DebugPageState extends State { child: ListView( padding: const EdgeInsets.all(8), children: [ - const Text('Settings'), - RaisedButton( - onPressed: () => settings.reset().then((_) => setState(() {})), - child: const Text('Reset settings'), + Row( + children: [ + const Text('Settings'), + const Spacer(), + RaisedButton( + onPressed: () => settings.reset().then((_) => setState(() {})), + child: const Text('Reset'), + ), + ], ), Text('collectionGroupFactor: ${settings.collectionGroupFactor}'), Text('collectionSortFactor: ${settings.collectionSortFactor}'), + Text('collectionTileExtent: ${settings.collectionTileExtent}'), Text('infoMapZoom: ${settings.infoMapZoom}'), const Divider(), Text('Entries: ${entries.length}'), @@ -73,7 +79,7 @@ class DebugPageState extends State { const Spacer(), RaisedButton( onPressed: () => metadataDb.reset().then((_) => _startDbReport()), - child: const Text('Reset DB'), + child: const Text('Reset'), ), ], );