写在前面
成品展示
相关依赖
- 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,
),
);
}