在WebView中处理屏幕旋转

13
我的 Web 应用在 Chrome 中运行良好,可以很好地处理配置更改(例如屏幕旋转)。一切都完美保留。
当将我的 Web 应用加载到我的 Android 应用程序中的 WebView 中时,Web 应用在屏幕方向更改时会丢失状态。它只能部分地保存状态,即它会保留 <input> 表单元素的数据,但是所有 JavaScript 变量和 DOM 操作都会丢失。
我希望我的 WebView 能像 Chrome 一样运行,即完全保留状态,包括任何 JavaScript 变量。需要注意的是,虽然 Chrome 和 WebView 派生自相同的代码库,但 Chrome 不会在内部使用 WebView。
当屏幕方向改变时,Activity(以及任何可能的Fragments)会被销毁,然后重新创建。 WebView继承自View并覆盖了onSaveInstanceStateonRestoreInstanceState方法来处理配置更改,因此它会自动保存和恢复任何HTML表单元素的内容以及后退/前进导航历史状态。但是JavaScript变量和DOM的状态不会被保存和恢复。

提出的解决方案

已经有一些提出的解决方案。所有这些解决方案都无法正常工作,只能部分保留状态或以其他方式不够优化。

为WebView分配一个id

WebView继承自View,后者拥有setId方法,该方法也可以在布局XML文件中使用android:id属性声明<WebView>元素。这是必要的,以便保存和恢复状态,但状态仅部分恢复。这会恢复表单输入元素,但不会恢复JavaScript变量和DOM的状态。 onRetainNonConfigurationInstancegetLastNonConfigurationInstance自API 13起已被弃用
强制屏幕方向
一个 Activity 可以通过在 AndroidManifest.xml 文件中为 <Activity> 元素设置 screenOrientation 属性或通过 setRequestedOrientation 方法来强制其屏幕方向。这是不希望出现的,因为它破坏了屏幕旋转的预期。这也仅涉及屏幕方向的更改,而不涉及其他配置更改。
保留片段实例无效。在片段上调用 setRetainInstance 方法确实会保留该片段(它不会被销毁),因此片段的所有实例变量都得以保存,但是它确实会销毁片段的视图,因此 WebView 也会被销毁。
手动处理配置更改。

AndroidManifest.xml文件中,可以为Activity声明configChanges属性,如android:configChanges="orientation|screenSize"以处理配置更改并防止它们。这种方法可以避免活动被销毁,因此WebView及其内容完全保留。但是,这已经不被鼓励使用,并且被认为只是最后的解决方案,因为它可能导致应用程序出现微妙的问题和错误。当设置了configChanges属性时,会调用onConfigurationChanged方法。

MutableContextWrapper

我听说可以使用MutableContextWrapper,但我还没有评估过这种方法。

saveState()和restoreState()

WebViewsaveStaterestoreState方法。根据文档,saveState方法不再存储WebView的显示数据,不知道具体含义。无论如何,这些方法似乎不能完全保留WebView的状态。

WebViewFragment

WebViewFragment只是一个方便的片段,为您包装了WebView,使您可以轻松地开始使用少量的样板代码,就像ListFragment一样。它不会进行任何额外的状态保存以完全保留状态。

此类在API级别28中已弃用。

问题

有没有真正的解决方案来解决WebView在配置更改(如屏幕旋转)时被销毁并且失去其状态的问题?

一种可以完全保留所有状态的解决方案,包括JavaScript变量和DOM操作。一种干净的解决方案,不是基于hack或已弃用的方法构建的。

4个回答

9
经过研究和尝试不同的方法,我发现了我认为是最佳解决方案。
它使用 setRetainInstance 保留片段实例,以及 addViewremoveViewonCreateViewonDestroyView 方法中来防止 WebView 被销毁。

MainActivity.java

public class MainActivity extends Activity {
    private static final String TAG_FRAGMENT = "webView";

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        WebViewFragment fragment = (WebViewFragment) getFragmentManager().findFragmentByTag(TAG_FRAGMENT);
        if (fragment == null) {
            fragment = new WebViewFragment();
        }

        getFragmentManager().beginTransaction().replace(android.R.id.content, fragment, TAG_FRAGMENT).commit();
    }
}

WebViewFragment.java

public class WebViewFragment extends Fragment {
    private WebView mWebView;

    public WebViewFragment() {
        setRetainInstance(true);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.fragment_webview, container, false);
        LinearLayout layout = (LinearLayout)v.findViewById(R.id.linearLayout);
        if (mWebView == null) {
            mWebView = new WebView(getActivity());
            setupWebView();
        }
        layout.removeAllViews();
        layout.addView(mWebView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

        return v;
    }

    @Override
    public void onDestroyView() {
        if (getRetainInstance() && mWebView.getParent() instanceof ViewGroup) {
            ((ViewGroup) mWebView.getParent()).removeView(mWebView);
        }
        super.onDestroyView();
    }

    private void setupWebView() {
        mWebView.loadUrl("https:///www.example.com/");
    }
}

1
这个问题的总结很好。随着我正在处理的应用程序回到嵌入式 Web 视图,我又遇到了这个问题。 - lanakin
2
保留 new WebView(getActivity()) 会在配置更改时泄漏 Activity 上下文(因为 WebView 仍然持有对初始 Activity 的引用)。此外,在配置更改后打开某些表单元素(如下拉列表)时,WebView 可能会崩溃。 - Marten
我们可以使用new WebView(applicationContext)来实现吗?另外,setRetainInstance已经被弃用了。我们可以使用ViewModel来保存WebView吗? - Jonas
@Jonas 很好的问题。我不知道,我已经好几年没有做Android编程了,如果你找到了,请告诉我。 - Fred

1

你可以做的简单事情是这样的:

WebView webView;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View v = inflater.inflate(R.layout.fragment_webview, container, false);

    webView = v.findViewById(R.id.webView);

    if(savedInstanceState != null){
        webView.restoreState(savedInstanceState);
    } else {
        loadUrl();
    }
    return v;
}

private void loadUrl(){
    webView.loadUrl("someUrlYouWant");
}

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    webView.saveState(outState);
}

注意: 没有尝试过这段代码,只是写下来,但认为它应该可以工作。


1
以下清单代码声明了一个处理屏幕方向和键盘可用性更改的活动: 这段代码:
<activity android:name=".MyActivity">

变成:

<activity android:name=".MyActivity"
          android:configChanges="orientation|screenSize|keyboardHidden"
          android:label="@string/app_name">

在这里你只需要添加:screenSize,然后它就可以正常工作了。

参考链接:https://developer.android.com/guide/topics/resources/runtime-changes.html#RetainingAnObject


0

我建议您重新渲染整个页面。我搜索了一段时间,但没有找到一个干净的现成解决方案。所有的解决方案都表明您应该让网页在方向改变时重新渲染。

但是,如果您真的需要保留JS变量,您可以模仿saveStaterestoreState的做法。这些方法理想情况下是使用活动的onSaveInstanceState()onRestoreInstanceState()保存和恢复WebView中的内容。由于潜在的内存泄漏问题,这些方法不会执行它们所做的操作。

所以,你需要做的就是创建自己的WebView(MyWebView extends WebView)。在这个WebView中有两个方法:saveVariableState()restoreVariableState()。在你的saveVariableState()方法中,只需将你想要保存的每个变量保存到Bundle对象中并返回它 (public Bundle saveVariableState(){})。现在,在ActivityonSaveInstanceState()中调用MyWebView.saveVariableState并保存它返回的Bundle。一旦屏幕方向改变,你可以从onRestoreInstanceState或者onCreate方法中获得Bundle,然后通过构造函数或者restoreVariableState方法将其传递给MyWebView

这不是一种黑客行为,而是保存其他视图数据的正常方式。对于WebView,你将保存JS变量而不是视图数据。


这对于全局命名空间中的一个或两个变量可能有效,但不适用于具有修改的私有变量和无法从外部访问的创建对象以及DOM操作。 - Fred

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接