10、自定义认证对话框:使用Fragment自定义UI、设置认证标题与副标题、取消按钮处理、动画与过渡效果
说实话,系统自带的BiometricPrompt对话框,在大多数场景下够用了。但总有那么些时候——比如你的App走的是深色定制主题,或者产品经理非要让对话框上显示个品牌Logo——这时候你就得自己动手了。
我个人习惯的做法是:用Fragment来承载自定义的认证UI。为什么?因为Fragment的生命周期和Activity天然绑定,而且处理屏幕旋转、后台回收这些场景,比直接怼一个Dialog要稳得多。
10.1 为什么选择Fragment作为认证容器
你想想看,指纹认证过程中,用户可能锁屏、可能接电话、可能切到别的App再回来。如果用的是普通的Dialog,这些场景下很容易出现内存泄漏或者UI状态丢失。
Fragment就不一样了。它有自己的生命周期,配合setRetainInstance(true)(虽然现在更推荐用ViewModel),可以很好地保持认证状态。我在项目中遇到过一个问题:用户指纹认证到一半,突然来了个电话,接完电话回来Dialog消失了,但认证回调还在跑——结果就是crash。换成Fragment之后,这个问题再也没出现过。
10.2 基础实现:自定义认证Fragment
我们先搭一个最基础的架子。这个Fragment要能显示指纹图标、标题、副标题,还有一个取消按钮。
class BiometricAuthFragment : DialogFragment() {
private lateinit var binding: FragmentBiometricAuthBinding
private var authCallback: BiometricAuthCallback? = null
interface BiometricAuthCallback {
fun onAuthenticated()
fun onError(errorCode: Int, errString: CharSequence)
fun onCancelled()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentBiometricAuthBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupUI()
startBiometricAuth()
}
private fun setupUI() {
// 设置标题和副标题
binding.tvTitle.text = arguments?.getString(ARG_TITLE) ?: "指纹验证"
binding.tvSubtitle.text = arguments?.getString(ARG_SUBTITLE) ?: "请触摸指纹传感器"
// 取消按钮处理
binding.btnCancel.setOnClickListener {
authCallback?.onCancelled()
dismiss()
}
}
private fun startBiometricAuth() {
// 这里启动系统指纹认证,后续章节会详细讲
}
companion object {
private const val ARG_TITLE = "title"
private const val ARG_SUBTITLE = "subtitle"
fun newInstance(title: String, subtitle: String): BiometricAuthFragment {
return BiometricAuthFragment().apply {
arguments = Bundle().apply {
putString(ARG_TITLE, title)
putString(ARG_SUBTITLE, subtitle)
}
}
}
}
}
嗯,这里要注意一点:DialogFragment默认的样式是居中弹窗,但如果你想要底部弹出或者全屏效果,可以重写onCreate方法设置主题。
10.3 布局文件:UI细节决定成败
布局文件里,我习惯把指纹图标放在最上面,中间是标题和副标题,底部是取消按钮。这样符合用户从上到下的阅读习惯。
<!-- fragment_biometric_auth.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:background="@drawable/bg_auth_dialog">
<ImageView
android:id="@+id/iv_fingerprint"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:src="@drawable/ic_fingerprint"
android:contentDescription="指纹图标" />
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary" />
<TextView
android:id="@+id/tv_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:textSize="14sp"
android:textColor="@color/text_secondary" />
<Button
android:id="@+id/btn_cancel"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="24dp"
android:text="取消"
android:background="@drawable/btn_cancel_bg"
android:textColor="@color/white" />
</LinearLayout>
10.4 取消按钮的处理逻辑
取消按钮看似简单,但处理不好容易出问题。我曾经遇到过一个bug:用户点击取消后,Fragment被dismiss了,但底层的BiometricPrompt认证还在继续,结果几秒后回调了一个认证成功——这就尴尬了。
正确的做法是:点击取消时,不仅要dismiss Fragment,还要主动取消BiometricPrompt的认证。
private var biometricPrompt: BiometricPrompt? = null
private fun setupCancelButton() {
binding.btnCancel.setOnClickListener {
// 1. 先取消系统认证
biometricPrompt?.cancelAuthentication()
// 2. 再通知回调
authCallback?.onCancelled()
// 3. 最后关闭Fragment
dismiss()
}
}
override fun onDestroyView() {
super.onDestroyView()
// 防止内存泄漏:如果Fragment被销毁但认证还没结束
biometricPrompt?.cancelAuthentication()
}
onPause里取消认证!因为用户可能只是临时切换到其他App,回来后续认证。只有在用户明确点击取消,或者Fragment被销毁时,才应该取消认证。
10.5 动画与过渡效果
默认的DialogFragment切换动画比较生硬。我个人习惯加一点淡入淡出和缩放效果,让用户体验更顺滑。
首先,在onCreate里设置动画样式:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.Theme_BiometricAuthDialog)
}
然后在主题中定义动画:
<style name="Theme.BiometricAuthDialog" parent="Theme.MaterialComponents.Light.Dialog">
<item name="android:windowEnterAnimation">@anim/dialog_enter</item>
<item name="android:windowExitAnimation">@anim/dialog_exit</item>
</style>
动画文件res/anim/dialog_enter.xml:
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:fromAlpha="0"
android:toAlpha="1"
android:duration="300" />
<scale
android:fromXScale="0.8"
android:fromYScale="0.8"
android:toXScale="1"
android:toYScale="1"
android:pivotX="50%"
android:pivotY="50%"
android:duration="300" />
</set>
退出动画res/anim/dialog_exit.xml:
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:fromAlpha="1"
android:toAlpha="0"
android:duration="200" />
<scale
android:fromXScale="1"
android:fromYScale="1"
android:toXScale="0.8"
android:toYScale="0.8"
android:pivotX="50%"
android:pivotY="50%"
android:duration="200" />
</set>
如果你用的是AndroidX,还可以用Transition API实现更复杂的过渡效果。比如让指纹图标从点击位置飞入,或者让文字逐行显示——这些效果在金融类App里很常见。
10.6 知识体系总览
下面这张图,是我自己整理的自定义认证对话框的核心结构。你看一眼就能明白各个模块之间的关系:
10.7 避坑指南:我踩过的几个坑
做自定义认证对话框,有几个坑我不得不提一下:
- 背景点击问题: 默认DialogFragment点击外部区域会dismiss。如果你不想让用户点外面就关掉,记得设置
dialog?.setCanceledOnTouchOutside(false)。 - 屏幕旋转: 旋转屏幕时Fragment会重建。我建议用
savedInstanceState保存认证状态,或者干脆锁定竖屏。 - 多实例问题: 曾经有个同事连续点了两次"指纹验证",弹出了两个Fragment。解决办法是用一个全局标记,或者用
isAdded判断。 - 动画卡顿: 如果动画在低端机上卡顿,试试把
alpha和scale分开执行,或者缩短动画时长到200ms以内。
好了,自定义认证对话框的核心内容就这些。说白了,就是把系统提供的认证能力,用你自己的UI包装一下。关键是要处理好生命周期和回调的对应关系,别让用户点了取消之后,后台还在偷偷认证——那可就闹笑话了。