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