Skip to content

Commit 457252c

Browse files
committed
perf: defer chapter-boundary paint work after page turns
Move prune/warm off the animation-complete path, clear rapid-tap queue when crossing chapters, and avoid stacking picture recording on the same frame as chapter swaps.
1 parent 0585363 commit 457252c

4 files changed

Lines changed: 131 additions & 39 deletions

File tree

lib/model/read_model.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,13 @@ class ReadModel with ChangeNotifier {
6363
showLoading: _showTextLoading,
6464
hideLoading: _hideTextLoading,
6565
prunePictures: _prunePictureCache,
66-
warmPictures: () => _pictures.warmAroundCurrent(),
66+
warmPictures: ({bool deferHeavy = false}) =>
67+
_pictures.warmAroundCurrent(deferHeavy: deferHeavy),
6768
scheduleProgressSave: () => scheduleProgressSave(),
6869
notify: notifyListeners,
6970
markNeedsPaint: _markNeedsPaint,
7071
activeBookId: () => book?.id,
72+
clearQueuedTurns: () => pagePainter?.pageManager?.clearQueuedTurns(),
7173
);
7274
GlobalKey? canvasKey;
7375
final ReaderPainter _painter = ReaderPainter();

lib/model/reader/page_picture_resolver.dart

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,42 @@ class PagePictureResolver {
6666

6767
/// Eagerly paint current page (if missing) then schedule prev/next on the
6868
/// next frame so the first gesture never cold-records mid-drag.
69-
void warmAroundCurrent({bool includeNeighbors = true}) {
69+
///
70+
/// When [deferHeavy] is true, recording is postponed to the next frame so
71+
/// chapter-boundary commits (animation status callbacks) stay jank-free.
72+
/// Cache hits are still free either way.
73+
void warmAroundCurrent({
74+
bool includeNeighbors = true,
75+
bool deferHeavy = false,
76+
}) {
7077
final b = bookOf();
7178
final current = curPageOf();
7279
if (b == null || current == null || current.pages.isEmpty) return;
80+
final key = _key(b.id, b.chapterIndex, b.pageIndex.clamp(0, current.pageOffsets - 1));
81+
final cached = cache.containsKey(key);
82+
if (cached) {
83+
if (includeNeighbors) scheduleNeighborWarm();
84+
return;
85+
}
86+
if (deferHeavy) {
87+
final gen = ++_warmGeneration;
88+
final bookId = b.id;
89+
final chapter = b.chapterIndex;
90+
final page = b.pageIndex;
91+
SchedulerBinding.instance.addPostFrameCallback((_) {
92+
if (gen != _warmGeneration) return;
93+
if (activeBookId() != bookId) return;
94+
final now = bookOf();
95+
if (now == null ||
96+
now.chapterIndex != chapter ||
97+
now.pageIndex != page) {
98+
return;
99+
}
100+
paintCurrent();
101+
if (includeNeighbors) preloadNeighbors();
102+
});
103+
return;
104+
}
73105
paintCurrent();
74106
if (includeNeighbors) {
75107
scheduleNeighborWarm();

lib/model/reader/page_turn_committer.dart

Lines changed: 88 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:book/entity/chapter_toc_entry.dart';
33
import 'package:book/entity/read_page.dart';
44
import 'package:bot_toast/bot_toast.dart';
55
import 'package:flutter/foundation.dart';
6+
import 'package:flutter/scheduler.dart';
67

78
/// Commits a logical page/chapter advance after a swipe or tap turn.
89
///
@@ -27,6 +28,7 @@ class PageTurnCommitter {
2728
required this.notify,
2829
required this.markNeedsPaint,
2930
required this.activeBookId,
31+
this.clearQueuedTurns,
3032
});
3133

3234
final Book? Function() bookOf;
@@ -42,12 +44,18 @@ class PageTurnCommitter {
4244
final void Function() hideLoading;
4345
final void Function() prunePictures;
4446
/// Warm current + schedule prev/next after page/chapter advance.
45-
final void Function() warmPictures;
47+
///
48+
/// [deferHeavy] postpones picture recording when the page is a cache miss
49+
/// so animation-complete handlers do not hitch the UI thread.
50+
final void Function({bool deferHeavy}) warmPictures;
4651
final void Function() scheduleProgressSave;
4752
final void Function() notify;
4853
final void Function() markNeedsPaint;
4954
final String? Function() activeBookId;
5055

56+
/// Drop queued rapid-taps when crossing a chapter so we don't cascade loads.
57+
final void Function()? clearQueuedTurns;
58+
5159
void commit(Object? offsetDifference) {
5260
final b = bookOf();
5361
if (b == null) return;
@@ -106,12 +114,25 @@ class PageTurnCommitter {
106114
);
107115
}
108116
// New current may already be cached from neighbor warm; schedule next ±1.
109-
warmPictures();
117+
warmPictures(deferHeavy: false);
110118
markNeedsPaint();
111119
notify();
112120
scheduleProgressSave();
113121
}
114122

123+
/// Prune + warm after the current frame so animation-complete stays smooth.
124+
void _deferChapterHousekeeping({required int chapterAfter}) {
125+
final bookId = activeBookId();
126+
SchedulerBinding.instance.addPostFrameCallback((_) {
127+
if (activeBookId() != bookId) return;
128+
final b = bookOf();
129+
if (b == null || b.chapterIndex != chapterAfter) return;
130+
prunePictures();
131+
// Cache hit for page 0 is free; miss records next frame (already post-frame).
132+
warmPictures(deferHeavy: false);
133+
});
134+
}
135+
115136
void _turnToNextChapter(
116137
Book b,
117138
List<ChapterTocEntry> chapters,
@@ -126,19 +147,34 @@ class PageTurnCommitter {
126147
return;
127148
}
128149

150+
// Rapid taps must not chain-load multiple chapters while this one settles.
151+
clearQueuedTurns?.call();
152+
129153
b.chapterIndex += 1;
130154
setPrePage(curPageOf());
131155
final following = nextPageOf();
132-
if (following == null || following.chapterName == '-1') {
156+
final needsLoad =
157+
following == null || following.chapterName == '-1' || following.pages.isEmpty;
158+
if (needsLoad) {
133159
showLoading('正在加载下一章…');
134-
loadChapter(b.chapterIndex).then((value) {
135-
if (activeBookId() == b.id) {
136-
setCurPage(value);
137-
warmPictures();
138-
markNeedsPaint();
139-
notify();
160+
final target = b.chapterIndex;
161+
final bookId = b.id;
162+
loadChapter(target).then((value) {
163+
if (activeBookId() != bookId) {
164+
hideLoading();
165+
return;
140166
}
167+
final cur = bookOf();
168+
if (cur == null || cur.chapterIndex != target) {
169+
hideLoading();
170+
return;
171+
}
172+
setCurPage(value);
173+
// Defer paint — pagination just finished; don't stack record on same turn.
174+
markNeedsPaint();
175+
notify();
141176
hideLoading();
177+
_deferChapterHousekeeping(chapterAfter: target);
142178
});
143179
} else {
144180
setCurPage(following);
@@ -149,21 +185,29 @@ class PageTurnCommitter {
149185
debugPrint(
150186
'[ReadModel] commitPageTurn +chapter '
151187
'$beforeCur:$beforeIdx → ${b.chapterIndex}:${b.pageIndex} '
152-
'dir=$dir pages=$curLen',
188+
'dir=$dir pages=$curLen ready=${!needsLoad}',
153189
);
154190
}
155-
prunePictures();
156-
warmPictures();
191+
// Swap first; prune/paint after this frame (avoids anim-callback jank).
192+
markNeedsPaint();
193+
notify();
157194
scheduleProgressSave();
195+
if (!needsLoad) {
196+
_deferChapterHousekeeping(chapterAfter: b.chapterIndex);
197+
}
198+
final nextTarget = b.chapterIndex + 1;
199+
final bookId = b.id;
158200
Future.delayed(const Duration(milliseconds: 500), () {
159-
if (activeBookId() == b.id) {
160-
loadChapter(b.chapterIndex + 1).then((value) {
161-
if (activeBookId() == b.id) {
162-
setNextPage(value);
163-
warmPictures();
164-
}
165-
});
166-
}
201+
if (activeBookId() != bookId) return;
202+
final cur = bookOf();
203+
if (cur == null || cur.chapterIndex != nextTarget - 1) return;
204+
loadChapter(nextTarget).then((value) {
205+
if (activeBookId() != bookId) return;
206+
final now = bookOf();
207+
if (now == null || now.chapterIndex != nextTarget - 1) return;
208+
setNextPage(value);
209+
warmPictures(deferHeavy: true);
210+
});
167211
});
168212
}
169213

@@ -178,11 +222,13 @@ class PageTurnCommitter {
178222
BotToast.showText(text: '第一页');
179223
return;
180224
}
225+
clearQueuedTurns?.call();
181226
final previous = prePageOf();
182-
if (previous == null) {
227+
if (previous == null || previous.pages.isEmpty) {
183228
showLoading('正在加载上一章…');
229+
final bookId = b.id;
184230
loadChapter(tempCur).then((value) {
185-
if (activeBookId() != b.id) {
231+
if (activeBookId() != bookId) {
186232
hideLoading();
187233
return;
188234
}
@@ -191,16 +237,17 @@ class PageTurnCommitter {
191237
b.chapterIndex = tempCur;
192238
b.pageIndex = (curPageOf()?.pageOffsets ?? 1) - 1;
193239
setPrePage(null);
194-
warmPictures();
195240
markNeedsPaint();
196241
notify();
197242
hideLoading();
198-
prunePictures();
199243
scheduleProgressSave();
244+
_deferChapterHousekeeping(chapterAfter: tempCur);
200245
loadChapter(b.chapterIndex - 1).then((v) {
201-
if (activeBookId() == b.id) {
246+
if (activeBookId() == bookId) {
247+
final now = bookOf();
248+
if (now == null || now.chapterIndex != tempCur) return;
202249
setPrePage(v);
203-
warmPictures();
250+
warmPictures(deferHeavy: true);
204251
}
205252
});
206253
});
@@ -210,7 +257,6 @@ class PageTurnCommitter {
210257
setCurPage(previous);
211258
b.chapterIndex -= 1;
212259
b.pageIndex = (curPageOf()?.pageOffsets ?? 1) - 1;
213-
notify();
214260
setPrePage(null);
215261
if (kDebugMode) {
216262
debugPrint(
@@ -219,18 +265,23 @@ class PageTurnCommitter {
219265
'dir=$dir',
220266
);
221267
}
222-
prunePictures();
223-
warmPictures();
268+
markNeedsPaint();
269+
notify();
224270
scheduleProgressSave();
271+
_deferChapterHousekeeping(chapterAfter: b.chapterIndex);
272+
final prevTarget = b.chapterIndex - 1;
273+
final bookId = b.id;
225274
Future.delayed(const Duration(milliseconds: 500), () {
226-
if (activeBookId() == b.id) {
227-
loadChapter(b.chapterIndex - 1).then((value) {
228-
if (activeBookId() == b.id) {
229-
setPrePage(value);
230-
warmPictures();
231-
}
232-
});
233-
}
275+
if (activeBookId() != bookId) return;
276+
final cur = bookOf();
277+
if (cur == null || cur.chapterIndex != prevTarget + 1) return;
278+
loadChapter(prevTarget).then((value) {
279+
if (activeBookId() != bookId) return;
280+
final now = bookOf();
281+
if (now == null || now.chapterIndex != prevTarget + 1) return;
282+
setPrePage(value);
283+
warmPictures(deferHeavy: true);
284+
});
234285
});
235286
}
236287
}

lib/view/page_turn/reader_page_manager.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ class ReaderPageManager {
7171
return left.isNegative ? Duration.zero : left;
7272
}
7373

74+
/// Drop any queued rapid-tap intents (e.g. on chapter boundary).
75+
void clearQueuedTurns() {
76+
if (_queuedDirs.isEmpty) return;
77+
_log('clearQueuedTurns dropped=$_queuedDirs');
78+
_queuedDirs.clear();
79+
}
80+
7481
bool get _needsController =>
7582
currentAnimationType == TYPE_ANIMATION_COVER_TURN ||
7683
currentAnimationType == TYPE_ANIMATION_SIMULATION_TURN;

0 commit comments

Comments
 (0)