blob: d85bbb9e15716c730fef987b74f31a08be8b7ccb [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;
Alan Viverettef023c252014-08-28 13:55:18 -070023import android.content.res.TypedArray;
Adam Powellc3fa6302010-05-18 11:36:27 -070024import android.database.DataSetObserver;
25import android.graphics.Rect;
26import android.graphics.drawable.Drawable;
27import android.os.Handler;
Alan Viverette78efdba2014-03-28 16:53:56 -070028import android.os.SystemClock;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -070029import android.text.TextUtils;
Adam Powellc3fa6302010-05-18 11:36:27 -070030import android.util.AttributeSet;
Alan Viverettec0502722013-08-15 18:05:52 -070031import android.util.IntProperty;
Adam Powellc3fa6302010-05-18 11:36:27 -070032import android.util.Log;
Adam Powell54c94de2013-09-26 15:36:34 -070033import android.view.Gravity;
Adam Powellc3fa6302010-05-18 11:36:27 -070034import android.view.KeyEvent;
35import android.view.MotionEvent;
36import android.view.View;
Adam Powellc3fa6302010-05-18 11:36:27 -070037import android.view.View.MeasureSpec;
38import android.view.View.OnTouchListener;
Alan Viveretteca6a36112013-08-16 14:41:06 -070039import android.view.ViewConfiguration;
Gilles Debunne711734a2011-02-07 18:26:11 -080040import android.view.ViewGroup;
41import android.view.ViewParent;
Alan Viverettec0502722013-08-15 18:05:52 -070042import android.view.animation.AccelerateDecelerateInterpolator;
Adam Powellc3fa6302010-05-18 11:36:27 -070043
Alan Viverettef023c252014-08-28 13:55:18 -070044import com.android.internal.R;
Alan Viverette5e660212013-08-21 13:21:45 -070045import com.android.internal.widget.AutoScrollHelper.AbsListViewAutoScroller;
46
Fabrice Di Meglio1d3d7da2012-07-27 15:15:04 -070047import java.util.Locale;
48
Adam Powellc3fa6302010-05-18 11:36:27 -070049/**
50 * A ListPopupWindow anchors itself to a host view and displays a
Adam Powell65d79fb2010-08-11 22:05:46 -070051 * list of choices.
Adam Powellc3fa6302010-05-18 11:36:27 -070052 *
53 * <p>ListPopupWindow contains a number of tricky behaviors surrounding
54 * positioning, scrolling parents to fit the dropdown, interacting
55 * sanely with the IME if present, and others.
56 *
57 * @see android.widget.AutoCompleteTextView
58 * @see android.widget.Spinner
59 */
60public class ListPopupWindow {
61 private static final String TAG = "ListPopupWindow";
62 private static final boolean DEBUG = false;
63
64 /**
65 * This value controls the length of time that the user
66 * must leave a pointer down without scrolling to expand
67 * the autocomplete dropdown list to cover the IME.
68 */
69 private static final int EXPAND_LIST_TIMEOUT = 250;
70
71 private Context mContext;
72 private PopupWindow mPopup;
73 private ListAdapter mAdapter;
74 private DropDownListView mDropDownList;
75
76 private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
77 private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
78 private int mDropDownHorizontalOffset;
79 private int mDropDownVerticalOffset;
Adam Powell8132ba52011-07-15 17:37:11 -070080 private boolean mDropDownVerticalOffsetSet;
Adam Powellc3fa6302010-05-18 11:36:27 -070081
Adam Powell54c94de2013-09-26 15:36:34 -070082 private int mDropDownGravity = Gravity.NO_GRAVITY;
83
Adam Powellc3fa6302010-05-18 11:36:27 -070084 private boolean mDropDownAlwaysVisible = false;
85 private boolean mForceIgnoreOutsideTouch = false;
Adam Powell348e69c2011-02-16 16:49:50 -080086 int mListItemExpandMaximum = Integer.MAX_VALUE;
Adam Powellc3fa6302010-05-18 11:36:27 -070087
88 private View mPromptView;
89 private int mPromptPosition = POSITION_PROMPT_ABOVE;
90
91 private DataSetObserver mObserver;
92
93 private View mDropDownAnchorView;
94
95 private Drawable mDropDownListHighlight;
96
97 private AdapterView.OnItemClickListener mItemClickListener;
98 private AdapterView.OnItemSelectedListener mItemSelectedListener;
99
Adam Powell42675342010-07-09 18:02:59 -0700100 private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable();
Adam Powellc3fa6302010-05-18 11:36:27 -0700101 private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor();
102 private final PopupScrollListener mScrollListener = new PopupScrollListener();
103 private final ListSelectorHider mHideSelector = new ListSelectorHider();
104 private Runnable mShowDropDownRunnable;
105
106 private Handler mHandler = new Handler();
107
108 private Rect mTempRect = new Rect();
109
110 private boolean mModal;
111
Fabrice Di Meglio1d3d7da2012-07-27 15:15:04 -0700112 private int mLayoutDirection;
113
Adam Powellc3fa6302010-05-18 11:36:27 -0700114 /**
115 * The provided prompt view should appear above list content.
116 *
117 * @see #setPromptPosition(int)
118 * @see #getPromptPosition()
119 * @see #setPromptView(View)
120 */
121 public static final int POSITION_PROMPT_ABOVE = 0;
122
123 /**
124 * The provided prompt view should appear below list content.
125 *
126 * @see #setPromptPosition(int)
127 * @see #getPromptPosition()
128 * @see #setPromptView(View)
129 */
130 public static final int POSITION_PROMPT_BELOW = 1;
131
132 /**
133 * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}.
134 * If used to specify a popup width, the popup will match the width of the anchor view.
135 * If used to specify a popup height, the popup will fill available space.
136 */
137 public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT;
138
139 /**
140 * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}.
141 * If used to specify a popup width, the popup will use the width of its content.
142 */
143 public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT;
144
145 /**
146 * Mode for {@link #setInputMethodMode(int)}: the requirements for the
147 * input method should be based on the focusability of the popup. That is
148 * if it is focusable than it needs to work with the input method, else
149 * it doesn't.
150 */
151 public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE;
152
153 /**
154 * Mode for {@link #setInputMethodMode(int)}: this popup always needs to
155 * work with an input method, regardless of whether it is focusable. This
156 * means that it will always be displayed so that the user can also operate
157 * the input method while it is shown.
158 */
159 public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED;
160
161 /**
162 * Mode for {@link #setInputMethodMode(int)}: this popup never needs to
163 * work with an input method, regardless of whether it is focusable. This
164 * means that it will always be displayed to use as much space on the
165 * screen as needed, regardless of whether this covers the input method.
166 */
167 public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED;
168
169 /**
170 * Create a new, empty popup window capable of displaying items from a ListAdapter.
171 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
172 *
173 * @param context Context used for contained views.
174 */
175 public ListPopupWindow(Context context) {
Daniel Lehmannc2238d02010-10-21 11:52:55 -0700176 this(context, null, com.android.internal.R.attr.listPopupWindowStyle, 0);
Adam Powellc3fa6302010-05-18 11:36:27 -0700177 }
178
179 /**
180 * Create a new, empty popup window capable of displaying items from a ListAdapter.
181 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
182 *
183 * @param context Context used for contained views.
184 * @param attrs Attributes from inflating parent views used to style the popup.
185 */
186 public ListPopupWindow(Context context, AttributeSet attrs) {
Adam Powell0b2d3062010-09-14 16:15:02 -0700187 this(context, attrs, com.android.internal.R.attr.listPopupWindowStyle, 0);
Adam Powellc3fa6302010-05-18 11:36:27 -0700188 }
189
190 /**
191 * Create a new, empty popup window capable of displaying items from a ListAdapter.
192 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
193 *
194 * @param context Context used for contained views.
195 * @param attrs Attributes from inflating parent views used to style the popup.
196 * @param defStyleAttr Default style attribute to use for popup content.
197 */
198 public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr) {
199 this(context, attrs, defStyleAttr, 0);
200 }
201
202 /**
203 * Create a new, empty popup window capable of displaying items from a ListAdapter.
204 * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
205 *
206 * @param context Context used for contained views.
207 * @param attrs Attributes from inflating parent views used to style the popup.
208 * @param defStyleAttr Style attribute to read for default styling of popup content.
209 * @param defStyleRes Style resource ID to use for default styling of popup content.
210 */
211 public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
212 mContext = context;
Alan Viverettef023c252014-08-28 13:55:18 -0700213
214 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListPopupWindow,
215 defStyleAttr, defStyleRes);
216 mDropDownHorizontalOffset = a.getDimensionPixelOffset(
217 R.styleable.ListPopupWindow_dropDownHorizontalOffset, 0);
218 mDropDownVerticalOffset = a.getDimensionPixelOffset(
219 R.styleable.ListPopupWindow_dropDownVerticalOffset, 0);
220 if (mDropDownVerticalOffset != 0) {
221 mDropDownVerticalOffsetSet = true;
222 }
223 a.recycle();
224
Adam Powellc3fa6302010-05-18 11:36:27 -0700225 mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes);
Adam Powell6f5e9342011-01-27 13:30:55 -0800226 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
Fabrice Di Meglio1d3d7da2012-07-27 15:15:04 -0700227 // Set the default layout direction to match the default locale one
228 final Locale locale = mContext.getResources().getConfiguration().locale;
Fabrice Di Megliod3d9f3f2012-09-18 12:55:32 -0700229 mLayoutDirection = TextUtils.getLayoutDirectionFromLocale(locale);
Adam Powellc3fa6302010-05-18 11:36:27 -0700230 }
231
232 /**
233 * Sets the adapter that provides the data and the views to represent the data
234 * in this popup window.
235 *
236 * @param adapter The adapter to use to create this window's content.
237 */
238 public void setAdapter(ListAdapter adapter) {
239 if (mObserver == null) {
240 mObserver = new PopupDataSetObserver();
241 } else if (mAdapter != null) {
Adam Powell99969da2010-06-16 10:51:30 -0700242 mAdapter.unregisterDataSetObserver(mObserver);
Adam Powellc3fa6302010-05-18 11:36:27 -0700243 }
244 mAdapter = adapter;
245 if (mAdapter != null) {
246 adapter.registerDataSetObserver(mObserver);
247 }
248
249 if (mDropDownList != null) {
250 mDropDownList.setAdapter(mAdapter);
251 }
252 }
253
254 /**
255 * Set where the optional prompt view should appear. The default is
256 * {@link #POSITION_PROMPT_ABOVE}.
257 *
258 * @param position A position constant declaring where the prompt should be displayed.
259 *
260 * @see #POSITION_PROMPT_ABOVE
261 * @see #POSITION_PROMPT_BELOW
262 */
263 public void setPromptPosition(int position) {
264 mPromptPosition = position;
265 }
266
267 /**
268 * @return Where the optional prompt view should appear.
269 *
270 * @see #POSITION_PROMPT_ABOVE
271 * @see #POSITION_PROMPT_BELOW
272 */
273 public int getPromptPosition() {
274 return mPromptPosition;
275 }
276
277 /**
278 * Set whether this window should be modal when shown.
279 *
280 * <p>If a popup window is modal, it will receive all touch and key input.
281 * If the user touches outside the popup window's content area the popup window
282 * will be dismissed.
283 *
284 * @param modal {@code true} if the popup window should be modal, {@code false} otherwise.
285 */
286 public void setModal(boolean modal) {
Chet Haasee04499a2014-08-05 15:24:43 -0700287 mModal = modal;
Adam Powellc3fa6302010-05-18 11:36:27 -0700288 mPopup.setFocusable(modal);
289 }
290
291 /**
292 * Returns whether the popup window will be modal when shown.
293 *
294 * @return {@code true} if the popup window will be modal, {@code false} otherwise.
295 */
296 public boolean isModal() {
297 return mModal;
298 }
299
300 /**
301 * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
302 * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
303 * ignore outside touch even when the drop down is not set to always visible.
304 *
305 * @hide Used only by AutoCompleteTextView to handle some internal special cases.
306 */
307 public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
308 mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch;
309 }
310
311 /**
312 * Sets whether the drop-down should remain visible under certain conditions.
313 *
314 * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless
315 * of the size or content of the list. {@link #getBackground()} will fill any space
316 * that is not used by the list.
317 *
318 * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
319 *
320 * @hide Only used by AutoCompleteTextView under special conditions.
321 */
322 public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
323 mDropDownAlwaysVisible = dropDownAlwaysVisible;
324 }
325
326 /**
327 * @return Whether the drop-down is visible under special conditions.
328 *
329 * @hide Only used by AutoCompleteTextView under special conditions.
330 */
331 public boolean isDropDownAlwaysVisible() {
332 return mDropDownAlwaysVisible;
333 }
334
335 /**
336 * Sets the operating mode for the soft input area.
337 *
338 * @param mode The desired mode, see
339 * {@link android.view.WindowManager.LayoutParams#softInputMode}
340 * for the full list
341 *
342 * @see android.view.WindowManager.LayoutParams#softInputMode
343 * @see #getSoftInputMode()
344 */
345 public void setSoftInputMode(int mode) {
346 mPopup.setSoftInputMode(mode);
347 }
348
349 /**
350 * Returns the current value in {@link #setSoftInputMode(int)}.
351 *
352 * @see #setSoftInputMode(int)
353 * @see android.view.WindowManager.LayoutParams#softInputMode
354 */
355 public int getSoftInputMode() {
356 return mPopup.getSoftInputMode();
357 }
358
359 /**
360 * Sets a drawable to use as the list item selector.
361 *
362 * @param selector List selector drawable to use in the popup.
363 */
364 public void setListSelector(Drawable selector) {
365 mDropDownListHighlight = selector;
366 }
367
368 /**
369 * @return The background drawable for the popup window.
370 */
371 public Drawable getBackground() {
372 return mPopup.getBackground();
373 }
374
375 /**
376 * Sets a drawable to be the background for the popup window.
377 *
378 * @param d A drawable to set as the background.
379 */
380 public void setBackgroundDrawable(Drawable d) {
381 mPopup.setBackgroundDrawable(d);
382 }
383
384 /**
385 * Set an animation style to use when the popup window is shown or dismissed.
386 *
387 * @param animationStyle Animation style to use.
388 */
389 public void setAnimationStyle(int animationStyle) {
390 mPopup.setAnimationStyle(animationStyle);
391 }
392
393 /**
394 * Returns the animation style that will be used when the popup window is
395 * shown or dismissed.
396 *
397 * @return Animation style that will be used.
398 */
399 public int getAnimationStyle() {
400 return mPopup.getAnimationStyle();
401 }
402
403 /**
404 * Returns the view that will be used to anchor this popup.
405 *
406 * @return The popup's anchor view
407 */
408 public View getAnchorView() {
409 return mDropDownAnchorView;
410 }
411
412 /**
413 * Sets the popup's anchor view. This popup will always be positioned relative to
414 * the anchor view when shown.
415 *
416 * @param anchor The view to use as an anchor.
417 */
418 public void setAnchorView(View anchor) {
419 mDropDownAnchorView = anchor;
420 }
421
422 /**
423 * @return The horizontal offset of the popup from its anchor in pixels.
424 */
425 public int getHorizontalOffset() {
426 return mDropDownHorizontalOffset;
427 }
428
429 /**
430 * Set the horizontal offset of this popup from its anchor view in pixels.
431 *
Adam Powella984b382010-06-04 14:20:10 -0700432 * @param offset The horizontal offset of the popup from its anchor.
Adam Powellc3fa6302010-05-18 11:36:27 -0700433 */
434 public void setHorizontalOffset(int offset) {
435 mDropDownHorizontalOffset = offset;
436 }
437
438 /**
439 * @return The vertical offset of the popup from its anchor in pixels.
440 */
441 public int getVerticalOffset() {
Adam Powell8132ba52011-07-15 17:37:11 -0700442 if (!mDropDownVerticalOffsetSet) {
443 return 0;
444 }
Adam Powellc3fa6302010-05-18 11:36:27 -0700445 return mDropDownVerticalOffset;
446 }
447
448 /**
449 * Set the vertical offset of this popup from its anchor view in pixels.
450 *
Adam Powella984b382010-06-04 14:20:10 -0700451 * @param offset The vertical offset of the popup from its anchor.
Adam Powellc3fa6302010-05-18 11:36:27 -0700452 */
453 public void setVerticalOffset(int offset) {
454 mDropDownVerticalOffset = offset;
Adam Powell8132ba52011-07-15 17:37:11 -0700455 mDropDownVerticalOffsetSet = true;
Adam Powellc3fa6302010-05-18 11:36:27 -0700456 }
457
458 /**
Adam Powell54c94de2013-09-26 15:36:34 -0700459 * Set the gravity of the dropdown list. This is commonly used to
460 * set gravity to START or END for alignment with the anchor.
461 *
462 * @param gravity Gravity value to use
463 */
464 public void setDropDownGravity(int gravity) {
465 mDropDownGravity = gravity;
466 }
467
468 /**
Adam Powellc3fa6302010-05-18 11:36:27 -0700469 * @return The width of the popup window in pixels.
470 */
471 public int getWidth() {
472 return mDropDownWidth;
473 }
474
475 /**
476 * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT}
477 * or {@link #WRAP_CONTENT}.
478 *
479 * @param width Width of the popup window.
480 */
481 public void setWidth(int width) {
482 mDropDownWidth = width;
483 }
484
485 /**
Adam Powell42675342010-07-09 18:02:59 -0700486 * Sets the width of the popup window by the size of its content. The final width may be
487 * larger to accommodate styled window dressing.
488 *
489 * @param width Desired width of content in pixels.
490 */
491 public void setContentWidth(int width) {
492 Drawable popupBackground = mPopup.getBackground();
493 if (popupBackground != null) {
Adam Powella39b9872011-01-05 16:07:54 -0800494 popupBackground.getPadding(mTempRect);
495 mDropDownWidth = mTempRect.left + mTempRect.right + width;
Adam Powell62e2bde2011-08-15 15:50:05 -0700496 } else {
497 setWidth(width);
Adam Powell42675342010-07-09 18:02:59 -0700498 }
499 }
500
501 /**
Adam Powellc3fa6302010-05-18 11:36:27 -0700502 * @return The height of the popup window in pixels.
503 */
504 public int getHeight() {
505 return mDropDownHeight;
506 }
507
508 /**
509 * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}.
510 *
511 * @param height Height of the popup window.
512 */
513 public void setHeight(int height) {
514 mDropDownHeight = height;
515 }
516
517 /**
518 * Sets a listener to receive events when a list item is clicked.
519 *
520 * @param clickListener Listener to register
521 *
522 * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener)
523 */
524 public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) {
525 mItemClickListener = clickListener;
526 }
527
528 /**
529 * Sets a listener to receive events when a list item is selected.
530 *
531 * @param selectedListener Listener to register.
532 *
533 * @see ListView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener)
534 */
535 public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener selectedListener) {
536 mItemSelectedListener = selectedListener;
537 }
538
539 /**
540 * Set a view to act as a user prompt for this popup window. Where the prompt view will appear
541 * is controlled by {@link #setPromptPosition(int)}.
542 *
543 * @param prompt View to use as an informational prompt.
544 */
545 public void setPromptView(View prompt) {
546 boolean showing = isShowing();
547 if (showing) {
548 removePromptView();
549 }
550 mPromptView = prompt;
551 if (showing) {
552 show();
553 }
554 }
555
556 /**
557 * Post a {@link #show()} call to the UI thread.
558 */
559 public void postShow() {
560 mHandler.post(mShowDropDownRunnable);
561 }
562
563 /**
564 * Show the popup list. If the list is already showing, this method
565 * will recalculate the popup's size and position.
566 */
567 public void show() {
568 int height = buildDropDown();
569
570 int widthSpec = 0;
571 int heightSpec = 0;
572
573 boolean noInputMethod = isInputMethodNotNeeded();
Adam Powell348e69c2011-02-16 16:49:50 -0800574 mPopup.setAllowScrollingAnchorParent(!noInputMethod);
Adam Powellc3fa6302010-05-18 11:36:27 -0700575
576 if (mPopup.isShowing()) {
577 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
578 // The call to PopupWindow's update method below can accept -1 for any
579 // value you do not want to update.
580 widthSpec = -1;
581 } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
582 widthSpec = getAnchorView().getWidth();
583 } else {
584 widthSpec = mDropDownWidth;
585 }
586
587 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
588 // The call to PopupWindow's update method below can accept -1 for any
589 // value you do not want to update.
590 heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT;
591 if (noInputMethod) {
592 mPopup.setWindowLayoutMode(
593 mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
594 ViewGroup.LayoutParams.MATCH_PARENT : 0, 0);
595 } else {
596 mPopup.setWindowLayoutMode(
597 mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
598 ViewGroup.LayoutParams.MATCH_PARENT : 0,
599 ViewGroup.LayoutParams.MATCH_PARENT);
600 }
601 } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
602 heightSpec = height;
603 } else {
604 heightSpec = mDropDownHeight;
605 }
606
607 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
608
609 mPopup.update(getAnchorView(), mDropDownHorizontalOffset,
610 mDropDownVerticalOffset, widthSpec, heightSpec);
611 } else {
612 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
613 widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
614 } else {
615 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
616 mPopup.setWidth(getAnchorView().getWidth());
617 } else {
618 mPopup.setWidth(mDropDownWidth);
619 }
620 }
621
622 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
623 heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
624 } else {
625 if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
626 mPopup.setHeight(height);
627 } else {
628 mPopup.setHeight(mDropDownHeight);
629 }
630 }
631
632 mPopup.setWindowLayoutMode(widthSpec, heightSpec);
Adam Powell56c2d332010-11-05 20:03:03 -0700633 mPopup.setClipToScreenEnabled(true);
Adam Powellc3fa6302010-05-18 11:36:27 -0700634
635 // use outside touchable to dismiss drop down when touching outside of it, so
636 // only set this if the dropdown is not always visible
637 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
638 mPopup.setTouchInterceptor(mTouchInterceptor);
Alan Viverette560f1702014-05-05 14:40:07 -0700639 mPopup.showAsDropDown(getAnchorView(), mDropDownHorizontalOffset,
640 mDropDownVerticalOffset, mDropDownGravity);
Adam Powellc3fa6302010-05-18 11:36:27 -0700641 mDropDownList.setSelection(ListView.INVALID_POSITION);
642
643 if (!mModal || mDropDownList.isInTouchMode()) {
644 clearListSelection();
645 }
646 if (!mModal) {
647 mHandler.post(mHideSelector);
648 }
649 }
650 }
651
652 /**
653 * Dismiss the popup window.
654 */
655 public void dismiss() {
656 mPopup.dismiss();
657 removePromptView();
658 mPopup.setContentView(null);
659 mDropDownList = null;
Adam Powellca51e872011-02-14 19:54:29 -0800660 mHandler.removeCallbacks(mResizePopupRunnable);
Adam Powellc3fa6302010-05-18 11:36:27 -0700661 }
662
Adam Powell6c6f5752010-08-20 18:34:46 -0700663 /**
664 * Set a listener to receive a callback when the popup is dismissed.
665 *
666 * @param listener Listener that will be notified when the popup is dismissed.
667 */
668 public void setOnDismissListener(PopupWindow.OnDismissListener listener) {
669 mPopup.setOnDismissListener(listener);
670 }
671
Adam Powellc3fa6302010-05-18 11:36:27 -0700672 private void removePromptView() {
673 if (mPromptView != null) {
674 final ViewParent parent = mPromptView.getParent();
675 if (parent instanceof ViewGroup) {
676 final ViewGroup group = (ViewGroup) parent;
677 group.removeView(mPromptView);
678 }
679 }
680 }
681
682 /**
683 * Control how the popup operates with an input method: one of
684 * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED},
685 * or {@link #INPUT_METHOD_NOT_NEEDED}.
686 *
687 * <p>If the popup is showing, calling this method will take effect only
688 * the next time the popup is shown or through a manual call to the {@link #show()}
689 * method.</p>
690 *
691 * @see #getInputMethodMode()
692 * @see #show()
693 */
694 public void setInputMethodMode(int mode) {
695 mPopup.setInputMethodMode(mode);
696 }
697
698 /**
699 * Return the current value in {@link #setInputMethodMode(int)}.
700 *
701 * @see #setInputMethodMode(int)
702 */
703 public int getInputMethodMode() {
704 return mPopup.getInputMethodMode();
705 }
706
707 /**
708 * Set the selected position of the list.
709 * Only valid when {@link #isShowing()} == {@code true}.
710 *
711 * @param position List position to set as selected.
712 */
713 public void setSelection(int position) {
714 DropDownListView list = mDropDownList;
715 if (isShowing() && list != null) {
716 list.mListSelectionHidden = false;
717 list.setSelection(position);
718 if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) {
719 list.setItemChecked(position, true);
720 }
721 }
722 }
723
724 /**
725 * Clear any current list selection.
726 * Only valid when {@link #isShowing()} == {@code true}.
727 */
728 public void clearListSelection() {
729 final DropDownListView list = mDropDownList;
730 if (list != null) {
731 // WARNING: Please read the comment where mListSelectionHidden is declared
732 list.mListSelectionHidden = true;
733 list.hideSelector();
734 list.requestLayout();
735 }
736 }
737
738 /**
739 * @return {@code true} if the popup is currently showing, {@code false} otherwise.
740 */
741 public boolean isShowing() {
742 return mPopup.isShowing();
743 }
744
745 /**
746 * @return {@code true} if this popup is configured to assume the user does not need
747 * to interact with the IME while it is showing, {@code false} otherwise.
748 */
749 public boolean isInputMethodNotNeeded() {
750 return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED;
751 }
752
753 /**
754 * Perform an item click operation on the specified list adapter position.
755 *
756 * @param position Adapter position for performing the click
757 * @return true if the click action could be performed, false if not.
758 * (e.g. if the popup was not showing, this method would return false.)
759 */
760 public boolean performItemClick(int position) {
761 if (isShowing()) {
762 if (mItemClickListener != null) {
763 final DropDownListView list = mDropDownList;
764 final View child = list.getChildAt(position - list.getFirstVisiblePosition());
Adam Powellcdee4462010-09-02 17:13:24 -0700765 final ListAdapter adapter = list.getAdapter();
766 mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position));
Adam Powellc3fa6302010-05-18 11:36:27 -0700767 }
768 return true;
769 }
770 return false;
771 }
772
773 /**
774 * @return The currently selected item or null if the popup is not showing.
775 */
776 public Object getSelectedItem() {
777 if (!isShowing()) {
778 return null;
779 }
780 return mDropDownList.getSelectedItem();
781 }
782
783 /**
784 * @return The position of the currently selected item or {@link ListView#INVALID_POSITION}
785 * if {@link #isShowing()} == {@code false}.
786 *
787 * @see ListView#getSelectedItemPosition()
788 */
789 public int getSelectedItemPosition() {
790 if (!isShowing()) {
791 return ListView.INVALID_POSITION;
792 }
793 return mDropDownList.getSelectedItemPosition();
794 }
795
796 /**
797 * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID}
798 * if {@link #isShowing()} == {@code false}.
799 *
800 * @see ListView#getSelectedItemId()
801 */
802 public long getSelectedItemId() {
803 if (!isShowing()) {
804 return ListView.INVALID_ROW_ID;
805 }
806 return mDropDownList.getSelectedItemId();
807 }
808
809 /**
810 * @return The View for the currently selected item or null if
811 * {@link #isShowing()} == {@code false}.
812 *
813 * @see ListView#getSelectedView()
814 */
815 public View getSelectedView() {
816 if (!isShowing()) {
817 return null;
818 }
819 return mDropDownList.getSelectedView();
820 }
821
822 /**
823 * @return The {@link ListView} displayed within the popup window.
824 * Only valid when {@link #isShowing()} == {@code true}.
825 */
826 public ListView getListView() {
827 return mDropDownList;
828 }
829
830 /**
Adam Powell348e69c2011-02-16 16:49:50 -0800831 * The maximum number of list items that can be visible and still have
832 * the list expand when touched.
833 *
834 * @param max Max number of items that can be visible and still allow the list to expand.
835 */
836 void setListItemExpandMax(int max) {
837 mListItemExpandMaximum = max;
838 }
839
840 /**
Jeff Brown8d6d3b82011-01-26 19:31:18 -0800841 * Filter key down events. By forwarding key down events to this function,
Adam Powellc3fa6302010-05-18 11:36:27 -0700842 * views using non-modal ListPopupWindow can have it handle key selection of items.
843 *
844 * @param keyCode keyCode param passed to the host view's onKeyDown
845 * @param event event param passed to the host view's onKeyDown
846 * @return true if the event was handled, false if it was ignored.
847 *
848 * @see #setModal(boolean)
849 */
850 public boolean onKeyDown(int keyCode, KeyEvent event) {
851 // when the drop down is shown, we drive it directly
852 if (isShowing()) {
853 // the key events are forwarded to the list in the drop down view
854 // note that ListView handles space but we don't want that to happen
855 // also if selection is not currently in the drop down, then don't
856 // let center or enter presses go there since that would cause it
857 // to select one of its items
858 if (keyCode != KeyEvent.KEYCODE_SPACE
859 && (mDropDownList.getSelectedItemPosition() >= 0
Michael Wright24d36f52013-07-19 15:55:14 -0700860 || !KeyEvent.isConfirmKey(keyCode))) {
Adam Powellc3fa6302010-05-18 11:36:27 -0700861 int curIndex = mDropDownList.getSelectedItemPosition();
862 boolean consumed;
863
864 final boolean below = !mPopup.isAboveAnchor();
865
866 final ListAdapter adapter = mAdapter;
867
868 boolean allEnabled;
869 int firstItem = Integer.MAX_VALUE;
870 int lastItem = Integer.MIN_VALUE;
871
872 if (adapter != null) {
873 allEnabled = adapter.areAllItemsEnabled();
874 firstItem = allEnabled ? 0 :
875 mDropDownList.lookForSelectablePosition(0, true);
876 lastItem = allEnabled ? adapter.getCount() - 1 :
877 mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false);
878 }
879
880 if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) ||
881 (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) {
882 // When the selection is at the top, we block the key
883 // event to prevent focus from moving.
884 clearListSelection();
885 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
886 show();
887 return true;
888 } else {
889 // WARNING: Please read the comment where mListSelectionHidden
890 // is declared
891 mDropDownList.mListSelectionHidden = false;
892 }
893
894 consumed = mDropDownList.onKeyDown(keyCode, event);
895 if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed);
896
897 if (consumed) {
898 // If it handled the key event, then the user is
899 // navigating in the list, so we should put it in front.
900 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
901 // Here's a little trick we need to do to make sure that
902 // the list view is actually showing its focus indicator,
903 // by ensuring it has focus and getting its window out
904 // of touch mode.
905 mDropDownList.requestFocusFromTouch();
906 show();
907
908 switch (keyCode) {
909 // avoid passing the focus from the text view to the
910 // next component
911 case KeyEvent.KEYCODE_ENTER:
912 case KeyEvent.KEYCODE_DPAD_CENTER:
913 case KeyEvent.KEYCODE_DPAD_DOWN:
914 case KeyEvent.KEYCODE_DPAD_UP:
915 return true;
916 }
917 } else {
918 if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
919 // when the selection is at the bottom, we block the
920 // event to avoid going to the next focusable widget
921 if (curIndex == lastItem) {
922 return true;
923 }
924 } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP &&
925 curIndex == firstItem) {
926 return true;
927 }
928 }
929 }
930 }
931
932 return false;
933 }
934
935 /**
936 * Filter key down events. By forwarding key up events to this function,
937 * views using non-modal ListPopupWindow can have it handle key selection of items.
938 *
939 * @param keyCode keyCode param passed to the host view's onKeyUp
940 * @param event event param passed to the host view's onKeyUp
941 * @return true if the event was handled, false if it was ignored.
942 *
943 * @see #setModal(boolean)
944 */
945 public boolean onKeyUp(int keyCode, KeyEvent event) {
946 if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) {
947 boolean consumed = mDropDownList.onKeyUp(keyCode, event);
Michael Wright24d36f52013-07-19 15:55:14 -0700948 if (consumed && KeyEvent.isConfirmKey(keyCode)) {
949 // if the list accepts the key events and the key event was a click, the text view
950 // gets the selected item from the drop down as its content
951 dismiss();
Adam Powellc3fa6302010-05-18 11:36:27 -0700952 }
953 return consumed;
954 }
955 return false;
956 }
957
958 /**
959 * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)}
960 * events to this function, views using ListPopupWindow can have it dismiss the popup
961 * when the back key is pressed.
962 *
963 * @param keyCode keyCode param passed to the host view's onKeyPreIme
964 * @param event event param passed to the host view's onKeyPreIme
965 * @return true if the event was handled, false if it was ignored.
966 *
967 * @see #setModal(boolean)
968 */
969 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
970 if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) {
971 // special case for the back key, we do not even try to send it
972 // to the drop down list but instead, consume it immediately
973 final View anchorView = mDropDownAnchorView;
974 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
Jeff Brownb3ea9222011-01-10 16:26:36 -0800975 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
976 if (state != null) {
977 state.startTracking(event, this);
978 }
Adam Powellc3fa6302010-05-18 11:36:27 -0700979 return true;
980 } else if (event.getAction() == KeyEvent.ACTION_UP) {
Jeff Brownb3ea9222011-01-10 16:26:36 -0800981 KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
982 if (state != null) {
983 state.handleUpEvent(event);
984 }
Adam Powellc3fa6302010-05-18 11:36:27 -0700985 if (event.isTracking() && !event.isCanceled()) {
986 dismiss();
987 return true;
988 }
989 }
990 }
991 return false;
992 }
993
994 /**
Alan Viverette1955a5b52013-08-27 15:45:16 -0700995 * Returns an {@link OnTouchListener} that can be added to the source view
996 * to implement drag-to-open behavior. Generally, the source view should be
997 * the same view that was passed to {@link #setAnchorView}.
998 * <p>
999 * When the listener is set on a view, touching that view and dragging
1000 * outside of its bounds will open the popup window. Lifting will select the
1001 * currently touched list item.
1002 * <p>
1003 * Example usage:
Alan Viverette3f9832d2013-08-30 14:43:25 -07001004 * <pre>
1005 * ListPopupWindow myPopup = new ListPopupWindow(context);
Alan Viverette1955a5b52013-08-27 15:45:16 -07001006 * myPopup.setAnchor(myAnchor);
1007 * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor);
Alan Viverette3f9832d2013-08-30 14:43:25 -07001008 * myAnchor.setOnTouchListener(dragListener);
1009 * </pre>
Alan Viverette1955a5b52013-08-27 15:45:16 -07001010 *
1011 * @param src the view on which the resulting listener will be set
1012 * @return a touch listener that controls drag-to-open behavior
1013 */
1014 public OnTouchListener createDragToOpenListener(View src) {
1015 return new ForwardingListener(src) {
1016 @Override
1017 public ListPopupWindow getPopup() {
1018 return ListPopupWindow.this;
1019 }
1020 };
1021 }
1022
1023 /**
Adam Powellc3fa6302010-05-18 11:36:27 -07001024 * <p>Builds the popup window's content and returns the height the popup
1025 * should have. Returns -1 when the content already exists.</p>
1026 *
1027 * @return the content's height or -1 if content already exists
1028 */
1029 private int buildDropDown() {
1030 ViewGroup dropDownView;
1031 int otherHeights = 0;
1032
1033 if (mDropDownList == null) {
1034 Context context = mContext;
1035
1036 /**
1037 * This Runnable exists for the sole purpose of checking if the view layout has got
1038 * completed and if so call showDropDown to display the drop down. This is used to show
1039 * the drop down as soon as possible after user opens up the search dialog, without
1040 * waiting for the normal UI pipeline to do it's job which is slower than this method.
1041 */
1042 mShowDropDownRunnable = new Runnable() {
1043 public void run() {
1044 // View layout should be all done before displaying the drop down.
1045 View view = getAnchorView();
1046 if (view != null && view.getWindowToken() != null) {
1047 show();
1048 }
1049 }
1050 };
1051
1052 mDropDownList = new DropDownListView(context, !mModal);
1053 if (mDropDownListHighlight != null) {
1054 mDropDownList.setSelector(mDropDownListHighlight);
1055 }
1056 mDropDownList.setAdapter(mAdapter);
Adam Powellc3fa6302010-05-18 11:36:27 -07001057 mDropDownList.setOnItemClickListener(mItemClickListener);
1058 mDropDownList.setFocusable(true);
1059 mDropDownList.setFocusableInTouchMode(true);
1060 mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
1061 public void onItemSelected(AdapterView<?> parent, View view,
1062 int position, long id) {
1063
1064 if (position != -1) {
1065 DropDownListView dropDownList = mDropDownList;
1066
1067 if (dropDownList != null) {
1068 dropDownList.mListSelectionHidden = false;
1069 }
1070 }
1071 }
1072
1073 public void onNothingSelected(AdapterView<?> parent) {
1074 }
1075 });
1076 mDropDownList.setOnScrollListener(mScrollListener);
1077
1078 if (mItemSelectedListener != null) {
1079 mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
1080 }
1081
1082 dropDownView = mDropDownList;
1083
1084 View hintView = mPromptView;
1085 if (hintView != null) {
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001086 // if a hint has been specified, we accomodate more space for it and
Adam Powellc3fa6302010-05-18 11:36:27 -07001087 // add a text view in the drop down menu, at the bottom of the list
1088 LinearLayout hintContainer = new LinearLayout(context);
1089 hintContainer.setOrientation(LinearLayout.VERTICAL);
1090
1091 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
1092 ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
1093 );
1094
1095 switch (mPromptPosition) {
1096 case POSITION_PROMPT_BELOW:
1097 hintContainer.addView(dropDownView, hintParams);
1098 hintContainer.addView(hintView);
1099 break;
1100
1101 case POSITION_PROMPT_ABOVE:
1102 hintContainer.addView(hintView);
1103 hintContainer.addView(dropDownView, hintParams);
1104 break;
1105
1106 default:
1107 Log.e(TAG, "Invalid hint position " + mPromptPosition);
1108 break;
1109 }
1110
1111 // measure the hint's height to find how much more vertical space
1112 // we need to add to the drop down's height
1113 int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST);
1114 int heightSpec = MeasureSpec.UNSPECIFIED;
1115 hintView.measure(widthSpec, heightSpec);
1116
1117 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
1118 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
1119 + hintParams.bottomMargin;
1120
1121 dropDownView = hintContainer;
1122 }
1123
1124 mPopup.setContentView(dropDownView);
1125 } else {
1126 dropDownView = (ViewGroup) mPopup.getContentView();
1127 final View view = mPromptView;
1128 if (view != null) {
1129 LinearLayout.LayoutParams hintParams =
1130 (LinearLayout.LayoutParams) view.getLayoutParams();
1131 otherHeights = view.getMeasuredHeight() + hintParams.topMargin
1132 + hintParams.bottomMargin;
1133 }
1134 }
1135
Adam Powell8132ba52011-07-15 17:37:11 -07001136 // getMaxAvailableHeight() subtracts the padding, so we put it back
Adam Powellc3fa6302010-05-18 11:36:27 -07001137 // to get the available height for the whole window
1138 int padding = 0;
1139 Drawable background = mPopup.getBackground();
1140 if (background != null) {
1141 background.getPadding(mTempRect);
1142 padding = mTempRect.top + mTempRect.bottom;
Adam Powell8132ba52011-07-15 17:37:11 -07001143
1144 // If we don't have an explicit vertical offset, determine one from the window
1145 // background so that content will line up.
1146 if (!mDropDownVerticalOffsetSet) {
1147 mDropDownVerticalOffset = -mTempRect.top;
1148 }
Adam Powell7507d3d2012-03-08 12:01:16 -08001149 } else {
1150 mTempRect.setEmpty();
Adam Powellc3fa6302010-05-18 11:36:27 -07001151 }
1152
Adam Powell8132ba52011-07-15 17:37:11 -07001153 // Max height available on the screen for a popup.
1154 boolean ignoreBottomDecorations =
1155 mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
1156 final int maxHeight = mPopup.getMaxAvailableHeight(
1157 getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations);
1158
Adam Powellc3fa6302010-05-18 11:36:27 -07001159 if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
1160 return maxHeight + padding;
1161 }
1162
Adam Powell7507d3d2012-03-08 12:01:16 -08001163 final int childWidthSpec;
1164 switch (mDropDownWidth) {
1165 case ViewGroup.LayoutParams.WRAP_CONTENT:
1166 childWidthSpec = MeasureSpec.makeMeasureSpec(
1167 mContext.getResources().getDisplayMetrics().widthPixels -
1168 (mTempRect.left + mTempRect.right),
1169 MeasureSpec.AT_MOST);
1170 break;
1171 case ViewGroup.LayoutParams.MATCH_PARENT:
1172 childWidthSpec = MeasureSpec.makeMeasureSpec(
1173 mContext.getResources().getDisplayMetrics().widthPixels -
1174 (mTempRect.left + mTempRect.right),
1175 MeasureSpec.EXACTLY);
1176 break;
1177 default:
1178 childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY);
1179 break;
1180 }
1181 final int listContent = mDropDownList.measureHeightOfChildren(childWidthSpec,
Adam Powella7845ed2011-08-07 15:48:03 -07001182 0, ListView.NO_POSITION, maxHeight - otherHeights, -1);
Adam Powellc3fa6302010-05-18 11:36:27 -07001183 // add padding only if the list has items in it, that way we don't show
1184 // the popup if it is not needed
1185 if (listContent > 0) otherHeights += padding;
1186
1187 return listContent + otherHeights;
1188 }
1189
Fabrice Di Meglio1d3d7da2012-07-27 15:15:04 -07001190 /**
Alan Viveretteca6a36112013-08-16 14:41:06 -07001191 * Abstract class that forwards touch events to a {@link ListPopupWindow}.
1192 *
1193 * @hide
1194 */
Alan Viverette69960142013-08-22 17:26:57 -07001195 public static abstract class ForwardingListener
1196 implements View.OnTouchListener, View.OnAttachStateChangeListener {
Alan Viveretteca6a36112013-08-16 14:41:06 -07001197 /** Scaled touch slop, used for detecting movement outside bounds. */
1198 private final float mScaledTouchSlop;
1199
Alan Viverette69960142013-08-22 17:26:57 -07001200 /** Timeout before disallowing intercept on the source's parent. */
1201 private final int mTapTimeout;
1202
Alan Viverettebca05f52014-07-23 20:15:44 -07001203 /** Timeout before accepting a long-press to start forwarding. */
1204 private final int mLongPressTimeout;
1205
Alan Viverette69960142013-08-22 17:26:57 -07001206 /** Source view from which events are forwarded. */
1207 private final View mSrc;
1208
1209 /** Runnable used to prevent conflicts with scrolling parents. */
1210 private Runnable mDisallowIntercept;
1211
Alan Viverettebca05f52014-07-23 20:15:44 -07001212 /** Runnable used to trigger forwarding on long-press. */
1213 private Runnable mTriggerLongPress;
1214
Alan Viveretteca6a36112013-08-16 14:41:06 -07001215 /** Whether this listener is currently forwarding touch events. */
1216 private boolean mForwarding;
1217
Alan Viverettebca05f52014-07-23 20:15:44 -07001218 /**
1219 * Whether forwarding was initiated by a long-press. If so, we won't
1220 * force the window to dismiss when the touch stream ends.
1221 */
1222 private boolean mWasLongPress;
1223
Alan Viveretteca6a36112013-08-16 14:41:06 -07001224 /** The id of the first pointer down in the current event stream. */
1225 private int mActivePointerId;
1226
Alan Viverette69960142013-08-22 17:26:57 -07001227 public ForwardingListener(View src) {
1228 mSrc = src;
1229 mScaledTouchSlop = ViewConfiguration.get(src.getContext()).getScaledTouchSlop();
1230 mTapTimeout = ViewConfiguration.getTapTimeout();
1231
Alan Viverettebca05f52014-07-23 20:15:44 -07001232 // Use a medium-press timeout. Halfway between tap and long-press.
1233 mLongPressTimeout = (mTapTimeout + ViewConfiguration.getLongPressTimeout()) / 2;
1234
Alan Viverette69960142013-08-22 17:26:57 -07001235 src.addOnAttachStateChangeListener(this);
Alan Viveretteca6a36112013-08-16 14:41:06 -07001236 }
1237
1238 /**
1239 * Returns the popup to which this listener is forwarding events.
1240 * <p>
1241 * Override this to return the correct popup. If the popup is displayed
1242 * asynchronously, you may also need to override
1243 * {@link #onForwardingStopped} to prevent premature cancelation of
1244 * forwarding.
1245 *
1246 * @return the popup to which this listener is forwarding events
1247 */
1248 public abstract ListPopupWindow getPopup();
1249
1250 @Override
1251 public boolean onTouch(View v, MotionEvent event) {
1252 final boolean wasForwarding = mForwarding;
1253 final boolean forwarding;
1254 if (wasForwarding) {
Alan Viverette99b4e852014-10-27 16:42:26 -07001255 forwarding = onTouchForwarded(event) || !onForwardingStopped();
Alan Viveretteca6a36112013-08-16 14:41:06 -07001256 } else {
Alan Viverette69960142013-08-22 17:26:57 -07001257 forwarding = onTouchObserved(event) && onForwardingStarted();
Alan Viverette78efdba2014-03-28 16:53:56 -07001258
1259 if (forwarding) {
1260 // Make sure we cancel any ongoing source event stream.
1261 final long now = SystemClock.uptimeMillis();
1262 final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL,
1263 0.0f, 0.0f, 0);
1264 mSrc.onTouchEvent(e);
1265 e.recycle();
1266 }
Alan Viveretteca6a36112013-08-16 14:41:06 -07001267 }
1268
1269 mForwarding = forwarding;
1270 return forwarding || wasForwarding;
1271 }
1272
Alan Viverette69960142013-08-22 17:26:57 -07001273 @Override
1274 public void onViewAttachedToWindow(View v) {
1275 }
1276
1277 @Override
1278 public void onViewDetachedFromWindow(View v) {
1279 mForwarding = false;
1280 mActivePointerId = MotionEvent.INVALID_POINTER_ID;
1281
1282 if (mDisallowIntercept != null) {
1283 mSrc.removeCallbacks(mDisallowIntercept);
1284 }
1285 }
1286
Alan Viveretteca6a36112013-08-16 14:41:06 -07001287 /**
1288 * Called when forwarding would like to start.
1289 * <p>
1290 * By default, this will show the popup returned by {@link #getPopup()}.
1291 * It may be overridden to perform another action, like clicking the
1292 * source view or preparing the popup before showing it.
1293 *
1294 * @return true to start forwarding, false otherwise
1295 */
Alan Viverette69960142013-08-22 17:26:57 -07001296 protected boolean onForwardingStarted() {
Alan Viveretteca6a36112013-08-16 14:41:06 -07001297 final ListPopupWindow popup = getPopup();
1298 if (popup != null && !popup.isShowing()) {
1299 popup.show();
1300 }
1301 return true;
1302 }
1303
1304 /**
1305 * Called when forwarding would like to stop.
1306 * <p>
1307 * By default, this will dismiss the popup returned by
1308 * {@link #getPopup()}. It may be overridden to perform some other
1309 * action.
1310 *
1311 * @return true to stop forwarding, false otherwise
1312 */
Alan Viverette69960142013-08-22 17:26:57 -07001313 protected boolean onForwardingStopped() {
Alan Viveretteca6a36112013-08-16 14:41:06 -07001314 final ListPopupWindow popup = getPopup();
1315 if (popup != null && popup.isShowing()) {
1316 popup.dismiss();
1317 }
1318 return true;
1319 }
1320
1321 /**
1322 * Observes motion events and determines when to start forwarding.
1323 *
Alan Viveretteca6a36112013-08-16 14:41:06 -07001324 * @param srcEvent motion event in source view coordinates
1325 * @return true to start forwarding motion events, false otherwise
1326 */
Alan Viverette69960142013-08-22 17:26:57 -07001327 private boolean onTouchObserved(MotionEvent srcEvent) {
1328 final View src = mSrc;
Alan Viveretteca6a36112013-08-16 14:41:06 -07001329 if (!src.isEnabled()) {
1330 return false;
1331 }
1332
Alan Viveretteca6a36112013-08-16 14:41:06 -07001333 final int actionMasked = srcEvent.getActionMasked();
Alan Viverette69960142013-08-22 17:26:57 -07001334 switch (actionMasked) {
1335 case MotionEvent.ACTION_DOWN:
1336 mActivePointerId = srcEvent.getPointerId(0);
Alan Viverettebca05f52014-07-23 20:15:44 -07001337 mWasLongPress = false;
1338
Alan Viverette69960142013-08-22 17:26:57 -07001339 if (mDisallowIntercept == null) {
1340 mDisallowIntercept = new DisallowIntercept();
1341 }
1342 src.postDelayed(mDisallowIntercept, mTapTimeout);
Alan Viverettebca05f52014-07-23 20:15:44 -07001343
1344 if (mTriggerLongPress == null) {
1345 mTriggerLongPress = new TriggerLongPress();
1346 }
1347 src.postDelayed(mTriggerLongPress, mLongPressTimeout);
Alan Viverette69960142013-08-22 17:26:57 -07001348 break;
1349 case MotionEvent.ACTION_MOVE:
1350 final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId);
1351 if (activePointerIndex >= 0) {
1352 final float x = srcEvent.getX(activePointerIndex);
1353 final float y = srcEvent.getY(activePointerIndex);
Alan Viverettebca05f52014-07-23 20:15:44 -07001354
1355 // Has the pointer has moved outside of the view?
Alan Viverette69960142013-08-22 17:26:57 -07001356 if (!src.pointInView(x, y, mScaledTouchSlop)) {
Alan Viverettebca05f52014-07-23 20:15:44 -07001357 clearCallbacks();
1358
1359 // Don't let the parent intercept our events.
Alan Viverette69960142013-08-22 17:26:57 -07001360 src.getParent().requestDisallowInterceptTouchEvent(true);
1361 return true;
1362 }
1363 }
1364 break;
1365 case MotionEvent.ACTION_CANCEL:
1366 case MotionEvent.ACTION_UP:
Alan Viverettebca05f52014-07-23 20:15:44 -07001367 clearCallbacks();
Alan Viverette69960142013-08-22 17:26:57 -07001368 break;
Alan Viveretteca6a36112013-08-16 14:41:06 -07001369 }
1370
1371 return false;
1372 }
1373
Alan Viverettebca05f52014-07-23 20:15:44 -07001374 private void clearCallbacks() {
1375 if (mTriggerLongPress != null) {
1376 mSrc.removeCallbacks(mTriggerLongPress);
1377 }
1378
1379 if (mDisallowIntercept != null) {
1380 mSrc.removeCallbacks(mDisallowIntercept);
1381 }
1382 }
1383
1384 private void onLongPress() {
1385 clearCallbacks();
1386
1387 final View src = mSrc;
Alan Viverette298536b12014-12-03 15:24:31 -08001388 if (!src.isEnabled() || src.isLongClickable()) {
1389 // Ignore long-press if the view is disabled or has its own
1390 // handler.
Alan Viverettebca05f52014-07-23 20:15:44 -07001391 return;
1392 }
1393
1394 if (!onForwardingStarted()) {
1395 return;
1396 }
1397
1398 // Don't let the parent intercept our events.
Alan Viverette298536b12014-12-03 15:24:31 -08001399 src.getParent().requestDisallowInterceptTouchEvent(true);
Alan Viverettebca05f52014-07-23 20:15:44 -07001400
1401 // Make sure we cancel any ongoing source event stream.
1402 final long now = SystemClock.uptimeMillis();
1403 final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
Alan Viverette298536b12014-12-03 15:24:31 -08001404 src.onTouchEvent(e);
Alan Viverettebca05f52014-07-23 20:15:44 -07001405 e.recycle();
1406
1407 mForwarding = true;
1408 mWasLongPress = true;
1409 }
1410
Alan Viveretteca6a36112013-08-16 14:41:06 -07001411 /**
1412 * Handled forwarded motion events and determines when to stop
1413 * forwarding.
1414 *
Alan Viveretteca6a36112013-08-16 14:41:06 -07001415 * @param srcEvent motion event in source view coordinates
1416 * @return true to continue forwarding motion events, false to cancel
1417 */
Alan Viverette69960142013-08-22 17:26:57 -07001418 private boolean onTouchForwarded(MotionEvent srcEvent) {
1419 final View src = mSrc;
Alan Viveretteca6a36112013-08-16 14:41:06 -07001420 final ListPopupWindow popup = getPopup();
1421 if (popup == null || !popup.isShowing()) {
1422 return false;
1423 }
1424
1425 final DropDownListView dst = popup.mDropDownList;
1426 if (dst == null || !dst.isShown()) {
1427 return false;
1428 }
1429
1430 // Convert event to destination-local coordinates.
1431 final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
1432 src.toGlobalMotionEvent(dstEvent);
1433 dst.toLocalMotionEvent(dstEvent);
1434
1435 // Forward converted event to destination view, then recycle it.
1436 final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId);
1437 dstEvent.recycle();
Alan Viveretted361a4f2014-06-30 16:47:40 -07001438
1439 // Always cancel forwarding when the touch stream ends.
1440 final int action = srcEvent.getActionMasked();
1441 final boolean keepForwarding = action != MotionEvent.ACTION_UP
1442 && action != MotionEvent.ACTION_CANCEL;
1443
1444 return handled && keepForwarding;
Alan Viveretteca6a36112013-08-16 14:41:06 -07001445 }
Alan Viverette69960142013-08-22 17:26:57 -07001446
1447 private class DisallowIntercept implements Runnable {
1448 @Override
1449 public void run() {
1450 final ViewParent parent = mSrc.getParent();
1451 parent.requestDisallowInterceptTouchEvent(true);
1452 }
1453 }
Alan Viverettebca05f52014-07-23 20:15:44 -07001454
1455 private class TriggerLongPress implements Runnable {
1456 @Override
1457 public void run() {
1458 onLongPress();
1459 }
1460 }
Alan Viveretteca6a36112013-08-16 14:41:06 -07001461 }
1462
1463 /**
Adam Powellc3fa6302010-05-18 11:36:27 -07001464 * <p>Wrapper class for a ListView. This wrapper can hijack the focus to
1465 * make sure the list uses the appropriate drawables and states when
1466 * displayed on screen within a drop down. The focus is never actually
1467 * passed to the drop down in this mode; the list only looks focused.</p>
1468 */
1469 private static class DropDownListView extends ListView {
Alan Viverettec0502722013-08-15 18:05:52 -07001470 /** Duration in milliseconds of the drag-to-open click animation. */
1471 private static final long CLICK_ANIM_DURATION = 150;
1472
1473 /** Target alpha value for drag-to-open click animation. */
1474 private static final int CLICK_ANIM_ALPHA = 0x80;
1475
1476 /** Wrapper around Drawable's <code>alpha</code> property. */
1477 private static final IntProperty<Drawable> DRAWABLE_ALPHA =
1478 new IntProperty<Drawable>("alpha") {
1479 @Override
1480 public void setValue(Drawable object, int value) {
1481 object.setAlpha(value);
1482 }
1483
1484 @Override
1485 public Integer get(Drawable object) {
1486 return object.getAlpha();
1487 }
1488 };
1489
Adam Powellc3fa6302010-05-18 11:36:27 -07001490 /*
1491 * WARNING: This is a workaround for a touch mode issue.
1492 *
1493 * Touch mode is propagated lazily to windows. This causes problems in
1494 * the following scenario:
1495 * - Type something in the AutoCompleteTextView and get some results
1496 * - Move down with the d-pad to select an item in the list
1497 * - Move up with the d-pad until the selection disappears
1498 * - Type more text in the AutoCompleteTextView *using the soft keyboard*
1499 * and get new results; you are now in touch mode
1500 * - The selection comes back on the first item in the list, even though
1501 * the list is supposed to be in touch mode
1502 *
1503 * Using the soft keyboard triggers the touch mode change but that change
1504 * is propagated to our window only after the first list layout, therefore
1505 * after the list attempts to resurrect the selection.
1506 *
1507 * The trick to work around this issue is to pretend the list is in touch
1508 * mode when we know that the selection should not appear, that is when
1509 * we know the user moved the selection away from the list.
1510 *
1511 * This boolean is set to true whenever we explicitly hide the list's
1512 * selection and reset to false whenever we know the user moved the
1513 * selection back to the list.
1514 *
1515 * When this boolean is true, isInTouchMode() returns true, otherwise it
1516 * returns super.isInTouchMode().
1517 */
1518 private boolean mListSelectionHidden;
1519
1520 /**
1521 * True if this wrapper should fake focus.
1522 */
1523 private boolean mHijackFocus;
1524
Alan Viverettec0502722013-08-15 18:05:52 -07001525 /** Whether to force drawing of the pressed state selector. */
1526 private boolean mDrawsInPressedState;
1527
1528 /** Current drag-to-open click animation, if any. */
1529 private Animator mClickAnimation;
1530
Alan Viverette5e660212013-08-21 13:21:45 -07001531 /** Helper for drag-to-open auto scrolling. */
1532 private AbsListViewAutoScroller mScrollHelper;
1533
Adam Powellc3fa6302010-05-18 11:36:27 -07001534 /**
1535 * <p>Creates a new list view wrapper.</p>
1536 *
1537 * @param context this view's context
1538 */
1539 public DropDownListView(Context context, boolean hijackFocus) {
1540 super(context, null, com.android.internal.R.attr.dropDownListViewStyle);
1541 mHijackFocus = hijackFocus;
Amith Yamasanib1818e82010-10-20 10:06:08 -07001542 // TODO: Add an API to control this
1543 setCacheColorHint(0); // Transparent, since the background drawable could be anything.
Adam Powellc3fa6302010-05-18 11:36:27 -07001544 }
1545
1546 /**
Alan Viverettec0502722013-08-15 18:05:52 -07001547 * Handles forwarded events.
1548 *
1549 * @param activePointerId id of the pointer that activated forwarding
1550 * @return whether the event was handled
1551 */
1552 public boolean onForwardedEvent(MotionEvent event, int activePointerId) {
1553 boolean handledEvent = true;
1554 boolean clearPressedItem = false;
1555
1556 final int actionMasked = event.getActionMasked();
1557 switch (actionMasked) {
1558 case MotionEvent.ACTION_CANCEL:
1559 handledEvent = false;
1560 break;
1561 case MotionEvent.ACTION_UP:
1562 handledEvent = false;
1563 // $FALL-THROUGH$
1564 case MotionEvent.ACTION_MOVE:
1565 final int activeIndex = event.findPointerIndex(activePointerId);
1566 if (activeIndex < 0) {
1567 handledEvent = false;
1568 break;
1569 }
1570
1571 final int x = (int) event.getX(activeIndex);
1572 final int y = (int) event.getY(activeIndex);
1573 final int position = pointToPosition(x, y);
1574 if (position == INVALID_POSITION) {
1575 clearPressedItem = true;
1576 break;
1577 }
1578
1579 final View child = getChildAt(position - getFirstVisiblePosition());
Alan Viveretted361a4f2014-06-30 16:47:40 -07001580 setPressedItem(child, position, x, y);
Alan Viverettec0502722013-08-15 18:05:52 -07001581 handledEvent = true;
1582
1583 if (actionMasked == MotionEvent.ACTION_UP) {
1584 clickPressedItem(child, position);
1585 }
1586 break;
1587 }
1588
1589 // Failure to handle the event cancels forwarding.
1590 if (!handledEvent || clearPressedItem) {
1591 clearPressedItem();
1592 }
1593
Alan Viverette5e660212013-08-21 13:21:45 -07001594 // Manage automatic scrolling.
1595 if (handledEvent) {
1596 if (mScrollHelper == null) {
1597 mScrollHelper = new AbsListViewAutoScroller(this);
1598 }
1599 mScrollHelper.setEnabled(true);
1600 mScrollHelper.onTouch(this, event);
1601 } else if (mScrollHelper != null) {
1602 mScrollHelper.setEnabled(false);
1603 }
1604
Alan Viverettec0502722013-08-15 18:05:52 -07001605 return handledEvent;
1606 }
1607
1608 /**
1609 * Starts an alpha animation on the selector. When the animation ends,
1610 * the list performs a click on the item.
1611 */
1612 private void clickPressedItem(final View child, final int position) {
1613 final long id = getItemIdAtPosition(position);
1614 final Animator anim = ObjectAnimator.ofInt(
1615 mSelector, DRAWABLE_ALPHA, 0xFF, CLICK_ANIM_ALPHA, 0xFF);
1616 anim.setDuration(CLICK_ANIM_DURATION);
1617 anim.setInterpolator(new AccelerateDecelerateInterpolator());
1618 anim.addListener(new AnimatorListenerAdapter() {
1619 @Override
1620 public void onAnimationEnd(Animator animation) {
1621 performItemClick(child, position, id);
1622 }
1623 });
1624 anim.start();
1625
1626 if (mClickAnimation != null) {
1627 mClickAnimation.cancel();
1628 }
1629 mClickAnimation = anim;
1630 }
1631
1632 private void clearPressedItem() {
1633 mDrawsInPressedState = false;
1634 setPressed(false);
1635 updateSelectorState();
1636
Alan Viverette62a8b612014-10-01 16:41:31 -07001637 final View motionView = getChildAt(mMotionPosition - mFirstPosition);
1638 if (motionView != null) {
1639 motionView.setPressed(false);
1640 }
1641
Alan Viverettec0502722013-08-15 18:05:52 -07001642 if (mClickAnimation != null) {
1643 mClickAnimation.cancel();
1644 mClickAnimation = null;
1645 }
1646 }
1647
Alan Viveretted361a4f2014-06-30 16:47:40 -07001648 private void setPressedItem(View child, int position, float x, float y) {
Alan Viverettec0502722013-08-15 18:05:52 -07001649 mDrawsInPressedState = true;
1650
Alan Viverette25630952015-01-26 10:24:08 -08001651 // Ordering is essential. First, update the container's pressed state.
1652 drawableHotspotChanged(x, y);
1653 if (!isPressed()) {
1654 setPressed(true);
1655 }
1656
1657 // Next, run layout if we need to stabilize child positions.
1658 if (mDataChanged) {
1659 layoutChildren();
1660 }
Alan Viverettec0502722013-08-15 18:05:52 -07001661
Alan Viverette62a8b612014-10-01 16:41:31 -07001662 // Manage the pressed view based on motion position. This allows us to
1663 // play nicely with actual touch and scroll events.
1664 final View motionView = getChildAt(mMotionPosition - mFirstPosition);
Alan Viverette25630952015-01-26 10:24:08 -08001665 if (motionView != null && motionView != child && motionView.isPressed()) {
Alan Viverette62a8b612014-10-01 16:41:31 -07001666 motionView.setPressed(false);
1667 }
1668 mMotionPosition = position;
Alan Viverette25630952015-01-26 10:24:08 -08001669
1670 // Offset for child coordinates.
1671 final float childX = x - child.getLeft();
1672 final float childY = y - child.getTop();
1673 child.drawableHotspotChanged(childX, childY);
1674 if (!child.isPressed()) {
1675 child.setPressed(true);
1676 }
Alan Viverette62a8b612014-10-01 16:41:31 -07001677
Alan Viverettec0502722013-08-15 18:05:52 -07001678 // Ensure that keyboard focus starts from the last touched position.
1679 setSelectedPositionInt(position);
Alan Viveretted361a4f2014-06-30 16:47:40 -07001680 positionSelectorLikeTouch(position, child, x, y);
Alan Viverettec0502722013-08-15 18:05:52 -07001681
1682 // Refresh the drawable state to reflect the new pressed state,
1683 // which will also update the selector state.
1684 refreshDrawableState();
1685
1686 if (mClickAnimation != null) {
1687 mClickAnimation.cancel();
1688 mClickAnimation = null;
1689 }
1690 }
1691
1692 @Override
1693 boolean touchModeDrawsInPressedState() {
1694 return mDrawsInPressedState || super.touchModeDrawsInPressedState();
1695 }
1696
1697 /**
Adam Powellc3fa6302010-05-18 11:36:27 -07001698 * <p>Avoids jarring scrolling effect by ensuring that list elements
1699 * made of a text view fit on a single line.</p>
1700 *
1701 * @param position the item index in the list to get a view for
1702 * @return the view for the specified item
1703 */
1704 @Override
1705 View obtainView(int position, boolean[] isScrap) {
1706 View view = super.obtainView(position, isScrap);
1707
1708 if (view instanceof TextView) {
1709 ((TextView) view).setHorizontallyScrolling(true);
1710 }
1711
1712 return view;
1713 }
1714
1715 @Override
1716 public boolean isInTouchMode() {
1717 // WARNING: Please read the comment where mListSelectionHidden is declared
1718 return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
1719 }
1720
1721 /**
1722 * <p>Returns the focus state in the drop down.</p>
1723 *
1724 * @return true always if hijacking focus
1725 */
1726 @Override
1727 public boolean hasWindowFocus() {
1728 return mHijackFocus || super.hasWindowFocus();
1729 }
1730
1731 /**
1732 * <p>Returns the focus state in the drop down.</p>
1733 *
1734 * @return true always if hijacking focus
1735 */
1736 @Override
1737 public boolean isFocused() {
1738 return mHijackFocus || super.isFocused();
1739 }
1740
1741 /**
1742 * <p>Returns the focus state in the drop down.</p>
1743 *
1744 * @return true always if hijacking focus
1745 */
1746 @Override
1747 public boolean hasFocus() {
1748 return mHijackFocus || super.hasFocus();
1749 }
1750 }
1751
1752 private class PopupDataSetObserver extends DataSetObserver {
1753 @Override
1754 public void onChanged() {
1755 if (isShowing()) {
1756 // Resize the popup to fit new content
1757 show();
1758 }
1759 }
1760
1761 @Override
1762 public void onInvalidated() {
1763 dismiss();
1764 }
1765 }
1766
1767 private class ListSelectorHider implements Runnable {
1768 public void run() {
1769 clearListSelection();
1770 }
1771 }
1772
1773 private class ResizePopupRunnable implements Runnable {
1774 public void run() {
Adam Powell348e69c2011-02-16 16:49:50 -08001775 if (mDropDownList != null && mDropDownList.getCount() > mDropDownList.getChildCount() &&
1776 mDropDownList.getChildCount() <= mListItemExpandMaximum) {
1777 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
1778 show();
1779 }
Adam Powellc3fa6302010-05-18 11:36:27 -07001780 }
1781 }
1782
1783 private class PopupTouchInterceptor implements OnTouchListener {
1784 public boolean onTouch(View v, MotionEvent event) {
1785 final int action = event.getAction();
1786 final int x = (int) event.getX();
1787 final int y = (int) event.getY();
1788
1789 if (action == MotionEvent.ACTION_DOWN &&
1790 mPopup != null && mPopup.isShowing() &&
Gilles Debunne711734a2011-02-07 18:26:11 -08001791 (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) {
Adam Powellc3fa6302010-05-18 11:36:27 -07001792 mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
1793 } else if (action == MotionEvent.ACTION_UP) {
1794 mHandler.removeCallbacks(mResizePopupRunnable);
1795 }
1796 return false;
1797 }
1798 }
1799
1800 private class PopupScrollListener implements ListView.OnScrollListener {
1801 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1802 int totalItemCount) {
1803
1804 }
1805
1806 public void onScrollStateChanged(AbsListView view, int scrollState) {
1807 if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
1808 !isInputMethodNotNeeded() && mPopup.getContentView() != null) {
1809 mHandler.removeCallbacks(mResizePopupRunnable);
1810 mResizePopupRunnable.run();
1811 }
1812 }
1813 }
1814}