blob: 2fb210124f11776a751c006f3e03636d207c2d1d [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
22import android.animation.Animator;
23import android.animation.AnimatorListenerAdapter;
24import android.animation.ObjectAnimator;
25import android.content.Context;
26import android.graphics.drawable.Drawable;
27import android.util.IntProperty;
Oren Blasbergf44d90b2015-08-31 14:15:26 -070028import android.view.MotionEvent;
29import android.view.View;
Oren Blasbergf44d90b2015-08-31 14:15:26 -070030import android.view.animation.AccelerateDecelerateInterpolator;
31import android.widget.TextView;
32import android.widget.ListView;
33
34
35/**
36 * Wrapper class for a ListView. This wrapper can hijack the focus to
37 * make sure the list uses the appropriate drawables and states when
38 * displayed on screen within a drop down. The focus is never actually
39 * passed to the drop down in this mode; the list only looks focused.
40 *
41 * @hide
42 */
43public class DropDownListView extends ListView {
44 /** Duration in milliseconds of the drag-to-open click animation. */
45 private static final long CLICK_ANIM_DURATION = 150;
46
47 /** Target alpha value for drag-to-open click animation. */
48 private static final int CLICK_ANIM_ALPHA = 0x80;
49
50 /** Wrapper around Drawable's <code>alpha</code> property. */
51 private static final IntProperty<Drawable> DRAWABLE_ALPHA =
52 new IntProperty<Drawable>("alpha") {
53 @Override
54 public void setValue(Drawable object, int value) {
55 object.setAlpha(value);
56 }
57
58 @Override
59 public Integer get(Drawable object) {
60 return object.getAlpha();
61 }
62 };
63
64 /*
65 * WARNING: This is a workaround for a touch mode issue.
66 *
67 * Touch mode is propagated lazily to windows. This causes problems in
68 * the following scenario:
69 * - Type something in the AutoCompleteTextView and get some results
70 * - Move down with the d-pad to select an item in the list
71 * - Move up with the d-pad until the selection disappears
72 * - Type more text in the AutoCompleteTextView *using the soft keyboard*
73 * and get new results; you are now in touch mode
74 * - The selection comes back on the first item in the list, even though
75 * the list is supposed to be in touch mode
76 *
77 * Using the soft keyboard triggers the touch mode change but that change
78 * is propagated to our window only after the first list layout, therefore
79 * after the list attempts to resurrect the selection.
80 *
81 * The trick to work around this issue is to pretend the list is in touch
82 * mode when we know that the selection should not appear, that is when
83 * we know the user moved the selection away from the list.
84 *
85 * This boolean is set to true whenever we explicitly hide the list's
86 * selection and reset to false whenever we know the user moved the
87 * selection back to the list.
88 *
89 * When this boolean is true, isInTouchMode() returns true, otherwise it
90 * returns super.isInTouchMode().
91 */
92 private boolean mListSelectionHidden;
93
94 /**
95 * True if this wrapper should fake focus.
96 */
97 private boolean mHijackFocus;
98
99 /** Whether to force drawing of the pressed state selector. */
100 private boolean mDrawsInPressedState;
101
102 /** Current drag-to-open click animation, if any. */
103 private Animator mClickAnimation;
104
105 /** Helper for drag-to-open auto scrolling. */
106 private AbsListViewAutoScroller mScrollHelper;
107
108 /**
109 * Creates a new list view wrapper.
110 *
111 * @param context this view's context
112 */
113 public DropDownListView(Context context, boolean hijackFocus) {
114 this(context, hijackFocus, com.android.internal.R.attr.dropDownListViewStyle);
115 }
116
117 /**
118 * Creates a new list view wrapper.
119 *
120 * @param context this view's context
121 */
122 public DropDownListView(Context context, boolean hijackFocus, int defStyleAttr) {
123 super(context, null, defStyleAttr);
124 mHijackFocus = hijackFocus;
125 // TODO: Add an API to control this
126 setCacheColorHint(0); // Transparent, since the background drawable could be anything.
127 }
128
Oren Blasberg8e12f8d2015-09-02 14:25:56 -0700129 @Override
Alan Viverette00aa5102015-11-03 13:03:15 -0500130 boolean shouldShowSelector() {
131 return isHovered() || super.shouldShowSelector();
Oren Blasberg8e12f8d2015-09-02 14:25:56 -0700132 }
133
Oren Blasberg8e12f8d2015-09-02 14:25:56 -0700134 @Override
135 public boolean onHoverEvent(MotionEvent ev) {
Alan Viverette00aa5102015-11-03 13:03:15 -0500136 // Allow the super class to handle hover state management first.
137 final boolean handled = super.onHoverEvent(ev);
138
Oren Blasberg8e12f8d2015-09-02 14:25:56 -0700139 final int action = ev.getActionMasked();
140 if (action == MotionEvent.ACTION_HOVER_ENTER
141 || action == MotionEvent.ACTION_HOVER_MOVE) {
142 final int position = pointToPosition((int) ev.getX(), (int) ev.getY());
143 if (position != INVALID_POSITION && position != mSelectedPosition) {
144 final View hoveredItem = getChildAt(position - getFirstVisiblePosition());
145 if (hoveredItem.isEnabled()) {
146 // Force a focus so that the proper selector state gets used when we update.
147 requestFocus();
148
149 positionSelector(position, hoveredItem);
150 setSelectedPositionInt(position);
151 setNextSelectedPositionInt(position);
152 }
153 updateSelectorState();
154 }
155 } else {
156 // Do not cancel the selected position if the selection is visible by other reasons.
157 if (!super.shouldShowSelector()) {
158 setSelectedPositionInt(INVALID_POSITION);
Alan Viverette00aa5102015-11-03 13:03:15 -0500159 setNextSelectedPositionInt(INVALID_POSITION);
Oren Blasberg8e12f8d2015-09-02 14:25:56 -0700160 }
161 }
Oren Blasberg8e12f8d2015-09-02 14:25:56 -0700162
Alan Viverette00aa5102015-11-03 13:03:15 -0500163 return handled;
Alan Viverette2ac975d2015-10-08 19:54:16 +0000164 }
165
Oren Blasbergf44d90b2015-08-31 14:15:26 -0700166 /**
167 * Handles forwarded events.
168 *
169 * @param activePointerId id of the pointer that activated forwarding
170 * @return whether the event was handled
171 */
172 public boolean onForwardedEvent(MotionEvent event, int activePointerId) {
173 boolean handledEvent = true;
174 boolean clearPressedItem = false;
175
176 final int actionMasked = event.getActionMasked();
177 switch (actionMasked) {
178 case MotionEvent.ACTION_CANCEL:
179 handledEvent = false;
180 break;
181 case MotionEvent.ACTION_UP:
182 handledEvent = false;
183 // $FALL-THROUGH$
184 case MotionEvent.ACTION_MOVE:
185 final int activeIndex = event.findPointerIndex(activePointerId);
186 if (activeIndex < 0) {
187 handledEvent = false;
188 break;
189 }
190
191 final int x = (int) event.getX(activeIndex);
192 final int y = (int) event.getY(activeIndex);
193 final int position = pointToPosition(x, y);
194 if (position == INVALID_POSITION) {
195 clearPressedItem = true;
196 break;
197 }
198
199 final View child = getChildAt(position - getFirstVisiblePosition());
200 setPressedItem(child, position, x, y);
201 handledEvent = true;
202
203 if (actionMasked == MotionEvent.ACTION_UP) {
204 clickPressedItem(child, position);
205 }
206 break;
207 }
208
209 // Failure to handle the event cancels forwarding.
210 if (!handledEvent || clearPressedItem) {
211 clearPressedItem();
212 }
213
214 // Manage automatic scrolling.
215 if (handledEvent) {
216 if (mScrollHelper == null) {
217 mScrollHelper = new AbsListViewAutoScroller(this);
218 }
219 mScrollHelper.setEnabled(true);
220 mScrollHelper.onTouch(this, event);
221 } else if (mScrollHelper != null) {
222 mScrollHelper.setEnabled(false);
223 }
224
225 return handledEvent;
226 }
227
228 /**
229 * Sets whether the list selection is hidden, as part of a workaround for a touch mode issue
230 * (see the declaration for mListSelectionHidden).
231 * @param listSelectionHidden
232 */
233 public void setListSelectionHidden(boolean listSelectionHidden) {
234 this.mListSelectionHidden = listSelectionHidden;
235 }
236
237 /**
238 * Starts an alpha animation on the selector. When the animation ends,
239 * the list performs a click on the item.
240 */
241 private void clickPressedItem(final View child, final int position) {
242 final long id = getItemIdAtPosition(position);
243 final Animator anim = ObjectAnimator.ofInt(
244 mSelector, DRAWABLE_ALPHA, 0xFF, CLICK_ANIM_ALPHA, 0xFF);
245 anim.setDuration(CLICK_ANIM_DURATION);
246 anim.setInterpolator(new AccelerateDecelerateInterpolator());
247 anim.addListener(new AnimatorListenerAdapter() {
248 @Override
249 public void onAnimationEnd(Animator animation) {
250 performItemClick(child, position, id);
251 }
252 });
253 anim.start();
254
255 if (mClickAnimation != null) {
256 mClickAnimation.cancel();
257 }
258 mClickAnimation = anim;
259 }
260
261 private void clearPressedItem() {
262 mDrawsInPressedState = false;
263 setPressed(false);
264 updateSelectorState();
265
266 final View motionView = getChildAt(mMotionPosition - mFirstPosition);
267 if (motionView != null) {
268 motionView.setPressed(false);
269 }
270
271 if (mClickAnimation != null) {
272 mClickAnimation.cancel();
273 mClickAnimation = null;
274 }
275 }
276
277 private void setPressedItem(View child, int position, float x, float y) {
278 mDrawsInPressedState = true;
279
280 // Ordering is essential. First, update the container's pressed state.
281 drawableHotspotChanged(x, y);
282 if (!isPressed()) {
283 setPressed(true);
284 }
285
286 // Next, run layout if we need to stabilize child positions.
287 if (mDataChanged) {
288 layoutChildren();
289 }
290
291 // Manage the pressed view based on motion position. This allows us to
292 // play nicely with actual touch and scroll events.
293 final View motionView = getChildAt(mMotionPosition - mFirstPosition);
294 if (motionView != null && motionView != child && motionView.isPressed()) {
295 motionView.setPressed(false);
296 }
297 mMotionPosition = position;
298
299 // Offset for child coordinates.
300 final float childX = x - child.getLeft();
301 final float childY = y - child.getTop();
302 child.drawableHotspotChanged(childX, childY);
303 if (!child.isPressed()) {
304 child.setPressed(true);
305 }
306
307 // Ensure that keyboard focus starts from the last touched position.
308 setSelectedPositionInt(position);
309 positionSelectorLikeTouch(position, child, x, y);
310
311 // Refresh the drawable state to reflect the new pressed state,
312 // which will also update the selector state.
313 refreshDrawableState();
314
315 if (mClickAnimation != null) {
316 mClickAnimation.cancel();
317 mClickAnimation = null;
318 }
319 }
320
321 @Override
322 boolean touchModeDrawsInPressedState() {
323 return mDrawsInPressedState || super.touchModeDrawsInPressedState();
324 }
325
326 /**
327 * Avoids jarring scrolling effect by ensuring that list elements
328 * made of a text view fit on a single line.
329 *
330 * @param position the item index in the list to get a view for
331 * @return the view for the specified item
332 */
333 @Override
334 View obtainView(int position, boolean[] isScrap) {
335 View view = super.obtainView(position, isScrap);
336
337 if (view instanceof TextView) {
338 ((TextView) view).setHorizontallyScrolling(true);
339 }
340
341 return view;
342 }
343
344 @Override
345 public boolean isInTouchMode() {
346 // WARNING: Please read the comment where mListSelectionHidden is declared
347 return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
348 }
349
350 /**
351 * Returns the focus state in the drop down.
352 *
353 * @return true always if hijacking focus
354 */
355 @Override
356 public boolean hasWindowFocus() {
357 return mHijackFocus || super.hasWindowFocus();
358 }
359
360 /**
361 * Returns the focus state in the drop down.
362 *
363 * @return true always if hijacking focus
364 */
365 @Override
366 public boolean isFocused() {
367 return mHijackFocus || super.isFocused();
368 }
369
370 /**
371 * Returns the focus state in the drop down.
372 *
373 * @return true always if hijacking focus
374 */
375 @Override
376 public boolean hasFocus() {
377 return mHijackFocus || super.hasFocus();
378 }
379}