1 /* 2 * Copyright (C) 2012 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.launcher3; 18 19 import android.os.Handler; 20 import android.view.MotionEvent; 21 import android.view.View; 22 import android.view.ViewConfiguration; 23 24 import com.android.launcher3.util.TouchUtil; 25 26 /** 27 * Utility class to handle tripper long press or right click on a view with custom timeout and 28 * stylus event 29 */ 30 public class CheckLongPressHelper { 31 32 public static final float DEFAULT_LONG_PRESS_TIMEOUT_FACTOR = 0.75f; 33 34 private final View mView; 35 private final View.OnLongClickListener mListener; 36 private final float mSlop; 37 38 private float mLongPressTimeoutFactor = DEFAULT_LONG_PRESS_TIMEOUT_FACTOR; 39 40 private boolean mHasPerformedLongPress; 41 private boolean mIsInMouseRightClick; 42 43 private Runnable mPendingCheckForLongPress; 44 CheckLongPressHelper(View v)45 public CheckLongPressHelper(View v) { 46 this(v, null); 47 } 48 CheckLongPressHelper(View v, View.OnLongClickListener listener)49 public CheckLongPressHelper(View v, View.OnLongClickListener listener) { 50 mView = v; 51 mListener = listener; 52 mSlop = ViewConfiguration.get(mView.getContext()).getScaledTouchSlop(); 53 } 54 55 /** 56 * Handles the touch event on a view 57 * 58 * @see View#onTouchEvent(MotionEvent) 59 */ onTouchEvent(MotionEvent ev)60 public void onTouchEvent(MotionEvent ev) { 61 switch (ev.getAction()) { 62 case MotionEvent.ACTION_DOWN: { 63 // Just in case the previous long press hasn't been cleared, we make sure to 64 // start fresh on touch down. 65 cancelLongPress(); 66 67 // Mouse right click should immediately trigger a long press 68 if (TouchUtil.isMouseRightClickDownOrMove(ev)) { 69 mIsInMouseRightClick = true; 70 triggerLongPress(); 71 final Handler handler = mView.getHandler(); 72 if (handler != null) { 73 // Send an ACTION_UP to end this click gesture to avoid user dragging with 74 // mouse's right button. Note that we need to call 75 // {@link Handler#postAtFrontOfQueue()} instead of {@link View#post()} to 76 // make sure ACTION_UP is sent before any ACTION_MOVE if user is dragging. 77 final MotionEvent actionUpEvent = MotionEvent.obtain(ev); 78 actionUpEvent.setAction(MotionEvent.ACTION_UP); 79 handler.postAtFrontOfQueue(() -> { 80 mView.getRootView().dispatchTouchEvent(actionUpEvent); 81 actionUpEvent.recycle(); 82 }); 83 } 84 break; 85 } 86 87 postCheckForLongPress(); 88 if (isStylusButtonPressed(ev)) { 89 triggerLongPress(); 90 } 91 break; 92 } 93 case MotionEvent.ACTION_CANCEL: 94 case MotionEvent.ACTION_UP: 95 cancelLongPress(); 96 break; 97 case MotionEvent.ACTION_MOVE: 98 if (mIsInMouseRightClick 99 || !Utilities.pointInView(mView, ev.getX(), ev.getY(), mSlop)) { 100 cancelLongPress(); 101 } else if (mPendingCheckForLongPress != null && isStylusButtonPressed(ev)) { 102 // Only trigger long press if it has not been cancelled before 103 triggerLongPress(); 104 } 105 break; 106 } 107 } 108 109 /** 110 * Overrides the default long press timeout. 111 */ setLongPressTimeoutFactor(float longPressTimeoutFactor)112 public void setLongPressTimeoutFactor(float longPressTimeoutFactor) { 113 mLongPressTimeoutFactor = longPressTimeoutFactor; 114 } 115 postCheckForLongPress()116 private void postCheckForLongPress() { 117 mHasPerformedLongPress = false; 118 119 if (mPendingCheckForLongPress == null) { 120 mPendingCheckForLongPress = this::triggerLongPress; 121 } 122 mView.postDelayed(mPendingCheckForLongPress, 123 (long) (ViewConfiguration.getLongPressTimeout() * mLongPressTimeoutFactor)); 124 } 125 126 /** 127 * Cancels any pending long press and right click 128 */ cancelLongPress()129 public void cancelLongPress() { 130 mIsInMouseRightClick = false; 131 mHasPerformedLongPress = false; 132 clearCallbacks(); 133 } 134 135 /** 136 * Returns true if long press has been performed in the current touch gesture 137 */ hasPerformedLongPress()138 public boolean hasPerformedLongPress() { 139 return mHasPerformedLongPress; 140 } 141 triggerLongPress()142 private void triggerLongPress() { 143 if ((mView.getParent() != null) 144 && mView.hasWindowFocus() 145 && (!mView.isPressed() || mListener != null) 146 && !mHasPerformedLongPress) { 147 boolean handled; 148 if (mListener != null) { 149 handled = mListener.onLongClick(mView); 150 } else { 151 handled = mView.performLongClick(); 152 } 153 if (handled) { 154 mView.setPressed(false); 155 mHasPerformedLongPress = true; 156 } 157 clearCallbacks(); 158 } 159 } 160 clearCallbacks()161 private void clearCallbacks() { 162 if (mPendingCheckForLongPress != null) { 163 mView.removeCallbacks(mPendingCheckForLongPress); 164 mPendingCheckForLongPress = null; 165 } 166 } 167 168 169 /** 170 * Identifies if the provided {@link MotionEvent} is a stylus with the primary stylus button 171 * pressed. 172 * 173 * @param event The event to check. 174 * @return Whether a stylus button press occurred. 175 */ isStylusButtonPressed(MotionEvent event)176 private static boolean isStylusButtonPressed(MotionEvent event) { 177 return event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS 178 && event.isButtonPressed(MotionEvent.BUTTON_SECONDARY); 179 } 180 } 181