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();