mirror of
https://github.com/immich-app/immich.git
synced 2026-07-10 06:03:14 -07:00
Switch to using Flutter recommendDeferredLoading
This commit is contained in:
@@ -106,7 +106,7 @@ class _FixedSegmentRow extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final shouldDeferAssetLoading = ref.watch(timelineStateProvider.select((s) => s.isFastScrolling));
|
||||
final recommendDeferredLoading = ref.watch(timelineStateProvider.select((s) => s.recommendDeferredLoading));
|
||||
final timelineService = ref.read(timelineServiceProvider);
|
||||
final isDynamicLayout = columnCount <= (context.isMobile ? 2 : 3);
|
||||
|
||||
@@ -119,7 +119,7 @@ class _FixedSegmentRow extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldDeferAssetLoading) {
|
||||
if (recommendDeferredLoading) {
|
||||
return _buildPlaceholder(context);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,24 +54,24 @@ class TimelineState {
|
||||
|
||||
/// Indicates whether the timeline is scrolling beyond some configured "high" speed,
|
||||
/// such as when programmatically scrolling to the top or a really fast user fling
|
||||
final bool isFastScrolling;
|
||||
final bool recommendDeferredLoading;
|
||||
|
||||
const TimelineState({this.isScrolling = false, this.isFastScrolling = false});
|
||||
const TimelineState({this.isScrolling = false, this.recommendDeferredLoading = false});
|
||||
|
||||
bool get isInteracting => isScrolling || isFastScrolling;
|
||||
bool get isInteracting => isScrolling || recommendDeferredLoading;
|
||||
|
||||
@override
|
||||
bool operator ==(covariant TimelineState other) {
|
||||
return isScrolling == other.isScrolling && isFastScrolling == other.isFastScrolling;
|
||||
return isScrolling == other.isScrolling && recommendDeferredLoading == other.recommendDeferredLoading;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => isScrolling.hashCode ^ isFastScrolling.hashCode;
|
||||
int get hashCode => isScrolling.hashCode ^ recommendDeferredLoading.hashCode;
|
||||
|
||||
TimelineState copyWith({bool? isScrubbing, bool? isScrolling, bool? isFastScrolling}) {
|
||||
TimelineState copyWith({bool? isScrubbing, bool? isScrolling, bool? recommendDeferredLoading}) {
|
||||
return TimelineState(
|
||||
isScrolling: isScrolling ?? this.isScrolling,
|
||||
isFastScrolling: isFastScrolling ?? this.isFastScrolling,
|
||||
recommendDeferredLoading: recommendDeferredLoading ?? this.recommendDeferredLoading,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -81,12 +81,12 @@ class TimelineStateNotifier extends Notifier<TimelineState> {
|
||||
state = state.copyWith(isScrolling: isScrolling);
|
||||
}
|
||||
|
||||
void setFastScrolling(bool isFastScrolling) {
|
||||
state = state.copyWith(isFastScrolling: isFastScrolling);
|
||||
void setRecommendDeferredLoading(bool recommendDeferredLoading) {
|
||||
state = state.copyWith(recommendDeferredLoading: recommendDeferredLoading);
|
||||
}
|
||||
|
||||
@override
|
||||
TimelineState build() => const TimelineState(isScrolling: false, isFastScrolling: false);
|
||||
TimelineState build() => const TimelineState(isScrolling: false, recommendDeferredLoading: false);
|
||||
}
|
||||
|
||||
// This provider watches the buckets from the timeline service & args and serves the segments.
|
||||
|
||||
@@ -150,9 +150,8 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
double _baseScaleFactor = 3.0;
|
||||
int? _restoreAssetIndex;
|
||||
|
||||
final Stopwatch _scrollClock = Stopwatch()..start();
|
||||
double _lastScrollPixelOffset = 0;
|
||||
Duration? _lastScrollTimestamp;
|
||||
static const _fastScrollDebounceDuration = Duration(milliseconds: 100);
|
||||
Timer? _fastScrollDebounceTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -246,65 +245,32 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fastScrollDebounceTimer?.cancel();
|
||||
_scrollController.dispose();
|
||||
_eventSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Calculate the velocity of a timeline scroll event and track if it exceeds some threshold
|
||||
/// Track whether the timeline is moving fast enough to defer per-row asset loading
|
||||
bool _onScrollVelocityNotification(ScrollNotification notification) {
|
||||
// Only consider the primary timeline ScrollView (no nested views)
|
||||
if (notification.depth != 0) {
|
||||
// Only consider the primary timeline ScrollView (no nested views) and update events
|
||||
if (notification.depth != 0 || notification is! ScrollUpdateNotification) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (notification is ScrollStartNotification) {
|
||||
_lastScrollTimestamp = null;
|
||||
// Use Flutter's built in fast velocity tracking
|
||||
if (_scrollController.position.recommendDeferredLoading(context)) {
|
||||
ref.read(timelineStateProvider.notifier).setRecommendDeferredLoading(true);
|
||||
|
||||
return false;
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
_lastScrollTimestamp = null;
|
||||
ref.read(timelineStateProvider.notifier).setFastScrolling(false);
|
||||
|
||||
return false;
|
||||
} else if (notification is! ScrollUpdateNotification) {
|
||||
// Assert we have a `ScrollUpdateNotification`
|
||||
return false;
|
||||
// We cannot rely on scroll end events, as the timeline scrubber jumps from position
|
||||
// to position, resulting in large spikes in velocity followed by low velocity
|
||||
// Instead, cancel fast scrolling after a timeout
|
||||
_fastScrollDebounceTimer?.cancel();
|
||||
_fastScrollDebounceTimer = Timer(
|
||||
_fastScrollDebounceDuration,
|
||||
() => ref.read(timelineStateProvider.notifier).setRecommendDeferredLoading(false),
|
||||
);
|
||||
}
|
||||
|
||||
final now = _scrollClock.elapsed;
|
||||
final lastTimestamp = _lastScrollTimestamp;
|
||||
final lastPixelOffset = _lastScrollPixelOffset;
|
||||
|
||||
_lastScrollTimestamp = now;
|
||||
_lastScrollPixelOffset = notification.metrics.pixels;
|
||||
|
||||
if (lastTimestamp == null) {
|
||||
// First tick of scroll
|
||||
return false;
|
||||
}
|
||||
|
||||
final deltaSeconds = (now - lastTimestamp).inMicroseconds / Duration.microsecondsPerSecond;
|
||||
if (deltaSeconds <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final velocity = (_lastScrollPixelOffset - lastPixelOffset).abs() / deltaSeconds;
|
||||
|
||||
// Use same threshold calculation as Flutter's `ScrollPhysics.recommendDeferredLoading`, adding hysteresis
|
||||
// Pixels per second must exceed the length of the longest view dimension to start fast scrolling
|
||||
// and it must drop below 50% of that dimension to stop fast scrolling
|
||||
final enterThreshold = View.of(context).physicalSize.longestSide;
|
||||
|
||||
final notifier = ref.read(timelineStateProvider.notifier);
|
||||
final isFastScrolling = ref.read(timelineStateProvider).isFastScrolling;
|
||||
|
||||
if (!isFastScrolling && velocity > enterThreshold) {
|
||||
notifier.setFastScrolling(true);
|
||||
} else if (isFastScrolling && velocity < enterThreshold / 2) {
|
||||
notifier.setFastScrolling(false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user