《Android 自定义 WebView 组件:从封装到路由,打造灵活可复用的混合开发利器》

《Android 自定义 WebView 组件:从封装到路由,打造灵活可复用的混合开发利器》

引言:痛点与目标

混合开发中 WebView 的高频使用场景(H5 活动页、帮助中心、支付协议等)

原生 WebView 的痛点:初始化配置重复、生命周期管理混乱、进度条/错误处理耦合、难以在 Dialog/Fragment 等不同容器中复用。

本文目标:基于抽象 + 策略模式,封装一个自带进度条、统一路由、支持页面加载监听、且可放置于任意容器中的 BaseWebView 组件。

设计思想

组合优于继承:通过 BindingBaseCombineWidget(类似 DataBinding + 组合视图)封装,使 WebView 自带布局(含进度条)。

模板方法模式:BaseWebView 抽象类定义骨架,子类(MyWebView)实现 setInitializer、setUrlHandler 等具体策略。

单一职责:将初始化、URL 处理、WebViewClient/WebChromeClient 分离到独立类或接口中。

路由层:WebRouter 单例统一处理 URL 与 Data 的差异加载。

核心代码实现

BaseWebView 抽象类定义骨架,下面来看看BaseWebView的核心逻辑

java 复制代码
package com.outlink.base.router.webview.base;

import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.webkit.WebView;
import android.widget.ProgressBar;

import com.outlink.base.databinding.MyWebViewBinding;
import com.outlink.base.router.webview.help.IUrlHandler;
import com.outlink.base.router.webview.help.IWebViewInitializer;
import com.outlink.base.router.webview.help.JsApi;
import com.outlink.base.router.widget.BindingBaseCombineWidget;

public abstract class BaseWebView extends BindingBaseCombineWidget<MyWebViewBinding> implements IWebViewInitializer {
    // JavaScript接口对象,用于与H5页面通信
    private JsApi mJsApi;
    // WebView实例
    private WebView mWebView;
    // 加载的URL地址
    private String mUrl;
    // WebView是否可用的标志
    private boolean mIsWebViewAvailable = false;

    /**
     * 设置WebView初始化器
     *
     * @return IWebViewInitializer实例
     */
    public abstract IWebViewInitializer setInitializer();

    public abstract void initWebData();

    /**
     * 设置URL处理器
     *
     * @return IUrlHandler实例
     */
    public abstract IUrlHandler setUrlHandler();

    public void loadUrl(String url) {
        this.mUrl = url;
        IUrlHandler handler = setUrlHandler();
        if (handler != null) {
            handler.handleUrl();
        }
    }

    public BaseWebView(Context context) {
        this(context, null);
    }

    public BaseWebView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BaseWebView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initWebView();
        initWebData();
    }

    /**
     * 初始化WebView
     * 配置WebView设置、添加JavaScript接口等
     */
    @SuppressLint("JavascriptInterface")
    private void initWebView() {
        if (mWebView != null) {
            mWebView.removeAllViews();
            mWebView.destroy();
        } else {
            IWebViewInitializer initializer = setInitializer();
            if (initializer != null) {
                mJsApi = JsApi.create();
                mWebView = getBinding().wvWebView;
                mWebView = initializer.initWebView(mWebView);
                mWebView.setWebViewClient(initializer.initWebViewClient());
                mWebView.setWebChromeClient(initializer.initWebChromeClient());
                mWebView.addJavascriptInterface(mJsApi, null);
                mIsWebViewAvailable = true;
            } else {
                throw new NullPointerException("Initializer is null!");
            }
        }
    }

    /**
     * 获取进度条视图
     *
     * @return ProgressBar进度条
     */
    public ProgressBar getProgressView() {
        return getBinding().pbProgressBar;
    }

    /**
     * 获取WebView实例
     *
     * @return WebView实例
     * @throws NullPointerException 如果WebView为null
     */
    public WebView getWebView() {
        if (mWebView == null) {
            throw new NullPointerException("WebView IS NULL!");
        }
        return mIsWebViewAvailable ? mWebView : null;
    }

    /**
     * 获取当前加载的URL
     *
     * @return URL字符串
     * @throws NullPointerException 如果URL为null
     */
    public String getUrl() {
        if (mUrl == null) {
            throw new NullPointerException("WebView IS NULL!");
        }
        return mUrl;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mIsWebViewAvailable = false;
        if (mWebView != null) {
            mWebView.removeAllViews();
            mWebView.destroy();
            mWebView = null;
        }

        if (mJsApi != null) {
            mJsApi = null;
        }
    }
}

有关webview的基础骨架配置都在基类中完成,具体的实现交给它的子类

关于WebView的相关接口核心实现

java 复制代码
public interface IPageLoadListener {
    void onLoadStart();

    void onLoadEnd();
}


public interface IUrlHandler {
    void handleUrl();
}


public interface IWebViewInitializer {
    WebView initWebView(WebView webView);

    WebViewClient initWebViewClient();

    WebChromeClient initWebChromeClient();
}

最终我们的MyWebView的核心代码

java 复制代码
package com.outlink.base.router.webview;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import com.outlink.base.router.webview.base.BaseWebView;
import com.outlink.base.router.webview.chromeclient.WebChromeClientImpl;
import com.outlink.base.router.webview.client.WebViewClientImpl;
import com.outlink.base.router.webview.help.IPageLoadListener;
import com.outlink.base.router.webview.help.IUrlHandler;
import com.outlink.base.router.webview.help.IWebViewInitializer;
import com.outlink.base.router.webview.help.WebRouter;
import com.outlink.base.router.webview.help.WebViewInitializer;

public class MyWebView extends BaseWebView implements IUrlHandler, IPageLoadListener {

    public MyWebView(Context context) {
        super(context);
    }

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

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

    @Override
    public IWebViewInitializer setInitializer() {
        return this;
    }

    @Override
    public void initWebData() {

    }

    @Override
    public IUrlHandler setUrlHandler() {
        return this;
    }

    @Override
    public WebView initWebView(WebView webView) {
        return new WebViewInitializer().createWebView(webView);
    }

    @Override
    public WebViewClient initWebViewClient() {
        WebViewClientImpl client = new WebViewClientImpl(this);
        client.setPageLoadListener(this);
        return client;
    }

    @Override
    public WebChromeClient initWebChromeClient() {
        return new WebChromeClientImpl(this);
    }

    @Override
    public void handleUrl() {
        String url = getUrl();
        if (url != null) {
            Log.d(TAG, "url:" + url);
            WebRouter.INSTANCE.loadPage(this, url);
        }
    }

    @Override
    public void onLoadStart() {

    }

    @Override
    public void onLoadEnd() {

    }
}

围绕MyWebView看核心类WebChromeClientImpl

java 复制代码
package com.outlink.base.router.webview.chromeclient;

import android.webkit.WebChromeClient;
import android.webkit.WebView;

import androidx.annotation.Nullable;

import com.outlink.base.router.webview.base.BaseWebView;

/**
 * 自定义WebChromeClient实现类
 * 处理WebView的进度、标题等Chrome相关事件
 *
 * @author shan.peng
 */
public class WebChromeClientImpl extends WebChromeClient {
    private final BaseWebView delegate;


    public WebChromeClientImpl(BaseWebView delegate) {
        this.delegate = delegate;
    }

    @Override
    public void onReceivedTitle(WebView view, @Nullable String title) {
        super.onReceivedTitle(view, title);
    }

    @Override
    public void onProgressChanged(WebView view, int newProgress) {
        super.onProgressChanged(view, newProgress);
        if (delegate == null) {
            return;
        }
//        View progressView = delegate.getProgressView();
//        if (newProgress >= 100) { // WebView的进度最大值是100
//            progressView.setVisibility(View.GONE);
//        } else {
//            // 假设progressView是ProgressBar,设置进度
//            if (progressView instanceof android.widget.ProgressBar) {
//                ((android.widget.ProgressBar) progressView).setProgress(newProgress);
//            }
//            progressView.setVisibility(View.VISIBLE);
//        }
    }
}

WebViewClientImpl的实现

java 复制代码
package com.outlink.base.router.webview.client;

import android.graphics.Bitmap;
import android.net.http.SslError;
import android.webkit.SslErrorHandler;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import androidx.annotation.Nullable;

import com.outlink.base.router.webview.base.BaseWebView;
import com.outlink.base.router.webview.help.IPageLoadListener;

/**
 * 自定义WebViewClient实现类
 * 处理WebView的各种回调事件,包括页面开始加载、加载完成、错误处理等
 * 支持通过IPageLoadListener接口将事件传递给外部监听器
 *
 * @author shan.peng
 */
public class WebViewClientImpl extends WebViewClient {
    private final BaseWebView delegate;
    private IPageLoadListener mIPageLoadListener;

    public WebViewClientImpl(@Nullable BaseWebView delegate) {
        this.delegate = delegate;
    }

    public void setPageLoadListener(@Nullable IPageLoadListener listener) {
        this.mIPageLoadListener = listener;
    }

    /**
     * 当WebView需要加载URL时调用(针对Android API < 24)
     * 决定是否由应用程序处理该URL,而不是WebView
     *
     * @param view WebView实例
     * @param url  要加载的URL
     * @return true表示应用程序处理该URL,false表示由WebView处理
     */
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        return super.shouldOverrideUrlLoading(view, url);
    }

    /**
     * 当页面开始加载时调用
     * 在此处可以显示加载动画或进度条
     *
     * @param view    WebView实例
     * @param url     正在加载的URL
     * @param favicon 网页的图标,可能为null
     */
    @Override
    public void onPageStarted(WebView view, String url, @Nullable Bitmap favicon) {
        super.onPageStarted(view, url, favicon);
        // 加载动画
        if (mIPageLoadListener != null) {
            mIPageLoadListener.onLoadStart();
        }
    }

    /**
     * 当页面加载过程中发生错误时调用(针对Android API < 23)
     * 注意:此方法在Android 6.0(API 23)及更高版本中已废弃
     *
     * @param view        WebView实例
     * @param errorCode   错误代码
     * @param description 错误描述
     * @param failingUrl  加载失败的URL
     */
    @Override
    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
        super.onReceivedError(view, errorCode, description, failingUrl);
    }

    /**
     * 当页面加载过程中发生HTTP错误时调用(针对Android API >= 23)
     *
     * @param view          WebView实例
     * @param request       导致错误的请求
     * @param errorResponse HTTP错误响应,包含状态码等信息
     */
    @Override
    public void onReceivedHttpError(WebView view, @Nullable WebResourceRequest request,
                                    @Nullable WebResourceResponse errorResponse) {
        super.onReceivedHttpError(view, request, errorResponse);
    }

    /**
     * 当WebView遇到SSL错误时调用
     * 可以在此处决定是否继续加载页面
     *
     * @param view    WebView实例
     * @param handler SSL错误处理器,用于决定是否继续
     * @param error   SSL错误信息
     */
    @Override
    public void onReceivedSslError(WebView view, @Nullable SslErrorHandler handler,
                                   @Nullable SslError error) {
        super.onReceivedSslError(view, handler, error);
    }

    /**
     * 当页面加载完成时调用
     * 在此处可以隐藏加载动画或进度条
     *
     * @param view WebView实例
     * @param url  已加载完成的URL
     */
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
        if (mIPageLoadListener != null) {
            mIPageLoadListener.onLoadEnd();
        }
    }

    /**
     * 当WebView需要加载URL时调用(针对Android API >= 24)
     * 决定是否由应用程序处理该URL,而不是WebView
     *
     * @param view    WebView实例
     * @param request 要加载的请求
     * @return true表示应用程序处理该URL,false表示由WebView处理
     */
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        return super.shouldOverrideUrlLoading(view, request);
    }
}

webview的配置

java 复制代码
package com.outlink.base.router.webview.help;

import android.annotation.SuppressLint;
import android.graphics.Color;
import android.os.Build;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.WebSettings;
import android.webkit.WebView;

import androidx.annotation.Nullable;

public class WebViewInitializer {
    private static final String TAG = "WebViewInitializer";

    @SuppressLint({"SetJavaScriptEnabled", "ObsoleteSdkInt"})
    @Nullable
    public WebView createWebView(@Nullable WebView webView) {
        Log.d(TAG, "createWebView");
        //https://github.com/wendux/DSBridge-Android/issues/101
        WebView.setWebContentsDebuggingEnabled(false);
        WebView.setWebContentsDebuggingEnabled(false);

        //cookie
        CookieManager cookieManager = CookieManager.getInstance();
        cookieManager.setAcceptCookie(true);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            cookieManager.setAcceptThirdPartyCookies(webView, true);
        }
        CookieManager.setAcceptFileSchemeCookies(true);

        if (webView != null) {
            webView.setBackgroundColor(Color.TRANSPARENT);
            webView.setBackgroundResource(android.R.color.transparent);
            //不能横向滚动
            webView.setHorizontalScrollBarEnabled(false);
            //不能纵向滚动
            webView.setVerticalScrollBarEnabled(false);
            //允许截图
            webView.setDrawingCacheEnabled(true);
            //屏蔽长按事件
            webView.setOnLongClickListener(v -> true);

            //初始化WebSettings
            WebSettings settings = webView.getSettings();
            settings.setJavaScriptEnabled(true);
            //隐藏缩放控件
            settings.setBuiltInZoomControls(true);
            settings.setDisplayZoomControls(false);
            settings.setUseWideViewPort(true);
            settings.setLoadWithOverviewMode(true);
            //禁止缩放
            settings.setSupportZoom(true);
            //文件权限
            settings.setAllowFileAccess(true);
            settings.setAllowFileAccessFromFileURLs(true);
            settings.setAllowUniversalAccessFromFileURLs(true);
            settings.setAllowContentAccess(true);
            //缓存相关
            settings.setDomStorageEnabled(true);
            settings.setDatabaseEnabled(true);
            settings.setCacheMode(WebSettings.LOAD_DEFAULT);
            settings.setSupportMultipleWindows(false);
        }

        return webView;
    }
}

具体的路由URL的WebRouter类

java 复制代码
package com.outlink.base.router.webview.help;

import android.util.Log;
import android.webkit.URLUtil;
import android.webkit.WebView;

import androidx.annotation.Nullable;

import com.outlink.base.router.webview.base.BaseWebView;

/**
 * Web页面路由管理器
 * 负责根据URL类型选择合适的方式加载Web页面
 * 采用单例模式,提供统一的页面加载接口
 */
public class WebRouter {

    private static final String TAG = "WebRouter";
    public static final WebRouter INSTANCE = new WebRouter();

    // 私有构造函数,防止外部实例化
    private WebRouter() {
    }

    /**
     * 加载网络或本地资源页面
     * 用于加载标准URL(http/https协议或本地资源)
     *
     * @param webView WebView实例,不能为null
     * @param url     要加载的URL,可为null(会转换为空字符串)
     * @throws NullPointerException 如果webView为null
     */
    private void loadWebPage(@Nullable WebView webView, @Nullable String url) {
        if (webView != null) {
            Log.d(TAG, "loadWebPage: " + url);
            webView.loadUrl(url != null ? url : "");
        } else {
            throw new NullPointerException("WebView is null!");
        }
    }

    /**
     * 加载HTML数据页面
     * 用于加载非URL格式的HTML内容(如字符串形式的HTML)
     *
     * @param webView WebView实例,不能为null
     * @param url     HTML数据内容,可为null(会转换为空字符串)
     * @throws NullPointerException 如果webView为null
     */
    private void loadDataPage(@Nullable WebView webView, @Nullable String url) {
        if (webView != null) {
            webView.loadData(url != null ? url : "", "text/html", "utf-8");
        } else {
            throw new NullPointerException("WebView is null!");
        }
    }

    /**
     * 根据URL类型选择加载方式
     * 判断URL是网络/资源URL还是HTML数据,选择对应的加载方法
     *
     * @param webView WebView实例
     * @param url     要加载的内容或URL
     * @throws NullPointerException 如果webView为null
     */
    private void loadPage(@Nullable WebView webView, @Nullable String url) {
        if (URLUtil.isNetworkUrl(url) || URLUtil.isAssetUrl(url)) {
            loadWebPage(webView, url);
        } else {
            loadDataPage(webView, url);
        }
    }

    /**
     * 公开的页面加载方法
     * 通过BaseWebFragment加载页面,简化调用
     *
     * @param delegate BaseWebFragment实例,不能为null
     * @param url      要加载的URL或HTML内容
     * @throws NullPointerException 如果delegate为null
     */
    public void loadPage(@Nullable BaseWebView delegate, @Nullable String url) {
        if (delegate != null) {
            loadPage(delegate.getWebView(), url);
        } else {
            throw new NullPointerException("BaseWebFragment is null!");
        }
    }
}

使用示例

java 复制代码
package com.auto.module_app;

import android.annotation.SuppressLint;
import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;

import com.outlink.base.router.webview.MyWebView;

public class WebViewActivity extends AppCompatActivity {
    private MyWebView myWebView;

    @SuppressLint("MissingInflatedId")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_web_view);
        myWebView = findViewById(R.id.myWebView);
        myWebView.loadUrl("https://www.baidu.com/?tn=15007414_13_dg");
    }
}
相关推荐
程序员陆业聪2 小时前
AI Code Review:让每一行代码都有AI审查员
android
程序员陆业聪2 小时前
AI Bug修复与测试生成:从崩溃日志到修复PR的自动化 | AI提效Android开发(5)
android
诸神黄昏EX2 小时前
Android Google Widevine
android
HealthScience5 小时前
【Bib 2026】基因最新综述(有什么任务、benchmark、代表性模型)
android·开发语言·kotlin
夏沫琅琊6 小时前
Android拨打电话技术文档
android·kotlin
a2591748032-随心所记6 小时前
android studio gradle快速编译配置
android·android studio
一块小土坷垃7 小时前
# 《电影猎手》观影伴侣:一款支持iOS/安卓/电视盒子的全平台影视工具“电影猎手”(附自用评价)
android·ios·电视盒子
敲代码的鱼哇9 小时前
发送短信/拨打电话/获取联系人能力 UTS 插件(cz-sms)
android·前端·ios·uni-app·安卓·harmonyos·鸿蒙
用户5052372099159 小时前
Android 13/14 通知权限与前台服务适配指南
android