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 PopupWindow 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 PopupWindow(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 PopupWindow(wrapped, attrs, defStyleAttr);
} else {
mPopup = new PopupWindow(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);
}
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();
}
}
}
}