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