blob: de0f9e5c5cf69d9e9f1f5e2eb05c5dbfa0bfcb77 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2008 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
17package android.view;
18
Rhed Jaoae638752018-09-18 19:40:13 +080019import android.annotation.NonNull;
Artur Satayevad9254c2019-12-10 17:47:54 +000020import android.compat.annotation.UnsupportedAppUsage;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080021import android.graphics.Rect;
Rhed Jaoae638752018-09-18 19:40:13 +080022import android.graphics.Region;
23import android.util.ArrayMap;
24import android.view.accessibility.AccessibilityNodeInfo.TouchDelegateInfo;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080025
26/**
27 * Helper class to handle situations where you want a view to have a larger touch area than its
28 * actual view bounds. The view whose touch area is changed is called the delegate view. This
29 * class should be used by an ancestor of the delegate. To use a TouchDelegate, first create an
30 * instance that specifies the bounds that should be mapped to the delegate and the delegate
31 * view itself.
32 * <p>
33 * The ancestor should then forward all of its touch events received in its
34 * {@link android.view.View#onTouchEvent(MotionEvent)} to {@link #onTouchEvent(MotionEvent)}.
35 * </p>
36 */
37public class TouchDelegate {
Aurimas Liutikas67e2ae82016-10-11 18:17:42 -070038
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080039 /**
Aurimas Liutikas67e2ae82016-10-11 18:17:42 -070040 * View that should receive forwarded touch events
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080041 */
42 private View mDelegateView;
Aurimas Liutikas67e2ae82016-10-11 18:17:42 -070043
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080044 /**
45 * Bounds in local coordinates of the containing view that should be mapped to the delegate
46 * view. This rect is used for initial hit testing.
47 */
48 private Rect mBounds;
Aurimas Liutikas67e2ae82016-10-11 18:17:42 -070049
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080050 /**
51 * mBounds inflated to include some slop. This rect is to track whether the motion events
Siarhei Vishniakoud27a6312017-10-20 18:06:57 -070052 * should be considered to be within the delegate view.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080053 */
54 private Rect mSlopBounds;
Aurimas Liutikas67e2ae82016-10-11 18:17:42 -070055
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080056 /**
57 * True if the delegate had been targeted on a down event (intersected mBounds).
58 */
Mathew Inwooda570dee2018-08-17 14:56:00 +010059 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080060 private boolean mDelegateTargeted;
61
62 /**
63 * The touchable region of the View extends above its actual extent.
64 */
65 public static final int ABOVE = 1;
66
67 /**
68 * The touchable region of the View extends below its actual extent.
69 */
70 public static final int BELOW = 2;
71
72 /**
Siarhei Vishniakoud27a6312017-10-20 18:06:57 -070073 * The touchable region of the View extends to the left of its actual extent.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080074 */
75 public static final int TO_LEFT = 4;
76
77 /**
Siarhei Vishniakoud27a6312017-10-20 18:06:57 -070078 * The touchable region of the View extends to the right of its actual extent.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080079 */
80 public static final int TO_RIGHT = 8;
81
82 private int mSlop;
83
84 /**
Rhed Jaoae638752018-09-18 19:40:13 +080085 * Touch delegate information for accessibility
86 */
87 private TouchDelegateInfo mTouchDelegateInfo;
88
89 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080090 * Constructor
Aurimas Liutikas67e2ae82016-10-11 18:17:42 -070091 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080092 * @param bounds Bounds in local coordinates of the containing view that should be mapped to
93 * the delegate view
94 * @param delegateView The view that should receive motion events
95 */
96 public TouchDelegate(Rect bounds, View delegateView) {
97 mBounds = bounds;
98
99 mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
100 mSlopBounds = new Rect(bounds);
101 mSlopBounds.inset(-mSlop, -mSlop);
102 mDelegateView = delegateView;
103 }
104
105 /**
Dieter Hsufa1b8de2018-08-22 10:35:58 +0800106 * Forward touch events to the delegate view if the event is within the bounds
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800107 * specified in the constructor.
Aurimas Liutikas67e2ae82016-10-11 18:17:42 -0700108 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800109 * @param event The touch event to forward
Dieter Hsufa1b8de2018-08-22 10:35:58 +0800110 * @return True if the event was consumed by the delegate, false otherwise.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800111 */
Dieter Hsufa1b8de2018-08-22 10:35:58 +0800112 public boolean onTouchEvent(@NonNull MotionEvent event) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800113 int x = (int)event.getX();
114 int y = (int)event.getY();
115 boolean sendToDelegate = false;
116 boolean hit = true;
117 boolean handled = false;
118
Siarhei Vishniakou01b729f2018-02-16 17:27:22 -0600119 switch (event.getActionMasked()) {
Siarhei Vishniakoud27a6312017-10-20 18:06:57 -0700120 case MotionEvent.ACTION_DOWN:
121 mDelegateTargeted = mBounds.contains(x, y);
122 sendToDelegate = mDelegateTargeted;
123 break;
Siarhei Vishniakou01b729f2018-02-16 17:27:22 -0600124 case MotionEvent.ACTION_POINTER_DOWN:
125 case MotionEvent.ACTION_POINTER_UP:
Siarhei Vishniakoud27a6312017-10-20 18:06:57 -0700126 case MotionEvent.ACTION_UP:
127 case MotionEvent.ACTION_MOVE:
128 sendToDelegate = mDelegateTargeted;
129 if (sendToDelegate) {
130 Rect slopBounds = mSlopBounds;
131 if (!slopBounds.contains(x, y)) {
132 hit = false;
133 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800134 }
Siarhei Vishniakoud27a6312017-10-20 18:06:57 -0700135 break;
136 case MotionEvent.ACTION_CANCEL:
137 sendToDelegate = mDelegateTargeted;
138 mDelegateTargeted = false;
139 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800140 }
141 if (sendToDelegate) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800142 if (hit) {
143 // Offset event coordinates to be inside the target view
Dieter Hsufa1b8de2018-08-22 10:35:58 +0800144 event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800145 } else {
146 // Offset event coordinates to be outside the target view (in case it does
147 // something like tracking pressed state)
148 int slop = mSlop;
149 event.setLocation(-(slop * 2), -(slop * 2));
150 }
Dieter Hsufa1b8de2018-08-22 10:35:58 +0800151 handled = mDelegateView.dispatchTouchEvent(event);
152 }
153 return handled;
154 }
155
156 /**
157 * Forward hover events to the delegate view if the event is within the bounds
158 * specified in the constructor and touch exploration is enabled.
159 *
Dieter Hsu4ee79ff2018-12-07 18:56:39 +0800160 * <p>This method is provided for accessibility purposes so touch exploration, which is
161 * commonly used by screen readers, can properly place accessibility focus on views that
162 * use touch delegates. Therefore, touch exploration must be enabled for hover events
163 * to be dispatched through the delegate.</p>
164 *
Dieter Hsufa1b8de2018-08-22 10:35:58 +0800165 * @param event The hover event to forward
166 * @return True if the event was consumed by the delegate, false otherwise.
167 *
168 * @see android.view.accessibility.AccessibilityManager#isTouchExplorationEnabled
169 */
170 public boolean onTouchExplorationHoverEvent(@NonNull MotionEvent event) {
171 if (mBounds == null) {
172 return false;
173 }
174
175 final int x = (int) event.getX();
176 final int y = (int) event.getY();
177 boolean hit = true;
178 boolean handled = false;
179
180 final boolean isInbound = mBounds.contains(x, y);
181 switch (event.getActionMasked()) {
182 case MotionEvent.ACTION_HOVER_ENTER:
183 mDelegateTargeted = isInbound;
184 break;
185 case MotionEvent.ACTION_HOVER_MOVE:
186 if (isInbound) {
187 mDelegateTargeted = true;
188 } else {
189 // delegated previously
190 if (mDelegateTargeted && !mSlopBounds.contains(x, y)) {
191 hit = false;
192 }
193 }
194 break;
195 case MotionEvent.ACTION_HOVER_EXIT:
196 mDelegateTargeted = true;
197 break;
198 }
199 if (mDelegateTargeted) {
200 if (hit) {
201 event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2);
202 } else {
203 mDelegateTargeted = false;
204 }
205 handled = mDelegateView.dispatchHoverEvent(event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800206 }
207 return handled;
208 }
Rhed Jaoae638752018-09-18 19:40:13 +0800209
210 /**
211 * Return a {@link TouchDelegateInfo} mapping from regions (in view coordinates) to
212 * delegated views for accessibility usage.
213 *
214 * @return A TouchDelegateInfo.
215 */
216 @NonNull
217 public TouchDelegateInfo getTouchDelegateInfo() {
218 if (mTouchDelegateInfo == null) {
219 final ArrayMap<Region, View> targetMap = new ArrayMap<>(1);
Rhed Jao3a0a6ee2018-10-22 11:37:10 +0800220 Rect bounds = mBounds;
221 if (bounds == null) {
222 bounds = new Rect();
223 }
224 targetMap.put(new Region(bounds), mDelegateView);
Rhed Jaoae638752018-09-18 19:40:13 +0800225 mTouchDelegateInfo = new TouchDelegateInfo(targetMap);
226 }
227 return mTouchDelegateInfo;
228 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800229}