com.actionbarsherlock.internal.widget.IcsListPopupWindow Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sample Show documentation
Show all versions of sample Show documentation
Android library for better number/date/time-picker DialogFragments.
package com.actionbarsherlock.internal.widget;
import com.actionbarsherlock.R;
import android.content.Context;
import android.content.res.Resources;
import android.database.DataSetObserver;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.PopupWindow;
/**
* A proxy between pre- and post-Honeycomb implementations of this class.
*/
public class IcsListPopupWindow {
/**
* This value controls the length of time that the user
* must leave a pointer down without scrolling to expand
* the autocomplete dropdown list to cover the IME.
*/
private static final int EXPAND_LIST_TIMEOUT = 250;
private Context mContext;
private final PopupWindowCompat mPopup;
private ListAdapter mAdapter;
private DropDownListView mDropDownList;
private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
private int mDropDownHorizontalOffset;
private int mDropDownVerticalOffset;
private boolean mDropDownVerticalOffsetSet;
private int mListItemExpandMaximum = Integer.MAX_VALUE;
private View mPromptView;
private int mPromptPosition = POSITION_PROMPT_ABOVE;
private DataSetObserver mObserver;
private View mDropDownAnchorView;
private Drawable mDropDownListHighlight;
private AdapterView.OnItemClickListener mItemClickListener;
private AdapterView.OnItemSelectedListener mItemSelectedListener;
private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable();
private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor();
private final PopupScrollListener mScrollListener = new PopupScrollListener();
private final ListSelectorHider mHideSelector = new ListSelectorHider();
private Handler mHandler = new Handler();
private Rect mTempRect = new Rect();
private boolean mModal;
public static final int POSITION_PROMPT_ABOVE = 0;
public static final int POSITION_PROMPT_BELOW = 1;
public IcsListPopupWindow(Context context) {
this(context, null, R.attr.listPopupWindowStyle);
}
public IcsListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr) {
mContext = context;
mPopup = new PopupWindowCompat(context, attrs, defStyleAttr);
mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
}
public IcsListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
mContext = context;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
Context wrapped = new ContextThemeWrapper(context, defStyleRes);
mPopup = new PopupWindowCompat(wrapped, attrs, defStyleAttr);
} else {
mPopup = new PopupWindowCompat(context, attrs, defStyleAttr, defStyleRes);
}
mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
}
public void setAdapter(ListAdapter adapter) {
if (mObserver == null) {
mObserver = new PopupDataSetObserver();
} else if (mAdapter != null) {
mAdapter.unregisterDataSetObserver(mObserver);
}
mAdapter = adapter;
if (mAdapter != null) {
adapter.registerDataSetObserver(mObserver);
}
if (mDropDownList != null) {
mDropDownList.setAdapter(mAdapter);
}
}
public void setPromptPosition(int position) {
mPromptPosition = position;
}
public void setModal(boolean modal) {
mModal = true;
mPopup.setFocusable(modal);
}
public void setBackgroundDrawable(Drawable d) {
mPopup.setBackgroundDrawable(d);
}
public void setAnchorView(View anchor) {
mDropDownAnchorView = anchor;
}
public void setHorizontalOffset(int offset) {
mDropDownHorizontalOffset = offset;
}
public void setVerticalOffset(int offset) {
mDropDownVerticalOffset = offset;
mDropDownVerticalOffsetSet = true;
}
public void setContentWidth(int width) {
Drawable popupBackground = mPopup.getBackground();
if (popupBackground != null) {
popupBackground.getPadding(mTempRect);
mDropDownWidth = mTempRect.left + mTempRect.right + width;
} else {
mDropDownWidth = width;
}
}
public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) {
mItemClickListener = clickListener;
}
public void show() {
int height = buildDropDown();
int widthSpec = 0;
int heightSpec = 0;
boolean noInputMethod = isInputMethodNotNeeded();
//XXX mPopup.setAllowScrollingAnchorParent(!noInputMethod);
if (mPopup.isShowing()) {
if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
// The call to PopupWindow's update method below can accept -1 for any
// value you do not want to update.
widthSpec = -1;
} else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
widthSpec = mDropDownAnchorView.getWidth();
} else {
widthSpec = mDropDownWidth;
}
if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
// The call to PopupWindow's update method below can accept -1 for any
// value you do not want to update.
heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT;
if (noInputMethod) {
mPopup.setWindowLayoutMode(
mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
ViewGroup.LayoutParams.MATCH_PARENT : 0, 0);
} else {
mPopup.setWindowLayoutMode(
mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
ViewGroup.LayoutParams.MATCH_PARENT : 0,
ViewGroup.LayoutParams.MATCH_PARENT);
}
} else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
heightSpec = height;
} else {
heightSpec = mDropDownHeight;
}
mPopup.setOutsideTouchable(true);
mPopup.update(mDropDownAnchorView, mDropDownHorizontalOffset,
mDropDownVerticalOffset, widthSpec, heightSpec);
} else {
if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
} else {
if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
mPopup.setWidth(mDropDownAnchorView.getWidth());
} else {
mPopup.setWidth(mDropDownWidth);
}
}
if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
} else {
if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
mPopup.setHeight(height);
} else {
mPopup.setHeight(mDropDownHeight);
}
}
mPopup.setWindowLayoutMode(widthSpec, heightSpec);
//XXX mPopup.setClipToScreenEnabled(true);
// use outside touchable to dismiss drop down when touching outside of it, so
// only set this if the dropdown is not always visible
mPopup.setOutsideTouchable(true);
mPopup.setTouchInterceptor(mTouchInterceptor);
mPopup.showAsDropDown(mDropDownAnchorView,
mDropDownHorizontalOffset, mDropDownVerticalOffset);
mDropDownList.setSelection(ListView.INVALID_POSITION);
if (!mModal || mDropDownList.isInTouchMode()) {
clearListSelection();
}
if (!mModal) {
mHandler.post(mHideSelector);
}
}
}
public void dismiss() {
mPopup.dismiss();
if (mPromptView != null) {
final ViewParent parent = mPromptView.getParent();
if (parent instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) parent;
group.removeView(mPromptView);
}
}
mPopup.setContentView(null);
mDropDownList = null;
mHandler.removeCallbacks(mResizePopupRunnable);
}
public void setOnDismissListener(PopupWindow.OnDismissListener listener) {
mPopup.setOnDismissListener(listener);
}
public void setInputMethodMode(int mode) {
mPopup.setInputMethodMode(mode);
}
/**
* Set the selected position of the list.
* Only valid when {@link #isShowing()} == {@code true}.
*
* @param position List position to set as selected.
*/
public void setSelection(int position) {
DropDownListView list = mDropDownList;
if (isShowing() && list != null) {
list.mListSelectionHidden = false;
list.setSelection(position);
if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) {
list.setItemChecked(position, true);
}
}
}
public void clearListSelection() {
final DropDownListView list = mDropDownList;
if (list != null) {
// WARNING: Please read the comment where mListSelectionHidden is declared
list.mListSelectionHidden = true;
//XXX list.hideSelector();
list.requestLayout();
}
}
public boolean isShowing() {
return mPopup.isShowing();
}
private boolean isInputMethodNotNeeded() {
return mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
}
public ListView getListView() {
return mDropDownList;
}
private int buildDropDown() {
ViewGroup dropDownView;
int otherHeights = 0;
if (mDropDownList == null) {
Context context = mContext;
mDropDownList = new DropDownListView(context, !mModal);
if (mDropDownListHighlight != null) {
mDropDownList.setSelector(mDropDownListHighlight);
}
mDropDownList.setAdapter(mAdapter);
mDropDownList.setOnItemClickListener(mItemClickListener);
mDropDownList.setFocusable(true);
mDropDownList.setFocusableInTouchMode(true);
mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
public void onItemSelected(AdapterView> parent, View view,
int position, long id) {
if (position != -1) {
DropDownListView dropDownList = mDropDownList;
if (dropDownList != null) {
dropDownList.mListSelectionHidden = false;
}
}
}
public void onNothingSelected(AdapterView> parent) {
}
});
mDropDownList.setOnScrollListener(mScrollListener);
if (mItemSelectedListener != null) {
mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
}
dropDownView = mDropDownList;
View hintView = mPromptView;
if (hintView != null) {
// if an hint has been specified, we accomodate more space for it and
// add a text view in the drop down menu, at the bottom of the list
LinearLayout hintContainer = new LinearLayout(context);
hintContainer.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
);
switch (mPromptPosition) {
case POSITION_PROMPT_BELOW:
hintContainer.addView(dropDownView, hintParams);
hintContainer.addView(hintView);
break;
case POSITION_PROMPT_ABOVE:
hintContainer.addView(hintView);
hintContainer.addView(dropDownView, hintParams);
break;
default:
break;
}
// measure the hint's height to find how much more vertical space
// we need to add to the drop down's height
int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST);
int heightSpec = MeasureSpec.UNSPECIFIED;
hintView.measure(widthSpec, heightSpec);
hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
+ hintParams.bottomMargin;
dropDownView = hintContainer;
}
mPopup.setContentView(dropDownView);
} else {
dropDownView = (ViewGroup) mPopup.getContentView();
final View view = mPromptView;
if (view != null) {
LinearLayout.LayoutParams hintParams =
(LinearLayout.LayoutParams) view.getLayoutParams();
otherHeights = view.getMeasuredHeight() + hintParams.topMargin
+ hintParams.bottomMargin;
}
}
// getMaxAvailableHeight() subtracts the padding, so we put it back
// to get the available height for the whole window
int padding = 0;
Drawable background = mPopup.getBackground();
if (background != null) {
background.getPadding(mTempRect);
padding = mTempRect.top + mTempRect.bottom;
// If we don't have an explicit vertical offset, determine one from the window
// background so that content will line up.
if (!mDropDownVerticalOffsetSet) {
mDropDownVerticalOffset = -mTempRect.top;
}
}
// Max height available on the screen for a popup.
boolean ignoreBottomDecorations =
mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
final int maxHeight = /*mPopup.*/getMaxAvailableHeight(
mDropDownAnchorView, mDropDownVerticalOffset, ignoreBottomDecorations);
if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
return maxHeight + padding;
}
final int listContent = /*mDropDownList.*/measureHeightOfChildren(MeasureSpec.UNSPECIFIED,
0, -1/*ListView.NO_POSITION*/, maxHeight - otherHeights, -1);
// add padding only if the list has items in it, that way we don't show
// the popup if it is not needed
if (listContent > 0) otherHeights += padding;
return listContent + otherHeights;
}
private int getMaxAvailableHeight(View anchor, int yOffset, boolean ignoreBottomDecorations) {
final Rect displayFrame = new Rect();
anchor.getWindowVisibleDisplayFrame(displayFrame);
final int[] anchorPos = new int[2];
anchor.getLocationOnScreen(anchorPos);
int bottomEdge = displayFrame.bottom;
if (ignoreBottomDecorations) {
Resources res = anchor.getContext().getResources();
bottomEdge = res.getDisplayMetrics().heightPixels;
}
final int distanceToBottom = bottomEdge - (anchorPos[1] + anchor.getHeight()) - yOffset;
final int distanceToTop = anchorPos[1] - displayFrame.top + yOffset;
// anchorPos[1] is distance from anchor to top of screen
int returnedHeight = Math.max(distanceToBottom, distanceToTop);
if (mPopup.getBackground() != null) {
mPopup.getBackground().getPadding(mTempRect);
returnedHeight -= mTempRect.top + mTempRect.bottom;
}
return returnedHeight;
}
private int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
final int maxHeight, int disallowPartialChildPosition) {
final ListAdapter adapter = mAdapter;
if (adapter == null) {
return mDropDownList.getListPaddingTop() + mDropDownList.getListPaddingBottom();
}
// Include the padding of the list
int returnedHeight = mDropDownList.getListPaddingTop() + mDropDownList.getListPaddingBottom();
final int dividerHeight = ((mDropDownList.getDividerHeight() > 0) && mDropDownList.getDivider() != null) ? mDropDownList.getDividerHeight() : 0;
// The previous height value that was less than maxHeight and contained
// no partial children
int prevHeightWithoutPartialChild = 0;
int i;
View child;
// mItemCount - 1 since endPosition parameter is inclusive
endPosition = (endPosition == -1/*NO_POSITION*/) ? adapter.getCount() - 1 : endPosition;
for (i = startPosition; i <= endPosition; ++i) {
child = mAdapter.getView(i, null, mDropDownList);
if (mDropDownList.getCacheColorHint() != 0) {
child.setDrawingCacheBackgroundColor(mDropDownList.getCacheColorHint());
}
measureScrapChild(child, i, widthMeasureSpec);
if (i > 0) {
// Count the divider for all but one child
returnedHeight += dividerHeight;
}
returnedHeight += child.getMeasuredHeight();
if (returnedHeight >= maxHeight) {
// We went over, figure out which height to return. If returnedHeight > maxHeight,
// then the i'th position did not fit completely.
return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
&& (i > disallowPartialChildPosition) // We've past the min pos
&& (prevHeightWithoutPartialChild > 0) // We have a prev height
&& (returnedHeight != maxHeight) // i'th child did not fit completely
? prevHeightWithoutPartialChild
: maxHeight;
}
if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
prevHeightWithoutPartialChild = returnedHeight;
}
}
// At this point, we went through the range of children, and they each
// completely fit, so return the returnedHeight
return returnedHeight;
}
private void measureScrapChild(View child, int position, int widthMeasureSpec) {
ListView.LayoutParams p = (ListView.LayoutParams) child.getLayoutParams();
if (p == null) {
p = new ListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT, 0);
child.setLayoutParams(p);
}
//XXX p.viewType = mAdapter.getItemViewType(position);
//XXX p.forceAdd = true;
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
mDropDownList.getPaddingLeft() + mDropDownList.getPaddingRight(), p.width);
int lpHeight = p.height;
int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
}
private static class DropDownListView extends ListView {
/*
* WARNING: This is a workaround for a touch mode issue.
*
* Touch mode is propagated lazily to windows. This causes problems in
* the following scenario:
* - Type something in the AutoCompleteTextView and get some results
* - Move down with the d-pad to select an item in the list
* - Move up with the d-pad until the selection disappears
* - Type more text in the AutoCompleteTextView *using the soft keyboard*
* and get new results; you are now in touch mode
* - The selection comes back on the first item in the list, even though
* the list is supposed to be in touch mode
*
* Using the soft keyboard triggers the touch mode change but that change
* is propagated to our window only after the first list layout, therefore
* after the list attempts to resurrect the selection.
*
* The trick to work around this issue is to pretend the list is in touch
* mode when we know that the selection should not appear, that is when
* we know the user moved the selection away from the list.
*
* This boolean is set to true whenever we explicitly hide the list's
* selection and reset to false whenever we know the user moved the
* selection back to the list.
*
* When this boolean is true, isInTouchMode() returns true, otherwise it
* returns super.isInTouchMode().
*/
private boolean mListSelectionHidden;
private boolean mHijackFocus;
public DropDownListView(Context context, boolean hijackFocus) {
super(context, null, /*com.android.internal.*/R.attr.dropDownListViewStyle);
mHijackFocus = hijackFocus;
// TODO: Add an API to control this
setCacheColorHint(0); // Transparent, since the background drawable could be anything.
}
//XXX @Override
//View obtainView(int position, boolean[] isScrap) {
// View view = super.obtainView(position, isScrap);
// if (view instanceof TextView) {
// ((TextView) view).setHorizontallyScrolling(true);
// }
// return view;
//}
@Override
public boolean isInTouchMode() {
// WARNING: Please read the comment where mListSelectionHidden is declared
return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
}
@Override
public boolean hasWindowFocus() {
return mHijackFocus || super.hasWindowFocus();
}
@Override
public boolean isFocused() {
return mHijackFocus || super.isFocused();
}
@Override
public boolean hasFocus() {
return mHijackFocus || super.hasFocus();
}
}
private class PopupDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
if (isShowing()) {
// Resize the popup to fit new content
show();
}
}
@Override
public void onInvalidated() {
dismiss();
}
}
private class ListSelectorHider implements Runnable {
public void run() {
clearListSelection();
}
}
private class ResizePopupRunnable implements Runnable {
public void run() {
if (mDropDownList != null && mDropDownList.getCount() > mDropDownList.getChildCount() &&
mDropDownList.getChildCount() <= mListItemExpandMaximum) {
mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
show();
}
}
}
private class PopupTouchInterceptor implements OnTouchListener {
public boolean onTouch(View v, MotionEvent event) {
final int action = event.getAction();
final int x = (int) event.getX();
final int y = (int) event.getY();
if (action == MotionEvent.ACTION_DOWN &&
mPopup != null && mPopup.isShowing() &&
(x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) {
mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
} else if (action == MotionEvent.ACTION_UP) {
mHandler.removeCallbacks(mResizePopupRunnable);
}
return false;
}
}
private class PopupScrollListener implements ListView.OnScrollListener {
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
}
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
!isInputMethodNotNeeded() && mPopup.getContentView() != null) {
mHandler.removeCallbacks(mResizePopupRunnable);
mResizePopupRunnable.run();
}
}
}
}