@@ -3,6 +3,7 @@ import 'package:book/entity/chapter_toc_entry.dart';
33import 'package:book/entity/read_page.dart' ;
44import 'package:bot_toast/bot_toast.dart' ;
55import '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}
0 commit comments