311 lines
8.3 KiB
Dart
311 lines
8.3 KiB
Dart
import 'dart:async';
|
|
import 'dart:math';
|
|
import 'dart:ui' show FlutterView;
|
|
|
|
import 'package:aves/model/highlight.dart';
|
|
import 'package:aves/theme/durations.dart';
|
|
import 'package:aves/widgets/common/grid/sections/list_layout.dart';
|
|
import 'package:aves_model/aves_model.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
class GridItemTracker<T> extends StatefulWidget {
|
|
final GlobalKey scrollableKey;
|
|
final ValueNotifier<double> appBarHeightNotifier;
|
|
final TileLayout tileLayout;
|
|
final ScrollController scrollController;
|
|
final Widget child;
|
|
|
|
const GridItemTracker({
|
|
super.key,
|
|
required this.scrollableKey,
|
|
required this.appBarHeightNotifier,
|
|
required this.tileLayout,
|
|
required this.scrollController,
|
|
required this.child,
|
|
});
|
|
|
|
@override
|
|
State<GridItemTracker<T>> createState() => _GridItemTrackerState<T>();
|
|
}
|
|
|
|
class _GridItemTrackerState<T> extends State<GridItemTracker<T>> with WidgetsBindingObserver {
|
|
ValueNotifier<double> get appBarHeightNotifier => widget.appBarHeightNotifier;
|
|
|
|
ScrollController get scrollController => widget.scrollController;
|
|
|
|
final Set<StreamSubscription> _subscriptions = {};
|
|
|
|
FlutterView? _view;
|
|
ModalRoute<dynamic>? _route;
|
|
HighlightInfo? _highlightInfo;
|
|
SectionedListLayout<T>? _sectionedListLayout;
|
|
|
|
bool _disposed = false;
|
|
bool _hasSavedLayoutMetrics = false;
|
|
|
|
late SectionedListLayout<T> _lastSectionedListLayout;
|
|
late Size _lastScrollableSize;
|
|
Orientation _lastOrientation = Orientation.portrait;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return widget.child;
|
|
}
|
|
|
|
Size? get scrollableSize {
|
|
final scrollableContext = widget.scrollableKey.currentContext;
|
|
if (scrollableContext == null) return null;
|
|
|
|
try {
|
|
final renderObject = scrollableContext.findRenderObject();
|
|
if (renderObject is! RenderBox) return null;
|
|
if (!renderObject.hasSize) return null;
|
|
|
|
return renderObject.size;
|
|
} catch (e, st) {
|
|
debugPrint('GridItemTracker scrollableSize ignored: $e');
|
|
debugPrint('$st');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
bool get _canUseTracker {
|
|
if (_disposed || !mounted) return false;
|
|
|
|
final route = _route;
|
|
if (route != null && !route.isCurrent) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
|
|
_view = View.maybeOf(context);
|
|
_route = ModalRoute.of(context);
|
|
|
|
try {
|
|
final highlightInfo = Provider.of<HighlightInfo>(
|
|
context,
|
|
listen: false,
|
|
);
|
|
|
|
if (!identical(_highlightInfo, highlightInfo)) {
|
|
_subscriptions
|
|
..forEach((sub) => sub.cancel())
|
|
..clear();
|
|
|
|
_highlightInfo = highlightInfo;
|
|
_subscriptions.add(
|
|
highlightInfo.eventBus.on<TrackEvent<T>>().listen(_trackItem),
|
|
);
|
|
}
|
|
} catch (e, st) {
|
|
debugPrint('GridItemTracker HighlightInfo lookup ignored: $e');
|
|
debugPrint('$st');
|
|
}
|
|
|
|
try {
|
|
_sectionedListLayout = Provider.of<SectionedListLayout<T>>(
|
|
context,
|
|
listen: false,
|
|
);
|
|
} catch (e, st) {
|
|
debugPrint('GridItemTracker SectionedListLayout lookup ignored: $e');
|
|
debugPrint('$st');
|
|
}
|
|
|
|
_saveLayoutMetrics();
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant GridItemTracker<T> oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
|
|
if (!_canUseTracker) return;
|
|
|
|
if (oldWidget.tileLayout != widget.tileLayout) {
|
|
_onLayoutChanged();
|
|
}
|
|
|
|
_saveLayoutMetrics();
|
|
}
|
|
|
|
@override
|
|
void didChangeMetrics() {
|
|
if (!_canUseTracker) return;
|
|
|
|
final view = _view;
|
|
if (view == null) return;
|
|
|
|
final size = view.physicalSize;
|
|
final orientation = size.width > size.height ? Orientation.landscape : Orientation.portrait;
|
|
|
|
if (_lastOrientation == orientation) return;
|
|
|
|
_lastOrientation = orientation;
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!_canUseTracker) return;
|
|
if (widget.scrollableKey.currentContext == null) return;
|
|
|
|
try {
|
|
_onLayoutChanged();
|
|
_saveLayoutMetrics();
|
|
} catch (e, st) {
|
|
debugPrint('GridItemTracker didChangeMetrics ignored: $e');
|
|
debugPrint('$st');
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_disposed = true;
|
|
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
|
|
_view = null;
|
|
_route = null;
|
|
_highlightInfo = null;
|
|
_sectionedListLayout = null;
|
|
|
|
_subscriptions
|
|
..forEach((sub) => sub.cancel())
|
|
..clear();
|
|
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _trackItem(TrackEvent event) async {
|
|
if (!_canUseTracker) return;
|
|
|
|
final sectionedListLayout = _sectionedListLayout;
|
|
if (sectionedListLayout == null) return;
|
|
|
|
try {
|
|
final tileRect = sectionedListLayout.getTileRect(event.item);
|
|
if (tileRect == null) return;
|
|
|
|
final currentScrollableSize = scrollableSize;
|
|
if (currentScrollableSize == null) return;
|
|
|
|
if (scrollController.positions.length != 1) return;
|
|
|
|
final viewportRect = Offset(0, scrollController.offset) & currentScrollableSize;
|
|
|
|
final itemVisibility = max(0, tileRect.intersect(viewportRect).height) / tileRect.height;
|
|
|
|
if (!event.predicate(itemVisibility)) return;
|
|
|
|
double scrollOffset = tileRect.top + (tileRect.height - currentScrollableSize.height) * ((event.alignment.y + 1) / 2);
|
|
|
|
scrollOffset += appBarHeightNotifier.value;
|
|
scrollOffset = scrollOffset.clamp(0, scrollController.position.maxScrollExtent);
|
|
|
|
if (scrollOffset > 0) {
|
|
if (event.animate) {
|
|
await scrollController.animateTo(
|
|
scrollOffset,
|
|
duration: Duration(
|
|
milliseconds: (scrollOffset / 2).round().clamp(
|
|
ADurations.highlightScrollAnimationMinMillis,
|
|
ADurations.highlightScrollAnimationMaxMillis,
|
|
),
|
|
),
|
|
curve: Curves.easeInOutCubic,
|
|
);
|
|
} else {
|
|
scrollController.jumpTo(scrollOffset);
|
|
await Future.delayed(ADurations.highlightJumpDelay);
|
|
}
|
|
}
|
|
|
|
if (!_canUseTracker) return;
|
|
|
|
final highlightItem = event.highlightItem;
|
|
if (highlightItem != null) {
|
|
_highlightInfo?.set(highlightItem);
|
|
}
|
|
} catch (e, st) {
|
|
debugPrint('GridItemTracker _trackItem ignored: $e');
|
|
debugPrint('$st');
|
|
}
|
|
}
|
|
|
|
Future<void> _saveLayoutMetrics() async {
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
|
|
if (!_canUseTracker) return;
|
|
if (widget.scrollableKey.currentContext == null) return;
|
|
|
|
final currentScrollableSize = scrollableSize;
|
|
if (currentScrollableSize == null) return;
|
|
|
|
final sectionedListLayout = _sectionedListLayout;
|
|
if (sectionedListLayout == null) return;
|
|
|
|
try {
|
|
_lastSectionedListLayout = sectionedListLayout;
|
|
_lastScrollableSize = currentScrollableSize;
|
|
_hasSavedLayoutMetrics = true;
|
|
} catch (e, st) {
|
|
debugPrint('GridItemTracker _saveLayoutMetrics ignored: $e');
|
|
debugPrint('$st');
|
|
}
|
|
}
|
|
|
|
void _onLayoutChanged() {
|
|
if (!_canUseTracker) return;
|
|
if (!_hasSavedLayoutMetrics) return;
|
|
if (scrollController.positions.length != 1) return;
|
|
|
|
if (scrollController.offset == 0) return;
|
|
|
|
try {
|
|
final layout = _lastSectionedListLayout;
|
|
final halfSize = _lastScrollableSize / 2;
|
|
final center = Offset(
|
|
halfSize.width,
|
|
halfSize.height + scrollController.offset - appBarHeightNotifier.value,
|
|
);
|
|
|
|
var pivotItem = layout.getItemAt(center) ?? layout.getItemAt(Offset(0, center.dy));
|
|
|
|
if (pivotItem == null) {
|
|
final pivotSectionKey = layout.getSectionAt(center.dy)?.sectionKey;
|
|
if (pivotSectionKey != null) {
|
|
pivotItem = layout.sections[pivotSectionKey]?.firstOrNull;
|
|
}
|
|
}
|
|
|
|
if (pivotItem != null) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!_canUseTracker) return;
|
|
|
|
try {
|
|
_highlightInfo?.trackItem(
|
|
pivotItem,
|
|
animate: false,
|
|
);
|
|
} catch (e, st) {
|
|
debugPrint('GridItemTracker _onLayoutChanged ignored: $e');
|
|
debugPrint('$st');
|
|
}
|
|
});
|
|
}
|
|
} catch (e, st) {
|
|
debugPrint('GridItemTracker _onLayoutChanged ignored: $e');
|
|
debugPrint('$st');
|
|
}
|
|
}
|
|
}
|