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.ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE; 25 import static com.android.launcher3.Utilities.squaredHypot; 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.ResourceUtils; 34 import com.android.launcher3.Utilities; 35 import com.android.launcher3.util.DisplayController; 36 import com.android.quickstep.InputConsumer; 37 import com.android.quickstep.RecentsAnimationDeviceState; 38 import com.android.quickstep.SystemUiProxy; 39 import com.android.systemui.shared.system.InputMonitorCompat; 40 41 /** 42 * Touch consumer for handling gesture event to launch one handed 43 * One handed gestural in quickstep only active on NO_BUTTON, TWO_BUTTONS, and portrait mode 44 */ 45 public class OneHandedModeInputConsumer extends DelegateInputConsumer { 46 47 private static final int ANGLE_MAX = 150; 48 private static final int ANGLE_MIN = 30; 49 50 private final Context mContext; 51 private final Point mDisplaySize; 52 private final RecentsAnimationDeviceState mDeviceState; 53 54 private final float mDragDistThreshold; 55 private final float mSquaredSlop; 56 57 private final int mNavBarSize; 58 59 private final PointF mDownPos = new PointF(); 60 private final PointF mLastPos = new PointF(); 61 62 private boolean mPassedSlop; 63 private boolean mIsStopGesture; 64 OneHandedModeInputConsumer(Context context, RecentsAnimationDeviceState deviceState, InputConsumer delegate, InputMonitorCompat inputMonitor)65 public OneHandedModeInputConsumer(Context context, RecentsAnimationDeviceState deviceState, 66 InputConsumer delegate, InputMonitorCompat inputMonitor) { 67 super(delegate, inputMonitor); 68 mContext = context; 69 mDeviceState = deviceState; 70 mDragDistThreshold = context.getResources().getDimensionPixelSize( 71 R.dimen.gestures_onehanded_drag_threshold); 72 mSquaredSlop = Utilities.squaredTouchSlop(context); 73 mDisplaySize = DisplayController.INSTANCE.get(mContext).getInfo().currentSize; 74 mNavBarSize = ResourceUtils.getNavbarSize(NAVBAR_BOTTOM_GESTURE_SIZE, 75 mContext.getResources()); 76 } 77 78 @Override getType()79 public int getType() { 80 return TYPE_ONE_HANDED | mDelegate.getType(); 81 } 82 83 @Override onMotionEvent(MotionEvent ev)84 public void onMotionEvent(MotionEvent ev) { 85 switch (ev.getActionMasked()) { 86 case ACTION_DOWN: { 87 mDownPos.set(ev.getX(), ev.getY()); 88 mLastPos.set(mDownPos); 89 break; 90 } 91 case ACTION_MOVE: { 92 if (mState == STATE_DELEGATE_ACTIVE) { 93 break; 94 } 95 if (!mDelegate.allowInterceptByParent()) { 96 mState = STATE_DELEGATE_ACTIVE; 97 break; 98 } 99 100 mLastPos.set(ev.getX(), ev.getY()); 101 if (!mPassedSlop) { 102 if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) 103 > mSquaredSlop) { 104 if ((!mDeviceState.isOneHandedModeActive() && isValidStartAngle( 105 mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y)) 106 || (mDeviceState.isOneHandedModeActive() && isValidExitAngle( 107 mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y))) { 108 // To avoid mis-trigger when motion not touch system gesture region. 109 mPassedSlop = isInSystemGestureRegion(mLastPos); 110 setActive(ev); 111 } else { 112 mState = STATE_DELEGATE_ACTIVE; 113 } 114 } 115 } else { 116 float distance = (float) Math.hypot(mLastPos.x - mDownPos.x, 117 mLastPos.y - mDownPos.y); 118 if (distance > mDragDistThreshold && mPassedSlop) { 119 mIsStopGesture = true; 120 } 121 } 122 break; 123 } 124 case ACTION_UP: { 125 if (mLastPos.y >= mDownPos.y && mPassedSlop) { 126 onStartGestureDetected(); 127 } else if (mIsStopGesture) { 128 onStopGestureDetected(); 129 } 130 clearState(); 131 break; 132 } 133 case ACTION_CANCEL: 134 clearState(); 135 break; 136 } 137 138 if (mState != STATE_ACTIVE) { 139 mDelegate.onMotionEvent(ev); 140 } 141 } 142 clearState()143 private void clearState() { 144 mPassedSlop = false; 145 mState = STATE_INACTIVE; 146 mIsStopGesture = false; 147 } 148 onStartGestureDetected()149 private void onStartGestureDetected() { 150 if (mDeviceState.isSwipeToNotificationEnabled()) { 151 SystemUiProxy.INSTANCE.get(mContext).expandNotificationPanel(); 152 } else if (!mDeviceState.isOneHandedModeActive()) { 153 SystemUiProxy.INSTANCE.get(mContext).startOneHandedMode(); 154 } 155 } 156 onStopGestureDetected()157 private void onStopGestureDetected() { 158 if (!mDeviceState.isOneHandedModeEnabled() || !mDeviceState.isOneHandedModeActive()) { 159 return; 160 } 161 162 SystemUiProxy.INSTANCE.get(mContext).stopOneHandedMode(); 163 } 164 isInSystemGestureRegion(PointF lastPos)165 private boolean isInSystemGestureRegion(PointF lastPos) { 166 final int navBarUpperBound = mDisplaySize.y - mNavBarSize; 167 return mDeviceState.isGesturalNavMode() && lastPos.y > navBarUpperBound; 168 } 169 isValidStartAngle(float deltaX, float deltaY)170 private boolean isValidStartAngle(float deltaX, float deltaY) { 171 final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX)); 172 return angle > -(ANGLE_MAX) && angle < -(ANGLE_MIN); 173 } 174 isValidExitAngle(float deltaX, float deltaY)175 private boolean isValidExitAngle(float deltaX, float deltaY) { 176 final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX)); 177 return angle > ANGLE_MIN && angle < ANGLE_MAX; 178 } 179 } 180