文章比較長,先放項目地址: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原創,轉載請註明做者、出處以及連接。