之前在工作中,遇到了一个比较奇葩的问题。

就是在webview通过QQ空间分享一篇文章成功后,点击网页“返回继续浏览”无响应,无法返回上一页面。

经排查,是因为网页实现“返回继续浏览”的方法是window.close,window.close的作用是关闭当前窗口页,而当webivew在只有一个窗口页时,当前页面不能关闭,导致方法无效。

当然有同学说可以通过webview的onCreateWindow()方法来创新一个新的窗口页啊,但调用onCreateWindow就要再对新建的webview进行一遍,也就是每新开一个新窗口就得嵌套一次webview初始化,除了代码量惊人,还得清楚最多嵌套多少次。这个对于性能和可读性来说,都是灾难性的。

通过找一些资料和自己实践,就利用一个ArrayList存储webview的方法来实现多窗口页的功能。虽然方法用的有点low,但是起码可行...心痛自己太渣一秒钟。

-----------------------------------------------------------------------------------------

实现的步骤:

1. 创建一个注解代替枚举类

/*
* 每一个枚举值都是一个单例对象,在使用它时会增加额外的内存消耗,所以枚举相比与 Integer 和 String 会占用更多的内存
* 较多的使用 Enum 会增加 DEX 文件的大小,会造成运行时更多的IO开销,使我们的应用需要更多的空间
* 特别是分dex多的大型APP,枚举的初始化很容易导致ANR
* 所以使用注解来代替枚举
*/

public class WebEvent {
    public static final int OVERRIDE_URL_RESOLVE = 11;
    public static final int INTERCEPT_REQUEST_RESOLVE = 12;
    public static final int RECEIVED_ERROR = 44;

    public static final int WEB_NETWORK_ERROR_OPEN_WIFI = 50;
    public static final int WEB_OPEN_EXTERNAL_LINK = 51;

    @IntDef({OVERRIDE_URL_RESOLVE,
            INTERCEPT_REQUEST_RESOLVE,
            RECEIVED_ERROR,
            WEB_NETWORK_ERROR_OPEN_WIFI,
            WEB_OPEN_EXTERNAL_LINK})
    public @interface webEvent {
    }
}

2. 创建一个webView公共类,继承WebView:

    当执行onCreateWindow()方法时,就new一个WebView的对象添加到List中去。

public class WebViewTest extends WebView {

    private static final String _TAG = WebViewTest.class.getSimpleName();
    private static final String SCHEME_HTTP = "http";
    private static final String SCHEME_HTTPS = "https";

    /*
    * 定义IEvnentPublish接口,通过接口把对应的webview操作放到主页面执行
    */
    public interface IEventPublish {
        void onOpen(String url);
        boolean onCreateWindow(final WebView view, boolean dialog, boolean userGesture, Message resultMsg);
        void onCloseWindow(WebView window);
        void onClose(String url);
        void onReceivedError(String url);
        void onProcessing(boolean processing);
        void onProcessChanged(int processValue);
        void onEventPush(@WebEvent.webEvent int event, String url);
        void onDownload(@NonNull DownloadElement element);
        void onOpenFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture);
    }
    private IEventPublish eventPublish;

    /*
    *  webview子类的构造函数
    */
    public WebViewTest(Context context) {
        super(context);
    }

    public WebViewTest(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public WebViewTest(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /*
    *  设置webview各项属性
    */
    public void initSettings() {
        WebSettings settings = this.getSettings();
        //设置WebView中加载页面字体变焦百分比,默认100,整型数
        settings.setTextZoom(100);
        //设置在WebView内部是否允许访问文件,默认允许访问。
        settings.setAllowFileAccess(true);
        //设置WebView是否允许执行JavaScript脚本,默认false,不允许
        settings.setJavaScriptEnabled(true);
        //设置WebView是否加载图片资源,默认true,自动加载图片
        settings.setLoadsImagesAutomatically(true);
        //设置是否开启定位功能,默认true,开启定位
        settings.setGeolocationEnabled(true);
        //设置WebView是否使用viewport,当该属性被设置为false时,加载页面的宽度总是适应WebView控件宽度;当被设置为true,当前页面包含viewport属性标签,在标签中指定宽度值生效,如果页面不包含viewport标签,无法提供一个宽度值,这个时候该方法将被使用。
        settings.setUseWideViewPort(true);
        //设置WebView是否使用预览模式加载界面。
        settings.setLoadWithOverviewMode(true);
        //设置脚本是否允许自动打开弹窗,默认false,不允许
        settings.setJavaScriptCanOpenWindowsAutomatically(true);
        //设置WebView是否支持使用屏幕控件或手势进行缩放,默认是true,支持缩放。
        settings.setSupportZoom(true);
        //设置WebView是否使用其内置的变焦机制,该机制集合屏幕缩放控件使用,默认是false,不使用内置变焦机制。
        settings.setBuiltInZoomControls(true);
        //设置WebView使用内置缩放机制时,是否展现在屏幕缩放控件上,默认true,展现在控件上。
        settings.setDisplayZoomControls(false);
        //设置是否开启DOM存储API权限,默认false,未开启,设置为true,WebView能够使用DOM storage API
        settings.setDomStorageEnabled(true);
        //重写缓存被使用到的方法,该方法基于Navigation Type,加载普通的页面,将会检查缓存同时重新验证是否需要加载,如果不需要重新加载,将直接从缓存读取数据,允许客户端通过指定LOAD_DEFAULT、LOAD_CACHE_ELSE_NETWORK、LOAD_NO_CACHE、LOAD_CACHE_ONLY其中之一重写该行为方法,默认值LOAD_DEFAULT
        settings.setCacheMode(WebSettings.LOAD_DEFAULT);
        //设置WebView加载页面文本内容的编码,默认“UTF-8”。
        settings.setDefaultTextEncodingName("UTF-8");
        //设置WebView底层的布局算法,参考LayoutAlgorithm#NARROW_COLUMNS,将会重新生成WebView布局
        settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
        //设置WebView是否支持多屏窗口,参考WebChromeClient#onCreateWindow,默认false,不支持。(设置为true,onCreateWindow才会生效)
        settings.setSupportMultipleWindows(true);
        //设置是否开启数据库存储API权限,默认false,未开启
        settings.setDatabaseEnabled(true);

        String dir = VocApplication.getVocApplication().getDir("database", Context.MODE_PRIVATE).getPath();
        //设置WebView保存地理位置信息数据路径,指定的路径Application具备写入权限
        settings.setGeolocationDatabasePath(dir);
        //设置当一个安全站点企图加载来自一个不安全站点资源时WebView的行为
		settings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
       
        垂直不显示滚动条
        this.setVerticalScrollBarEnabled(false);

        this.setWebViewClient(webViewClient);
        this.setWebChromeClient(webChromeClient);
        this.setDownloadListener(downloadListener);
    }

    /*
    * 传入主页面的IEventPublish接口
    */
    public void setEventCallback(@NonNull IEventPublish publish) {
        eventPublish = publish;
    }

    /*
    * 创建webViewClient的对象,WebViewClient可以拿到WebView在访问网络各个阶段的回调,包括加载前后,失败等
    */
    public final WebViewClient webViewClient = new WebViewClient() {

        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon) {
            Log.d(_TAG, "onPageStarted url=" + url);
            if (eventPublish != null) {
                eventPublish.onProcessing(true);
            }
        }

        @Override
        public void onPageFinished(WebView view, String url) {
            if (url != null) {
                Log.d(_TAG, "onPageFinished url = " + url);
            }
            if (eventPublish != null) {
                eventPublish.onProcessing(false);
            }
        }

        //拦截 url 跳转,在里边添加点击链接跳转或者操作 
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            return super.shouldOverrideUrlLoading(view, url);
        }

        //在每一次请求资源时,都会通过这个函数来回调 
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
            Log.d(_TAG, "shouldInterceptRequest url = " + url);    
            eventPublish.onEventPush(INTERCEPT_REQUEST_RESOLVE, url);
            return super.shouldInterceptRequest(view, url);
        }

        //加载错误的时候会回调,在其中可做错误处理,比如再请求加载一次,或者提示404的错误页面
        public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
            Log.d(_TAG, "errorCode:" + errorCode + " description:" + description + " failingUrl:" + failingUrl);
            if (errorCode == -2) {
                view.stopLoading();
                if (view.canGoBack()) {
                    view.goBack();
                }

                if (eventPublish != null) {
                    eventPublish.onEventPush(RECEIVED_ERROR, failingUrl);
                }
            }
        }

        //当接收到https错误时,会回调此函数,在其中可以做错误处理
        @Override
        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
            handler.proceed();//表示忽略错误继续加载,SslErrorHandler.cancel()表示取消加载。在onReceivedSslError的默认实现中是使用的
        }
    };

    /*
    * 可以在其中加载进度条,获取链接的标题等方法。
    */
    public final WebChromeClient webChromeClient = new WebChromeClient() {

        //通知主程序web内容尝试使用定位API,但是没有相关的权限。主程序需要调用调用指定的定位权限申请的回调。更多说明查看GeolocationPermissions相关API。
        @Override
        public void onGeolocationPermissionsShowPrompt(final String origin, final GeolocationPermissions.Callback callback) {
            callback.invoke(origin, true, false);
            super.onGeolocationPermissionsShowPrompt(origin, callback);
        }

        //请求主程序创建一个新的Window,如果主程序接收请求,返回true并创建一个新的WebView来装载Window,然后添加到View中,发送带有创建的WebView作为参数的resultMsg的给Target。如果主程序拒绝接收请求,则方法返回false。默认不做任何处理,返回false
        @Override
        public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
            Log.d(_TAG, "onCreateWindow");
            if (eventPublish != null) {
                return eventPublish.onCreateWindow(view, isDialog, isUserGesture, resultMsg);
            } else {
                return true;
            }
        }

        //通知主程序关闭WebView,并从View中移除,WebCore停止任何的进行中的加载和JS功能。
        @Override
        public void onCloseWindow(WebView window) {
            Log.d(_TAG, "onCloseWindow");
            super.onCloseWindow(window);
            if (eventPublish != null) {
                eventPublish.onCloseWindow(window);
            }
        }

        //通知程序当前页面加载进度
        @Override
        public void onProgressChanged(WebView view, int newProgress) {
            Log.d(_TAG, "onProgressChanged -> " + newProgress);
            super.onProgressChanged(view, newProgress);
            if (eventPublish != null) {
                eventPublish.onProcessChanged(newProgress);
            }
        }

        //告诉客户端显示离开当前页面的导航提示框。如果返回true,由客户端处理确认提示框,调用合适的JsResult方法。如果返回false,则返回默认值true给javascript接受离开当前页面的导航。默认:false。JsResult设置false,当前页面取消导航提示,否则离开当前页面。
        @Override
        public boolean onJsBeforeUnload(WebView view, String url, String message, final JsResult result) {
            Log.d(_TAG, "onJsBeforeUnload url:" + url + " message:" + message + " result:" + result);
            return super.onJsBeforeUnload(view, url, message, result);
        }

        // For Android 3.0+ webview打开手机相册
        public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
            openFileChooser(uploadMsg, "", "filesystem");
        }

        // For Android < 3.0
        public void openFileChooser(ValueCallback<Uri> uploadMsg) {
            openFileChooser(uploadMsg, "");
        }

        // For Android  > 4.1.1
        public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
            if (eventPublish != null) {
                eventPublish.onOpenFileChooser(uploadMsg, acceptType, capture);
            }
        }

        // Android 4.4, 4.4.1, 4.4.2
        // openFileChooser function is not called on Android 4.4, 4.4.1, 4.4.2

        // Android > 5.0.1
        public boolean onShowFileChooser(
                WebView WebViewTest, ValueCallback<Uri[]> filePathCallback,
                FileChooserParams fileChooserParams) {
            String acceptTypes[] = fileChooserParams.getAcceptTypes();

            String acceptType = "";
            for (String acceptType1 : acceptTypes) {
                if (acceptType1 != null && acceptType1.length() != 0)
                    acceptType += acceptType1 + ";";
            }
            if (acceptType.length() == 0)
                acceptType = "*/*";

            final ValueCallback<Uri[]> finalFilePathCallback = filePathCallback;

            ValueCallback<Uri> vc = new ValueCallback<Uri>() {

                @Override
                public void onReceiveValue(Uri value) {
                    Uri[] result;
                    if (value != null)
                        result = new Uri[]{value};
                    else
                        result = null;

                    finalFilePathCallback.onReceiveValue(result);
                }
            };

            openFileChooser(vc, acceptType, "filesystem");

            return true;
        }
    };

    @Override
    public ActionMode startActionMode(ActionMode.Callback callback, int type) {
        return null;
    }

    @Override
    public ActionMode startActionMode(ActionMode.Callback callback) {
        return null;
    }

    //实现webview的下载文件功能
    public final DownloadListener downloadListener = new DownloadListener() {
        @Override
        public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
            String fileName;
            if (contentDisposition != null && contentDisposition.contains("filename")) {
                fileName = contentDisposition.substring(contentDisposition.indexOf("filename") + 9, contentDisposition.length());
            } else {
                fileName = url.substring(url.lastIndexOf("/") + 1, url.length());
            }

            String suffix = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length());
            if (mimetype == null || mimetype.equals("")) {
                mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix);
            }

            if (eventPublish != null) {
                eventPublish.onDownload(new DownloadElement(url, fileName, mimetype));
            }
        }
    };
}

3. 在主界面实现webviewList的操作

/*
* webview的viewmodel,对progressBar,加载进度、显示webview的数据绑定。
*/
public class WebViewTestModel extends BaseObservable {
    public final ObservableInt progress = new ObservableInt(0);
    public final ObservableBoolean progressing = new ObservableBoolean(false);
    public final ObservableBoolean webViewVisible = new ObservableBoolean(true);

    public WebViewTestModel() {
    }
}

public class TestFragment extends Fragment {
    private static final String TAG = TestFragment.class.getSimpleName();

    private static final int DOCUMENTS_UI_POLICY_SEC = 1;
    public static final int REQUEST_WEBVIEW_GET_PHOTO = 3001;
    private static final String DOCUMENTS_UI_POLICY = "DocumentsUIPolicy";
	
    private View mRootView;
	private FrameLayout mWebViewContainer;
	private WebViewTestActivity mActivity = null;
	private FragmentWebViewTestBinding mWebBinding;
    
    private WebTestView mWebTestView;
    private WebViewTestModel mWebTestViewModel;
    private ArrayList<WebTestView> mWebViewList = new ArrayList<>();

	/*
	* 初始化webview的viewmodel,通过generateWebView()创建WebTestView的对象并赋予mWebTestView
	*/
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
        mWebBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_test, container, false);
        mRootView = mWebBinding.getRoot(); 
		
        mWebTestViewModel = new WebViewTestModel();
        mWebBinding.setClubViewModel(mWebTestViewModel);
		
        mWebViewContainer = mRootView.findViewById(R.id.webViewContainer);
		
        generateWebView();
        mWebTestView.loadUrl("www.xxxx.com");
		
        return mRootView;
    }
 
    /*
	* 实现IEventPublish接口的方法
	*/
    private WebTestView.IEventPublish eventPublish = new WebTestView.IEventPublish() {
        @Override
        public void onOpen(String url) {
            Log.d(TAG, "onOpen");
        }

        @Override
        public void onClose(String url) {
            Log.d(TAG, "onClose");
        }

        //每当调用到onCreateWindow函数时,就会调用generateWebView来生成一个webview对象并添加到List中
        @Override
        public boolean onCreateWindow(WebView view, boolean dialog, boolean userGesture, Message resultMsg) {
            if (resultMsg != null && resultMsg.obj instanceof WebView.WebViewTransport) {
                generateWebView();
                WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
                transport.setWebView(mWebTestView);
                resultMsg.sendToTarget();
                return true;
            }

            return false;
        }

        @Override
        public void onCloseWindow(WebView window) {
            closeWebView();
        }

        @Override
        public void onReceivedError(String url) {
            Log.d(TAG, "onReceivedError");
        }

        /*向与viewmodel绑定的progressBar传入进度数据*/
        @Override
        public void onProcessing(boolean processing) {
            Log.d(TAG, "onProcessing");
            mWebTestViewModel.progressing.set(processing);
        }

        @Override
        public void onProcessChanged(int processValue) {
            Log.d(TAG, "onProcessChanged");
            mWebTestViewModel.progress.set(processValue);
        }

        @Override
        public void onEventPush(int event, String url) {
            Log.d(TAG, "onEventPush");
            handleEvent(event, url);
        }

        @Override
        public void onDownload(@NonNull DownloadElement element) {
            Log.d(TAG, "onDownload");
           /*进行download操作*/
        }

        @Override
        public void onOpenFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
            Log.d(TAG, "onOpenFileChooser");
            final String imageMimeType = "image/*";
			
            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)
                    && mContext.getExternalCacheDir() != null) {
                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
                intent.addCategory(Intent.CATEGORY_OPENABLE);
                intent.putExtra(DOCUMENTS_UI_POLICY, DOCUMENTS_UI_POLICY_SEC);
                intent.setType(imageMimeType);
                startActivityForResult(intent, REQUEST_WEBVIEW_GET_PHOTO);
            }
        }
    };

	/*
	* 创建新的webview对象,设置属性和接口后添加到list
	* List最多只能有10个webview,超过后就把list后面的5个webview前移并覆盖前面的5个
	*/
    private final int mMaxWebViewCount = 10;
    private void generateWebView() {
        if (mWebViewList.size() >= mMaxWebViewCount) {
            WebTestView webView;
            for (int i = mMaxWebViewCount - 1; i >= 5; i--) {
                webView = mWebViewList.get(i);
                mWebViewList.set(i - 5, webView);
                mWebViewList.remove(i);
            }
        }

        WebTestView webView = new WebTestView(mActivity);
        webView.initSettings();
        mWebViewList.add(webView);
        webView.addJavascriptInterface(new TestDemoInterface(), "TestDemo");
        webView.setEventCallback(eventPublish);

        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        webView.setLayoutParams(params);
        mWebViewContainer.addView(webView);
        mWebTestView = webView;
    }

	/*
	* 删除List中的当前webview,并让mWebTestView等下list中下一个位置的webview
	*/
    private void closeWebView() {
        int lastIndex = mWebViewList.size() - 1;

        mWebTestViewModel.progress.set(0);
        mWebTestViewModel.progressing.set(false);

        if (lastIndex >= 1) {
            WebTestView webView = mWebViewList.remove(lastIndex);
            WebUtil.clearWebView(webView);
            mWebTestView = mWebViewList.get(lastIndex - 1);
            return;
        }
        mWebTestView = mWebViewList.get(0);
    }

	/*
	* 将list中的所有webview删除
	*/
    private void clearWebView() {
        for (int i = 0; i < mWebViewList.size(); i++) {
            mWebTestView = mWebViewList.get(i);
            WebUtil.clearWebView(mWebTestView);
        }
        mWebViewList.clear();
    }

	/*
	* 处理webview中的事件
	*/
    private void handleEvent(int event, String url) {
        switch (event) {
            case WebEvent.OVERRIDE_URL_RESOLVE:
            case WebEvent.INTERCEPT_REQUEST_RESOLVE:
                try {
                    Uri uri = Uri.parse(url);
                    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                    if (intent.resolveActivity(mContext.getPackageManager()) == null) {
                        Log.e(TAG, "target app not install " + url);
                    } else {
                        if (!uri.toString().equals("about:blank")) {
                            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                            startActivity(intent);
                            mActivity.onBackPressed();
                        }
                    }
                } catch (Exception e) {
                    Log.e(TAG, "invalid url " + url);
                }
                break;
            case WebEvent.RECEIVED_ERROR:
                /*显示弹出错误warning框*/
                break;
            case WebEvent.WEB_OPEN_EXTERNAL_LINK:
                /*跳转外链*/
                break;
            case WebEvent.WEB_NETWORK_ERROR_OPEN_WIFI:
                /*提示需要打开WIFI*/
                break;
        }
    }

	/*
	* 退出页面时,将list中的webview清空
	*/
    @Override
    public void onDestroy() {
        clearWebView();
        super.onDestroy();
    }

	/*
	* 在webview中打开相册并选择后,对返回的图片uri进行处理
	*/
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_WEBVIEW_GET_PHOTO) {
            Uri result = (data == null || resultCode != Activity.RESULT_OK ? null : data.getData());
            /*对返回数据进行操作*/
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

	/*
	* 点击toolbar的返回箭头时,退出页面
	*/
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                mActivity.popFragment();
        }
        return true;
    }

	/*
	* 点击back键后的处理,当网页可以返回时则goback,否则退出当前窗口页或当前页面
	*/
    @Override
    public void onBackPressed() {
        if (mWebTestView != null) {
            if (mWebTestView.canGoBack()) {
                mWebTestView.goBack();
            } else if (mWebViewList.size() > 1){
                closeWebView();
            } else {
                mActivity.popFragment();
            }
        } else {
            mActivity.popFragment();
        }
    }

}