笔者因公司需求,从0打造一款WebApp,一直维护到现在。整个接口算是从混乱到现在的有序。笔者也从一个WebView H5的小菜鸟,磨炼成了中等生。 WebApp简单来讲,就是利用原生的WebView承载H5的html页面,并且实现JS和原生之间的通信。 WebApp的好处是显而易见的。业务页面来源于H5,原生作为一个承载壳提供流畅性支持,能够低成本的实现跨平台的实施以及快速嵌入微信小程序、钉钉、OA等APP中。与纯H5的App相比较,它能够更轻易的使用原生底层库,并且更加流畅;而与纯原生的相比较,它实现了跨平台,能够通过H5的特性快速嵌套进其他APP中。
核心类:
image.png
WebViewActivity:
代码语言:javascript复制public class WebViewActivity extends Activity {
private final String TAG = "WebViewActivity";
private final String PRE = "protocol://android";
private WebView webView;
private String url = "http://222.211.90.120:6071/td_mobile/mingtu/login/html/login.html";
private CustomWebChromeClient webChromeClient;
private NullPageControll nullPageControll;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
webView = findViewById(R.id.webview);
nullPageControll=new NullPageControll(this,webView);
WebSettingUtil.getInstance(webView,this).initWebSetting();
webView.loadUrl(url);
webView.setWebViewClient(new CustomWebViewClient(PRE,this,nullPageControll));
webChromeClient=new CustomWebChromeClient(this,nullPageControll);
webView.setWebChromeClient(webChromeClient);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
WebViewH5Order.parseOnActivityForResult(webView,this,requestCode,resultCode,intent);
webChromeClient.forActivityResult(requestCode,resultCode,intent);
}
}
我们的WebView页面,这里是所有拓展设置的入口,为了尽量减少这个类的代码量,让其清晰,所以分离出了拓展设置,形成了核心类中的其他内容。 &emsp: PRE:该常量可以理解为一个规则,在JS和原生通信的时候作为唯一标识
WebSettingUtil:
代码语言:javascript复制public class WebSettingUtil {
private static WebSettingUtil webSettingUtil;
private static WebView webView;
private static Context context;
private WebSettings webSettings;
private WebSettingUtil(){
}
public static synchronized WebSettingUtil getInstance(WebView webView, Context context){
if(webSettingUtil==null) webSettingUtil=new WebSettingUtil();
WebSettingUtil.context=context;
WebSettingUtil.webView=webView;
return webSettingUtil;
}
public void initWebSetting(){
webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);//设置支持javascrip
webSettings.setRenderPriority(WebSettings.RenderPriority.HIGH);
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);//设置缓存模式
webSettings.setDomStorageEnabled(true);//是否支持持久化存储,保存到本地
webSettings.setAppCacheMaxSize(1024 * 1024 * 8);
String appCachePath = context.getCacheDir().getAbsolutePath();
webSettings.setAppCachePath(appCachePath);//设置数据库缓存路径
webSettings.setAllowFileAccess(true);
webSettings.setDatabaseEnabled(true);//开启database storage API功能
webSettings.setAppCacheEnabled(true);//设置开启Application H5 Caches功能
if (Build.VERSION.SDK_INT >= 19) {
webSettings.setLoadsImagesAutomatically(true);
} else {
webSettings.setLoadsImagesAutomatically(false);
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
webSettings.setDatabasePath("/data/data/" webView.getContext().getPackageName() "/databases/");
}
// 设置可以支持缩放
webSettings.setSupportZoom(false);//设置是否支持缩放
webSettings.setBuiltInZoomControls(false);//设置是否出现缩放工具
webSettings.setDisplayZoomControls(true); // 隐藏webview缩放按钮
// 自适应屏幕
webSettings.setUseWideViewPort(true);
webSettings.setLoadWithOverviewMode(true);
webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
webSettings.setSavePassword(true);
webSettings.setSaveFormData(true);
}
}
WebView的基本设置,笔者已经做了好了常用设置,在WebViewActivity调用
void initWebSetting()
即可。 在WebSettings的设置中可以对浏览器的常用设置进行配置。例如:对javascrip的支持、缓存模式以及本地持久化保存相关设置、浏览器缩放设置等等。
CustomWebViewClient
代码语言:javascript复制public class CustomWebViewClient extends WebViewClient {
private final String TAG = "CustomWebViewClient";
private String pre = "";//约定的字段,用于拦截浏览器跳转,然后自定义操作
private Activity activity;
private NullPageControll nullPageControll;
private boolean isError=false;
public CustomWebViewClient(String pre, Activity activity, NullPageControll nullPageControll) {
this.pre = pre;
this.activity = activity;
this.nullPageControll = nullPageControll;
}
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Log.i(TAG, "拦截到的url----" url);
if (url.contains(pre)) {
Map<String, String> map = getParamsMap(url, pre);
String code = map.get("code");
String data = map.get("data");
WebViewH5Order.parseCodeOrder(view, activity, null, code, data);
return true;
} else {
//。。。这里执行浏览器的正常跳转
return false;
}
}
//开始载入页面调用的,我们可以设定一个loading的页面,告诉用户程序在等待网络响应。
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Log.i(TAG, "onPageStarted");
nullPageControll.beginload();
}
//在页面加载结束时调用。我们可以关闭loading条,切换程序动作
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.i(TAG, "onPageFinished");
if (!isError){
nullPageControll.initNullPage();
}else{
nullPageControll.errorload();
isError=false;
}
}
//在加载页面资源时会调用,每一个资源(比如图片)的加载都会调用一次。
@Override
public void onLoadResource(WebView view, String url) {
super.onLoadResource(view, url);
// Log.i(TAG,"onLoadResource");
}
//加载页面的服务器没有网络或者超时,触发
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
Log.i(TAG, "onReceivedError");
isError=true;
nullPageControll.changeErrorImage(R.mipmap.notinternet);
}
@TargetApi(android.os.Build.VERSION_CODES.M)
@Override
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
super.onReceivedHttpError(view, request, errorResponse);
int statusCode = errorResponse.getStatusCode();
if (!request.getUrl().toString().contains(".html")) return;
Log.i(TAG, "onReceivedHttpError——" statusCode "turl——" request.getUrl());
if (404 == statusCode || 500 == statusCode) {
isError=true;
nullPageControll.changeErrorImage(R.mipmap.notpage);
}
}
//处理https请求
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
super.onReceivedSslError(view, handler, error);
Log.i(TAG, "onReceivedSslError");
}
/**
* 通过拦截到的URL和约定的标识,获取HTML传递过来的信息
*
* @param url
* @param pre
* @return
*/
private Map<String, String> getParamsMap(String url, String pre) {
ArrayMap<String, String> queryStringMap = new ArrayMap<>();
if (url.contains(pre)) {
int index = url.indexOf(pre);
int end = index pre.length();
String queryString = url.substring(end 1);
String[] queryStringSplit = queryString.split("&");
String[] queryStringParam;
for (String qs : queryStringSplit) {
if (qs.toLowerCase().startsWith("data=")) {
//单独处理data项,避免data内部的&被拆分
int dataIndex = queryString.indexOf("data=");
String dataValue = queryString.substring(dataIndex 5);
queryStringMap.put("data", dataValue);
} else {
queryStringParam = qs.split("=");
String value = "";
if (queryStringParam.length > 1) {
//避免后台有时候不传值,如“key=”这种
value = queryStringParam[1];
}
queryStringMap.put(queryStringParam[0].toLowerCase(), value);
}
}
}
return queryStringMap;
}
}
CustomWebViewClient是WebViewClient的子类,在WebView中通过
webView.setWebViewClient(new CustomWebViewClient(PRE,this,nullPageControll));
Override shouldOverrideUrlLoading():
重写该方法,拦截浏览器打开以及跳转时的url,可以通过拦截到的url与pre比对,实现js和原生的通信。值得一提的是这列也可以用来处理因为HTTP劫持导致打开H5页面出现广告的问题。Override onPageStarted():
页面开始加载时回调Override onPageFinished():
页面加载结束后回调,在改方法执行前,原生是无法与js通信的Override onReceivedError():
当没有网络,或者链接超时是触发Override onReceivedHttpError():
当加载页面发生报错的时候回调,例如404/500等
CustomWebChromeClient:
代码语言:javascript复制public class CustomWebChromeClient extends WebChromeClient {
private final String TAG="CustomWebChromeClient";
private ValueCallback<Uri> mUploadMessage;
public ValueCallback<Uri[]> uploadMessage;
public static final int REQUEST_SELECT_FILE = 100;
private final static int FILECHOOSER_RESULTCODE = 2;
private NullPageControll nullPageControll;
private Activity activity;
public CustomWebChromeClient(Activity activity,NullPageControll nullPageControll){
this.activity=activity;
this.nullPageControll=nullPageControll;
}
//获取网页的加载进度并显示
@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
Log.i(TAG,"onProgressChanged——" newProgress);
}
//获取Web网页中的标题
@Override
public void onReceivedTitle(WebView view, String title) {
Log.i(TAG,"onReceivedTitle——" title);
if(isChinese(title)) nullPageControll.setTitle(title);
// android 6.0 以下通过title获取加载错误的信息
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
if (title.contains("404") || title.contains("500") || title.contains("Error")) {
nullPageControll.changeErrorImage(R.mipmap.notpage);
nullPageControll.errorload();
}
}
}
// For Lollipop 5.0 Devices
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public boolean onShowFileChooser(WebView mWebView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
if (uploadMessage != null) {
uploadMessage.onReceiveValue(null);
uploadMessage = null;
}
uploadMessage = filePathCallback;
Intent intent = fileChooserParams.createIntent();
try {
activity.startActivityForResult(intent, REQUEST_SELECT_FILE);
} catch (ActivityNotFoundException e) {
uploadMessage = null;
Log.i(TAG, "Cannot Open File Chooser");
return false;
}
return true;
}
private boolean isChinese(String str){
String regEx="^[\u0391-\uFFE5] $ ";
return Pattern.compile(regEx).matcher(str).matches();
}
/**
* Activity回调调用
* @param requestCode
* @param resultCode
* @param intent
*/
public void forActivityResult(int requestCode, int resultCode, Intent intent){
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (requestCode == REQUEST_SELECT_FILE) {
if (uploadMessage == null)
return;
uploadMessage.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, intent));
uploadMessage = null;
}
} else if (requestCode == FILECHOOSER_RESULTCODE) {
if (null == mUploadMessage)
return;
// Use MainActivity.RESULT_OK if you're implementing WebView inside Fragment
// Use RESULT_OK only if you're implementing WebView inside an Activity
Uri result = intent == null || resultCode != WebViewActivity.RESULT_OK ? null : intent.getData();
mUploadMessage.onReceiveValue(result);
mUploadMessage = null;
} else{
Log.e(TAG,"Failed to Upload Image");
}
}
// For 3.0 Devices (Start)
// onActivityResult attached before constructor
protected void openFileChooser(ValueCallback uploadMsg, String acceptType) {
mUploadMessage = uploadMsg;
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("image/*");
activity.startActivityForResult(Intent.createChooser(i, "File Browser"), FILECHOOSER_RESULTCODE);
}
//For Android 4.1 only
protected void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
mUploadMessage = uploadMsg;
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
activity.startActivityForResult(Intent.createChooser(intent, "File Browser"), FILECHOOSER_RESULTCODE);
}
protected void openFileChooser(ValueCallback<Uri> uploadMsg) {
mUploadMessage = uploadMsg;
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("image/*");
activity.startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_RESULTCODE);
}
}
CustomWebChromeClient是WebChromeClient的子类,在WebView中通过
webChromeClient=new CustomWebChromeClient(this,nullPageControll); webView.setWebChromeClient(webChromeClient);
引用。它可以辅助 WebView 处理 Javascript 的对话框,网站图标,网站标题等等。
NullPageControll
代码语言:javascript复制public class NullPageControll {
private Activity activity;
private ViewGroup nullpage,reloadpage;
private TextView tv_reload,title;
private ImageView img_back,img_error;
private ProgressBar progressBar;
private WebView webView;
private int progress=0;
public NullPageControll(Activity activity, WebView webView){
this.activity=activity;
this.webView=webView;
findView();
init();
}
private void init(){
tv_reload.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
webView.reload();
}
});
img_back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(webView.canGoBack()){
webView.goBack();
}else{
activity.finish();
}
}
});
}
private void findView(){
nullpage=activity.findViewById(R.id.nullpage);
reloadpage=activity.findViewById(R.id.reloadpage);
tv_reload=activity.findViewById(R.id.reload);
img_back=activity.findViewById(R.id.back);
title=activity.findViewById(R.id.title);
progressBar=activity.findViewById(R.id.progressbar);
img_error=activity.findViewById(R.id.errorimg);
}
public void setTitle(String str){
title.setText(str);
}
/**
* 开始加载时调用
*/
public void beginload(){
isShowNullPage(true);
isShowReloadPage(false);
startProgress();
}
/**
* 错误加载时调用
*/
public void errorload(){
isShowReloadPage(true);
isShowReloadPage(true);
finishProgress();
}
/**
* 将各个部件还原初始状态
*/
public void initNullPage(){
progress=0;
progressBar.setProgress(progress);
progressBar.setVisibility(View.GONE);
isShowNullPage(false);
isShowReloadPage(false);
}
/**
* 更换错误加载时显示的图片,默认显示页面不存在的提示
* @param resource
*/
public void changeErrorImage(int resource){
img_error.setImageResource(resource);
}
/**
* 是否显示nullpage页面
* @param isShow
*/
private void isShowNullPage(boolean isShow){
nullpage.setVisibility(isShow?View.VISIBLE:View.GONE);
}
/**
* 是否显示reloadpage页面
* @param isShow
*/
private void isShowReloadPage(boolean isShow){
reloadpage.setVisibility(isShow?View.VISIBLE:View.GONE);
}
/**
* 开始加载动画
* 这是一个假的进度条动画,目的是有个更好的体验效果
*/
private void startProgress(){
progress=0;
progressBar.setProgress(progress);
progressBar.setVisibility(View.VISIBLE);
new Thread(new Runnable() {
@Override
public void run() {
while(progress<95){
try {
Thread.sleep(20);
progress =1;
progressBar.setProgress(progress);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
/**
* 结束加载动画
*/
private void finishProgress(){
progress=100;
progressBar.setProgress(progress);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
progressBar.setVisibility(View.GONE);
setTitle("");
}
},100);
}
}
NullPageControll是对WebView用户体验的一个优化类。提供加载页和错误页的控制器。 H5有一个通病在于在网络不流畅的时候,点击页面跳转时,因为会先请求页面html导致卡顿,整个页面没有反应,跟卡死了一样。IOS稍微好一点,Android尤为明显。为了解决这个问题,我们需要利用原生自己绘制一个请求html页面时的加载动画页面和错误加载提示页。这样可以大幅度的提高用户的体验。而我们可以通过WebViewClient提供的回调去处理各个阶段的状态。
附件:
GitHub下载地址
笔者还在学习中,文章大多以笔记的风格为主。欢迎留言交流沟通,不喜勿喷。