Skip to content

Latest commit

 

History

History
358 lines (326 loc) · 16.9 KB

WebView2.md

File metadata and controls

358 lines (326 loc) · 16.9 KB

基础使用目录介绍

  • 11.什么是302/303重定向
  • 12.301/302业务场景白屏描述
  • 13.301/302业务白屏解决方案
  • 14.301/302回退栈问题描述
  • 15.301/302回退栈问题解决方案1
  • 16.301/302回退栈问题解决方案2
  • 17.301/302回退栈问题解决方案3
  • 18.如何用代码判断是否重定向
  • 19.shouldOverrideUrlLoading
  • 20.重定向终极优雅解决方案

12.301/302业务场景白屏描述

  • 业务场景问题
    • 对于需要对url进行拦截以及在url中需要拼接特定参数的WebView来说,301和302发生的情景主要有以下几种:
    • 首次进入,有重定向,然后直接加载H5页面,如http跳转https
    • 首次进入,有重定向,然后跳转到native页面,如扫一扫短链,然后跳转到native
    • 二次加载,有重定向,跳转到native页面
    • 类似登录后跳转到某个页面的需求。如我的拼团,未登录状态下点击我的拼团跳转到登录页面,登录完成后再加载我的拼团页
  • 遇到问题分析
    • 第一种情况属于正常情况,暂时没遇到什么坑。
    • 第二种情况,会遇到WebView空白页问题,属于原始url不能拦截到native页面,但301/302后的url拦截到native页面的情况,当遇到这种情况时,需要把WebView对应的Activity结束,否则当用户从拦截后的页面返回上一个页面时,是一个WebView空白页。
    • 第三种情况,也会遇到WebView空白页问题,原因在于加载的第一个页面发生了重定向到了第二个页面,第二个页面被客户端拦截跳转到native页面,那么WebView就停留在第一个页面的状态了,第一个页面显然是空白页。
    • 第四种情况,会遇到无限加载登录页面的问题。
  • 为何会出现白屏
    • webView自带的背景就是白色的

13.301/302回退栈问题描述

  • 无论是哪种重定向场景,都不可避免地会遇到回退栈的处理问题,如果处理不当,用户按返回键的时候不一定能回到重定向之前的那个页面。很多开发者在覆写WebViewClient.shouldOverrideUrlLoading()方法时,会简单地使用以下方式粗暴处理:
    WebView.setWebViewClient(new WebViewClient() {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            view.loadUrl(url);
            return true;
        }
    )
    
  • 这种方法最致命的弱点就是如果不经过特殊处理,那么按返回键是没有效果的,还会停留在302之前的页面。现有的解决方案无非就几种:
    • 手动管理回退栈,遇到重定向时回退两次。
    • 通过HitTestResult判断是否是重定向,从而决定是否自己加载url。具体看:16.301/302回退栈问题解决方案2
    • 通过设置标记位,在onPageStarted和onPageFinished分别标记变量避免重定向。具体看:17.301/302回退栈问题解决方案3
    • 通过用户的touch事件来判断重定向。这个看:15.301/302回退栈如何处理1
  • 这几种解决方案都不是完美的,都有缺陷。

15.301/302回退栈如何处理1

  • 在提供解决方案之前,我们需要了解一下shouldOverrideUrlLoading方法的返回值代表什么意思。
    • 简单地说,就是返回true,那么url就已经由客户端处理了,WebView就不管了,如果返回false,那么当前的WebView实现就会去处理这个url。
    • WebView能否知道某个url是不是301/302呢?当然知道,WebView能够拿到url的请求信息和响应信息,根据header里的code很轻松就可以实现,事实正是如此,交给WebView来处理重定向(return false),这时候按返回键,是可以正常地回到重定向之前的那个页面的。(PS:从上面的章节可知,WebView在5.0以后是一个独立的apk,可以单独升级,新版本的WebView实现肯定处理了重定向问题)
    • 但是,业务对url拦截有需求,肯定不能把所有的情况都交给系统WebView处理。为了解决url拦截问题,本文引入了另一种思想——通过用户的touch事件来判断重定向。下面通过代码来说明。
  • 核心代码如下所示,具体代码见本案例中的ScrollWebView类代码
    @Override
    public void setWebViewClient(final WebViewClient client) {
        super.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                boolean handleByChild = null != client && client.shouldOverrideUrlLoading(view, url);
                   if (handleByChild) {
                    // 开放client接口给上层业务调用,如果返回true,表示业务已处理。
                    return true;
                   } else if (!isTouchByUser()) {
                    // 如果业务没有处理,并且在加载过程中用户没有再次触摸屏幕,认为是301/302事件,直接交由系统处理。
                    return super.shouldOverrideUrlLoading(view, url);
                } else {
                    //否则,属于二次加载某个链接的情况,为了解决拼接参数丢失问题,重新调用loadUrl方法添加固有参数。
                    loadUrl(url);
                    return true;
                }
            }
            @RequiresApi(api = Build.VERSION_CODES.N)
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                boolean handleByChild = null != client && client.shouldOverrideUrlLoading(view, request);
                if (handleByChild) {
                    return true;
                } else if (!isTouchByUser()) {
                    return super.shouldOverrideUrlLoading(view, request);
                } else {
                    loadUrl(request.getUrl().toString());
                    return true;
                }
            }
        });
    }
    
  • 如何在android中的webView上获得onclick事件
    • WebView似乎没有发送点击事件OnClickListener
  • 如何设置触摸事件

16.301/302回退栈问题解决方案2

  • WebView有一个getHitTestResult():返回的是一个HitTestResult,一般会根据打开的链接的类型,返回一个extra的信息
    • 如果打开链接不是一个url,或者打开的链接是JavaScript的url,他的类型是UNKNOWN_TYPE,这个url就会通过requestFocusNodeHref(Message)异步重定向。
    • 返回的extra为null,或者没有返回extra。根据此方法的返回值,判断是否为null,可以用于解决网页重定向问题。
    • requestFocusNodeHref的意思是:请求焦点节点Href
  • 代码如下所示
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
            WebView.HitTestResult hitTestResult = view.getHitTestResult();
        //hitTestResult==null解决重定向问题
        if (!TextUtils.isEmpty(url) && hitTestResult == null) {
            view.loadUrl(url);
            return true;
        }
        return super.shouldOverrideUrlLoading(view, url);
    }
    

17.301/302回退栈问题解决方案3

  • 页面被重定向了会走怎样的步骤呢,首先会回调shouldOverrideUrlLoading,然后回调onPageStarted,最后走onPageFinished。
    • 据此我们可以修改代码如下,增加一个计数器判断当前的onPageFinished
  • 代码如下所示
    mWebView.setWebViewClient(new WebViewClient() {
        int running = 0;
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            view.loadUrl(url);
            running++;
            return true;
        }
    
        @Override
        public void onPageStarted(WebView webView, String s, Bitmap bitmap) {
            super.onPageStarted(webView, s, bitmap);
            running = Math.max(running, 1);
        }
    
        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            if (--running==0) {
                setWebViewHeight();
            }
        }
    });
    

18.如何用代码判断是否重定向

  • 遇到问题说明
    • 用WebView调用webView.loadUrl(url)加载某个网页。点击WebView的某个链接跳转下一个页面。但是下面这样做,会出现问题;
    • 如果webView第一次加载的url重定向到了另一个地址,此时也会走shouldOverrideUrlLoading的回调。这样一来,出现的现象就是WebView是空的,直接打开了浏览器。
    webView.setWebViewClient(new WebViewClient(){
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
                Uri uri = Uri.parse(url);
                Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                try {
                    view.getContext().startActivity(intent);
                } catch (ActivityNotFoundException e) {
                    e.printStackTrace();
                }
                return true;
        }
    });
    
  • 解决问题分析思考
    • 要解决这个问题,很容易想到的解决方案是找找WebView有没有对重定向的判断方法,如果有的话,我们就可以对重定向的回调另外处理。
    • 很不幸,WebView并没有提供相应的方法。是不是就没办法处理了呢?当然不是。
  • 第一种解决方案案例
    • WebView有一个getHitTestResult():返回的是一个HitTestResult,一般会根据打开的链接的类型,返回一个extra的信息,如果打开链接不是一个url,或者打开的链接是JavaScript的url,他的类型是UNKNOWN_TYPE,这个url就会通过requestFocusNodeHref(Message)异步重定向。返回的extra为null,或者没有返回extra。根据此方法的返回值,判断是否为null,可以用于解决网页重定向。
    • 该方案有个缺陷是,如果遇到的需求是前言描述的那样,二正好点击的链接发生了重定向,就不会在另一个页面打开,而是直接在当前的WebView里了。
    webView.setWebViewClient(new WebViewClient(){
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            //判断重定向的方式一
            WebView.HitTestResult hitTestResult = view.getHitTestResult();
            if(hitTestResult == null) {
                return false;
            }
            if(hitTestResult.getType() == WebView.HitTestResult.UNKNOWN_TYPE) {
                return false;
            }
    
            Uri uri = Uri.parse(url);
            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
            try {
                view.getContext().startActivity(intent);
            } catch (ActivityNotFoundException e) {
                e.printStackTrace();
            }
            return true;
        }
    });
    
  • 第二种解决方案案例
    • WebView在加载一个页面开始的时候会回调onPageStarted方法,在该页面加载完成之后会回调onPageFinished方法。而如果该链接发生了重定向,回调shouldOverrideUrlLoading会在回调onPageFinished之前。
    • 有了这个前提,我们就可以加一个mIsPageLoading的标记,在onPageStarted回调的时候置为true,在onPageFinished回调的时候置为false。在shouldOverrideUrlLoading里面就可以判断该标记,如果为true,则表示该回调是重定向,否则直接打开浏览器。代码如下:
    private boolean mIsPageLoading;
    //代码省略
    webView.setWebViewClient(new WebViewClient(){
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            //判断重定向的方式二
            if(mIsPageLoading) {
                return false;
            }
    
            if(url != null && url.startsWith("http")) {
                webView.loadUrl(url);
                return true;
            } else {
                Uri uri = Uri.parse(url);
                Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                try {
                    view.getContext().startActivity(intent);
                } catch (ActivityNotFoundException e) {
                    e.printStackTrace();
                }
                return true;
            }
        }
    
        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon) {
            super.onPageStarted(view, url, favicon);
            mIsPageLoading= true;
            Log.d(TAG, "onPageStarted");
        }
    
        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            mIsPageLoading= false;
            Log.d(TAG, "onPageFinished");
        }
    });
    
    • 该方案也会产生另一个问题:当页面没有全部加载完之前,加载出来的部分页面的链接也是可以点击的。这样一来在shouldOverrideUrlLoading里面本来是对链接的点击也会被当成重定向链接在当前的WebView里面打开。
    • 要避免这种情况,只能在页面完全加载出来之前禁止WebView的点击。
    webView.setOnTouchListener(new WebViewTouchListener());
    //代码省略
    private class WebViewTouchListener implements View.OnTouchListener {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            return !mIsLoading;
        }
    }
    

19.shouldOverrideUrlLoading

20.重定向终极优雅解决方案

  • 需要准备的条件
    • 创建一个栈,主要是用来存取和移除url的操作。这个url包括所有的请求链接
    • 定义一个变量,用于判断页面是否处于正在加载中。
    • 定义一个变量,用于记录重定向前的链接url
    • 定一个重定向时间间隔,主要为了避免刷新造成循环重定向
  • 具体怎么操作呢
    • 在执行onPageStarted时,先移除栈中上一个url,然后将url加载到栈中。
    • 当出现错误重定向的时候,如果和上一次重定向的时间间隔大于3秒,则reload页面。
    • 在回退操作的时候,判断如果可以回退,则从栈中获取最后停留的url,然后loadUrl。即可解决回退问题。
  • 具体的代码如下所示
    /**
     * 记录上次出现重定向的时间.
     * 避免由于刷新造成循环重定向.
     */
    private long mLastRedirectTime = 0;
    /**
     * 默认重定向间隔.
     * 避免由于刷新造成循环重定向.
     */
    private static final long DEFAULT_REDIRECT_INTERVAL = 3000;
    /**
     * URL栈
     */
    private final Stack<String> mUrlStack = new Stack<>();
    /**
     * 判断页面是否加载完成
     */
    private boolean mIsLoading = false;
    /**
     * 记录重定向前的链接
     */
    private String mUrlBeforeRedirect;
    /**
     * 太多的重定向错误
     */
    private static int ERR_TOO_MANY_REDIRECTS = -9;
    
    
    @Override
    public void onPageStarted(WebView webView, String url, Bitmap bitmap) {
        super.onPageStarted(webView, url, bitmap);
        if (mIsLoading && mUrlStack.size() > 0) {
            mUrlBeforeRedirect = mUrlStack.pop();
        }
        recordUrl(url);
        mIsLoading = true;
    }
    
    @Override
    public void onPageFinished(WebView view, String url) {
        if (mIsLoading) {
            mIsLoading = false;
        }
    }
    
    private void recordUrl(String url) {
        if (!TextUtils.isEmpty(url) && !url.equals(getUrl())) {
            if (!TextUtils.isEmpty(mUrlBeforeRedirect)) {
                mUrlStack.push(mUrlBeforeRedirect);
                mUrlBeforeRedirect = null;
            }
        }
    }
    
    @Nullable
    public String getUrl() {
        //peek方法,查看此堆栈顶部的对象,而不将其从堆栈中删除。
        return mUrlStack.size() > 0 ? mUrlStack.peek() : null;
    }
    
    /**
     * 回退操作
     * @param webView                           webView
     * @return
     */
    public final boolean pageGoBack(@NonNull WebView webView) {
        //判断是否可以回退操作
        if (pageCanGoBack()) {
            //获取最后停留的页面url
            final String url = popBackUrl();
            //如果不为空
            if (!TextUtils.isEmpty(url)) {
                webView.loadUrl(url);
                return true;
            }
        }
        return false;
    }