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