d13c6cea9720d4140bfcf88b50c3363ba4c876ee
[pub/Android/ownCloud.git] / actionbarsherlock / src / com / actionbarsherlock / internal / widget / IcsListPopupWindow.java
1 package com.actionbarsherlock.internal.widget;
2
3 import com.actionbarsherlock.R;
4
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;
26
27 /**
28 * A proxy between pre- and post-Honeycomb implementations of this class.
29 */
30 public class IcsListPopupWindow {
31 /**
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.
35 */
36 private static final int EXPAND_LIST_TIMEOUT = 250;
37
38 private Context mContext;
39 private PopupWindow mPopup;
40 private ListAdapter mAdapter;
41 private DropDownListView mDropDownList;
42
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;
48
49 private int mListItemExpandMaximum = Integer.MAX_VALUE;
50
51 private View mPromptView;
52 private int mPromptPosition = POSITION_PROMPT_ABOVE;
53
54 private DataSetObserver mObserver;
55
56 private View mDropDownAnchorView;
57
58 private Drawable mDropDownListHighlight;
59
60 private AdapterView.OnItemClickListener mItemClickListener;
61 private AdapterView.OnItemSelectedListener mItemSelectedListener;
62
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();
67
68 private Handler mHandler = new Handler();
69
70 private Rect mTempRect = new Rect();
71
72 private boolean mModal;
73
74 public static final int POSITION_PROMPT_ABOVE = 0;
75 public static final int POSITION_PROMPT_BELOW = 1;
76
77 public IcsListPopupWindow(Context context) {
78 this(context, null, R.attr.listPopupWindowStyle);
79 }
80
81 public IcsListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr) {
82 mContext = context;
83 mPopup = new PopupWindow(context, attrs, defStyleAttr);
84 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
85 }
86
87 public IcsListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
88 mContext = context;
89 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
90 Context wrapped = new ContextThemeWrapper(context, defStyleRes);
91 mPopup = new PopupWindow(wrapped, attrs, defStyleAttr);
92 } else {
93 mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes);
94 }
95 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
96 }
97
98 public void setAdapter(ListAdapter adapter) {
99 if (mObserver == null) {
100 mObserver = new PopupDataSetObserver();
101 } else if (mAdapter != null) {
102 mAdapter.unregisterDataSetObserver(mObserver);
103 }
104 mAdapter = adapter;
105 if (mAdapter != null) {
106 adapter.registerDataSetObserver(mObserver);
107 }
108
109 if (mDropDownList != null) {
110 mDropDownList.setAdapter(mAdapter);
111 }
112 }
113
114 public void setPromptPosition(int position) {
115 mPromptPosition = position;
116 }
117
118 public void setModal(boolean modal) {
119 mModal = true;
120 mPopup.setFocusable(modal);
121 }
122
123 public void setBackgroundDrawable(Drawable d) {
124 mPopup.setBackgroundDrawable(d);
125 }
126
127 public void setAnchorView(View anchor) {
128 mDropDownAnchorView = anchor;
129 }
130
131 public void setHorizontalOffset(int offset) {
132 mDropDownHorizontalOffset = offset;
133 }
134
135 public void setVerticalOffset(int offset) {
136 mDropDownVerticalOffset = offset;
137 mDropDownVerticalOffsetSet = true;
138 }
139
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;
145 } else {
146 mDropDownWidth = width;
147 }
148 }
149
150 public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) {
151 mItemClickListener = clickListener;
152 }
153
154 public void show() {
155 int height = buildDropDown();
156
157 int widthSpec = 0;
158 int heightSpec = 0;
159
160 boolean noInputMethod = isInputMethodNotNeeded();
161 //XXX mPopup.setAllowScrollingAnchorParent(!noInputMethod);
162
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.
167 widthSpec = -1;
168 } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
169 widthSpec = mDropDownAnchorView.getWidth();
170 } else {
171 widthSpec = mDropDownWidth;
172 }
173
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;
178 if (noInputMethod) {
179 mPopup.setWindowLayoutMode(
180 mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
181 ViewGroup.LayoutParams.MATCH_PARENT : 0, 0);
182 } else {
183 mPopup.setWindowLayoutMode(
184 mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
185 ViewGroup.LayoutParams.MATCH_PARENT : 0,
186 ViewGroup.LayoutParams.MATCH_PARENT);
187 }
188 } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
189 heightSpec = height;
190 } else {
191 heightSpec = mDropDownHeight;
192 }
193
194 mPopup.setOutsideTouchable(true);
195
196 mPopup.update(mDropDownAnchorView, mDropDownHorizontalOffset,
197 mDropDownVerticalOffset, widthSpec, heightSpec);
198 } else {
199 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
200 widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
201 } else {
202 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
203 mPopup.setWidth(mDropDownAnchorView.getWidth());
204 } else {
205 mPopup.setWidth(mDropDownWidth);
206 }
207 }
208
209 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
210 heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
211 } else {
212 if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
213 mPopup.setHeight(height);
214 } else {
215 mPopup.setHeight(mDropDownHeight);
216 }
217 }
218
219 mPopup.setWindowLayoutMode(widthSpec, heightSpec);
220 //XXX mPopup.setClipToScreenEnabled(true);
221
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);
229
230 if (!mModal || mDropDownList.isInTouchMode()) {
231 clearListSelection();
232 }
233 if (!mModal) {
234 mHandler.post(mHideSelector);
235 }
236 }
237 }
238
239 public void dismiss() {
240 mPopup.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);
246 }
247 }
248 mPopup.setContentView(null);
249 mDropDownList = null;
250 mHandler.removeCallbacks(mResizePopupRunnable);
251 }
252
253 public void setOnDismissListener(PopupWindow.OnDismissListener listener) {
254 mPopup.setOnDismissListener(listener);
255 }
256
257 public void setInputMethodMode(int mode) {
258 mPopup.setInputMethodMode(mode);
259 }
260
261 public void clearListSelection() {
262 final DropDownListView list = mDropDownList;
263 if (list != null) {
264 // WARNING: Please read the comment where mListSelectionHidden is declared
265 list.mListSelectionHidden = true;
266 //XXX list.hideSelector();
267 list.requestLayout();
268 }
269 }
270
271 public boolean isShowing() {
272 return mPopup.isShowing();
273 }
274
275 private boolean isInputMethodNotNeeded() {
276 return mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
277 }
278
279 public ListView getListView() {
280 return mDropDownList;
281 }
282
283 private int buildDropDown() {
284 ViewGroup dropDownView;
285 int otherHeights = 0;
286
287 if (mDropDownList == null) {
288 Context context = mContext;
289
290 mDropDownList = new DropDownListView(context, !mModal);
291 if (mDropDownListHighlight != null) {
292 mDropDownList.setSelector(mDropDownListHighlight);
293 }
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) {
301
302 if (position != -1) {
303 DropDownListView dropDownList = mDropDownList;
304
305 if (dropDownList != null) {
306 dropDownList.mListSelectionHidden = false;
307 }
308 }
309 }
310
311 public void onNothingSelected(AdapterView<?> parent) {
312 }
313 });
314 mDropDownList.setOnScrollListener(mScrollListener);
315
316 if (mItemSelectedListener != null) {
317 mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
318 }
319
320 dropDownView = mDropDownList;
321
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);
328
329 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
330 ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
331 );
332
333 switch (mPromptPosition) {
334 case POSITION_PROMPT_BELOW:
335 hintContainer.addView(dropDownView, hintParams);
336 hintContainer.addView(hintView);
337 break;
338
339 case POSITION_PROMPT_ABOVE:
340 hintContainer.addView(hintView);
341 hintContainer.addView(dropDownView, hintParams);
342 break;
343
344 default:
345 break;
346 }
347
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);
353
354 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
355 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
356 + hintParams.bottomMargin;
357
358 dropDownView = hintContainer;
359 }
360
361 mPopup.setContentView(dropDownView);
362 } else {
363 dropDownView = (ViewGroup) mPopup.getContentView();
364 final View view = mPromptView;
365 if (view != null) {
366 LinearLayout.LayoutParams hintParams =
367 (LinearLayout.LayoutParams) view.getLayoutParams();
368 otherHeights = view.getMeasuredHeight() + hintParams.topMargin
369 + hintParams.bottomMargin;
370 }
371 }
372
373 // getMaxAvailableHeight() subtracts the padding, so we put it back
374 // to get the available height for the whole window
375 int padding = 0;
376 Drawable background = mPopup.getBackground();
377 if (background != null) {
378 background.getPadding(mTempRect);
379 padding = mTempRect.top + mTempRect.bottom;
380
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;
385 }
386 }
387
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);
393
394 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
395 return maxHeight + padding;
396 }
397
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;
403
404 return listContent + otherHeights;
405 }
406
407 private int getMaxAvailableHeight(View anchor, int yOffset, boolean ignoreBottomDecorations) {
408 final Rect displayFrame = new Rect();
409 anchor.getWindowVisibleDisplayFrame(displayFrame);
410
411 final int[] anchorPos = new int[2];
412 anchor.getLocationOnScreen(anchorPos);
413
414 int bottomEdge = displayFrame.bottom;
415 if (ignoreBottomDecorations) {
416 Resources res = anchor.getContext().getResources();
417 bottomEdge = res.getDisplayMetrics().heightPixels;
418 }
419 final int distanceToBottom = bottomEdge - (anchorPos[1] + anchor.getHeight()) - yOffset;
420 final int distanceToTop = anchorPos[1] - displayFrame.top + yOffset;
421
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;
427 }
428
429 return returnedHeight;
430 }
431
432 private int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
433 final int maxHeight, int disallowPartialChildPosition) {
434
435 final ListAdapter adapter = mAdapter;
436 if (adapter == null) {
437 return mDropDownList.getListPaddingTop() + mDropDownList.getListPaddingBottom();
438 }
439
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;
446 int i;
447 View child;
448
449 // mItemCount - 1 since endPosition parameter is inclusive
450 endPosition = (endPosition == -1/*NO_POSITION*/) ? adapter.getCount() - 1 : endPosition;
451
452 for (i = startPosition; i <= endPosition; ++i) {
453 child = mAdapter.getView(i, null, mDropDownList);
454 if (mDropDownList.getCacheColorHint() != 0) {
455 child.setDrawingCacheBackgroundColor(mDropDownList.getCacheColorHint());
456 }
457
458 measureScrapChild(child, i, widthMeasureSpec);
459
460 if (i > 0) {
461 // Count the divider for all but one child
462 returnedHeight += dividerHeight;
463 }
464
465 returnedHeight += child.getMeasuredHeight();
466
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
475 : maxHeight;
476 }
477
478 if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
479 prevHeightWithoutPartialChild = returnedHeight;
480 }
481 }
482
483 // At this point, we went through the range of children, and they each
484 // completely fit, so return the returnedHeight
485 return returnedHeight;
486 }
487 private void measureScrapChild(View child, int position, int widthMeasureSpec) {
488 ListView.LayoutParams p = (ListView.LayoutParams) child.getLayoutParams();
489 if (p == null) {
490 p = new ListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
491 ViewGroup.LayoutParams.WRAP_CONTENT, 0);
492 child.setLayoutParams(p);
493 }
494 //XXX p.viewType = mAdapter.getItemViewType(position);
495 //XXX p.forceAdd = true;
496
497 int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
498 mDropDownList.getPaddingLeft() + mDropDownList.getPaddingRight(), p.width);
499 int lpHeight = p.height;
500 int childHeightSpec;
501 if (lpHeight > 0) {
502 childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
503 } else {
504 childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
505 }
506 child.measure(childWidthSpec, childHeightSpec);
507 }
508
509 private static class DropDownListView extends ListView {
510 /*
511 * WARNING: This is a workaround for a touch mode issue.
512 *
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
522 *
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.
526 *
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.
530 *
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.
534 *
535 * When this boolean is true, isInTouchMode() returns true, otherwise it
536 * returns super.isInTouchMode().
537 */
538 private boolean mListSelectionHidden;
539
540 private boolean mHijackFocus;
541
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.
547 }
548
549 //XXX @Override
550 //View obtainView(int position, boolean[] isScrap) {
551 // View view = super.obtainView(position, isScrap);
552
553 // if (view instanceof TextView) {
554 // ((TextView) view).setHorizontallyScrolling(true);
555 // }
556
557 // return view;
558 //}
559
560 @Override
561 public boolean isInTouchMode() {
562 // WARNING: Please read the comment where mListSelectionHidden is declared
563 return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
564 }
565
566 @Override
567 public boolean hasWindowFocus() {
568 return mHijackFocus || super.hasWindowFocus();
569 }
570
571 @Override
572 public boolean isFocused() {
573 return mHijackFocus || super.isFocused();
574 }
575
576 @Override
577 public boolean hasFocus() {
578 return mHijackFocus || super.hasFocus();
579 }
580 }
581
582 private class PopupDataSetObserver extends DataSetObserver {
583 @Override
584 public void onChanged() {
585 if (isShowing()) {
586 // Resize the popup to fit new content
587 show();
588 }
589 }
590
591 @Override
592 public void onInvalidated() {
593 dismiss();
594 }
595 }
596
597 private class ListSelectorHider implements Runnable {
598 public void run() {
599 clearListSelection();
600 }
601 }
602
603 private class ResizePopupRunnable implements Runnable {
604 public void run() {
605 if (mDropDownList != null && mDropDownList.getCount() > mDropDownList.getChildCount() &&
606 mDropDownList.getChildCount() <= mListItemExpandMaximum) {
607 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
608 show();
609 }
610 }
611 }
612
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();
618
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);
625 }
626 return false;
627 }
628 }
629
630 private class PopupScrollListener implements ListView.OnScrollListener {
631 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
632 int totalItemCount) {
633
634 }
635
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();
641 }
642 }
643 }
644 }