阅读之前推荐阅读博客大佬的这2篇
Android开发:最全面、最易懂的Webview使用详解
最全面总结 Android WebView与 JS 的交互方式
本文作者: @youyuge
个人博客站点: https://youyuge.cn
参考自imooc实战课程,感谢猿猿老师,另外猿猿老师让我发一下课程链接~~(笑,非广告)http://coding.imooc.com/learn/list/116.html
一、回顾与规划
回顾一下,我们在第一、二章中已经完成了一些封装:
我们已经建立了图中全部的东西(除了最左边的那个特殊子类),现在我们已经可以正常的使用,new一个默认子类
WebDelegateDefault
,就能获得一个里面是webView的Fragment,并且,它已经初始化三部曲配置完毕,生命周期也根本无需我们担心~-
WebViewClientDefault
可是,我们上节课中,我们用内部匿名类的方式,简单的给默认子类设置了一个简陋的WebViewClient
,我们这节课想建立一个单独的默认WebViewClientDefault.java
类,在里面完成网页的loading界面。
基本的封装到这里正式完结了~~~可是,我们还有业务需求呢!
-
WebDelegateFirst 与 WebViewClientFirst
终于,我们迎来了业务需求,我们封装的一套东西终于要有用武之地了!简单地说,根据我们的业务需求,我们创立了两个文件,一个是特殊的webView碎片,一个是特殊的webViewClient,具体关系如下图,具体的业务实现我们正文再说:
二、默认Client的实现——WebViewClientDefault.java
- 准确的说,它应该放在上一篇中来写,因为它算WebView初始化三部曲里的第二步。但是,上一篇篇幅过长,就拿到这次来说了。首先先写一个页面开始和结束的接口,方便以后用到:
/**
* @function webView加载进度回调
* Created by 尤晟 on 2017-08-02.
*/
public interface IPageLoadListener {
void onLoadStart();
void onLoadEnd();
}
- 下面就是我们的默认的Client——WebViewClientDefault的代码:
/**
* @function 默认的 WebViewClient实体类,页面内跳转,带loading
* Created by 尤晟 on 2017-08-02.
*/
public class WebViewClientDefault extends WebViewClient {
protected final WebDelegate DELEGATE;
private IPageLoadListener mIPageLoadListener = null;
//这是我之前自己封装好的一个全局Handler
private Handler HANDLER = Latte.getHandler();
public void setIPageLoadListener(IPageLoadListener mIPageLoadListener) {
this.mIPageLoadListener = mIPageLoadListener;
}
public WebViewClientDefault(WebDelegate DELEGATE) {
this.DELEGATE = DELEGATE;
}
//表示重定向和url跳转,由这个webView自己来处理,不会打开外部浏览器
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return false;
}
//页面开始加载时回调(页面后退也会回调)
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
if(mIPageLoadListener!=null){
mIPageLoadListener.onLoadStart();
}
//之前自己封装的loading工具类,大家可替换成自己的,github多得是
LatteLoader.showLoading(view.getContext());
}
//页面完成加载时回调(返回页面也会回调)
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if(mIPageLoadListener != null){
mIPageLoadListener.onLoadEnd();
}
//加一个延时,让效果更加明显,可去除
HANDLER.postDelayed(new Runnable() {
@Override
public void run() {
//之前自己封装的loading工具类,大家可替换成自己的,github多得是
LatteLoader.stopLoading();
}
},600);
}
}
- 别忘了,在我们的默认webDelegate中,要把client改成我们上面创建的默认client
//WebDelegateDefault中进行改动
@Override
public WebViewClient initWebViewClient() {
WebViewClientDefault clientDefault = new WebViewClientDefault(this);
//设置页面start和end时候的回调,这里我们只是留了个接口,以后之后扩展,所以传入null了
clientDefault.setIPageLoadListener(null);
return clientDefault;
}
至此,基础架构全部封装完毕,当然js和native交互方面还可以继续来封装,以后再说。现在,我们把焦点放到业务实现上。
三、业务需求实现
3.1 业务需求与效果图
我们现在默认的是点击页面内链接后直接在内部跳转,但是,第一次点击不应该是这样。比如淘宝,京东的发现页面,上下滑动的时候,是看的到底部的那个tab的,而第一次点击某个链接后,会跳转到一个全屏的WebView,之后的链接跳转全部在这个WebView里实现,后退键也是回退到上一个页面,最后才返回到我们的app主界面,最终效果如下gif图所示:
3.2 需求分析与实现方案
我们看到,一开始加载的webDelegate是在发现tab中,看的到toolbar和底部栏,但是当我们点击了某个链接后,就跳转到了一个新的全屏WebView中,之后的页面前进/回退都是在这个WebDelegate里面。
经过观察,我们发现有两个WebDelegate,而第二个就是我们默认实现的WebDelegateDefault类。而第一个WebDelegate就是我们的特殊类,需要实现的特殊功能是:点击页面内链接后,开启一个新的WebDelegateDefault。
-
我们再来看下架构图:
3.3 代码实现
经过上面的实现分析,我们发现,要实现点击页面内链接后,开启一个新的WebDelegateDefault
功能。大家都想到了,重写shouldOverrideUrlLoading
方法不就分分钟搞定了嘛~确实如此,需要改动的地方有:
重写
shouldOverrideUrlLoading
方法--->所以要新建一个特殊的Client类特殊的
WebDelegateFirst
类里,不用拦截返回键了:毕竟就一个页面,不用再点击返回键网页后退了。
分析完毕,好像就这两个点要改动,那么接下来就轻而易举了,建立两个类,WebDelegateFirst
和 WebViewClientFirst
。
WebDelegateFirst
中,代码真是干净的不行,多亏了我们之前的封装:
/**
* @function 默认的特殊具体实现子类, 首次点击内部链接后会开启新的Delegate
* Created by 尤晟 on 2017-07-30.
*/
public class WebDelegateFirst extends WebDelegateDefault {
public static WebDelegateFirst create(String url) {
final Bundle bundle = new Bundle();
bundle.putString(RouteKeys.URL.name(), url);
final WebDelegateFirst delegate = new WebDelegateFirst();
delegate.setArguments(bundle);
return delegate;
}
@Override
public WebViewClient initWebViewClient() {
final WebViewClientFirst client = new WebViewClientFirst(this);
return client;
}
@Override
public boolean onBackPressedSupport() {
return false;
}
}
重头戏WebViewClientFirst
来了,写之前,大家要注意shouldOverrideUrlLoading
这个方法有大坑!!!
shouldOverrideUrlLoading
方法触发的条件有:
点击了页面内的某个链接,跳转
重定向,跳转
注意,loadUrl
方法不会触发 shouldOverrideUrlLoading
方法。那么问题来了,当我们在 WebViewClientFirst
的shouldOverrideUrlLoading
方法中,start第二个WebDelegate
(即WebDelegateDefault
),理论上是没问题的。
可是如果遇到了首页重定向,我们的第一个WebDelegateFirst
还没加载完就会触发 shouldOverrideUrlLoading
方法,就会立刻在第二个WebDelegate
中开启页面,我们的第一个WebDelegateFirst
会成空白页面。
举个例子,我们让
WebDelegateFirst
去loadUrl("m.baidu.com");
一切都正常,因为这个过程不会触发shouldOverrideUrlLoading
方法,只触发onPageStarted
和onPageFinished
。我们点击了某个页面内url后,才会执行shouldOverrideUrlLoading
方法,然后在这个方法中截获url,开启第二个全屏WebDelegateDefault
。
而我们让
WebDelegateFirst
去loadUrl("www.baidu.com");
的时候,问题来了,它会立刻自己重定向到"m.baidu.com"手机版百度,而这个重定向是会触发shouldOverrideUrlLoading
方法的,导致立刻会开启第二个全屏WebDelegateDefault
,显示"m.baidu.com"页面,第一个WebDelegateFirst
是白屏。
在这个情况下,方法调用顺序为:onPageStarted--->shouldOverrideUrlLoading--->onPageStarted--->onPageFinished。
因此,重定向问题解决方案:设置一个false标志位,只有经过了onPageFinished
方法,才变为为true。而只有true时候,shouldOverrideUrlLoading
方法才拦截。
/**
* @function 一个client的具体特殊实现
* Created by 尤晟 on 2017-07-30.
*/
public class WebViewClientFirst extends WebViewClientDefault {
//过滤重定向。应用内部页面是否加载完毕,因为重定向也会触发shouldOverrideUrlLoading方法
private boolean isPageOk = false;
public WebViewClientFirst(WebDelegate DELEGATE) {
super(DELEGATE);
}
// 复写shouldOverrideUrlLoading()方法,
//若没有设置 WebViewClient 则在点击链接之后由系统处理该 url,通常是使用浏览器打开或弹出浏览器选择对话框。
// 若设置 WebViewClient 且该方法返回 true ,则说明由应用的代码处理该 url,WebView 不处理。
// 若设置 WebViewClient 且该方法返回 false,则说明由 WebView 处理该 url,即用 WebView 加载该 url。
//使用旧方法,兼容旧机型,在网页上的所有加载都经过这个方法,这个函数我们可以做很多操作。
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
LatteLogger.d("shouldOverrideUrlLoading", url);
if (isPageOk) {
return Router.getInstance().handleWebUrl(DELEGATE, url);
} else
return false;
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
LatteLogger.d("onPageStart:" + url);
super.onPageStarted(view, url, favicon);
}
@Override
public void onPageFinished(WebView view, String url) {
LatteLogger.d("onPageFinish: " + url);
isPageOk = true;
super.onPageFinished(view, url);
}
}
-
Router
路由类中增加开启第二个WebDelegateDefault
的方法:
public final boolean handleWebUrl(WebDelegate webDelegate, String url) {
//如果js里包含电话协议
if (url.contains("tel:")) {
callPhone(webDelegate.getContext(), url);
return true;
}
//必须要是第一个WebDelegate的父布局开启一个新的delegate,这样才是全屏,否则还是下面有bar,上面有toolbar
final LatteDelegate topDelegate = webDelegate.getTopDelegate();
final WebDelegate toWebDelegate = WebDelegateDefault.create(url);
topDelegate.getSupportDelegate().start(toWebDelegate);
return true;
}
private void callPhone(Context context, String uri) {
final Intent intent = new Intent(Intent.ACTION_DIAL);
final Uri data = Uri.parse(uri);
intent.setData(data);
ContextCompat.startActivity(context, intent, null);
}
四、总结
基本的封装到此结束,可能还会出一篇有关js和native交互的进一步封装,如果大家看到这里了,那就别吝啬,点个喜欢,点个关注吧~~嘿嘿嘿嘿