Android 进阶学习(十) App内嵌的H5打开目标页面功能
现在主流的App基本上都会用H5,但是由于业务发展App肯定会出现H5打开app的目标页,或者调用app的方法,我们App最开始使用的是使用WebViewClient的shouldOverrideUrlLoading 这个方法,拦截h5需要跳转的url,然后根据url里面的信息跳转,这样做虽然能实现我所说的功能,但是非常不友好,每次需要都要h5新增跳转的url,然后app再判断这个url做跳转,只要功能增加,两端都要做代码修改,还不能兼容前面的版本,在探索的过程中我将原来拦截url的方法替换为JS调java的形式,修改之后每次h5需要打开app的页面都需要调用我们定义的方法,
/**
*
* @param action 动作
* @param extra extra1 参数
*/
@JavascriptInterface
public void JsBridgeCall(String action,String extra,String extra1){
if(callBack!=null){
if(mPostHandler!=null){
mPostHandler.post(() -> callBack.onJsBridgeCall(action,extra,extra1));
}
}
}
注意这里我使用Handler post 了一下,返回来的时候在子线程,
可以看到我们这么做之后的可以接收到h5传给我们的三个参数,虽然这个操作的写法相对于拦截url那种方式看起来高大上一些,但是并没有解决实际问题,那就是如何让h5打开app的任意页面呢,而且不同的页面可能还有不同的参数,参数的类型也多种多样,想来想去可以利用ClassLoader来解决这个问题,说一下解决这个问题的思路,
JsBridgeCall(String action,String extra,String extra1)
可以看到我定义这个方法有三个参数,第一个参数是action ,顾名思义就是动作的意思,比如 goback h5向后退一步,close 关闭当前页面,openPage 打开目标页
第二个参数就是extra ,这个就是打开目标页的参数, 具体怎么打开我们在下面仔细分析,第三个参数extra1 作为额外参数的意思就是在执行完action 后,是否还伴随这个其他额外的动作,比如后退和关闭,这里我举一个简单的例子,可以看到在执行完openPage后,如果第三个参数不为空,那么将第三个参数作为第一个参数再执行一个遍这个方法
@Override
public void onJsBridgeCall(String action, String extra,String extra1) {
switch (action){
case "goBack":
onBackPressed();
break;
case "close":
finish();
break;
case "openPage":
try {
if(TextUtils.isEmpty(extra))
return;
WebInfoMode infoMode=JsonUtils.getTFromStr(extra,WebInfoMode.class);
infoMode.goIntent(this);
if(!TextUtils.isEmpty(extra1)){
onJsBridgeCall(extra1,"","");
}
}catch (Exception e){
}
break;
case "hideTitle":
break;
}
}
///H5 后退一步,这样做会过滤掉重定向
@Override
public void onBackPressed() {
if (null == webview_info) {finish();return;}
WebBackForwardList webBackForwardList = webview_info.copyBackForwardList();
if (null == webBackForwardList) {finish();return;}
WebHistoryItem currentItem = webBackForwardList.getCurrentItem();
int currentIndex = webBackForwardList.getCurrentIndex();
if (null == currentItem) {
finish();
} else if (currentIndex == 0) {
finish();
} else {
String url = currentItem.getUrl();
if(!TextUtils.isEmpty(url) && url.contains("transfer=webviewCloseH5")){
finish();
}
if (webview_info.canGoBack()) {
webview_info.goBack();
} else {
webview_info.goBackOrForward(-1);
}
}
}
下面我们来聊一聊具体如何通过ClassLoader来打开目标页,
我们知道Context 类在创建的时候会加载一个ClassLoader,通过这个ClassLoader,我们只需要知道包名+类名就可以得到这个class,
内部类是使用 $ 符号修饰的,避免踩坑,
这个class就是我们在创建Intent的时候的目标页,代码如下
public void goIntent(Context context){
Intent intent=new Intent();
String packageName="你的包名";
ClassLoader classLoader=context.getClassLoader();
try {
Class<?> intentCls = classLoader.loadClass(packageName+getIntentString());
intent.setClass(context,intentCls);
context.startActivity(intent);
} catch (ClassNotFoundException e) {
//TODO 提示更新最新版本,连这个类都没找到,相关功能肯定还没有
}
}
在打开的页面没有参数的时候,我们只需要知道一个类的全路径+类名就可以了,比如我们的包名是 "com.tsm.myapplication",而我们目标页的路径是 com.tsm.myapplication.find.SecondActivtiy, 那么我们利用classLoader.loadClass(packageName+"find.SecondActivtiy"),,这样就解决了目标页的问题,我们再分析一下如何传递参数的问题,我现在的做法是兼容大部分参数 int string list map Serializable Parcelable,
这种写法不能兼容所有的问题,但是绝对能解决大部分问题,而且再后面的修改的过程中还不用动前台代码,可以向前兼容,
先来说一下我为参数的设计的类
public static class WebIntentModel{
/**
* intent 中 putExtra( kay,value) 的value ,我们将它定义为String ,根据不同的类型将这个String 解析成想要的参数
*/
private String params_info;
/**
* intent 中 putExtra( kay,value) 的key 这个肯定是String
*/
private String key;
/**
* 为了区分 参数的类型 int String Map List Serializable Parcelable
*/
private String modelType;
/**
* 如果定义的是 List Serializable 或者 Parcelable 我么就需要知道 是哪个类
*/
private String modelCls;
}
通过这些参数,还有classLoader ,我们就可以将intent的参数拼接起来了,h5传递给我们打开目标也的格式就是
image.png
这个就是我们定义的第二个参数,也就是extra的 所有定义,
下面我们贴一下所有的代码
public class WebInfoMode {
private String intentString;
private List<WebIntentModel> listMode;
public String getIntentString() {
return intentString;
}
public void setIntentString(String intentString) {
this.intentString = intentString;
}
public List<WebIntentModel> getListMode() {
return listMode;
}
public void setListMode(List<WebIntentModel> listMode) {
this.listMode = listMode;
}
public void goIntent(Context context){
Intent intent=new Intent();
String packageName="你的包名";
ClassLoader classLoader=context.getClassLoader();
try {
Class<?> intentCls = classLoader.loadClass(packageName+getIntentString());
intent.setClass(context,intentCls);
if(listMode!=null&&listMode.size()>0){
for (int i=0;i<listMode.size();i++){
intent=listMode.get(i).initIntent(intent,classLoader,packageName);
}
}
context.startActivity(intent);
} catch (ClassNotFoundException e) {
//TODO 提示升级 找不到类就需要升级了
e.printStackTrace();
}
}
public static class WebIntentModel{
/**
* intent 中 putExtra( kay,value) 的value ,我们将它定义为String ,根据不同的类型将这个String 解析成想要的参数
*/
private String params_info;
/**
* intent 中 putExtra( kay,value) 的key 这个肯定是String
*/
private String key;
/**
* 为了区分 参数的类型 int String Map List Serializable Parcelable
*/
private String modelType;
/**
* 如果定义的是 Serializable 或者 Parcelable 我么就需要知道 是哪个类
*/
private String modelCls;
public String getModelType() {
return modelType;
}
public void setModelType(String modelType) {
this.modelType = modelType;
}
public String getParams_info() {
return params_info;
}
public void setParams_info(String params_info) {
this.params_info = params_info;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getModelCls() {
return modelCls;
}
public void setModelCls(String modelCls) {
this.modelCls = modelCls;
}
public Intent initIntent(Intent intent,ClassLoader classLoader,String packageName){
if(TextUtils.isEmpty(modelType)){
return intent;
}
try {
switch (modelType.toLowerCase()){
case "str":
intent.putExtra(key,params_info);
break;
case "int":
intent.putExtra(key, TextUtils.parseInt(params_info));
break;
case "bool":
intent.putExtra(key,Boolean.parseBoolean(params_info));
break;
case "map":
intent.putExtra(key, (Serializable) JsonUtils.getStrMap(params_info));
break;
case "ser":
Class<?> cls = classLoader.loadClass(packageName+modelCls);
intent.putExtra(key,(Serializable) JsonUtils.getTFromStr(params_info,cls));
break;
case "par":
Class<?> cls1 = classLoader.loadClass(packageName+modelCls);
intent.putExtra(key,(Parcelable) JsonUtils.getTFromStr(params_info,cls1));
break;
case "list":
if(!TextUtils.isEmpty(listType)){
switch (listType){
case "str":
intent.putExtra(key,JsonUtils.getTsFromStr(params_info,String.class));
break;
case "int":
intent.putExtra(key, JsonUtils.getTsFromStr(params_info,Integer.class));
break;
case "map":
intent.putExtra(key, (Serializable) JsonUtils.getList(params_info));
break;
case "ser":
Class<?> cls2 = classLoader.loadClass(packageName+modelCls);
intent.putExtra(key,(Serializable) JsonUtils.getTsFromStr(params_info,cls2));
break;
case "par":
Class<?> cls3 = classLoader.loadClass(packageName+modelCls);
intent.putExtra(key,(Parcelable) JsonUtils.getTsFromStr(params_info,cls3));
break;
}
}
break;
}
}catch (ClassNotFoundException e){
//TODO 能找到目标页,但是找不到参数,证明参数类型有问题,
e.printStackTrace();
}catch (Exception e){
e.printStackTrace();
}
return intent;
}
}
}
这里有一个地方比较坑,那就是params_info 如果作为Serializable或者Parcelable 的时候,内部的字符串前面有三个转义字符即 \"name\" 这种形式,原因是他被字符串处理了2次,
贴一下我写的时候的案例
///这个是给H5 openPage 的时候的第二个参数
String extra= "{\"intentString\":\"tsm.activity.TsmActivity\"," +
"\"listMode\":[{\"key\":\"bean\",\"modelCls\":\"model.tsm.Model\",\"modelType\":\"ser\"," +
"\"params_info\":\"{\\\"airlineCode\\\":\\\"CA\\\",\\\"arrivalName\\\":\\\"DLC\\\",\\\"arriveTer\\\":\\\"--\\\",\\\"certId\\\":\\\"DHDHDHDH\\\",\\\"certType\\\":\\\"P\\\",\\\"copunStatus\\\":\\\"O\\\",\\\"couponNumber\\\":\\\"1\\\",\\\"departDate\\\":\\\"2020-11-21\\\",\\\"departName\\\":\\\"PEK\\\",\\\"departTer\\\":\\\"3\\\",\\\"flightNumber\\\":\\\"8908\\\",\\\"gate\\\":\\\"--\\\",\\\"pushFlag\\\":\\\"0\\\",\\\"seatNo\\\":\\\"--\\\",\\\"ticketNumber\\\":\\\"9992155241078\\\",\\\"userLabel\\\":\\\"0\\\",\\\"userLevel\\\":\\\"Silver\\\",\\\"version\\\":\\\"6\\\"}\"" +
"}]}";
发现如果直接我给h5小姐姐字符串让她弄的话比较麻烦,每次还要我给她生成这个字符串,转义字符弄得比较麻烦,烦不胜烦,经过研究后直接使用JS 创建model,附上小姐姐的例子
var model = new Object();
model.intentString = 'tsm.activity.TsmAcitivity';
var innrList=new Array();
var model1 = new Object();
model1.type = 1;
model1.org = 'PEK';
model1.dst = 'SHA';
var innr=new Object();
innr.params_info = JSON.stringify(model1);
innr.key='bean';
innr.modelType='ser';
innr.modelCls='model.TsmModel$TsmsBean';
innrList.push(innr); //
model.listMode = innrList;
console.log(JSON.stringify(model));
window.__native__.AirJsBridgeCall("openPage",JSON.stringify(model),"close");
注意内部类的修饰字符 $
可能有人还没有json的工具类,我也贴一下
public class JsonUtils {
private static Gson mGson = new GsonBuilder().registerTypeAdapterFactory(new GsonAdapterUtils.TypeAdapterFactory()).create();
/**获取json字符串*/
public static String getJsonStr(List<Map<String, Object>> list) {
return mGson.toJson(list);
}
public static List<String> getJsonStr(String json) {
return mGson.fromJson(json, new TypeToken<List<String>>() {}.getType());
}
public static String toJson(Object list) {
return mGson.toJson(list);
}
/** 返回List */
public static List<Map<String, Object>> getList(String jsonStr) {
return mGson.fromJson(jsonStr, new TypeToken<List<Map<String, Object>>>() {}.getType());
}
public static List<List<Map<String, Object>>> getListWithInList(String jsonStr) {
return mGson.fromJson(jsonStr, new TypeToken<List<List<Map<String, Object>>>>() {}.getType());
}
public static <T> List<List<T>> getListWithInList(String jsonStr, Class<T> c) {
List<List<T>> lst = new ArrayList<>();
JsonArray array = new JsonParser().parse(jsonStr).getAsJsonArray();
for(JsonElement elem : array){
lst.add(getTsFromStr(elem.toString(),c));
}
return lst;
}
public static List<Map<String, String>> getStrList(String jsonStr) {
return mGson.fromJson(jsonStr, new TypeToken<List<Map<String, String>>>() {}.getType());
}
/**返回List*/
public static List<String> getStringList(String jsonStr) {
return mGson.fromJson(jsonStr, new TypeToken<List<String>>() {}.getType());
}
/**返回Map */
public static Map<String, Object> getMap(String jsonStr) {
return mGson.fromJson(jsonStr, new TypeToken<Map<String, Object>>() {}.getType());
}
/**返回Map */
public static Map<String, String> getStrMap(String jsonStr) {
return mGson.fromJson(jsonStr, new TypeToken<Map<String, Object>>() {}.getType());
}
/**返回实体类*/
public static <T> T fromJson(String json, Class<T> classOfT) {
return mGson.fromJson(json, classOfT);
}
/**返回实体类*/
public static <T> T fromJson(Reader json, Class<T> classOfT) {
return mGson.fromJson(json, classOfT);
}
/**返回实体类*/
public static <T> T fromJson(String json, Type type) {
if (TextUtils.isEmpty(json)) {
return null;
}
return mGson.fromJson(json, type);
}
/** 获取字符串from map*/
public static String getJsonStrFromMap(Map<String, Object> map) {
try {
String res = mGson.toJson(map);
return res;
} catch (Exception e) {}
return "";
}
/**字符串转实例*/
public static <T> T getTFromStr(String json, Class<T> cls){
return mGson.fromJson(json, cls);
}
/**字符串转list*/
public static <T> ArrayList<T> getTsFromStr(String json, Class<T> clazz){
ArrayList<T> lst = new ArrayList<>();
if (!TextUtils.isEmpty(json)) {
JsonArray array = new JsonParser().parse(json).getAsJsonArray();
for (final JsonElement elem : array) {
lst.add(mGson.fromJson(elem, clazz));
}
}
return lst;
}
/**字符串转list*/
public static <T> LinkedList<T> getLListFromStr(String json, Class<T> clazz){
LinkedList<T> lst = new LinkedList<>();
if (!TextUtils.isEmpty(json)) {
JsonArray array = new JsonParser().parse(json).getAsJsonArray();
for (final JsonElement elem : array) {
lst.add(mGson.fromJson(elem, clazz));
}
}
return lst;
}
}
public class GsonAdapterUtils {
public static class TypeAdapterFactory implements com.google.gson.TypeAdapterFactory {
@SuppressWarnings("unchecked")
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class<T> rawType = (Class<T>) type.getRawType();
if (rawType == Float.class || rawType == float.class) {
return (TypeAdapter<T>) new FloatTypeAdapter();
} else if (rawType == Integer.class || rawType == int.class) {
return (TypeAdapter<T>) new IntegerTypeAdapter();
} else if (rawType == Long.class || rawType == long.class) {
return (TypeAdapter<T>) new LongTypeAdapter();
} else if (rawType == Double.class || rawType == double.class) {
return (TypeAdapter<T>) new DoubleTypeAdapter();
}
return null;
}
}
public static class LongTypeAdapter extends TypeAdapter<Long> {
@Override
public void write(JsonWriter out, Long value) {
try {
if (value == null) {
value = 0L;
}
out.value(value);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Long read(JsonReader in) {
try {
Long value;
if (in.peek() == JsonToken.NULL) {
in.nextNull();
Log.e("TypeAdapter", "null is not a number");
return 0L;
}
if (in.peek() == JsonToken.BOOLEAN) {
boolean b = in.nextBoolean();
Log.e("TypeAdapter", b + " is not a number");
return 0L;
}
if (in.peek() == JsonToken.STRING) {
String str = in.nextString();
return Long.parseLong(str);
} else {
value = in.nextLong();
return value;
}
} catch (Exception e) {
Log.e("TypeAdapter", "Not a number", e);
}
return 0L;
}
}
public static class IntegerTypeAdapter extends TypeAdapter<Integer> {
@Override
public void write(JsonWriter out, Integer value) {
try {
if (value == null) {
value = 0;
}
out.value(value);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Integer read(JsonReader in) {
try {
Integer value;
if (in.peek() == JsonToken.NULL) {
in.nextNull();
Log.e("TypeAdapter", "null is not a number");
return 0;
}
if (in.peek() == JsonToken.BOOLEAN) {
boolean b = in.nextBoolean();
Log.e("TypeAdapter", b + " is not a number");
return 0;
}
if (in.peek() == JsonToken.STRING) {
String str = in.nextString();
return Integer.parseInt(str);
} else {
value = in.nextInt();
return value;
}
} catch (Exception e) {
//Log.e("TypeAdapter", "Not a number", e);
}
return 0;
}
}
public static class DoubleTypeAdapter extends TypeAdapter<Double> {
@Override
public void write(JsonWriter out, Double value) {
try {
if (value == null) {
value = 0D;
}
out.value(value);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Double read(JsonReader in) {
try {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
Log.e("TypeAdapter", "null is not a number");
return 0D;
}
if (in.peek() == JsonToken.BOOLEAN) {
boolean b = in.nextBoolean();
Log.e("TypeAdapter", b + " is not a number");
return 0D;
}
if (in.peek() == JsonToken.STRING) {
String str = in.nextString();
return Double.parseDouble(str);
} else {
Double value = in.nextDouble();
return value == null ? 0D : value;
}
} catch (NumberFormatException e) {
Log.e("TypeAdapter", e.getMessage(), e);
} catch (Exception e) {
Log.e("TypeAdapter", e.getMessage(), e);
}
return 0D;
}
}
public static class FloatTypeAdapter extends TypeAdapter<Float> {
@Override
public void write(JsonWriter out, Float value) {
try {
if (value == null) {
value = 0F;
}
out.value(value.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Float read(JsonReader in) {
try {
Float value;
if (in.peek() == JsonToken.NULL) {
in.nextNull();
Log.e("TypeAdapter", "null is not a number");
return 0F;
}
if (in.peek() == JsonToken.BOOLEAN) {
boolean b = in.nextBoolean();
Log.e("TypeAdapter", b + " is not a number");
return 0F;
}
if (in.peek() == JsonToken.STRING) {
String str = in.nextString();
return Float.parseFloat(str);
} else {
String str = in.nextString();
value = Float.valueOf(str);
}
return value;
} catch (Exception e) {
Log.e("TypeAdapter", "Not a number", e);
}
return 0F;
}
}
}