Ведение содержимого прокрутки содержимого WebView при изменении ориентации
Браузеры в Android 2.3+ отлично справляются с сохранением прокрученной позиции содержимого при изменении ориентации.
Я пытаюсь добиться того же самого для WebView, который я показываю в рамках действия, но без успеха.
Я протестировал изменение манифеста (андроид: configChanges = "orientation | keyboardHidden" ), чтобы избежать активного отдыха на ротации, однако из-за разных пропорций положение прокрутки не устанавливается так, как я хочу. Кроме того, это не решение для меня, поскольку мне нужно иметь разные макеты в портретной и альбомной ориентации.
Я попытался сохранить состояние WebView и восстановить его, но это снова возвращает содержимое, отображаемое наверху.
Кроме того, попытка прокрутки в onPageFinished
с помощью scrollTo
не работает, даже если высота WebView в данный момент не равна нулю.
Любые идеи? Заранее спасибо. Питер.
Частичное решение:
Моему коллеге удалось прокрутить работу с помощью 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.
Ответы
Ответ 1
Чтобы восстановить текущую позицию WebView во время изменения ориентации, я боюсь, что вам придется делать это вручную.
Я использовал этот метод:
- Рассчитать фактический процент прокрутки в WebView
- Сохраните его в onRetainNonConfigurationInstance
- Восстановить положение 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, чтобы заставить свиток работать. 300 мс должно быть нормально. Эта задержка заставит дисплей отображаться (сначала отобразится при прокрутке 0, затем перейдите в правильное положение).
Возможно, есть и другой лучший способ сделать это, но я не знаю.
Ответ 2
onPageFinished не может быть вызван, потому что вы не перезагружаете страницу, вы просто меняете ориентацию, не уверен, что это вызывает перезагрузку или нет.
Попробуйте использовать scrollTo в методе onConfigurationChanged вашей активности.
Ответ 3
Изменение формата, скорее всего, всегда приведет к тому, что текущее местоположение в вашем веб-браузере не будет подходящим местом для прокрутки. Вы могли бы подкрасться и определить самый верхний наиболее заметный элемент в WebView и после имплантации изменения ориентации anchor в этот момент в источнике и перенаправить пользователь к нему...
Ответ 4
чтобы вычислить правильный процент WebView, важно также подсчитать mWebView.getScale(). Фактически, возвращаемое значение getScrollY() соответствует mWebView.getContentHeight() * mWebView.getScale().
Ответ 5
Как нам удалось достичь этого, чтобы иметь локальную ссылку на WebView
в наших фрагментах.
/**
* WebView reference
*/
private WebView webView;
Затем setRetainInstance
до true
в onCreate
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
// Argument loading settings
}
Затем, когда вызывается onCreateView
, верните существующий локальный экземпляр WebView
, в противном случае создайте новую копию, настроив содержимое и другие настройки. Ключевым этапом он является то, что он повторно присоединяет WebView
, чтобы удалить родительский элемент, который вы можете увидеть в следующем разделе ниже.
@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;
}
Ответ 6
Я использовал частичное решение, описанное в исходном сообщении, но вместо этого разместил фрагмент javascript внутри 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. Это позволит нам точно узнать, какой элемент находится в верхней части экрана и сколько прокручивается.
Во-первых, убедитесь, что 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-страницу. Этот script отправит данные прокрутки в java (если вы являетесь генератором html, просто добавьте этот script, в противном случае вам может потребоваться позвонить 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 (для фрагмента):
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 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);
}
}
Это он. После поворота экрана элемент, который был наверху вашего экрана, должен оставаться там.