2014年10月29日,万维网联盟宣布,经过接近8年的艰苦努力,该标准(HTML5)规范终于制定完成。接着,整个移动端开发领域开始了一场翻天覆地的改革变迁,主要「受益」者, ios , Android.
现如今的形势无疑使这个行业的门槛抬的非常高,我们都知道以前会写几个 demo 工作就没问题了,这是移动开发正火热的时期。之后接着有很多同学的自学效率不错,凭着一己之力,培训也算,每年大量新人涌入这个高薪行业!
前两天吃饭的时候 HR 告诉我说,他说两年前招 Android 的时候给出的价格是 15k,而且还招不到,如今 10 都不愿意给了,发一个职位来的人排着队面试 !可见移动行业的现状的确是跟之前的差距拉的太大。
包括我刚出来的时候基本没有接触过 WebApp 领域的东西,更不会去娴熟的利用 WebView 做着各种交互,以至于找工作就费了很大的力气,如今,Hybrid App 已然是绝对的主流地位!
什么是 Hybrid App
Native App(原生界面)+ WebApp (基于 H5 的 WebView 网页)== Hybrid App(混合型)
那这样一个公式就说的很清楚了,所谓的 Hybrid 就是原生与网页混合开发,有时候原生界面调 H5 页面,有时 H5 网页又来调原生,为什么会衍生出这种开发模式呢?H5 可跨平台,一套页面 ios 和 android 公用,开发成本较低,再者网页不占内存等等,一张图来稍微概括:
关于三者之间的对比这只是片面的,更多详细的资料大家自行搜索查看!虽然前两者的体验与性能都不会优于后者,但是大势所趋,我们谁也改变不了!
- 使用 WebView 加载网页
这里没什么好说的,直接上代码:
<!-- 页面布局 -->
<WebView
android:id="@+id/web"
android:layout_width="match_parent
android:layout_height="match_parent" />
***第一部分 :
//加载显示网页
mWebView = (WebView) mMainContainer.findViewById(R.id.wv_container);
// 设置加载网页的地址
mWebView.loadUrl("http://www.baidu.com");WebSettings settings = mWebView.getSettings();
// 启用 JavaScript 支持(WebView的设置)
settings.setJavaScriptEnabled(true);
new WebViewClient解决跳转到第三方浏览器*
这里面处理行对应的页面逻辑*/mWebView.setWebViewClient(new WebViewClient() {
//处理页面逻辑的方法 详细举例在下一片段
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
mWebView.loadUrl(url);
return true;
}
// 页面开始加载
@Override public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
if (!mDialog.isShowing())
mDialog.show(); //显示加载进度条
}
/**
* 页面加载完成
* @param view
* @param url
*/ @Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (mDialog.isShowing())
mDialog.dismiss(); //加载完成 隐藏进度条
}
});
我们首先利用 WebView 加载了这个网页,其次给他设置,解决浏览器跳转,显示加载进度条等等!
相信大家都很明白这还只是入门 Demo 的写法,那么在项目中我们是怎么处理这些设置和逻辑呢?
第一 :实际项目中的 WebView 存在与多个地方
第二 :原生与 Web 混合型
第三 : 网页大多数有他后台自己加好的逻辑与功能(包括 js 方法),怎么配合!
第四:你必须要做的 Cookie 处理
那么我们将以这几个问题开始进行分享,所以接下来会开是一个 HyBrid 主题系列的文章,估计会有好几篇!
- 建立 Activity 管理类
这一步是项目开发必备,我们面对这越来越多的页面的时候,如果不管理,你总有无从下手的时候!
/**
* Activity 管理类
* Created by ShiYunpeng on 2016/12/28.
*/
public class ActivityCollector {
private static List<Activity> activityList = new ArrayList<>();
//用来管理所有的activity
/**
* 添加当前的Activity到栈中
* @param activity
*/
public static void addActivity(Activity activity){
activityList.add(activity);
}
/**
* 将当前Activity移出返回栈
* @param activity
*/
public static void removeActivity(Activity activity){
activityList.remove(activity);
}
/**
* 获取到栈顶的Activity
* @return
*/
public static Activity getTopActivity(){
if (activityList.isEmpty()){
return null;
}else {
return activityList.get(activityList.size() - 1);
}
}
}
那么这三个方法解决了我们很多问题,包括由于资源未及时释放以及让你措手不及的 ANR,可别小看他!
以上写法的应用:在 Activity 中我们需要在项目初始化的时,用我们建好的这个类 ActivityClooector.addActivity() 来添加我们的 Activity 到栈中,同样的方法,在 Activity 将要销毁的时候去选择 remove 他,后面我们会在一些重要的类中(非 Activity)去用 getToopActivity() 来获取到我们的栈顶 Activity,非常重要!
- 建立 WebViewController 管理类
那么这一步更加重要了,还记得我们第一小节是怎么设置 WebView 的吗?我们是在加载他的 Activity 中来进行设置的,那么当有多个页面时,又当每个页面加载 WebView 所要的设置数量不同的时候,就比较难处理了!
所以这里只用一个类来将项目中所有关于 WebView 的设置,以及 WebView 单层面的处理都放在这里!
先行列出 WebView 中常用设置以及方法
WebSettings
//下面三个最常用,基本都需要设置
setCacheMode 设置缓存的模式 eg: settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
setJavaSciptEnabled 设置是否支持Javascript eg: settings.setJavaScriptEnabled(true);
setDefaultTextEncodingName 设置在解码时使用的默认编码 eg: settings.setDefaultTextEncodingName(“utf-8”);
setAllowFileAccess 启用或禁止WebView访问文件数据 setBlockNetworkImage 是否显示网络图像 setBuiltInZoomControls 设置是否支持缩放
setDefaultFontSize 设置默认的字体大小
setFixedFontFamily 设置固定使用的字体
setLayoutAlgorithm 设置布局方式
setLightTouchEnabled 设置用鼠标激活被选项
setSupportZoom 设置是否支持变焦
WebViewClient
mWebView.setWebViewClient(new WebViewClient(){
这里包含以下方法
});
方法:
onPageStarted 网页开始加载
onReceivedError 报告错误信息
onLoadResource 加载指定地址提供的资源
shouldOverrideUrlLoading 控制新的连接在当前WebView中打开
onPageFinished 网页加载完毕,此方法并没有方法名表现的那么美好,调用时机很不确定。如需监听网页加载完成可以使用onProgressChanged,当int progress返回100时表示网页加载完毕。
doUpdate VisitedHistory 更新历史记录
onFormResubmission 应用程序重新请求网页数据
onScaleChanged WebView发生改变
**WebChromeClient** mWebView.setWebViewClient(new WebChromeClient(){
同样例举出常用方法
});
方法:
onProgressChanged 加载进度条改变
onJsPrompt 用在解决4.2以下addJavascriptInterface漏洞问题
onCloseWindow 关闭WebView
onCreateWindow 创建WebView
onJsAlert 处理Javascript中的Alert对话框
onJsConfirm处理Javascript中的Confirm对话框
onJsPrompt处理Javascript中的Prompt对话框
onReceivedlcon 网页图标更改
onReceivedTitle 网页Title更改
onRequestFocus WebView显示焦点
onConsoleMessage 在Logcat中输出javascript的日志信息
下来列举我对于此模块的处理方法
我在这里建立了一个 WebViewCtroller类,用于将所有能用到设置以及需要添加的功能都写在里面,据我所知,只是现在普遍流行的一种写法,我们开始看,注释很详细!
//项目所在包路径,待会要用
package com.lansum.eip.webview;
public class WebViewController extends WebView {
//声明上下文对象
private Context context;
//声明WebViewController全局对象
private WebViewController control;
//网络未加载完的loading图片
private ImageView imageView;
//截取到的网址上的消息头字符串 表示要进入到那个页面
private String sbFunName = "";
public WebViewController(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
webviewSettings();
}
public WebViewController(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
webviewSettings();
}
public WebViewController(Context context) {
super(context);
webviewSettings();
}
@SuppressLint("JavascriptInterface")
private void webviewSettings() {
control = this;
// TODO Auto-generated constructor stub
WebSettings webSettings = this.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
webSettings.setSupportZoom(false);
webSettings.setBuiltInZoomControls(false);
webSettings.setAllowFileAccess(true);
webSettings.setAllowContentAccess(true);
webSettings.setAllowFileAccessFromFileURLs(true);
webSettings.setAllowUniversalAccessFromFileURLs(true);
webSettings.setDatabaseEnabled(true);
webSettings.setDomStorageEnabled(true);
webSettings.setGeolocationEnabled(true);
webSettings.setAppCacheEnabled(true);
webSettings.setAppCachePath(context.getCacheDir().getPath());
webSettings.setDefaultTextEncodingName("gbk");
// 屏幕自适应
webSettings.setUseWideViewPort(true);
webSettings.setLoadWithOverviewMode(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
} else {
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
webSettings.setDisplayZoomControls(false);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
webSettings.setLoadsImagesAutomatically(true);
} else {
webSettings.setLoadsImagesAutomatically(false);
}
/**禁止屏幕自动旋转*/
this.setScrollBarStyle(this.SCROLLBARS_INSIDE_OVERLAY);
this.setHorizontalScrollBarEnabled(false);
this.setHorizontalFadingEdgeEnabled(false);
this.setVerticalFadingEdgeEnabled(false);
HtmlMessageForLocal newWebViewActivity = new HtmlMessageForLocal();
this.addJavascriptInterface(newWebViewActivity, "android");
/**
* 让网页的弹框转化为原生化的弹框
*/
setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
AlertDialog.Builder builder = new AlertDialog.Builder(context).setTitle("错误提醒")
.setMessage(message)
.setIcon(R.drawable.icon_login)
.setPositiveButton("好", new AlertDialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
});
builder.setCancelable(false);
builder.create();
builder.show();
return true;
}
});
setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 这些页面添加顶部导航
if (url != null && !url.matches(".*"+ Constants.urlLogIn+".*")) {
Intent intent = new Intent(context, NewWebViewActivity.class);
intent.putExtra("url", url);
intent.putExtra("animation",R.anim.slide_right_out);
context.startActivity(intent);
//((Activity)context).overridePendingTransition(R.anim.slide_right_in, R.anim.none);
return true;
} else {
return false;
}
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
}
/**
* WebView开始加载
* 1.通过截取网页 url 中指定的参数确定这个页面
* 2.根据匹配结果而确定所要操作的是哪个页面
* 3.注册广播接收器,接受消息处理类中发过来的广播去刷新数据
* @param view
* @param url
* @param favicon
*/
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
// TODO Auto-generated method stub
super.onPageStarted(view, url, favicon);
control.setTag("");
int hhh = url.indexOf("WebViewRefreshNotification=");
if (hhh != -1){
int xxx = hhh + 27;
sbFunName = url.substring(xxx);
int ttt = sbFunName.indexOf('&');
if (ttt!= -1){
sbFunName = sbFunName.substring(0,ttt);
}
}
/**
* 当最终截取到的字符串不是空的时候
* 再去注册广播来接受发过来的广播消息
*/
if (!sbFunName.equals("")){
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(sbFunName);
context.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(sbFunName)) { //接受广播
String name = intent.getStringExtra("name");
WebViewController.this.loadUrl("javascript:" + name);
}
}
}, intentFilter);
}
/**
* 动态创建网络加载中的GIf图片
*/
imageView = new ImageView(ActivityCollector.getTopActivity().getApplicationContext());
ViewGroup.LayoutParams size = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 80);
imageView.setLayoutParams(size);
Glide.with(ActivityCollector.getTopActivity()).load(R.drawable.loading3).into(imageView);
ViewGroup parentLayout = (ViewGroup) WebViewController.this.getParent();
parentLayout.addView(imageView);
parentLayout.getPaddingRight();
imageView.setVisibility(View.VISIBLE);
}
/**
* WebView加载完成
* @param view
* @param url
*/
@Override
public void onPageFinished(WebView view, String url) {
imageView.setVisibility(View.GONE);
}
});
}
其实说起来代码也不多,根据需求来添加即可。注意,这个类世纪城于 WebView 的!
关于如何知道网页服务端给我们传过来的是什么参数,以便于我们将参数填到所需的地方,这里就以这个在 WebView 中弹出原生的提示框为例!
如果不处理网页的 js 弹框,他是这样的:
那么这样用户体验就不好了!凡是网页时有 js 弹框的地方,我们都可以用在 setWebChromeClient(new WebChromeClient() {} 中 重写父类的 onJsAlert() 来讲之转换为原生弹框。
接下来我们打一个断点来追踪一下网页需要什么参数让我们传,关于网页服务端的所有接口都是这样来看参数的,所以你看好了!
可以看到,我在没有填密码的时候点击了修改密码,它就接着执行了这个方法,并且有一个 message 参数显示的是提示信息,那么我们直接就将这个 message 设置到我们原生弹框中所需要的消息就 OK 了!记住,后面的接口方法都是这么判断,再说一遍!
重点来了,将设置应用到 WebView 中去。
<com.lansum.eip.webview.WebViewController
android:id="@+id/main_webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
还记得上面类第一行的包吧!没错,就是把这个类的包路径直接添加到布局中,像添加自定义布局的方式一样,现在起我们的 WebView 就已经具备了代码中所有的设置!
关于这个类(WebViewController),是我们专门用于处理 WebView 的类,如何去搭配下一个重头戏的类(HtmlMessageForLocal 类)!作用,本地与WebView交互类,根据点击的WebView位置服务端发送相应接口,本地去实现这些接口!非常重要!
此次就到这里,我们下篇文章继续,揭开这个 HtmlMessageForLocal 类的真实面目,并与我们的 WebViewController 类搭配起来完成所有网页的处理,此次分享落幕!