From a46fe91e440819bfbac0165cfaaa1cf4c2dae79e Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 25 Oct 2026 13:41:08 +0100 Subject: [PATCH] Load the later parts of a view on demand Core 7.6 can write the parts of a view after the first as scripts beside the page, which the page loads when the reader comes near. CoreLoader now turns this on with lazyLoading, and states lazyLayout. An editable render stays whole, so in the app this applies mostly to a pdf: core renders a page only when the reader comes near it. The app reads the DOM in two places, so both now load what they need: - Read aloud reads a pdf page by page. A page that has not loaded is loaded with odr.chunks.show, and read when odr-chunk-filled arrives. If its script never arrives, read aloud skips the page after 30 s. - A print from the page shows a part that has not loaded as empty space. PageView.loadAllChunks loads every part first, and gives up after 30 s. A second print while it loads is ignored. The page search counts the parts that have not loaded from the search index of core, so find needs no change. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01JuNa2W75khbpynkiG9iDqF --- CHANGELOG.md | 2 +- .../droid/background/CoreLoader.kt | 5 ++ .../droid/ui/activity/MainActivity.kt | 6 +- .../opendocument/droid/ui/widget/PageView.kt | 69 +++++++++++++++++-- 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31b66f20b2fb..df4b66eaefaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ section the body of the GitHub release, and refuses a version without one. `scri ## Unreleased - Long text files, PDFs, spreadsheets and presentations open much faster, because the app - lays out only the part near the screen. + lays out only the part near the screen, and renders a PDF page only when you scroll near it. - Images in long documents load when you scroll near them. - Search finds every match in a long document, also far from the screen. It ignores case and accents, so "cafe" finds "Café". diff --git a/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt b/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt index 58acf9301376..d4a9ccc4cce3 100644 --- a/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt +++ b/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt @@ -170,6 +170,11 @@ class CoreLoader(private val context: Context) { // document. PageView.setDarkeningAllowed picks between them htmlConfig.colorScheme = HtmlColorScheme.SYSTEM + // lay out only the lines, pages, rows and slides near the viewport, and load the parts of + // a view after the first as the reader comes near. An editable render stays whole + htmlConfig.lazyLayout = true + htmlConfig.lazyLoading = true + // stated rather than inherited, and this device's answer - see SpreadsheetBudget htmlConfig.spreadsheetLimit = TableDimensions(SpreadsheetBudget.ROWS, SpreadsheetBudget.COLUMNS) diff --git a/app/src/main/java/app/opendocument/droid/ui/activity/MainActivity.kt b/app/src/main/java/app/opendocument/droid/ui/activity/MainActivity.kt index d293d1f5adad..47929820d29a 100644 --- a/app/src/main/java/app/opendocument/droid/ui/activity/MainActivity.kt +++ b/app/src/main/java/app/opendocument/droid/ui/activity/MainActivity.kt @@ -633,8 +633,10 @@ class MainActivity : AppCompatActivity() { pageView.suspendDarkening() val pageSize = documentFragment.pageSize - printingManager.print(this, pageView, pageSize) { - documentFragment?.pageView?.resumeDarkening() + pageView.loadAllChunks { + printingManager.print(this, pageView, pageSize) { + documentFragment?.pageView?.resumeDarkening() + } } } } diff --git a/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt b/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt index 0625381b8714..77bf46887647 100644 --- a/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt +++ b/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt @@ -8,6 +8,7 @@ import android.net.Uri import android.os.Build import android.os.Handler import android.os.Looper +import android.os.SystemClock import android.util.AttributeSet import android.util.Base64 import android.util.Base64InputStream @@ -54,6 +55,9 @@ constructor(context: Context, attributeSet: AttributeSet?) : /** Told what the page's search reports, on the main thread - see [postMessage]. */ var searchListener: SearchListener? = null + /** Whether [loadAllChunks] is waiting for the page. */ + private var loadingChunks = false + /** What [setEditing] was last told, applied again to every page that loads. */ private var editingKind = EditingKind.NONE private var isEditing = false @@ -445,26 +449,72 @@ constructor(context: Context, attributeSet: AttributeSet?) : fun getParagraph(index: Int) { post { - // a pdf is one wrapper below the body, with every glyph in an aria-hidden layer and - // again in the selection layer; read that layer one page at a time. Its pages skip - // rendering off screen, which empties their innerText, so join the lines instead + // a pdf is a page per chunk, with every glyph in an aria-hidden layer and again in the + // selection layer; read that layer one page at a time. Its pages skip rendering off + // screen, which empties their innerText, so join the lines instead. A page that has not + // loaded is read once it has, or skipped if its script never arrives loadUrl( - "javascript:var pages = document.querySelectorAll('.p > .sel'); " + + "javascript:var pages = document.querySelectorAll('.p[data-odr-page]'); " + "var children = pages.length ? pages : document.body.childNodes; " + "if (children.length <= $index) { " + "paragraphListener.end();" + "} else {" + "var child = children[$index]; " + - "var text = !child ? null : pages.length ? " + - "Array.from(child.children, function (l) { return l.textContent; }).join('\\n') : " + + "var done = false; " + + "var read = function () { " + + "if (done) { return; } done = true; " + + "var layer = pages.length ? child.querySelector(':scope > .sel') : null; " + + "var text = !child ? null : pages.length ? (layer ? " + + "Array.from(layer.children, function (l) { return l.textContent; }).join('\\n') : null) : " + "child.nodeName.toLowerCase() != 'script' ? child.innerText : null; " + "if (text) {" + " paragraphListener.paragraph(text); } else {" + - " paragraphListener.increaseIndex(); } }" + " paragraphListener.increaseIndex(); } }; " + + "if (pages.length && child.hasAttribute('data-odr-chunk-src') && window.odr && odr.chunks) { " + + "child.addEventListener('odr-chunk-filled', read, {once: true}); " + + "setTimeout(read, $CHUNK_LOAD_TIMEOUT_MS); " + + "odr.chunks.show(child); " + + "} else { read(); } }" ) } } + /** + * Loads every part of the page that lazy loading left for later, then calls [then] on the main + * thread: a print shows a part that has not loaded as empty space. Gives up after + * [CHUNK_LOAD_TIMEOUT_MS] and calls [then] all the same. A call while one loads is ignored. + */ + fun loadAllChunks(then: () -> Unit) { + if (loadingChunks) { + return + } + loadingChunks = true + + val deadline = SystemClock.uptimeMillis() + CHUNK_LOAD_TIMEOUT_MS + val script = + "(function(){var c=document.querySelectorAll('[data-odr-chunk-src]');" + + "if(window.odr&&odr.chunks){c.forEach(function(e){" + + "if(!e.hasAttribute('data-odr-chunk-loading'))odr.chunks.show(e);});}" + + "return c.length;})()" + + fun poll() { + evaluateJavascript(script) { pending -> + if (destroyed) { + return@evaluateJavascript + } + + if (pending == "0" || pending == "null" || SystemClock.uptimeMillis() > deadline) { + loadingChunks = false + then() + } else { + postDelayed({ poll() }, CHUNK_LOAD_INTERVAL_MS) + } + } + } + + poll() + } + /** Turns the page's edit mode on or off. A pdf has no mode, so leaving only disarms. */ fun setEditing(kind: EditingKind, editing: Boolean) { editingKind = kind @@ -712,5 +762,10 @@ constructor(context: Context, attributeSet: AttributeSet?) : private const val SCROLL_RESTORE_ATTEMPTS = 20 private const val SCROLL_RESTORE_INTERVAL_MS = 100L + + /** How long a print waits for every part, and read aloud for one page. */ + private const val CHUNK_LOAD_TIMEOUT_MS = 30000L + + private const val CHUNK_LOAD_INTERVAL_MS = 200L } }