blob: 66fe46f29fceb6bb637803fc5f22103fb5515b98 [file] [log] [blame]
Adam Powellc3fa6302010-05-18 11:36:27 -07001/*
2 * Copyright (C) 2010 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
Alan Viverettec0502722013-08-15 18:05:52 -070019import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
Adam Powellc3fa6302010-05-18 11:36:27 -070022import android.content.Context;
23import android.database.DataSetObserver;
24import android.graphics.Rect;
25import android.graphics.drawable.Drawable;
26import android.os.Handler;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -070027import android.text.TextUtils;
Adam Powellc3fa6302010-05-18 11:36:27 -070028import android.util.AttributeSet;
Alan Viverettec0502722013-08-15 18:05:52 -070029import android.util.IntProperty;
Adam Powellc3fa6302010-05-18 11:36:27 -070030import android.util.Log;
Adam Powell54c94de2013-09-26 15:36:34 -070031import android.view.Gravity;
Adam Powellc3fa6302010-05-18 11:36:27 -070032import android.view.KeyEvent;
33import android.view.MotionEvent;
34import android.view.View;
Adam Powellc3fa6302010-05-18 11:36:27 -070035import android.view.View.MeasureSpec;
Alan Viverette69960142013-08-22 17:26:57 -070036import android.view.View.OnAttachStateChangeListener;
Adam Powellc3fa6302010-05-18 11:36:27 -070037import android.view.View.OnTouchListener;
Alan Viveretteca6a36112013-08-16 14:41:06 -070038import android.view.ViewConfiguration;
Gilles Debunne711734a2011-02-07 18:26:11 -080039import android.view.ViewGroup;
40import android.view.ViewParent;
Alan Viverettec0502722013-08-15 18:05:52 -070041import android.view.animation.AccelerateDecelerateInterpolator;
Adam Powellc3fa6302010-05-18 11:36:27 -070042
Alan Viverette5e660212013-08-21 13:21:45 -070043import com.android.internal.widget.AutoScrollHelper.AbsListViewAutoScroller;
44
Fabrice Di Meglio1d3d7da2012-07-27 15:15:04 -070045import java.util.Locale;
46
Adam Powellc3fa6302010-05-18 11:36:27 -070047/**
48 * A ListPopupWindow anchors itself to a host view and displays a
Adam Powell65d79fb2010-08-11 22:05:46 -070049 * list of choices.
Adam Powellc3fa6302010-05-18 11:36:27 -070050 *
51 * <p>ListPopupWindow contains a number of tricky behaviors surrounding
52 * positioning, scrolling parents to fit the dropdown, interacting
53 * sanely with the IME if present, and others.
54 *
55 * @see android.widget.AutoCompleteTextView
56 * @see android.widget.Spinner
57 */
58public class ListPopupWindow {
59 private static final String TAG = "ListPopupWindow";
60 private static final boolean DEBUG = false;
61
62 /**
63 * This value controls the length of time that the user
64 * must leave a pointer down without scrolling to expand
65 * the autocomplete dropdown list to cover the IME.
66 */
67 private static final int EXPAND_LIST_TIMEOUT = 250;
68
69 private Context mContext;
70 private PopupWindow mPopup;
71 private ListAdapter mAdapter;
72 private DropDownListView mDropDownList;
73
74 private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
75 private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
76 private int mDropDownHorizontalOffset;
77 private int mDropDownVerticalOffset;
Adam Powell8132ba52011-07-15 17:37:11 -070078 private boolean mDropDownVerticalOffsetSet;
Adam Powellc3fa6302010-05-18 11:36:27 -070079
Adam Powell54c94de2013-09-26 15:36:34 -070080 private int mDropDownGravity = Gravity.NO_GRAVITY;
81
Adam Powellc3fa6302010-05-18 11:36:27 -070082 private boolean mDropDownAlwaysVisible = false;
83 private boolean mForceIgnoreOutsideTouch = false;
Adam Powell348e69c2011-02-16 16:49:50 -080084 int mListItemExpandMaximum = Integer.MAX_VALUE;
Adam Powellc3fa6302010-05-18 11:36:27 -070085
86 private View mPromptView;
87 private int mPromptPosition = POSITION_PROMPT_ABOVE;
88
89 private DataSetObserver mObserver;
90
91 private View mDropDownAnchorView;
92
93 private Drawable mDropDownListHighlight;
94
95 private AdapterView.OnItemClickListener mItemClickListener;
96 private AdapterView.OnItemSelectedListener mItemSelectedListener;
97
Adam Powell42675342010-07-09 18:02:59 -070098 private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable();
Adam Powellc3fa6302010-05-18 11:36:27 -070099 private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor();
100 private final PopupScrollListener mScrollListener = new PopupScrollListener();
101 private final ListSelectorHider mHideSelector = new ListSelectorHider();
102 private Runnable mShowDropDownRunnable;
103
104 private Handler mHandler = new Handler();
105
106 private Rect mTempRect = new Rect();
107
108 private boolean mModal;
109
Fabrice Di Meglio1d3d7da2012-07-27 15:15:04 -0700110 private int mLayoutDirection;
111
Adam Powellc3fa6302010-05-18 11:36:27 -0700112 /**
113 * The provided prompt view should appear above list content.
114 *
115 * @see #setPromptPosition(int)
116 * @see #getPromptPosition()
117 * @see #setPromptView(View)
118 */
119 public static final int POSITION_PROMPT_ABOVE = 0;
120
121 /**
122 * The provided prompt view should appear below list content.
123 *
124 * @see #setPromptPosition(int)
125 * @see #getPromptPosition()
126 * @see #setPromptView(View)
127 */
128 public static final int POSITION_PROMPT_BELOW = 1;
129
130 /**
131 * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}.
132 * If used to specify a popup width, the popup will match the width of the anchor view.
133 * If used to specify a popup height, the popup will fill available space.
134 */
135 public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT;
136
137 /**
138 * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}.
139 * If used to specify a popup width, the popup will use the width of its content.
140 */
141 public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT;
142
143 /**
144 * Mode for {@link #setInputMethodMode(int)}: the requirements for the
145 * input method should be based on the focusability of the popup. That is
146 * if it is focusable than it needs to work with the input method, else
147 * it doesn't.
148 */
149 public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE;
150
151 /**
152 * Mode for {@link #setInputMethodMode(int)}: this popup always needs to
153 * work with an input method, regardless of whether it is focusable. This
154 * means that it will always be displayed so that the user can also operate
155 * the input method while it is shown.
156 */
157 public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED;
158
159 /**
160 * Mode for {@link #setInputMethodMode(int)}: this popup never needs to
161 * work with an input method, regardless of whether it is focusable. This
162 * means that it will always be displayed to use as much space on the
163 * screen as needed, regardless of whether this covers the input method.
164 */
165 public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED;
166
167 /**
168 * Create a new, empty popup window capable of displaying items from a ListAdapter.
169 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
170 *
171 * @param context Context used for contained views.
172 */
173 public ListPopupWindow(Context context) {
Daniel Lehmannc2238d02010-10-21 11:52:55 -0700174 this(context, null, com.android.internal.R.attr.listPopupWindowStyle, 0);
Adam Powellc3fa6302010-05-18 11:36:27 -0700175 }
176
177 /**
178 * Create a new, empty popup window capable of displaying items from a ListAdapter.
179 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
180 *
181 * @param context Context used for contained views.
182 * @param attrs Attributes from inflating parent views used to style the popup.
183 */
184 public ListPopupWindow(Context context, AttributeSet attrs) {
Adam Powell0b2d3062010-09-14 16:15:02 -0700185 this(context, attrs, com.android.internal.R.attr.listPopupWindowStyle, 0);
Adam Powellc3fa6302010-05-18 11:36:27 -0700186 }
187
188 /**
189 * Create a new, empty popup window capable of displaying items from a ListAdapter.
190 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
191 *
192 * @param context Context used for contained views.
193 * @param attrs Attributes from inflating parent views used to style the popup.
194 * @param defStyleAttr Default style attribute to use for popup content.
195 */
196 public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr) {
197 this(context, attrs, defStyleAttr, 0);
198 }
199
200 /**
201 * Create a new, empty popup window capable of displaying items from a ListAdapter.
202 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
203 *
204 * @param context Context used for contained views.
205 * @param attrs Attributes from inflating parent views used to style the popup.
206 * @param defStyleAttr Style attribute to read for default styling of popup content.
207 * @param defStyleRes Style resource ID to use for default styling of popup content.
208 */
209 public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
210 mContext = context;
211 mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes);
Adam Powell6f5e9342011-01-27 13:30:55 -0800212 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
Fabrice Di Meglio1d3d7da2012-07-27 15:15:04 -0700213 // Set the default layout direction to match the default locale one
214 final Locale locale = mContext.getResources().getConfiguration().locale;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -0700215 mLayoutDirection = TextUtils.getLayoutDirectionFromLocale(locale);
Adam Powellc3fa6302010-05-18 11:36:27 -0700216 }
217
218 /**
219 * Sets the adapter that provides the data and the views to represent the data
220 * in this popup window.
221 *
222 * @param adapter The adapter to use to create this window's content.
223 */
224 public void setAdapter(ListAdapter adapter) {
225 if (mObserver == null) {
226 mObserver = new PopupDataSetObserver();
227 } else if (mAdapter != null) {
Adam Powell99969da2010-06-16 10:51:30 -0700228 mAdapter.unregisterDataSetObserver(mObserver);
Adam Powellc3fa6302010-05-18 11:36:27 -0700229 }
230 mAdapter = adapter;
231 if (mAdapter != null) {
232 adapter.registerDataSetObserver(mObserver);
233 }
234
235 if (mDropDownList != null) {
236 mDropDownList.setAdapter(mAdapter);
237 }
238 }
239
240 /**
241 * Set where the optional prompt view should appear. The default is
242 * {@link #POSITION_PROMPT_ABOVE}.
243 *
244 * @param position A position constant declaring where the prompt should be displayed.
245 *
246 * @see #POSITION_PROMPT_ABOVE
247 * @see #POSITION_PROMPT_BELOW
248 */
249 public void setPromptPosition(int position) {
250 mPromptPosition = position;
251 }
252
253 /**
254 * @return Where the optional prompt view should appear.
255 *
256 * @see #POSITION_PROMPT_ABOVE
257 * @see #POSITION_PROMPT_BELOW
258 */
259 public int getPromptPosition() {
260 return mPromptPosition;
261 }
262
263 /**
264 * Set whether this window should be modal when shown.
265 *
266 * <p>If a popup window is modal, it will receive all touch and key input.
267 * If the user touches outside the popup window's content area the popup window
268 * will be dismissed.
269 *
270 * @param modal {@code true} if the popup window should be modal, {@code false} otherwise.
271 */
272 public void setModal(boolean modal) {
273 mModal = true;
274 mPopup.setFocusable(modal);
275 }
276
277 /**
278 * Returns whether the popup window will be modal when shown.
279 *
280 * @return {@code true} if the popup window will be modal, {@code false} otherwise.
281 */
282 public boolean isModal() {
283 return mModal;
284 }
285
286 /**
287 * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
288 * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
289 * ignore outside touch even when the drop down is not set to always visible.
290 *
291 * @hide Used only by AutoCompleteTextView to handle some internal special cases.
292 */
293 public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
294 mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch;
295 }
296
297 /**
298 * Sets whether the drop-down should remain visible under certain conditions.
299 *
300 * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless
301 * of the size or content of the list. {@link #getBackground()} will fill any space
302 * that is not used by the list.
303 *
304 * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
305 *
306 * @hide Only used by AutoCompleteTextView under special conditions.
307 */
308 public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
309 mDropDownAlwaysVisible = dropDownAlwaysVisible;
310 }
311
312 /**
313 * @return Whether the drop-down is visible under special conditions.
314 *
315 * @hide Only used by AutoCompleteTextView under special conditions.
316 */
317 public boolean isDropDownAlwaysVisible() {
318 return mDropDownAlwaysVisible;
319 }
320
321 /**
322 * Sets the operating mode for the soft input area.
323 *
324 * @param mode The desired mode, see
325 * {@link android.view.WindowManager.LayoutParams#softInputMode}
326 * for the full list
327 *
328 * @see android.view.WindowManager.LayoutParams#softInputMode
329 * @see #getSoftInputMode()
330 */
331 public void setSoftInputMode(int mode) {
332 mPopup.setSoftInputMode(mode);
333 }
334
335 /**
336 * Returns the current value in {@link #setSoftInputMode(int)}.
337 *
338 * @see #setSoftInputMode(int)
339 * @see android.view.WindowManager.LayoutParams#softInputMode
340 */
341 public int getSoftInputMode() {
342 return mPopup.getSoftInputMode();
343 }
344
345 /**
346 * Sets a drawable to use as the list item selector.
347 *
348 * @param selector List selector drawable to use in the popup.
349 */
350 public void setListSelector(Drawable selector) {
351 mDropDownListHighlight = selector;
352 }
353
354 /**
355 * @return The background drawable for the popup window.
356 */
357 public Drawable getBackground() {
358 return mPopup.getBackground();
359 }
360
361 /**
362 * Sets a drawable to be the background for the popup window.
363 *
364 * @param d A drawable to set as the background.
365 */
366 public void setBackgroundDrawable(Drawable d) {
367 mPopup.setBackgroundDrawable(d);
368 }
369
370 /**
371 * Set an animation style to use when the popup window is shown or dismissed.
372 *
373 * @param animationStyle Animation style to use.
374 */
375 public void setAnimationStyle(int animationStyle) {
376 mPopup.setAnimationStyle(animationStyle);
377 }
378
379 /**
380 * Returns the animation style that will be used when the popup window is
381 * shown or dismissed.
382 *
383 * @return Animation style that will be used.
384 */
385 public int getAnimationStyle() {
386 return mPopup.getAnimationStyle();
387 }
388
389 /**
390 * Returns the view that will be used to anchor this popup.
391 *
392 * @return The popup's anchor view
393 */
394 public View getAnchorView() {
395 return mDropDownAnchorView;
396 }
397
398 /**
399 * Sets the popup's anchor view. This popup will always be positioned relative to
400 * the anchor view when shown.
401 *
402 * @param anchor The view to use as an anchor.
403 */
404 public void setAnchorView(View anchor) {
405 mDropDownAnchorView = anchor;
406 }
407
408 /**
409 * @return The horizontal offset of the popup from its anchor in pixels.
410 */
411 public int getHorizontalOffset() {
412 return mDropDownHorizontalOffset;
413 }
414
415 /**
416 * Set the horizontal offset of this popup from its anchor view in pixels.
417 *
Adam Powella984b382010-06-04 14:20:10 -0700418 * @param offset The horizontal offset of the popup from its anchor.
Adam Powellc3fa6302010-05-18 11:36:27 -0700419 */
420 public void setHorizontalOffset(int offset) {
421 mDropDownHorizontalOffset = offset;
422 }
423
424 /**
425 * @return The vertical offset of the popup from its anchor in pixels.
426 */
427 public int getVerticalOffset() {
Adam Powell8132ba52011-07-15 17:37:11 -0700428 if (!mDropDownVerticalOffsetSet) {
429 return 0;
430 }
Adam Powellc3fa6302010-05-18 11:36:27 -0700431 return mDropDownVerticalOffset;
432 }
433
434 /**
435 * Set the vertical offset of this popup from its anchor view in pixels.
436 *
Adam Powella984b382010-06-04 14:20:10 -0700437 * @param offset The vertical offset of the popup from its anchor.
Adam Powellc3fa6302010-05-18 11:36:27 -0700438 */
439 public void setVerticalOffset(int offset) {
440 mDropDownVerticalOffset = offset;
Adam Powell8132ba52011-07-15 17:37:11 -0700441 mDropDownVerticalOffsetSet = true;
Adam Powellc3fa6302010-05-18 11:36:27 -0700442 }
443
444 /**
Adam Powell54c94de2013-09-26 15:36:34 -0700445 * Set the gravity of the dropdown list. This is commonly used to
446 * set gravity to START or END for alignment with the anchor.
447 *
448 * @param gravity Gravity value to use
449 */
450 public void setDropDownGravity(int gravity) {
451 mDropDownGravity = gravity;
452 }
453
454 /**
Adam Powellc3fa6302010-05-18 11:36:27 -0700455 * @return The width of the popup window in pixels.
456 */
457 public int getWidth() {
458 return mDropDownWidth;
459 }
460
461 /**
462 * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT}
463 * or {@link #WRAP_CONTENT}.
464 *
465 * @param width Width of the popup window.
466 */
467 public void setWidth(int width) {
468 mDropDownWidth = width;
469 }
470
471 /**
Adam Powell42675342010-07-09 18:02:59 -0700472 * Sets the width of the popup window by the size of its content. The final width may be
473 * larger to accommodate styled window dressing.
474 *
475 * @param width Desired width of content in pixels.
476 */
477 public void setContentWidth(int width) {
478 Drawable popupBackground = mPopup.getBackground();
479 if (popupBackground != null) {
Adam Powella39b9872011-01-05 16:07:54 -0800480 popupBackground.getPadding(mTempRect);
481 mDropDownWidth = mTempRect.left + mTempRect.right + width;
Adam Powell62e2bde2011-08-15 15:50:05 -0700482 } else {
483 setWidth(width);
Adam Powell42675342010-07-09 18:02:59 -0700484 }
485 }
486
487 /**
Adam Powellc3fa6302010-05-18 11:36:27 -0700488 * @return The height of the popup window in pixels.
489 */
490 public int getHeight() {
491 return mDropDownHeight;
492 }
493
494 /**
495 * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}.
496 *
497 * @param height Height of the popup window.
498 */
499 public void setHeight(int height) {
500 mDropDownHeight = height;
501 }
502
503 /**
504 * Sets a listener to receive events when a list item is clicked.
505 *
506 * @param clickListener Listener to register
507 *
508 * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener)
509 */
510 public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) {
511 mItemClickListener = clickListener;
512 }
513
514 /**
515 * Sets a listener to receive events when a list item is selected.
516 *
517 * @param selectedListener Listener to register.
518 *
519 * @see ListView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener)
520 */
521 public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener selectedListener) {
522 mItemSelectedListener = selectedListener;
523 }
524
525 /**
526 * Set a view to act as a user prompt for this popup window. Where the prompt view will appear
527 * is controlled by {@link #setPromptPosition(int)}.
528 *
529 * @param prompt View to use as an informational prompt.
530 */
531 public void setPromptView(View prompt) {
532 boolean showing = isShowing();
533 if (showing) {
534 removePromptView();
535 }
536 mPromptView = prompt;
537 if (showing) {
538 show();
539 }
540 }
541
542 /**
543 * Post a {@link #show()} call to the UI thread.
544 */
545 public void postShow() {
546 mHandler.post(mShowDropDownRunnable);
547 }
548
549 /**
550 * Show the popup list. If the list is already showing, this method
551 * will recalculate the popup's size and position.
552 */
553 public void show() {
554 int height = buildDropDown();
555
556 int widthSpec = 0;
557 int heightSpec = 0;
558
559 boolean noInputMethod = isInputMethodNotNeeded();
Adam Powell348e69c2011-02-16 16:49:50 -0800560 mPopup.setAllowScrollingAnchorParent(!noInputMethod);
Adam Powellc3fa6302010-05-18 11:36:27 -0700561
562 if (mPopup.isShowing()) {
563 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
564 // The call to PopupWindow's update method below can accept -1 for any
565 // value you do not want to update.
566 widthSpec = -1;
567 } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
568 widthSpec = getAnchorView().getWidth();
569 } else {
570 widthSpec = mDropDownWidth;
571 }
572
573 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
574 // The call to PopupWindow's update method below can accept -1 for any
575 // value you do not want to update.
576 heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT;
577 if (noInputMethod) {
578 mPopup.setWindowLayoutMode(
579 mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
580 ViewGroup.LayoutParams.MATCH_PARENT : 0, 0);
581 } else {
582 mPopup.setWindowLayoutMode(
583 mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
584 ViewGroup.LayoutParams.MATCH_PARENT : 0,
585 ViewGroup.LayoutParams.MATCH_PARENT);
586 }
587 } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
588 heightSpec = height;
589 } else {
590 heightSpec = mDropDownHeight;
591 }
592
593 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
594
595 mPopup.update(getAnchorView(), mDropDownHorizontalOffset,
596 mDropDownVerticalOffset, widthSpec, heightSpec);
597 } else {
598 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
599 widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
600 } else {
601 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
602 mPopup.setWidth(getAnchorView().getWidth());
603 } else {
604 mPopup.setWidth(mDropDownWidth);
605 }
606 }
607
608 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
609 heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
610 } else {
611 if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
612 mPopup.setHeight(height);
613 } else {
614 mPopup.setHeight(mDropDownHeight);
615 }
616 }
617
618 mPopup.setWindowLayoutMode(widthSpec, heightSpec);
Adam Powell56c2d332010-11-05 20:03:03 -0700619 mPopup.setClipToScreenEnabled(true);
Adam Powellc3fa6302010-05-18 11:36:27 -0700620
621 // use outside touchable to dismiss drop down when touching outside of it, so
622 // only set this if the dropdown is not always visible
623 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
624 mPopup.setTouchInterceptor(mTouchInterceptor);
625 mPopup.showAsDropDown(getAnchorView(),
Adam Powell54c94de2013-09-26 15:36:34 -0700626 mDropDownHorizontalOffset, mDropDownVerticalOffset, mDropDownGravity);
Adam Powellc3fa6302010-05-18 11:36:27 -0700627 mDropDownList.setSelection(ListView.INVALID_POSITION);
628
629 if (!mModal || mDropDownList.isInTouchMode()) {
630 clearListSelection();
631 }
632 if (!mModal) {
633 mHandler.post(mHideSelector);
634 }
635 }
636 }
637
638 /**
639 * Dismiss the popup window.
640 */
641 public void dismiss() {
642 mPopup.dismiss();
643 removePromptView();
644 mPopup.setContentView(null);
645 mDropDownList = null;
Adam Powellca51e872011-02-14 19:54:29 -0800646 mHandler.removeCallbacks(mResizePopupRunnable);
Adam Powellc3fa6302010-05-18 11:36:27 -0700647 }
648
Adam Powell6c6f5752010-08-20 18:34:46 -0700649 /**
650 * Set a listener to receive a callback when the popup is dismissed.
651 *
652 * @param listener Listener that will be notified when the popup is dismissed.
653 */
654 public void setOnDismissListener(PopupWindow.OnDismissListener listener) {
655 mPopup.setOnDismissListener(listener);
656 }
657
Adam Powellc3fa6302010-05-18 11:36:27 -0700658 private void removePromptView() {
659 if (mPromptView != null) {
660 final ViewParent parent = mPromptView.getParent();
661 if (parent instanceof ViewGroup) {
662 final ViewGroup group = (ViewGroup) parent;
663 group.removeView(mPromptView);
664 }
665 }
666 }
667
668 /**
669 * Control how the popup operates with an input method: one of
670 * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED},
671 * or {@link #INPUT_METHOD_NOT_NEEDED}.
672 *
673 * <p>If the popup is showing, calling this method will take effect only
674 * the next time the popup is shown or through a manual call to the {@link #show()}
675 * method.</p>
676 *
677 * @see #getInputMethodMode()
678 * @see #show()
679 */
680 public void setInputMethodMode(int mode) {
681 mPopup.setInputMethodMode(mode);
682 }
683
684 /**
685 * Return the current value in {@link #setInputMethodMode(int)}.
686 *
687 * @see #setInputMethodMode(int)
688 */
689 public int getInputMethodMode() {
690 return mPopup.getInputMethodMode();
691 }
692
693 /**
694 * Set the selected position of the list.
695 * Only valid when {@link #isShowing()} == {@code true}.
696 *
697 * @param position List position to set as selected.
698 */
699 public void setSelection(int position) {
700 DropDownListView list = mDropDownList;
701 if (isShowing() && list != null) {
702 list.mListSelectionHidden = false;
703 list.setSelection(position);
704 if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) {
705 list.setItemChecked(position, true);
706 }
707 }
708 }
709
710 /**
711 * Clear any current list selection.
712 * Only valid when {@link #isShowing()} == {@code true}.
713 */
714 public void clearListSelection() {
715 final DropDownListView list = mDropDownList;
716 if (list != null) {
717 // WARNING: Please read the comment where mListSelectionHidden is declared
718 list.mListSelectionHidden = true;
719 list.hideSelector();
720 list.requestLayout();
721 }
722 }
723
724 /**
725 * @return {@code true} if the popup is currently showing, {@code false} otherwise.
726 */
727 public boolean isShowing() {
728 return mPopup.isShowing();
729 }
730
731 /**
732 * @return {@code true} if this popup is configured to assume the user does not need
733 * to interact with the IME while it is showing, {@code false} otherwise.
734 */
735 public boolean isInputMethodNotNeeded() {
736 return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED;
737 }
738
739 /**
740 * Perform an item click operation on the specified list adapter position.
741 *
742 * @param position Adapter position for performing the click
743 * @return true if the click action could be performed, false if not.
744 * (e.g. if the popup was not showing, this method would return false.)
745 */
746 public boolean performItemClick(int position) {
747 if (isShowing()) {
748 if (mItemClickListener != null) {
749 final DropDownListView list = mDropDownList;
750 final View child = list.getChildAt(position - list.getFirstVisiblePosition());
Adam Powellcdee4462010-09-02 17:13:24 -0700751 final ListAdapter adapter = list.getAdapter();
752 mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position));
Adam Powellc3fa6302010-05-18 11:36:27 -0700753 }
754 return true;
755 }
756 return false;
757 }
758
759 /**
760 * @return The currently selected item or null if the popup is not showing.
761 */
762 public Object getSelectedItem() {
763 if (!isShowing()) {
764 return null;
765 }
766 return mDropDownList.getSelectedItem();
767 }
768
769 /**
770 * @return The position of the currently selected item or {@link ListView#INVALID_POSITION}
771 * if {@link #isShowing()} == {@code false}.
772 *
773 * @see ListView#getSelectedItemPosition()
774 */
775 public int getSelectedItemPosition() {
776 if (!isShowing()) {
777 return ListView.INVALID_POSITION;
778 }
779 return mDropDownList.getSelectedItemPosition();
780 }
781
782 /**
783 * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID}
784 * if {@link #isShowing()} == {@code false}.
785 *
786 * @see ListView#getSelectedItemId()
787 */
788 public long getSelectedItemId() {
789 if (!isShowing()) {
790 return ListView.INVALID_ROW_ID;
791 }
792 return mDropDownList.getSelectedItemId();
793 }
794
795 /**
796 * @return The View for the currently selected item or null if
797 * {@link #isShowing()} == {@code false}.
798 *
799 * @see ListView#getSelectedView()
800 */
801 public View getSelectedView() {
802 if (!isShowing()) {
803 return null;
804 }
805 return mDropDownList.getSelectedView();
806 }
807
808 /**
809 * @return The {@link ListView} displayed within the popup window.
810 * Only valid when {@link #isShowing()} == {@code true}.
811 */
812 public ListView getListView() {
813 return mDropDownList;
814 }
815
816 /**
Adam Powell348e69c2011-02-16 16:49:50 -0800817 * The maximum number of list items that can be visible and still have
818 * the list expand when touched.
819 *
820 * @param max Max number of items that can be visible and still allow the list to expand.
821 */
822 void setListItemExpandMax(int max) {
823 mListItemExpandMaximum = max;
824 }
825
826 /**
Jeff Brown8d6d3b82011-01-26 19:31:18 -0800827 * Filter key down events. By forwarding key down events to this function,
Adam Powellc3fa6302010-05-18 11:36:27 -0700828 * views using non-modal ListPopupWindow can have it handle key selection of items.
829 *
830 * @param keyCode keyCode param passed to the host view's onKeyDown
831 * @param event event param passed to the host view's onKeyDown
832 * @return true if the event was handled, false if it was ignored.
833 *
834 * @see #setModal(boolean)
835 */
836 public boolean onKeyDown(int keyCode, KeyEvent event) {
837 // when the drop down is shown, we drive it directly
838 if (isShowing()) {
839 // the key events are forwarded to the list in the drop down view
840 // note that ListView handles space but we don't want that to happen
841 // also if selection is not currently in the drop down, then don't
842 // let center or enter presses go there since that would cause it
843 // to select one of its items
844 if (keyCode != KeyEvent.KEYCODE_SPACE
845 && (mDropDownList.getSelectedItemPosition() >= 0
Michael Wright24d36f52013-07-19 15:55:14 -0700846 || !KeyEvent.isConfirmKey(keyCode))) {
Adam Powellc3fa6302010-05-18 11:36:27 -0700847 int curIndex = mDropDownList.getSelectedItemPosition();
848 boolean consumed;
849
850 final boolean below = !mPopup.isAboveAnchor();
851
852 final ListAdapter adapter = mAdapter;
853
854 boolean allEnabled;
855 int firstItem = Integer.MAX_VALUE;
856 int lastItem = Integer.MIN_VALUE;
857
858 if (adapter != null) {
859 allEnabled = adapter.areAllItemsEnabled();
860 firstItem = allEnabled ? 0 :
861 mDropDownList.lookForSelectablePosition(0, true);
862 lastItem = allEnabled ? adapter.getCount() - 1 :
863 mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false);
864 }
865
866 if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) ||
867 (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) {
868 // When the selection is at the top, we block the key
869 // event to prevent focus from moving.
870 clearListSelection();
871 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
872 show();
873 return true;
874 } else {
875 // WARNING: Please read the comment where mListSelectionHidden
876 // is declared
877 mDropDownList.mListSelectionHidden = false;
878 }
879
880 consumed = mDropDownList.onKeyDown(keyCode, event);
881 if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed);
882
883 if (consumed) {
884 // If it handled the key event, then the user is
885 // navigating in the list, so we should put it in front.
886 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
887 // Here's a little trick we need to do to make sure that
888 // the list view is actually showing its focus indicator,
889 // by ensuring it has focus and getting its window out
890 // of touch mode.
891 mDropDownList.requestFocusFromTouch();
892 show();
893
894 switch (keyCode) {
895 // avoid passing the focus from the text view to the
896 // next component
897 case KeyEvent.KEYCODE_ENTER:
898 case KeyEvent.KEYCODE_DPAD_CENTER:
899 case KeyEvent.KEYCODE_DPAD_DOWN:
900 case KeyEvent.KEYCODE_DPAD_UP:
901 return true;
902 }
903 } else {
904 if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
905 // when the selection is at the bottom, we block the
906 // event to avoid going to the next focusable widget
907 if (curIndex == lastItem) {
908 return true;
909 }
910 } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP &&
911 curIndex == firstItem) {
912 return true;
913 }
914 }
915 }
916 }
917
918 return false;
919 }
920
921 /**
922 * Filter key down events. By forwarding key up events to this function,
923 * views using non-modal ListPopupWindow can have it handle key selection of items.
924 *
925 * @param keyCode keyCode param passed to the host view's onKeyUp
926 * @param event event param passed to the host view's onKeyUp
927 * @return true if the event was handled, false if it was ignored.
928 *
929 * @see #setModal(boolean)
930 */
931 public boolean onKeyUp(int keyCode, KeyEvent event) {
932 if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) {
933 boolean consumed = mDropDownList.onKeyUp(keyCode, event);
Michael Wright24d36f52013-07-19 15:55:14 -0700934 if (consumed && KeyEvent.isConfirmKey(keyCode)) {
935 // if the list accepts the key events and the key event was a click, the text view
936 // gets the selected item from the drop down as its content
937 dismiss();
Adam Powellc3fa6302010-05-18 11:36:27 -0700938 }
939 return consumed;
940 }
941 return false;
942 }
943
944 /**
945 * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)}
946 * events to this function, views using ListPopupWindow can have it dismiss the popup
947 * when the back key is pressed.
948 *
949 * @param keyCode keyCode param passed to the host view's onKeyPreIme
950 * @param event event param passed to the host view's onKeyPreIme
951 * @return true if the event was handled, false if it was ignored.
952 *
953 * @see #setModal(boolean)
954 */
955 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
956 if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) {
957 // special case for the back key, we do not even try to send it
958 // to the drop down list but instead, consume it immediately
959 final View anchorView = mDropDownAnchorView;
960 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
Jeff Brownb3ea9222011-01-10 16:26:36 -0800961 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
962 if (state != null) {
963 state.startTracking(event, this);
964 }
Adam Powellc3fa6302010-05-18 11:36:27 -0700965 return true;
966 } else if (event.getAction() == KeyEvent.ACTION_UP) {
Jeff Brownb3ea9222011-01-10 16:26:36 -0800967 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
968 if (state != null) {
969 state.handleUpEvent(event);
970 }
Adam Powellc3fa6302010-05-18 11:36:27 -0700971 if (event.isTracking() && !event.isCanceled()) {
972 dismiss();
973 return true;
974 }
975 }
976 }
977 return false;
978 }
979
980 /**
Alan Viverette1955a5b52013-08-27 15:45:16 -0700981 * Returns an {@link OnTouchListener} that can be added to the source view
982 * to implement drag-to-open behavior. Generally, the source view should be
983 * the same view that was passed to {@link #setAnchorView}.
984 * <p>
985 * When the listener is set on a view, touching that view and dragging
986 * outside of its bounds will open the popup window. Lifting will select the
987 * currently touched list item.
988 * <p>
989 * Example usage:
Alan Viverette3f9832d2013-08-30 14:43:25 -0700990 * <pre>
991 * ListPopupWindow myPopup = new ListPopupWindow(context);
Alan Viverette1955a5b52013-08-27 15:45:16 -0700992 * myPopup.setAnchor(myAnchor);
993 * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor);
Alan Viverette3f9832d2013-08-30 14:43:25 -0700994 * myAnchor.setOnTouchListener(dragListener);
995 * </pre>
Alan Viverette1955a5b52013-08-27 15:45:16 -0700996 *
997 * @param src the view on which the resulting listener will be set
998 * @return a touch listener that controls drag-to-open behavior
999 */
1000 public OnTouchListener createDragToOpenListener(View src) {
1001 return new ForwardingListener(src) {
1002 @Override
1003 public ListPopupWindow getPopup() {
1004 return ListPopupWindow.this;
1005 }
1006 };
1007 }
1008
1009 /**
Adam Powellc3fa6302010-05-18 11:36:27 -07001010 * <p>Builds the popup window's content and returns the height the popup
1011 * should have. Returns -1 when the content already exists.</p>
1012 *
1013 * @return the content's height or -1 if content already exists
1014 */
1015 private int buildDropDown() {
1016 ViewGroup dropDownView;
1017 int otherHeights = 0;
1018
1019 if (mDropDownList == null) {
1020 Context context = mContext;
1021
1022 /**
1023 * This Runnable exists for the sole purpose of checking if the view layout has got
1024 * completed and if so call showDropDown to display the drop down. This is used to show
1025 * the drop down as soon as possible after user opens up the search dialog, without
1026 * waiting for the normal UI pipeline to do it's job which is slower than this method.
1027 */
1028 mShowDropDownRunnable = new Runnable() {
1029 public void run() {
1030 // View layout should be all done before displaying the drop down.
1031 View view = getAnchorView();
1032 if (view != null && view.getWindowToken() != null) {
1033 show();
1034 }
1035 }
1036 };
1037
1038 mDropDownList = new DropDownListView(context, !mModal);
1039 if (mDropDownListHighlight != null) {
1040 mDropDownList.setSelector(mDropDownListHighlight);
1041 }
1042 mDropDownList.setAdapter(mAdapter);
Adam Powellc3fa6302010-05-18 11:36:27 -07001043 mDropDownList.setOnItemClickListener(mItemClickListener);
1044 mDropDownList.setFocusable(true);
1045 mDropDownList.setFocusableInTouchMode(true);
1046 mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
1047 public void onItemSelected(AdapterView<?> parent, View view,
1048 int position, long id) {
1049
1050 if (position != -1) {
1051 DropDownListView dropDownList = mDropDownList;
1052
1053 if (dropDownList != null) {
1054 dropDownList.mListSelectionHidden = false;
1055 }
1056 }
1057 }
1058
1059 public void onNothingSelected(AdapterView<?> parent) {
1060 }
1061 });
1062 mDropDownList.setOnScrollListener(mScrollListener);
1063
1064 if (mItemSelectedListener != null) {
1065 mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
1066 }
1067
1068 dropDownView = mDropDownList;
1069
1070 View hintView = mPromptView;
1071 if (hintView != null) {
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001072 // if a hint has been specified, we accomodate more space for it and
Adam Powellc3fa6302010-05-18 11:36:27 -07001073 // add a text view in the drop down menu, at the bottom of the list
1074 LinearLayout hintContainer = new LinearLayout(context);
1075 hintContainer.setOrientation(LinearLayout.VERTICAL);
1076
1077 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
1078 ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
1079 );
1080
1081 switch (mPromptPosition) {
1082 case POSITION_PROMPT_BELOW:
1083 hintContainer.addView(dropDownView, hintParams);
1084 hintContainer.addView(hintView);
1085 break;
1086
1087 case POSITION_PROMPT_ABOVE:
1088 hintContainer.addView(hintView);
1089 hintContainer.addView(dropDownView, hintParams);
1090 break;
1091
1092 default:
1093 Log.e(TAG, "Invalid hint position " + mPromptPosition);
1094 break;
1095 }
1096
1097 // measure the hint's height to find how much more vertical space
1098 // we need to add to the drop down's height
1099 int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST);
1100 int heightSpec = MeasureSpec.UNSPECIFIED;
1101 hintView.measure(widthSpec, heightSpec);
1102
1103 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
1104 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
1105 + hintParams.bottomMargin;
1106
1107 dropDownView = hintContainer;
1108 }
1109
1110 mPopup.setContentView(dropDownView);
1111 } else {
1112 dropDownView = (ViewGroup) mPopup.getContentView();
1113 final View view = mPromptView;
1114 if (view != null) {
1115 LinearLayout.LayoutParams hintParams =
1116 (LinearLayout.LayoutParams) view.getLayoutParams();
1117 otherHeights = view.getMeasuredHeight() + hintParams.topMargin
1118 + hintParams.bottomMargin;
1119 }
1120 }
1121
Adam Powell8132ba52011-07-15 17:37:11 -07001122 // getMaxAvailableHeight() subtracts the padding, so we put it back
Adam Powellc3fa6302010-05-18 11:36:27 -07001123 // to get the available height for the whole window
1124 int padding = 0;
1125 Drawable background = mPopup.getBackground();
1126 if (background != null) {
1127 background.getPadding(mTempRect);
1128 padding = mTempRect.top + mTempRect.bottom;
Adam Powell8132ba52011-07-15 17:37:11 -07001129
1130 // If we don't have an explicit vertical offset, determine one from the window
1131 // background so that content will line up.
1132 if (!mDropDownVerticalOffsetSet) {
1133 mDropDownVerticalOffset = -mTempRect.top;
1134 }
Adam Powell7507d3d2012-03-08 12:01:16 -08001135 } else {
1136 mTempRect.setEmpty();
Adam Powellc3fa6302010-05-18 11:36:27 -07001137 }
1138
Adam Powell8132ba52011-07-15 17:37:11 -07001139 // Max height available on the screen for a popup.
1140 boolean ignoreBottomDecorations =
1141 mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
1142 final int maxHeight = mPopup.getMaxAvailableHeight(
1143 getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations);
1144
Adam Powellc3fa6302010-05-18 11:36:27 -07001145 if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
1146 return maxHeight + padding;
1147 }
1148
Adam Powell7507d3d2012-03-08 12:01:16 -08001149 final int childWidthSpec;
1150 switch (mDropDownWidth) {
1151 case ViewGroup.LayoutParams.WRAP_CONTENT:
1152 childWidthSpec = MeasureSpec.makeMeasureSpec(
1153 mContext.getResources().getDisplayMetrics().widthPixels -
1154 (mTempRect.left + mTempRect.right),
1155 MeasureSpec.AT_MOST);
1156 break;
1157 case ViewGroup.LayoutParams.MATCH_PARENT:
1158 childWidthSpec = MeasureSpec.makeMeasureSpec(
1159 mContext.getResources().getDisplayMetrics().widthPixels -
1160 (mTempRect.left + mTempRect.right),
1161 MeasureSpec.EXACTLY);
1162 break;
1163 default:
1164 childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY);
1165 break;
1166 }
1167 final int listContent = mDropDownList.measureHeightOfChildren(childWidthSpec,
Adam Powella7845ed2011-08-07 15:48:03 -07001168 0, ListView.NO_POSITION, maxHeight - otherHeights, -1);
Adam Powellc3fa6302010-05-18 11:36:27 -07001169 // add padding only if the list has items in it, that way we don't show
1170 // the popup if it is not needed
1171 if (listContent > 0) otherHeights += padding;
1172
1173 return listContent + otherHeights;
1174 }
1175
Fabrice Di Meglio1d3d7da2012-07-27 15:15:04 -07001176 /**
Alan Viveretteca6a36112013-08-16 14:41:06 -07001177 * Abstract class that forwards touch events to a {@link ListPopupWindow}.
1178 *
1179 * @hide
1180 */
Alan Viverette69960142013-08-22 17:26:57 -07001181 public static abstract class ForwardingListener
1182 implements View.OnTouchListener, View.OnAttachStateChangeListener {
Alan Viveretteca6a36112013-08-16 14:41:06 -07001183 /** Scaled touch slop, used for detecting movement outside bounds. */
1184 private final float mScaledTouchSlop;
1185
Alan Viverette69960142013-08-22 17:26:57 -07001186 /** Timeout before disallowing intercept on the source's parent. */
1187 private final int mTapTimeout;
1188
1189 /** Source view from which events are forwarded. */
1190 private final View mSrc;
1191
1192 /** Runnable used to prevent conflicts with scrolling parents. */
1193 private Runnable mDisallowIntercept;
1194
Alan Viveretteca6a36112013-08-16 14:41:06 -07001195 /** Whether this listener is currently forwarding touch events. */
1196 private boolean mForwarding;
1197
1198 /** The id of the first pointer down in the current event stream. */
1199 private int mActivePointerId;
1200
Alan Viverette69960142013-08-22 17:26:57 -07001201 public ForwardingListener(View src) {
1202 mSrc = src;
1203 mScaledTouchSlop = ViewConfiguration.get(src.getContext()).getScaledTouchSlop();
1204 mTapTimeout = ViewConfiguration.getTapTimeout();
1205
1206 src.addOnAttachStateChangeListener(this);
Alan Viveretteca6a36112013-08-16 14:41:06 -07001207 }
1208
1209 /**
1210 * Returns the popup to which this listener is forwarding events.
1211 * <p>
1212 * Override this to return the correct popup. If the popup is displayed
1213 * asynchronously, you may also need to override
1214 * {@link #onForwardingStopped} to prevent premature cancelation of
1215 * forwarding.
1216 *
1217 * @return the popup to which this listener is forwarding events
1218 */
1219 public abstract ListPopupWindow getPopup();
1220
1221 @Override
1222 public boolean onTouch(View v, MotionEvent event) {
1223 final boolean wasForwarding = mForwarding;
1224 final boolean forwarding;
1225 if (wasForwarding) {
Alan Viverette69960142013-08-22 17:26:57 -07001226 forwarding = onTouchForwarded(event) || !onForwardingStopped();
Alan Viveretteca6a36112013-08-16 14:41:06 -07001227 } else {
Alan Viverette69960142013-08-22 17:26:57 -07001228 forwarding = onTouchObserved(event) && onForwardingStarted();
Alan Viveretteca6a36112013-08-16 14:41:06 -07001229 }
1230
1231 mForwarding = forwarding;
1232 return forwarding || wasForwarding;
1233 }
1234
Alan Viverette69960142013-08-22 17:26:57 -07001235 @Override
1236 public void onViewAttachedToWindow(View v) {
1237 }
1238
1239 @Override
1240 public void onViewDetachedFromWindow(View v) {
1241 mForwarding = false;
1242 mActivePointerId = MotionEvent.INVALID_POINTER_ID;
1243
1244 if (mDisallowIntercept != null) {
1245 mSrc.removeCallbacks(mDisallowIntercept);
1246 }
1247 }
1248
Alan Viveretteca6a36112013-08-16 14:41:06 -07001249 /**
1250 * Called when forwarding would like to start.
1251 * <p>
1252 * By default, this will show the popup returned by {@link #getPopup()}.
1253 * It may be overridden to perform another action, like clicking the
1254 * source view or preparing the popup before showing it.
1255 *
1256 * @return true to start forwarding, false otherwise
1257 */
Alan Viverette69960142013-08-22 17:26:57 -07001258 protected boolean onForwardingStarted() {
Alan Viveretteca6a36112013-08-16 14:41:06 -07001259 final ListPopupWindow popup = getPopup();
1260 if (popup != null && !popup.isShowing()) {
1261 popup.show();
1262 }
1263 return true;
1264 }
1265
1266 /**
1267 * Called when forwarding would like to stop.
1268 * <p>
1269 * By default, this will dismiss the popup returned by
1270 * {@link #getPopup()}. It may be overridden to perform some other
1271 * action.
1272 *
1273 * @return true to stop forwarding, false otherwise
1274 */
Alan Viverette69960142013-08-22 17:26:57 -07001275 protected boolean onForwardingStopped() {
Alan Viveretteca6a36112013-08-16 14:41:06 -07001276 final ListPopupWindow popup = getPopup();
1277 if (popup != null && popup.isShowing()) {
1278 popup.dismiss();
1279 }
1280 return true;
1281 }
1282
1283 /**
1284 * Observes motion events and determines when to start forwarding.
1285 *
Alan Viveretteca6a36112013-08-16 14:41:06 -07001286 * @param srcEvent motion event in source view coordinates
1287 * @return true to start forwarding motion events, false otherwise
1288 */
Alan Viverette69960142013-08-22 17:26:57 -07001289 private boolean onTouchObserved(MotionEvent srcEvent) {
1290 final View src = mSrc;
Alan Viveretteca6a36112013-08-16 14:41:06 -07001291 if (!src.isEnabled()) {
1292 return false;
1293 }
1294
Alan Viveretteca6a36112013-08-16 14:41:06 -07001295 final int actionMasked = srcEvent.getActionMasked();
Alan Viverette69960142013-08-22 17:26:57 -07001296 switch (actionMasked) {
1297 case MotionEvent.ACTION_DOWN:
1298 mActivePointerId = srcEvent.getPointerId(0);
1299 if (mDisallowIntercept == null) {
1300 mDisallowIntercept = new DisallowIntercept();
1301 }
1302 src.postDelayed(mDisallowIntercept, mTapTimeout);
1303 break;
1304 case MotionEvent.ACTION_MOVE:
1305 final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId);
1306 if (activePointerIndex >= 0) {
1307 final float x = srcEvent.getX(activePointerIndex);
1308 final float y = srcEvent.getY(activePointerIndex);
1309 if (!src.pointInView(x, y, mScaledTouchSlop)) {
1310 // The pointer has moved outside of the view.
1311 if (mDisallowIntercept != null) {
1312 src.removeCallbacks(mDisallowIntercept);
1313 }
1314 src.getParent().requestDisallowInterceptTouchEvent(true);
1315 return true;
1316 }
1317 }
1318 break;
1319 case MotionEvent.ACTION_CANCEL:
1320 case MotionEvent.ACTION_UP:
1321 if (mDisallowIntercept != null) {
1322 src.removeCallbacks(mDisallowIntercept);
1323 }
1324 break;
Alan Viveretteca6a36112013-08-16 14:41:06 -07001325 }
1326
1327 return false;
1328 }
1329
1330 /**
1331 * Handled forwarded motion events and determines when to stop
1332 * forwarding.
1333 *
Alan Viveretteca6a36112013-08-16 14:41:06 -07001334 * @param srcEvent motion event in source view coordinates
1335 * @return true to continue forwarding motion events, false to cancel
1336 */
Alan Viverette69960142013-08-22 17:26:57 -07001337 private boolean onTouchForwarded(MotionEvent srcEvent) {
1338 final View src = mSrc;
Alan Viveretteca6a36112013-08-16 14:41:06 -07001339 final ListPopupWindow popup = getPopup();
1340 if (popup == null || !popup.isShowing()) {
1341 return false;
1342 }
1343
1344 final DropDownListView dst = popup.mDropDownList;
1345 if (dst == null || !dst.isShown()) {
1346 return false;
1347 }
1348
1349 // Convert event to destination-local coordinates.
1350 final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
1351 src.toGlobalMotionEvent(dstEvent);
1352 dst.toLocalMotionEvent(dstEvent);
1353
1354 // Forward converted event to destination view, then recycle it.
1355 final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId);
1356 dstEvent.recycle();
1357 return handled;
1358 }
Alan Viverette69960142013-08-22 17:26:57 -07001359
1360 private class DisallowIntercept implements Runnable {
1361 @Override
1362 public void run() {
1363 final ViewParent parent = mSrc.getParent();
1364 parent.requestDisallowInterceptTouchEvent(true);
1365 }
1366 }
Alan Viveretteca6a36112013-08-16 14:41:06 -07001367 }
1368
1369 /**
Adam Powellc3fa6302010-05-18 11:36:27 -07001370 * <p>Wrapper class for a ListView. This wrapper can hijack the focus to
1371 * make sure the list uses the appropriate drawables and states when
1372 * displayed on screen within a drop down. The focus is never actually
1373 * passed to the drop down in this mode; the list only looks focused.</p>
1374 */
1375 private static class DropDownListView extends ListView {
Alan Viverettec0502722013-08-15 18:05:52 -07001376 /** Duration in milliseconds of the drag-to-open click animation. */
1377 private static final long CLICK_ANIM_DURATION = 150;
1378
1379 /** Target alpha value for drag-to-open click animation. */
1380 private static final int CLICK_ANIM_ALPHA = 0x80;
1381
1382 /** Wrapper around Drawable's <code>alpha</code> property. */
1383 private static final IntProperty<Drawable> DRAWABLE_ALPHA =
1384 new IntProperty<Drawable>("alpha") {
1385 @Override
1386 public void setValue(Drawable object, int value) {
1387 object.setAlpha(value);
1388 }
1389
1390 @Override
1391 public Integer get(Drawable object) {
1392 return object.getAlpha();
1393 }
1394 };
1395
Adam Powellc3fa6302010-05-18 11:36:27 -07001396 /*
1397 * WARNING: This is a workaround for a touch mode issue.
1398 *
1399 * Touch mode is propagated lazily to windows. This causes problems in
1400 * the following scenario:
1401 * - Type something in the AutoCompleteTextView and get some results
1402 * - Move down with the d-pad to select an item in the list
1403 * - Move up with the d-pad until the selection disappears
1404 * - Type more text in the AutoCompleteTextView *using the soft keyboard*
1405 * and get new results; you are now in touch mode
1406 * - The selection comes back on the first item in the list, even though
1407 * the list is supposed to be in touch mode
1408 *
1409 * Using the soft keyboard triggers the touch mode change but that change
1410 * is propagated to our window only after the first list layout, therefore
1411 * after the list attempts to resurrect the selection.
1412 *
1413 * The trick to work around this issue is to pretend the list is in touch
1414 * mode when we know that the selection should not appear, that is when
1415 * we know the user moved the selection away from the list.
1416 *
1417 * This boolean is set to true whenever we explicitly hide the list's
1418 * selection and reset to false whenever we know the user moved the
1419 * selection back to the list.
1420 *
1421 * When this boolean is true, isInTouchMode() returns true, otherwise it
1422 * returns super.isInTouchMode().
1423 */
1424 private boolean mListSelectionHidden;
1425
1426 /**
1427 * True if this wrapper should fake focus.
1428 */
1429 private boolean mHijackFocus;
1430
Alan Viverettec0502722013-08-15 18:05:52 -07001431 /** Whether to force drawing of the pressed state selector. */
1432 private boolean mDrawsInPressedState;
1433
1434 /** Current drag-to-open click animation, if any. */
1435 private Animator mClickAnimation;
1436
Alan Viverette5e660212013-08-21 13:21:45 -07001437 /** Helper for drag-to-open auto scrolling. */
1438 private AbsListViewAutoScroller mScrollHelper;
1439
Adam Powellc3fa6302010-05-18 11:36:27 -07001440 /**
1441 * <p>Creates a new list view wrapper.</p>
1442 *
1443 * @param context this view's context
1444 */
1445 public DropDownListView(Context context, boolean hijackFocus) {
1446 super(context, null, com.android.internal.R.attr.dropDownListViewStyle);
1447 mHijackFocus = hijackFocus;
Amith Yamasanib1818e82010-10-20 10:06:08 -07001448 // TODO: Add an API to control this
1449 setCacheColorHint(0); // Transparent, since the background drawable could be anything.
Adam Powellc3fa6302010-05-18 11:36:27 -07001450 }
1451
1452 /**
Alan Viverettec0502722013-08-15 18:05:52 -07001453 * Handles forwarded events.
1454 *
1455 * @param activePointerId id of the pointer that activated forwarding
1456 * @return whether the event was handled
1457 */
1458 public boolean onForwardedEvent(MotionEvent event, int activePointerId) {
1459 boolean handledEvent = true;
1460 boolean clearPressedItem = false;
1461
1462 final int actionMasked = event.getActionMasked();
1463 switch (actionMasked) {
1464 case MotionEvent.ACTION_CANCEL:
1465 handledEvent = false;
1466 break;
1467 case MotionEvent.ACTION_UP:
1468 handledEvent = false;
1469 // $FALL-THROUGH$
1470 case MotionEvent.ACTION_MOVE:
1471 final int activeIndex = event.findPointerIndex(activePointerId);
1472 if (activeIndex < 0) {
1473 handledEvent = false;
1474 break;
1475 }
1476
1477 final int x = (int) event.getX(activeIndex);
1478 final int y = (int) event.getY(activeIndex);
1479 final int position = pointToPosition(x, y);
1480 if (position == INVALID_POSITION) {
1481 clearPressedItem = true;
1482 break;
1483 }
1484
1485 final View child = getChildAt(position - getFirstVisiblePosition());
1486 setPressedItem(child, position);
1487 handledEvent = true;
1488
1489 if (actionMasked == MotionEvent.ACTION_UP) {
1490 clickPressedItem(child, position);
1491 }
1492 break;
1493 }
1494
1495 // Failure to handle the event cancels forwarding.
1496 if (!handledEvent || clearPressedItem) {
1497 clearPressedItem();
1498 }
1499
Alan Viverette5e660212013-08-21 13:21:45 -07001500 // Manage automatic scrolling.
1501 if (handledEvent) {
1502 if (mScrollHelper == null) {
1503 mScrollHelper = new AbsListViewAutoScroller(this);
1504 }
1505 mScrollHelper.setEnabled(true);
1506 mScrollHelper.onTouch(this, event);
1507 } else if (mScrollHelper != null) {
1508 mScrollHelper.setEnabled(false);
1509 }
1510
Alan Viverettec0502722013-08-15 18:05:52 -07001511 return handledEvent;
1512 }
1513
1514 /**
1515 * Starts an alpha animation on the selector. When the animation ends,
1516 * the list performs a click on the item.
1517 */
1518 private void clickPressedItem(final View child, final int position) {
1519 final long id = getItemIdAtPosition(position);
1520 final Animator anim = ObjectAnimator.ofInt(
1521 mSelector, DRAWABLE_ALPHA, 0xFF, CLICK_ANIM_ALPHA, 0xFF);
1522 anim.setDuration(CLICK_ANIM_DURATION);
1523 anim.setInterpolator(new AccelerateDecelerateInterpolator());
1524 anim.addListener(new AnimatorListenerAdapter() {
1525 @Override
1526 public void onAnimationEnd(Animator animation) {
1527 performItemClick(child, position, id);
1528 }
1529 });
1530 anim.start();
1531
1532 if (mClickAnimation != null) {
1533 mClickAnimation.cancel();
1534 }
1535 mClickAnimation = anim;
1536 }
1537
1538 private void clearPressedItem() {
1539 mDrawsInPressedState = false;
1540 setPressed(false);
1541 updateSelectorState();
1542
1543 if (mClickAnimation != null) {
1544 mClickAnimation.cancel();
1545 mClickAnimation = null;
1546 }
1547 }
1548
1549 private void setPressedItem(View child, int position) {
1550 mDrawsInPressedState = true;
1551
1552 // Ordering is essential. First update the pressed state and layout
1553 // the children. This will ensure the selector actually gets drawn.
1554 setPressed(true);
1555 layoutChildren();
1556
1557 // Ensure that keyboard focus starts from the last touched position.
1558 setSelectedPositionInt(position);
1559 positionSelector(position, child);
1560
1561 // Refresh the drawable state to reflect the new pressed state,
1562 // which will also update the selector state.
1563 refreshDrawableState();
1564
1565 if (mClickAnimation != null) {
1566 mClickAnimation.cancel();
1567 mClickAnimation = null;
1568 }
1569 }
1570
1571 @Override
1572 boolean touchModeDrawsInPressedState() {
1573 return mDrawsInPressedState || super.touchModeDrawsInPressedState();
1574 }
1575
1576 /**
Adam Powellc3fa6302010-05-18 11:36:27 -07001577 * <p>Avoids jarring scrolling effect by ensuring that list elements
1578 * made of a text view fit on a single line.</p>
1579 *
1580 * @param position the item index in the list to get a view for
1581 * @return the view for the specified item
1582 */
1583 @Override
1584 View obtainView(int position, boolean[] isScrap) {
1585 View view = super.obtainView(position, isScrap);
1586
1587 if (view instanceof TextView) {
1588 ((TextView) view).setHorizontallyScrolling(true);
1589 }
1590
1591 return view;
1592 }
1593
1594 @Override
1595 public boolean isInTouchMode() {
1596 // WARNING: Please read the comment where mListSelectionHidden is declared
1597 return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
1598 }
1599
1600 /**
1601 * <p>Returns the focus state in the drop down.</p>
1602 *
1603 * @return true always if hijacking focus
1604 */
1605 @Override
1606 public boolean hasWindowFocus() {
1607 return mHijackFocus || super.hasWindowFocus();
1608 }
1609
1610 /**
1611 * <p>Returns the focus state in the drop down.</p>
1612 *
1613 * @return true always if hijacking focus
1614 */
1615 @Override
1616 public boolean isFocused() {
1617 return mHijackFocus || super.isFocused();
1618 }
1619
1620 /**
1621 * <p>Returns the focus state in the drop down.</p>
1622 *
1623 * @return true always if hijacking focus
1624 */
1625 @Override
1626 public boolean hasFocus() {
1627 return mHijackFocus || super.hasFocus();
1628 }
1629 }
1630
1631 private class PopupDataSetObserver extends DataSetObserver {
1632 @Override
1633 public void onChanged() {
1634 if (isShowing()) {
1635 // Resize the popup to fit new content
1636 show();
1637 }
1638 }
1639
1640 @Override
1641 public void onInvalidated() {
1642 dismiss();
1643 }
1644 }
1645
1646 private class ListSelectorHider implements Runnable {
1647 public void run() {
1648 clearListSelection();
1649 }
1650 }
1651
1652 private class ResizePopupRunnable implements Runnable {
1653 public void run() {
Adam Powell348e69c2011-02-16 16:49:50 -08001654 if (mDropDownList != null && mDropDownList.getCount() > mDropDownList.getChildCount() &&
1655 mDropDownList.getChildCount() <= mListItemExpandMaximum) {
1656 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
1657 show();
1658 }
Adam Powellc3fa6302010-05-18 11:36:27 -07001659 }
1660 }
1661
1662 private class PopupTouchInterceptor implements OnTouchListener {
1663 public boolean onTouch(View v, MotionEvent event) {
1664 final int action = event.getAction();
1665 final int x = (int) event.getX();
1666 final int y = (int) event.getY();
1667
1668 if (action == MotionEvent.ACTION_DOWN &&
1669 mPopup != null && mPopup.isShowing() &&
Gilles Debunne711734a2011-02-07 18:26:11 -08001670 (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) {
Adam Powellc3fa6302010-05-18 11:36:27 -07001671 mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
1672 } else if (action == MotionEvent.ACTION_UP) {
1673 mHandler.removeCallbacks(mResizePopupRunnable);
1674 }
1675 return false;
1676 }
1677 }
1678
1679 private class PopupScrollListener implements ListView.OnScrollListener {
1680 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1681 int totalItemCount) {
1682
1683 }
1684
1685 public void onScrollStateChanged(AbsListView view, int scrollState) {
1686 if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
1687 !isInputMethodNotNeeded() && mPopup.getContentView() != null) {
1688 mHandler.removeCallbacks(mResizePopupRunnable);
1689 mResizePopupRunnable.run();
1690 }
1691 }
1692 }
1693}