diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-09-29 07:12:48 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-09-29 07:12:48 +0000 |
| commit | a492e2def3dabe5e8de5ef4486b92859850503db (patch) | |
| tree | 5b8ca48f754b26fbbbbdbebda9686d0b758b9a31 /app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt | |
| parent | cf8ff865f7764953ded1b93c8ec42d4aff589e81 (diff) | |
Reflow reader text live while pinching, and let a zoomed image pan
Pinching in the reader used to scale the whole WebView like a photo and snap
back to the real font size on release. It now drives WebSettings.textZoom as
the fingers move, so the text itself resizes and reflows during the gesture
with nothing to snap back from; images and margins stay put. The reload at the
new saved size is what returns textZoom to 100%, so the two never overlap --
and loading reader HTML now goes through ReaderWebView.loadReaderHtml, which
does both together, so a future load site can't forget the reset.
Reader view for a tapped link now honors the saved font size and the same
pinch gesture, instead of always rendering at the 18px default.
A zoomed image can be panned again. The viewer computed its pan bounds from
values captured when the dialog first composed -- an empty viewport around a
not-yet-decoded image -- because a pointerInput(Unit) block keeps the lambda it
was created with. Those bounds allowed exactly one offset, 0, so zooming worked
and dragging did nothing. The whole pinch/pan step is now one pure function the
gesture callback calls with values it reads fresh each time.
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt')
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt | 69 |
1 files changed, 52 insertions, 17 deletions
diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt index 9a11dd3..6d96e79 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt @@ -24,7 +24,9 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import org.terst.feedreader.data.DEFAULT_READER_FONT_SIZE_PX +import org.terst.feedreader.data.UNZOOMED_TEXT_ZOOM_PERCENT import org.terst.feedreader.data.pinchZoomFontSizePx +import org.terst.feedreader.data.readerTextZoomPercent // In-memory only (process lifetime, not persisted to disk) -- scoped to "don't lose my // place if Next/Previous takes me away and back," not to surviving an app restart. A @@ -99,16 +101,25 @@ private fun extractYouTubeVideoId(uri: Uri): String? { * exactly as before. ScaleGestureDetector only calls back once a second pointer is down, * so a single-finger drag never triggers any of this. * - * currentFontSizePx is read fresh (not captured once) when a gesture ends, since the - * WebView this attaches to is created once but can outlive a Settings-screen-driven font - * size change made while this same article is still open. + * currentFontSizePx is read fresh (not captured once) at the START of each gesture, since + * the WebView this attaches to is created once but can outlive a Settings-screen-driven + * font size change made while this same article is still open. Everything during the + * gesture is measured against that one starting size, so a pinch is judged against where + * it began rather than against a size it's already changed. * - * During the gesture the WebView is visually scaled live (View.scaleX/scaleY) for - * immediate feedback -- cheap, and independent of the WebView's own content layout. - * At gesture end the scale resets to 1x and onFontSizeChanged reports the new, - * clamped, rounded font size for the caller to persist; ReaderScreen's existing - * content-vs-fontSizePx reload logic (see ReaderScreen.ReaderContent) then re-renders - * the actual HTML at that size. + * The text reflows live, as the fingers move: each step sets WebSettings.textZoom, which + * re-lays-out the text at the new size right away -- no reload, no JavaScript, and + * images/padding stay exactly where they are. An earlier version instead scaled the whole + * WebView (View.scaleX/scaleY) during the gesture and snapped back to 1x on release, which + * zoomed the canvas like a photo (blurring text, magnifying the page's own margins) and + * then visibly jumped when the real font size took over. Nothing is scaled now, so there's + * nothing to snap back from. + * + * At gesture end onFontSizeChanged reports the new, clamped, rounded font size for the + * caller to persist; ReaderScreen's content-vs-fontSizePx reload logic then re-renders the + * HTML at that size via loadReaderHtml, which is also what returns textZoom to 100% -- the + * live zoom stays applied until the reload it hands over to is on screen, so the two never + * both apply and there's no gap where the text flicks back to its old size. */ data class ReaderWebViewCallbacks( val onOpenLink: (String) -> Unit, @@ -121,8 +132,30 @@ data class ReaderWebViewCallbacks( val onFontSizeChanged: (Int) -> Unit = {} ) -fun newReaderWebView(context: Context, callbacks: ReaderWebViewCallbacks): WebView = - WebView(context).apply { +/** + * The reader's WebView, which knows what font size the HTML it's showing was written at. + * + * That's the whole reason it's a class rather than a plain WebView: a live pinch lifts + * WebSettings.textZoom off 100% and leaves it there until the reload at the new font size + * arrives (see the gesture notes above). Anything that loads HTML into this WebView must put + * textZoom back, or the lifted zoom multiplies the new, already-bigger CSS size -- so loading + * goes through [loadReaderHtml], which does both together and can't be forgotten the way a + * separate "and also reset the zoom" line at each call site could. + */ +class ReaderWebView(context: Context) : WebView(context) { + /** The `font-size` the currently-loaded HTML was written with (see wrapReaderHtml). */ + var renderedFontSizePx: Int = DEFAULT_READER_FONT_SIZE_PX + private set + + fun loadReaderHtml(baseUrl: String?, html: String, fontSizePx: Int) { + renderedFontSizePx = fontSizePx + settings.textZoom = UNZOOMED_TEXT_ZOOM_PERCENT + loadDataWithBaseURL(baseUrl, html, "text/html", "UTF-8", null) + } +} + +fun newReaderWebView(context: Context, callbacks: ReaderWebViewCallbacks): ReaderWebView = + ReaderWebView(context).apply { settings.javaScriptEnabled = false overScrollMode = View.OVER_SCROLL_NEVER isVerticalScrollBarEnabled = false @@ -135,27 +168,29 @@ fun newReaderWebView(context: Context, callbacks: ReaderWebViewCallbacks): WebVi setOnScrollChangeListener { _, _, scrollY, _, _ -> callbacks.onScrollChanged(scrollY) } var pinchScale = 1f + var pinchBaseFontSizePx = DEFAULT_READER_FONT_SIZE_PX val webView = this val scaleDetector = ScaleGestureDetector( context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() { override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { pinchScale = 1f + pinchBaseFontSizePx = callbacks.currentFontSizePx() return true } override fun onScale(detector: ScaleGestureDetector): Boolean { pinchScale *= detector.scaleFactor - webView.scaleX = pinchScale - webView.scaleY = pinchScale + // Steps in whole pixels of font size, not in raw gesture scale, so a slow + // pinch relayouts a handful of times over its whole range rather than on + // every touch-move frame -- and lands on exactly the size it will persist. + val targetPx = pinchZoomFontSizePx(pinchBaseFontSizePx, pinchScale) + webView.settings.textZoom = readerTextZoomPercent(webView.renderedFontSizePx, targetPx) return true } override fun onScaleEnd(detector: ScaleGestureDetector) { - val newSizePx = pinchZoomFontSizePx(callbacks.currentFontSizePx(), pinchScale) - webView.scaleX = 1f - webView.scaleY = 1f - callbacks.onFontSizeChanged(newSizePx) + callbacks.onFontSizeChanged(pinchZoomFontSizePx(pinchBaseFontSizePx, pinchScale)) } } ) |
