Android自动更新:这里的更新静悄悄~
产品:APP的底部按钮能够做到自动更新吗?
累人猿:有些麻烦(无辜脸)~
产品:那京东、美团是怎么做到的?
累人猿:……
心疼自己十分钟~
经常看到有什么活动的时候,京东、淘宝、美团这些应用都不需要更新应用就能够实现应用UI的更新,特别是底部菜单按钮的图标,及时的营造出活动的气氛。作为一个移动开发者都想弄明白他们是怎么做到的,我所了解的方式有以下三种:
1、使用前端框架,如果是这种情况的话,整个应用都没有多少原生的东西,比较火的框架:React Native、ionic等;
2、纯原生,这种方式是通过图片下载,本地图片读取,动态生成StateListDrawable,大致这样一个流程实现不更新应用就更新底部按钮的图标;
3、JS交互:将整个底部按钮做成H5,然后使用webview加载,这也是今天的重点。
其中第二和第三种方式,必须得做好容错处理,比如网络问题导致图片下载失败,本地图片丢失等问题,为了避免这些问题导致应用的不正常运行,我们需要有备选方案,一旦问题出现,就放弃整个流程,从本地读取资源。
我并不是前端开发者,h5相关的东西学得不多,所以在实现这个功能的过程中还是花了一些功夫的,JS和CSS用得也不熟,所以在处理有些细节的时候,用得方式有些暴力,希望大伙儿能够理解,有考虑使用这种方式的小伙伴可以让h5开发的小伙伴来做。
整个功能是基于JS交互这个基础上的,至于需要怎么交互,我就不做过多的讲解了,不清楚的小伙伴可以去网上找一些资料来学学。
一、CSS布局
整个底部按钮的界面,都是一些html标签,我使用了比较经典的四个按钮,如果有特别需求的小伙伴,可以自己增加或者删除几个标签,源码如下:
<div> <label οnclick="myFun('img_one')"> <input type="radio" id="one" name="tabBtn" checked> <img id="img_one"> <p style="color: #f00;"></p> </label> <label οnclick="myFun('img_two')"> <input type="radio" id="two" name="tabBtn"> <img id="img_two"> <p></p> </label> <label οnclick="myFun('img_three')"> <input type="radio" id="three" name="tabBtn"> <img id="img_three"> <p></p> </label> <label οnclick="myFun('img_four')"> <input type="radio" id="four" name="tabBtn"> <img id="img_four"> <p></p> </label> </div>
当然,要实现整个界面方式不止一种,我采用的是radio的方式,接着我们讲讲css中比较关键的地方,第一label的p标签设置了文字颜色为红色,是为了初始化,一般应进入应用第一个按钮是选中状态,当然这个工作可以放在JS中来做,和图片初始化一起做,如果设计不是红色的小伙伴就要注意了,在使用的时候需要修改成设计要求的颜色。
label的CSS代码:
label { margin-top: 0px; margin-bottom: -3px; padding-top: 5px; display: inline-block; width: 25%; font-weight: normal; vertical-align: middle; cursor: pointer; float: left; text-align: center; }
一定要注意25%,因为是四个按钮,我们需要平分100%,所以每个按钮的范围是25%,如果按钮的个数修改一定要注意这个百分比的修改。
因为使用的webview加载h5的方式,细心的小伙伴会发现,我长按某个按钮的时候可以复制文本的,所以为了禁用复制功能,我们需要在CSS中做一些限制
/* 禁止长按复制功能 */ * { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; }
其实就做了一件事,需要这么些代码是因为兼容不懂浏览器内核。关于div、p、body、input的CSS代码这里就不详细讲解了,这几个标签的CSS代码可以不做任何修改,直接使用,如果要改的话可以让h5的小伙伴帮忙改得更规范一些。
二、JS交互
不管是ios还是android都提供了Webview和h5交互的方法。
我们先来看看本地提供给JS的接口方法:
/* js调用本地的方法改变底部按钮的选中状态 原理:js调用本地方法,方法内部通过广播的方式传递选中按钮的编号 */ @JavascriptInterface public void changeTab(String index) { Intent intent = new Intent(); intent.setAction("ChangeTab"); intent.putExtra("index", index); context.sendBroadcast(intent); } /* 将Json数组传递给h5页面 */ @JavascriptInterface public String getImages(List<Item> lists) { try { JSONArray array = new JSONArray(); for (Item item : lists) { JSONObject object = new JSONObject(); object.put("icon_nor", item.getIcon_nor()); object.put("icon_sel", item.getIcon_sel()); object.put("title", item.getTitle()); array.put(object); } String json = array.toString(); return json; } catch (Exception e) { e.printStackTrace(); } return ""; }
第一个方法的作用是,在h5界面点击某个按钮的使用,js调用本地的方法实现原生fragment的切换,我使用了广播的方式,来通知原生界面的切换操作。
第二个方法,是为了初始化h5界面,通过js调用这个方法,将数据传递给h5页面,达到初始化界面的效果。
其中Item是我将每个按钮封装成了一个对象,这样方便数据的读取和传递,源码如下:
public class Item { private String icon_nor;//图标正常状态 private String icon_sel;//图标选中状态 private String title;//按钮文案 public String getIcon_nor() { return icon_nor; } public void setIcon_nor(String icon_nor) { this.icon_nor = icon_nor; } public String getIcon_sel() { return icon_sel; } public void setIcon_sel(String icon_sel) { this.icon_sel = icon_sel; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } }
其实这个对象中还应该增加一个颜色变量,这样的话,我们不需要修改h5的颜色,直接通过传值来改变颜色,有兴趣的小伙伴可以尝试一下,一定要记得在Js中获取颜色和应用颜色。
不如我们先来看看应用截图吧!
这是最正常的情况,也就是说,服务器正常开启,图片地址正确传递。
这种情况是服务器关闭的情况,看着并没有什么区别,因为我做了容错处理,其实这种情况图片是读取失败的,如果没有容错处理,图片会显示一个错误图片的图标。
这种情况是,我故意少传了一张图片的运行界面,细心的小伙伴会发现这张图中的tab后面的编号跟前面一张的图片不一样,其实这也算是一种保底的做法,一旦出错,就放弃方法,采用本地的不就方案,保证应用的运行没有问题。
继续回到源码分析,在点击某个tab的时候,相应ui也应该发生变化,还记得刚刚我们在原生提供的第一个方法吗,现在就是使用它的时候,
/* 通过js调用本地方法,改变tab选中状态 */ var oneRB = document.getElementById("one"); oneRB.addEventListener('click', function () { if (oneRB.checked) { appNative.changeTab('one'); } }, false); var twoRB = document.getElementById("two"); twoRB.addEventListener('click', function () { if (twoRB.checked) { appNative.changeTab('two'); } }, false); var threeRB = document.getElementById("three"); threeRB.addEventListener('click', function () { if (threeRB.checked) { appNative.changeTab('three'); } }, false); var fourRB = document.getElementById("four"); fourRB.addEventListener('click', function () { if (fourRB.checked) { appNative.changeTab('four'); } }, false);
启动应用的时候,我们需要初始化整个tab界面,第二个方法排上用场了,
/* 初始化数据,为了排除网络等不确定因素,需要在images文件夹下面放一套缺省图标 */ var datas; var isOk = true; var icons = new Array(); var icon1 = new Object(); icon1.icon_sel = "images/icon_tab_one_sel.png"; icon1.icon_nor = "images/icon_tab_one_nor.png"; icon1.title = "tab0"; icons.push(icon1); var icon2 = new Object(); icon2.icon_sel = "images/icon_tab_two_sel.png"; icon2.icon_nor = "images/icon_tab_two_nor.png"; icon2.title = "tab1"; icons.push(icon2); var icon3 = new Object(); icon3.icon_sel = "images/icon_tab_three_sel.png"; icon3.icon_nor = "images/icon_tab_three_nor.png"; icon3.title = "tab2"; icons.push(icon3); var icon4 = new Object(); icon4.icon_sel = "images/icon_tab_four_sel.png"; icon4.icon_nor = "images/icon_tab_four_nor.png"; icon4.title = "tab3"; icons.push(icon4); var oImg = document.getElementsByTagName('img'); function tranData(jsondata) { datas = eval(jsondata); if(isEmpty(jsondata)){ isOk = false; }else{ for(i = 0;i<datas.length;i++){ if(typeof datas[i].icon_sel === "undefined" || typeof datas[i].icon_nor === "undefined"){ isOk = false; } } } if(isOk){ oImg[0].src = datas[0].icon_sel; oImg[0].nextSibling.nextSibling.innerText = datas[0].title; oImg[0].onerror = function(){ oImg[0].src = icons[0].icon_sel; isOk = false; } oImg[1].src = datas[1].icon_nor; oImg[1].nextSibling.nextSibling.innerText = datas[1].title; oImg[1].onerror = function(){ oImg[1].src = icons[1].icon_nor; isOk = false; } oImg[2].src = datas[2].icon_nor; oImg[2].nextSibling.nextSibling.innerText = datas[2].title; oImg[2].onerror = function(){ oImg[2].src = icons[2].icon_nor; isOk = false; } oImg[3].src = datas[3].icon_nor; oImg[3].nextSibling.nextSibling.innerText = datas[3].title; oImg[3].onerror = function(){ oImg[3].src = icons[3].icon_nor; isOk = false; } }else{ oImg[0].src = icons[0].icon_sel; oImg[0].nextSibling.nextSibling.innerText = icons[0].title; oImg[1].src = icons[1].icon_nor; oImg[1].nextSibling.nextSibling.innerText = icons[1].title; oImg[2].src = icons[2].icon_nor; oImg[2].nextSibling.nextSibling.innerText = icons[2].title; oImg[3].src = icons[3].icon_nor; oImg[3].nextSibling.nextSibling.innerText = icons[3].title; } }
这段处理有些复杂,因为需要考虑容错方法,所以我建议小伙伴们,把整个html放在app的assets文件夹下面,不要放在服务端,同时我们还要再assets文件夹下面放一套默认图标,我个人认为图标更新失败,比应用不能够正常使用要有好的多,建议采用如下的方式,使用这种方案,主要的思路就是不管什么原因导致远程图片读取失败,都直接使用本地的缺省图片,还要注意,是整套图片都使用本地的,不能够某一张读取失败了,只替换某一张噻~
建议使用的小伙伴不要把你们的图片改成截图中的名字,这样就不需要再html源码中修改。
然后就是处理某个tab选中之后的样式修改了。
/* 改变选中按钮的样式和状态 */ function myFun(sId) { for (var i = 0; i < oImg.length; i++) { iconSelect(i,sId); } } function iconSelect(i, sId){ if (oImg[i].id == sId) { oImg[i].previousSibling.previousSibling.checked = true; oImg[i].nextSibling.nextSibling.style.color = "red"; if(isOk){ oImg[i].src = datas[i].icon_sel; }else{ oImg[i].src = icons[i].icon_sel; } } else { if(isOk){ oImg[i].src = datas[i].icon_nor; }else{ oImg[i].src = icons[i].icon_nor; } oImg[i].nextSibling.nextSibling.style.color = "black"; } }
另外,有时候我们会遇到在某个具体的fragment中点击某个按钮跳到其它tab页面中去,比如在fragment中我点击了按钮,需要跳到第二个tab页面,所以我通过js提供了一个切换tab的方法。
/* 提供给原生的方法,原生调用该方法,实现设置某个tab选中 */ function setChecked(id) { var btn = document.getElementById(id); btn.checked = true; if (typeof appNative !== "undefined" && appNative.changeTab) { appNative.changeTab(id); myFun("img_" + id); } }
使用的时候只需要原生中调用,方法如下:
webView.loadUrl("javascript:setChecked('four')");
其中的”four”对应某个需要跳转到的tab编号,注意只能是“one”、“two”、“three”、“four”
前面就是整个功能的js代码,我知道,有些功能是完全可以用CSS来实现的,CSS我还不是很熟,所以暴力的使用了JS来处理细节,并且我并没有使用JQuery这些简单的语法结构,没有别的原因,我还不会~
三、原生调用
讲了CSS和JS,接着应该讲讲怎么在原生中调用。
首先加载整个tab的html页面
webView.loadUrl("file:///android_asset/radioGroup.html");
当然初始化数据是少不了的
/* 将图片地址和按钮文案传递给h5页面 */ private void initData() { lists = new ArrayList<>(); Item item = new Item(); item.setIcon_nor("http://192.168.111.20/drawable-hdpi/icon_tab_one_nor.png"); item.setIcon_sel("http://192.168.111.20/drawable-hdpi/icon_tab_one_sel.png"); // item.setIcon_sel("http://192.168.111.20/drawable-hdpi/push.png"); item.setTitle("tab1"); Item item1 = new Item(); item1.setIcon_nor("http://192.168.111.20/drawable-hdpi/icon_tab_two_nor.png"); item1.setIcon_sel("http://192.168.111.20/drawable-hdpi/icon_tab_two_sel.png"); item1.setTitle("tab2"); Item item2 = new Item(); item2.setIcon_nor("http://192.168.111.20/drawable-hdpi/icon_tab_three_nor.png"); item2.setIcon_sel("http://192.168.111.20/drawable-hdpi/icon_tab_three_sel.png"); item2.setTitle("tab3"); Item item3 = new Item(); item3.setIcon_nor("http://192.168.111.20/drawable-hdpi/icon_tab_four_nor.png"); item3.setIcon_sel("http://192.168.111.20/drawable-hdpi/icon_tab_four_sel.png"); item3.setTitle("tab4"); lists.add(item); lists.add(item1); lists.add(item2); lists.add(item3); }
然后需要通过js将数据传递给html页面
webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { return super.shouldOverrideUrlLoading(view, url); } @Override public void onLoadResource(WebView view, String url) { super.onLoadResource(view, url); } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); webView.loadUrl("javascript:tranData(" + javaScriptInterface.getImages(lists) + ")"); // webView.loadUrl("javascript:setChecked('four')"); } });
建议在onPageFinished这个方法里面调用js方法,这样不容易出错。
至于原生是怎么处理tab页面的切换的,我就不讲了,有兴趣的小伙伴可以把源码下载下来看一看,当然自己改改是最好的方式。
传送门开启:http://download.csdn.net/detail/zhimingshangyan/9648669
我的实现思路讲完了,文章的开篇我提供了三种实现方式,第二和第三两种方式我都实现了,当然第二种方式我没有整理,欢迎小伙伴提供你们的思路给我,同时有什么问题和值得改进的地方小伙伴可以留言给我。