写在前面

成品展示

应用锁

相关依赖

  • get
  • local_auth
  • shared_preferences

组件代码

这里使用 getx 作为状态管理,shared_preferences 持久化数据,local_auth 库用于调用系统的生物识别功能,比如 Android 平台的指纹识别,IOS 平台的 Face ID 等。如果没有需求可以不依赖。

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mood_diary/utils/utils.dart';

import 'lock_logic.dart';

class LockPage extends StatelessWidget {
  const LockPage({super.key});

  @override
  Widget build(BuildContext context) {
    final logic = Bind.find<LockLogic>();
    final state = Bind.find<LockLogic>().state;
    final colorScheme = Theme.of(context).colorScheme;
    final textStyle = Theme.of(context).textTheme;

    final buttonSize = (textStyle.displayLarge!.fontSize! * textStyle.displayLarge!.height!);
    Widget buildNumButton(String num) {
      return Ink(
        decoration: BoxDecoration(color: colorScheme.surfaceContainerHighest, shape: BoxShape.circle),
        child: InkWell(
          borderRadius: BorderRadius.all(Radius.circular(buttonSize / 2)),
          onTap: () async {
            await logic.updatePassword(num);
          },
          child: Center(
              child: Text(
            num,
            style: textStyle.displaySmall,
          )),
        ),
      );
    }

    Widget buildDeleteButton() {
      return Ink(
        decoration: const BoxDecoration(shape: BoxShape.circle),
        child: InkWell(
          borderRadius: BorderRadius.all(Radius.circular(buttonSize / 2)),
          onTap: () {
            logic.deletePassword();
          },
          child: const Icon(
            Icons.backspace,
          ),
        ),
      );
    }

    Widget buildBiometricsButton() {
      return Ink(
        decoration: const BoxDecoration(shape: BoxShape.circle),
        child: InkWell(
          borderRadius: BorderRadius.all(Radius.circular(buttonSize / 2)),
          onTap: () async {
            if (await Utils().authUtil.check()) {
              logic.checked();
            }
          },
          child: const Icon(
            Icons.fingerprint,
          ),
        ),
      );
    }

    List<Widget> buildPasswordIndicator() {
      return List.generate(4, (index) {
        return Obx(() {
          return Icon(
            Icons.circle,
            size: 16,
            color: Color.lerp(
                state.password.value.length > index ? colorScheme.onSurface : colorScheme.surfaceContainerHighest,
                Colors.red,
                logic.animation.value),
          );
        });
      });
    }

    return GetBuilder<LockLogic>(
      init: logic,
      assignId: true,
      builder: (logic) {
        return PopScope(
          canPop: false,
          child: Scaffold(
            appBar: AppBar(
              automaticallyImplyLeading: false,
            ),
            body: Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  const Icon(Icons.lock),
                  const SizedBox(
                    height: 16.0,
                  ),
                  Text(
                    '请输入密码',
                    style: textStyle.titleMedium,
                  ),
                  const SizedBox(
                    height: 16.0,
                  ),
                  AnimatedBuilder(
                    animation: logic.animation,
                    builder: (context, child) {
                      return Transform.translate(
                        offset: Offset(logic.interpolate(logic.animation.value), 0),
                        child: Wrap(
                          spacing: 16.0,
                          children: buildPasswordIndicator(),
                        ),
                      );
                    },
                  ),
                  const SizedBox(
                    height: 32.0,
                  ),
                  SizedBox(
                    width: buttonSize * 3 + 20,
                    height: buttonSize * 4 + 30,
                    child: GridView.count(
                      crossAxisCount: 3,
                      childAspectRatio: 1.0,
                      crossAxisSpacing: 10,
                      mainAxisSpacing: 10,
                      children: [
                        buildNumButton('1'),
                        buildNumButton('2'),
                        buildNumButton('3'),
                        buildNumButton('4'),
                        buildNumButton('5'),
                        buildNumButton('6'),
                        buildNumButton('7'),
                        buildNumButton('8'),
                        buildNumButton('9'),
                        buildBiometricsButton(),
                        buildNumButton('0'),
                        buildDeleteButton()
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}

import 'package:get/get.dart';
import 'package:mood_diary/utils/utils.dart';

class LockState {
  late RxString password;

  late RxString realPassword;

  //锁定类型,是立即锁定导致,还是启动锁定导致
  late String? lockType;

  LockState() {
    password = ''.obs;
    realPassword = Utils().prefUtil.getValue<String>('password')!.obs;
    lockType = Get.arguments;

    ///Initialize variables
  }
}

import 'package:flutter/animation.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:mood_diary/router/app_routes.dart';

import 'lock_state.dart';

class LockLogic extends GetxController with GetSingleTickerProviderStateMixin {
  final LockState state = LockState();
  late AnimationController animationController =
      AnimationController(vsync: this, duration: const Duration(milliseconds: 200));
  late Animation<double> animation =
      Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: animationController, curve: Curves.easeInOut));

  @override
  void onReady() {
    // TODO: implement onReady
    super.onReady();
  }

  @override
  void onClose() {
    // TODO: implement onClose
    animationController.dispose();
    super.onClose();
  }

  double interpolate(double x) {
    var step = 10.0;
    if (x <= 0.25) {
      return 4 * step * x;
    } else if (x <= 0.75) {
      return step - 4 * step * (x - 0.25);
    } else {
      return -step + 4 * step * (x - 0.75);
    }
  }

  void deletePassword() {
    if (state.password.value.isNotEmpty) {
      state.password.value = state.password.value.substring(0, state.password.value.length - 1);
      HapticFeedback.selectionClick();
    }
  }

  Future<void> updatePassword(String value) async {
    if (state.password.value.length < 4) {
      state.password.value += value;
      HapticFeedback.selectionClick();
    }
    Future.delayed(const Duration(milliseconds: 100), () async {
      if (state.password.value.length == 4) {
        //密码正确
        if (state.password.value == state.realPassword.value) {
          checked();
        } else {
          animationController.forward();
          await HapticFeedback.mediumImpact();
          Future.delayed(const Duration(milliseconds: 200), () {
            animationController.reverse();
            state.password.value = '';
          });
        }
      }
    });
  }

  void checked() {
    //如果是启动时的锁定,就跳转到homepage
    if (state.lockType == null) {
      Get.offAllNamed(AppRoutes.homePage);
    }

    //如果是开启立即锁定后生命周期变化导致的锁定
    if (state.lockType == 'pause') {
      Get.backLegacy();
    }
  }
}

其他

如果要实现软件离开后自动锁定的功能,可以在一个不会被销毁的页面比如主页,引入 AppLifecycleListener 来监听生命周期变化,当生命周期变化时自动跳转到锁屏页面,这样就可以实现自动锁屏的功能,需要注意的是,由于使用过程中导致的锁屏需要保存路由状态,所以要防止用户通过返回的防止返回上一个路由,可以使用 PopScope 包裹锁屏页面,拦截返回操作。

如果引入 local_auth 包使用生物识别,可能需要修改弹窗内容,因为调用的是系统底层的弹窗而不是来自于 Flutter,如果不修改弹窗内容,可能会是英文的界面。

以安卓平台为例,修改弹窗内容。

//生物识别
  Future<bool> check() async {
    return await _authentication.authenticate(
      authMessages: [
        const AndroidAuthMessages(
          biometricHint: "",
          biometricNotRecognized: "验证失败",
          biometricSuccess: "验证成功",
          cancelButton: "取消",
          goToSettingsButton: "设置",
          goToSettingsDescription: "请先在系统中开启指纹",
          signInTitle: "扫描您的指纹以继续",
        )
      ],
      localizedReason: '安全验证',
      options: const AuthenticationOptions(
        useErrorDialogs: true,
        stickyAuth: true,
        sensitiveTransaction: true,
        biometricOnly: true,
      ),
    );
  }