summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-09-29 07:12:48 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-09-29 07:12:48 +0000
commita492e2def3dabe5e8de5ef4486b92859850503db (patch)
tree5b8ca48f754b26fbbbbdbebda9686d0b758b9a31 /app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt
parentcf8ff865f7764953ded1b93c8ec42d4aff589e81 (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.kt69
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))
}
}
)