/*
* Copyright (c) Huawei Device Co., Ltd. 2024-2025. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LogDomain, LogHelper } from '@ohos/basicutils';
import {
EventConstants,
HiSysBackEventData,
HiSysEventUtil,
obtainLocalEvent,
sEventManager
} from '@ohos/frameworkwrapper';
import { ResUtils, SCBScenePanelManager, SCBSceneSessionManager } from '@ohos/windowscene';
import { SendTouchEventController, SendState } from '@ohos/windowscene/Index';
import { GestureFailReason, BackEventViewModelCallBack } from './BackEventViewModelCallBack';
import { BackGestureModel } from './BackGestureModel';
import { BackGestureModelCallBack } from './BackGestureModelCallBack';
import { StyleConstants } from '@ohos/launchercommon/src/main/ets/TsIndex';
import { GestureBackConstants } from '../../common/GestureBackConstants';
import { gestureBackSettings } from '../../common/GestureBackSettings';
const TAG = 'BackEventViewModel';
const log: LogHelper = LogHelper.getLogHelper(LogDomain.GESTURE, TAG);
export enum ActionType {
START,
UPDATE,
END,
CANCEL
}
export enum RecognizeState {
NONE,
SUCCESS,
FAIL
}
export class BackEventViewModel {
private persistentId: number = 0;
private zIndex: number = 0;
protected startTime: number = 0;
private startX: number = 0;
protected startY: number = 0;
protected isLeftArea: boolean = false;
// 是否在滑动过程中,防误触识别了
private hasCheckAccidentalTouch: boolean = false;
protected recognizeState: RecognizeState = RecognizeState.NONE;
private isPanGestureFinished: boolean = false;
protected sendTouchEventController: SendTouchEventController | null = null;
protected backGestureModel: BackGestureModel | null = null;
private viewModelCallBack: BackEventViewModelCallBack | null = null;
constructor(persistentId: number, zIndex: number, isLeftArea: boolean,
viewModelCallBack: BackEventViewModelCallBack | null) {
this.persistentId = persistentId;
this.zIndex = zIndex;
this.isLeftArea = isLeftArea;
this.sendTouchEventController = new SendTouchEventController(TAG, this.persistentId, this.zIndex);
this.viewModelCallBack = viewModelCallBack;
if (this.backGestureCallBack !== null) {
this.backGestureModel = new BackGestureModel(this.backGestureCallBack, viewModelCallBack);
}
}
protected backGestureCallBack: BackGestureModelCallBack = {
// back手势被互斥
onIgnoreBackGesture: (): void => {
this.recognizeState = RecognizeState.NONE;
this.sendTouchEventController?.setSendState(SendState.NONE);
},
// back手势反悔Cancel
onBackGestureCancelForUser: (backDirection: string): void => {
this.viewModelCallBack?.onBackGestureCancelForUser();
this.setGestureFailed(GestureFailReason.CANCEL_FROM_USER);
this.reportBackParam(HiSysBackEventData.CANCEL, backDirection);
this.recognizeState = RecognizeState.NONE;
this.sendTouchEventController?.setSendState(SendState.NONE);
},
// back手势系統Cancel
onBackGestureCancel: (): void => {
this.viewModelCallBack?.onBackGestureCancel();
this.setGestureFailed(GestureFailReason.CANCEL_FROM_SYSTEM);
this.recognizeState = RecognizeState.NONE;
this.sendTouchEventController?.setSendState(SendState.NONE);
},
// 发送back事件
onSendGestureBackTo: (event: GestureEvent): void => {
this.viewModelCallBack?.onSendGestureBackTo(this.isLeftArea, event);
this.recognizeState = RecognizeState.NONE;
this.sendTouchEventController?.setSendState(SendState.NONE);
sEventManager.publish(obtainLocalEvent(EventConstants.BACK_GESTURE, true));
}
};
/**
* back打点参数
*/
public reportBackParam(result: string, side?: string) {
let containerSession = SCBScenePanelManager.getInstance().getSceneContainerSessionList().getTopActiveSession();
HiSysEventUtil.reportBackEvent(result, side, containerSession?.isMidScene,
containerSession?.getBundleName() ?? 'desktop');
}
/**
* 无障碍事件透传
*
* @param event 无障碍事件
*/
public onAccessibilityHoverTransparent(event: TouchEvent) {
log.showInfo(`onAccessibilityHoverTransparent event:fingerId=${event?.changedTouches[0]?.id},type:${event?.type}`);
this.sendTouchEventController?.sendTouchEvent(event);
}
/**
* touch事件相关操作
* @param sendTouchEventController 事件发送控制器
* @param startY 事件发生点在纵向的位置
* @param event 事件对象
* @returns 事件发生点在纵向的位置
*/
public onTouchEvent(screenHeight: number, event?: TouchEvent) {
//接受touch事件
this.sendTouchEventController?.receiveTouchEvent(event);
if (event?.type === TouchType.Down) {
this.startY = this.getBackGestureStartY(event.changedTouches[0].y, screenHeight);
this.startX = event.changedTouches[0].x;
}
}
/**
* Click事件相关操作
* @param sendTouchEventController 事件发送控制器
* @param persistentId 组件ID
*/
public onClickEvent(): void {
let curSendTouchModel = this.sendTouchEventController?.getCurSendTouchModel();
if (curSendTouchModel) {
this.viewModelCallBack?.onSendClickEventTo(curSendTouchModel?.getDownEvent(), curSendTouchModel?.getUpEvent())
this.sendTouchEventController?.clearSendTouchModelList();
}
}
/**
* 长按相关操作
*
* @param actionType 操作类型
* @param sendTouchEventController 事件发送控制器
*/
public onLongPressGestureAction(actionType: ActionType): void {
switch (actionType) {
case ActionType.START:
this.sendTouchEventController?.sendALLTouchModeEvent();
this.sendTouchEventController?.setSendState(SendState.SEND_EVENT);
break;
case ActionType.END:
case ActionType.CANCEL:
this.sendTouchEventController?.setSendState(SendState.NONE);
break;
}
}
/**
* 鼠标事件透传
*/
public onMouseTouch(): void {
this.sendTouchEventController?.sendALLTouchModeEvent();
this.sendTouchEventController?.setSendState(SendState.SEND_EVENT);
}
/**
* 滑动事件-Start
* @param event
*/
public onPanGestureActionStart(event: GestureEvent) {
let sysSceneSession = SCBSceneSessionManager.getInstance().getSystemSceneSessionWithId(this.persistentId);
sysSceneSession.setSkipDraw(false);
this.startTime = event.timestamp;
this.isPanGestureFinished = false;
this.hasCheckAccidentalTouch = false;
this.recognizeState = RecognizeState.NONE;
this.sendTouchEventController?.setSendState(SendState.NONE);
}
/**
* 滑动事件-Update
* @param event
*/
public onPanGestureActionUpdate(event: GestureEvent) {
if (this.isPanGestureFinished) {
log.showInfo('back gesture has been finished.');
return;
}
if (this.recognizeState === RecognizeState.FAIL) {
log.showError(`recognize back gesture fail.offsetX=${event.offsetX},isLeftArea=true`);
this.sendTouchEventController?.handleExceptionTouchModels();
return;
}
if (this.recognizeState === RecognizeState.SUCCESS) {
// back手势识别成为后,做back手势跟手防误触
// 已检测过大于120msBack手势跟手防误触, 后续不再识别
if (!this.hasCheckAccidentalTouch && this.checkAccidentalTouch(event)) {
log.showInfo(`Update antiAccidentalTouch, in ResponseRegion over ${gestureBackSettings.getBackTimeThreshold()}ms, back cancel.`);
this.onPanGestureActionCancel();
return;
}
this.backGestureModel?.backGestureUpdate(event, this.isLeftArea);
return;
}
this.backGestureRecognize(event);
}
/**
* 滑动事件-End
* @param event
*/
public onPanGestureActionEnd(event: GestureEvent) {
if (this.isPanGestureFinished) {
log.showInfo('back gesture has been finish.');
return;
}
this.isPanGestureFinished = true;
const backDirection = this.isLeftArea ? HiSysBackEventData.LEFT : HiSysBackEventData.RIGHT
switch (this.recognizeState) {
case RecognizeState.NONE:
log.showInfo('backGestureEnd ignore, not recognized');
this.sendTouchEventController?.sendALLTouchModeEvent();
this.reportBackParam(HiSysBackEventData.CANCEL, backDirection);
break;
case RecognizeState.FAIL:
log.showInfo('backGestureEnd ignore, not recognized');
this.recognizeState = RecognizeState.NONE;
this.sendTouchEventController?.setSendState(SendState.NONE);
this.reportBackParam(HiSysBackEventData.CANCEL, backDirection);
break;
case RecognizeState.SUCCESS:
this.backGestureModel?.backGestureEnd(event, this.startX, this.startTime, this.isLeftArea);
break;
default:
break;
}
}
/**
* 滑动事件-Cancel
* @param event
*/
public onPanGestureActionCancel() {
log.showInfo('onActionCancel.');
if (this.isPanGestureFinished) {
log.showInfo('back gesture has been finish.');
return;
}
this.isPanGestureFinished = true;
switch (this.recognizeState) {
case RecognizeState.NONE:
this.sendTouchEventController?.sendALLTouchModeEvent();
break;
case RecognizeState.FAIL:
this.recognizeState = RecognizeState.NONE;
this.sendTouchEventController?.setSendState(SendState.NONE);
break;
case RecognizeState.SUCCESS:
this.backGestureModel?.backGestureCancel();
break;
default:
break;
}
let containerSession = SCBScenePanelManager.getInstance().getSceneContainerSessionList().getTopActiveSession();
HiSysEventUtil.reportBackEvent(HiSysBackEventData.CANCEL, undefined, undefined,
containerSession?.getBundleName() ?? 'desktop');
}
/**
* back手势识别
* @param event
*/
protected backGestureRecognize(event: GestureEvent): void {
if ((event.timestamp - this.startTime) / GestureBackConstants.NS_TO_MS >
gestureBackSettings.getBackTimeThreshold()) {
// if haven't recognized, check:
// 1. not exceed 120ms from onActionStart
this.setGestureFailed(GestureFailReason.TIMEOUT);
return;
}
// 2. x distance from onActionStart > 15px
if (Math.abs(event.offsetX) <= px2vp(gestureBackSettings.getBackDistanceThreshold())) {
return;
}
// 3. angle in 150° hot zone
if (this.isValidAngle(event.offsetX, event.offsetY, GestureBackConstants.BACK_ANGLE_THRESHOLD)) {
log.showInfo('back gesture recognize success');
this.recognizeState = RecognizeState.SUCCESS;
this.sendTouchEventController?.setSendState(SendState.NOT_SEND_EVENT);
this.backGestureModel?.backGestureStart(event, this.isLeftArea, this.startY);
} else {
this.setGestureFailed(GestureFailReason.INVALID_ANGLE);
}
}
/*
* 检测是否是back手势跟手误触
*
* return true:属于误触, false:非误触
*/
private checkAccidentalTouch(event: GestureEvent): boolean {
if ((event.timestamp - this.startTime) / GestureBackConstants.NS_TO_MS >
gestureBackSettings.getBackTimeThreshold()) {
// 大于120ms, 开始检测是否是back手势跟手误触
this.hasCheckAccidentalTouch = true;
// 大于120ms, 手指在back热区, 则识别失败, back手势跟手误触
if (this.backGestureModel?.isInBackResponseRegion(event, this.isLeftArea, this.startX)) {
return true;
}
}
return false;
}
/**
* 判断手指滑动角度是否符合要求
* @param offsetX 手指横向移动距离
* @param offsetY 手指纵向移动距离
* @param backAngleThresold 手指滑动角度界限
* @returns 手指滑动角度是否符合要求
*/
protected isValidAngle(offsetX: number, offsetY: number, backAngleThresold: number): boolean {
if (offsetX === 0) {
return false;
}
const angle = Math.atan(offsetY / offsetX) / Math.PI * 180;
return Math.abs(angle) < backAngleThresold;
}
/**
* back手势识别失败
*
* @param reason 失败原因
*/
protected setGestureFailed(reason: GestureFailReason): void {
log.showError(`back gesture recognize failed, reason=${reason}`);
this.recognizeState = RecognizeState.FAIL;
this.viewModelCallBack?.onRecognizeFail(reason);
this.GestureFailInjectDown(reason);
}
/**
* 获取back手势起始的Y坐标
*/
private getBackGestureStartY(startY: number, screenHeight: number): number {
let backGestureStartY = startY;
let topY = px2vp(StyleConstants.GESTURE_NAVIGATION_BACK_CANVAS_WIDTH);
let bottomY = px2vp(screenHeight) - (StyleConstants.GESTURE_NAV_BOTTOM_HOTAREA_HEIGHT +
ResUtils.getNumber($r('app.float.status_bar_phone_height')) + px2vp(StyleConstants.GESTURE_NAVIGATION_BACK_CANVAS_WIDTH));
log.showInfo(`topY: ${px2vp(StyleConstants.GESTURE_NAVIGATION_BACK_CANVAS_WIDTH)}`);
if (backGestureStartY < topY) {
backGestureStartY = topY;
}
if (backGestureStartY > bottomY) {
backGestureStartY = bottomY;
}
return backGestureStartY;
}
public setSendStateNotSend() {
this.sendTouchEventController?.setSendState(SendState.NOT_SEND_EVENT);
}
public getGestureRecognizeState(): RecognizeState {
return this.recognizeState;
}
/**
* back手势识别失败, 事件向下层窗口注入
* @param reason 失败原因
* @param sendTouchEventController 事件发送控制器
*/
public GestureFailInjectDown(reason: GestureFailReason) {
if (reason !== (GestureFailReason.CANCEL_FROM_USER || GestureFailReason.CANCEL_FROM_SYSTEM)) {
this.sendTouchEventController?.setSendState(SendState.SEND_EVENT);
this.sendTouchEventController?.sendALLTouchModeEvent();
}
}
}