声明:本文为 赵元杰 原创,授权发布在 Android程序员公众号,转载请参考原文协议。
原文:https://qq157755587.github.io/2016/08/12/custom-tabs-best-practices/
今天的文章来自武汉时光小屋团队的 赵元杰 同学,很多人知道我目前也在武汉工作,武汉的Android圈里比较活跃的同学我认识不太多,元杰是其中较熟的一位,我们都做海外方向业务,有时会简单交流,偶尔会在一些线下技术活动中碰到他,也一起吃饭聊过,印象中的他内敛沉稳,从业十年依然保持对技术最初的热情,在Kotlin正式发布前,就已经在他们公司正式项目中用上了Kotlin,真可谓艺高人胆大,其博客产量不高,但质量很高,推荐大家关注
https://qq157755587.github.io/
今天的主题 Chrome Custom Tabs 刚推出时,我也有关注过,但并没有深入实践,元杰今天分享的这篇最佳实践,弥补了当时的缺憾,也希望能帮助到一些有相关需要的朋友。
距离Google发布Chrome Custom Tabs已经一年,Twitter、Medium等国外App早已支持了这个功能,但遗憾的是国内App鲜有支持。这篇文章以官方开发文档和示例源码为基础,加上自己的理解,希望能帮助读者快速掌握Chrome Custom Tabs的用法。
为什么要用Chrome Custom Tabs?
当App需要打开一个网站时,开发者面临两种选择:默认浏览器或WebView。这两种选择都有不足。从App跳转到浏览器是一个非常重的切换,并且浏览器无法自定义;而WebView无法与浏览器共享cookies等数据,并且需要开发者处理非常多的场景。
Chrome Custom Tabs提供了一种新的选择,既能在App和网页之间流畅切换,又能有多种自定义选项。其实它本质上是调用了Chrome中的一个Activity来打开网页,这样想就能理解这些优点了。能自定义的项目有:
Toolbar颜色
进场和出场动画
Toolbar上的action button,menu item和bottom toolbar(通过RemoveView实现)
Chrome Custom Tabs还提供预启动Chrome和预加载网页内容的功能,与传统方式相比加载速度有显著提升。
什么时候用Chrome Custom Tabs,什么时候用WebView?
如果Web页面是你自己的内容(比如淘宝商品页之于手机淘宝),那么WebView是最好的选择,因为你可能需要针对网页内容及用户操作做非常多的自定义。如果是跳到一个外部网站,比如在App中点了一个广告链接跳转到广告商的网站,那么建议使用Chrome Custom Tabs。
前置条件
用户的手机上需要安装Chrome 45或以上版本,并且设为默认浏览器。考虑到Chrome在国内手机上的占有率,这确实是个问题……但如果你的APP不只是面对国内市场,那么以Google在海外市场的影响力,这完全不是问题。
肯定有人要问,如果手机上没有装Chrome,调用Chrome Custom Tabs会发生什么行为呢?我们查看CustomTabsIntent.Builder的源码可以发现,Builder的内部构造了一个action为Intent.ACTION_VIEW的Intent,所以答案是调用默认浏览器来打开URL。
这时我们可以发现Chrome Custom Tabs的原理:如果Chrome是默认浏览器,那么这个Intent自然就会唤起Chrome,然后Chrome会根据Intent的各个Extra来配置前面所讲的自定义项。这里有一个隐藏的好处:如果你的工作恰好是开发浏览器,那么也可以根据这些Extra信息来定制界面!
开发向导 快速上手
首先在你的build.gradle文件中加入dependency
dependencies {
...
compile 'com.android.support:customtabs:24.1.1'
}
然后写几行代码
String url = "https://www.google.com";
CustomTabsIntent.Builder builder
= newCustomTabsIntent.Builder();
CustomTabsIntent customTabsIntent
= builder.build();
customTabsIntent.launchUrl(
this, Uri.parse(url));
就好了!不费吹灰之力~
注意launchUrl这个方法的第一个参数是个Activity。肯定有人会跳起来说,我要在ViewHolder里面处理点击跳转,根本拿不到Activity,只有Context肿么办?!!(其实这个人就是我)
那么我们看看launchUrl的源码:
publicvoidlaunchUrl(
Activity context, Uri url){
intent.setData(url);
ActivityCompat.startActivity(
context,
intent,
startAnimationBundle); }
原来是为了传入startAnimationBundle这个参数来实现自定义转场动画。那么我们自己处理一下就好了:
if(context instanceofActivity) { customTabsIntent.launchUrl(
(Activity) context, uri); } else{ Intent intent
= customTabsIntent.intent; intent.setData(uri);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
context.startActivity(
intent,
customTabsIntent.startAnimationBundle); } else{ context.startActivity(intent); } } 修改Toolbar颜色
你一定希望Chrome Custom Tabs的Toolbar颜色与你自己APP的Toolbar颜色保持相同,看起来就像是在APP内打开网页一样。一行代码搞定:
builder.setToolbarColor(colorInt); 添加action button
你可能想要在Toolbar上加上action button,那么需要创建一个PendingIntent。下面的代码增加了一个发送邮件的action button:
Intent actionIntent
= newIntent(Intent.ACTION_SEND);
actionIntent.setType( "*/*"); actionIntent.putExtra(
Intent.EXTRA_EMAIL,
"example@example.com"); actionIntent.putExtra(
Intent.EXTRA_SUBJECT,
"example");
PendingIntent pi
= PendingIntent.getActivity(
this, 0, actionIntent, 0);
//注意在正式项目中不要在UI线程读取图片
Bitmap icon
= BitmapFactory.decodeResource(
getResources(),
R.drawable.ic_share);
builder.setActionButton(
icon,
"send email",
pi,
true); 添加menu item
Chrome Custom Tabs的menu默认包含了三个显示为图标的item(Forward, Page Info, Refresh)和两个文字item(Find in page, Open in Browser)。我们可以再添加最多5个文字item。同样需要创建PendingIntent:
Intent menuIntent = newIntent();
menuIntent.setClass(
getApplicationContext(),
SomeActivity.class);
PendingIntent pi
= PendingIntent.getActivity(
getApplicationContext(),
0,
menuIntent,
0);
builder.addMenuItem(
"Menu entry 1",
pi); 设置转场动画
如果你的APP设置了转场动画,那么为了统一的用户体验,可以在Chrome Custom Tabs中设置同样的动画
builder.setStartAnimations(
this,
R.anim.slide_in_right,
R.anim.slide_out_left);
builder.setExitAnimations(
this,
R.anim.slide_in_left,
R.anim.slide_out_right); 预启动(Warm up) Chrome和预加载
默认情况下,调用了CustomTabsIntent#launchUrl方法之后,才会在后台启动(原文是spin up)Chrome,然后加载网页。这个过程会花费宝贵的时间,并影响到用户体验。要是能「秒开」网页那就爽了。Chrome Custom Tabs可以绑定Chrome的一个Service,绑定成功之后可以预启动Chrome,还可以让Chrome预加载一些网页(当然这是要消耗一些流量的)。大致的过程是这样的:
通过CustomTabsClient#bindCustomTabsService方法绑定Service。绑定成功后会得到一个CustomTabsClient的instance。如果绑定失败,说明Chrome版本过低或者没有安装。
通过CustomTabsClient#warmup方法在后台启动Chrome。
通过CustomTabsClient#newSession方法创建一个新的session。这一步还可以传入一个CustomTabsCallback参数用来得到session的状态,比如页面是否加载完成。
通过CustomTabsSession#mayLaunchUrl来告诉Chrome要预加载哪些网页。
当用户点击事件发生时,将前面那个session传入CustomTabsIntent.Builder,然后用文章最开头的方法打开网页。
下面是完成这个过程的简单代码:
publicclassMainActivity
extendsActivity
implementsServiceConnectionCallback{
privateCustomTabsSession mSession;
privateCustomTabsClient mClient;
privateCustomTabsServiceConnection mConnection;
...
privatevoidbindCustomTabsService(){
mConnection = newServiceConnection( this);
CustomTabsClient.bindCustomTabsService(
this,
"com.android.chrome",
mConnection);
}
privatevoidwarmup(){
if(mClient != null)
mClient.warmup( 0);
}
privatevoidpreLaunch(String url){
if(mClent != null
&& mSession == null) {
mSession = mClient.newSession(
newCustomTabsCallback({
@Override
publicvoidonNavigationEvent(
intnavigationEvent,
Bundle extras){
//这里可以取到Session的状态
}
}));
}
//这里的第三个参数可以传入一些低优先级
//的Url,但不能保证会被预加载
mSession.mayLaunchUrl(
Uri.parse(url), null, null);
}
privatevoidlaunch(String url){
CustomTabsIntent.Builder builder
= newCustomTabsIntent.Builder(mSession);
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.launchUrl( this, Uri.parse(url)); }
@Override
publicvoidonDestroy(){
if(mConnection != null) {
unbindService(mConnection);
mClient = null;
mSession = null;
}
super.onDestroy();
}
// ServiceConnectionCallback的回调方法
@Override
publicvoidonServiceConnected(
CustomTabsClient client){
mClient = client;
}
@Override
publicvoidonServiceDisconnected(){
mClient = null;
}
} 简单的预启动
Android Support Library 24.0.0开始加入了CustomTabsClient#connectAndInitialize方法来简化预启动代码。如果你无法预料到用户会打开哪个URL,或者出于省电的考虑不想预加载URL,那么可以在Activity的onStart中加入一行代码来预启动。
CustomTabsClient
.connectAndInitialize(
this, "com.android.chrome"); 最佳实践 绑定Custom Tabs的service并预启动
预启动Chrome可以帮助你节省高达700ms的宝贵时间!这几乎是可以划分卡与不卡的差别。启动过程是在后台以低优先级进行,所以不会对你的APP性能有负面影响。
预加载网页内容
预加载网页后可以达到秒开的效果!所以如果你至少有50%的把握用户会打开某个URL,你应当调用mayLaunchUrl()方法。这个方法会提前下载并渲染网页内容,但不可避免的会有一点流量和电量的消耗。如果用户正在使用收费的数据流量,或者手机电量不足,那么这个方法不会生效。所以我们完全不用自己考虑性能优化。
备选方案
如果用户的手机上没有安装Chrome,那么打开默认浏览器可能并不是最好的用户体验。所以如果在bindService那一步失败了,无论是打开默认浏览器还是WebView,选择一个你认为最好的备选方案。
referrer
很多网站都会统计自己的流量是从哪儿来的,所以最好告诉他们是你的帅气APP给他们带来了流量:
intent.putExtra(
Intent.EXTRA_REFERRER,
Uri.parse(
Intent.URI_ANDROID_APP_SCHEME
+ "//"
+ context.getPackageName())); 加入自定义动画
自定义的转场动画会让你的网页跳转更流畅。确保进场动画和出场动画是反向的,比如网页从右边进来,就从右边出去,这样能帮助用户理解跳转关系。
builder.setStartAnimations(
this,
R.anim.slide_in_right,
R.anim.slide_out_left);
builder.setExitAnimations(
this,
R.anim.slide_in_left,
R.anim.slide_out_right); 为Action Button选个合适的图标
一个合适的图标能让用户快速的理解到APP的功能。但记住图标的最大尺寸是宽48dp高24dp。
其他支持的浏览器
前面有讲到其他浏览器也有机会支持Custom Tabs的功能(虽然我还没发现有哪款已经支持了)。如果你检测到不止一个浏览器支持Custom Tabs,那么第一次调用时最好询问用户打算用哪个浏览器。
/** * Returns a list of packages that support Custom Tabs. */
publicstaticArrayList getCustomTabsPackages(Context context){
PackageManager pm = context.getPackageManager();
// Get default VIEW intent handler.
Intent activityIntent
= newIntent(
Intent.ACTION_VIEW,
Uri.parse( "https://www.example.com"));
// Get all apps that can handle VIEW intents.
List resolvedActivityList
= pm.queryIntentActivities(activityIntent, 0);
ArrayList packagesSupportingCustomTabs = newArrayList<>();
for(ResolveInfo info : resolvedActivityList) {
Intent serviceIntent = newIntent();
serviceIntent.setAction(
ACTION_CUSTOM_TABS_CONNECTION);
serviceIntent.setPackage(
info.activityInfo.packageName);
// Check if this package also
// resolves the Custom Tabs service.
if(pm.resolveService(serviceIntent, 0) != null) {
packagesSupportingCustomTabs.add(info);
}
}
returnpackagesSupportingCustomTabs;
} 给用户选择权
如果你的APP之前一直是用默认浏览器打开URL,后来才加入的Custom Tabs,那么老用户可能希望保留原来的习惯。可以考虑在设置里面增加一个选项,让用户自行选择。
尽量让Native APP处理URL
有些URL可以由Native APP处理。如果用户安装了Twitter的APP并且点击了Twitter的URL,他可能更期望用Twitter APP打开。所以在打开URL之前,检查看看手机里有没有其他APP可以处理这个URL。
自定义Toolbar颜色
如果你希望用户觉得网页内容是你的APP的一部分,那么就将Toolbar颜色设为你的primaryColor。如果希望清除的表明网页内容与APP无关,那么选个不同的颜色吧。
增加一个分享按钮
用户可能想要把URL分享给好友,但Custom Tabs默认并没有分享按钮,所以最好自己加个吧。
自定义关闭按钮
Custom Tabs左上角的关闭按钮默认是一个叉叉。如果你希望用户感觉到网页内容是APP的一部分,那么最好把叉叉换成返回按钮。
builder.setCloseButtonIcon(
BitmapFactory.decodeResource(
getResources(),
R.drawable.ic_arrow_back)); 分清内部链接和外部链接
举个例子,如果用户在Twitter APP里点击了一个https://twitter.com开头的URL,那么应该在APP内部处理。Custom Tabs只应用来处理外部链接。
处理连击
如果你想在用户点击URL到打开Custom Tabs之间的这段时间做一点准备工作,确保不要超过100ms,否则用户可能会觉得APP没有反应而再次点击。
然而你懂的在Android上是无法完全避免卡顿的,所以当用户反复点击同一个URL时,你应该只将URL打开一次。
结尾
如果你的APP是使用默认浏览器打开URL,那么身为一个合格的开发者,即使你的APP只在国内上架,也应该加入Custom Tabs支持。毕竟国内还是有一些会科学上网的用户使用Chrome的。更重要的是,我们这些开发者如果看到有国内APP使用了Custom Tabs,会欣慰的点个赞!
赞赏支持 元杰
我来说两句排行榜