451 lines
14 KiB
Java
451 lines
14 KiB
Java
|
/*
|
||
|
* Copyright (C) 2006 The Android Open Source Project
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
|
||
|
package android.support.v7.internal.widget;
|
||
|
|
||
|
import android.content.Context;
|
||
|
import android.database.DataSetObserver;
|
||
|
import android.graphics.Rect;
|
||
|
import android.os.Parcel;
|
||
|
import android.os.Parcelable;
|
||
|
import android.support.v4.view.ViewCompat;
|
||
|
import android.util.AttributeSet;
|
||
|
import android.util.SparseArray;
|
||
|
import android.view.View;
|
||
|
import android.view.ViewGroup;
|
||
|
import android.widget.SpinnerAdapter;
|
||
|
|
||
|
/**
|
||
|
* An abstract base class for spinner widgets. SDK users will probably not
|
||
|
* need to use this class.
|
||
|
*/
|
||
|
abstract class AbsSpinnerCompat extends AdapterViewCompat<SpinnerAdapter> {
|
||
|
SpinnerAdapter mAdapter;
|
||
|
|
||
|
int mHeightMeasureSpec;
|
||
|
int mWidthMeasureSpec;
|
||
|
|
||
|
int mSelectionLeftPadding = 0;
|
||
|
int mSelectionTopPadding = 0;
|
||
|
int mSelectionRightPadding = 0;
|
||
|
int mSelectionBottomPadding = 0;
|
||
|
final Rect mSpinnerPadding = new Rect();
|
||
|
|
||
|
final RecycleBin mRecycler = new RecycleBin();
|
||
|
private DataSetObserver mDataSetObserver;
|
||
|
|
||
|
/** Temporary frame to hold a child View's frame rectangle */
|
||
|
private Rect mTouchFrame;
|
||
|
|
||
|
AbsSpinnerCompat(Context context) {
|
||
|
super(context);
|
||
|
initAbsSpinner();
|
||
|
}
|
||
|
|
||
|
AbsSpinnerCompat(Context context, AttributeSet attrs) {
|
||
|
this(context, attrs, 0);
|
||
|
}
|
||
|
|
||
|
AbsSpinnerCompat(Context context, AttributeSet attrs, int defStyle) {
|
||
|
super(context, attrs, defStyle);
|
||
|
initAbsSpinner();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Common code for different constructor flavors
|
||
|
*/
|
||
|
private void initAbsSpinner() {
|
||
|
setFocusable(true);
|
||
|
setWillNotDraw(false);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* The Adapter is used to provide the data which backs this Spinner.
|
||
|
* It also provides methods to transform spinner items based on their position
|
||
|
* relative to the selected item.
|
||
|
* @param adapter The SpinnerAdapter to use for this Spinner
|
||
|
*/
|
||
|
@Override
|
||
|
public void setAdapter(SpinnerAdapter adapter) {
|
||
|
if (null != mAdapter) {
|
||
|
mAdapter.unregisterDataSetObserver(mDataSetObserver);
|
||
|
resetList();
|
||
|
}
|
||
|
|
||
|
mAdapter = adapter;
|
||
|
|
||
|
mOldSelectedPosition = INVALID_POSITION;
|
||
|
mOldSelectedRowId = INVALID_ROW_ID;
|
||
|
|
||
|
if (mAdapter != null) {
|
||
|
mOldItemCount = mItemCount;
|
||
|
mItemCount = mAdapter.getCount();
|
||
|
checkFocus();
|
||
|
|
||
|
mDataSetObserver = new AdapterDataSetObserver();
|
||
|
mAdapter.registerDataSetObserver(mDataSetObserver);
|
||
|
|
||
|
int position = mItemCount > 0 ? 0 : INVALID_POSITION;
|
||
|
|
||
|
setSelectedPositionInt(position);
|
||
|
setNextSelectedPositionInt(position);
|
||
|
|
||
|
if (mItemCount == 0) {
|
||
|
// Nothing selected
|
||
|
checkSelectionChanged();
|
||
|
}
|
||
|
|
||
|
} else {
|
||
|
checkFocus();
|
||
|
resetList();
|
||
|
// Nothing selected
|
||
|
checkSelectionChanged();
|
||
|
}
|
||
|
|
||
|
requestLayout();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Clear out all children from the list
|
||
|
*/
|
||
|
void resetList() {
|
||
|
mDataChanged = false;
|
||
|
mNeedSync = false;
|
||
|
|
||
|
removeAllViewsInLayout();
|
||
|
mOldSelectedPosition = INVALID_POSITION;
|
||
|
mOldSelectedRowId = INVALID_ROW_ID;
|
||
|
|
||
|
setSelectedPositionInt(INVALID_POSITION);
|
||
|
setNextSelectedPositionInt(INVALID_POSITION);
|
||
|
invalidate();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @see android.view.View#measure(int, int)
|
||
|
*
|
||
|
* Figure out the dimensions of this Spinner. The width comes from
|
||
|
* the widthMeasureSpec as Spinnners can't have their width set to
|
||
|
* UNSPECIFIED. The height is based on the height of the selected item
|
||
|
* plus padding.
|
||
|
*/
|
||
|
@Override
|
||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||
|
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
|
||
|
int widthSize;
|
||
|
int heightSize;
|
||
|
|
||
|
final int paddingLeft = getPaddingLeft();
|
||
|
final int paddingTop = getPaddingTop();
|
||
|
final int paddingRight = getPaddingRight();
|
||
|
final int paddingBottom = getPaddingBottom();
|
||
|
|
||
|
mSpinnerPadding.left = paddingLeft > mSelectionLeftPadding ? paddingLeft
|
||
|
: mSelectionLeftPadding;
|
||
|
mSpinnerPadding.top = paddingTop > mSelectionTopPadding ? paddingTop
|
||
|
: mSelectionTopPadding;
|
||
|
mSpinnerPadding.right = paddingRight > mSelectionRightPadding ? paddingRight
|
||
|
: mSelectionRightPadding;
|
||
|
mSpinnerPadding.bottom = paddingBottom > mSelectionBottomPadding ? paddingBottom
|
||
|
: mSelectionBottomPadding;
|
||
|
|
||
|
if (mDataChanged) {
|
||
|
handleDataChanged();
|
||
|
}
|
||
|
|
||
|
int preferredHeight = 0;
|
||
|
int preferredWidth = 0;
|
||
|
boolean needsMeasuring = true;
|
||
|
|
||
|
int selectedPosition = getSelectedItemPosition();
|
||
|
if (selectedPosition >= 0 && mAdapter != null && selectedPosition < mAdapter.getCount()) {
|
||
|
// Try looking in the recycler. (Maybe we were measured once already)
|
||
|
View view = mRecycler.get(selectedPosition);
|
||
|
if (view == null) {
|
||
|
// Make a new one
|
||
|
view = mAdapter.getView(selectedPosition, null, this);
|
||
|
}
|
||
|
|
||
|
if (view != null) {
|
||
|
// Put in recycler for re-measuring and/or layout
|
||
|
mRecycler.put(selectedPosition, view);
|
||
|
|
||
|
if (view.getLayoutParams() == null) {
|
||
|
mBlockLayoutRequests = true;
|
||
|
view.setLayoutParams(generateDefaultLayoutParams());
|
||
|
mBlockLayoutRequests = false;
|
||
|
}
|
||
|
measureChild(view, widthMeasureSpec, heightMeasureSpec);
|
||
|
|
||
|
preferredHeight = getChildHeight(view) + mSpinnerPadding.top + mSpinnerPadding.bottom;
|
||
|
preferredWidth = getChildWidth(view) + mSpinnerPadding.left + mSpinnerPadding.right;
|
||
|
|
||
|
needsMeasuring = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (needsMeasuring) {
|
||
|
// No views -- just use padding
|
||
|
preferredHeight = mSpinnerPadding.top + mSpinnerPadding.bottom;
|
||
|
if (widthMode == MeasureSpec.UNSPECIFIED) {
|
||
|
preferredWidth = mSpinnerPadding.left + mSpinnerPadding.right;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
preferredHeight = Math.max(preferredHeight, getSuggestedMinimumHeight());
|
||
|
preferredWidth = Math.max(preferredWidth, getSuggestedMinimumWidth());
|
||
|
|
||
|
heightSize = ViewCompat.resolveSizeAndState(preferredHeight, heightMeasureSpec, 0);
|
||
|
widthSize = ViewCompat.resolveSizeAndState(preferredWidth, widthMeasureSpec, 0);
|
||
|
|
||
|
setMeasuredDimension(widthSize, heightSize);
|
||
|
mHeightMeasureSpec = heightMeasureSpec;
|
||
|
mWidthMeasureSpec = widthMeasureSpec;
|
||
|
}
|
||
|
|
||
|
int getChildHeight(View child) {
|
||
|
return child.getMeasuredHeight();
|
||
|
}
|
||
|
|
||
|
int getChildWidth(View child) {
|
||
|
return child.getMeasuredWidth();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
|
||
|
return new ViewGroup.LayoutParams(
|
||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||
|
}
|
||
|
|
||
|
void recycleAllViews() {
|
||
|
final int childCount = getChildCount();
|
||
|
final AbsSpinnerCompat.RecycleBin recycleBin = mRecycler;
|
||
|
final int position = mFirstPosition;
|
||
|
|
||
|
// All views go in recycler
|
||
|
for (int i = 0; i < childCount; i++) {
|
||
|
View v = getChildAt(i);
|
||
|
int index = position + i;
|
||
|
recycleBin.put(index, v);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Jump directly to a specific item in the adapter data.
|
||
|
*/
|
||
|
public void setSelection(int position, boolean animate) {
|
||
|
// Animate only if requested position is already on screen somewhere
|
||
|
boolean shouldAnimate = animate && mFirstPosition <= position &&
|
||
|
position <= mFirstPosition + getChildCount() - 1;
|
||
|
setSelectionInt(position, shouldAnimate);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setSelection(int position) {
|
||
|
setNextSelectedPositionInt(position);
|
||
|
requestLayout();
|
||
|
invalidate();
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Makes the item at the supplied position selected.
|
||
|
*
|
||
|
* @param position Position to select
|
||
|
* @param animate Should the transition be animated
|
||
|
*
|
||
|
*/
|
||
|
void setSelectionInt(int position, boolean animate) {
|
||
|
if (position != mOldSelectedPosition) {
|
||
|
mBlockLayoutRequests = true;
|
||
|
int delta = position - mSelectedPosition;
|
||
|
setNextSelectedPositionInt(position);
|
||
|
layout(delta, animate);
|
||
|
mBlockLayoutRequests = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
abstract void layout(int delta, boolean animate);
|
||
|
|
||
|
@Override
|
||
|
public View getSelectedView() {
|
||
|
if (mItemCount > 0 && mSelectedPosition >= 0) {
|
||
|
return getChildAt(mSelectedPosition - mFirstPosition);
|
||
|
} else {
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Override to prevent spamming ourselves with layout requests
|
||
|
* as we place views
|
||
|
*
|
||
|
* @see android.view.View#requestLayout()
|
||
|
*/
|
||
|
@Override
|
||
|
public void requestLayout() {
|
||
|
if (!mBlockLayoutRequests) {
|
||
|
super.requestLayout();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public SpinnerAdapter getAdapter() {
|
||
|
return mAdapter;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getCount() {
|
||
|
return mItemCount;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Maps a point to a position in the list.
|
||
|
*
|
||
|
* @param x X in local coordinate
|
||
|
* @param y Y in local coordinate
|
||
|
* @return The position of the item which contains the specified point, or
|
||
|
* {@link #INVALID_POSITION} if the point does not intersect an item.
|
||
|
*/
|
||
|
public int pointToPosition(int x, int y) {
|
||
|
Rect frame = mTouchFrame;
|
||
|
if (frame == null) {
|
||
|
mTouchFrame = new Rect();
|
||
|
frame = mTouchFrame;
|
||
|
}
|
||
|
|
||
|
final int count = getChildCount();
|
||
|
for (int i = count - 1; i >= 0; i--) {
|
||
|
View child = getChildAt(i);
|
||
|
if (child.getVisibility() == View.VISIBLE) {
|
||
|
child.getHitRect(frame);
|
||
|
if (frame.contains(x, y)) {
|
||
|
return mFirstPosition + i;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return INVALID_POSITION;
|
||
|
}
|
||
|
|
||
|
static class SavedState extends BaseSavedState {
|
||
|
long selectedId;
|
||
|
int position;
|
||
|
|
||
|
/**
|
||
|
* Constructor called from {@link AbsSpinnerCompat#onSaveInstanceState()}
|
||
|
*/
|
||
|
SavedState(Parcelable superState) {
|
||
|
super(superState);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Constructor called from {@link #CREATOR}
|
||
|
*/
|
||
|
SavedState(Parcel in) {
|
||
|
super(in);
|
||
|
selectedId = in.readLong();
|
||
|
position = in.readInt();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(Parcel out, int flags) {
|
||
|
super.writeToParcel(out, flags);
|
||
|
out.writeLong(selectedId);
|
||
|
out.writeInt(position);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String toString() {
|
||
|
return "AbsSpinner.SavedState{"
|
||
|
+ Integer.toHexString(System.identityHashCode(this))
|
||
|
+ " selectedId=" + selectedId
|
||
|
+ " position=" + position + "}";
|
||
|
}
|
||
|
|
||
|
public static final Parcelable.Creator<SavedState> CREATOR
|
||
|
= new Parcelable.Creator<SavedState>() {
|
||
|
public SavedState createFromParcel(Parcel in) {
|
||
|
return new SavedState(in);
|
||
|
}
|
||
|
|
||
|
public SavedState[] newArray(int size) {
|
||
|
return new SavedState[size];
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public Parcelable onSaveInstanceState() {
|
||
|
Parcelable superState = super.onSaveInstanceState();
|
||
|
SavedState ss = new SavedState(superState);
|
||
|
ss.selectedId = getSelectedItemId();
|
||
|
if (ss.selectedId >= 0) {
|
||
|
ss.position = getSelectedItemPosition();
|
||
|
} else {
|
||
|
ss.position = INVALID_POSITION;
|
||
|
}
|
||
|
return ss;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onRestoreInstanceState(Parcelable state) {
|
||
|
SavedState ss = (SavedState) state;
|
||
|
|
||
|
super.onRestoreInstanceState(ss.getSuperState());
|
||
|
|
||
|
if (ss.selectedId >= 0) {
|
||
|
mDataChanged = true;
|
||
|
mNeedSync = true;
|
||
|
mSyncRowId = ss.selectedId;
|
||
|
mSyncPosition = ss.position;
|
||
|
mSyncMode = SYNC_SELECTED_POSITION;
|
||
|
requestLayout();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class RecycleBin {
|
||
|
private final SparseArray<View> mScrapHeap = new SparseArray<View>();
|
||
|
|
||
|
public void put(int position, View v) {
|
||
|
mScrapHeap.put(position, v);
|
||
|
}
|
||
|
|
||
|
View get(int position) {
|
||
|
// System.out.print("Looking for " + position);
|
||
|
View result = mScrapHeap.get(position);
|
||
|
if (result != null) {
|
||
|
// System.out.println(" HIT");
|
||
|
mScrapHeap.delete(position);
|
||
|
} else {
|
||
|
// System.out.println(" MISS");
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
void clear() {
|
||
|
final SparseArray<View> scrapHeap = mScrapHeap;
|
||
|
final int count = scrapHeap.size();
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
final View view = scrapHeap.valueAt(i);
|
||
|
if (view != null) {
|
||
|
removeDetachedView(view, true);
|
||
|
}
|
||
|
}
|
||
|
scrapHeap.clear();
|
||
|
}
|
||
|
}
|
||
|
}
|