前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

默认的模糊遮罩虽然能保护内容,但看起来很"素"。对于正式的商业应用,锁屏界面应该体现品牌调性——Logo、品牌色、专业的解锁交互。secure_application 的 lockedBuilder 回调给了我们完全的自由度,可以在模糊遮罩上方放置任何 Widget。

这篇分享几种实用的锁屏界面设计方案。

一、lockedBuilder 回调的灵活运用

1.1 回调签名

final Widget Function(
  BuildContext context,
  SecureApplicationController? secureApplicationController,
)? lockedBuilder;

1.2 基本结构

SecureGate(
  blurr: 40,
  opacity: 0.8,
  lockedBuilder: (context, controller) {
    return YourCustomLockScreen(controller: controller);
  },
  child: YourProtectedContent(),
)

1.3 lockedBuilder 的渲染层级

┌─────────────────────────────────┐
│  lockedBuilder(你的自定义界面)   │  ← 最上层,可交互
├─────────────────────────────────┤
│  BackdropFilter(模糊遮罩)       │  ← 中间层,视觉遮挡
├─────────────────────────────────┤
│  child(受保护的内容)             │  ← 最下层,被遮挡
└─────────────────────────────────┘

lockedBuilder 返回的 Widget 在模糊遮罩上方,可以正常接收触摸事件。

二、自定义 PIN 码输入界面

2.1 完整实现

lockedBuilder: (context, controller) => Container(
  color: Colors.transparent,
  child: Center(
    child: Card(
      elevation: 8,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      child: Padding(
        padding: EdgeInsets.all(32),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(Icons.lock_outline, size: 48, color: Colors.blue),
            SizedBox(height: 16),
            Text('请输入 PIN 码', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
            SizedBox(height: 24),
            PinCodeField(
              length: 4,
              onComplete: (pin) {
                if (pin == '1234') {
                  controller?.authSuccess(unlock: true);
                } else {
                  controller?.authFailed();
                  // 显示错误提示
                }
              },
            ),
            SizedBox(height: 16),
            TextButton(
              onPressed: () => controller?.authLogout(),
              child: Text('退出登录'),
            ),
          ],
        ),
      ),
    ),
  ),
)

2.2 PIN 码输入组件

class PinCodeField extends StatefulWidget {
  final int length;
  final Function(String) onComplete;

  const PinCodeField({required this.length, required this.onComplete});

  
  _PinCodeFieldState createState() => _PinCodeFieldState();
}

class _PinCodeFieldState extends State<PinCodeField> {
  String _pin = '';

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: List.generate(widget.length, (i) => Container(
            margin: EdgeInsets.symmetric(horizontal: 8),
            width: 16, height: 16,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: i < _pin.length ? Colors.blue : Colors.grey.shade300,
            ),
          )),
        ),
        SizedBox(height: 24),
        _buildNumpad(),
      ],
    );
  }

  Widget _buildNumpad() {
    return Wrap(
      spacing: 16, runSpacing: 16,
      children: [
        for (int i = 1; i <= 9; i++) _numButton('$i'),
        _numButton('', enabled: false),
        _numButton('0'),
        _deleteButton(),
      ],
    );
  }

  Widget _numButton(String num, {bool enabled = true}) {
    return SizedBox(
      width: 64, height: 64,
      child: ElevatedButton(
        onPressed: enabled ? () {
          setState(() {
            _pin += num;
            if (_pin.length == widget.length) {
              widget.onComplete(_pin);
              _pin = '';
            }
          });
        } : null,
        style: ElevatedButton.styleFrom(shape: CircleBorder()),
        child: Text(num, style: TextStyle(fontSize: 24)),
      ),
    );
  }

  Widget _deleteButton() {
    return SizedBox(
      width: 64, height: 64,
      child: ElevatedButton(
        onPressed: () {
          if (_pin.isNotEmpty) setState(() => _pin = _pin.substring(0, _pin.length - 1));
        },
        style: ElevatedButton.styleFrom(shape: CircleBorder()),
        child: Icon(Icons.backspace_outlined),
      ),
    );
  }
}

三、生物识别解锁按钮

3.1 指纹/面容解锁

lockedBuilder: (context, controller) => Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Container(
        width: 80, height: 80,
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          color: Colors.white.withOpacity(0.9),
        ),
        child: IconButton(
          iconSize: 48,
          icon: Icon(Icons.fingerprint, color: Colors.blue),
          onPressed: () async {
            try {
              final localAuth = LocalAuthentication();
              final didAuth = await localAuth.authenticate(
                localizedReason: '请验证身份',
                options: AuthenticationOptions(biometricOnly: true),
              );
              if (didAuth) {
                controller?.authSuccess(unlock: true);
              } else {
                controller?.authFailed();
              }
            } catch (e) {
              controller?.authFailed();
            }
          },
        ),
      ),
      SizedBox(height: 16),
      Text(
        '点击指纹解锁',
        style: TextStyle(color: Colors.white, fontSize: 16),
      ),
      SizedBox(height: 32),
      TextButton(
        onPressed: () => _showPinInput(context, controller),
        child: Text('使用 PIN 码', style: TextStyle(color: Colors.white70)),
      ),
    ],
  ),
)

3.2 OpenHarmony 上的生物识别

// 平台判断
Future<void> _authenticate(SecureApplicationController? controller) async {
  if (Platform.isAndroid || Platform.isIOS) {
    // 使用 local_auth
    final result = await LocalAuthentication().authenticate(
      localizedReason: '请验证身份',
    );
    if (result) controller?.authSuccess(unlock: true);
  } else if (Platform.isOhos) {
    // OpenHarmony 需要自定义实现
    // 通过 MethodChannel 调用 User Authentication Kit
    final result = await _ohosAuthChannel.invokeMethod('authenticate');
    if (result == true) controller?.authSuccess(unlock: true);
  } else {
    // 其他平台:直接解锁或使用 PIN
    controller?.authSuccess(unlock: true);
  }
}

📌 当前限制:local_auth 尚未适配 OpenHarmony。在 OpenHarmony 上使用生物识别需要自己桥接 User Authentication Kit。

四、品牌 Logo + 模糊背景

4.1 品牌化锁屏

lockedBuilder: (context, controller) => Container(
  decoration: BoxDecoration(
    gradient: LinearGradient(
      begin: Alignment.topCenter,
      end: Alignment.bottomCenter,
      colors: [
        Colors.blue.shade900.withOpacity(0.3),
        Colors.blue.shade700.withOpacity(0.5),
      ],
    ),
  ),
  child: SafeArea(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        // 品牌 Logo
        Image.asset('assets/logo_white.png', width: 120, height: 120),
        SizedBox(height: 24),
        // 品牌名称
        Text(
          'MyBank',
          style: TextStyle(
            color: Colors.white,
            fontSize: 28,
            fontWeight: FontWeight.bold,
            letterSpacing: 2,
          ),
        ),
        SizedBox(height: 8),
        Text(
          '安全守护您的财富',
          style: TextStyle(color: Colors.white70, fontSize: 14),
        ),
        SizedBox(height: 48),
        // 解锁按钮
        ElevatedButton.icon(
          icon: Icon(Icons.fingerprint),
          label: Text('验证身份'),
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.white,
            foregroundColor: Colors.blue.shade900,
            padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(30),
            ),
          ),
          onPressed: () => _authenticate(controller),
        ),
      ],
    ),
  ),
)

4.2 设计要点

要素 建议 原因
Logo 使用白色/浅色版本 模糊背景通常偏暗
文字 白色 + 适当阴影 确保在模糊背景上可读
按钮 高对比度 引导用户操作
渐变 半透明渐变叠加 增强品牌感
SafeArea 必须使用 避免被刘海/状态栏遮挡

五、深色模式适配

5.1 根据主题切换

lockedBuilder: (context, controller) {
  final isDark = Theme.of(context).brightness == Brightness.dark;

  return Container(
    decoration: BoxDecoration(
      gradient: LinearGradient(
        colors: isDark
            ? [Colors.black54, Colors.black87]
            : [Colors.white54, Colors.white70],
      ),
    ),
    child: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.lock_outline,
            size: 64,
            color: isDark ? Colors.white : Colors.black87,
          ),
          SizedBox(height: 16),
          Text(
            '应用已锁定',
            style: TextStyle(
              fontSize: 24,
              color: isDark ? Colors.white : Colors.black87,
            ),
          ),
          SizedBox(height: 32),
          ElevatedButton(
            onPressed: () => controller?.authSuccess(unlock: true),
            child: Text('解锁'),
          ),
        ],
      ),
    ),
  );
}

5.2 blurr 和 opacity 的主题适配

SecureGate(
  blurr: isDark ? 30 : 20,
  opacity: isDark ? 0.7 : 0.6,
  lockedBuilder: ...,
  child: ...,
)
参数 浅色模式 深色模式 原因
blurr 20 30 深色内容需要更多模糊才能遮挡
opacity 0.6 0.7 深色模式下需要更高透明度

六、动画增强

6.1 解锁成功动画

class AnimatedLockScreen extends StatefulWidget {
  final SecureApplicationController? controller;
  const AnimatedLockScreen({this.controller});

  
  _AnimatedLockScreenState createState() => _AnimatedLockScreenState();
}

class _AnimatedLockScreenState extends State<AnimatedLockScreen>
    with SingleTickerProviderStateMixin {
  late AnimationController _animController;
  late Animation<double> _scaleAnimation;

  
  void initState() {
    super.initState();
    _animController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 300),
    );
    _scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
      CurvedAnimation(parent: _animController, curve: Curves.easeOut),
    );
    _animController.forward();
  }

  
  void dispose() {
    _animController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return ScaleTransition(
      scale: _scaleAnimation,
      child: FadeTransition(
        opacity: _animController,
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.lock, size: 64, color: Colors.white),
              SizedBox(height: 16),
              Text('应用已锁定', style: TextStyle(color: Colors.white, fontSize: 24)),
              SizedBox(height: 32),
              ElevatedButton(
                onPressed: () async {
                  await _animController.reverse();
                  widget.controller?.authSuccess(unlock: true);
                },
                child: Text('解锁'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

6.2 使用方式

lockedBuilder: (context, controller) => AnimatedLockScreen(controller: controller),

七、不同场景的锁屏设计

7.1 场景与设计对照

场景 设计风格 解锁方式 blurr opacity
银行App 严肃专业 生物识别 + PIN 60 0.9
社交App 轻松活泼 滑动解锁 20 0.6
企业App 简洁商务 PIN 码 40 0.8
医疗App 安全可信 生物识别 80 0.9
笔记App 简约文艺 图案解锁 20 0.5

7.2 无认证场景

// 只需要遮挡,不需要认证
SecureGate(
  blurr: 20,
  opacity: 0.6,
  // 不提供 lockedBuilder
  // 遮罩会在用户切回时自动消失(通过 onNeedUnlock 自动解锁)
  child: MyContent(),
)

配合自动解锁:

SecureApplication(
  onNeedUnlock: (controller) async {
    controller?.authSuccess(unlock: true);
    return null;
  },
  child: ...,
)

总结

本文展示了多种自定义锁屏界面的设计方案:

  1. PIN 码输入:数字键盘 + 圆点指示器
  2. 生物识别:指纹/面容按钮 + 备用 PIN 码
  3. 品牌化设计:Logo + 渐变背景 + 品牌色
  4. 深色模式:根据主题动态调整颜色和参数
  5. 动画增强:缩放 + 淡入淡出的进入动画

下一篇我们讲敏感数据清除与安全增强——认证失败后如何保护用户数据。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐