1 package com
.actionbarsherlock
.internal
.widget
;
3 import com
.actionbarsherlock
.R
;
5 import android
.content
.Context
;
6 import android
.content
.res
.Resources
;
7 import android
.database
.DataSetObserver
;
8 import android
.graphics
.Rect
;
9 import android
.graphics
.drawable
.Drawable
;
10 import android
.os
.Build
;
11 import android
.os
.Handler
;
12 import android
.util
.AttributeSet
;
13 import android
.view
.ContextThemeWrapper
;
14 import android
.view
.MotionEvent
;
15 import android
.view
.View
;
16 import android
.view
.View
.MeasureSpec
;
17 import android
.view
.View
.OnTouchListener
;
18 import android
.view
.ViewGroup
;
19 import android
.view
.ViewParent
;
20 import android
.widget
.AbsListView
;
21 import android
.widget
.AdapterView
;
22 import android
.widget
.LinearLayout
;
23 import android
.widget
.ListAdapter
;
24 import android
.widget
.ListView
;
25 import android
.widget
.PopupWindow
;
28 * A proxy between pre- and post-Honeycomb implementations of this class.
30 public class IcsListPopupWindow
{
32 * This value controls the length of time that the user
33 * must leave a pointer down without scrolling to expand
34 * the autocomplete dropdown list to cover the IME.
36 private static final int EXPAND_LIST_TIMEOUT
= 250;
38 private Context mContext
;
39 private PopupWindow mPopup
;
40 private ListAdapter mAdapter
;
41 private DropDownListView mDropDownList
;
43 private int mDropDownHeight
= ViewGroup
.LayoutParams
.WRAP_CONTENT
;
44 private int mDropDownWidth
= ViewGroup
.LayoutParams
.WRAP_CONTENT
;
45 private int mDropDownHorizontalOffset
;
46 private int mDropDownVerticalOffset
;
47 private boolean mDropDownVerticalOffsetSet
;
49 private int mListItemExpandMaximum
= Integer
.MAX_VALUE
;
51 private View mPromptView
;
52 private int mPromptPosition
= POSITION_PROMPT_ABOVE
;
54 private DataSetObserver mObserver
;
56 private View mDropDownAnchorView
;
58 private Drawable mDropDownListHighlight
;
60 private AdapterView
.OnItemClickListener mItemClickListener
;
61 private AdapterView
.OnItemSelectedListener mItemSelectedListener
;
63 private final ResizePopupRunnable mResizePopupRunnable
= new ResizePopupRunnable();
64 private final PopupTouchInterceptor mTouchInterceptor
= new PopupTouchInterceptor();
65 private final PopupScrollListener mScrollListener
= new PopupScrollListener();
66 private final ListSelectorHider mHideSelector
= new ListSelectorHider();
68 private Handler mHandler
= new Handler();
70 private Rect mTempRect
= new Rect();
72 private boolean mModal
;
74 public static final int POSITION_PROMPT_ABOVE
= 0;
75 public static final int POSITION_PROMPT_BELOW
= 1;
77 public IcsListPopupWindow(Context context
) {
78 this(context
, null
, R
.attr
.listPopupWindowStyle
);
81 public IcsListPopupWindow(Context context
, AttributeSet attrs
, int defStyleAttr
) {
83 mPopup
= new PopupWindow(context
, attrs
, defStyleAttr
);
84 mPopup
.setInputMethodMode(PopupWindow
.INPUT_METHOD_NEEDED
);
87 public IcsListPopupWindow(Context context
, AttributeSet attrs
, int defStyleAttr
, int defStyleRes
) {
89 if (Build
.VERSION
.SDK_INT
< Build
.VERSION_CODES
.HONEYCOMB
) {
90 Context wrapped
= new ContextThemeWrapper(context
, defStyleRes
);
91 mPopup
= new PopupWindow(wrapped
, attrs
, defStyleAttr
);
93 mPopup
= new PopupWindow(context
, attrs
, defStyleAttr
, defStyleRes
);
95 mPopup
.setInputMethodMode(PopupWindow
.INPUT_METHOD_NEEDED
);
98 public void setAdapter(ListAdapter adapter
) {
99 if (mObserver
== null
) {
100 mObserver
= new PopupDataSetObserver();
101 } else if (mAdapter
!= null
) {
102 mAdapter
.unregisterDataSetObserver(mObserver
);
105 if (mAdapter
!= null
) {
106 adapter
.registerDataSetObserver(mObserver
);
109 if (mDropDownList
!= null
) {
110 mDropDownList
.setAdapter(mAdapter
);
114 public void setPromptPosition(int position
) {
115 mPromptPosition
= position
;
118 public void setModal(boolean modal
) {
120 mPopup
.setFocusable(modal
);
123 public void setBackgroundDrawable(Drawable d
) {
124 mPopup
.setBackgroundDrawable(d
);
127 public void setAnchorView(View anchor
) {
128 mDropDownAnchorView
= anchor
;
131 public void setHorizontalOffset(int offset
) {
132 mDropDownHorizontalOffset
= offset
;
135 public void setVerticalOffset(int offset
) {
136 mDropDownVerticalOffset
= offset
;
137 mDropDownVerticalOffsetSet
= true
;
140 public void setContentWidth(int width
) {
141 Drawable popupBackground
= mPopup
.getBackground();
142 if (popupBackground
!= null
) {
143 popupBackground
.getPadding(mTempRect
);
144 mDropDownWidth
= mTempRect
.left
+ mTempRect
.right
+ width
;
146 mDropDownWidth
= width
;
150 public void setOnItemClickListener(AdapterView
.OnItemClickListener clickListener
) {
151 mItemClickListener
= clickListener
;
155 int height
= buildDropDown();
160 boolean noInputMethod
= isInputMethodNotNeeded();
161 //XXX mPopup.setAllowScrollingAnchorParent(!noInputMethod);
163 if (mPopup
.isShowing()) {
164 if (mDropDownWidth
== ViewGroup
.LayoutParams
.MATCH_PARENT
) {
165 // The call to PopupWindow's update method below can accept -1 for any
166 // value you do not want to update.
168 } else if (mDropDownWidth
== ViewGroup
.LayoutParams
.WRAP_CONTENT
) {
169 widthSpec
= mDropDownAnchorView
.getWidth();
171 widthSpec
= mDropDownWidth
;
174 if (mDropDownHeight
== ViewGroup
.LayoutParams
.MATCH_PARENT
) {
175 // The call to PopupWindow's update method below can accept -1 for any
176 // value you do not want to update.
177 heightSpec
= noInputMethod ? height
: ViewGroup
.LayoutParams
.MATCH_PARENT
;
179 mPopup
.setWindowLayoutMode(
180 mDropDownWidth
== ViewGroup
.LayoutParams
.MATCH_PARENT ?
181 ViewGroup
.LayoutParams
.MATCH_PARENT
: 0, 0);
183 mPopup
.setWindowLayoutMode(
184 mDropDownWidth
== ViewGroup
.LayoutParams
.MATCH_PARENT ?
185 ViewGroup
.LayoutParams
.MATCH_PARENT
: 0,
186 ViewGroup
.LayoutParams
.MATCH_PARENT
);
188 } else if (mDropDownHeight
== ViewGroup
.LayoutParams
.WRAP_CONTENT
) {
191 heightSpec
= mDropDownHeight
;
194 mPopup
.setOutsideTouchable(true
);
196 mPopup
.update(mDropDownAnchorView
, mDropDownHorizontalOffset
,
197 mDropDownVerticalOffset
, widthSpec
, heightSpec
);
199 if (mDropDownWidth
== ViewGroup
.LayoutParams
.MATCH_PARENT
) {
200 widthSpec
= ViewGroup
.LayoutParams
.MATCH_PARENT
;
202 if (mDropDownWidth
== ViewGroup
.LayoutParams
.WRAP_CONTENT
) {
203 mPopup
.setWidth(mDropDownAnchorView
.getWidth());
205 mPopup
.setWidth(mDropDownWidth
);
209 if (mDropDownHeight
== ViewGroup
.LayoutParams
.MATCH_PARENT
) {
210 heightSpec
= ViewGroup
.LayoutParams
.MATCH_PARENT
;
212 if (mDropDownHeight
== ViewGroup
.LayoutParams
.WRAP_CONTENT
) {
213 mPopup
.setHeight(height
);
215 mPopup
.setHeight(mDropDownHeight
);
219 mPopup
.setWindowLayoutMode(widthSpec
, heightSpec
);
220 //XXX mPopup.setClipToScreenEnabled(true);
222 // use outside touchable to dismiss drop down when touching outside of it, so
223 // only set this if the dropdown is not always visible
224 mPopup
.setOutsideTouchable(true
);
225 mPopup
.setTouchInterceptor(mTouchInterceptor
);
226 mPopup
.showAsDropDown(mDropDownAnchorView
,
227 mDropDownHorizontalOffset
, mDropDownVerticalOffset
);
228 mDropDownList
.setSelection(ListView
.INVALID_POSITION
);
230 if (!mModal
|| mDropDownList
.isInTouchMode()) {
231 clearListSelection();
234 mHandler
.post(mHideSelector
);
239 public void dismiss() {
241 if (mPromptView
!= null
) {
242 final ViewParent parent
= mPromptView
.getParent();
243 if (parent
instanceof ViewGroup
) {
244 final ViewGroup group
= (ViewGroup
) parent
;
245 group
.removeView(mPromptView
);
248 mPopup
.setContentView(null
);
249 mDropDownList
= null
;
250 mHandler
.removeCallbacks(mResizePopupRunnable
);
253 public void setOnDismissListener(PopupWindow
.OnDismissListener listener
) {
254 mPopup
.setOnDismissListener(listener
);
257 public void setInputMethodMode(int mode
) {
258 mPopup
.setInputMethodMode(mode
);
261 public void clearListSelection() {
262 final DropDownListView list
= mDropDownList
;
264 // WARNING: Please read the comment where mListSelectionHidden is declared
265 list
.mListSelectionHidden
= true
;
266 //XXX list.hideSelector();
267 list
.requestLayout();
271 public boolean isShowing() {
272 return mPopup
.isShowing();
275 private boolean isInputMethodNotNeeded() {
276 return mPopup
.getInputMethodMode() == PopupWindow
.INPUT_METHOD_NOT_NEEDED
;
279 public ListView
getListView() {
280 return mDropDownList
;
283 private int buildDropDown() {
284 ViewGroup dropDownView
;
285 int otherHeights
= 0;
287 if (mDropDownList
== null
) {
288 Context context
= mContext
;
290 mDropDownList
= new DropDownListView(context
, !mModal
);
291 if (mDropDownListHighlight
!= null
) {
292 mDropDownList
.setSelector(mDropDownListHighlight
);
294 mDropDownList
.setAdapter(mAdapter
);
295 mDropDownList
.setOnItemClickListener(mItemClickListener
);
296 mDropDownList
.setFocusable(true
);
297 mDropDownList
.setFocusableInTouchMode(true
);
298 mDropDownList
.setOnItemSelectedListener(new AdapterView
.OnItemSelectedListener() {
299 public void onItemSelected(AdapterView
<?
> parent
, View view
,
300 int position
, long id
) {
302 if (position
!= -1) {
303 DropDownListView dropDownList
= mDropDownList
;
305 if (dropDownList
!= null
) {
306 dropDownList
.mListSelectionHidden
= false
;
311 public void onNothingSelected(AdapterView
<?
> parent
) {
314 mDropDownList
.setOnScrollListener(mScrollListener
);
316 if (mItemSelectedListener
!= null
) {
317 mDropDownList
.setOnItemSelectedListener(mItemSelectedListener
);
320 dropDownView
= mDropDownList
;
322 View hintView
= mPromptView
;
323 if (hintView
!= null
) {
324 // if an hint has been specified, we accomodate more space for it and
325 // add a text view in the drop down menu, at the bottom of the list
326 LinearLayout hintContainer
= new LinearLayout(context
);
327 hintContainer
.setOrientation(LinearLayout
.VERTICAL
);
329 LinearLayout
.LayoutParams hintParams
= new LinearLayout
.LayoutParams(
330 ViewGroup
.LayoutParams
.MATCH_PARENT
, 0, 1.0f
333 switch (mPromptPosition
) {
334 case POSITION_PROMPT_BELOW
:
335 hintContainer
.addView(dropDownView
, hintParams
);
336 hintContainer
.addView(hintView
);
339 case POSITION_PROMPT_ABOVE
:
340 hintContainer
.addView(hintView
);
341 hintContainer
.addView(dropDownView
, hintParams
);
348 // measure the hint's height to find how much more vertical space
349 // we need to add to the drop down's height
350 int widthSpec
= MeasureSpec
.makeMeasureSpec(mDropDownWidth
, MeasureSpec
.AT_MOST
);
351 int heightSpec
= MeasureSpec
.UNSPECIFIED
;
352 hintView
.measure(widthSpec
, heightSpec
);
354 hintParams
= (LinearLayout
.LayoutParams
) hintView
.getLayoutParams();
355 otherHeights
= hintView
.getMeasuredHeight() + hintParams
.topMargin
356 + hintParams
.bottomMargin
;
358 dropDownView
= hintContainer
;
361 mPopup
.setContentView(dropDownView
);
363 dropDownView
= (ViewGroup
) mPopup
.getContentView();
364 final View view
= mPromptView
;
366 LinearLayout
.LayoutParams hintParams
=
367 (LinearLayout
.LayoutParams
) view
.getLayoutParams();
368 otherHeights
= view
.getMeasuredHeight() + hintParams
.topMargin
369 + hintParams
.bottomMargin
;
373 // getMaxAvailableHeight() subtracts the padding, so we put it back
374 // to get the available height for the whole window
376 Drawable background
= mPopup
.getBackground();
377 if (background
!= null
) {
378 background
.getPadding(mTempRect
);
379 padding
= mTempRect
.top
+ mTempRect
.bottom
;
381 // If we don't have an explicit vertical offset, determine one from the window
382 // background so that content will line up.
383 if (!mDropDownVerticalOffsetSet
) {
384 mDropDownVerticalOffset
= -mTempRect
.top
;
388 // Max height available on the screen for a popup.
389 boolean ignoreBottomDecorations
=
390 mPopup
.getInputMethodMode() == PopupWindow
.INPUT_METHOD_NOT_NEEDED
;
391 final int maxHeight
= /*mPopup.*/getMaxAvailableHeight(
392 mDropDownAnchorView
, mDropDownVerticalOffset
, ignoreBottomDecorations
);
394 if (mDropDownHeight
== ViewGroup
.LayoutParams
.MATCH_PARENT
) {
395 return maxHeight
+ padding
;
398 final int listContent
= /*mDropDownList.*/measureHeightOfChildren(MeasureSpec
.UNSPECIFIED
,
399 0, -1/*ListView.NO_POSITION*/, maxHeight
- otherHeights
, -1);
400 // add padding only if the list has items in it, that way we don't show
401 // the popup if it is not needed
402 if (listContent
> 0) otherHeights
+= padding
;
404 return listContent
+ otherHeights
;
407 private int getMaxAvailableHeight(View anchor
, int yOffset
, boolean ignoreBottomDecorations
) {
408 final Rect displayFrame
= new Rect();
409 anchor
.getWindowVisibleDisplayFrame(displayFrame
);
411 final int[] anchorPos
= new int[2];
412 anchor
.getLocationOnScreen(anchorPos
);
414 int bottomEdge
= displayFrame
.bottom
;
415 if (ignoreBottomDecorations
) {
416 Resources res
= anchor
.getContext().getResources();
417 bottomEdge
= res
.getDisplayMetrics().heightPixels
;
419 final int distanceToBottom
= bottomEdge
- (anchorPos
[1] + anchor
.getHeight()) - yOffset
;
420 final int distanceToTop
= anchorPos
[1] - displayFrame
.top
+ yOffset
;
422 // anchorPos[1] is distance from anchor to top of screen
423 int returnedHeight
= Math
.max(distanceToBottom
, distanceToTop
);
424 if (mPopup
.getBackground() != null
) {
425 mPopup
.getBackground().getPadding(mTempRect
);
426 returnedHeight
-= mTempRect
.top
+ mTempRect
.bottom
;
429 return returnedHeight
;
432 private int measureHeightOfChildren(int widthMeasureSpec
, int startPosition
, int endPosition
,
433 final int maxHeight
, int disallowPartialChildPosition
) {
435 final ListAdapter adapter
= mAdapter
;
436 if (adapter
== null
) {
437 return mDropDownList
.getListPaddingTop() + mDropDownList
.getListPaddingBottom();
440 // Include the padding of the list
441 int returnedHeight
= mDropDownList
.getListPaddingTop() + mDropDownList
.getListPaddingBottom();
442 final int dividerHeight
= ((mDropDownList
.getDividerHeight() > 0) && mDropDownList
.getDivider() != null
) ? mDropDownList
.getDividerHeight() : 0;
443 // The previous height value that was less than maxHeight and contained
444 // no partial children
445 int prevHeightWithoutPartialChild
= 0;
449 // mItemCount - 1 since endPosition parameter is inclusive
450 endPosition
= (endPosition
== -1/*NO_POSITION*/) ? adapter
.getCount() - 1 : endPosition
;
452 for (i
= startPosition
; i
<= endPosition
; ++i
) {
453 child
= mAdapter
.getView(i
, null
, mDropDownList
);
454 if (mDropDownList
.getCacheColorHint() != 0) {
455 child
.setDrawingCacheBackgroundColor(mDropDownList
.getCacheColorHint());
458 measureScrapChild(child
, i
, widthMeasureSpec
);
461 // Count the divider for all but one child
462 returnedHeight
+= dividerHeight
;
465 returnedHeight
+= child
.getMeasuredHeight();
467 if (returnedHeight
>= maxHeight
) {
468 // We went over, figure out which height to return. If returnedHeight > maxHeight,
469 // then the i'th position did not fit completely.
470 return (disallowPartialChildPosition
>= 0) // Disallowing is enabled (> -1)
471 && (i
> disallowPartialChildPosition
) // We've past the min pos
472 && (prevHeightWithoutPartialChild
> 0) // We have a prev height
473 && (returnedHeight
!= maxHeight
) // i'th child did not fit completely
474 ? prevHeightWithoutPartialChild
478 if ((disallowPartialChildPosition
>= 0) && (i
>= disallowPartialChildPosition
)) {
479 prevHeightWithoutPartialChild
= returnedHeight
;
483 // At this point, we went through the range of children, and they each
484 // completely fit, so return the returnedHeight
485 return returnedHeight
;
487 private void measureScrapChild(View child
, int position
, int widthMeasureSpec
) {
488 ListView
.LayoutParams p
= (ListView
.LayoutParams
) child
.getLayoutParams();
490 p
= new ListView
.LayoutParams(ViewGroup
.LayoutParams
.MATCH_PARENT
,
491 ViewGroup
.LayoutParams
.WRAP_CONTENT
, 0);
492 child
.setLayoutParams(p
);
494 //XXX p.viewType = mAdapter.getItemViewType(position);
495 //XXX p.forceAdd = true;
497 int childWidthSpec
= ViewGroup
.getChildMeasureSpec(widthMeasureSpec
,
498 mDropDownList
.getPaddingLeft() + mDropDownList
.getPaddingRight(), p
.width
);
499 int lpHeight
= p
.height
;
502 childHeightSpec
= MeasureSpec
.makeMeasureSpec(lpHeight
, MeasureSpec
.EXACTLY
);
504 childHeightSpec
= MeasureSpec
.makeMeasureSpec(0, MeasureSpec
.UNSPECIFIED
);
506 child
.measure(childWidthSpec
, childHeightSpec
);
509 private static class DropDownListView
extends ListView
{
511 * WARNING: This is a workaround for a touch mode issue.
513 * Touch mode is propagated lazily to windows. This causes problems in
514 * the following scenario:
515 * - Type something in the AutoCompleteTextView and get some results
516 * - Move down with the d-pad to select an item in the list
517 * - Move up with the d-pad until the selection disappears
518 * - Type more text in the AutoCompleteTextView *using the soft keyboard*
519 * and get new results; you are now in touch mode
520 * - The selection comes back on the first item in the list, even though
521 * the list is supposed to be in touch mode
523 * Using the soft keyboard triggers the touch mode change but that change
524 * is propagated to our window only after the first list layout, therefore
525 * after the list attempts to resurrect the selection.
527 * The trick to work around this issue is to pretend the list is in touch
528 * mode when we know that the selection should not appear, that is when
529 * we know the user moved the selection away from the list.
531 * This boolean is set to true whenever we explicitly hide the list's
532 * selection and reset to false whenever we know the user moved the
533 * selection back to the list.
535 * When this boolean is true, isInTouchMode() returns true, otherwise it
536 * returns super.isInTouchMode().
538 private boolean mListSelectionHidden
;
540 private boolean mHijackFocus
;
542 public DropDownListView(Context context
, boolean hijackFocus
) {
543 super(context
, null
, /*com.android.internal.*/R
.attr
.dropDownListViewStyle
);
544 mHijackFocus
= hijackFocus
;
545 // TODO: Add an API to control this
546 setCacheColorHint(0); // Transparent, since the background drawable could be anything.
550 //View obtainView(int position, boolean[] isScrap) {
551 // View view = super.obtainView(position, isScrap);
553 // if (view instanceof TextView) {
554 // ((TextView) view).setHorizontallyScrolling(true);
561 public boolean isInTouchMode() {
562 // WARNING: Please read the comment where mListSelectionHidden is declared
563 return (mHijackFocus
&& mListSelectionHidden
) || super.isInTouchMode();
567 public boolean hasWindowFocus() {
568 return mHijackFocus
|| super.hasWindowFocus();
572 public boolean isFocused() {
573 return mHijackFocus
|| super.isFocused();
577 public boolean hasFocus() {
578 return mHijackFocus
|| super.hasFocus();
582 private class PopupDataSetObserver
extends DataSetObserver
{
584 public void onChanged() {
586 // Resize the popup to fit new content
592 public void onInvalidated() {
597 private class ListSelectorHider
implements Runnable
{
599 clearListSelection();
603 private class ResizePopupRunnable
implements Runnable
{
605 if (mDropDownList
!= null
&& mDropDownList
.getCount() > mDropDownList
.getChildCount() &&
606 mDropDownList
.getChildCount() <= mListItemExpandMaximum
) {
607 mPopup
.setInputMethodMode(PopupWindow
.INPUT_METHOD_NOT_NEEDED
);
613 private class PopupTouchInterceptor
implements OnTouchListener
{
614 public boolean onTouch(View v
, MotionEvent event
) {
615 final int action
= event
.getAction();
616 final int x
= (int) event
.getX();
617 final int y
= (int) event
.getY();
619 if (action
== MotionEvent
.ACTION_DOWN
&&
620 mPopup
!= null
&& mPopup
.isShowing() &&
621 (x
>= 0 && x
< mPopup
.getWidth() && y
>= 0 && y
< mPopup
.getHeight())) {
622 mHandler
.postDelayed(mResizePopupRunnable
, EXPAND_LIST_TIMEOUT
);
623 } else if (action
== MotionEvent
.ACTION_UP
) {
624 mHandler
.removeCallbacks(mResizePopupRunnable
);
630 private class PopupScrollListener
implements ListView
.OnScrollListener
{
631 public void onScroll(AbsListView view
, int firstVisibleItem
, int visibleItemCount
,
632 int totalItemCount
) {
636 public void onScrollStateChanged(AbsListView view
, int scrollState
) {
637 if (scrollState
== SCROLL_STATE_TOUCH_SCROLL
&&
638 !isInputMethodNotNeeded() && mPopup
.getContentView() != null
) {
639 mHandler
.removeCallbacks(mResizePopupRunnable
);
640 mResizePopupRunnable
.run();