《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");
}
}