实现Google原生PIN码锁屏密码效果

Jetpack Compose 实现 Google 原生 PIN 锁屏解锁效果

在 Android 系统中,Google 原生的 PIN 锁屏解锁界面简洁又酷炫:

  • 输入 PIN 时,上方圆点实时变化
  • 数字键盘带点击波纹、触感反馈
  • 全局确认按钮触发缩放动画
  • 支持删除、清空操作,还原系统体验

这篇文章带大家用 Jetpack Compose 从零实现一个高度还原的 PIN 解锁界面。


🎯 效果展示

最终效果包含以下几个点:

✅ 数字键盘点击缩放、颜色过渡

✅ 输入 PIN → 上方圆点动态显示

✅ 支持删除、清空 PIN

✅ "确认键"触发全局缩放波纹动画

✅ 带触感反馈(Haptic Feedback)


🛠 整体思路

  1. 状态管理

    • 使用 mutableStateListOf 存储用户输入的密码。
    • 输入 PIN → 更新状态 → 上方圆点重新绘制。
  2. 圆点指示器

    • 使用 Row + Box 绘制实心圆点。
    • 数量与密码长度绑定。
  3. 数字键盘

    • 使用 LazyVerticalGrid 创建 3x4 的数字键盘。
    • 包含数字键、删除键、确认键。
  4. 按钮交互动画

    • 每个按键是一个 PressEffectButton
      • 点击时圆角 & 颜色渐变
      • 松手后恢复
      • 可被全局 SharedFlow 触发缩放动画(例如确认键触发所有按钮波纹)。
  5. 触感反馈

    • 使用 LocalHapticFeedback 实现系统级震动反馈。

📦 核心代码实现

PIN 输入页面

kotlin 复制代码
@Composable
fun UnlockScreen() {
    // 全局动画触发器
    val triggerFlow = remember { MutableSharedFlow<Unit>() }
    val scop = rememberCoroutineScope()
    val password = remember { mutableStateListOf<Char>() }
    val haptic = LocalHapticFeedback.current

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black)
            .statusBarsPadding()
            .navigationBarsPadding(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // 标题
        Text(
            "输入 PIN 码",
            color = Color.White,
            modifier = Modifier.padding(top = 40.dp),
            style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.W600)
        )

        Spacer(modifier = Modifier.weight(1f))

        // 上方圆点指示器
        SecretDots(passwordLength = password.size)

        Spacer(modifier = Modifier.weight(1f))

        // 数字键盘
        LazyVerticalGrid(
            columns = GridCells.Fixed(3),
            modifier = Modifier.fillMaxWidth(),
            contentPadding = PaddingValues(36.dp)
        ) {
            items(12) { index ->
                val normalColor = if (index == 9 || index == 11) Color.Cyan else Color.White.copy(alpha = 0.2f)
                val normalTextColor = if (index == 9 || index == 11) Color.Black else Color.White

                PressEffectButton(
                    normalColor = normalColor,
                    triggerFlow = triggerFlow,
                    modifier = Modifier
                        .fillMaxWidth()
                        .aspectRatio(1f)
                        .padding(12.dp),
                    itemIndex = index,
                    onClick = {
                        when (index) {
                            // 删除键
                            9 -> if (password.isNotEmpty()) password.removeLast()
                            // 确认键 → 全局触发动画 + 清空输入
                            11 -> {
                                scop.launch { triggerFlow.emit(Unit) }
                                password.clear()
                            }
                            // 数字键
                            else -> password.add(('0' + ((index + 1) % 10)))
                        }
                        haptic.performHapticFeedback(HapticFeedbackType.Confirm)
                    }
                ) {
                    when (index) {
                        9 -> Icon(Icons.Filled.Close, null, tint = Color.Black)
                        11 -> Icon(Icons.Filled.ArrowForward, null, tint = Color.Black)
                        else -> Text("${(index + 1) % 10}", color = normalTextColor, fontSize = 24.sp, fontWeight = FontWeight.W700)
                    }
                }
            }
        }

        // 底部按钮
        Button(
            modifier = Modifier.padding(bottom = 30.dp),
            onClick = { /* 紧急呼叫逻辑 */ },
            colors = ButtonDefaults.buttonColors(containerColor = Color(0XFFADD8E6))
        ) {
            Text("紧急呼叫", color = Color.Black)
        }
    }
}

圆点指示器

scss 复制代码
@Composable
fun SecretDots(passwordLength: Int, dotSize: Dp = 20.dp, dotSpacing: Dp = 10.dp) {
    Row(horizontalArrangement = Arrangement.spacedBy(dotSpacing)) {
        repeat(passwordLength) {
            Box(
                modifier = Modifier
                    .size(dotSize)
                    .background(Color.White, shape = CircleShape)
            )
        }
    }
}

带缩放/颜色过渡的按钮

scss 复制代码
@Composable
fun PressEffectButton(
    modifier: Modifier = Modifier,
    normalColor: Color = Color.White.copy(alpha = 0.2f),
    pressedColor: Color = Color.Green,
    triggerFlow: MutableSharedFlow<Unit>,
    itemIndex: Int,
    onClick: () -> Unit,
    animationDuration: Int = 250,
    content: @Composable () -> Unit,
) {
    val cornerAnim = remember { Animatable(50f) }
    val colorAnim = remember { Animatable(normalColor) }
    val scaleAnim = remember { Animatable(1f) }

    // 监听全局触发
    LaunchedEffect(triggerFlow) {
        triggerFlow.collectLatest {
            delay(itemIndex * 50L)
            scaleAnim.animateTo(0.8f, tween(100))
            scaleAnim.animateTo(1f, tween(100))
        }
    }

    Box(
        modifier = modifier
            .graphicsLayer {
                scaleX = scaleAnim.value
                scaleY = scaleAnim.value
            }
            .background(colorAnim.value, RoundedCornerShape(percent = cornerAnim.value.toInt()))
            .pointerInput(Unit) {
                detectTapGestures(
                    onPress = {
                        coroutineScope {
                            launch { cornerAnim.animateTo(16f, tween(animationDuration)) }
                            launch { colorAnim.animateTo(pressedColor, tween(animationDuration)) }
                        }
                        val released = tryAwaitRelease()
                        coroutineScope {
                            launch { cornerAnim.animateTo(50f, tween(animationDuration)) }
                            launch { colorAnim.animateTo(normalColor, tween(animationDuration)) }
                            if (released) onClick()
                        }
                    }
                )
            },
        contentAlignment = Alignment.Center
    ) {
        content()
    }
}

✨ 优化点

  1. 触感反馈

    使用 LocalHapticFeedback 提升交互质感。

  2. 全局动画

    确认键触发 SharedFlow,所有按钮按顺序缩放,营造"波纹扩散"的动效。

  3. 颜色/圆角过渡
    Animatable 控制颜色和圆角半径,点击时柔和过渡,避免生硬跳变。

  4. 扩展性

    • PIN 长度可控(4/6 位)
    • 支持错误输入时加上抖动动画
    • 键盘样式可主题化(深色/浅色模式切换)

📌 总结

通过 Jetpack Compose,我们可以用非常声明式的写法实现一个 还原度极高的 PIN 锁屏解锁界面

  • LazyVerticalGrid → 数字键盘
  • Animatable → 按键动画
  • SharedFlow → 全局缩放波纹效果
  • LocalHapticFeedback → 系统级触感反馈

Compose 的优势在于 UI 与状态高度绑定,让类似系统解锁这种交互复杂的界面也能写得清晰易扩展。

👉 如果你觉得这篇文章有帮助,欢迎点赞 + 收藏,我会继续分享更多 Compose 自定义控件实战 🙌

相关推荐
ZSQA7 小时前
mac安装Homebrew解决网络问题
前端
烽学长7 小时前
(附源码)基于Vue的教师档案管理系统的设计与实现
前端·javascript·vue.js
雨白7 小时前
自定义 ViewGroup:实现一个流式标签布局
android
前端一课7 小时前
前端监控 SDK,支持页面访问、性能监控、错误追踪、用户行为和网络请求监控
前端
lee5767 小时前
UniApp + SignalR + Asp.net Core 做一个聊天IM,含emoji 表情包
前端·vue.js·typescript·c#
✎﹏赤子·墨筱晗♪7 小时前
Shell函数进阶:返回值妙用与模块化开发实践
前端·chrome
没有了遇见7 小时前
免费替代高德 / 百度!Android 原生定位 + GeoNames 离线方案:精准经纬度与模糊位置工具包
android
再学一点就睡7 小时前
从 npm 到 pnpm:包管理器的进化与 pnpm 核心原理解析
前端·npm
Light607 小时前
领码方案:低代码平台前端缓存与 IndexedDB 智能组件深度实战
前端·低代码·缓存·indexeddb·离线优先·ai优化