在方向更改时保持WebView内容滚动位置

时间:2011-07-28 07:55:57

标签: android

Android 2.3+中的浏览器在维护内容的滚动位置方面做得很好。

我正在尝试为我在活动中显示但未成功的WebView实现相同的目标。

我已经测试了清单更改(android:configChanges =“orientation | keyboardHidden”)以避免旋转时的活动重新创建,但是由于不同的宽高比,滚动位置没有设置到我想要的位置。此外,这对我来说是的解决方案,因为我需要在纵向和横向上使用不同的布局。

我已经尝试保存WebView状态并恢复它,但这会导致内容再次显示在顶部。

此外,即使此时WebView的高度不为零,尝试使用onPageFinished滚动scrollTo也不起作用。

有什么想法吗?提前致谢。彼得。

部分解决方案:

我的同事设法通过JavaScript解决方案滚动工作。对于简单滚动到相同的垂直位置,WebView的“Y”滚动位置保存在onSaveInstanceState状态。然后将以下内容添加到onPageFinished

public void onPageFinished(final WebView view, final String url) {
    if (mScrollY > 0) {
        final StringBuilder sb = new StringBuilder("javascript:window.scrollTo(0, ");
        sb.append(mScrollY);
        sb.append("/ window.devicePixelRatio);");
        view.loadUrl(sb.toString());
    }
    super.onPageFinished(view, url);
}

当内容从开头跳到新的滚动位置时,你会得到轻微的闪烁,但几乎不会引起注意。下一步是尝试基于百分比的方法(基于高度的差异),并调查WebView保存并恢复其状态。

8 个答案:

答案 0 :(得分:40)

要在方向更改期间恢复WebView的当前位置,我担心您必须手动执行此操作。

我使用了这种方法:

  1. 计算WebView中滚动的实际百分比
  2. 将其保存在onRetainNonConfigurationInstance
  3. 重新创建WebView时恢复其位置
  4. 由于纵向和横向模式下WebView的宽度和高度不同,我使用百分比来表示用户滚动位置。

    一步一步:

    1)计算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)将其保存在onRetainNonConfigurationInstance

    在方向更改之前保存进度

    @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)重新创建WebView时恢复WebView的位置

    从方向更改数据

    获取进度
    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;
        }
    }
    

    要恢复当前位置,您必须等待重新加载页面( 如果您的页面需要很长时间才能加载,则此方法可能会出现问题

    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);
        }
    }
    

    在我的测试中,我遇到你需要在调用onPageFinished方法后稍等一下才能使滚动工作。 300毫秒应该没问题。此延迟使显示屏轻弹(首先在滚动0处显示,然后转到正确的位置)。

    也许还有其他更好的方法,但我不知道。

答案 1 :(得分:1)

onPageFinished可能无法调用,因为您没有重新加载页面,您只是更改方向,不确定是否会导致重新加载。

尝试在活动的onConfigurationChanged方法中使用scrollTo。

答案 2 :(得分:1)

方面更改很可能始终导致WebView中的当前位置不是之后滚动到的正确位置。您可以偷偷摸摸地确定WebView中最顶部的可见元素,并在更改方向后在源中的那个点植入anchor并将用户重定向到它...

答案 3 :(得分:0)

要计算WebView的正确百分比,重要的是还要计算 mWebView.getScale()。实际上, getScrollY()的返回值分别为 mWebView.getContentHeight()* mWebView.getScale()

答案 4 :(得分:0)

我们设法实现此目的的方式是在片段中对WebView进行本地引用。

/**
* WebView reference
*/
private WebView webView;

然后在setRetainInstance

trueonCreate
@Override
public void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setRetainInstance(true);

    // Argument loading settings
}

然后,当调用onCreateView时,返回WebView的现有本地实例,否则实例化设置内容和其他设置的新副本。他重新连接WebView以删除您可以在下面的else子句中看到的父级时的关键步骤。

@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
        final Bundle savedInstanceState) {

    final View view;
    if (webView == null) {

        view = inflater.inflate(R.layout.webview_dialog, container);

        webView = (WebView) view.findViewById(R.id.dialog_web_view);

        // Setup webview and content etc... 
    } else {
        ((ViewGroup) webView.getParent()).removeView(webView);
        view = webView;
    }

    return view;
}

答案 5 :(得分:0)

我使用了原帖中描述的部分解决方案,而是将javascript片段放在onPageStarted()中,而不是onPageFinished()。似乎没有跳跃为我工作。

我的用例略有不同:我尝试在用户刷新页面后保持水平位置。

但我已经能够使用您的解决方案,它对我来说非常有用:)

答案 6 :(得分:0)

在原始帖子中描述的部分解决方案中,如果更改以下代码,则可以改进解决方案

private float calculateProgression(WebView content) {
    float contentHeight = content.getContentHeight();
    float currentScrollPosition = content.getScrollY();
    float percentWebview = currentScrollPosition/ contentHeight;
    return percentWebview;
}

由于组件的顶部位置,当您使用content.getTop()时没有必要;因此它所做的是添加到组件相对于父组件所在的滚动Y位置的实际位置并向下运行内容。我希望我的解释清楚。

答案 7 :(得分:0)

为什么你应该在接受的答案上考虑这个答案:

接受的答案提供了体面和简单方式来保存滚动位置,但它远非完美。这种方法的问题在于,有时在旋转过程中,您甚至看不到旋转前在屏幕上看到的任何元素。旋转后,位于屏幕顶部的元素现在可以位于底部。通过滚动百分比保存位置不是很准确,在大型文档中,这种不准确性可能会增加。

所以这是另一种方法:它的方式更复杂,但它几乎可以保证您在旋转之前看到旋转后看到完全相同的元素。在我看来,这会带来更好的用户体验,特别是在大型文档上。

==

首先,我们将通过javascript跟踪当前滚动位置。这将使我们能够确切地知道当前位于屏幕顶部的元素以及它滚动了多少。

首先,确保为您的WebView启用了javascript:

webView.getSettings().setJavaScriptEnabled(true);

接下来,我们需要创建将接受来自javascript中的信息的java类:

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;
    }

}

然后我们将这个类添加到WebView:

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

现在我们需要将javascript代码插入到html页面中。此脚本将滚动数据发送到java(如果您是生成html,只需附加此脚本;否则,您可能需要通过document.write()调用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>

如果您此时运行应用程序,您应该已经在logcat中看到消息,告诉您已收到新的滚动位置。

现在我们需要保存webView状态:

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

然后我们在onCreate(for Activity)或onCreateView(for fragment)方法中读取它:

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

我们还需要将WebViewClient添加到我们的webView并覆盖onPageFinished方法:

@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);
    }
}

就是这样。屏幕旋转后,屏幕顶部的元素现在应该保留在那里。