1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.quickstep.inputconsumers; 18 19 import static android.view.MotionEvent.ACTION_CANCEL; 20 import static android.view.MotionEvent.ACTION_DOWN; 21 import static android.view.MotionEvent.ACTION_MOVE; 22 import static android.view.MotionEvent.ACTION_UP; 23 24 import static com.android.launcher3.Utilities.squaredHypot; 25 import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE; 26 27 import android.content.Context; 28 import android.graphics.Point; 29 import android.graphics.PointF; 30 import android.view.MotionEvent; 31 32 import com.android.launcher3.R; 33 import com.android.launcher3.testing.shared.ResourceUtils; 34 import com.android.launcher3.util.DisplayController; 35 import com.android.quickstep.InputConsumer; 36 import com.android.quickstep.RecentsAnimationDeviceState; 37 import com.android.quickstep.SystemUiProxy; 38 import com.android.systemui.shared.system.InputMonitorCompat; 39 40 /** 41 * Touch consumer for handling gesture event to launch one handed 42 * One handed gestural in quickstep only active on NO_BUTTON, TWO_BUTTONS, and portrait mode 43 */ 44 public class OneHandedModeInputConsumer extends DelegateInputConsumer { 45 46 private static final int ANGLE_MAX = 150; 47 private static final int ANGLE_MIN = 30; 48 49 private final Context mContext; 50 private final Point mDisplaySize; 51 private final RecentsAnimationDeviceState mDeviceState; 52 53 private final float mDragDistThreshold; 54 private final float mSquaredSlop; 55 56 private final int mNavBarSize; 57 58 private final PointF mDownPos = new PointF(); 59 private final PointF mLastPos = new PointF(); 60 61 private boolean mPassedSlop; 62 private boolean mIsStopGesture; 63 OneHandedModeInputConsumer(Context context, RecentsAnimationDeviceState deviceState, InputConsumer delegate, InputMonitorCompat inputMonitor)64 public OneHandedModeInputConsumer(Context context, RecentsAnimationDeviceState deviceState, 65 InputConsumer delegate, InputMonitorCompat inputMonitor) { 66 super(delegate, inputMonitor); 67 mContext = context; 68 mDeviceState = deviceState; 69 mDragDistThreshold = context.getResources().getDimensionPixelSize( 70 R.dimen.gestures_onehanded_drag_threshold); 71 mSquaredSlop = mDeviceState.getSquaredTouchSlop(); 72 mDisplaySize = DisplayController.INSTANCE.get(mContext).getInfo().currentSize; 73 mNavBarSize = ResourceUtils.getNavbarSize(NAVBAR_BOTTOM_GESTURE_SIZE, 74 mContext.getResources()); 75 } 76 77 @Override getType()78 public int getType() { 79 return TYPE_ONE_HANDED | mDelegate.getType(); 80 } 81 82 @Override onMotionEvent(MotionEvent ev)83 public void onMotionEvent(MotionEvent ev) { 84 switch (ev.getActionMasked()) { 85 case ACTION_DOWN: { 86 mDownPos.set(ev.getX(), ev.getY()); 87 mLastPos.set(mDownPos); 88 break; 89 } 90 case ACTION_MOVE: { 91 if (mState == STATE_DELEGATE_ACTIVE) { 92 break; 93 } 94 if (!mDelegate.allowInterceptByParent()) { 95 mState = STATE_DELEGATE_ACTIVE; 96 break; 97 } 98 99 mLastPos.set(ev.getX(), ev.getY()); 100 if (!mPassedSlop) { 101 if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) 102 > mSquaredSlop) { 103 if ((!mDeviceState.isOneHandedModeActive() && isValidStartAngle( 104 mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y)) 105 || (mDeviceState.isOneHandedModeActive() && isValidExitAngle( 106 mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y))) { 107 // To avoid mis-trigger when motion not touch system gesture region. 108 mPassedSlop = isInSystemGestureRegion(mLastPos); 109 setActive(ev); 110 } else { 111 mState = STATE_DELEGATE_ACTIVE; 112 } 113 } 114 } else { 115 float distance = (float) Math.hypot(mLastPos.x - mDownPos.x, 116 mLastPos.y - mDownPos.y); 117 if (distance > mDragDistThreshold && mPassedSlop) { 118 mIsStopGesture = true; 119 } 120 } 121 break; 122 } 123 case ACTION_UP: { 124 if (mLastPos.y >= mDownPos.y && mPassedSlop) { 125 onStartGestureDetected(); 126 } else if (mIsStopGesture) { 127 onStopGestureDetected(); 128 } 129 clearState(); 130 break; 131 } 132 case ACTION_CANCEL: 133 clearState(); 134 break; 135 } 136 137 if (mState != STATE_ACTIVE) { 138 mDelegate.onMotionEvent(ev); 139 } 140 } 141 clearState()142 private void clearState() { 143 mPassedSlop = false; 144 mState = STATE_INACTIVE; 145 mIsStopGesture = false; 146 } 147 onStartGestureDetected()148 private void onStartGestureDetected() { 149 if (mDeviceState.isSwipeToNotificationEnabled()) { 150 SystemUiProxy.INSTANCE.get(mContext).expandNotificationPanel(); 151 } else if (!mDeviceState.isOneHandedModeActive()) { 152 SystemUiProxy.INSTANCE.get(mContext).startOneHandedMode(); 153 } 154 } 155 onStopGestureDetected()156 private void onStopGestureDetected() { 157 if (!mDeviceState.isOneHandedModeEnabled() || !mDeviceState.isOneHandedModeActive()) { 158 return; 159 } 160 161 SystemUiProxy.INSTANCE.get(mContext).stopOneHandedMode(); 162 } 163 isInSystemGestureRegion(PointF lastPos)164 private boolean isInSystemGestureRegion(PointF lastPos) { 165 final int navBarUpperBound = mDisplaySize.y - mNavBarSize; 166 return mDeviceState.isGesturalNavMode() && lastPos.y > navBarUpperBound; 167 } 168 isValidStartAngle(float deltaX, float deltaY)169 private boolean isValidStartAngle(float deltaX, float deltaY) { 170 final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX)); 171 return angle > -(ANGLE_MAX) && angle < -(ANGLE_MIN); 172 } 173 isValidExitAngle(float deltaX, float deltaY)174 private boolean isValidExitAngle(float deltaX, float deltaY) { 175 final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX)); 176 return angle > ANGLE_MIN && angle < ANGLE_MAX; 177 } 178 179 @Override getDelegatorName()180 protected String getDelegatorName() { 181 return "OneHandedModeInputConsumer"; 182 } 183 } 184