Maintain WebView content scroll position on orientation change

前端 未结 8 672
隐瞒了意图╮
隐瞒了意图╮ 2020-12-23 02:04

The browsers in Android 2.3+ do a good job at maintaining the scrolled position of content on an orientation changed.

I\'m trying to achieve the same thing for a We

相关标签:
8条回答
  • 2020-12-23 02:26

    To restore the current position of a WebView during orientation change I'm afraid you will have to do it manually.

    I used this method:

    1. Calculate actual percent of scroll in the WebView
    2. Save it in the onRetainNonConfigurationInstance
    3. Restore the position of the WebView when it's recreated

    Because the width and height of the WebView is not the same in portrait and landscape mode, I use a percent to represent the user scroll position.

    Step by step:

    1) Calculate actual percent of scroll in the WebView

    // Calculate the % of scroll progress in the actual web page content
    private float calculateProgression(WebView content) {
        float positionTopView = content.getTop();
        float contentHeight = content.getContentHeight();
        float currentScrollPosition = content.getScrollY();
        float percentWebview = (currentScrollPosition - positionTopView) / contentHeight;
        return percentWebview;
    }
    

    2) Save it in the onRetainNonConfigurationInstance

    Save the progress just before the orientation change

    @Override
    public Object onRetainNonConfigurationInstance() {
        OrientationChangeData objectToSave = new OrientationChangeData();
        objectToSave.mProgress = calculateProgression(mWebView);
        return objectToSave;
    }
    
    // Container class used to save data during the orientation change
    private final static class OrientationChangeData {
        public float mProgress;
    }
    

    3) Restore the position of the WebView when it's recreated

    Get the progress from the orientation change data

    private boolean mHasToRestoreState = false;
    private float mProgressToRestore;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        setContentView(R.layout.main);
    
        mWebView = (WebView) findViewById(R.id.WebView);
        mWebView.setWebViewClient(new MyWebViewClient());
        mWebView.loadUrl("http://stackoverflow.com/");
    
        OrientationChangeData data = (OrientationChangeData) getLastNonConfigurationInstance();
        if (data != null) {
            mHasToRestoreState = true;
            mProgressToRestore = data.mProgress;
        }
    }
    

    To restore the current position you will have to wait the page to be reloaded ( this method can be problematic if your page takes a long time to load)

    private class MyWebViewClient extends WebViewClient {
        @Override
        public void onPageFinished(WebView view, String url) {
            if (mHasToRestoreState) {
                mHasToRestoreState = false;
                view.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        float webviewsize = mWebView.getContentHeight() - mWebView.getTop();
                        float positionInWV = webviewsize * mProgressToRestore;
                        int positionY = Math.round(mWebView.getTop() + positionInWV);
                        mWebView.scrollTo(0, positionY);
                    }
                // Delay the scrollTo to make it work
                }, 300);
            }
            super.onPageFinished(view, url);
        }
    }
    

    During my test I encounter that you need to wait a little after the onPageFinished method is called to make the scroll working. 300ms should be ok. This delay make the display to flick (first display at scroll 0 then go to the correct position).

    Maybe there is an other better way to do it but I'm not aware of.

    0 讨论(0)
  • 2020-12-23 02:27

    Why you should consider this answer over accepted answer:

    Accepted answer provides decent and simple way to save scroll position, however it is far from perfect. The problem with that approach is that sometimes during rotation you won't even see any of the elements you saw on the screen before rotation. Element that was at the top of the screen can now be at the bottom after rotation. Saving position via percent of scroll is not very accurate and on large documents this inaccuracy can add up.

    So here is another method: it's way more complicated, but it almost guarantees that you'll see exactly the same element after rotation that you saw before rotation. In my opinion, this leads to a much better user experience, especially on a large documents.

    ======

    First of all, we will track current scroll position via javascript. This will allow us to know exactly which element is currently at the top of the screen and how much is it scrolled.

    First, ensure that javascript is enabled for your WebView:

    webView.getSettings().setJavaScriptEnabled(true);
    

    Next, we need to create java class that will accept information from within javascript:

    public class WebScrollListener {
    
        private String element;
        private int margin;
    
        @JavascriptInterface
        public void onScrollPositionChange(String topElementCssSelector, int topElementTopMargin) {
            Log.d("WebScrollListener", "Scroll position changed: " + topElementCssSelector + " " + topElementTopMargin);
            element = topElementCssSelector;
            margin = topElementTopMargin;
        }
    
    }
    

    Then we add this class to WebView:

    scrollListener = new WebScrollListener(); // save this in an instance variable
    webView.addJavascriptInterface(scrollListener, "WebScrollListener");
    

    Now we need to insert javascript code into html page. This script will send scroll data to java (if you are generation html, just append this script; otherwise, you might need to resort to calling document.write() via webView.loadUrl("javascript:document.write(" + script + ")");):

    <script>
        // We will find first visible element on the screen 
        // by probing document with the document.elementFromPoint function;
        // we need to make sure that we dont just return 
        // body element or any element that is very large;
        // best case scenario is if we get any element that 
        // doesn't contain other elements, but any small element is good enough;
        var findSmallElementOnScreen = function() {
            var SIZE_LIMIT = 1024;
            var elem = undefined;
            var offsetY = 0;
            while (!elem) {
                var e = document.elementFromPoint(100, offsetY);
                if (e.getBoundingClientRect().height < SIZE_LIMIT) {
                    elem = e;
                } else {
                    offsetY += 50;
                }
            }
            return elem;
        };
    
        // Convert dom element to css selector for later use
        var getCssSelector = function(el) {
            if (!(el instanceof Element)) 
                return;
            var path = [];
            while (el.nodeType === Node.ELEMENT_NODE) {
                var selector = el.nodeName.toLowerCase();
                if (el.id) {
                    selector += '#' + el.id;
                    path.unshift(selector);
                    break;
                } else {
                    var sib = el, nth = 1;
                    while (sib = sib.previousElementSibling) {
                        if (sib.nodeName.toLowerCase() == selector)
                           nth++;
                    }
                    if (nth != 1)
                        selector += ':nth-of-type('+nth+')';
                }
                path.unshift(selector);
                el = el.parentNode;
            }
            return path.join(' > ');    
        };
    
        // Send topmost element and its top offset to java
        var reportScrollPosition = function() {
            var elem = findSmallElementOnScreen();
            if (elem) {
                var selector = getCssSelector(elem);
                var offset = elem.getBoundingClientRect().top;
                WebScrollListener.onScrollPositionChange(selector, offset);
            }
        }
    
        // We will report scroll position every time when scroll position changes,
        // but timer will ensure that this doesn't happen more often than needed
        // (scroll event fires way too rapidly)
        var previousTimeout = undefined;
        window.addEventListener('scroll', function() {
            clearTimeout(previousTimeout);
            previousTimeout = setTimeout(reportScrollPosition, 200);
        });
    </script>
    

    If you run your app at this point, you should already see messages in logcat telling you that the new scroll position is received.

    Now we need to save webView state:

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        webView.saveState(outState);
        outState.putString("scrollElement", scrollListener.element);
        outState.putInt("scrollMargin", scrollListener.margin);
    }
    

    Then we read it in the onCreate (for Activity) or onCreateView (for fragment) method:

    if (savedInstanceState != null) {
        webView.restoreState(savedInstanceState);
        initialScrollElement = savedInstanceState.getString("scrollElement");
        initialScrollMargin = savedInstanceState.getInt("scrollMargin");
    }
    

    We also need to add WebViewClient to our webView and override onPageFinished method:

    @Override
    public void onPageFinished(final WebView view, String url) {
        if (initialScrollElement != null) {
            // It's very hard to detect when web page actually finished loading;
            // At the time onPageFinished is called, page might still not be parsed
            // Any javascript inside <script>...</script> tags might still not be executed;
            // Dom tree might still be incomplete;
            // So we are gonna use a combination of delays and checks to ensure
            // that scroll position is only restored after page has actually finished loading
            webView.postDelayed(new Runnable() {
                @Override
                public void run() {
                    String javascript = "(function ( selectorToRestore, positionToRestore ) {\n" +
                            "       var previousTop = 0;\n" +
                            "       var check = function() {\n" +
                            "           var elem = document.querySelector(selectorToRestore);\n" +
                            "           if (!elem) {\n" +
                            "               setTimeout(check, 100);\n" +
                            "               return;\n" +
                            "           }\n" +
                            "           var currentTop = elem.getBoundingClientRect().top;\n" +
                            "           if (currentTop !== previousTop) {\n" +
                            "               previousTop = currentTop;\n" +
                            "               setTimeout(check, 100);\n" +
                            "           } else {\n" +
                            "               window.scrollBy(0, currentTop - positionToRestore);\n" +
                            "           }\n" +
                            "       };\n" +
                            "       check();\n" +
                            "}('" + initialScrollElement + "', " + initialScrollMargin + "));";
    
                    webView.loadUrl("javascript:" + javascript);
                    initialScrollElement = null;
                }
            }, 300);
        }
    }
    

    This is it. After screen rotation, element that was at the top of your screen should now remain there.

    0 讨论(0)
提交回复
热议问题