【问题标题】:Maintain WebView content scroll position on orientation change在方向更改时保持 WebView 内容滚动位置
【发布时间】:2011-10-14 21:42:34
【问题描述】:

Android 2.3+ 中的浏览器在保持内容在方向改变时的滚动位置方面做得很好。

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

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

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

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

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

部分解决方案:

我的同事设法通过 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 保存和恢复其状态。

【问题讨论】:

    标签: android


    【解决方案1】:

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

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

    【讨论】:

    • 我在方向更改时收到 onPageFinished,但调用 WebView::scrollTo 无效。
    • 此外,也许此解决方案适用于某些人,但我希望活动在方向更改时重新启动。
    【解决方案2】:

    外观更改很可能总是会导致 WebView 中的当前位置不是以后滚动到的正确位置。您可能会偷偷摸摸地确定 WebView 中最可见的元素,并在方向更改后在源中的该点植入 anchor 并将用户重定向到它...

    【讨论】:

    • 谢谢,我不禁觉得应该有一个简单/内置的解决方案来解决这个问题。
    【解决方案3】:

    要在方向更改期间恢复 WebView 的当前位置,恐怕您必须手动完成。

    我用了这个方法:

    1. 计算 WebView 中的实际滚动百分比
    2. 保存在 onRetainNonConfigurationInstance 中
    3. 重新创建时恢复 WebView 的位置

    因为纵向和横向模式下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 的位置

    从方向变化数据中获取进度

    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 方法后稍等片刻才能使滚动工作。 300ms应该没问题。此延迟使显示闪烁(首先显示在滚动 0 处,然后转到正确的位置)。

    也许还有其他更好的方法可以做到这一点,但我不知道。

    【讨论】:

    • @ol_v-er 感谢您提供信息。我有兴趣尝试按照您的描述引入延迟以查看 scrollTo 工作(我很惊讶需要延迟,但无论如何......)。终于通过 javascript 解决方案滚动工作(我将很快发布)我有了百分比滚动的想法,我还没有尝试看到结果。
    • 其实当onPageFinished回调被调用时,WebView才开始绘制。延迟是为了确保 WebView 已完成绘制(并且具有正确的高度)。期待您的 javascript 解决方案。
    • 谢谢,添加了我的 javascript 解决方案。
    • 好的,谢谢。我认为这两种解决方案的混合会很棒
    • 为了获得最小延迟并尽快执行滚动,使用WebView.setPictureListener() 是一个不错的选择(尽管它已被弃用,即使在果冻豆上它仍然可以工作)。在 PictureListeneronNewPicture() 方法中,您可以检查内容是否足够高 (WebView.getContentHeight()) 以滚动到请求的位置,如果可能的话这样做。你应该记住你已经滚动了,所以你只做了一次:if (positionY > 0 && positionY <= mWebView.getContentHeight) { setScrollY(positionY); positionY = 0; }
    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

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

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

      然后setRetainInstancetrueonCreate

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

      【讨论】:

      • 第二次返回包含 webview 的片段时,我得到一个空指针。片段 1 使用 webview 加载。我滚动到一个位置,移动到片段 2,回到片段 1。一切都很好。但是,然后我移动到片段 2,然后回到片段 1,应用程序因空指针而崩溃。有什么想法吗?
      • 此解决方法会导致内存泄漏,因为您的 WebView 在附加到新 Activity 时将保留原始 Activity 的上下文。避免这种情况。
      【解决方案6】:

      我使用了原帖中描述的部分解决方案,而是将 javascript sn-p 放在 onPageStarted() 中,而不是 onPageFinished()。似乎对我有用,没有跳跃。

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

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

      【讨论】:

        【解决方案7】:

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

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

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

        【讨论】:

          【解决方案8】:

          为什么你应该考虑这个答案而不是接受的答案:

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

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

          ======

          首先,我们将通过 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");
          

          现在我们需要在 html 页面中插入 javascript 代码。该脚本会将滚动数据发送到java(如果您是生成html,只需附加此脚本;否则,您可能需要通过webView.loadUrl("javascript:document.write(" + script + ")"); 调用document.write()):

          <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(对于Activity)或者onCreateView(对于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);
              }
          }
          

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-01-15
            • 1970-01-01
            • 2011-12-02
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多