blob: ec29b297a9757d14b1a9e1741f9bfa95c7ab3c1f [file] [log] [blame]
Winson Chung88f33452012-02-23 15:23:44 -08001/*
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
Daniel Sandler325dc232013-06-05 22:57:57 -040017package com.android.launcher3;
Winson Chung88f33452012-02-23 15:23:44 -080018
Fengjiang Li9f90efb2022-12-01 16:53:17 -080019import android.os.Handler;
20import android.view.InputDevice;
Sunny Goyal17feee82020-03-24 13:55:15 -070021import android.view.MotionEvent;
Winson Chung88f33452012-02-23 15:23:44 -080022import android.view.View;
Tony2ca999c2018-09-24 17:24:51 -040023import android.view.ViewConfiguration;
Winson Chung88f33452012-02-23 15:23:44 -080024
Sunny Goyal17feee82020-03-24 13:55:15 -070025/**
Fengjiang Li9f90efb2022-12-01 16:53:17 -080026 * Utility class to handle tripper long press or right click on a view with custom timeout and
27 * stylus event
Sunny Goyal17feee82020-03-24 13:55:15 -070028 */
Winson Chung88f33452012-02-23 15:23:44 -080029public class CheckLongPressHelper {
Winson Chung7501adf2015-06-02 11:24:28 -070030
Tony2ca999c2018-09-24 17:24:51 -040031 public static final float DEFAULT_LONG_PRESS_TIMEOUT_FACTOR = 0.75f;
Tony Wickham1bce7fd2016-04-28 17:39:03 -070032
Sunny Goyal17feee82020-03-24 13:55:15 -070033 private final View mView;
34 private final View.OnLongClickListener mListener;
35 private final float mSlop;
Winson Chung88f33452012-02-23 15:23:44 -080036
Sunny Goyal17feee82020-03-24 13:55:15 -070037 private float mLongPressTimeoutFactor = DEFAULT_LONG_PRESS_TIMEOUT_FACTOR;
38
39 private boolean mHasPerformedLongPress;
Fengjiang Li9f90efb2022-12-01 16:53:17 -080040 private boolean mIsInMouseRightClick;
Sunny Goyal17feee82020-03-24 13:55:15 -070041
42 private Runnable mPendingCheckForLongPress;
Winson Chung88f33452012-02-23 15:23:44 -080043
44 public CheckLongPressHelper(View v) {
Sunny Goyal17feee82020-03-24 13:55:15 -070045 this(v, null);
Winson Chung88f33452012-02-23 15:23:44 -080046 }
47
Winson Chung6b276142015-05-13 15:44:26 -070048 public CheckLongPressHelper(View v, View.OnLongClickListener listener) {
49 mView = v;
50 mListener = listener;
Sunny Goyal17feee82020-03-24 13:55:15 -070051 mSlop = ViewConfiguration.get(mView.getContext()).getScaledTouchSlop();
52 }
53
54 /**
55 * Handles the touch event on a view
56 *
57 * @see View#onTouchEvent(MotionEvent)
58 */
59 public void onTouchEvent(MotionEvent ev) {
60 switch (ev.getAction()) {
61 case MotionEvent.ACTION_DOWN: {
62 // Just in case the previous long press hasn't been cleared, we make sure to
63 // start fresh on touch down.
64 cancelLongPress();
65
Fengjiang Li9f90efb2022-12-01 16:53:17 -080066 // Mouse right click should immediately trigger a long press
67 if (isMouseRightClickDownOrMove(ev)) {
68 mIsInMouseRightClick = true;
69 triggerLongPress();
70 final Handler handler = mView.getHandler();
71 if (handler != null) {
72 // Send an ACTION_UP to end this click gesture to avoid user dragging with
73 // mouse's right button. Note that we need to call
74 // {@link Handler#postAtFrontOfQueue()} instead of {@link View#post()} to
75 // make sure ACTION_UP is sent before any ACTION_MOVE if user is dragging.
76 final MotionEvent actionUpEvent = MotionEvent.obtain(ev);
77 actionUpEvent.setAction(MotionEvent.ACTION_UP);
78 handler.postAtFrontOfQueue(() -> {
79 mView.getRootView().dispatchTouchEvent(actionUpEvent);
80 actionUpEvent.recycle();
81 });
82 }
83 break;
84 }
85
Sunny Goyal17feee82020-03-24 13:55:15 -070086 postCheckForLongPress();
87 if (isStylusButtonPressed(ev)) {
88 triggerLongPress();
89 }
90 break;
91 }
92 case MotionEvent.ACTION_CANCEL:
93 case MotionEvent.ACTION_UP:
94 cancelLongPress();
95 break;
96 case MotionEvent.ACTION_MOVE:
Fengjiang Li9f90efb2022-12-01 16:53:17 -080097 if (mIsInMouseRightClick
98 || !Utilities.pointInView(mView, ev.getX(), ev.getY(), mSlop)) {
Sunny Goyal17feee82020-03-24 13:55:15 -070099 cancelLongPress();
100 } else if (mPendingCheckForLongPress != null && isStylusButtonPressed(ev)) {
101 // Only trigger long press if it has not been cancelled before
102 triggerLongPress();
103 }
104 break;
105 }
Winson Chung6b276142015-05-13 15:44:26 -0700106 }
107
Winson Chung7501adf2015-06-02 11:24:28 -0700108 /**
109 * Overrides the default long press timeout.
110 */
Tony2ca999c2018-09-24 17:24:51 -0400111 public void setLongPressTimeoutFactor(float longPressTimeoutFactor) {
112 mLongPressTimeoutFactor = longPressTimeoutFactor;
Winson Chung7501adf2015-06-02 11:24:28 -0700113 }
114
Sunny Goyal17feee82020-03-24 13:55:15 -0700115 private void postCheckForLongPress() {
Winson Chung88f33452012-02-23 15:23:44 -0800116 mHasPerformedLongPress = false;
117
118 if (mPendingCheckForLongPress == null) {
Sunny Goyal17feee82020-03-24 13:55:15 -0700119 mPendingCheckForLongPress = this::triggerLongPress;
Winson Chung88f33452012-02-23 15:23:44 -0800120 }
Tony2ca999c2018-09-24 17:24:51 -0400121 mView.postDelayed(mPendingCheckForLongPress,
122 (long) (ViewConfiguration.getLongPressTimeout() * mLongPressTimeoutFactor));
Winson Chung88f33452012-02-23 15:23:44 -0800123 }
124
Sunny Goyal17feee82020-03-24 13:55:15 -0700125 /**
Fengjiang Li9f90efb2022-12-01 16:53:17 -0800126 * Cancels any pending long press and right click
Sunny Goyal17feee82020-03-24 13:55:15 -0700127 */
Winson Chung88f33452012-02-23 15:23:44 -0800128 public void cancelLongPress() {
Fengjiang Li9f90efb2022-12-01 16:53:17 -0800129 mIsInMouseRightClick = false;
Winson Chung88f33452012-02-23 15:23:44 -0800130 mHasPerformedLongPress = false;
Sunny Goyal17feee82020-03-24 13:55:15 -0700131 clearCallbacks();
132 }
133
134 /**
135 * Returns true if long press has been performed in the current touch gesture
136 */
137 public boolean hasPerformedLongPress() {
138 return mHasPerformedLongPress;
139 }
140
141 private void triggerLongPress() {
Jon Mirandaa1567d52020-04-15 16:00:01 -0700142 if ((mView.getParent() != null)
143 && mView.hasWindowFocus()
Jon Miranda6fa63472021-01-25 15:30:26 -0500144 && (!mView.isPressed() || mListener != null)
Jon Mirandaa1567d52020-04-15 16:00:01 -0700145 && !mHasPerformedLongPress) {
Sunny Goyal17feee82020-03-24 13:55:15 -0700146 boolean handled;
147 if (mListener != null) {
148 handled = mListener.onLongClick(mView);
149 } else {
150 handled = mView.performLongClick();
151 }
152 if (handled) {
153 mView.setPressed(false);
154 mHasPerformedLongPress = true;
155 }
156 clearCallbacks();
157 }
158 }
159
160 private void clearCallbacks() {
Winson Chung88f33452012-02-23 15:23:44 -0800161 if (mPendingCheckForLongPress != null) {
Winson Chung88f33452012-02-23 15:23:44 -0800162 mView.removeCallbacks(mPendingCheckForLongPress);
163 mPendingCheckForLongPress = null;
164 }
165 }
166
Sunny Goyal17feee82020-03-24 13:55:15 -0700167
168 /**
169 * Identifies if the provided {@link MotionEvent} is a stylus with the primary stylus button
170 * pressed.
171 *
172 * @param event The event to check.
173 * @return Whether a stylus button press occurred.
174 */
175 private static boolean isStylusButtonPressed(MotionEvent event) {
176 return event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS
177 && event.isButtonPressed(MotionEvent.BUTTON_SECONDARY);
Winson Chung88f33452012-02-23 15:23:44 -0800178 }
Fengjiang Li9f90efb2022-12-01 16:53:17 -0800179
180 /**
181 * Detect ACTION_DOWN or ACTION_MOVE from mouse right button. Note that we cannot detect
182 * ACTION_UP from mouse's right button because, in that case,
183 * {@link MotionEvent#getButtonState()} returns 0 for any mouse button (right, middle, right).
184 */
185 private static boolean isMouseRightClickDownOrMove(MotionEvent event) {
186 return event.isFromSource(InputDevice.SOURCE_MOUSE)
187 && ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0);
188 }
Winson Chung88f33452012-02-23 15:23:44 -0800189}