WebView 的常见功能和问题(持更)

本篇继基本使用之后,来看看都有那些实战中的功能和问题,有帮助到你请赐小心心❤

Part 1 传送门 WebView 使用和方法
Part 2 传送门 常见功能 & 问题

常见功能 & 问题

1. 加载空白页

重写 onReceivedSslError() 方法

2. WebView 与父控件滑动冲突问题

比如在一个 ViewPager 中添加了 WebView,WebView 加载了一个可滑动的界面(查看图片滑动等),当网页滑动的时候,就直接切换了另一个 Tab。就可以用 onOverScrolled 方法处理。

当 WebView 滑动时会触发 overScrollBy() 方法,在 overScrollBy() 方法里面会调用 onOverScrolled() 方法。
其中,前两个参数是 距离原点的 x 和 y 的距离,后两个是 当 View 滑动到 两侧 或 上下 时为 true
当点击的时候允许 父控件(ViewPager)滑动,当滑动到边界的时候禁止父控件滑动。

 /**
     * Called by {@link #overScrollBy(int, int, int, int, int, int, int, int, boolean)} to
     * respond to the results of an over-scroll operation.
     *
     * @param scrollX New X scroll value in pixels
     * @param scrollY New Y scroll value in pixels
     * @param clampedX True if scrollX was clamped to an over-scroll boundary
     * @param clampedY True if scrollY was clamped to an over-scroll boundary
     */
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
     if (clampedX) {  // 滑动到两侧 允许 ViewPager 滑动
            requestDisallowInterceptTouchEvent(false);
     }
    super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
    super.onTouchEvent(event);
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
         //disables ViewPager when user presses down
        requestDisallowInterceptTouchEvent(true);
        return true;
    }
    return true;
}

参考方案:Android中WebView和父控件滑动冲突

3. WebView 注入 Js 代码,实现功能

实现点击图片滑动查看

在信息流当中经常穿插着一些网页界面,提供更丰富的内容,如何能让 H5 的界面里的图片,向原生开发一样可以产生图片集并且左右滑动呢?

思路

  • 提取网页中的 img 标签,形成图片集

  • 为每个图片添加点击事件

    Js 方法如下,可以看到里面也调用了原生方法,用来设置 图片集 和 打开查看图片界面,其中图片链接用 (;)分号隔开

function pic(){
        var imgList = "";
        var imgs = document.getElementsByTagName("img");
        for(var i = 0;i < imgs.length; i++){
            var img = imgs[i];
            imgList = imgList + img.src +";";
            img.onclick = function(){
                window.android.goToGallery(this.src);
            }
        }
        window.android.setImgUrls(imgList);
    }

但是要通过 WebView 把这个代码段注入进去,注意这里要把代码转化成字符串,有些符号需要转义处理。Android 也要处理 setImgUrls()goToGallery() 方法

 private void addJs() {
      mWebVew.loadUrl("javascript:(" + getImageJS()+")()");
 }

private String getImageJS(){
        return
            "function pic(){"
            +" var imgList = \"\";"
            +" var imgs = document.getElementsByTagName(\"img\");"
            +" for(var i = 0;i < imgs.length; i++){"
            +"     var img = imgs[i];"
            +"     imgList = imgList + img.src + \";\";"
            +"     img.onclick = function(){"
            +"         window." + JS_NAME + ".goToGallery(this.src);"
            +"     }"
            +" }"
            +" window." + JS_NAME + ".setImgUrls(imgList);"
            +"}";
    }
@JavascriptInterface
public void goToGallery(String url) {
    // 传到展示图片的viewPager mUrls.indexOf(url) 可以获取当前图片的 index
}
@JavascriptInterface
public void setImgUrls(String imgLists) {
    String[] urls = imgLists.split(";");//url拼接成的字符串,有分号隔开
    for (String url : urls) {
        mUrls.add(url);
    }
}

H5 链接重新处理

除了上面有图片需求,有时候H5自己带的连接也需要重新处理,比如跳转到 App 的界面

思路:获取 <a> 标签,为连接标签重新设置点击方式,同时原来的链接点击取消掉,参考代码如下:

function link(){
    var href = document.getElementsByTagName("a");
    for (var i = 0; i < href.length; i++) {
        var a = href[i];
        a.onclick = function(){
            window.andorid.toWebView(this.value);
        };
        a.value = a.href;
        a.href = "javascript:void(0)";
     } 
}

4. 音视频播放问题

当网页支持播放时,退出会发现声音会继续播放

有很多帖子给出方案是在 onPause 方法暂停 和 在 onResume 恢复

protected void onPause() {
    if(webView != null){//暂停WebView
        webView.onPause();
        webView.pauseTimers();
    }
    super.onPause();
}
 
@Override
protected void onResume() {
    if(webView != null){//恢复WebView
        webView.onResume();
        webView.resumeTimers();
    }
    super.onResume();
}

在实战过程中,由于我是在一个ViewPager 嵌入一个 含有 WebView 的 Fragment ,当切换 tab 的时候 是不会触发 onResume 的, 所以采用如下方案,下面用到了 reload() 方法,因为 直接用 WebView 的 resume 之类的也没有啥作用。

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    if (isPause && isVisibleToUser){ // (onResume) onPause 后不 reload
        isPause = false;
        return;
    }
    if (mWebView != null && !isPause && !isVisibleToUser) { // 切出时 调用 reload 停止播放
        mWebView.reload();
    }
}

@Override
public void onPause() {
    super.onPause();
    isPause = true;
}

5. 视频全屏功能

如果 WebView 含有视频资源,如果要点击视频可以全屏播放可以通过处理 WebChromeClient 中的两个方法来实现

@Override
public void onHideCustomView() {
    //退出全屏
}
@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
    // 进入全屏
    // 当网页中有视频的时候,点击全屏按键会调用这个方法,可设置网页播放全屏
}
private View mCustomView;
private ViewGroup mRootViewGroup;
@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
    // 进入全屏
    if (mCustomView != null) {
        return;
    }
    mCustomView = view;
    mCustomView.setLayoutParams(new WindowManager.LayoutParams(
            WindowManager.LayoutParams.MATCH_PARENT, 
            WindowManager.LayoutParams.MATCH_PARENT));
    // 或者 getActivity().getWindow().getDecorView(); 获取根视图并addView
    mRootViewGroup = getActivity().findViewById(android.R.id.content); 获取根视图并addView
    mRootViewGroup .addView(mCustomView); // 添加到根视图
   
    if (getActivity() != null) {
        //设置横屏
        getActivity().setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
        //设置全屏
        getActivity().getWindow().setFlags(
                WindowManager.LayoutParams.FLAG_FULLSCREEN, 
                WindowManager.LayoutParams.FLAG_FULLSCREEN);
    }
}

@Override
public void onHideCustomView() {
    //退出全屏
    if (mCustomView == null) {
        return;
    }
    //移除全屏视图并隐藏
    mRootViewGroup.removeView(mCustomView);
    mCustomView = null;
    if (getActivity() != null) {
        //设置竖屏
        getActivity().setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT);
        //清除全屏
        getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
    }
}

注意:在使用上面方法处理的时候,还需要考虑物理返回键。

有的视频容器按键异常,比如不可点击之类的,可以添加如下设置:

settings.setMediaPlaybackRequiresUserGesture(false);//SDK>18 是否支持手势控制网页媒体,比如视频的全屏

6. WebView 软键盘遮挡问题

如果通过 H5 可能做些互动功能,比如评论之类,此时会调用软键盘,会发现软键盘遮挡会遮挡输入框(这个问题很眼熟,在正常页面有时也会遮挡,真是问题不分控件)
有很多大神给的方案是创建一个 KeyBoardListener 类来处理,在我使用的项目中会在 ViewPager 中添加WebView 所以就需要改写一下,需要处理上面 tab的空间,参考方法类如下:
使用方式

 KeyBoardListener mKeyBoardListener = KeyBoardListener.getInstance(getActivity(), height);
 mKeyBoardListener.addListener();

KeyBoardListener.java

package cn.ninebot.webview;

import android.app.Activity;
import android.graphics.Rect;
import android.os.Build;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;

import cn.ninebot.commonlibs.utils.DisplayUtils;

import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;

public class KeyBoardListener {
    private Activity activity;

    private View mChildOfContent;
    private int usableHeightPrevious;
    private FrameLayout.LayoutParams frameLayoutParams;

    private static KeyBoardListener keyBoardListener;
    private static int mHeight = 0;
    private static int mStatusBarHeight = 0;

    private int contentHeight;
    private boolean isFirst = true;
    private boolean isAddListener = false;

    /**
     * 这个传进来的 height 是 tablayout 控件的高度, 因为 tablayout 被键盘弹起来,
     * 可以传入这个值对其进行操作
     */
    public static KeyBoardListener getInstance(Activity activity, int height) {
        keyBoardListener = new KeyBoardListener(activity);
        mHeight = height;
        if (height != 0) {
            mStatusBarHeight = DisplayUtils.getStatusBarHeight();
        }
        return keyBoardListener;
    }

    public KeyBoardListener(Activity activity) {
        super();
        this.activity = activity;
        init();
    }

    private void init() {
        FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
        mChildOfContent = content.getChildAt(0);
        frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
    }

    public void addListener() {
        if (!isAddListener) {
            mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
            isAddListener = true;
            isFirst = true;
        }
    }

    public void removeListener() {
        if (mChildOfContent != null && isAddListener) {
            mChildOfContent.getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalLayoutListener);
            isAddListener = false;
        }
    }

    ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
        public void onGlobalLayout() {
            if (isFirst) {
                contentHeight = mChildOfContent.getHeight();
                isFirst = false;
            }
            possiblyResizeChildOfContent();
        }
    };

    //重新调整跟布局的高度
    private void possiblyResizeChildOfContent() {
        if (activity.getRequestedOrientation() == SCREEN_ORIENTATION_PORTRAIT) { //竖屏
            int usableHeightNow = computeUsableHeight();
            //当前可见高度和上一次可见高度不一致 布局变动
            if (usableHeightNow != usableHeightPrevious) {
                int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
                int heightDifference = usableHeightSansKeyboard - usableHeightNow;
                if (heightDifference > (usableHeightSansKeyboard / 4)) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                        // keyboard probably just became visible
                        frameLayoutParams.height = usableHeightSansKeyboard - heightDifference + mStatusBarHeight + mHeight;
                    } else {
                        frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
                    }
                } else {
                    // keyboard probably just became hidden
                    frameLayoutParams.height = contentHeight;
                }
                mChildOfContent.requestLayout();
                usableHeightPrevious = usableHeightNow;
            }
        }
    }

    private int computeUsableHeight() {
        Rect r = new Rect();
        mChildOfContent.getWindowVisibleDisplayFrame(r);
        return (r.bottom - r.top);
    }
}

7. WebView 安全警告

在使用 WebView 加载网页时,有时候会出现警告:您要访问的网站包含有害应用。

这是 WebView 的安全浏览保护策略,在 Android 8.0(API Level 26)开始的默认策略,被应用在所有 App 的 WebView 当中。

Google 会自己维护一套“不安全”网站的列表,并通过 Google Play 服务,同步到所有的设备上。当你要访问某些被标记为“不安全”的网站时,它就会以此“红屏”警告用户。

注意这是默认策略,虽然出发点是为了保护用户,但是有时候我们自己的 App 还是要有自主管控的权利。

在 Android 8.0(API Level 26)中,可以通过以下两种方法关闭安全策略:

方法一:

<manifest> 
    <meta-data
        android:name="android.webkit.WebView.EnableSafeBrowsing"
        android:value="false" />
          <application> ... </application>
 </manifest>

方法二:
WebView 的安全策略是默认开始的,如果想要关闭它,需要通过 WebSettings 这个类,其中有 setSafeBrowsingEnabled(boolean) 方法,可以用于设置是否开启安全模式。

 settings.setSafeBrowsingEnabled(false);// 是否开启安全模式

此方法是一种全局的策略,也就是要么开启、要么关闭。

相关参考:
Android Developer 管理 WebView 对象
Android webview(安全策略) 出现 您要访问的网站包含有害应用

WebView 监听下载

WebView 是可以监听网页的下载链接,但是默认不会开启,所以点击不会有动作,可以使用系统 DownloadListener 可以监听下载。

webView.setDownloadListener(new DownloadListener() {

    @Override
    public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
            
        // url 你要访问的下载链接
        // userAgent 是HTTP请求头部用来标识客户端信息的字符串
        // contentDisposition 为保存文件提供一个默认的文件名
        // mimetype 该资源的媒体类型
        // contentLength 该资源的大小
        // 这几个参数都是可以通过抓包获取的

        // 用手机默认浏览器打开链接
        Uri uri = Uri.parse(url);
        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
        startActivity(intent);
    }
});

Android P 以上 进入WebView可能 crash

 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        boolean isMainProcess = getApplicationContext().getPackageName().equals(getCurrentProcessName());
        if (!isMainProcess) {
            WebView.setDataDirectorySuffix("any-folder-name");
        }
 }

Admob banner ad not loading in android P

Vivo 设备,Android 5.1报错

部分log

Caused by: android.content.res.Resources$NotFoundException: String resource ID #0x2040003
       at android.content.res.Resources.getText(Resources.java:318)
       at android.content.res.VivoResources.getText(VivoResources.java:123)
       at android.content.res.Resources.getString(Resources.java:404)

fixed

public class LollipopFixedWebView extends WebView {
    public LollipopFixedWebView(Context context) {
        super(getFixedContext(context));
    }

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

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

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public LollipopFixedWebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(getFixedContext(context), attrs, defStyleAttr, defStyleRes);
    }

    public LollipopFixedWebView(Context context, AttributeSet attrs, int defStyleAttr, boolean privateBrowsing) {
        super(getFixedContext(context), attrs, defStyleAttr, privateBrowsing);
    }

    public static Context getFixedContext(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) // Android Lollipop 5.0 & 5.1
            return context.createConfigurationContext(new Configuration());
        return context;
    }
}

Webview 无法打开谷歌文档短链接

Set WebViewClient to webview

mywebview.webViewClient = object : WebViewClient() {
    override fun shouldOverrideUrlLoading(webView: WebView, url: String): Boolean {
        if (url.startsWith("intent://")) {
            val intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
            if (intent != null) {
                val fallbackUrl = intent.getStringExtra("browser_fallback_url")
                return if (fallbackUrl != null) {
                    webView.loadUrl(fallbackUrl)
                    true
                } else {
                    false
                }
            }
        }
        return false
    }
}

参考:Google Forum short URL not working in Android app webview.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335