距离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和预加载网页内容的功能,与传统方式相比加载速度有显著提升。

alt

什么时候用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

1
2
3
4
dependencies {
...
compile 'com.android.support:customtabs:24.1.1'
}

然后写几行代码

1
2
3
4
String url = "https://www.google.com";
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.launchUrl(this, Uri.parse(url));

就好了!不费吹灰之力~

注意launchUrl这个方法的第一个参数是个Activity。肯定有人会跳起来说,我要在ViewHolder里面处理点击跳转,根本拿不到Activity,只有Context肿么办?!!(其实这个人就是我)

那么我们看看launchUrl的源码:

1
2
3
4
public void launchUrl(Activity context, Uri url) {
intent.setData(url);
ActivityCompat.startActivity(context, intent, startAnimationBundle);
}

原来是为了传入startAnimationBundle这个参数来实现自定义转场动画。那么我们自己处理一下就好了:

1
2
3
4
5
6
7
8
9
10
11
if (context instanceof Activity) {
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内打开网页一样。一行代码搞定:

1
builder.setToolbarColor(colorInt);

添加action button

你可能想要在Toolbar上加上action button,那么需要创建一个PendingIntent。下面的代码增加了一个发送邮件的action button:

1
2
3
4
5
6
7
Intent actionIntent = new Intent(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);
Bitmap icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_share);//注意在正式项目中不要在UI线程读取图片
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:

1
2
3
4
Intent menuIntent = new Intent();
menuIntent.setClass(getApplicationContext(), SomeActivity.class);
PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0, menuIntent, 0);
builder.addMenuItem("Menu entry 1", pi);

设置转场动画

如果你的APP设置了转场动画,那么为了统一的用户体验,可以在Chrome Custom Tabs中设置同样的动画

1
2
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预加载一些网页(当然这是要消耗一些流量的)。大致的过程是这样的:

下面是完成这个过程的简单代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class MainActivity extends Activity implements ServiceConnectionCallback {
private CustomTabsSession mSession;
private CustomTabsClient mClient;
private CustomTabsServiceConnection mConnection;

...

private void bindCustomTabsService() {
mConnection = new ServiceConnection(this);
CustomTabsClient.bindCustomTabsService(this, "com.android.chrome", mConnection);
}

private void warmup() {
if (mClient != null) mClient.warmup(0);
}

private void preLaunch(String url) {
if (mClent != null && mSession == null) {
mSession = mClient.newSession(new CustomTabsCallback({
@Override
public void onNavigationEvent(int navigationEvent, Bundle extras) {
// 这里可以取到Session的状态
}
}));
}
mSession.mayLaunchUrl(Uri.parse(url), null, null);//这里的第三个参数可以传入一些低优先级的Url,但不能保证会被预加载
}

private void launch(String url) {
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(mSession);
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.launchUrl(this, Uri.parse(url));
}

@Override
public void onDestroy() {
if (mConnection != null) {
unbindService(mConnection);
mClient = null;
mSession = null;
}
super.onDestroy();
}

/** ServiceConnectionCallback的回调方法 */
@Override
public void onServiceConnected(CustomTabsClient client) {
mClient = client;
}

@Override
public void onServiceDisconnected() {
mClient = null;
}
}

简单的预启动

Android Support Library 24.0.0开始加入了CustomTabsClient#connectAndInitialize方法来简化预启动代码。如果你无法预料到用户会打开哪个URL,或者出于省电的考虑不想预加载URL,那么可以在Activity的onStart中加入一行代码来预启动。

1
CustomTabsClient.connectAndInitialize(this, "com.android.chrome");

最佳实践

绑定Custom Tabs的service并预启动

预启动Chrome可以帮助你节省高达700ms的宝贵时间!这几乎是可以划分卡与不卡的差别。启动过程是在后台以低优先级进行,所以不会对你的APP性能有负面影响

预加载网页内容

预加载网页后可以达到秒开的效果!所以如果你至少有50%的把握用户会打开某个URL,你应当调用mayLaunchUrl()方法。这个方法会提前下载并渲染网页内容,但不可避免的会有一点流量和电量的消耗。如果用户正在使用收费的数据流量,或者手机电量不足,那么这个方法不会生效。所以我们完全不用自己考虑性能优化

备选方案

如果用户的手机上没有安装Chrome,那么打开默认浏览器可能并不是最好的用户体验。所以如果在bindService那一步失败了,无论是打开默认浏览器还是WebView,选择一个你认为最好的备选方案。

referrer

很多网站都会统计自己的流量是从哪儿来的,所以最好告诉他们是你的帅气APP给他们带来了流量:

1
2
intent.putExtra(Intent.EXTRA_REFERRER, 
Uri.parse(Intent.URI_ANDROID_APP_SCHEME + "//" + context.getPackageName()));

加入自定义动画

自定义的转场动画会让你的网页跳转更流畅。确保进场动画和出场动画是反向的,比如网页从右边进来,就从右边出去,这样能帮助用户理解跳转关系。

1
2
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,那么第一次调用时最好询问用户打算用哪个浏览器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Returns a list of packages that support Custom Tabs.
*/
public static ArrayList getCustomTabsPackages(Context context) {
PackageManager pm = context.getPackageManager();
// Get default VIEW intent handler.
Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));

// Get all apps that can handle VIEW intents.
List resolvedActivityList = pm.queryIntentActivities(activityIntent, 0);
ArrayList packagesSupportingCustomTabs = new ArrayList<>();
for (ResolveInfo info : resolvedActivityList) {
Intent serviceIntent = new Intent();
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);
}
}
return packagesSupportingCustomTabs;
}

给用户选择权

如果你的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的一部分,那么最好把叉叉换成返回按钮。

1
builder.setCloseButtonIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_arrow_back));

分清内部链接和外部链接

举个例子,如果用户在Twitter APP里点击了一个http://twitter.com开头的URL,那么应该在APP内部处理。Custom Tabs只应用来处理外部链接。

处理连击

如果你想在用户点击URL到打开Custom Tabs之间的这段时间做一点准备工作,确保不要超过100ms,否则用户可能会觉得APP没有反应而再次点击。

然而你懂的在Android上是无法完全避免卡顿的,所以当用户反复点击同一个URL时,你应该只将URL打开一次。

结尾

如果你的APP是使用默认浏览器打开URL,那么身为一个合格的开发者,即使你的APP只在国内上架,也应该加入Custom Tabs支持。毕竟国内还是有一些会科学上网的用户使用Chrome的。更重要的是,我们这些开发者如果看到有国内APP使用了Custom Tabs,会欣慰的点个赞!