1/* 2 * Copyright (c) 2022 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import Input from '@ohos.multimodalInput.inputEventClient'; 17import { Log } from '@ohos/common'; 18import { windowManager } from '@ohos/common'; 19import { CommonConstants } from '@ohos/common'; 20 21const TAG = 'GestureNavigationExecutors'; 22 23export default class GestureNavigationExecutors { 24 private static readonly HOME_DISTANCE_LIMIT_MIN = 0.1; 25 private static readonly RECENT_DISTANCE_LIMIT_MIN = 0.15; 26 private static readonly NS_PER_MS = 1000000; 27 private timeOfFirstLeavingTheBackEventHotArea: number | null = null; 28 private screenWidth = 0; 29 private screenHeight = 0; 30 private curEventType: string | null = null; 31 private eventName: string | null = null; 32 private startEventPosition: {x: number, y: number} | null = null; 33 private preEventPosition: {x: number, y: number} | null = null; 34 private preEventTime: number | null = null; 35 private preSpeed = 0; 36 private startTime = 0; 37 38 private constructor() { 39 } 40 41 /** 42 * Set screenWidth. 43 */ 44 setScreenWidth(screenWidth: number) { 45 this.screenWidth = screenWidth; 46 } 47 48 /** 49 * Set screenHeight. 50 */ 51 setScreenHeight(screenHeight: number) { 52 this.screenHeight = screenHeight; 53 } 54 55 /** 56 * Get the GestureNavigationExecutors instance. 57 */ 58 static getInstance(): GestureNavigationExecutors { 59 if (globalThis.sGestureNavigationExecutors == null) { 60 globalThis.sGestureNavigationExecutors = new GestureNavigationExecutors(); 61 } 62 return globalThis.sGestureNavigationExecutors; 63 } 64 65 /** 66 * touchEvent Callback. 67 * @return true: Returns true if the gesture is within the specified hot zone. 68 */ 69 touchEventCallback(event: any): boolean { 70 Log.showDebug(TAG, 'touchEventCallback enter'); 71 if (event.touches.length != 1) { 72 return false; 73 } 74 const startXPosition = event.touches[0].globalX; 75 const startYPosition = event.touches[0].globalY; 76 if (event.type == 'down' && this.isSpecifiesRegion(startXPosition, startYPosition)) { 77 this.initializationParameters(); 78 this.startEventPosition = this.preEventPosition = { 79 x: startXPosition, 80 y: startYPosition 81 }; 82 this.startTime = this.preEventTime = event.timestamp; 83 this.curEventType = event.type; 84 if (vp2px(16) >= startXPosition || startXPosition >= (this.screenWidth - vp2px(16))) { 85 this.eventName = 'backEvent'; 86 return true; 87 } 88 } 89 if (this.startEventPosition && this.isSpecifiesRegion(this.startEventPosition.x, this.startEventPosition.y)) { 90 if (event.type == 'move') { 91 this.curEventType = event.type; 92 const curTime = event.timestamp; 93 const speedX = (startXPosition - this.preEventPosition.x) / ((curTime - this.preEventTime) / 1000); 94 const speedY = (startYPosition - this.preEventPosition.y) / ((curTime - this.preEventTime) / 1000); 95 const sqrt = Math.sqrt(speedX * speedX + speedY * speedY); 96 const curSpeed = startYPosition <= this.preEventPosition.y ? -sqrt : sqrt; 97 const acceleration = (curSpeed - this.preSpeed) / ((curTime - this.preEventTime) / 1000); 98 this.preEventPosition = { 99 x: startXPosition, 100 y: startYPosition 101 }; 102 this.preSpeed = curSpeed; 103 const isDistance = this.isRecentsViewShowOfDistanceLimit(startYPosition); 104 const isSpeed = this.isRecentsViewShowOfSpeedLimit(curTime, acceleration, curSpeed); 105 this.preEventTime = curTime; 106 if (isDistance && isSpeed && !this.eventName && curSpeed) { 107 this.eventName = 'recentEvent'; 108 this.recentEventCall(); 109 return true; 110 } 111 if (this.eventName == 'backEvent' && startXPosition > vp2px(16) && !this.timeOfFirstLeavingTheBackEventHotArea) { 112 this.timeOfFirstLeavingTheBackEventHotArea = (curTime - this.startTime) / 1000; 113 } 114 } 115 if (event.type == 'up') { 116 let distance = 0; 117 let slidingSpeed = 0; 118 if (this.curEventType == 'move') { 119 if (this.eventName == 'backEvent') { 120 distance = Math.abs((startXPosition - this.startEventPosition.x)); 121 if (distance >= vp2px(16) * 1.2 && this.timeOfFirstLeavingTheBackEventHotArea <= 120) { 122 this.backEventCall(); 123 this.initializationParameters(); 124 return true; 125 } 126 } else if (this.eventName == 'recentEvent') { 127 this.initializationParameters(); 128 return true; 129 } else { 130 distance = this.startEventPosition.y - startYPosition; 131 const isDistance = this.isHomeViewShowOfDistanceLimit(startYPosition); 132 Log.showDebug(TAG, `touchEventCallback isDistance: ${isDistance}`); 133 if (isDistance) { 134 slidingSpeed = distance / ((event.timestamp - this.startTime) / GestureNavigationExecutors.NS_PER_MS); 135 Log.showDebug(TAG, `touchEventCallback homeEvent slidingSpeed: ${slidingSpeed}`); 136 if (slidingSpeed >= vp2px(500)) { 137 this.homeEventCall(); 138 } 139 this.initializationParameters(); 140 return true; 141 } 142 } 143 } 144 this.initializationParameters(); 145 } 146 } 147 return false; 148 } 149 150 private initializationParameters() { 151 this.startEventPosition = null; 152 this.eventName = null; 153 this.preEventPosition = null; 154 this.timeOfFirstLeavingTheBackEventHotArea = null; 155 this.startTime = 0; 156 this.preSpeed = 0; 157 } 158 159 private backEventCall() { 160 Log.showInfo(TAG, 'backEventCall backEvent start'); 161 let keyEvent = { 162 isPressed: true, 163 keyCode: 2, 164 keyDownDuration: 1, 165 isIntercepted: false 166 }; 167 168 let res = Input.injectEvent({KeyEvent: keyEvent}); 169 Log.showDebug(TAG, `backEventCall result: ${res}`); 170 keyEvent = { 171 isPressed: false, 172 keyCode: 2, 173 keyDownDuration: 1, 174 isIntercepted: false 175 }; 176 177 setTimeout(() => { 178 res = Input.injectEvent({KeyEvent: keyEvent}); 179 Log.showDebug(TAG, `backEventCall result: ${res}`); 180 }, 20) 181 } 182 183 private homeEventCall() { 184 Log.showInfo(TAG, 'homeEventCall homeEvent start'); 185 globalThis.desktopContext.startAbility({ 186 bundleName: CommonConstants.LAUNCHER_BUNDLE, 187 abilityName: CommonConstants.LAUNCHER_ABILITY 188 }) 189 .then(() => { 190 Log.showDebug(TAG, 'homeEventCall startAbility Promise in service successful.'); 191 }) 192 .catch(() => { 193 Log.showDebug(TAG, 'homeEventCall startAbility Promise in service failed.'); 194 }); 195 } 196 197 private recentEventCall() { 198 Log.showInfo(TAG, 'recentEventCall recentEvent start'); 199 globalThis.createWindowWithName(windowManager.RECENT_WINDOW_NAME, windowManager.RECENT_RANK); 200 } 201 202 private isRecentsViewShowOfDistanceLimit(eventY: number) { 203 return (this.screenHeight - eventY) / this.screenHeight >= GestureNavigationExecutors.RECENT_DISTANCE_LIMIT_MIN; 204 } 205 206 private isHomeViewShowOfDistanceLimit(eventY: number) { 207 return (this.screenHeight - eventY) / this.screenHeight >= GestureNavigationExecutors.HOME_DISTANCE_LIMIT_MIN; 208 } 209 210 private isRecentsViewShowOfSpeedLimit(curTime: number, acceleration: number, curSpeed: number) { 211 return (acceleration > 0.05 && curSpeed > -2.0) || ((curSpeed > -1.0) && (curTime - this.preEventTime) / 1000 > 80.0); 212 } 213 214 private isSpecifiesRegion(startXPosition: number, startYPosition: number) { 215 const isStatusBarRegion = startYPosition <= this.screenHeight * 0.07; 216 const isSpecifiesXRegion = startXPosition <= vp2px(16) || startXPosition >= (this.screenWidth - vp2px(16)); 217 const isSpecifiesYRegion = (this.screenHeight - vp2px(22)) <= startYPosition && startYPosition <= this.screenHeight; 218 return (isSpecifiesXRegion && !isStatusBarRegion) || (isSpecifiesYRegion && !isSpecifiesXRegion); 219 } 220}