Rust Slint实现Android的图案密码(图形解锁)源码分享
一、效果展示



二、源码分享
1、工程结构

2、Caogo.toml
[package]
name = "chart"
version = "0.1.0"
edition = "2024"
[dependencies]
slint = "1.16.1"
[build-dependencies]
slint-build = "1.16.1"
3、main.rs
slint::include_modules!();
use slint::{ModelRc, VecModel, SharedString, Model};
use std::cell::RefCell;
use std::rc::Rc;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let main_window = MainWindow::new()?;
let pattern: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
let saved_pattern: Rc<RefCell<Option<Vec<i32>>>> = Rc::new(RefCell::new(None));
let window_weak = main_window.as_weak();
// clearPattern callback
main_window.on_clearPattern({
let window_weak = window_weak.clone();
let pattern = pattern.clone();
move || {
if let Some(window) = window_weak.upgrade() {
pattern.borrow_mut().clear();
window.set_patternDots(ModelRc::new(VecModel::from(pattern.borrow().clone())));
window.set_pathData(SharedString::new());
window.set_hasLines(false);
window.set_dotStates(ModelRc::new(VecModel::from(vec![
DotState::Idle, DotState::Idle, DotState::Idle,
DotState::Idle, DotState::Idle, DotState::Idle,
DotState::Idle, DotState::Idle, DotState::Idle,
])));
window.set_showSuccess(false);
window.set_statusText(SharedString::from(""));
eprintln!("DEBUG: Pattern cleared");
}
}
});
// patternDotMoved callback
main_window.on_patternDotMoved({
let window_weak = window_weak.clone();
let pattern = pattern.clone();
move |dot_index| {
eprintln!("DEBUG: Dot touched: {}", dot_index);
if let Some(window) = window_weak.upgrade() {
let mut pat = pattern.borrow_mut();
if pat.contains(&dot_index) {
eprintln!("DEBUG: Skipping already touched dot {}", dot_index);
return;
}
pat.push(dot_index);
eprintln!("DEBUG: Pattern now: {:?}", *pat);
drop(pat);
let path = build_svg_path(&pattern.borrow());
eprintln!("DEBUG: SVG path: {}", &path);
window.set_pathData(path.into());
window.set_hasLines(true);
let mut states = vec![DotState::Idle; 9];
for &dot in pattern.borrow().iter() {
states[dot as usize] = DotState::Active;
}
window.set_dotStates(ModelRc::new(VecModel::from(states)));
window.set_patternDots(ModelRc::new(VecModel::from(pattern.borrow().clone())));
window.set_statusText(format!("Pattern: {} dots", pattern.borrow().len()).into());
window.set_showSuccess(false);
}
}
});
// patternDrawEnd callback - handles BOTH save and verify
main_window.on_patternDrawEnd({
let window_weak = window_weak.clone();
let _pattern = pattern.clone();
let saved_pattern = saved_pattern.clone();
move |pat_from_slint| {
if let Some(window) = window_weak.upgrade() {
let mode = window.get_currentMode();
let drawn: Vec<i32> = pat_from_slint.iter().map(|x| x).collect();
let len = drawn.len();
eprintln!("DEBUG: Draw ended, mode={:?}, len={}", mode, len);
if len < 4 {
// Too short - show error
window.set_dotStates(ModelRc::new(VecModel::from(vec![
DotState::Error, DotState::Error, DotState::Error,
DotState::Error, DotState::Error, DotState::Error,
DotState::Error, DotState::Error, DotState::Error,
])));
window.set_statusText("太短!至少需要4个点".into());
window.set_clearTimerActive(true);
return;
}
match mode {
AppMode::Set => {
// Save the pattern
*saved_pattern.borrow_mut() = Some(drawn.clone());
window.set_showSuccess(true);
window.set_statusText("[OK] 图案已保存!2秒后自动清除".into());
window.set_autoClearTimerActive(true);
}
AppMode::Verify => {
// Compare with saved pattern
let saved = saved_pattern.borrow();
if let Some(ref saved_pat) = *saved {
let drawn: Vec<i32> = pat_from_slint.iter().map(|x| x).collect();
if drawn == *saved_pat {
let mut states = vec![DotState::Idle; 9];
for &dot in &drawn {
states[dot as usize] = DotState::Active;
}
window.set_dotStates(ModelRc::new(VecModel::from(states)));
window.set_showSuccess(true);
window.set_statusText("[OK] 解锁成功!2秒后自动清除".into());
window.set_autoClearTimerActive(true);
} else {
// Only show error on drawn dots
let mut states = vec![DotState::Idle; 9];
for &dot in &drawn {
states[dot as usize] = DotState::Error;
}
window.set_dotStates(ModelRc::new(VecModel::from(states)));
window.set_showSuccess(false);
window.set_statusText("[ERR] 图案错误!2秒后自动清除".into());
window.set_autoClearTimerActive(true);
}
} else {
window.set_statusText("请先在设置模式下保存一个图案".into());
}
}
}
}
}
});
// setMode callback
main_window.on_setMode({
let window_weak = window_weak.clone();
let saved_pattern = saved_pattern.clone();
move |mode| {
if let Some(window) = window_weak.upgrade() {
window.set_currentMode(mode);
match mode {
AppMode::Set => {
if saved_pattern.borrow().is_some() {
window.set_statusText("画一个新图案来更新密码".into());
} else {
window.set_statusText("画一个图案作为密码(至少4个点)".into());
}
}
AppMode::Verify => {
if saved_pattern.borrow().is_some() {
window.set_statusText("画出你保存的图案来解锁".into());
} else {
window.set_statusText("请先在设置模式下保存一个图案".into());
}
}
}
}
}
});
main_window.run()?;
Ok(())
}
/// Build SVG path string from a sequence of dot indices
fn build_svg_path(pattern: &[i32]) -> String {
if pattern.len() < 2 {
return String::new();
}
let mut path = String::new();
let mut first = true;
for i in 0..pattern.len() - 1 {
let from = pattern[i];
let to = pattern[i + 1];
let intermediate = needs_intermediate(from, to);
if let Some(mid) = intermediate {
let (x1, y1) = dot_center(from);
let (mx, my) = dot_center(mid);
let (x2, y2) = dot_center(to);
if first {
path.push_str(&format!("M {} {}", x1, y1));
first = false;
}
path.push_str(&format!(" L {} {} L {} {}", mx, my, x2, y2));
} else {
let (x1, y1) = dot_center(from);
let (x2, y2) = dot_center(to);
if first {
path.push_str(&format!("M {} {}", x1, y1));
first = false;
}
path.push_str(&format!(" L {} {}", x2, y2));
}
}
path
}
/// Check if a move from `from` to `to` skips over an intermediate dot
fn needs_intermediate(from: i32, to: i32) -> Option<i32> {
let row_from = from / 3;
let col_from = from % 3;
let row_to = to / 3;
let col_to = to % 3;
if (row_from - row_to).abs() == 2 && (col_from - col_to).abs() == 2 {
Some(4) // center dot
} else if row_from == row_to && (col_from - col_to).abs() == 2 {
Some(row_from * 3 + 1) // horizontal skip
} else if col_from == col_to && (row_from - row_to).abs() == 2 {
Some(3 + col_from) // vertical skip
} else {
None
}
}
/// Convert a dot index (0-8) to center coordinates within the grid
fn dot_center(index: i32) -> (f32, f32) {
let row: i32 = index / 3;
let col: i32 = index % 3;
let cell_size = 100.0; // 300/3
let x: f32 = col as f32 * cell_size + cell_size / 2.0 - 2.0;
let y: f32 = row as f32 * cell_size + cell_size / 2.0 - 2.0;
(x, y)
}
4、build.rs
fn main(){
slint_build::compile("ui/main.slint")
.unwrap()
}
5、main.slint
// Android Pattern Lock - Premium UI
import { VerticalBox, HorizontalBox } from "std-widgets.slint";
enum DotState { Idle, Active, Error }
enum AppMode { Set, Verify }
enum VerifyStatus { Accepted, Rejected, WaitConfirm }
export component MainWindow inherits Window {
preferred-width: 400px;
preferred-height: 620px;
background: #0a0a1a;
// Properties from Rust
in-out property<[int]> patternDots: [];
in property <string> pathData;
in property <bool> hasLines: false;
in-out property<[DotState]> dotStates: [
DotState.Idle, DotState.Idle, DotState.Idle,
DotState.Idle, DotState.Idle, DotState.Idle,
DotState.Idle, DotState.Idle, DotState.Idle
];
in property <string> statusText;
in property <bool> showSuccess;
in-out property <AppMode> currentMode: AppMode.Set;
// Callbacks to Rust
callback userPatternChanged([int]);
callback verifyPattern([int]) -> VerifyStatus;
callback clearPattern();
callback setMode(AppMode);
callback patternDotTouched(int);
callback patternDotMoved(int);
callback patternDrawStart();
callback patternDrawEnd([int]);
property <bool> isDrawing: false;
in-out property <bool> clearTimerActive: false;
in-out property <bool> autoClearTimerActive: false;
property <length> gridSize: 300px;
property <length> cellSize: gridSize / 3;
property <length> dotRadius: 14px;
property <length> contentPadding: 50px;
// Colors
property <color> accentColor: root.showSuccess ? #00e676 :
root.dotStates[0] == DotState.Error ? #ff5252 : #4fc3f7;
property <color> glowColor: root.showSuccess ? #00e67644 :
root.dotStates[0] == DotState.Error ? #ff525244 : #4fc3f744;
property <color> lineColor: root.showSuccess ? #00e676 :
root.dotStates[0] == DotState.Error ? #ff5252 : #4fc3f7;
// Title area
titleArea := Rectangle {
x: root.contentPadding;
width: root.gridSize;
height: 50px;
background: transparent;
Text {
text: root.currentMode == AppMode.Set ? "🔒 设置图案密码" :
root.currentMode == AppMode.Verify ? "🔓 绘制解锁图案" : "Pattern Lock";
font-size: 20px;
font-weight: FontWeight.bold;
horizontal-alignment: center;
vertical-alignment: center;
color: root.accentColor;
width: 100%;
}
}
// Status text
statusText := Text {
x: root.contentPadding;
y: 55px;
width: root.gridSize;
text: root.statusText;
horizontal-alignment: center;
font-size: 14px;
color: root.accentColor;
opacity: 0.8;
}
// Grid card with glass morphism effect
gridContainer := Rectangle {
x: root.contentPadding;
y: 90px;
width: root.gridSize;
height: root.gridSize;
background: #12122a;
border-radius: 20px;
border-width: 1px;
border-color: #2a2a4a;
// Subtle inner shadow effect
Rectangle {
x: 2px; y: 2px;
width: root.gridSize - 4px;
height: root.gridSize - 4px;
border-radius: 18px;
background: transparent;
border-width: 1px;
border-color: #1a1a3a;
}
// Glow lines - thicker and more visible
lines := Path {
visible: root.hasLines;
width: root.gridSize;
height: root.gridSize;
commands: root.pathData;
fill: transparent;
stroke: root.lineColor;
stroke-width: 6px;
fit: preserve;
viewbox-x: 0; viewbox-y: 0;
viewbox-width: 300; viewbox-height: 300;
}
// Glow lines (wider, semi-transparent) - disabled for debugging
// glowLines := Path {
// visible: root.hasLines;
// width: root.gridSize;
// height: root.gridSize;
// commands: root.pathData;
// fill: transparent;
// stroke: root.glowColor;
// stroke-width: 16px;
// fit: preserve;
// viewbox-x: 0; viewbox-y: 0;
// viewbox-width: 300; viewbox-height: 300;
// }
// Dot 0
dot0Glow := Rectangle {
x: root.cellSize / 2 - root.dotRadius * 2; y: root.cellSize / 2 - root.dotRadius * 2;
width: root.dotRadius * 4; height: root.dotRadius * 4;
border-radius: root.dotRadius * 2;
background: root.dotStates[0] == DotState.Active ? root.glowColor : root.dotStates[0] == DotState.Error ? root.glowColor : transparent;
opacity: (root.dotStates[0] == DotState.Active || root.dotStates[0] == DotState.Error) ? 0.7 : 1;
}
dot0 := Rectangle {
x: root.cellSize / 2 - root.dotRadius; y: root.cellSize / 2 - root.dotRadius;
width: root.dotRadius * 2; height: root.dotRadius * 2;
border-radius: root.dotRadius;
background: root.dotStates[0] == DotState.Active ? root.accentColor :
root.dotStates[0] == DotState.Error ? #ff5252 :
root.showSuccess ? #00e676 : #3a3a5a;
border-width: root.dotStates[0] == DotState.Idle ? 2px : 0px;
border-color: root.dotStates[0] == DotState.Idle ? #555577 : transparent;
}
// Dot 1
dot1Glow := Rectangle {
x: root.cellSize + root.cellSize / 2 - root.dotRadius * 2; y: root.cellSize / 2 - root.dotRadius * 2;
width: root.dotRadius * 4; height: root.dotRadius * 4;
border-radius: root.dotRadius * 2;
background: root.dotStates[1] == DotState.Active ? root.glowColor : root.dotStates[1] == DotState.Error ? root.glowColor : transparent;
opacity: (root.dotStates[1] == DotState.Active || root.dotStates[1] == DotState.Error) ? 0.7 : 1;
}
dot1 := Rectangle {
x: root.cellSize + root.cellSize / 2 - root.dotRadius; y: root.cellSize / 2 - root.dotRadius;
width: root.dotRadius * 2; height: root.dotRadius * 2;
border-radius: root.dotRadius;
background: root.dotStates[1] == DotState.Active ? root.accentColor :
root.dotStates[1] == DotState.Error ? #ff5252 :
root.showSuccess ? #00e676 : #3a3a5a;
border-width: root.dotStates[1] == DotState.Idle ? 2px : 0px;
border-color: root.dotStates[1] == DotState.Idle ? #555577 : transparent;
}
// Dot 2
dot2Glow := Rectangle {
x: root.cellSize * 2 + root.cellSize / 2 - root.dotRadius * 2; y: root.cellSize / 2 - root.dotRadius * 2;
width: root.dotRadius * 4; height: root.dotRadius * 4;
border-radius: root.dotRadius * 2;
background: root.dotStates[2] == DotState.Active ? root.glowColor : root.dotStates[2] == DotState.Error ? root.glowColor : transparent;
opacity: (root.dotStates[2] == DotState.Active || root.dotStates[2] == DotState.Error) ? 0.7 : 1;
}
dot2 := Rectangle {
x: root.cellSize * 2 + root.cellSize / 2 - root.dotRadius; y: root.cellSize / 2 - root.dotRadius;
width: root.dotRadius * 2; height: root.dotRadius * 2;
border-radius: root.dotRadius;
background: root.dotStates[2] == DotState.Active ? root.accentColor :
root.dotStates[2] == DotState.Error ? #ff5252 :
root.showSuccess ? #00e676 : #3a3a5a;
border-width: root.dotStates[2] == DotState.Idle ? 2px : 0px;
border-color: root.dotStates[2] == DotState.Idle ? #555577 : transparent;
}
// Dot 3
dot3Glow := Rectangle {
x: root.cellSize / 2 - root.dotRadius * 2; y: root.cellSize + root.cellSize / 2 - root.dotRadius * 2;
width: root.dotRadius * 4; height: root.dotRadius * 4;
border-radius: root.dotRadius * 2;
background: root.dotStates[3] == DotState.Active ? root.glowColor : root.dotStates[3] == DotState.Error ? root.glowColor : transparent;
opacity: (root.dotStates[3] == DotState.Active || root.dotStates[3] == DotState.Error) ? 0.7 : 1;
}
dot3 := Rectangle {
x: root.cellSize / 2 - root.dotRadius; y: root.cellSize + root.cellSize / 2 - root.dotRadius;
width: root.dotRadius * 2; height: root.dotRadius * 2;
border-radius: root.dotRadius;
background: root.dotStates[3] == DotState.Active ? root.accentColor :
root.dotStates[3] == DotState.Error ? #ff5252 :
root.showSuccess ? #00e676 : #3a3a5a;
border-width: root.dotStates[3] == DotState.Idle ? 2px : 0px;
border-color: root.dotStates[3] == DotState.Idle ? #555577 : transparent;
}
// Dot 4
dot4Glow := Rectangle {
x: root.cellSize + root.cellSize / 2 - root.dotRadius * 2; y: root.cellSize + root.cellSize / 2 - root.dotRadius * 2;
width: root.dotRadius * 4; height: root.dotRadius * 4;
border-radius: root.dotRadius * 2;
background: root.dotStates[4] == DotState.Active ? root.glowColor : root.dotStates[4] == DotState.Error ? root.glowColor : transparent;
opacity: (root.dotStates[4] == DotState.Active || root.dotStates[4] == DotState.Error) ? 0.7 : 1;
}
dot4 := Rectangle {
x: root.cellSize + root.cellSize / 2 - root.dotRadius; y: root.cellSize + root.cellSize / 2 - root.dotRadius;
width: root.dotRadius * 2; height: root.dotRadius * 2;
border-radius: root.dotRadius;
background: root.dotStates[4] == DotState.Active ? root.accentColor :
root.dotStates[4] == DotState.Error ? #ff5252 :
root.showSuccess ? #00e676 : #3a3a5a;
border-width: root.dotStates[4] == DotState.Idle ? 2px : 0px;
border-color: root.dotStates[4] == DotState.Idle ? #555577 : transparent;
}
// Dot 5
dot5Glow := Rectangle {
x: root.cellSize * 2 + root.cellSize / 2 - root.dotRadius * 2; y: root.cellSize + root.cellSize / 2 - root.dotRadius * 2;
width: root.dotRadius * 4; height: root.dotRadius * 4;
border-radius: root.dotRadius * 2;
background: root.dotStates[5] == DotState.Active ? root.glowColor : root.dotStates[5] == DotState.Error ? root.glowColor : transparent;
opacity: (root.dotStates[5] == DotState.Active || root.dotStates[5] == DotState.Error) ? 0.7 : 1;
}
dot5 := Rectangle {
x: root.cellSize * 2 + root.cellSize / 2 - root.dotRadius; y: root.cellSize + root.cellSize / 2 - root.dotRadius;
width: root.dotRadius * 2; height: root.dotRadius * 2;
border-radius: root.dotRadius;
background: root.dotStates[5] == DotState.Active ? root.accentColor :
root.dotStates[5] == DotState.Error ? #ff5252 :
root.showSuccess ? #00e676 : #3a3a5a;
border-width: root.dotStates[5] == DotState.Idle ? 2px : 0px;
border-color: root.dotStates[5] == DotState.Idle ? #555577 : transparent;
}
// Dot 6
dot6Glow := Rectangle {
x: root.cellSize / 2 - root.dotRadius * 2; y: root.cellSize * 2 + root.cellSize / 2 - root.dotRadius * 2;
width: root.dotRadius * 4; height: root.dotRadius * 4;
border-radius: root.dotRadius * 2;
background: root.dotStates[6] == DotState.Active ? root.glowColor : root.dotStates[6] == DotState.Error ? root.glowColor : transparent;
opacity: (root.dotStates[6] == DotState.Active || root.dotStates[6] == DotState.Error) ? 0.7 : 1;
}
dot6 := Rectangle {
x: root.cellSize / 2 - root.dotRadius; y: root.cellSize * 2 + root.cellSize / 2 - root.dotRadius;
width: root.dotRadius * 2; height: root.dotRadius * 2;
border-radius: root.dotRadius;
background: root.dotStates[6] == DotState.Active ? root.accentColor :
root.dotStates[6] == DotState.Error ? #ff5252 :
root.showSuccess ? #00e676 : #3a3a5a;
border-width: root.dotStates[6] == DotState.Idle ? 2px : 0px;
border-color: root.dotStates[6] == DotState.Idle ? #555577 : transparent;
}
// Dot 7
dot7Glow := Rectangle {
x: root.cellSize + root.cellSize / 2 - root.dotRadius * 2; y: root.cellSize * 2 + root.cellSize / 2 - root.dotRadius * 2;
width: root.dotRadius * 4; height: root.dotRadius * 4;
border-radius: root.dotRadius * 2;
background: root.dotStates[7] == DotState.Active ? root.glowColor : root.dotStates[7] == DotState.Error ? root.glowColor : transparent;
opacity: (root.dotStates[7] == DotState.Active || root.dotStates[7] == DotState.Error) ? 0.7 : 1;
}
dot7 := Rectangle {
x: root.cellSize + root.cellSize / 2 - root.dotRadius; y: root.cellSize * 2 + root.cellSize / 2 - root.dotRadius;
width: root.dotRadius * 2; height: root.dotRadius * 2;
border-radius: root.dotRadius;
background: root.dotStates[7] == DotState.Active ? root.accentColor :
root.dotStates[7] == DotState.Error ? #ff5252 :
root.showSuccess ? #00e676 : #3a3a5a;
border-width: root.dotStates[7] == DotState.Idle ? 2px : 0px;
border-color: root.dotStates[7] == DotState.Idle ? #555577 : transparent;
}
// Dot 8
dot8Glow := Rectangle {
x: root.cellSize * 2 + root.cellSize / 2 - root.dotRadius * 2; y: root.cellSize * 2 + root.cellSize / 2 - root.dotRadius * 2;
width: root.dotRadius * 4; height: root.dotRadius * 4;
border-radius: root.dotRadius * 2;
background: root.dotStates[8] == DotState.Active ? root.glowColor : root.dotStates[8] == DotState.Error ? root.glowColor : transparent;
opacity: (root.dotStates[8] == DotState.Active || root.dotStates[8] == DotState.Error) ? 0.7 : 1;
}
dot8 := Rectangle {
x: root.cellSize * 2 + root.cellSize / 2 - root.dotRadius; y: root.cellSize * 2 + root.cellSize / 2 - root.dotRadius;
width: root.dotRadius * 2; height: root.dotRadius * 2;
border-radius: root.dotRadius;
background: root.dotStates[8] == DotState.Active ? root.accentColor :
root.dotStates[8] == DotState.Error ? #ff5252 :
root.showSuccess ? #00e676 : #3a3a5a;
border-width: root.dotStates[8] == DotState.Idle ? 2px : 0px;
border-color: root.dotStates[8] == DotState.Idle ? #555577 : transparent;
}
// Touch handling (must be last, on top)
gridTouch := TouchArea {
x: 0; y: 0; width: 100%; height: 100%;
property <int> lastCell: -1;
pointer-event(event) => {
if event.kind == PointerEventKind.down {
root.isDrawing = true;
root.autoClearTimerActive = false;
root.clearPattern();
self.lastCell = -1;
let col: int = Math.floor(self.mouse-x / root.cellSize);
let row: int = Math.floor(self.mouse-y / root.cellSize);
let idx: int = row * 3 + col;
if idx >= 0 && idx < 9 {
self.lastCell = idx;
root.patternDotMoved(idx);
}
}
if event.kind == PointerEventKind.up {
root.patternDrawEnd(root.patternDots);
}
}
moved => {
if self.pressed {
let col: int = Math.floor(self.mouse-x / root.cellSize);
let row: int = Math.floor(self.mouse-y / root.cellSize);
let idx: int = row * 3 + col;
if idx >= 0 && idx < 9 && idx != self.lastCell {
self.lastCell = idx;
root.patternDotMoved(idx);
}
}
}
}
}
// Auto-clear timer: clears pattern 2 seconds after verify
autoClearTimer := Timer {
interval: 2000ms;
running: root.autoClearTimerActive;
triggered => { root.autoClearTimerActive = false; root.clearPattern(); }
}
// Button bar
Rectangle {
x: root.contentPadding;
y: root.gridSize + 90px + 40px;
width: root.gridSize;
height: 42px;
setBtn := TouchArea {
x: 0;
property <bool> isSelected: root.currentMode == AppMode.Set;
Rectangle {
x: 0;
border-radius: 10px;
background: setBtn.isSelected ? #2196f3 : #1a1a3a;
width: 92px; height: 42px;
border-width: 1px;
border-color: setBtn.isSelected ? #2196f3 : #2a2a4a;
setText := Text {
text: "设置"; color: setBtn.isSelected ? #fff : #8888aa;
horizontal-alignment: center; vertical-alignment: center;
width: 100%; font-size: 14px; font-weight: FontWeight.medium;
}
}
clicked => { root.setMode(AppMode.Set); }
}
verifyBtn := TouchArea {
x: 104px;
property <bool> isSelected: root.currentMode == AppMode.Verify;
Rectangle {
x: 0;
border-radius: 10px;
background: verifyBtn.isSelected ? #2196f3 : #1a1a3a;
width: 92px; height: 42px;
border-width: 1px;
border-color: verifyBtn.isSelected ? #2196f3 : #2a2a4a;
verifyText := Text {
text: "验证"; color: verifyBtn.isSelected ? #fff : #8888aa;
horizontal-alignment: center; vertical-alignment: center;
width: 100%; font-size: 14px; font-weight: FontWeight.medium;
}
}
clicked => { root.setMode(AppMode.Verify); }
}
clearBtn := TouchArea {
x: 208px;
Rectangle {
x: 0;
border-radius: 10px;
background: clearBtn.has-hover ? #ff5252 : #c62828;
width: 92px; height: 42px;
opacity: clearBtn.has-hover ? 1 : 0.9;
clearText := Text {
text: "清除"; color: #fff;
horizontal-alignment: center; vertical-alignment: center;
width: 100%; font-size: 14px; font-weight: FontWeight.medium;
}
}
clicked => { root.clearPattern(); }
}
}
}
三、Android的图案密码的优势
Android的图案密码(Pattern Lock)作为一种经典的屏幕解锁方式,自Android早期版本就存在,至今仍被许多用户喜爱和使用。相比传统的数字密码(PIN)和文本密码,图案密码具有以下几个显著优势:
1、记忆友好,输入便捷
图案密码最大的优势在于其直观的记忆方式。人类大脑对空间位置和路径的记忆能力通常强于对抽象数字序列的记忆。用户可以通过设置一个简单、有意义的图形(如字母形状、几何图形等)来轻松记住解锁图案。
输入体验更流畅:
- 只需在9个点之间滑动手指,无需精确点击小键盘
- 支持连续滑动,一气呵成
- 视觉反馈明确,操作路径清晰可见
2、安全性相对较高
虽然图案密码的排列组合数量(9! = 362,880)理论上少于6位数字密码(10^6 = 1,000,000),但在实际使用中,图案密码的安全性表现不俗:
防窥视优势:
- 输入过程留下的“油迹轨迹”相对难以被远处窥视者准确复制
- 没有明显的按键声音提示
- 轨迹在屏幕上停留时间短,难以被拍照记录
复杂度可调:
- 最少连接4个点,最多连接9个点
- 可以设置复杂的交叉、折返图案
- Android系统会检测并阻止过于简单的图案(如直线、L形等)
3、用户体验优秀
快速解锁:
- 熟练用户可在1-2秒内完成解锁
- 无需切换输入法或键盘布局
- 支持盲操作(熟悉后可不看屏幕解锁)
个性化表达:
- 用户可以选择有个人意义的图案
- 不同的图案风格反映用户个性
- 相比冷冰冰的数字,图案更有“温度感”
4、容错性较好
输入宽容度:
- 系统对绘制路径的精确度要求相对宽松
- 允许一定程度的绘制偏差
- 自动纠正接近正确的连接
恢复便利:
- 忘记图案时,可以通过Google账户或安全问题找回
- 提供“忘记图案”的备用解锁方式
5、 技术实现简洁
从开发角度看,图案密码的实现相对简单:
存储安全:
- 系统存储的是图案的哈希值,而非原始坐标
- 每次验证时比较哈希值,保护用户隐私
性能高效:
- 验证算法复杂度低,响应迅速
- 占用系统资源少
6、 适用场景广泛
多设备适配:
- 在不同尺寸的Android设备上体验一致
- 适应各种屏幕比例和分辨率
特殊场景优势:
- 单手操作友好,特别适合大屏手机
- 在光线不足环境下仍可凭手感操作
- 适合戴手套操作(电容屏手套模式)
7、 与生物识别互补
在现代Android设备中,图案密码常与指纹、面部识别等生物特征结合使用:
备用方案:
- 生物识别失败时的可靠备选
- 重启设备后的首次解锁方式
安全层级:
- 作为第二重身份验证因素
- 与生物识别形成多层次安全防护
8、 发展趋势与局限性
持续优化:
- Android系统不断改进图案密码的安全算法
- 加入机器学习检测异常解锁尝试
- 与智能解锁场景(可信地点、设备等)结合
需要注意的局限性:
- 屏幕上的油迹可能暴露常用图案
- 在极度寒冷环境下,手指不灵活可能影响绘制精度
- 对于视力障碍用户不如语音或辅助功能友好
9、总结
Android图案密码以其记忆友好、操作便捷、安全性适中的特点,在众多解锁方式中保持了独特的地位。它不仅是技术实现简洁高效的典范,更是充分考虑用户体验的设计成果。在全面屏、折叠屏等新形态设备上,图案密码依然展现出良好的适应性和实用性。
对于大多数用户而言,图案密码在安全性和便利性之间取得了很好的平衡,是值得推荐的屏幕保护方式之一。

转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/qq_15181569/article/details/163699897




