show entry in collection when leaving viewer

This commit is contained in:
Thibault Deckers 2021-06-10 17:30:03 +09:00
parent 7ac6617154
commit e9de80f887
3 changed files with 45 additions and 17 deletions

View file

@ -6,17 +6,22 @@ class HighlightInfo extends ChangeNotifier {
final EventBus eventBus = EventBus(); final EventBus eventBus = EventBus();
void trackItem<T>( void trackItem<T>(
T item, { T? item, {
TrackPredicate? predicate,
Alignment? alignment, Alignment? alignment,
bool? animate, bool? animate,
Object? highlightItem, Object? highlightItem,
}) => }) {
if (item != null) {
eventBus.fire(TrackEvent<T>( eventBus.fire(TrackEvent<T>(
item, item,
predicate ?? (_) => true,
alignment ?? Alignment.center, alignment ?? Alignment.center,
animate ?? true, animate ?? true,
highlightItem, highlightItem,
)); ));
}
}
Object? _item; Object? _item;
@ -43,14 +48,20 @@ class HighlightInfo extends ChangeNotifier {
@immutable @immutable
class TrackEvent<T> { class TrackEvent<T> {
final T item; final T item;
final TrackPredicate predicate;
final Alignment alignment; final Alignment alignment;
final bool animate; final bool animate;
final Object? highlightItem; final Object? highlightItem;
const TrackEvent( const TrackEvent(
this.item, this.item,
this.predicate,
this.alignment, this.alignment,
this.animate, this.animate,
this.highlightItem, this.highlightItem,
); );
} }
// `itemVisibility`: percent of the item tracked already visible in viewport
// return whether to proceed with tracking
typedef TrackPredicate = bool Function(double itemVisibility);

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
@ -36,12 +37,7 @@ mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U>, WidgetsBind
void initState() { void initState() {
super.initState(); super.initState();
final highlightInfo = context.read<HighlightInfo>(); final highlightInfo = context.read<HighlightInfo>();
_subscriptions.add(highlightInfo.eventBus.on<TrackEvent<T>>().listen((e) => _trackItem( _subscriptions.add(highlightInfo.eventBus.on<TrackEvent<T>>().listen(_trackItem));
e.item,
alignment: e.alignment,
animate: e.animate,
highlightItem: e.highlightItem,
)));
_lastOrientation = _windowOrientation; _lastOrientation = _windowOrientation;
WidgetsBinding.instance!.addObserver(this); WidgetsBinding.instance!.addObserver(this);
_saveLayoutMetrics(); _saveLayoutMetrics();
@ -66,22 +62,21 @@ mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U>, WidgetsBind
// `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`
// `RenderViewport.scrollOffsetOf` is a good alternative // `RenderViewport.scrollOffsetOf` is a good alternative
Future<void> _trackItem( Future<void> _trackItem(TrackEvent event) async {
T item, {
required Alignment alignment,
required bool animate,
required Object? highlightItem,
}) async {
final sectionedListLayout = context.read<SectionedListLayout<T>>(); final sectionedListLayout = context.read<SectionedListLayout<T>>();
final tileRect = sectionedListLayout.getTileRect(item); final tileRect = sectionedListLayout.getTileRect(event.item);
if (tileRect == null) return; if (tileRect == null) return;
final viewportRect = Rect.fromLTWH(0, scrollController.offset, scrollableSize.width, scrollableSize.height);
final itemVisibility = max(0, tileRect.intersect(viewportRect).height) / tileRect.height;
if (!event.predicate(itemVisibility)) return;
// most of the time the app bar will be scrolled away after scaling, // most of the time the app bar will be scrolled away after scaling,
// so we compensate for it to center the focal point thumbnail // so we compensate for it to center the focal point thumbnail
final appBarHeight = appBarHeightNotifier.value; final appBarHeight = appBarHeightNotifier.value;
final scrollOffset = appBarHeight + tileRect.top + (tileRect.height - scrollableSize.height) * ((alignment.y + 1) / 2); final scrollOffset = appBarHeight + tileRect.top + (tileRect.height - scrollableSize.height) * ((event.alignment.y + 1) / 2);
if (animate) { if (event.animate) {
if (scrollOffset > 0) { if (scrollOffset > 0) {
await scrollController.animateTo( await scrollController.animateTo(
scrollOffset, scrollOffset,
@ -95,6 +90,7 @@ mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U>, WidgetsBind
await Future.delayed(Durations.highlightJumpDelay); await Future.delayed(Durations.highlightJumpDelay);
} }
final highlightItem = event.highlightItem;
if (highlightItem != null) { if (highlightItem != null) {
context.read<HighlightInfo>().set(highlightItem); context.read<HighlightInfo>().set(highlightItem);
} }

View file

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/highlight.dart';
import 'package:aves/model/multipage.dart'; import 'package:aves/model/multipage.dart';
import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
@ -62,6 +63,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
late EntryActionDelegate _actionDelegate; late EntryActionDelegate _actionDelegate;
final List<Tuple2<String, ValueNotifier<ViewState>>> _viewStateNotifiers = []; final List<Tuple2<String, ValueNotifier<ViewState>>> _viewStateNotifiers = [];
final ValueNotifier<HeroInfo?> _heroInfoNotifier = ValueNotifier(null); final ValueNotifier<HeroInfo?> _heroInfoNotifier = ValueNotifier(null);
bool _isEntryTracked = true;
CollectionLens? get collection => widget.collection; CollectionLens? get collection => widget.collection;
@ -166,6 +168,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
// back from info to image // back from info to image
_goToVerticalPage(imagePage); _goToVerticalPage(imagePage);
} else { } else {
if (!_isEntryTracked) _trackEntry();
_popVisual(); _popVisual();
} }
return SynchronousFuture(false); return SynchronousFuture(false);
@ -370,6 +373,9 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
} }
void _onVerticalPageControllerChange() { void _onVerticalPageControllerChange() {
if (!_isEntryTracked && _verticalPager.page?.floor() == transitionPage) {
_trackEntry();
}
_verticalScrollNotifier.notifyListeners(); _verticalScrollNotifier.notifyListeners();
} }
@ -451,6 +457,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
final newEntry = _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null; final newEntry = _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null;
if (_entryNotifier.value == newEntry) return; if (_entryNotifier.value == newEntry) return;
_entryNotifier.value = newEntry; _entryNotifier.value = newEntry;
_isEntryTracked = false;
await _pauseVideoControllers(); await _pauseVideoControllers();
await _initEntryControllers(); await _initEntryControllers();
} }
@ -478,6 +485,20 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
} }
} }
// track item when returning to collection,
// if they are not fully visible already
void _trackEntry() {
_isEntryTracked = true;
final entry = _entryNotifier.value;
if (entry != null && hasCollection) {
context.read<HighlightInfo>().trackItem(
entry,
predicate: (v) => v < 1,
animate: false,
);
}
}
void _onLeave() { void _onLeave() {
_showSystemUI(); _showSystemUI();
if (settings.keepScreenOn == KeepScreenOn.viewerOnly) { if (settings.keepScreenOn == KeepScreenOn.viewerOnly) {