10、人脸识别UI定制:自定义认证对话框、使用PromptBuilder、设置动画与主题

说实话,BiometricPrompt 自带的那个对话框,长得确实有点「原生」。圆角、图标、按钮样式,都是系统默认的。我刚开始做项目时,产品经理看了一眼就说:「这对话框跟我们 App 的风格完全不搭啊。」

嗯,这确实是个问题。但好消息是,Android 给了我们足够的定制空间。今天我们就来聊聊,怎么把人脸识别的 UI 做得跟 App 浑然一体。

10.1 系统默认对话框长什么样?

先看看默认的 BiometricPrompt 对话框。它分两种模式:

  • 系统弹窗模式:一个居中的圆角卡片,顶部是生物识别图标,中间是提示文字,底部是「取消」按钮
  • 全屏模式:用于 Android 10+,在折叠屏或平板上体验更好

默认的样式其实不丑,但问题是——它没法改。你不能直接改那个对话框的背景色、字体、按钮样式。那怎么办?

我个人的做法是:完全放弃系统对话框,自己写一个

10.2 使用 PromptBuilder 定制

BiometricPrompt.PromptBuilder 提供了几个关键方法,能让我们「半定制」对话框:

BiometricPrompt.PromptBuilder builder = new BiometricPrompt.PromptBuilder(context)
    .setTitle("验证身份")
    .setSubtitle("请面向手机,完成人脸识别")
    .setDescription("用于登录和支付操作")
    .setNegativeButton("取消", executor, (dialog, which) -> {
        // 用户点击取消
    })
    .setConfirmationRequired(false);  // 是否需要手动确认

这里有个坑,我踩过。setConfirmationRequired 这个参数,默认是 true。什么意思?就是人脸识别成功后,系统还会弹一个「确认」按钮,让用户手动点一下。你想想看,人脸识别都成功了,还要再点一下,这体验多割裂。

注意:setConfirmationRequired(false) 只在 Android 11+ 上生效。低版本上,即使你设了 false,系统也会忽略。

所以我的建议是:如果 minSdk 低于 30,就别依赖这个参数了,直接用自定义对话框

10.3 完全自定义认证对话框

这才是真正的「定制」。思路很简单:

  1. 自己写一个 Activity 或 DialogFragment
  2. 在里面调用 BiometricManager 进行认证
  3. 把认证结果通过回调传给调用方

来看代码:

public class CustomBiometricDialog extends DialogFragment {

    private BiometricPrompt biometricPrompt;
    private BiometricCallback callback;

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // 完全自定义布局
        View view = LayoutInflater.from(getContext())
            .inflate(R.layout.dialog_biometric, null);

        AlertDialog dialog = new AlertDialog.Builder(getContext())
            .setView(view)
            .create();

        // 设置圆角背景
        dialog.getWindow().setBackgroundDrawable(
            new ColorDrawable(Color.TRANSPARENT));

        initBiometric();
        return dialog;
    }

    private void initBiometric() {
        Executor executor = ContextCompat.getMainExecutor(getContext());
        biometricPrompt = new BiometricPrompt(this, executor,
            new BiometricPrompt.AuthenticationCallback() {
                @Override
                public void onAuthenticationSucceeded(
                        BiometricPrompt.AuthenticationResult result) {
                    // 成功:关闭对话框,回调结果
                    dismiss();
                    callback.onSuccess();
                }

                @Override
                public void onAuthenticationError(int errorCode,
                        CharSequence errString) {
                    // 错误:显示错误信息
                    showError(errString.toString());
                }

                @Override
                public void onAuthenticationFailed() {
                    // 失败:人脸不匹配
                    showError("人脸识别失败,请重试");
                }
            });

        // 启动认证
        BiometricPrompt.PromptInfo promptInfo = 
            new BiometricPrompt.PromptInfo.Builder()
                .setTitle("")  // 标题为空,因为我们自己显示
                .setSubtitle("")
                .setDescription("")
                .setNegativeButtonText("取消")
                .build();

        biometricPrompt.authenticate(promptInfo);
    }
}

布局文件 dialog_biometric.xml 就完全由你掌控了:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="280dp"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="@drawable/bg_dialog_rounded"
    android:padding="24dp">

    <ImageView
        android:id="@+id/iv_face_icon"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_gravity="center"
        android:src="@drawable/ic_face_scan" />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="验证身份"
        android:textSize="18sp"
        android:textStyle="bold"
        android:gravity="center"
        android:layout_marginTop="16dp" />

    <TextView
        android:id="@+id/tv_status"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="请面向手机"
        android:textSize="14sp"
        android:gravity="center"
        android:layout_marginTop="8dp" />

    <Button
        android:id="@+id/btn_cancel"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:text="取消"
        android:layout_marginTop="24dp"
        android:backgroundTint="@color/btn_cancel" />

</LinearLayout>
小技巧:我在项目中习惯把「人脸扫描动画」做成 Lottie 动画,放在 ImageView 的位置。用户看着动画,等待感会降低很多。

10.4 设置动画与主题

自定义对话框的动画,主要分两块:

  • 对话框进出动画:用 Dialog 的 window 设置
  • 人脸扫描动画:用 Lottie 或帧动画

对话框动画示例:

// 在 DialogFragment 的 onStart 中设置
@Override
public void onStart() {
    super.onStart();
    Dialog dialog = getDialog();
    if (dialog != null) {
        Window window = dialog.getWindow();
        if (window != null) {
            window.setWindowAnimations(R.style.DialogAnimation);
        }
    }
}

// styles.xml
<style name="DialogAnimation">
    <item name="android:windowEnterAnimation">@anim/slide_in_up</item>
    <item name="android:windowExitAnimation">@anim/slide_out_down</item>
</style>

至于主题,我建议在 Application 或 Activity 的主题中,统一设置对话框样式:

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <item name="android:alertDialogTheme">@style/CustomDialogTheme</item>
</style>

<style name="CustomDialogTheme" parent="ThemeOverlay.MaterialComponents.Dialog">
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowIsFloating">true</item>
    <item name="android:windowMinWidthMajor">85%</item>
    <item name="android:windowMinWidthMinor">90%</item>
</style>

10.5 核心逻辑流程图

下面这张图,是我自己整理的自定义认证流程,你看一眼就明白了:

自定义人脸认证对话框流程 用户触发认证 创建自定义 DialogFragment 加载自定义布局 + Lottie动画 初始化 BiometricPrompt 设置 AuthenticationCallback 认证结果回调 成功 / 失败 / 错误 ✅ 成功 关闭对话框 ❌ 失败/错误 显示错误提示 自定义对话框 + BiometricPrompt 组合使用,兼顾 UI 灵活性与系统安全性

10.6 避坑指南

做自定义对话框时,有几个坑我替你们踩过了:

坑1:DialogFragment 与 BiometricPrompt 的生命周期
我曾经在 DialogFragment 的 onCreateView 里初始化 BiometricPrompt,结果发现对话框还没显示,认证就开始了。正确的做法是在 onStart 或 onResume 里初始化。
坑2:横竖屏切换
自定义对话框在横竖屏切换时,默认会重建。如果你不处理,认证状态就丢了。我习惯在 onSaveInstanceState 里保存一个标志位,重建后重新初始化。
我的经验:如果你只是想要「换个颜色、改个圆角」,其实没必要完全自定义。用 PromptBuilder 的 setTitle/setSubtitle 就够了。只有当你需要完全控制布局时,才走自定义路线。

嗯,UI 定制这块就聊这么多。说白了,核心思路就是:用系统能力做认证,用自己的 UI 做展示。两者结合,既安全又好看。


本节要点回顾:

  • PromptBuilder 支持半定制:标题、副标题、描述、取消按钮
  • 完全自定义:用 DialogFragment + BiometricPrompt 组合
  • 动画用 Lottie 或帧动画,主题用 AlertDialog 主题覆盖
  • 注意生命周期和横竖屏切换的坑

公众号:蓝海资料掘金营,微信deep3321