中国优质的IT技术网站
专业IT技术创作平台
IT职业在线教育平台
微信扫码分享
// CustomPatternLockByCanvas.ets export interface Point { x: number; y: number; } export interface Position { x: number; y: number; width: number; height: number; centerX: number; centerY: number; radius: number; circleRadius: number; } @ObservedV2 export class PatternLockItemViewModel { @Trace id: number; @Trace selected: boolean; @Trace position: Position; constructor(id: number, selected: boolean, position: Position) { this.id = id; this.selected = selected; this.position = position; } } @ObservedV2 export class PatternLockItemArrayViewModel extends Array<PatternLockItemViewModel> { } export enum DotStatus { NORMAL, SELECTED, ERROR } export enum DrawStep { FIRST_DRAW, SECOND_DRAW } export enum DrawState { DEFAULT, START, MOVING, END } function isPointInPosition(point: Point, position: Position): boolean { let d = Math.pow(point.x - position.centerX, 2) + Math.pow(point.y - position.centerY, 2) return d <= Math.pow(position.circleRadius, 2); } const UNSELECTED_DOT_COLOR = '#aacdf3'; const SELECTED_DOT_COLOR = '#1379fe'; const SELECTED_DOT_CIRCLE_COLOR = '#a5b4c3'; const PATH_LINE_COLOR = '#e3efff' const PATH_LINE_WIDTH = 5 @ComponentV2 export struct CustomPatternLockByCanvas { dotItems: PatternLockItemArrayViewModel = []; private contextSetting: RenderingContextSettings = new RenderingContextSettings(true); private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.contextSetting); @Local canvasArea: Area | undefined = undefined; drawState: DrawState = DrawState.DEFAULT; drawStep: DrawStep = DrawStep.FIRST_DRAW; // 存储路径 curPaths: PatternLockItemArrayViewModel = []; firstConfirmPath: PatternLockItemArrayViewModel = []; secondConfirmPath: PatternLockItemArrayViewModel = []; minDot: number = 5; @Local tipMessage: string = ''; onFirstDrawFinish: () => void = () => { }; onSecondDrawFinish: () => void = () => { }; @Monitor('canvasArea') onCanvasAreaChange(monitor: IMonitor) { monitor.dirty.forEach((path: string) => { let areaChange = monitor.value<Area>(path); let canvasWith = this.length2Number(areaChange?.now.width); let canvasHeight = this.length2Number(areaChange?.now.height); if (!areaChange || !areaChange.now || !canvasWith || !canvasHeight) { return; } console.log(`tag ${path} onCanvasAreaChange from ${areaChange.before} to ${areaChange.now}`); let width = canvasWith / 3; let height = canvasHeight / 3; for (let index = 0; index < 9; index++) { let x = index % 3 * width; let y = Math.floor(index / 3) * height; console.log(`tag ${index} ${x} ${y} ${width} ${height}`); let dotPosition: Position = { x: x, y: y, width: width, height: height, centerX: x + width / 2, centerY: y + height / 2, radius: 0.06 * width, circleRadius: 0.3 * width }; this.dotItems.push(new PatternLockItemViewModel(index, false, dotPosition)); } this.drawDotItems(); }) } length2Number(len?: Length): number | undefined { if (typeof len === 'string') { return Number(len); } else if (typeof len === 'number') { return len; // todo only check Resource } else if (len && typeof len === 'object') { return getContext().resourceManager.getNumber(len.id); } else { return undefined; } } drawDot(dotItem: PatternLockItemViewModel) { let dotRadius = dotItem.position.radius; let dotFillColor = dotItem.selected ? SELECTED_DOT_COLOR : UNSELECTED_DOT_COLOR; this.context.beginPath(); this.context.arc(dotItem.position.centerX, dotItem.position.centerY, dotRadius, 0, 2 * Math.PI, false); this.context.fillStyle = dotFillColor; this.context.fill(); this.context.closePath(); if (dotItem.selected) { let dotCircleRadius = dotItem.position.circleRadius; this.context.beginPath(); this.context.arc(dotItem.position.centerX, dotItem.position.centerY, dotCircleRadius, 0, 2 * Math.PI, false); this.context.lineWidth = 1; this.context.strokeStyle = SELECTED_DOT_CIRCLE_COLOR; this.context.stroke(); this.context.closePath(); } } drawDotItems() { if (this.dotItems.length === 0) { return; } this.dotItems.forEach((item) => { this.drawDot(item); }) } drawPaths() { if (this.curPaths.length <= 1) { return; } let start = this.curPaths[0]; this.context.beginPath(); this.context.strokeStyle = PATH_LINE_COLOR; this.context.lineWidth = PATH_LINE_WIDTH; this.context.moveTo(start.position.centerX, start.position.centerY); this.curPaths.forEach((item, index) => { if (index === 0) { return; } this.context.lineTo(item.position.centerX, item.position.centerY); this.context.stroke(); }) this.context.closePath(); } getPatternLockItemByPointInCanvas(point: Point): PatternLockItemViewModel | undefined { for (let i = 0; i < this.dotItems.length; i++) { let dotItem = this.dotItems[i]; if (dotItem.selected) { continue; } let inPosition = this.isPositionInPatternLockItem(dotItem, point); if (inPosition) { return dotItem; } } return undefined; } isPositionInPatternLockItem(item: PatternLockItemViewModel, point: Point): boolean { return isPointInPosition(point, item.position); } clearCanvas() { this.context.clearRect(0, 0, this.context.width, this.context.height); } resetPatternLock() { this.clearCanvas(); this.dotItems.forEach((item, index) => { item.selected = false; }) this.curPaths = []; this.drawDotItems(); this.drawState = DrawState.DEFAULT; } isSameNotEmptyPath(path1: PatternLockItemArrayViewModel, path2: PatternLockItemArrayViewModel): boolean { if (path1.length !== path2.length || path1.length === 0) { return false; } let len = path1.length; for (let index = 0; index < len; index++) { if (path1[index] !== path2[index]) { return false; } } return true; } onGestureActionUpdateDrawPatternLock(event: GestureEvent) { let curX = event.fingerList[0].localX; let curY = event.fingerList[0].localY; let patternLockItem = this.getPatternLockItemByPointInCanvas({ x: curX, y: curY }); console.log(`tag onActionUpdate: curX: ${curX} curY: ${curY} item:${patternLockItem?.id}`); if (!patternLockItem) { return; } if (this.drawState === DrawState.DEFAULT) { this.drawState = DrawState.START; patternLockItem.selected = true; this.drawDot(patternLockItem); this.curPaths.push(patternLockItem); } else if (this.drawState === DrawState.START) { let existInPath = this.curPaths.some(item => item.id === patternLockItem?.id); if (!existInPath) { patternLockItem.selected = true; this.curPaths.push(patternLockItem); this.clearCanvas(); this.drawPaths(); this.drawDotItems(); } } } onGestureActionEndDrawPatternLock(event: GestureEvent) { if (this.curPaths.length < this.minDot) { this.tipMessage = `手势密码绘制点数需大于${this.minDot}个点`; // todo 先绘制红色轨迹和路径,等待200ms左右再恢复九宫格 this.resetPatternLock(); return; } if (this.drawStep === DrawStep.FIRST_DRAW) { this.tipMessage = '请再次绘制'; this.firstConfirmPath = this.curPaths; this.drawStep = DrawStep.SECOND_DRAW; this.resetPatternLock(); return; } if (this.drawStep === DrawStep.SECOND_DRAW) { // 比较两次 let isSame = this.isSameNotEmptyPath(this.firstConfirmPath, this.curPaths); if (isSame) { this.tipMessage = '完成绘制'; this.secondConfirmPath = this.curPaths; } else { this.tipMessage = '二次绘制不一致,请重新绘制'; this.resetPatternLock(); } } else { console.warn(`unexpected error, ${this.drawStep} ${this.drawState}`) } } build() { Column() { Text(this.tipMessage) .fontColor(Color.Red) .height(30) Canvas(this.context) .width('100%') .height('100%') .borderWidth(1) .onReady(() => { console.log('tag onReady') }) .onAreaChange((oldValue: Area, newValue: Area) => { console.log('tag onAreaChange' + JSON.stringify(newValue)) this.canvasArea = newValue; }) .gesture(PanGesture({ fingers: 1 }).onActionStart((event: GestureEvent) => { console.log('tag onActionStart:' + JSON.stringify(event)); }).onActionUpdate((event: GestureEvent) => { this.onGestureActionUpdateDrawPatternLock(event); }).onActionEnd((event: GestureEvent) => { this.onGestureActionEndDrawPatternLock(event); }).onActionCancel(() => { console.log('tag onActionCancel'); })) } } } // index.ets import { CustomPatternLockByCanvas } from '../component/CustomPatternLockByCanvas'; @Entry @Component struct Index { @State message: string = 'Hello World'; build() { Stack() { CustomPatternLockByCanvas() .width(300) .height(300) } .height('100%') .width('100%') } }