Android 通過Chrome Custom Tab開啟網頁

語言: CN / TW / HK

在日常開發中,偶爾會需要在App中開啟網頁,通常會使用WebView來實現。本文介紹一下另一種實現方式Chrome Custom Tab。

Chrome Custom Tab

Custom Tab是Chrome瀏覽器引入的一個功能,現在市面上大部分安卓裝置的瀏覽器都已經支援此功能。Custom Tab使App原生內容與網頁內容的過渡更加流暢,支援自定義部分樣式,可以保持與App一致的風格,支援預載入。

官方文件

新增庫

在app module下的build.gradle中新增程式碼,如下:

dependencies { implementation 'androidx.browser:browser:1.5.0' }

檢查Custom Tab是否可用

儘管現在市面上大部分安卓裝置的瀏覽器都已支援Custom Tab,但為了確保部分裝置不支援該功能的情況下使用者體驗正常,可以先檢查當前裝置是否支援該功能,不支援的話仍然通過WebView實現。程式碼如下:

fun checkCustomTabAvailable(context: Context): Boolean { val packageManager = context.packageManager val browsableIntent = Intent().apply { action = Intent.ACTION_VIEW addCategory(Intent.CATEGORY_BROWSABLE) data = Uri.fromParts("http", "", null) } // 獲取所有瀏覽器 val browsableResolverInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { packageManager.queryIntentActivities(browsableIntent, PackageManager.ResolveInfoFlags.of(0)) } else { packageManager.queryIntentActivities(browsableIntent, 0) } val supportingCustomTabResolveInfo = ArrayList<ResolveInfo>() browsableResolverInfo.forEach { val serviceIntent = Intent().apply { action = androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION setPackage(it.activityInfo.packageName) } val customTabServiceResolverInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { packageManager.resolveService(serviceIntent, PackageManager.ResolveInfoFlags.of(0)) } else { packageManager.resolveService(serviceIntent, 0) } // 判斷是否可以處理Custom Tabs service if (customTabServiceResolverInfo != null) { supportingCustomTabResolveInfo.add(it) } } return supportingCustomTabResolveInfo.isNotEmpty() }

開啟網頁

通過Custom Tab開啟網頁程式碼如下:

fun openCustomTab(context: Context, url: String) { // url 為要開啟的網址 CustomTabsIntent.Builder().build().launchUrl(context, url.toUri()) }

需要注意的是,不能通過此方式開啟Assets下的H5檔案。

調整UI

Custom Tab支援自定義部分樣式。

調整檢視高度

可以使用CustomTabsIntent.Builder中的setInitialActivityHeightPx方法來調整開啟的Custom Tab的高度,同時可以使用setToolbarCornerRadiusDp來設定圓角。具體實現方式有如下兩種:

    1. 連線Custom Tab Service(建議使用此方式)。

``` // 輔助類 object CustomTabHelper {

// Custom Tab 可用的包名
private var customTabAvailablePackageName: String = ""
private var customTabsClient: CustomTabsClient? = null
private var customTabsServiceConnection: CustomTabsServiceConnection? = null

fun openCustomTabWithInitialHeight(context: Context, url: String, activityHeight: Int = 0, radius: Int = 0, adjustable: Boolean = false) {
    val customTabsIntentBuilder = CustomTabsIntent.Builder(customTabsClient?.newSession(null))
    if (activityHeight != 0) {
        // 第二個引數配置預期行為
        // ACTIVITY_HEIGHT_ADJUSTABLE 使用者可以手動調整檢視高度
        // ACTIVITY_HEIGHT_FIXED 使用者無法手動調整檢視高度
        customTabsIntentBuilder.setInitialActivityHeightPx(activityHeight, if (adjustable) CustomTabsIntent.ACTIVITY_HEIGHT_ADJUSTABLE else CustomTabsIntent.ACTIVITY_HEIGHT_FIXED)
        if (radius != 0) {
            customTabsIntentBuilder.setToolbarCornerRadiusDp(radius)
        }
    }
    customTabsIntentBuilder.build().launchUrl(context, url.toUri())
}

fun checkCustomTabAvailable(context: Context): Boolean {
    val packageManager = context.packageManager
    val browsableIntent = Intent().apply {
        action = Intent.ACTION_VIEW
        addCategory(Intent.CATEGORY_BROWSABLE)
        data = Uri.fromParts("http", "", null)
    }
    // 獲取所有瀏覽器
    val browsableResolverInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        packageManager.queryIntentActivities(browsableIntent, PackageManager.ResolveInfoFlags.of(0))
    } else {
        packageManager.queryIntentActivities(browsableIntent, 0)
    }
    val supportingCustomTabResolveInfo = ArrayList<ResolveInfo>()
    browsableResolverInfo.forEach {
        val serviceIntent = Intent().apply {
            action = androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION
            setPackage(it.activityInfo.packageName)
        }
        val customTabServiceResolverInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            packageManager.resolveService(serviceIntent, PackageManager.ResolveInfoFlags.of(0))
        } else {
            packageManager.resolveService(serviceIntent, 0)
        }
        // 判斷是否可以處理Custom Tabs service
        if (customTabServiceResolverInfo != null) {
            supportingCustomTabResolveInfo.add(it)
        }
    }
    if (supportingCustomTabResolveInfo.isNotEmpty()) {
        customTabAvailablePackageName = supportingCustomTabResolveInfo[0].activityInfo.packageName
    }
    return supportingCustomTabResolveInfo.isNotEmpty()
}

fun bindCustomTabsService(activity: Activity) {
    if (checkCustomTabAvailable(activity)) {
        if (customTabsClient == null) {
            customTabsServiceConnection = object : CustomTabsServiceConnection() {
                override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
                    customTabsClient = client
                }

                override fun onServiceDisconnected(name: ComponentName?) {
                    customTabsClient = null
                }
            }
            customTabsServiceConnection?.let {
                CustomTabsClient.bindCustomTabsService(activity, customTabAvailablePackageName, it)
            }
        }
    }
}

fun unbindCustomTabsService(activity: Activity) {
    customTabsServiceConnection?.let { activity.unbindService(it) }
    customTabsClient = null
    customTabsServiceConnection = null
}

}

// 示例Activity class CustomTabExampleActivity : BaseGestureDetectorActivity() {

private val url = "https://go.minigame.vip/"
private var activityHeight = 0
private var topRadius = 16
private var changeHeightAdjustable = false

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding: LayoutCustomTabActivityBinding = DataBindingUtil.setContentView(this, R.layout.layout_custom_tab_activity)
    activityHeight = (resources.displayMetrics.heightPixels * 0.8).toInt()
    binding.btnChangeHeightFixed.setOnClickListener {
        checkCustomTabAvailable()
        changeHeightAdjustable = false
        CustomTabHelper.openCustomTabWithInitialHeight(this, url, activityHeight, topRadius, changeHeightAdjustable)
    }
    binding.btnChangeHeightAdjustable.setOnClickListener {
        checkCustomTabAvailable()
        changeHeightAdjustable = true
        CustomTabHelper.openCustomTabWithInitialHeight(this, url, activityHeight, topRadius, changeHeightAdjustable)
    }
}

private fun checkCustomTabAvailable() {
    if (!CustomTabHelper.checkCustomTabAvailable(this)) {
        startActivity(Intent(this, WebViewActivity::class.java).apply { putExtra(PARAMS_LINK_URL, url) })
        return
    }
}

override fun onStart() {
    super.onStart()
    CustomTabHelper.bindCustomTabsService(this)
}

override fun onDestroy() {
    super.onDestroy()
    CustomTabHelper.unbindCustomTabsService(this)
}

} ```

    1. 使用startActivityForResult

``` class CustomTabExampleActivity : BaseGestureDetectorActivity() {

private val url = "https://go.minigame.vip/"
private var activityHeight = 0
private var topRadius = 16
private var changeHeightAdjustable = false

private val customTabLauncher = registerForActivityResult(object : ActivityResultContract<String, Int>() {
    override fun createIntent(context: Context, input: String): Intent {
        val customTabsIntentBuilder = CustomTabsIntent.Builder()
            .setInitialActivityHeightPx(activityHeight, if (changeHeightAdjustable) CustomTabsIntent.ACTIVITY_HEIGHT_ADJUSTABLE else CustomTabsIntent.ACTIVITY_HEIGHT_FIXED)
            .setToolbarCornerRadiusDp(topRadius)
        return customTabsIntentBuilder.build().intent.apply {
            data = input.toUri()
        }
    }

    override fun parseResult(resultCode: Int, intent: Intent?): Int {
        return resultCode
    }
}) {
    // 頁面返回回撥
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding: LayoutCustomTabActivityBinding = DataBindingUtil.setContentView(this, R.layout.layout_custom_tab_activity)
    activityHeight = (resources.displayMetrics.heightPixels * 0.8).toInt()
    binding.btnChangeHeightFixed.setOnClickListener {
        checkCustomTabAvailable()
        changeHeightAdjustable = false
        customTabLauncher.launch(url)
    }
    binding.btnChangeHeightAdjustable.setOnClickListener {
        checkCustomTabAvailable()
        changeHeightAdjustable = true
        customTabLauncher.launch(url)
    }
}

private fun checkCustomTabAvailable() {
    if (!CustomTabHelper.checkCustomTabAvailable(this)) {
        startActivity(Intent(this, WebViewActivity::class.java).apply { putExtra(PARAMS_LINK_URL, url) })
        return
    }
}

} ```

效果如圖:

device-2023-02-19-09 -middle-original.gif

調整位址列

可以對Custom Tab的位址列進行一些配置,程式碼如下:

``` // 輔助類 object CustomTabHelper {

fun openCustomTabWithCustomUI(context: Context, url: String, @ColorInt color: Int = 0, showTitle: Boolean = false, autoHide: Boolean = false, backIconPosition: Int = CustomTabsIntent.CLOSE_BUTTON_POSITION_START) {
    val customTabsIntentBuilder = CustomTabsIntent.Builder(customTabsClient?.newSession(null))
    if (color != 0) {
        // 設定背景顏色
        customTabsIntentBuilder.setDefaultColorSchemeParams(CustomTabColorSchemeParams.Builder()
            .setToolbarColor(color)
            .build())
    }
    // 是否顯示標題
    customTabsIntentBuilder.setShowTitle(showTitle)
    // 位址列是否自動隱藏 ,此配置僅在Custom Tab全屏顯示時生效
    customTabsIntentBuilder.setUrlBarHidingEnabled(autoHide)
    // 調整關閉按鈕的位置
    // CustomTabsIntent.CLOSE_BUTTON_POSITION_START 在位址列的左側
    // CustomTabsIntent.CLOSE_BUTTON_POSITION_END 在位址列的右側
    customTabsIntentBuilder.setCloseButtonPosition(backIconPosition)
    customTabsIntentBuilder.build().launchUrl(context, url.toUri())
}

}

// 示例Activity class CustomTabExampleActivity : BaseGestureDetectorActivity() {

private val url = "https://go.minigame.vip/"

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding: LayoutCustomTabActivityBinding = DataBindingUtil.setContentView(this, R.layout.layout_custom_tab_activity)
    binding.btnCustomUi.setOnClickListener {
        checkCustomTabAvailable()
        CustomTabHelper.openCustomTabWithCustomUI(this, url, ContextCompat.getColor(this, R.color.color_FF2600), showTitle = true, autoHide = true, CustomTabsIntent.CLOSE_BUTTON_POSITION_END)
    }
}

} ```

效果如圖:

device-2023-02-19-09 -middle-original.gif

調整顯示隱藏動畫

當Custom Tab為全屏顯示時,可以調整顯示與隱藏時的動畫,程式碼如下:

``` // slide_in_right

// slide_out_left

// 輔助類 object CustomTabHelper { fun openCustomTabWithCustomAnimations(context: Context, url: String) { val customTabsIntentBuilder = CustomTabsIntent.Builder(customTabsClient?.newSession(null)) // 自定義動畫 customTabsIntentBuilder.setStartAnimations(context, R.anim.slide_in_right, R.anim.slide_out_left) // 系統動畫 customTabsIntentBuilder.setExitAnimations(context, android.R.anim.slide_in_left, android.R.anim.slide_out_right) customTabsIntentBuilder.build().launchUrl(context, url.toUri()) } }

// 示例Activity class CustomTabExampleActivity : BaseGestureDetectorActivity() {

private val url = "https://go.minigame.vip/"

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding: LayoutCustomTabActivityBinding = DataBindingUtil.setContentView(this, R.layout.layout_custom_tab_activity)
    binding.btnCustomAnimations.setOnClickListener {
        checkCustomTabAvailable()
        CustomTabHelper.openCustomTabWithCustomAnimations(this, url)
    }
}

} ```

效果如圖:

device-2023-02-19-09 -original-original.gif

示例Demo

在示例Demo中添加了相關的演示程式碼。

ExampleDemo github

ExampleDemo gitee