在日常开发中,偶然会需要在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
        }
    }
}

作用如图:

Android Chrome  Custom Tab(一)— 自定义样式与基本使用

调整地址

能够对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, backIcon: Bitmap? = null) {
        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)
        // 调整回来按钮图标
        backIcon?.let { customTabsIntentBuilder.setCloseButtonIcon(it) }
        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()
            var backIcon: Bitmap? = null
            ContextCompat.getDrawable(this, R.drawable.icon_back)?.let { backIcon = toBitmap(it) }
            CustomTabHelper.openCustomTabWithCustomUI(this, url, ContextCompat.getColor(this, R.color.color_FF2600), showTitle = true, autoHide = true, CustomTabsIntent.CLOSE_BUTTON_POSITION_END, backIcon)
        }
    }
    private fun toBitmap(drawable: Drawable): Bitmap? {
        // 注意图标的宽高必须为24dp,否则不会收效
        val width = DensityUtil.dp2Px(24)
        val height = DensityUtil.dp2Px(24)
        val oldBounds = Rect(drawable.bounds)
        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
        drawable.setBounds(0, 0, width, height)
        drawable.draw(Canvas(bitmap))
        drawable.bounds = oldBounds
        return bitmap
    }
}

作用如图:

Android Chrome  Custom Tab(一)— 自定义样式与基本使用

调整显现隐藏动画

当Custom Tab为全屏显现时,能够调整显现与隐藏时的动画,代码如下:

// slide_in_right
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="100%p" android:toXDelta="0"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>
// slide_out_left
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0" android:toXDelta="-100%p"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>
// 辅佐类
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)
        }
    }
}

作用如图:

示例Demo

在示例Demo中增加了相关的演示代码。

ExampleDemo github

ExampleDemo gitee