collection: modify tile extent, not column count

This commit is contained in:
Thibault Deckers 2020-04-09 18:03:16 +09:00
parent 2b2e7e31bd
commit 75143cf56b
6 changed files with 130 additions and 63 deletions

View file

@ -17,6 +17,7 @@ class Settings {
// preferences // preferences
static const collectionGroupFactorKey = 'collection_group_factor'; static const collectionGroupFactorKey = 'collection_group_factor';
static const collectionSortFactorKey = 'collection_sort_factor'; static const collectionSortFactorKey = 'collection_sort_factor';
static const collectionTileExtentKey = 'collection_tile_extent';
static const infoMapZoomKey = 'info_map_zoom'; static const infoMapZoomKey = 'info_map_zoom';
static const catalogTimeZoneKey = 'catalog_time_zone'; static const catalogTimeZoneKey = 'catalog_time_zone';
@ -64,6 +65,10 @@ class Settings {
set collectionSortFactor(SortFactor newValue) => setAndNotify(collectionSortFactorKey, newValue.toString()); set collectionSortFactor(SortFactor newValue) => setAndNotify(collectionSortFactorKey, newValue.toString());
double get collectionTileExtent => _prefs.getDouble(collectionTileExtentKey) ?? 0;
set collectionTileExtent(double newValue) => setAndNotify(collectionTileExtentKey, newValue);
// convenience methods // convenience methods
bool getBoolOrDefault(String key, bool defaultValue) => _prefs.getKeys().contains(key) ? _prefs.getBool(key) : defaultValue; bool getBoolOrDefault(String key, bool defaultValue) => _prefs.getKeys().contains(key) ? _prefs.getBool(key) : defaultValue;

View file

@ -4,19 +4,25 @@ import 'dart:ui' as ui;
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/collection_section.dart'; import 'package:aves/widgets/album/collection_section.dart';
import 'package:aves/widgets/album/thumbnail.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:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:tuple/tuple.dart';
class GridScaleGestureDetector extends StatefulWidget { class GridScaleGestureDetector extends StatefulWidget {
final GlobalKey scrollableKey; final GlobalKey scrollableKey;
final ValueNotifier<int> columnCountNotifier; final ValueNotifier<double> extentNotifier;
final Size mqSize;
final EdgeInsets mqPadding;
final Widget child; final Widget child;
const GridScaleGestureDetector({ const GridScaleGestureDetector({
this.scrollableKey, this.scrollableKey,
@required this.columnCountNotifier, @required this.extentNotifier,
@required this.mqSize,
@required this.mqPadding,
@required this.child, @required this.child,
}); });
@ -25,17 +31,21 @@ class GridScaleGestureDetector extends StatefulWidget {
} }
class _GridScaleGestureDetectorState extends State<GridScaleGestureDetector> { class _GridScaleGestureDetectorState extends State<GridScaleGestureDetector> {
int _start; Tuple2<double, double> _extentMinMax;
ValueNotifier<double> _scaledCountNotifier; double _startExtent;
ValueNotifier<double> _scaledExtentNotifier;
OverlayEntry _overlayEntry; OverlayEntry _overlayEntry;
ThumbnailMetadata _metadata; ThumbnailMetadata _metadata;
RenderSliver _renderSliver; RenderSliver _renderSliver;
RenderViewport _renderViewport; RenderViewport _renderViewport;
ValueNotifier<int> get countNotifier => widget.columnCountNotifier; ValueNotifier<double> get tileExtentNotifier => widget.extentNotifier;
static const columnCountMin = 2.0; @override
static const columnCountMax = 8.0; void initState() {
super.initState();
_extentMinMax = TileExtentManager.extentBoundsForSize(widget.mqSize);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -54,11 +64,12 @@ class _GridScaleGestureDetectorState extends State<GridScaleGestureDetector> {
_renderSliver = firstOf<RenderSliverStickyHeader>(result) ?? firstOf<RenderSliverGrid>(result); _renderSliver = firstOf<RenderSliverStickyHeader>(result) ?? firstOf<RenderSliverGrid>(result);
_renderViewport = firstOf<RenderViewport>(result); _renderViewport = firstOf<RenderViewport>(result);
_metadata = renderMetaData.metaData; _metadata = renderMetaData.metaData;
_start = countNotifier.value; _startExtent = tileExtentNotifier.value;
_scaledCountNotifier = ValueNotifier(_start.toDouble()); _scaledExtentNotifier = ValueNotifier(_startExtent);
// not the same as `MediaQuery.size.width`, because of screen insets/padding
final gridWidth = scrollableBox.size.width; final gridWidth = scrollableBox.size.width;
final halfExtent = gridWidth / _start / 2; final halfExtent = _startExtent / 2;
final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfExtent, halfExtent)); final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfExtent, halfExtent));
_overlayEntry = OverlayEntry( _overlayEntry = OverlayEntry(
builder: (context) { builder: (context) {
@ -66,30 +77,34 @@ class _GridScaleGestureDetectorState extends State<GridScaleGestureDetector> {
imageEntry: _metadata.entry, imageEntry: _metadata.entry,
center: thumbnailCenter, center: thumbnailCenter,
gridWidth: gridWidth, gridWidth: gridWidth,
scaledCountNotifier: _scaledCountNotifier, scaledExtentNotifier: _scaledExtentNotifier,
); );
}, },
); );
Overlay.of(scrollableContext).insert(_overlayEntry); Overlay.of(scrollableContext).insert(_overlayEntry);
}, },
onScaleUpdate: (details) { onScaleUpdate: (details) {
if (_scaledCountNotifier == null) return; if (_scaledExtentNotifier == null) return;
final s = details.scale; final s = details.scale;
_scaledCountNotifier.value = (_start / s).clamp(columnCountMin, columnCountMax); _scaledExtentNotifier.value = (_startExtent * s).clamp(_extentMinMax.item1, _extentMinMax.item2);
}, },
onScaleEnd: (details) { onScaleEnd: (details) {
if (_overlayEntry != null) { if (_overlayEntry != null) {
_overlayEntry.remove(); _overlayEntry.remove();
_overlayEntry = null; _overlayEntry = null;
} }
if (_scaledCountNotifier == null) return; if (_scaledExtentNotifier == null) return;
final newColumnCount = _scaledCountNotifier.value.round(); final oldExtent = tileExtentNotifier.value;
_scaledCountNotifier = null; // sanitize and update grid layout if necessary
if (newColumnCount == countNotifier.value) return; final newExtent = TileExtentManager.applyTileExtent(
widget.mqSize,
// update grid layout widget.mqPadding,
countNotifier.value = newColumnCount; tileExtentNotifier,
newExtent: _scaledExtentNotifier.value,
);
_scaledExtentNotifier = null;
if (newExtent == oldExtent) return;
// scroll to show the focal point thumbnail at its new position // scroll to show the focal point thumbnail at its new position
final sliverClosure = _renderSliver; final sliverClosure = _renderSliver;
@ -98,7 +113,7 @@ class _GridScaleGestureDetectorState extends State<GridScaleGestureDetector> {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final scrollableContext = widget.scrollableKey.currentContext; final scrollableContext = widget.scrollableKey.currentContext;
final gridSize = (scrollableContext.findRenderObject() as RenderBox).size; final gridSize = (scrollableContext.findRenderObject() as RenderBox).size;
final newExtent = gridSize.width / newColumnCount; final newColumnCount = gridSize.width / newExtent;
final row = index ~/ newColumnCount; final row = index ~/ newColumnCount;
// `Scrollable.ensureVisible` only works on already rendered objects // `Scrollable.ensureVisible` only works on already rendered objects
// `RenderViewport.showOnScreen` can find any `RenderSliver`, but not always a `RenderMetadata` // `RenderViewport.showOnScreen` can find any `RenderSliver`, but not always a `RenderMetadata`
@ -115,13 +130,13 @@ class ScaleOverlay extends StatefulWidget {
final ImageEntry imageEntry; final ImageEntry imageEntry;
final Offset center; final Offset center;
final double gridWidth; final double gridWidth;
final ValueNotifier<double> scaledCountNotifier; final ValueNotifier<double> scaledExtentNotifier;
const ScaleOverlay({ const ScaleOverlay({
@required this.imageEntry, @required this.imageEntry,
@required this.center, @required this.center,
@required this.gridWidth, @required this.gridWidth,
@required this.scaledCountNotifier, @required this.scaledExtentNotifier,
}); });
@override @override
@ -168,16 +183,16 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
), ),
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
child: ValueListenableBuilder<double>( child: ValueListenableBuilder<double>(
valueListenable: widget.scaledCountNotifier, valueListenable: widget.scaledExtentNotifier,
builder: (context, columnCount, child) { builder: (context, extent, child) {
final extent = gridWidth / columnCount;
// keep scaled thumbnail within the screen // keep scaled thumbnail within the screen
final xMin = MediaQuery.of(context).padding.left;
final xMax = xMin + gridWidth;
var dx = .0; var dx = .0;
if (center.dx - extent / 2 < 0) { if (center.dx - extent / 2 < xMin) {
dx = extent / 2 - center.dx; dx = xMin - (center.dx - extent / 2);
} else if (center.dx + extent / 2 > gridWidth) { } else if (center.dx + extent / 2 > xMax) {
dx = gridWidth - (center.dx + extent / 2); dx = xMax - (center.dx + extent / 2);
} }
final clampedCenter = center.translate(dx, 0); final clampedCenter = center.translate(dx, 0);

View file

@ -8,19 +8,18 @@ import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/fullscreen/fullscreen_page.dart'; import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:provider/provider.dart';
class SectionSliver extends StatelessWidget { class SectionSliver extends StatelessWidget {
final CollectionLens collection; final CollectionLens collection;
final dynamic sectionKey; final dynamic sectionKey;
final int columnCount; final double tileExtent;
final bool showHeader; final bool showHeader;
const SectionSliver({ const SectionSliver({
Key key, Key key,
@required this.collection, @required this.collection,
@required this.sectionKey, @required this.sectionKey,
@required this.columnCount, @required this.tileExtent,
@required this.showHeader, @required this.showHeader,
}) : super(key: key); }) : super(key: key);
@ -40,14 +39,14 @@ class SectionSliver extends StatelessWidget {
collection: collection, collection: collection,
index: index, index: index,
entry: sectionEntries[index], entry: sectionEntries[index],
columnCount: columnCount, tileExtent: tileExtent,
) )
: null, : null,
childCount: childCount, childCount: childCount,
addAutomaticKeepAlives: false, addAutomaticKeepAlives: false,
), ),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
crossAxisCount: columnCount, maxCrossAxisExtent: tileExtent,
), ),
); );
@ -69,7 +68,7 @@ class GridThumbnail extends StatelessWidget {
final CollectionLens collection; final CollectionLens collection;
final int index; final int index;
final ImageEntry entry; final ImageEntry entry;
final int columnCount; final double tileExtent;
final GestureTapCallback onTap; final GestureTapCallback onTap;
const GridThumbnail({ const GridThumbnail({
@ -77,7 +76,7 @@ class GridThumbnail extends StatelessWidget {
this.collection, this.collection,
this.index, this.index,
this.entry, this.entry,
this.columnCount, this.tileExtent,
this.onTap, this.onTap,
}) : super(key: key); }) : super(key: key);
@ -88,15 +87,10 @@ class GridThumbnail extends StatelessWidget {
onTap: () => _goToFullscreen(context), onTap: () => _goToFullscreen(context),
child: MetaData( child: MetaData(
metaData: ThumbnailMetadata(index, entry), metaData: ThumbnailMetadata(index, entry),
child: Selector<MediaQueryData, double>( child: Thumbnail(
selector: (c, mq) => mq.size.width, entry: entry,
builder: (c, mqWidth, child) { extent: tileExtent,
return Thumbnail( heroTag: collection.heroTag(entry),
entry: entry,
extent: mqWidth / columnCount,
heroTag: collection.heroTag(entry),
);
},
), ),
), ),
); );

View file

@ -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_scaling.dart';
import 'package:aves/widgets/album/collection_section.dart'; import 'package:aves/widgets/album/collection_section.dart';
import 'package:aves/widgets/album/empty.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:aves/widgets/common/scroll_thumb.dart';
import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:outline_material_icons/outline_material_icons.dart'; import 'package:outline_material_icons/outline_material_icons.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
class ThumbnailCollection extends StatelessWidget { class ThumbnailCollection extends StatelessWidget {
final ValueNotifier<PageState> stateNotifier; final ValueNotifier<PageState> stateNotifier;
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0); final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
final ValueNotifier<int> _columnCountNotifier = ValueNotifier(4); final ValueNotifier<double> _tileExtentNotifier = ValueNotifier(0);
final GlobalKey _scrollableKey = GlobalKey(); final GlobalKey _scrollableKey = GlobalKey();
ThumbnailCollection({ ThumbnailCollection({
@ -28,9 +30,13 @@ class ThumbnailCollection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return SafeArea(
child: Selector<MediaQueryData, double>( child: Selector<MediaQueryData, Tuple3<Size, EdgeInsets, double>>(
selector: (c, mq) => mq.viewInsets.bottom, selector: (c, mq) => Tuple3(mq.size, mq.padding, mq.viewInsets.bottom),
builder: (c, mqViewInsetsBottom, child) { builder: (c, mq, child) {
final mqSize = mq.item1;
final mqPadding = mq.item2;
final mqViewInsetsBottom = mq.item3;
TileExtentManager.applyTileExtent(mqSize, mqPadding, _tileExtentNotifier);
return Consumer<CollectionLens>( return Consumer<CollectionLens>(
builder: (context, collection, child) { builder: (context, collection, child) {
// debugPrint('$runtimeType collection builder entries=${collection.entryCount}'); // debugPrint('$runtimeType collection builder entries=${collection.entryCount}');
@ -38,11 +44,13 @@ class ThumbnailCollection extends StatelessWidget {
final showHeaders = collection.showHeaders; final showHeaders = collection.showHeaders;
return GridScaleGestureDetector( return GridScaleGestureDetector(
scrollableKey: _scrollableKey, scrollableKey: _scrollableKey,
columnCountNotifier: _columnCountNotifier, extentNotifier: _tileExtentNotifier,
child: ValueListenableBuilder<int>( mqSize: mqSize,
valueListenable: _columnCountNotifier, mqPadding: mqPadding,
builder: (context, columnCount, child) { child: ValueListenableBuilder<double>(
debugPrint('$runtimeType columnCount builder entries=${collection.entryCount} columnCount=$columnCount'); valueListenable: _tileExtentNotifier,
builder: (context, tileExtent, child) {
debugPrint('$runtimeType tileExtent builder entries=${collection.entryCount} tileExtent=$tileExtent');
final scrollView = CustomScrollView( final scrollView = CustomScrollView(
key: _scrollableKey, key: _scrollableKey,
primary: true, primary: true,
@ -63,7 +71,7 @@ class ThumbnailCollection extends StatelessWidget {
...sectionKeys.map((sectionKey) => SectionSliver( ...sectionKeys.map((sectionKey) => SectionSliver(
collection: collection, collection: collection,
sectionKey: sectionKey, sectionKey: sectionKey,
columnCount: columnCount, tileExtent: tileExtent,
showHeader: showHeaders, showHeader: showHeaders,
)), )),
SliverToBoxAdapter( SliverToBoxAdapter(

View file

@ -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<double> 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<double, double> extentBoundsForSize(Size mqSize) {
final min = mqSize.shortestSide / columnCountMax;
final max = mqSize.shortestSide / columnCountMin;
return Tuple2(min, max);
}
}

View file

@ -48,13 +48,19 @@ class DebugPageState extends State<DebugPage> {
child: ListView( child: ListView(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
children: [ children: [
const Text('Settings'), Row(
RaisedButton( children: [
onPressed: () => settings.reset().then((_) => setState(() {})), const Text('Settings'),
child: const Text('Reset settings'), const Spacer(),
RaisedButton(
onPressed: () => settings.reset().then((_) => setState(() {})),
child: const Text('Reset'),
),
],
), ),
Text('collectionGroupFactor: ${settings.collectionGroupFactor}'), Text('collectionGroupFactor: ${settings.collectionGroupFactor}'),
Text('collectionSortFactor: ${settings.collectionSortFactor}'), Text('collectionSortFactor: ${settings.collectionSortFactor}'),
Text('collectionTileExtent: ${settings.collectionTileExtent}'),
Text('infoMapZoom: ${settings.infoMapZoom}'), Text('infoMapZoom: ${settings.infoMapZoom}'),
const Divider(), const Divider(),
Text('Entries: ${entries.length}'), Text('Entries: ${entries.length}'),
@ -73,7 +79,7 @@ class DebugPageState extends State<DebugPage> {
const Spacer(), const Spacer(),
RaisedButton( RaisedButton(
onPressed: () => metadataDb.reset().then((_) => _startDbReport()), onPressed: () => metadataDb.reset().then((_) => _startDbReport()),
child: const Text('Reset DB'), child: const Text('Reset'),
), ),
], ],
); );