Skip to content

Conversation

@sande11
Copy link

@sande11 sande11 commented Nov 14, 2025

1. Overview

This PR addresses two intertwined UX issues affecting font scaling and layout consistency inside the WebView.

1.1 Issues Fixed

  1. Font size not persisting between page navigations.

  2. Layout shift occurring in specific sections — notably
    Chapter 5 → Subchapter C (“Special clinical situations”)
    where content shifts left after increasing font size, leaving the page, and returning.

  3. Visual elements not scaling consistently with text, specifically:

  • ic_chapter_icon

  • The decorative vertical bar generated by .uk-paragraph::before

All fixes are implemented entirely at runtime in BodyFragment without modifying any CSS files.

2. Issues in Detail

2.1 Font Persistence + Layout Shift

  • Previously the app mixed:

    • WebView page zoom (onZoomOut()), and

    • textZoom (CSS text scaling)

  • This combination caused conflicts, unpredictable reflow behavior, and improper restoration of layout when revisiting pages.

  • Result:

    • After changing the font, navigating away, and returning, content became left-aligned with inconsistent right padding (especially in Chapter 5C).

2.2 Inconsistent Visual Scaling

  • The chapter icon (ic_chapter_icon) remained a fixed size while text scaled via textZoom.

  • The decorative line (.uk-paragraph::before):

    -Used fixed rem units

    -Did not follow the textZoom scaling

    -Misaligned at larger sizes

    -Overlapped the following text at smaller sizes

3. Solution

3.1 Use Only textZoom + Reapply on Page Load

  1. Disable WebView page zoom

    • Commented out onZoomOut() to avoid conflicts.
  2. Reapply updates after navigation

    • Call updateFont() from onPageFinished(...)

    -Ensures consistent, correct scaling every time a page loads.

3.2 Scale All Visual Elements at Runtime

All adjustments happen dynamically using JavaScript + injected <style> blocks.

4. Implementation Details

4.1 Chapter Icon Scaling (ic_chapter_icon)

  • Determine scaling based on the selected font size.

  • Locate chapter icons with:

    -img[src*="ic_chapter.svg"]

  • Add the class .ic_chapter_icon for consistent targeting.

-Inject late <style> with !important:

-Guarantees icon width/height override earlier CSS.
  • Apply:

    -Inline width/height

    -HTML attributes (width="", height="") as a fallback

  • Ensure parent wrapper reserves enough space:

    • Set explicit size + flex: 0 0 auto

    -Prevents flex-based clipping at larger font sizes.

4.2 Decorative Line Scaling (.uk-paragraph::before)

-Inject a runtime <style id="uk-paragraph-override"> with:

  • Height

  • Width

  • Top offset

  • Border radius

  • All values computed in px based on the same scale factor used to compute text size.

  • Fixes issues where:

    -The line misaligns at large sizes

    -The line overlaps text at smaller sizes

  1. Code Changes
    5.1 app/src/main/java/.../BodyFragment.kt
    setupWebView()

Commented out onZoomOut() to eliminate page zoom.

Added updateFont() inside WebViewClient.onPageFinished(...) so scaling is reapplied on each navigation.

updateFont()

Read saved font preference.

Apply via webView.settings.textZoom.

Compute a scaleFactor derived from the font size.

Inject JavaScript to:

Detect chapter icons → add .ic_chapter_icon

Create/update <style id="chapter-icon-override">

 private fun updateFont() {
        val fontIndex =
            sharedPreferences.getString(getString(R.string.font_key), "1")?.toInt() ?: 1
        val fontSize = when (fontIndex) {
            0 -> 100 // Small
            1 -> 125// Normal
            2 -> 150 // Large
            3 -> 175 // Larger
            else -> 125
        }

        webViewFont.settings.textZoom = fontSize

        // Compute target pixel size for chapter icon relative to 16px @ 125% baseline
                val scaleFactor = fontSize / 125.0
                val iconPx = 16.0 * scaleFactor
                val iconPxStr = String.format("%.2f", iconPx)

            // Compute scaled metrics for the decorative line used by `.uk-paragraph`
            val remBasePx = 16.0 // 1rem baseline
            val ukHeightPx = 2.0 * remBasePx * scaleFactor      // was 2rem
            val ukWidthPx = 0.5 * remBasePx * scaleFactor       // was 0.5rem
            val ukTopPx = -0.25 * remBasePx * scaleFactor       // was -0.25rem
            val ukRadiusPx = 0.25 * remBasePx * scaleFactor     // was 0.25rem
            val ukHeightPxStr = String.format("%.2f", ukHeightPx)
            val ukWidthPxStr = String.format("%.2f", ukWidthPx)
            val ukTopPxStr = String.format("%.2f", ukTopPx)
            val ukRadiusPxStr = String.format("%.2f", ukRadiusPx)

                // 1) Set CSS variable consumed by .ic_chapter_icon in style.css (which uses !important)
                val setVar = """
                        (function(){
                            try {
                                var size='${iconPxStr}px';
                                document.documentElement.style.setProperty('--chapter-icon-size', size);
                            } catch(e) {}
                        })();
                """.trimIndent()

                // 2) Ensure chapter icon image gets the class so the CSS rule applies, without touching other content images
                val tagIcons = """
                        (function(){
                            try {
                                var nodes = document.querySelectorAll('img[src$="ic_chapter.svg"], img[src*="/ic_chapter.svg"], img[src*="ic_chapter.svg"]');
                                nodes.forEach(function(n){
                                    if(n.classList && !n.classList.contains('ic_chapter_icon')) n.classList.add('ic_chapter_icon');
                                });
                            } catch(e) {}
                        })();
                """.trimIndent()

                // 3) Inject an explicit override rule with !important placed after external CSS
                val injectOverride = """
                        (function(){
                             try {
                                 var style = document.getElementById('chapter-icon-override');
                                 if(!style){
                                     style = document.createElement('style');
                                     style.id = 'chapter-icon-override';
                                     document.head.appendChild(style);
                                 }
                                 var size='${iconPxStr}px';
                                 style.textContent = '.ic_chapter_icon{width:'+size+' !important;height:'+size+' !important;}';
                             } catch(e) {}
                        })();
                """.trimIndent()

                // 4) also apply inline size and attributes so pages missing style.css still resize correctly
                val sizeIcons = """
                        (function(){
                             try {
                                 var cssSize='${iconPxStr}px';
                                 var attrSize='${iconPxStr}';
                                 var nodes = document.querySelectorAll('img[src$=\"ic_chapter.svg\"], img[src*=\"/ic_chapter.svg\"], img[src*=\"ic_chapter.svg\"]');
                                 nodes.forEach(function(n){
                                     // Ensure the parent wrapper also reserves space for the icon
                                     var p = n.parentElement;
                                     if (p) {
                                         p.style.width = cssSize;
                                         p.style.height = cssSize;
                                         p.style.flex = '0 0 auto';
                                     }
                                     // Apply explicit sizing on the image
                                     n.style.display = 'inline-block';
                                     n.style.width = cssSize;
                                     n.style.height = cssSize;
                                     n.style.maxWidth = cssSize;
                                     n.style.maxHeight = cssSize;
                                     if (n.setAttribute) {
                                         n.setAttribute('width', attrSize);
                                         n.setAttribute('height', attrSize);
                                     }
                                 });
                             } catch(e) {}
                        })();
                """.trimIndent()

                // 5) Scale the decorative line for paragraphs `.uk-paragraph::before` to match text zoom
                val paragraphOverride = """
                        (function(){
                             try {
                                 var style = document.getElementById('uk-paragraph-override');
                                 if(!style){
                                     style = document.createElement('style');
                                     style.id = 'uk-paragraph-override';
                                     document.head.appendChild(style);
                                 }
                                 var h='${ukHeightPxStr}px';
                                 var w='${ukWidthPxStr}px';
                                 var t='${ukTopPxStr}px';
                                 var r='${ukRadiusPxStr}px';
                                 style.textContent = ''+
                                   '.uk-paragraph{position: relative;}'+
                                   '.uk-paragraph::before{'+
                                     'content:""; position:absolute; left:0; '+
                                     'top:'+t+' !important; '+
                                     'height:'+h+' !important; '+
                                     'width:'+w+' !important; '+
                                     'background-color: var(--primary-color); '+
                                     'border-radius:'+r+' !important;'+
                                   '}';
                             } catch(e) {}
                        })();
                """.trimIndent()

                // Execute after WebView layout pass
                webViewFont.post {
                        webViewFont.evaluateJavascript(setVar, null)
                        webViewFont.evaluateJavascript(tagIcons, null)
                        webViewFont.evaluateJavascript(injectOverride, null)
            webViewFont.evaluateJavascript(sizeIcons, null)
            webViewFont.evaluateJavascript(paragraphOverride, null)
                }
    }

Apply inline and HTML-attribute sizing

Adjust icon wrapper to fix layout clipping

Create/update <style id="uk-paragraph-override"> for scaling paragraph bars

The onZoomOut() method call in setupWebView was commented out, to prevent automatic zooming behavior which was causing conflicts in font size persistence and causing content to be pushed to the left of the screen
Added logic to compute and apply chapter icon size in the WebView based on the current font size. This includes setting a CSS variable, tagging relevant images, injecting override styles, and applying inline sizing to ensure consistent scaling even if external styles are missing. The update is triggered after layout and on every page load.
Adds logic to dynamically scale the `.uk-paragraph::before` decorative line in response to text zoom, ensuring consistent appearance. The computed CSS values are injected via a style element in the WebView.
@sande11 sande11 requested a review from MaxwellKJr November 14, 2025 13:55
@sande11 sande11 self-assigned this Nov 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants