fix(reader): 🐛 keep the reading position across refocus and rotation (#95) - #96
Merged
Merged
Conversation
Readium's EPUB navigator pads its own page container by the system-bar insets (Configuration.shouldApplyInsetsPadding, on by default). Quire already owns the reader's insets — full-screen reading is deliberately edge-to-edge, and with it off the whole reader subtree is padded by WindowInsets.systemBars — so that padding is either for bars that are hidden or a second helping of the same inset. It also arrived late. The page rendered at full height, then the first time the window regained focus the WebView finally re-measured against the padding and lost ~350px (measured: CoordinatorLayout pad=247/105, R2WebView 1080x2424 -> 1080x2072@247). That re-paginated the chapter, so the reader came back to a different page, and because the resize happened inside Readium's own view tree nothing here saw it coming: the post-re-pagination locator was published and written over the saved position, making the wrong page permanent. Turn that padding off, and harden the re-anchor while we're at it. It used to be armed only for resizes the reader could name — rotation and the full-screen-reading toggle — which is why an unnamed one did this much damage. ReaderViewModel.onViewportChanged now arms on the measured viewport instead, covering causes we haven't thought of; seek/goTo clear a pending anchor so an explicit jump isn't undone by one. Verified on an emulator with a 5-chapter fixture: full-screen reading on, five screenshots across four background/resume cycles are byte-identical (before: drifted, with a 247px gap that stuck); off, three cycles hold position with a single correct inset.
Rotate to landscape and back and the reader did not come back to the page you left. It moved by a few paragraphs, and rotating again moved it again, always further, so a few round trips could leave you a screen or two from where you were reading. A measured trace on a marker book went from paragraph 18 to 31 to 38 over three rotate-and-return trips. This is separate from the refocus bug in the previous commit and predates it. The reader put itself back by handing Readium the locator Readium had last published. That locator carries only `locations.progression`, a fraction of the way through the chapter, because Readium builds it from the EPUB positions service, which emits nothing but a progression and a position index. Readium restores such a locator arithmetically: `round(progression * numPages)`, then jump to that page. Rotating rebuilds the page grid the arithmetic is denominated in (12 portrait pages against 23 landscape ones in the test book) and the sum runs while the grid is still being rebuilt, so the page it picks is only loosely related to the page you left. Readium then publishes wherever it landed, that becomes the anchor for the next rotation, and the error compounds. So anchor on the document instead of on a fraction. Readium restores a locator precisely, by looking an element up in the DOM and scrolling to it, whenever the locator carries text to match, and it will tell you which element is on screen. Three things had to be true for that to actually hold still: The anchor has to be an element that *begins* on the page. Readium's own `findFirstVisibleLocator` returns the first element merely *overlapping* the page, which is usually one that began earlier, and restoring scrolls to where an element begins. Both round the same way, so they compound: anchoring that way still walked backwards, 22 to 21 to 20 to 18, down to the chapter heading. PAGE_START_ANCHOR_JS mirrors Readium's walk and changes the test to "begins here", which makes capture and restore inverses of each other. The re-anchor has to happen when the layout is final. Re-pagination arrives in steps and Readium snaps to page boundaries using a page width it caches in JavaScript, so an early re-anchor measures against the old grid. Anchoring only on the first step landed wrong, and inconsistently. It now re-anchors on every step and once more when the size stops changing, which is safe to repeat only because a DOM anchor lands in the same place every time. A rotation must not rewrite the anchor. Which element begins a page depends on the page's shape, so re-reading the anchor in the orientation you are only passing through replaces it with an earlier one and the return leg honours that. Readium reports the restored position about half a second after the jump, which arrives as an ordinary position update, so the anchor is held across that window. Verified on an emulator against a book whose every sentence carries its own paragraph marker, comparing screenshots. With full-screen reading off, ten starting positions over two round trips each, plus eight consecutive round trips from one position: every portrait screenshot byte-identical to the one before rotating, and no accumulation. With full-screen reading on, four positions over two round trips each, likewise identical. The already-fixed refocus behaviour still holds: three background-and-resume cycles in each mode leave the page untouched. The saved reading position now carries the DOM anchor too, so reopening a book lands on the exact page rather than near it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the two ways the reader lost your place: coming back to the app, and rotating the device. Both come down to the same thing, the reader being re-paginated underneath you and then saving whatever text happened to land on screen.
Refocus. Readium was padding its own page container by the system-bar insets, and doing it late. The page rendered full height, then the first time the window regained focus the WebView re-measured against that padding and lost around 350px, re-paginating the chapter under the reader. In full-screen reading those bars are hidden and the page is deliberately full-bleed, so the padding was for bars that were not there; with full-screen reading off it was simply double-inset, since the reader already pads itself. Quire owns the reader's insets now, so Readium is told not to apply them (
shouldApplyInsetsPadding = false). The reader also re-anchors on any change of its measured viewport rather than only the two changes it knew the names of, which is the backstop for the next one of these.Rotation. Rotate to landscape and back and the reader did not come back to the page you left. It moved by a few paragraphs, and rotating again moved it again, always further. A measured trace on a marker book went from paragraph 18 to 31 to 38 over three rotate-and-return trips. This one predates the refocus bug.
The cause is what the reader was handing Readium to restore. Readium's published locator carries only
locations.progression, a fraction of the way through the chapter, because Readium builds it from the EPUB positions service, which emits nothing else. Readium restores such a locator arithmetically,round(progression * numPages), and rotating rebuilds the very page grid that sum is denominated in (12 portrait pages against 23 landscape ones in the test book), while the sum runs before the rebuild has finished. Readium then publishes wherever it landed, that becomes the anchor for the next rotation, and the error compounds.So the anchor is now a place in the document rather than a fraction. Readium restores a locator precisely, by looking an element up in the DOM and scrolling to it, whenever the locator carries text to match. Three things had to hold for that to actually stay put:
findFirstVisibleLocatorreturns the first element merely overlapping the page, usually one that began earlier, and restoring scrolls to where an element begins. Both round the same way, so they compound: anchoring that way still walked backwards, 22 to 21 to 20 to 18, all the way down to the chapter heading.PAGE_START_ANCHOR_JSmirrors Readium's walk and changes the test to "begins here", which makes capturing and restoring inverses of each other.A side effect worth having: the saved reading position now carries the DOM anchor too, so reopening a book lands on the exact page rather than near it.
Verification
On an emulator, against a generated book whose every sentence carries its own paragraph marker, comparing screenshots before and after.
Unit tests: 941 passing. The new ones were mutation-checked, each guard was reverted in turn and the corresponding test fails without it.
Component(s)
Checklist
core/identitychanged: matching change on the other side + fixtures updated — N/ARelated issues
Closes #95