文章比较长,先放项目地址:PaperPlanehtml
俗话说,没图说个那啥,先看实际效果。java
全部的APP开发者都面临这样一个选择,当用户点击一个URL时,是应该用浏览器打开仍是应该用应用内置的WebView打开呢?android
两个选项都面临着一些问题。经过浏览器打开是一个很是重的上下文切换,而且是没法定制的。而WebView不能和浏览器共享数据而且须要须要手动去处理更多的场景。git
Chrome Custom Tabs让APP在进行网页浏览时更多的控制权限,在不采用WebView的状况下,这既能保证Native APP和网页之间流畅的切换,又容许APP定制Chrome的外观和操做。可定义的内容以下:github
toolbar的颜色web
进场和退场动画chrome
给Chrome的toolbar、overflow menu和bottom toolbar添加自定义操做浏览器
而且,Chrome Custom Tabs容许开发者对Chrome进行预启动和网页内容的预加载,以此提高加载的速度。安全
若是页面的内容是由咱们本身控制的,能够和Android组件进行交互,那么,WebView是一个好的选择,若是咱们的应用须要打开外部的网站,那么推荐使用Chrome Custom Tabs,缘由以下:性能优化
导入很是简单。不须要编写额外的代码来管理请求,授予权限或者存储cookie
定制UI:
Toolbar 颜色
动做按钮 (Action Button)
定制菜单项
定制进场退场动画
Bottom Toolbar
导航感知:浏览器通知回调接口通知应用网页的导航状况
安全性:浏览器使用了Google's Safe Browsing,用于保护用户和设备免受危险网站的侵害
性能优化:
浏览器会在后台进行预热,避免了应用占用大量资源的状况
提早向浏览器发送可能的URL,提升了页面加载速度
生命周期管理:在用户与Custom Tabs进行交互时,浏览器会将应用标示为前台应用,避免了应用被系统所回收
共享cookie数据和权限,这样,用户在已经受权过的网站,就不须要从新登陆或者受权权限了
若是用户开启了数据节省功能,在这里仍然能够从中受益
同步的自动补全功能
仅仅须要点击一下左上角按钮就能够直接返回原应用
想要在Lollipop以前的设备上最新引入的浏览器(Auto updating WebView),而不是旧版本的WebView
从Chrome 45版本开始,全部的Chrome用户均可以使用这项功能,目前仅支持Android系统。
完整的示例能够查看https://github.com/GoogleChrome/custom-tabs-client。包含了定制UI、链接后台服务、处理应用和Custom Tab Activity生命周期的可复用的类。
第一步固然是将 Custom Tabs Support Library 添加到工程中来。打开build.gradle
文件,添加support library的依赖。
dependencies { ... compile 'com.android.support:customtabs:23.3.0' }
一旦Support Library添加项目成功了,咱们就有两种可能的定制操做了:
定制UI和与Chrome Custom Tabs的交互
使页面加载更快速,保持应用激活
UI的定制是经过使用 CustomTabsIntent 和 CustomTabsIntent.Builder类完成的;而速度的提高则是经过使用 CustomTabsClient 连接Custom Tabs服务,预热Chrome和让Chrome知晓将要打开的URL实现的。
// 使用CustomTabsIntent.Builder配置CustomTabsIntent // 准备完成后,调用CustomTabsIntent.Builder.build()方法建立一个CustomTabsIntent // 并经过CustomTabsIntent.launchUrl()方法加载但愿加载的url String url = ¨https://github.com/marktony¨; CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); CustomTabsIntent customTabsIntent = builder.build(); customTabsIntent.launchUrl(this, Uri.parse(url));
Chrome Custom Tabs一个很重要的功能就是咱们可以改变地址栏的颜色,使之和咱们应用的颜色协调。
// 改变toolbar的背景色。colorInt就是想要指定的int值 builder.setToolbarColor(colorInt);
做为应用的开发者,咱们对呈如今用户眼前的Chrome Custom Tab内的Action Button拥有彻底的控制权。
在大部分的状况下,用户会执行最基础的操做像分享,或者是其余公共的Activity。
Action Button被表示为一个action button的图标和用户点击action button以后Chrome将要执行的pendingIntent。图标的高度通常为24dp,宽度通常为24-48dp。
// 向toolbar添加一个Action Button // ‘icon’是一张位图(Bitmap),做为action button的图片资源使用 // 'description'是一个字符串,做为按钮的无障碍描述所使用 // 'pendingIntent' 是一个PendingIntent,当action button或者菜单项被点击时调用。 // 在url做为data被添加以后,Chrome 会调用PendingIntent#send()方法。 // 客户端应用会经过调用Intent#getDataString()获取到URL // 'tint'是一个布尔值,定义了Action Button是否应该被着色 builder.setActionButton(icon, description, pendingIntent, tint);
Chrome浏览器拥有很是全面的action菜单,用户在浏览器内操做很是顺畅。然而,对于咱们本身的应用,可能就不适合了。
Chrome Custom Tabs顶部有三个横向排列的图标,分别是“前进”、"页面信息"和”刷新“。在菜单的底部分别是"查找页面"和“在浏览器中打开”。
做为开发者,咱们最多能够在顶部横向图标和底部菜单之间添加5个自定义菜单选项。
菜单项经过调用CustomTabsIntent.Builder#addMenuItem)添加,title和用户点击菜单选项后Chrome调用的pendingIntent须要做为参数被传入。
builder.addMenuItem(menuItemTitle, menuItemPendingIntent);
许多的Android都会在Activity之间切换时使用自定义的视图进入和退出动画。Chrome Custom Tabs也同样,咱们能够改变进入和退出动画,以此保持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);
默认状况下,当 CustomTabsIntent#launchUrl )被调用时会激活Chrome,加载URL。这会花费咱们宝贵的时间而且影响流畅度。
Chrome团队了解用户对于流畅体验的渴望,因此他们在Chrome中提供了一个Service使咱们的APP可以链接而且预热浏览器和原生组件。他们也把这种能力分享给了咱们普通开发者,开发者可以告知Chrome用户访问页面的可能性。而后,Chrome就能完成以下的操做:
主域名的DNS预解析
最有可能加载的资源的DNS预解析
包括HTTPS/TLS验证在内的预链接
预热Chrome的步骤以下:
使用CustomTabsClient#bindCustomTabsService)链接service
一旦service链接成功,后台调用 CustomTabsClient#warmup)启动Chrome
调用 CustomTabsClient#newSession )建立一个新的session.这个session被用做全部的API请求
咱们能够在建立session时选择性的添加一个 CustomTabsCallback做为参数,这样咱们就能知道页面是否被加载完成
经过 CustomTabsSession#mayLaunchUrl)告知Chrome用户最有可能加载的页面
调用 CustomTabsIntent.Builder 构造方法,并传入已经建立好的CustomTabsSession做为参数传入
CustomTabsClient#bindCustomTabsService http://developer.android.com/... java.lang.String, android.support.customtabs.CustomTabsServiceConnectio)) 方法简化了链接Custom Tabs服务的过程。
建立一个继承自CustomTabsServiceConnection的类并使用onCustomTabsServiceConnected )方法获取 CustomTabsClient的实例。在下一步中会用到此实例:
// 官方示例 // 客户端须要链接的Chrome的包名,取决于channel的名称 // Stable(发行版) = com.android.chrome // Beta(测试版) = com.chrome.beta // Dev(开发版) = com.chrome.dev public static final String CUSTOM_TAB_PACKAGE_NAME = "com.android.chrome"; // Change when in stable CustomTabsServiceConnection connection = new CustomTabsServiceConnection() { @Override public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) { mCustomTabsClient = client; } @Override public void onServiceDisconnected(ComponentName name) { } }; boolean ok = CustomTabsClient.bindCustomTabsService(this, mPackageNameToBind, connection);
// 个人示例 package com.marktony.zhihudaily.customtabs; import android.support.customtabs.CustomTabsServiceConnection; import android.content.ComponentName; import android.support.customtabs.CustomTabsClient; import java.lang.ref.WeakReference; /** * Created by Lizhaotailang on 2016/9/4. * Implementation for the CustomTabsServiceConnection that avoids leaking the * ServiceConnectionCallback */ public class ServiceConnection extends CustomTabsServiceConnection { // A weak reference to the ServiceConnectionCallback to avoid leaking it. private WeakReference<ServiceConnectionCallback> mConnectionCallback; public ServiceConnection(ServiceConnectionCallback connectionCallback) { mConnectionCallback = new WeakReference<>(connectionCallback); } @Override public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) { ServiceConnectionCallback connectionCallback = mConnectionCallback.get(); if (connectionCallback != null) connectionCallback.onServiceConnected(client); } @Override public void onServiceDisconnected(ComponentName name) { ServiceConnectionCallback connectionCallback = mConnectionCallback.get(); if (connectionCallback != null) connectionCallback.onServiceDisconnected(); } }
预热浏览器进程并加剧原生库文件。预热是异步进行的,返回值表示请求是否被接收。多个成功的请求都会返回true。
true
表明着成功。
boolean newSession(CustomTabsCallback callback))
session用于在连续请求中连接mayLaunchUrl方法。CustomTabsIntent和tab互相联系。这里所提供的回调和已经建立成功的session相关。经过这个回调,任何关于已经成功建立的session的更新都会被接收到。返回session是否被成功建立。多个具备相同CustomTabsCallback或者null值的请求都会返回false。
boolean mayLaunchUrl(Uri url, Bundle extras, List otherLikelyBundles))
CustomTabsSession方法告知浏览器将来可能导航到的url。warmup())方法应该先被调用。最有可能的url应该最早被指出。也能够选择性的提供可能加载的url的列表。列表中的数据被认为被加载的可能性小于最初的那一个,并且必须按照优先级降序排列。这些额外的url可能被忽略掉。全部以前对于这个方法的调用都会被去优先化。返回操做是否成功完成。
void onNavigationEvent(int navigationEvent, Bundle extras))
在custom tab中,导航事件发生时被调用。‘navigationEvent int’是关于页面内的6个状态值之一。6个状态值定义以下:
/** * 页面开始加载时被发送 */ public static final int NAVIGATION_STARTED = 1; /** * 页面完成加载时被发送 */ public static final int NAVIGATION_FINISHED = 2; /** * 因为错误tab不能完成加载时被发送 */ public static final int NAVIGATION_FAILED = 3; /** * 在加载完成以前,加载由于用户动做例如点击了另一个连接或者刷新页面 * 加载被停止时被发送 */ public static final int NAVIGATION_ABORTED = 4; /** * tab状态变为可见时发送 */ public static final int TAB_SHOWN = 5; /** * tab状态变为不可见时发送 */ public static final int TAB_HIDDEN = 6;
Custom Tabs经过带有key Extras的 ACTION_VIEW Intent来定制UI。这就意味着将要打开的页面会经过系统浏览器或者用户默认浏览器打开。
若是用户已经安装了Chrome而且是默认浏览器,它会自动的获取EXTRAS的值并提供一个定制化的UI。其余的浏览器使用Intent extras提供相同的定制UI也是有可能的。
全部支持Chrome Custom Tabs的Chrome浏览器都暴露了一个service。为了检测是否支持Chrome Custom Tabs,能够尝试着绑定service,若是成功的话,那么Customs Tabs能够成功的使用。
启用Chrome Custom Tabs后,咱们看到了各类不一样质量界别的实现效果。这里介绍一组实现优秀集成的最佳实践。
链接到Custom Tabs Service并预加载Chrome以后,经过Custom Tabs打开连接 最多能够节省700ms 。
在咱们打算启用Custom Tabs的Activity的 onStart()) 方法中链接 Custom Tabs service。链接成功后,调用warmup()方法。
Custom Tabs做为一个很是低优先级的进程,这也就意味着 它不会对咱们的应用不会有任何的负面的影响,可是当加载连接时,会得到很是好的启动性能。
预渲染让内容打开很是迅速。因此,若是用户 至少有50%的可能性 打开某个连接,调用mayLaunchUrl()
方法。
调用mayLaunchUrl()
方法方法能使Custom Tabs预获取主页面所支持的内容并预渲染。这会最大程度的加快页面的加载速度。可是会不可避免的有 一点流量和电量的消耗。
Custom Tabs很是的智能,可以感知用户是否在使用收费的网络或者设备电量不足,预渲染对设备的总体性能有负面的影响,在这样的场景下,Custom Tabs就不会进行预获取或者预渲染。因此,不用担忧应用的性能问题。
尽管Custom Tabs对于大多数用户都是适用的,仍然有一些场景不适用,例如设备上没有安装支持Custom Tabs的浏览器或者是设备上的浏览器版本不支持Custom Tabs。
确保提供了备选方案以提供好的应用体验。打开默认浏览器或者引入WebView都是不错的选择。
一般,对于网站而言,追用访问的来源很是地重要。当加载了Custom Tabs时,经过设置referrer,让他们知晓咱们正在给他们提升访问量。
intent.putExtra(Intent.EXTRA_REFERRER, Uri.parse(Intent.URI_ANDROID_APP_SCHEME + "//" + context.getPackageName()));
定制的动画可以使咱们的应用切换到网页内容时更加地顺畅。 确保进场动画和出厂动画是反向的,这样可以帮助用户理解跳转的关系。
//设置定制的进入/退出动画 CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder(); intentBuilder.setStartAnimations(this, R.anim.slide_in_right, R.anim.slide_out_left); intentBuilder.setExitAnimations(this, android.R.anim.slide_in_left, android.R.anim.slide_out_right); //打开Custom Tab intentBuilder.build().launchUrl(context, Uri.parse("https://github.com/marktony"));
添加一个Action Button可以使用户更加理解APP的功能。可是,若是没有好的icon表明Action Button将要执行的操做,有必要建立一个带操做文字描述的位图。
牢记位图的最大尺寸为高度24dp,宽度48dp。
String shareLabel = getString(R.string.label_action_share); Bitmap icon = BitmapFactory.decodeResource(getResources(), android.R.drawable.ic_menu_share); // 为咱们的BroadCastReceiver建立一个PendingIntent Intent actionIntent = new Intent( this.getApplicationContext(), ShareBroadcastReceiver.class); PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, actionIntent, 0); // 设置pendingIntent做为按钮被点击后将要执行的操做 intentBuilder.setActionButton(icon, shareLabel, pendingIntent);
牢记用户安装的浏览器中,支持Custom Tabs的数量可能不止一个。若是有不止一个浏览器支持Custom Tabs,而且没有任何一个浏览器被设置为偏好浏览器,须要询问用户如何打开连接
/** * 返回支持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")); // 获取全部可以处理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; }
为应用添加一个设置选项,容许用户经过默认浏览器而不是Custom Tab打开连接。若是咱们的应用在添加Custom Tabs以前,都是经过默认浏览器打开连接显得尤其重要。
Native应用能够处理一些url。若是用户安装了Twitter APP,在点击tweet内的连接时,她更加但愿Twitter应用可以处理这些连接。
在应用内打开连接以前,检查手机里有没有其余APP可以处理这些url。
若是想要让用户感受网页内容是咱们应用的一部分,将toolbar的颜色设置为primaryColor。
若是想要让用户清楚的了解到已经离开了咱们的应用,那就彻底不要定义toolbar的颜色。
// 设置自定义的toolbar的颜色 CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder(); intentBuilder.setToolbarColor(Color.BLUE);
确保在overflow菜单中添加了一个分享的操做,在大多数的状况下,用户但愿可以分享当前所见网页内容的连接,Custom Tabs默认没有添加分享的按钮。
// 在BroadCastReceiver中分享来自CustomTabs的内容 public void onReceive(Context context, Intent intent) { String url = intent.getDataString(); if (url != null) { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_TEXT, url); Intent chooserIntent = Intent.createChooser(shareIntent, "Share url"); chooserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(chooserIntent); } }
自定义关闭按钮使CustomTabs看起来像应用的一部分。
若是但愿CustomTabs在用户看来像一个Dialog, 使用'x'(叉叉)按钮。若是但愿Custom Tab是用户的一部分,使用返回箭头。
//设置自定义的关闭按钮 CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder(); intentBuilder.setCloseButtonIcon(BitmapFactory.decodeResource( getResources(), R.drawable.ic_arrow_back));
当监听到连接是由android:autoLink生成的或者在WebView中复写了click方法,确保咱们的应用处理了这些内容的连接,让CustomTabs处理外部连接。
WebView webView = (WebView)findViewById(R.id.webview); webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { return true; } @Override public void onLoadResource(WebView view, String url) { if (url.startsWith("http://www.example.com")) { //Handle Internal Link... } else { //Open Link in a Custom Tab Uri uri = Uri.parse(url); CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder(mCustomTabActivityHelper.getSession()); //Open the Custom Tab intentBuilder.build().launchUrl(context, url)); } } });
若是但愿在用户点击连接和打开CustomTabs之间作一些准备工做,确保所花费的时间不超过100ms。不然用户会认为APP没有响应,多是试着点击连接屡次。
若是不能避免延迟,确保咱们的应用对可能的状况作好准备,当用户点击相同的连接屡次时,不要屡次打开CustomTab。
尽管整合Custom Tabs的推荐方式是使用Custom Tabs Support Library,低API版本的系统也是可使用的。
完整的Support Library的导入方法能够参见GitHub,并能够作为一个起点。链接service的AIDL文件也被包含在其中,Chromium仓库中也包含了这些文件,而这些文件在Android Studio中是不能直接被使用的。
String url = ¨https://github.com/marktony¨; Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); private static final String EXTRA_CUSTOM_TABS_SESSION = "android.support.customtabs.extra.SESSION"; Bundle extras = new Bundle; extras.putBinder(EXTRA_CUSTOM_TABS_SESSION, sessionICustomTabsCallback.asBinder() /* 不须要session时设置为null */); intent.putExtras(extras);
UI定制是经过向ACTION_VIEW Intent添加Extras实现的。用于定制UI的完整的extras keys的列表能够在 CustomTabsIntent docs 找到。下面是添加自定义的toolbar的颜色的示例:
private static final String EXTRA_CUSTOM_TABS_TOOLBAR_COLOR = "android.support.customtabs.extra.TOOLBAR_COLOR"; intent.putExtra(EXTRA_CUSTOM_TABS_TOOLBAR_COLOR, colorInt);
Custom Tabs service和其余Android Service的使用方法相同。接口经过AIDL建立而且代理service类也会自动建立。
// 客户端须要链接的Chrome的包名,取决于channel的名称 // Stable(发行版) = com.android.chrome // Beta(测试版) = com.chrome.beta // Dev(开发版) = com.chrome.dev public static final String CUSTOM_TAB_PACKAGE_NAME = "com.chrome.dev"; // Change when in stable public static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService"; Intent serviceIntent = new Intent(ACTION_CUSTOM_TABS_CONNECTION); serviceIntent.setPackage(CUSTOM_TAB_PACKAGE_NAME); context.bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY);
若是咱们的应用是面向国外用户的,那理所固然的,应该加入Chrome Custom Tabs的支持,这在很大程度上可以提高用户的体验。若是咱们的应用只是面向国内用户,个人建议仍是应该加上这项功能,毕竟,仍是有部分用户安装了Chrome浏览器,当用户浏览到Custom Tab页面,应该也会像我同样,感受到眼前一亮吧。
文章比较长,感谢阅读。
本文章由简书用户TonnyL原创,转载请注明做者、出处以及连接。