近期,有同事咨询如何在Fragment中使用Cordova,看了下Cordova源码,官方并没有提供包含Cordova Webview的Fragment,以供我们继承。
上网查询了一下,也有几篇文章讲解Fragment中如何使用Cordova,不过Cordova逻辑与Fragment逻辑耦合太深,不太适用于常规项目开发。
通过分析CordovaActivity的源码实现,我们只需要将Cordova封装成自定义View就可以了。后面的演示,咱们还是基于之前的工程吧,代码会在后面分享给大家的。
CordovaView实现的目标:
应像系统Webview一样,与页面逻辑解耦,且方便使用
1、CordovaView的逻辑应独立;
2、能在Fragment中使用;
3、能在Activity中使用;
4、能在弹框中使用;
CordovaView封装
因我这边时间比较紧张,所以不带领大家去分析CordovaActivity的实现原理了,此处直接贴出自定义CordovaView的源码吧:
自定义控件内容,主要包含:
1、CordovaWebView的初始化、UI、URL加载、配合Activity、Fragment生命周期等;
2、读取Cordova配置文件,设置页面相关属性;
3、Crodova接口实现(无需自己写,直接使用Cordova自带的CordovaInterfaceImpl)
4、异常消息回调,以便于使用方自己控制错误提示等;
5、返回真实Webview控件,以便于使用方自行控制goback、reload、websetting等;
6、其他...
CordovaView.java实现 (重要的注释都已加上了):
package com.ccc.ddd;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Color;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.webkit.WebView;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import org.apache.cordova.Config;
import org.apache.cordova.ConfigXmlParser;
import org.apache.cordova.CordovaInterfaceImpl;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaPreferences;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CordovaWebViewEngine;
import org.apache.cordova.CordovaWebViewImpl;
import org.apache.cordova.PluginEntry;
import org.apache.cordova.PluginManager;
import org.apache.cordova.engine.SystemWebView;
import org.apache.cordova.engine.SystemWebViewEngine;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.reflect.Field;
import java.util.ArrayList;
/**
* 自定义Cordova控件
* 1、可用于Activity、Fragment集成
* 2、可在布局xml文件中引入
* 3、可在代码中使用new关键字创建实例
*
* <p>
* 使用示例:
* String launchUrl = "file:///android_asset/www/index.html";
* CordovaView cordovaView = view.findViewById(R.id.cv);
* cordovaView.initCordova(getActivity());
* cordovaView.loadUrl(launchUrl);
* <p>
*
* 作者:齐xc
* 日期:2019.09.15
*/
public class CordovaView extends RelativeLayout {
//页面对象
private Activity activity;
//Cordova浏览器对象: 初始化、UI布局控制、url加载、生命周期(开始、暂停、销毁...)
protected CordovaWebView appView;
//Cordova配置对象: 各类配置信息读取、设置、使用
protected CordovaPreferences preferences;
//Cordova接口实现对象: 消息处理(页面跳转、页面数据存取、权限申请...)
protected CordovaInterfaceImpl cordovaInterface;
//是否保持运行
protected boolean keepRunning = true;
//是否沉浸式
protected boolean immersiveMode;
//默认启动url
protected String launchUrl;
//插件实体类集合
protected ArrayList<PluginEntry> pluginEntries;
//接收错误的监听器(用于回调页面加载错误,如:页面未找到等等。使用方需先调用方法:setOnReceivedErrorListener())
private OnReceivedErrorListener errorListener;
/**
* 构造函数
*
* @param context 上下文
*/
public CordovaView(Context context) {
super(context);
}
/**
* 构造函数
*
* @param context 上下文
* @param attrs 属性
*/
public CordovaView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 初始化Cordova
*
* @param activity 页面
*/
public void initCordova(Activity activity) {
this.activity = activity;
//加载配置信息
loadConfig();
//设置页面是否全屏
if (preferences.getBoolean("SetFullscreen", false)) {
preferences.set("Fullscreen", true);
}
if (preferences.getBoolean("Fullscreen", false)) {
if (!preferences.getBoolean("FullscreenNotImmersive", false)) {
immersiveMode = true;
} else {
activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
} else {
activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
}
//实例化接口实现
cordovaInterface = makeCordovaInterface();
//设置背景为白色
activity.getWindow().getDecorView().setBackgroundColor(Color.WHITE);
//初始化
initCordova();
}
/**
* 加载配置信息
* 1.读取默认启动的url file:///android_asset/www/index.html
* 2.读取res/xml/config.xml文件,获得插件集合 pluginEntries
*/
private void loadConfig() {
ConfigXmlParser parser = new ConfigXmlParser();
parser.parse(activity);
preferences = parser.getPreferences();
preferences.setPreferencesBundle(activity.getIntent().getExtras());
launchUrl = parser.getLaunchUrl();
pluginEntries = parser.getPluginEntries();
try {
//通过反射处理
Field field = Config.class.getDeclaredField("parser");
field.setAccessible(true);
field.set(null, parser);
} catch (Exception ex) {
ex.printStackTrace();
}
}
/**
* 初始化
*/
private void initCordova() {
//实例化webview对象
appView = makeWebView();
//将webview加载到页面中,并根据参数配置其属性
createViews();
//如果"实例化接口"为空
if (!appView.isInitialized()) {
//appview初始化
//初始化插件管理
//初始化消息队列
//初始化桥模块
//......
appView.init(cordovaInterface, pluginEntries, preferences);
}
//设置插件管理器
//设置onActivityResult消息回调
//设置Activity销毁处理
cordovaInterface.onCordovaInit(appView.getPluginManager());
}
/**
* 创建views
*/
@SuppressWarnings({"deprecation", "ResourceType"})
private void createViews() {
//appView.getView()指SystemWebViewEngine的SystemWebView,继承自android.webkit.WebView
appView.getView().setId(100);
appView.getView().setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
//设置当前视图为SystemWebViewEngine的SystemWebView
//setContentView(appView.getView());
this.removeAllViews();
this.addView(appView.getView(), new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
//如果preferences有配置背景色,则设置webview背景色
if (preferences.contains("BackgroundColor")) {
try {
int backgroundColor = preferences.getInteger("BackgroundColor", Color.BLACK);
// Background of activity:
appView.getView().setBackgroundColor(backgroundColor);
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
//webview获得焦点(不受touch限制)
appView.getView().requestFocusFromTouch();
}
/**
* 构建CordovaWebView
*
* @return CordovaWebView
*/
private CordovaWebView makeWebView() {
//1.通过preferences配置信息构建CordovaWebViewEngine
//2.通过CordovaWebViewEngine构建CordovaWebView
return new CordovaWebViewImpl(makeWebViewEngine());
}
/**
* 通过preferences配置信息构建CordovaWebViewEngine
*
* @return CordovaWebViewEngine
*/
private CordovaWebViewEngine makeWebViewEngine() {
return CordovaWebViewImpl.createEngine(activity, preferences);
}
/**
* 构建接口实现类,接收消息
*
* @return CordovaInterfaceImpl
*/
private CordovaInterfaceImpl makeCordovaInterface() {
return new CordovaInterfaceImpl(activity) {
@Override
public Object onMessage(String id, Object data) {
return CordovaView.this.onMessage(id, data);
}
};
}
/**
* 处理消息
*
* @param id 消息id
* @param data 消息数据
* @return 处理结果
*/
public Object onMessage(String id, Object data) {
try {
if ("onReceivedError".equals(id)) {
JSONObject d = (JSONObject) data;
try {
//将消息透传给客户端
if (errorListener != null) {
int errorCode = d.getInt("errorCode");
String description = d.getString("description");
String failingUrl = d.getString("url");
errorListener.onReceivedError(errorCode, description, failingUrl);
}
} catch (JSONException e) {
e.printStackTrace();
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
/**
* 获得Webview组件,供客户端使用
*
* @return Webview组件
*/
public SystemWebView getWebview() {
if (appView != null && appView.getView() instanceof WebView) {
SystemWebView webView = (SystemWebView) appView.getView();
return webView;
}
return null;
}
/**
* 获得系统webview引擎
* @return 系统webview引擎
*/
public SystemWebViewEngine getSystemWebViewEngine(){
return (SystemWebViewEngine) appView.getEngine();
}
/**
* 加载url
*
* @param url 地址,默认应是:file:///android_asset/www/index.html
*/
public void loadUrl(String url) {
if (appView == null) {
initCordova();
}
// If keepRunning
// 如果preferences配置了KeepRunning,则页面置于后台时,仍可见
this.keepRunning = preferences.getBoolean("KeepRunning", true);
//加载url
//第2个参数表示重新初始化插件管理器、插件集合等
appView.loadUrlIntoView(url, true);
}
/**
* 当页面执行onPause方法时,可调用
*/
public void onPause() {
if (this.appView != null) {
CordovaPlugin activityResultCallback = null;
try {
Field field = CordovaInterfaceImpl.class.getDeclaredField("activityResultCallback");
field.setAccessible(true);
activityResultCallback = (CordovaPlugin) field.get(this.cordovaInterface);
} catch (Exception e) {
e.printStackTrace();
}
boolean keepRunning = this.keepRunning || activityResultCallback != null;
this.appView.handlePause(keepRunning);
}
}
/**
* 当页面执行onNewIntent方法时,可调用
*/
public void onNewIntent(Intent intent) {
if (this.appView != null)
this.appView.onNewIntent(intent);
}
/**
* 当页面执行onResume方法时,可调用
*/
public void onResume() {
if (this.appView == null) {
return;
}
activity.getWindow().getDecorView().requestFocus();
this.appView.handleResume(this.keepRunning);
}
/**
* 当页面执行onStop方法时,可调用
*/
public void onStop() {
if (this.appView == null) {
return;
}
this.appView.handleStop();
}
/**
* 当页面执行onStart方法时,可调用
*/
public void onStart() {
if (this.appView == null) {
return;
}
this.appView.handleStart();
}
/**
* 当页面执行onDestroy方法时,可调用
*/
public void onDestroy() {
if (this.appView != null) {
appView.handleDestroy();
}
}
/**
* 当页面执行onWindowFocusChanged方法时,可调用
*/
@SuppressLint("InlinedApi")
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
//设置沉浸式与全屏
if (hasFocus && immersiveMode) {
final int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
activity.getWindow().getDecorView().setSystemUiVisibility(uiOptions);
}
}
/**
* 当页面执行startActivityForResult方法时,可调用
*/
@SuppressLint({"NewApi", "RestrictedApi"})
public void startActivityForResult(Intent intent, int requestCode, Bundle options) {
// Capture requestCode here so that it is captured in the setActivityResultCallback() case.
cordovaInterface.setActivityResultRequestCode(requestCode);
//super.startActivityForResult(intent, requestCode, options);
}
/**
* 当页面执行onActivityResult方法时,可调用
*/
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
//super.onActivityResult(requestCode, resultCode, intent);
cordovaInterface.onActivityResult(requestCode, resultCode, intent);
}
/**
* 当页面执行onSaveInstanceState方法时,可调用
*/
public void onSaveInstanceState(Bundle outState) {
cordovaInterface.onSaveInstanceState(outState);
}
/**
* 当页面执行onConfigurationChanged方法时,可调用
*/
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (this.appView == null) {
return;
}
PluginManager pm = this.appView.getPluginManager();
if (pm != null) {
pm.onConfigurationChanged(newConfig);
}
}
/**
* 接口:接收到错误时的回调
*/
public interface OnReceivedErrorListener {
/**
* 接收到错误
*
* @param errorCode 错误码(请参考Cordova官方定义)
* @param description 错误描述
* @param failingUrl 发生异常的url
*/
void onReceivedError(int errorCode, String description, String failingUrl);
}
/**
* 设置监听
*
* @param listener 监听器
*/
public void setOnReceivedErrorListener(OnReceivedErrorListener listener) {
this.errorListener = listener;
}
}
如何使用
1、Fragment中(灰常简单)
布局文件:fragment_test.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffff00"
tools:context=".TestFragment">
<com.ccc.ddd.CordovaView
android:id="@+id/cv"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.ccc.ddd.CordovaView>
</FrameLayout>
代码:TestFragment.java
package com.ccc.ddd;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
public class TestFragment extends Fragment {
public TestFragment() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_test, container, false);
initView(view);
return view;
}
private void initView(View view) {
String launchUrl = "file:///android_asset/www/index1.html";
final CordovaView cordovaView = view.findViewById(R.id.cv);
cordovaView.initCordova(getActivity());
cordovaView.loadUrl(launchUrl);
//如果需要处理异常,设置此回调即可;
cordovaView.setOnReceivedErrorListener(new CordovaView.OnReceivedErrorListener() {
@Override
public void onReceivedError(int errorCode, String description, String failingUrl) {
Log.i("onReceivedError", "errorCode:" + errorCode + " description:" + description + " failingUrl:" + failingUrl);
}
});
//获得Cordova的Webview控件,执行操作,如:reload、goback、设置缓存、获得进度条等等
//cordovaView.getWebview().reload();
//cordovaView.getWebview().goBack();
/*cordovaView.getWebview().setWebChromeClient(new SystemWebChromeClient(cordovaView.getSystemWebViewEngine()) {
//监听进度
@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
//设置页面加载进度
Log.i("newProgress","newProgress: "+newProgress);
}
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
//设置标题
}
});*/
}
}
2、Activity中(同样灰常简单)
布局文件:activity_test_cordova_view.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".TestCordovaViewActivity">
<com.ccc.ddd.CordovaView
android:id="@+id/cv"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.ccc.ddd.CordovaView>
</android.support.constraint.ConstraintLayout>
代码实现:TestCordovaViewActivity.java
package com.ccc.ddd;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class TestCordovaViewActivity extends AppCompatActivity {
private CordovaView cordovaView;
private String launchUrl = "file:///android_asset/www/index.html";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test_cordova_view);
cordovaView = findViewById(R.id.cv);
cordovaView.initCordova(this);
cordovaView.loadUrl(launchUrl);
}
@Override
protected void onDestroy() {
super.onDestroy();
cordovaView.onDestroy();
}
}
其他用法
在弹框中实现,也是类似的,具体不再演示了。
1)生命周期
如果想关联Activity或Frament的生命周期,CordovaView中都已经预留了生命周期方法,只需在页面生命周期方法中,关联对应的方法即可,如:
@Override
protected void onDestroy() {
super.onDestroy();
cordovaView.onDestroy();
}
...其他生命周期函数写法类似...
2)异常消息处理
CordovaWebview加载过程中如果遇到问题,会将错误信息回传,如果开发者需要处理其错误信息,只需设置监听即可:
cordovaView.setOnReceivedErrorListener(new CordovaView.OnReceivedErrorListener() {
@Override
public void onReceivedError(int errorCode, String description, String failingUrl) {
Log.i("onReceivedError", "errorCode:" + errorCode + " description:" + description + " failingUrl:" + failingUrl);
}
});
3)Webview控件
通过cordovaView.getWebview()方法获得Webview控件,可用于配置WebSetting、设置goback、设置reload、监听加载进度等。
cordovaView.getWebview().reload();
cordovaView.getWebview().goBack();
cordovaView.getWebview().setWebChromeClient(new SystemWebChromeClient(cordovaView.getSystemWebViewEngine()) {
//监听进度
@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
//设置页面加载进度
Log.i("newProgress","newProgress: "+newProgress);
}
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
//设置标题
}
});
Fragment中运行效果