Android使用WebView实现离线阅读功能
1、先看效果图,加载动画:
加载完成,注意当前为飞行模式!
2、使用
1)、让你的javabean实现OffLineLevelItem接口,因为我的这个离线阅读支持多级下载,比如Demo中的每个频道下面的第一页item都可以缓存。
package com.zgh.offlinereader;
import java.util.List;
public interface OffLineLevelItem {
//是否有下一级
boolean haveNextLevel();
//内容url
String getWebUrl();
//下一级的url
String getNextLevelListUrl();
//生成下一级
List<OffLineLevelItem> getNextLevelList(String jsonStr);
}
public class Channel implements OffLineLevelItem {
String title;
String url;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
@Override
public boolean haveNextLevel() {
return true;
}
@Override
public String getWebUrl() {
return null;
}
@Override
public String getNextLevelListUrl() {
return url;
}
@Override
public List<OffLineLevelItem> getNextLevelList(String jsonStr) {
List<OffLineLevelItem> items = GsonUtil.jsonToBeanList(jsonStr, NewsItem.class);
return items;
}
}
2)、初始化
OfflineReaderServer.init(this, getCacheDir(), new MyFirstLevel(),new WaterWaveProgressUI(this));
3)、启动
@Override
public void onClick(View v) {
Intent intent=new Intent(this, OfflineReaderServer.class);
startService(intent);
}
4)、记得在你的webview使用前调用
//设置缓存目录
WebViewHelper.setWebViewConfig(webView);
就这么简单!
实现
首先我们为什么要使用webview实现离线阅读,因为简单。webview自带的缓存机制可以实现图片,js,css的缓存。不然你自己得实现数据库,html下载,js下载,css保存,html的拼装。下面我将讲解一些webview设置缓存,实现多级下载,webview遍历url,webview显示完成监听。
1.WebView设置缓存
这一部分比较简单,主要是缓存目录的设置,然后设置缓存模式为:
WebSettings.LOAD_CACHE_ELSE_NETWORK
这种模式下webview会优先加载本地缓存,如果没有缓存的话再加载网络。
mWebView.getSettings().setRenderPriority(WebSettings.RenderPriority.HIGH);
// 建议缓存策略为,判断是否有网络,有的话,使用LOAD_DEFAULT,无网络时,使用LOAD_CACHE_ELSE_NETWORK
mWebView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); // 设置缓存模式
// 开启DOM storage API 功能
mWebView.getSettings().setDomStorageEnabled(true);
// 开启database storage API功能
mWebView.getSettings().setDatabaseEnabled(true);
// String cacheDirPath = getFilesDir().getAbsolutePath()
// + APP_CACHE_DIRNAME;
String cacheDirPath = ConfigUtil.getCacheDir()
+ APP_CACHE_DIRNAME;
Log.i(TAG, "cachePath=" + cacheDirPath);
// 设置数据库缓存路径
mWebView.getSettings().setDatabasePath(cacheDirPath); // API 19 deprecated
// 设置Application caches缓存目录
mWebView.getSettings().setAppCachePath(cacheDirPath);
// 开启Application Cache功能
mWebView.getSettings().setAppCacheEnabled(true);
mWebView.getSettings().setAppCacheMaxSize(MAX_SIZE);
2.多级缓存
我的项目中需要将每个频道的首页中的每个item都缓存下来,所以涉及到多级缓存于是我设计了一个接口在离线阅读的时候最重要的是拿到叶子节点也就是每个item的url地址,如果是每叶子节点也就是haveNextLevel()返回true的时候就调用getNextLevelListUrl获取下一级的url,一般都是Jason字符串,再把json字符串传入getNextLevelList()方法获取下一级,如果到达叶子节点,则调用getWebUrl()获取url地址保存在一个集合中,当所有的url都获取以后,就开始用webview遍历url实现缓存。
public interface OffLineLevelItem {
//是否有下一级
boolean haveNextLevel();
//内容url
String getWebUrl();
//下一级的url
String getNextLevelListUrl();
//生成下一级
List<OffLineLevelItem> getNextLevelList(String jsonStr);
}
频道的javabean
public class Channel implements OffLineLevelItem {
String title;
String url;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
@Override
public boolean haveNextLevel() {
return true;
}
@Override
public String getWebUrl() {
return null;
}
@Override
public String getNextLevelListUrl() {
return url;
}
@Override
public List<OffLineLevelItem> getNextLevelList(String jsonStr) {
List<OffLineLevelItem> items = GsonUtil.jsonToBeanList(jsonStr, NewsItem.class);
return items;
}
}
item的javabean
public class NewsItem implements OffLineLevelItem{
String title;
String url;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
@Override
public String toString() {
return title;
}
@Override
public boolean haveNextLevel() {
return false;
}
@Override
public String getWebUrl() {
return url;
}
@Override
public String getNextLevelListUrl() {
return null;
}
@Override
public List<OffLineLevelItem> getNextLevelList(String jsonStr) {
return null;
}
}
当然为了获取到频道列表需要一个第一级的目录,而这个目录在初始化的时候就设置进去了。
public class MyFirstLevel implements OffLineLevelItem {
@Override
public boolean haveNextLevel() {
return true;
}
@Override
public String getWebUrl() {
return null;
}
@Override
public String getNextLevelListUrl() {
return "raw://news_list";
}
@Override
public List<OffLineLevelItem> getNextLevelList(String jsonStr) {
List<OffLineLevelItem> items = GsonUtil.jsonToBeanList(jsonStr, Channel.class);
return items;
}
}
3.使用WebView遍历URL,我原来的思路是给webview设置WebViewClient然后重写onPageFinished方法,在这个方法中获取下一个需要换成的url,然后再调用webview.loadurl()结果是很多页面加载出来是空的。而且在Android4.4以上onPageFinished会调用两次
于是乎,我重写了WebView的OnDraw()方法,在OnDraw()方法里设置了一个监听回调,但是由于我的WebView是在Service中创建的所以ondraw方法根本不会调用,但是这难得的我吗?,呵呵,于是我在service的onCreat方法中使用WindowManger将webview添加到屏幕,长宽都是一个像素
@Override
public void onCreate() {
super.onCreate();
if (!haveInit) {
throw new RuntimeException("请先调用init()方法,初始化OfflineReaderServer");
}
windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
params=new WindowManager.LayoutParams();
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
params.gravity = Gravity.LEFT | Gravity.TOP;
params.width = 1;
params.height = 1;
initWebView();
windowManager.addView(mWebView,params);
}
结果还是很明显的大部分的页面都能缓存下来,但是任然有部分页面是空白的,后来发现webview的OnDraw()方法会多次持续,webview的页面加载时间隙的,我通过getContentHeight()判断内容高度来实现显示完成的监听,结果任然不理想。于是我最终版是这样的
/**
* 可以监听显示完成的webview
*/
public class LoadWebView extends WebView {
private boolean isRendered = false;
private static final int MSG_FINISH=1;
private static final int MIN_CONTENT_HEIGHT=1000;
public LoadWebView(Context context) {
this(context, null);
}
public LoadWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}
private int contentHeight=MIN_CONTENT_HEIGHT;
Handler handler=new Handler(Looper.getMainLooper()){
@Override
public void handleMessage(Message msg) {
if(msg.what==MSG_FINISH) {
if (finishListenter != null) {
finishListenter.onFinish();
contentHeight=MIN_CONTENT_HEIGHT;
}
}
}
};
@Override
protected void onDraw(Canvas canvas) {
//与上一次的contentHeight比较,如果比上一次大,说明还在加载
if(getContentHeight()>=contentHeight){
//更新contentHeight
contentHeight=getContentHeight();
//取消消息
handler.removeMessages(MSG_FINISH);
//延迟200ms发送,如果在200ms内webview又加载了则这条消息会被取消,知道webview加载完成,
//这条消息会被发送,所以每离线一个页面有200ms的延迟,但是与功能相比这点是可以接受的。
handler.sendEmptyMessageDelayed(MSG_FINISH,200);
}
}
public interface OnLoadFinishListenter{
void onFinish();
}
private OnLoadFinishListenter finishListenter;
public void setFinishListenter(OnLoadFinishListenter listenter){
finishListenter=listenter;
}
}
3.进度提示
为了让用户知道离线的进度我抽取出了一个接口
public interface OffLineProgressUI {
void showProgress();
void closeProgress();
void updateProgress(int progress);
}
并默认实现了一个水波纹的进度球
设置进度提示有两种方式,一种是在初始化的时候设置
OfflineReaderServer.init(this, getCacheDir(), new MyFirstLevel(),new WaterWaveProgressUI(this));
还有一种是调用OfflineReaderServer的setProgressUI方法
public static void setProgressUI(@NonNull OffLineProgressUI progressUI) {
sProgressUI = progressUI;
}
源码地址:
https://github.com/zhuguohui/OffLineReaderDemo
到这里就结束啦.