2011-07-28 59 views
24

Android 2.3+中的瀏覽器在保持已更改方向上內容的滾動位置方面做得很好。在定位更改上維護WebView內容滾動位置

我試圖實現一個WebView的同樣的事情,我在活動中顯示但沒有成功。

我已經測試了清單更改(android:configChanges =「orientation | keyboardHidden」),以避免活動在旋轉時重新創建,但由於縱橫比不同,滾動位置未設置爲我想要的位置。此外,這是而不是一個解決方案,因爲我需要在縱向和橫向上有不同的佈局。

我試着保存WebView狀態並恢復它,但是這會導致再次顯示在頂部的內容。

而且嘗試使用scrollTo即使的WebView的高度是在這一點上非零不起作用在onPageFinished滾動。

任何想法?提前致謝。彼得。

部分解決:

我的同事設法通過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保存並恢復其狀態。

回答

37

要恢復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)恢復位置當它重新

從方位變化數據

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

取得的進展要恢復當前的位置,你將不得不等待頁面重新加載( 這種方法是有問題的,如果你的頁面採取的WebView很長一段時間來加載)

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處顯示,然後轉到正確的位置)。

也許還有其他更好的方法來做到這一點,但我不知道。

+0

@ ol_v-er感謝您的信息。我有興趣嘗試介紹延遲,因爲你看到'scrollTo'工作(我很驚訝,需要延遲,但無論如何...)。終於通過JavaScript解決方案滾動工作(我將很快發佈),我有了滾動百分比的想法,我還沒有試圖去看看結果。 – PJL

+0

事實上,當onPageFinished回調被調用時,WebView就開始繪製。延遲是確保WebView已完成繪圖(並具有正確的高度)。期待您的JavaScript解決方案。 –

+0

謝謝,添加了我的JavaScript解決方案。 – PJL

1

onPageFinished可能不會被調用,因爲你不重新加載頁面,你只是改變方向,不知道這是否導致重新加載。

嘗試在您的活動的onConfigurationChanged方法中使用scrollTo。

+0

我在方向更改中獲得'onPageFinished',但調用'WebView :: scrollTo'沒有效果。 – PJL

+0

此外,也許這種解決方案適用於某些人,但是我希望活動在方向更改時重新啓動。 – PJL

1

方面變化很可能總是會導致WebView中的當前位置不是滾動到之後的正確位置。您可能會鬼鬼祟祟,並確定WebView中最頂層的可見元素,並且在方向更改之後在源中植入一個anchor並將用戶重定向到它...

+0

謝謝,我不禁覺得應該有一個簡單/內置的解決方案來解決這個問題。 – PJL

0

要計算出正確的WebView百分比,重要的是要計算mWebView.getScale()也是很重要的。實際上,返回值爲getScrollY()分別爲mWebView.getContentHeight()* mWebView.getScale()

0

我們設法達到這個目的的方式是在我們的片段中對局部引用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; 
} 
+0

我第二次返回到包含webview的片段時,我得到一個空指針。片段1用webview加載。我滾動到一個位置,移動到片段2,移回片段1.一切都很好。然而,我然後移動到片段2,然後回到片段1,並且應用程序崩潰與空指針。有任何想法嗎? – piratemurray

+0

此替代方法會導致內存泄漏,因爲您的WebView將在附加到新活動時保留原始活動的上下文。避免這種情況。 – BladeCoder

0

我用在原來的職位描述的部分解決方案,而是把裏面onPageStarted()中的JavaScript代碼段,而不是onPageFinished()。似乎爲我工作,沒有跳躍。

我的用例略有不同:我試圖在用戶刷新頁面後保持水平位置。

但我已經能夠使用您的解決方案,它完美對我來說:)

0

在原來的職位描述的部分解決方案,可以提高解決方案,如果更改下面的代碼

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

由於組件的頂部位置,當您使用content.getTop()不是必需的;所以它所做的就是添加到組件所在的滾動Y位置的實際位置(相對於它的父節點並沿着內容運行)。我希望我的解釋清楚。

0

爲什麼你應該考慮這個答案比接受的答案:

接受的答案提供體面和簡單的方式來保存滾動位置,但它遠非完美。該方法的問題在於,有時在旋轉之前,甚至不會看到旋轉前在屏幕上看到的任何元素。旋轉後,位於屏幕頂部的元素現在可以位於底部。通過滾動百分比來保存位置不是很準確,而且在大型文檔中,這種不準確性會加起來。

所以這裏是另一種方法:它的方式更復雜,但它幾乎可以保證你會看到旋轉之前看到完全相同的元素。在我看來,這會帶來更好的用戶體驗,特別是在大型文檔上。

======

首先,我們將跟蹤通過JavaScript當前滾動位置。這將允許我們確切地知道哪個元素當前位於屏幕的頂部,以及它滾動了多少。

首先,確保您的JavaScript您的WebView啓用:

webView.getSettings().setJavaScriptEnabled(true); 

接下來,我們需要創建一個Java類將在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; 
    } 

} 

然後我們加入這個類到WebView:

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

現在我們需要插入javascript代碼到html頁面。該腳本將滾動數據發送到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讀它(對活動)或onCreateView(對於片段)方法:

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

我們還需要添加WebViewClient我們的web視圖,並覆蓋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); 
    } 
} 

就是這樣。屏幕旋轉後,位於屏幕頂部的元素現在應保留在那裏。