blob: 02f7e7a783079b37c0821eb4d3481b6686be29aa [file] [log] [blame]
Oren Blasbergf44d90b2015-08-31 14:15:26 -07001/*
2 * Copyright (C) 2015 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.widget;
18
19
20import com.android.internal.widget.AutoScrollHelper.AbsListViewAutoScroller;
21
Alan Viverettebe91ad52016-01-13 13:55:44 -050022import android.annotation.NonNull;
Oren Blasbergf44d90b2015-08-31 14:15:26 -070023import android.content.Context;
Oren Blasbergf44d90b2015-08-31 14:15:26 -070024import android.view.MotionEvent;
25import android.view.View;
Oren Blasbergf44d90b2015-08-31 14:15:26 -070026
27/**
28 * Wrapper class for a ListView. This wrapper can hijack the focus to
29 * make sure the list uses the appropriate drawables and states when
30 * displayed on screen within a drop down. The focus is never actually
31 * passed to the drop down in this mode; the list only looks focused.
32 *
33 * @hide
34 */
35public class DropDownListView extends ListView {
Oren Blasbergf44d90b2015-08-31 14:15:26 -070036 /*
37 * WARNING: This is a workaround for a touch mode issue.
38 *
39 * Touch mode is propagated lazily to windows. This causes problems in
40 * the following scenario:
41 * - Type something in the AutoCompleteTextView and get some results
42 * - Move down with the d-pad to select an item in the list
43 * - Move up with the d-pad until the selection disappears
44 * - Type more text in the AutoCompleteTextView *using the soft keyboard*
45 * and get new results; you are now in touch mode
46 * - The selection comes back on the first item in the list, even though
47 * the list is supposed to be in touch mode
48 *
49 * Using the soft keyboard triggers the touch mode change but that change
50 * is propagated to our window only after the first list layout, therefore
51 * after the list attempts to resurrect the selection.
52 *
53 * The trick to work around this issue is to pretend the list is in touch
54 * mode when we know that the selection should not appear, that is when
55 * we know the user moved the selection away from the list.
56 *
57 * This boolean is set to true whenever we explicitly hide the list's
58 * selection and reset to false whenever we know the user moved the
59 * selection back to the list.
60 *
61 * When this boolean is true, isInTouchMode() returns true, otherwise it
62 * returns super.isInTouchMode().
63 */
64 private boolean mListSelectionHidden;
65
66 /**
67 * True if this wrapper should fake focus.
68 */
69 private boolean mHijackFocus;
70
71 /** Whether to force drawing of the pressed state selector. */
72 private boolean mDrawsInPressedState;
73
Oren Blasbergf44d90b2015-08-31 14:15:26 -070074 /** Helper for drag-to-open auto scrolling. */
75 private AbsListViewAutoScroller mScrollHelper;
76
77 /**
78 * Creates a new list view wrapper.
79 *
80 * @param context this view's context
81 */
Alan Viverettebe91ad52016-01-13 13:55:44 -050082 public DropDownListView(@NonNull Context context, boolean hijackFocus) {
Oren Blasbergf44d90b2015-08-31 14:15:26 -070083 this(context, hijackFocus, com.android.internal.R.attr.dropDownListViewStyle);
84 }
85
86 /**
87 * Creates a new list view wrapper.
88 *
89 * @param context this view's context
90 */
Alan Viverettebe91ad52016-01-13 13:55:44 -050091 public DropDownListView(@NonNull Context context, boolean hijackFocus, int defStyleAttr) {
Oren Blasbergf44d90b2015-08-31 14:15:26 -070092 super(context, null, defStyleAttr);
93 mHijackFocus = hijackFocus;
94 // TODO: Add an API to control this
95 setCacheColorHint(0); // Transparent, since the background drawable could be anything.
96 }
97
Oren Blasberg8e12f8d2015-09-02 14:25:56 -070098 @Override
Alan Viverette00aa5102015-11-03 13:03:15 -050099 boolean shouldShowSelector() {
100 return isHovered() || super.shouldShowSelector();
Oren Blasberg8e12f8d2015-09-02 14:25:56 -0700101 }
102
Oren Blasberg8e12f8d2015-09-02 14:25:56 -0700103 @Override
Alan Viverettebe91ad52016-01-13 13:55:44 -0500104 public boolean onHoverEvent(@NonNull MotionEvent ev) {
Alan Viverette00aa5102015-11-03 13:03:15 -0500105 // Allow the super class to handle hover state management first.
106 final boolean handled = super.onHoverEvent(ev);
107
Oren Blasberg8e12f8d2015-09-02 14:25:56 -0700108 final int action = ev.getActionMasked();
109 if (action == MotionEvent.ACTION_HOVER_ENTER
110 || action == MotionEvent.ACTION_HOVER_MOVE) {
111 final int position = pointToPosition((int) ev.getX(), (int) ev.getY());
112 if (position != INVALID_POSITION && position != mSelectedPosition) {
113 final View hoveredItem = getChildAt(position - getFirstVisiblePosition());
114 if (hoveredItem.isEnabled()) {
115 // Force a focus so that the proper selector state gets used when we update.
116 requestFocus();
117
118 positionSelector(position, hoveredItem);
119 setSelectedPositionInt(position);
120 setNextSelectedPositionInt(position);
121 }
122 updateSelectorState();
123 }
124 } else {
125 // Do not cancel the selected position if the selection is visible by other reasons.
126 if (!super.shouldShowSelector()) {
127 setSelectedPositionInt(INVALID_POSITION);
Alan Viverette00aa5102015-11-03 13:03:15 -0500128 setNextSelectedPositionInt(INVALID_POSITION);
Oren Blasberg8e12f8d2015-09-02 14:25:56 -0700129 }
130 }
Oren Blasberg8e12f8d2015-09-02 14:25:56 -0700131
Alan Viverette00aa5102015-11-03 13:03:15 -0500132 return handled;
Alan Viverette2ac975d2015-10-08 19:54:16 +0000133 }
134
Oren Blasbergf44d90b2015-08-31 14:15:26 -0700135 /**
136 * Handles forwarded events.
137 *
138 * @param activePointerId id of the pointer that activated forwarding
139 * @return whether the event was handled
140 */
Alan Viverettebe91ad52016-01-13 13:55:44 -0500141 public boolean onForwardedEvent(@NonNull MotionEvent event, int activePointerId) {
Oren Blasbergf44d90b2015-08-31 14:15:26 -0700142 boolean handledEvent = true;
143 boolean clearPressedItem = false;
144
145 final int actionMasked = event.getActionMasked();
146 switch (actionMasked) {
147 case MotionEvent.ACTION_CANCEL:
148 handledEvent = false;
149 break;
150 case MotionEvent.ACTION_UP:
151 handledEvent = false;
152 // $FALL-THROUGH$
153 case MotionEvent.ACTION_MOVE:
154 final int activeIndex = event.findPointerIndex(activePointerId);
155 if (activeIndex < 0) {
156 handledEvent = false;
157 break;
158 }
159
160 final int x = (int) event.getX(activeIndex);
161 final int y = (int) event.getY(activeIndex);
162 final int position = pointToPosition(x, y);
163 if (position == INVALID_POSITION) {
164 clearPressedItem = true;
165 break;
166 }
167
168 final View child = getChildAt(position - getFirstVisiblePosition());
169 setPressedItem(child, position, x, y);
170 handledEvent = true;
171
172 if (actionMasked == MotionEvent.ACTION_UP) {
Alan Viverettebe91ad52016-01-13 13:55:44 -0500173 final long id = getItemIdAtPosition(position);
174 performItemClick(child, position, id);
Oren Blasbergf44d90b2015-08-31 14:15:26 -0700175 }
176 break;
177 }
178
179 // Failure to handle the event cancels forwarding.
180 if (!handledEvent || clearPressedItem) {
181 clearPressedItem();
182 }
183
184 // Manage automatic scrolling.
185 if (handledEvent) {
186 if (mScrollHelper == null) {
187 mScrollHelper = new AbsListViewAutoScroller(this);
188 }
189 mScrollHelper.setEnabled(true);
190 mScrollHelper.onTouch(this, event);
191 } else if (mScrollHelper != null) {
192 mScrollHelper.setEnabled(false);
193 }
194
195 return handledEvent;
196 }
197
198 /**
199 * Sets whether the list selection is hidden, as part of a workaround for a touch mode issue
200 * (see the declaration for mListSelectionHidden).
201 * @param listSelectionHidden
202 */
203 public void setListSelectionHidden(boolean listSelectionHidden) {
204 this.mListSelectionHidden = listSelectionHidden;
205 }
206
Oren Blasbergf44d90b2015-08-31 14:15:26 -0700207 private void clearPressedItem() {
208 mDrawsInPressedState = false;
209 setPressed(false);
210 updateSelectorState();
211
212 final View motionView = getChildAt(mMotionPosition - mFirstPosition);
213 if (motionView != null) {
214 motionView.setPressed(false);
215 }
Oren Blasbergf44d90b2015-08-31 14:15:26 -0700216 }
217
Alan Viverettebe91ad52016-01-13 13:55:44 -0500218 private void setPressedItem(@NonNull View child, int position, float x, float y) {
Oren Blasbergf44d90b2015-08-31 14:15:26 -0700219 mDrawsInPressedState = true;
220
221 // Ordering is essential. First, update the container's pressed state.
222 drawableHotspotChanged(x, y);
223 if (!isPressed()) {
224 setPressed(true);
225 }
226
227 // Next, run layout if we need to stabilize child positions.
228 if (mDataChanged) {
229 layoutChildren();
230 }
231
232 // Manage the pressed view based on motion position. This allows us to
233 // play nicely with actual touch and scroll events.
234 final View motionView = getChildAt(mMotionPosition - mFirstPosition);
235 if (motionView != null && motionView != child && motionView.isPressed()) {
236 motionView.setPressed(false);
237 }
238 mMotionPosition = position;
239
240 // Offset for child coordinates.
241 final float childX = x - child.getLeft();
242 final float childY = y - child.getTop();
243 child.drawableHotspotChanged(childX, childY);
244 if (!child.isPressed()) {
245 child.setPressed(true);
246 }
247
248 // Ensure that keyboard focus starts from the last touched position.
249 setSelectedPositionInt(position);
250 positionSelectorLikeTouch(position, child, x, y);
251
252 // Refresh the drawable state to reflect the new pressed state,
253 // which will also update the selector state.
254 refreshDrawableState();
Oren Blasbergf44d90b2015-08-31 14:15:26 -0700255 }
256
257 @Override
258 boolean touchModeDrawsInPressedState() {
259 return mDrawsInPressedState || super.touchModeDrawsInPressedState();
260 }
261
262 /**
263 * Avoids jarring scrolling effect by ensuring that list elements
264 * made of a text view fit on a single line.
265 *
266 * @param position the item index in the list to get a view for
267 * @return the view for the specified item
268 */
269 @Override
270 View obtainView(int position, boolean[] isScrap) {
271 View view = super.obtainView(position, isScrap);
272
273 if (view instanceof TextView) {
274 ((TextView) view).setHorizontallyScrolling(true);
275 }
276
277 return view;
278 }
279
280 @Override
281 public boolean isInTouchMode() {
282 // WARNING: Please read the comment where mListSelectionHidden is declared
283 return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
284 }
285
286 /**
287 * Returns the focus state in the drop down.
288 *
289 * @return true always if hijacking focus
290 */
291 @Override
292 public boolean hasWindowFocus() {
293 return mHijackFocus || super.hasWindowFocus();
294 }
295
296 /**
297 * Returns the focus state in the drop down.
298 *
299 * @return true always if hijacking focus
300 */
301 @Override
302 public boolean isFocused() {
303 return mHijackFocus || super.isFocused();
304 }
305
306 /**
307 * Returns the focus state in the drop down.
308 *
309 * @return true always if hijacking focus
310 */
311 @Override
312 public boolean hasFocus() {
313 return mHijackFocus || super.hasFocus();
314 }
315}