518 lines
13 KiB
Java
518 lines
13 KiB
Java
package com.scvngr.levelup.views.gallery;
|
|
|
|
/*
|
|
* Modified from the Android Source code. The example for how to do so was viewed here:
|
|
* http://www.inter-fuser.com/2010/01/android-coverflow-widget.html
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
import android.content.Context;
|
|
import android.database.DataSetObserver;
|
|
import android.graphics.Rect;
|
|
import android.os.Parcel;
|
|
import android.os.Parcelable;
|
|
import android.util.AttributeSet;
|
|
import android.util.SparseArray;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.animation.Interpolator;
|
|
import android.widget.SpinnerAdapter;
|
|
|
|
/**
|
|
* An abstract base class for spinner widgets. SDK users will probably not need
|
|
* to use this class.
|
|
*
|
|
* CHECKSTYLE:OFF Android Attr
|
|
*
|
|
* @attr ref android.R.styleable#AbsSpinner_entries CHECKSTYLE:ON
|
|
*/
|
|
public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> {
|
|
|
|
SpinnerAdapter mAdapter;
|
|
|
|
int mHeightMeasureSpec;
|
|
int mWidthMeasureSpec;
|
|
boolean mBlockLayoutRequests;
|
|
int mSelectionLeftPadding = 0;
|
|
int mSelectionTopPadding = 0;
|
|
int mSelectionRightPadding = 0;
|
|
int mSelectionBottomPadding = 0;
|
|
Rect mSpinnerPadding = new Rect();
|
|
View mSelectedView = null;
|
|
Interpolator mInterpolator;
|
|
|
|
RecycleBin mRecycler = new RecycleBin();
|
|
private DataSetObserver mDataSetObserver;
|
|
|
|
/*
|
|
* Temporary frame to hold a child View's frame rectangle.
|
|
*/
|
|
private Rect mTouchFrame;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param context the context to inflate into
|
|
*/
|
|
public AbsSpinner(final Context context) {
|
|
super(context);
|
|
initAbsSpinner();
|
|
}
|
|
|
|
/**
|
|
* Constructor for xml inflation.
|
|
*
|
|
* @param context the context to inflate into
|
|
* @param attrs the xml attrs
|
|
*/
|
|
public AbsSpinner(final Context context, final AttributeSet attrs) {
|
|
this(context, attrs, 0);
|
|
}
|
|
|
|
/**
|
|
* Constructor for xml inflation.
|
|
*
|
|
* @param context the context to inflate into
|
|
* @param attrs the xml attrs
|
|
* @param defStyle the default style resource
|
|
*/
|
|
public AbsSpinner(final Context context, final AttributeSet attrs, final int defStyle) {
|
|
super(context, attrs, defStyle);
|
|
initAbsSpinner();
|
|
}
|
|
|
|
@Override
|
|
public final SpinnerAdapter getAdapter() {
|
|
return mAdapter;
|
|
}
|
|
|
|
@Override
|
|
public final int getCount() {
|
|
return mItemCount;
|
|
}
|
|
|
|
@Override
|
|
public final View getSelectedView() {
|
|
if (mItemCount > 0 && mSelectedPosition >= 0)
|
|
return getChildAt(mSelectedPosition - mFirstPosition);
|
|
else
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public void onRestoreInstanceState(final Parcelable state) {
|
|
final 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();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Parcelable onSaveInstanceState() {
|
|
final Parcelable superState = super.onSaveInstanceState();
|
|
final SavedState ss = new SavedState(superState);
|
|
ss.selectedId = getSelectedItemId();
|
|
if (ss.selectedId >= 0) {
|
|
ss.position = getSelectedItemPosition();
|
|
} else {
|
|
ss.position = INVALID_POSITION;
|
|
}
|
|
return ss;
|
|
}
|
|
|
|
/**
|
|
* 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 final int pointToPosition(final int x, final 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--) {
|
|
final View child = getChildAt(i);
|
|
if (child.getVisibility() == View.VISIBLE) {
|
|
child.getHitRect(frame);
|
|
if (frame.contains(x, y)) return mFirstPosition + i;
|
|
}
|
|
}
|
|
return INVALID_POSITION;
|
|
}
|
|
|
|
/**
|
|
* Override to prevent spamming ourselves with layout requests as we place
|
|
* views.
|
|
*
|
|
* @see android.view.View#requestLayout()
|
|
*/
|
|
@Override
|
|
public final void requestLayout() {
|
|
if (!mBlockLayoutRequests) {
|
|
super.requestLayout();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
// CHECKSTYLE:OFF unmodified
|
|
@Override
|
|
public void setAdapter(final 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);
|
|
|
|
final int position = mItemCount > 0 ? 0 : INVALID_POSITION;
|
|
|
|
setSelectedPositionInt(position);
|
|
setNextSelectedPositionInt(position);
|
|
|
|
if (mItemCount == 0) {
|
|
// Nothing selected
|
|
checkSelectionChanged();
|
|
}
|
|
|
|
} else {
|
|
checkFocus();
|
|
resetList();
|
|
// Nothing selected
|
|
checkSelectionChanged();
|
|
}
|
|
|
|
requestLayout();
|
|
}
|
|
|
|
// CHECKSTYLE:ON
|
|
|
|
@Override
|
|
public final void setSelection(final int position) {
|
|
setNextSelectedPositionInt(position);
|
|
requestLayout();
|
|
invalidate();
|
|
}
|
|
|
|
/**
|
|
* Jump directly to a specific item in the adapter data.
|
|
*
|
|
* @param position the position to select
|
|
* @param animate if true, animate the selection
|
|
*/
|
|
public final void setSelection(final int position, final boolean animate) {
|
|
// Animate only if requested position is already on screen somewhere
|
|
final boolean shouldAnimate = animate && mFirstPosition <= position
|
|
&& position <= mFirstPosition + getChildCount() - 1;
|
|
setSelectionInt(position, shouldAnimate);
|
|
}
|
|
|
|
// CHECKSTYLE:OFF overridden in gallery
|
|
@Override
|
|
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
|
|
// CHECKSTYLE:ON
|
|
return new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
|
}
|
|
|
|
/**
|
|
* @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.
|
|
*/
|
|
// CHECKSTYLE:OFF unmodified
|
|
@Override
|
|
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
|
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
|
|
int widthSize;
|
|
int heightSize;
|
|
|
|
mSpinnerPadding.left = getPaddingLeft() > mSelectionLeftPadding ? getPaddingLeft() : mSelectionLeftPadding;
|
|
mSpinnerPadding.top = getPaddingTop() > mSelectionTopPadding ? getPaddingTop() : mSelectionTopPadding;
|
|
mSpinnerPadding.right = getPaddingRight() > mSelectionRightPadding ? getPaddingRight() : mSelectionRightPadding;
|
|
mSpinnerPadding.bottom = getPaddingBottom() > mSelectionBottomPadding ? getPaddingBottom()
|
|
: mSelectionBottomPadding;
|
|
|
|
if (mDataChanged) {
|
|
handleDataChanged();
|
|
}
|
|
|
|
int preferredHeight = 0;
|
|
int preferredWidth = 0;
|
|
boolean needsMeasuring = true;
|
|
|
|
final int selectedPosition = getSelectedItemPosition();
|
|
if (selectedPosition >= 0 && mAdapter != null) {
|
|
// 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 != null) {
|
|
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 = resolveSize(preferredHeight, heightMeasureSpec);
|
|
widthSize = resolveSize(preferredWidth, widthMeasureSpec);
|
|
|
|
setMeasuredDimension(widthSize, heightSize);
|
|
mHeightMeasureSpec = heightMeasureSpec;
|
|
mWidthMeasureSpec = widthMeasureSpec;
|
|
}
|
|
|
|
// CHECKSTYLE:ON
|
|
|
|
/**
|
|
* Common code for different constructor flavors.
|
|
*/
|
|
private void initAbsSpinner() {
|
|
setFocusable(true);
|
|
setWillNotDraw(false);
|
|
}
|
|
|
|
/**
|
|
* Gets the height of the child view passed.
|
|
*
|
|
* @param child the child to get the height of
|
|
* @return the child's measured height
|
|
*/
|
|
final int getChildHeight(final View child) {
|
|
return child.getMeasuredHeight();
|
|
}
|
|
|
|
/**
|
|
* Gets the width of the child view passed.
|
|
*
|
|
* @param child the child to get the width of
|
|
* @return the child's measure width
|
|
*/
|
|
final int getChildWidth(final View child) {
|
|
return child.getMeasuredWidth();
|
|
}
|
|
|
|
@Override
|
|
final void handleDataChanged() {
|
|
/*
|
|
* FIXME -- this is called from both measure and layout. This is
|
|
* harmless right now, but we don't want to do redundant work if this
|
|
* gets more complicated
|
|
*/
|
|
super.handleDataChanged();
|
|
}
|
|
|
|
// CHECKSTYLE:OFF unmodified
|
|
abstract void layout(int delta, boolean animate);
|
|
|
|
// CHECKSTYLE:ON
|
|
|
|
/**
|
|
* Recycle all child views.
|
|
*/
|
|
final void recycleAllViews() {
|
|
final int childCount = getChildCount();
|
|
final AbsSpinner.RecycleBin recycleBin = mRecycler;
|
|
|
|
// All views go in recycler
|
|
for (int i = 0; i < childCount; i++) {
|
|
final View v = getChildAt(i);
|
|
final int index = mFirstPosition + i;
|
|
recycleBin.put(index, v);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear out all children from the list.
|
|
*/
|
|
final void resetList() {
|
|
mDataChanged = false;
|
|
mNeedSync = false;
|
|
|
|
removeAllViewsInLayout();
|
|
mOldSelectedPosition = INVALID_POSITION;
|
|
mOldSelectedRowId = INVALID_ROW_ID;
|
|
|
|
setSelectedPositionInt(INVALID_POSITION);
|
|
setNextSelectedPositionInt(INVALID_POSITION);
|
|
invalidate();
|
|
}
|
|
|
|
/**
|
|
* Makes the item at the supplied position selected.
|
|
*
|
|
* @param position Position to select
|
|
* @param animate Should the transition be animated
|
|
*
|
|
*/
|
|
final void setSelectionInt(final int position, final boolean animate) {
|
|
if (position != mOldSelectedPosition) {
|
|
mBlockLayoutRequests = true;
|
|
final int delta = position - mSelectedPosition;
|
|
setNextSelectedPositionInt(position);
|
|
layout(delta, animate);
|
|
mBlockLayoutRequests = false;
|
|
}
|
|
}
|
|
|
|
class RecycleBin {
|
|
private final SparseArray<View> mScrapHeap = new SparseArray<View>();
|
|
|
|
public void put(final int position, final View v) {
|
|
mScrapHeap.put(position, v);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
View get(final int position) {
|
|
// System.out.print("Looking for " + position);
|
|
final View result = mScrapHeap.get(position);
|
|
if (result != null) {
|
|
// System.out.println(" HIT");
|
|
mScrapHeap.delete(position);
|
|
} else {
|
|
// System.out.println(" MISS");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
View peek(final int position) {
|
|
// System.out.print("Looking for " + position);
|
|
return mScrapHeap.get(position);
|
|
}
|
|
}
|
|
|
|
// CHECKSTYLE:ON
|
|
|
|
// CHECKSTYLE:OFF unmodified
|
|
static class SavedState extends BaseSavedState {
|
|
long selectedId;
|
|
int position;
|
|
|
|
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
|
|
@Override
|
|
public SavedState createFromParcel(final Parcel in) {
|
|
return new SavedState(in);
|
|
}
|
|
|
|
@Override
|
|
public SavedState[] newArray(final int size) {
|
|
return new SavedState[size];
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Constructor called from {@link #CREATOR}
|
|
*/
|
|
private SavedState(final Parcel in) {
|
|
super(in);
|
|
selectedId = in.readLong();
|
|
position = in.readInt();
|
|
}
|
|
|
|
/**
|
|
* Constructor called from {@link AbsSpinner#onSaveInstanceState()}
|
|
*/
|
|
SavedState(final Parcelable superState) {
|
|
super(superState);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "AbsSpinner.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " selectedId="
|
|
+ selectedId + " position=" + position + "}";
|
|
}
|
|
|
|
@Override
|
|
public void writeToParcel(final Parcel out, final int flags) {
|
|
super.writeToParcel(out, flags);
|
|
out.writeLong(selectedId);
|
|
out.writeInt(position);
|
|
}
|
|
}
|
|
}
|