在Android中,如果想要实现3D动画效果一般有两种选择:一是使用Open GL ES,二是使用Camera。Open GL ES使用起来太过复杂,一般是用于比较高级的3D特效或游戏,并且这个也不是开源的,像比较简单的一些3D效果,使用Camera就足够了。
一些熟知的Android 3D动画如对某个View进行旋转或翻转的 Rotate3dAnimation类,还有使用Gallery( Gallery目前已过时,现在都推荐使用 HorizontalScrollView或 RecyclerView替代其实现相应功能) 实现的3D画廊效果等,当然有一些特效要通过伪3D变换来实现,比如CoverFlow效果,它使用标准Android 2D库,还是继承的Gallery类并自定义一些方法,具体实现和使用请参照Android实现CoverFlow效果控件的实例代码。
本文要实现的3D星体旋转效果也是从这个CoverFlow演绎而来,不过CoverFlow只是对图像进行转动,我这里要实现的效果是要对所有的View进行类似旋转木马的转动,并且CoverFlow还存在很多已知bug,所以我这里需要重写一些类,并且将Scroller类用Rotator类替代,使界面看起来具有滚动效果,实际上是在转动一组图像。
首先我们需要自定义控件的一些属性,我们将控件取名Carousel,需要设置子项的最小个数和最大个数、当前选中项以及定义旋转角度等,attrs.xml
The CarouselImageView Class
这个类装载控件子项在3D空间的位置、子项的索引和当前子项的角度,通过实现Comparable接口,帮助我们确定子项绘制的顺序
package com.john.carousel.lib; import android.content.Context; import android.util.AttributeSet; import android.widget.ImageView; public class CarouselImageView extends ImageView implements Comparable{ private int index; private float currentAngle; private float x; private float y; private float z; private boolean drawn; public CarouselImageView(Context context) { this(context, null, 0); } public CarouselImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CarouselImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public void setIndex(int index) { this.index = index; } public int getIndex() { return index; } public void setCurrentAngle(float currentAngle) { this.currentAngle = currentAngle; } public float getCurrentAngle() { return currentAngle; } public int compareTo(CarouselImageView another) { return (int) (another.z - this.z); } public void setX(float x) { this.x = x; } public float getX() { return x; } public void setY(float y) { this.y = y; } public float getY() { return y; } public void setZ(float z) { this.z = z; } public float getZ() { return z; } public void setDrawn(boolean drawn) { this.drawn = drawn; } public boolean isDrawn() { return drawn; } }
The Carousel Item Class
这个类简化我上面定义的 CarouselImageView一些控件属性
package com.john.carousel.lib; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Matrix; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; public class CarouselItem extends FrameLayout implements Comparable{ public ImageView mImage; public TextView mText, mTextUp; public Context context; public int index; public float currentAngle; public float itemX; public float itemY; public float itemZ; public float degX; public float degY; public float degZ; public boolean drawn; // It's needed to find screen coordinates private Matrix mCIMatrix; public CarouselItem(Context context) { super(context); this.context = context; FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); this.setLayoutParams(params); LayoutInflater inflater = LayoutInflater.from(context); View itemTemplate = inflater.inflate(R.layout.carousel_item, this, true); mImage = (ImageView) itemTemplate.findViewById(R.id.item_image); mText = (TextView) itemTemplate.findViewById(R.id.item_text); mTextUp = (TextView) itemTemplate.findViewById(R.id.item_text_up); } public void setTextColor(int i) { this.mText.setTextColor(context.getResources().getColorStateList(i)); this.mTextUp.setTextColor(context.getResources().getColorStateList(i)); } public String getName() { return mText.getText().toString(); } public void setIndex(int index) { this.index = index; } public int getIndex() { return index; } public void setCurrentAngle(float currentAngle) { if (index == 0 && currentAngle > 5) { Log.d("", ""); } this.currentAngle = currentAngle; } public float getCurrentAngle() { return currentAngle; } public int compareTo(CarouselItem another) { return (int) (another.itemZ - this.itemZ); } public void setItemX(float x) { this.itemX = x; } public float getItemX() { return itemX; } public void setItemY(float y) { this.itemY = y; } public float getItemY() { return itemY; } public void setItemZ(float z) { this.itemZ = z; } public float getItemZ() { return itemZ; } public float getDegX() { return degX; } public void setDegX(float degX) { this.degX = degX; } public float getDegY() { return degY; } public void setDegY(float degY) { this.degY = degY; } public float getDegZ() { return degZ; } public void setDegZ(float degZ) { this.degZ = degZ; } public void setDrawn(boolean drawn) { this.drawn = drawn; } public boolean isDrawn() { return drawn; } public void setImageBitmap(Bitmap bitmap) { mImage.setImageBitmap(bitmap); } public void setText(int i) { String s = context.getResources().getString(i); mText.setText(s); mTextUp.setText(s); } public void setText(String txt) { mText.setText(txt); mTextUp.setText(txt); } Matrix getCIMatrix() { return mCIMatrix; } void setCIMatrix(Matrix mMatrix) { this.mCIMatrix = mMatrix; } public void setImage(int i) { mImage.setImageDrawable(context.getResources().getDrawable(i)); } public void setVisiblity(int id) { if (id == 0) { mText.setVisibility(View.INVISIBLE); mTextUp.setVisibility(View.VISIBLE); } else { mTextUp.setVisibility(View.INVISIBLE); mText.setVisibility(View.VISIBLE); } } }
The Rotator Class
如果你去查看Scroller类方法,你会发现它定义了两种操作模式:滑动模式和抛动作,用来计算当前相对于给出的起始位置的偏移量,我们需要移除一些不需要的成员变量,添加我们自己的成员,并且修改相应的计算方法
package com.john.carousel.lib; import android.content.Context; import android.view.animation.AnimationUtils; /** * This class encapsulates rotation. The duration of the rotation can be passed * in the constructor and specifies the maximum time that the rotation animation * should take. Past this time, the rotation is automatically moved to its final * stage and computeRotationOffset() will always return false to indicate that * scrolling is over. */ public class Rotator { private float mStartAngle; private float mCurrAngle; private long mStartTime; private long mDuration; private float mDeltaAngle; private boolean mFinished; private int direction; private float mCurrDeg; public Rotator(Context context) { mFinished = true; } public final boolean isFinished() { return mFinished; } /** * Force the finished field to a particular value. * * @param finished * The new finished value. */ public final void forceFinished(boolean finished) { mFinished = finished; } /** * Returns how long the scroll event will take, in milliseconds. * * @return The duration of the scroll in milliseconds. */ public final long getDuration() { return mDuration; } /** * Returns the current X offset in the scroll. * * @return The new X offset as an absolute distance from the origin. */ public final float getCurrAngle() { return mCurrAngle; } public final float getStartAngle() { return mStartAngle; } /** * Returns the time elapsed since the beginning of the scrolling. * * @return The elapsed time in milliseconds. */ public int timePassed() { return (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime); } public int getdirection() { return this.direction; } public float getCurrDeg() { return this.mCurrDeg; } /** * Extend the scroll animation. */ public void extendDuration(int extend) { int passed = timePassed(); mDuration = passed + extend; mFinished = false; } /** * Stops the animation. Contrary to {@link #forceFinished(boolean)}, * aborting the animating cause the scroller to move to the final x and y * position * * @see #forceFinished(boolean) */ public void abortAnimation() { mFinished = true; } /** * Call this when you want to know the new location. If it returns true, the * animation is not yet finished. loc will be altered to provide the new * location. */ public boolean computeAngleOffset() { if (mFinished) { return false; } long systemClock = AnimationUtils.currentAnimationTimeMillis(); long timePassed = systemClock - mStartTime; if (timePassed < mDuration) { float sc = (float) timePassed / mDuration; mCurrAngle = mStartAngle + Math.round(mDeltaAngle * sc); mCurrDeg = direction == 0 ? (Math.round(360 * sc)) : (Math.round(-360 * sc)); return true; } else { mCurrAngle = mStartAngle + mDeltaAngle; mCurrDeg = direction == 0 ? 360 : -360; mFinished = true; return false; } } public void startRotate(float startAngle, float dAngle, int duration, int direction) { mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartAngle = startAngle; mDeltaAngle = dAngle; this.direction = direction; } }
The CarouselSpinner Class
package com.john.carousel.lib; 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.widget.AbsSpinner; import android.widget.SpinnerAdapter; public abstract class CarouselSpinner extends CarouselAdapter{ SpinnerAdapter mAdapter; int mHeightMeasureSpec; int mWidthMeasureSpec; boolean mBlockLayoutRequests; 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; public CarouselSpinner(Context context) { super(context); initCarouselSpinner(); } public CarouselSpinner(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CarouselSpinner(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initCarouselSpinner(); } /** * Common code for different constructor flavors */ private void initCarouselSpinner() { setFocusable(true); setWillNotDraw(false); } @Override public SpinnerAdapter getAdapter() { return mAdapter; } @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(); } @Override public View getSelectedView() { if (mItemCount > 0 && mSelectedPosition >= 0) { return getChildAt(mSelectedPosition - mFirstPosition); } else { return null; } } /** * 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); } /** * 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 void setSelection(int position) { setSelectionInt(position, false); } /** * 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; 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 preferred; int preferred; 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 != 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; } int getChildHeight(View child) { return child.getMeasuredHeight(); } int getChildWidth(View child) { return child.getMeasuredWidth(); } @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { /** * Carousel expects Carousel.LayoutParams. */ return new Carousel.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } void recycleAllViews() { final int childCount = getChildCount(); final CarouselSpinner.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); } } /** * 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 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) { // All touch events are applied to selected item return mSelectedPosition; } static class SavedState extends BaseSavedState { long selectedId; int position; /** * Constructor called from {@link AbsSpinner#onSaveInstanceState()} */ SavedState(Parcelable superState) { super(superState); } /** * Constructor called from {@link #CREATOR} */ private 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 CREATOR = new Parcelable.Creator () { 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 mScrapHeap = new SparseArray (); 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 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(); } } }
The CarouselAdapter Class
[The CarouselAdapter vs. AdapterView]
The only changes are in updateEmptyStatus method where unavailable variables were replaced with their getters.
The Carousel Class
package com.john.carousel.lib; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Camera; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.Transformation; import android.widget.BaseAdapter; public class Carousel extends CarouselSpinner implements Constants { private int mAnimationDuration = 100; private int mAnimationDurationMin = 50; private Camera mCamera = null; private FlingRotateRunnable mFlingRunnable = null; private int mGravity = 0; private View mSelectedChild = null; private static int mSelectedItemIndex = 2; private boolean mShouldStopFling = false; private static final int LEFT = 0; private static final int RIGHT = 1; /** * If true, do not callback to item selected listener. */ private boolean mSuppressSelectionChanged = false; private float mTheta = 0.0f; private boolean isFocus = true; private ImageAdapter adapter = null; private static final int ONE_ITEM = 1; private CarouselItemClickListener callback = null; public Carousel(Context context) { this(context, null); } public Carousel(Context context, AttributeSet attrs) { this(context, attrs, 0); } public Carousel(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setChildrenDrawingOrderEnabled(false); setStaticTransformationsEnabled(true); TypedArray arr = getContext().obtainStyledAttributes(attrs, R.styleable.Carousel); int imageArrayID = arr.getResourceId(R.styleable.Carousel_Items, -1); TypedArray images = getResources().obtainTypedArray(imageArrayID); int namesForItems = arr.getResourceId(R.styleable.Carousel_Names, -1); TypedArray names = null; if (namesForItems != -1) { names = getResources().obtainTypedArray(namesForItems); } initView(images, names); arr.recycle(); images.recycle(); if (names != null) { names.recycle(); } } private void initView(TypedArray images, TypedArray names) { mCamera = new Camera(); mFlingRunnable = new FlingRotateRunnable(); mTheta = (float) (15.0f * (Math.PI / 180.0)); adapter = new ImageAdapter(getContext()); adapter.setImages(images, names); setAdapter(adapter); setSelectedPositionInt(mSelectedItemIndex); } @Override protected int computeHorizontalScrollExtent() { // Only 1 item is considered to be selected return ONE_ITEM; } @Override protected int computeHorizontalScrollOffset() { // Current scroll position is the same as the selected position return mSelectedPosition; } @Override protected int computeHorizontalScrollRange() { // Scroll range is the same as the item count return mItemCount; } public void setFocusFlag(boolean flag) { this.isFocus = flag; adapter.notifyDataSetChanged(); } public boolean getFocusFlag() { return this.isFocus; } public void setSelected(int index) { setNextSelectedPositionInt(index); mSelectedItemIndex = index; } public void setCarouselItemClickCallBack(CarouselItemClickListener listener) { callback = listener; } public interface CarouselItemClickListener { public void CarouselClickCallBack(int itemPosition); } /** * Handles left, right, and clicking * * @see android.view.View#onKeyDown */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KEY_OK: case KEY_CENTER: callback.CarouselClickCallBack(mSelectedItemIndex); return true; case KEY_LEFT: toNextLeftItem(); return true; case KEY_RIGHT: toNextRightItem(); return true; } return super.onKeyDown(keyCode, event); } @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); /** * The gallery shows focus by focusing the selected item. So, give focus * to our selected item instead. We steal keys from our selected item * elsewhere. */ if (gainFocus && mSelectedChild != null) { mSelectedChild.requestFocus(direction); } } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams; } @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new LayoutParams(p); } @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } @Override protected void dispatchSetPressed(boolean pressed) { if (mSelectedChild != null) { mSelectedChild.setPressed(pressed); } } @Override public boolean dispatchKeyEvent(KeyEvent event) { return false; } /** * Transform an item depending on it's coordinates */ @Override protected boolean getChildStaticTransformation(View child, Transformation transformation) { transformation.clear(); transformation.setTransformationType(Transformation.TYPE_MATRIX); // Center of the view float centerX = (float) getWidth() / 2, centerY = (float) getHeight() / 2; mCamera.save(); final Matrix matrix = transformation.getMatrix(); mCamera.translate(((CarouselItem) child).getItemX(), ((CarouselItem) child).getItemY(), ((CarouselItem) child).getItemZ()); mCamera.getMatrix(matrix); matrix.preTranslate(-centerX, -centerY); matrix.postTranslate(centerX, centerY); float[] values = new float[9]; matrix.getValues(values); mCamera.restore(); Matrix mm = new Matrix(); mm.setValues(values); ((CarouselItem) child).setCIMatrix(mm); child.invalidate(); return true; } // CarouselAdapter overrides /** * Setting up images */ void layout(int delta, boolean animate) { Log.d("ORDER", "layout"); if (mDataChanged) { handleDataChanged(); } if (mNextSelectedPosition >= 0) { setSelectedPositionInt(mNextSelectedPosition); } recycleAllViews(); detachAllViewsFromParent(); int count = getAdapter().getCount(); float angleUnit = 360.0f / count; float angleOffset = mSelectedPosition * angleUnit; for (int i = 0; i < getAdapter().getCount(); i++) { float angle = angleUnit * i - angleOffset; if (angle < 0.0f) { angle = 360.0f + angle; } makeAndAddView(i, angle); } mRecycler.clear(); invalidate(); setNextSelectedPositionInt(mSelectedPosition); checkSelectionChanged(); mNeedSync = false; updateSelectedItemMetadata(); } /** * Setting up images after layout changed */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); Log.d("ORDER", "onLayout"); /** * Remember that we are in layout to prevent more layout request from * being generated. */ mInLayout = true; layout(0, false); mInLayout = false; } @Override void selectionChanged() { if (!mSuppressSelectionChanged) { super.selectionChanged(); } } @Override void setSelectedPositionInt(int position) { super.setSelectedPositionInt(position); super.setNextSelectedPositionInt(position); updateSelectedItemMetadata(); } private class FlingRotateRunnable implements Runnable { private Rotator mRotator; private float mLastFlingAngle; public FlingRotateRunnable() { mRotator = new Rotator(getContext()); } private void startCommon() { removeCallbacks(this); } public void startUsingDistance(float deltaAngle, int flag, int direction) { if (deltaAngle == 0) return; startCommon(); mLastFlingAngle = 0; synchronized (this) { mRotator.startRotate(0.0f, -deltaAngle, flag == 0 ? mAnimationDuration : mAnimationDurationMin, direction); } post(this); } private void endFling(boolean scrollIntoSlots, int direction) { synchronized (this) { mRotator.forceFinished(true); } if (scrollIntoSlots) { scrollIntoSlots(direction); } } public void run() { Log.d("ORDER", "run"); mShouldStopFling = false; final Rotator rotator; final float angle; final float deg; boolean more; int direction; synchronized (this) { rotator = mRotator; more = rotator.computeAngleOffset(); angle = rotator.getCurrAngle(); deg = rotator.getCurrDeg(); direction = rotator.getdirection(); } if (more && !mShouldStopFling) { Log.d("GETVIEW", "========go"); float delta = mLastFlingAngle - angle; trackMotionScroll(delta, deg); mLastFlingAngle = angle; post(this); } else { Log.d("GETVIEW", "========end"); float delta = mLastFlingAngle - angle; trackMotionScroll(delta, deg); mLastFlingAngle = 0.0f; endFling(false, direction); } } } private class ImageAdapter extends BaseAdapter { private Context mContext; private CarouselItem[] mImages; private int[] lightImages = { R.drawable.icons_light_network, R.drawable.icons_light_update, R.drawable.icons_light_app, R.drawable.icons_light_stb, R.drawable.icons_light_other, R.drawable.icons_light_wallpaper, R.drawable.icons_light_media }; private final int[] normalImages = { R.drawable.icons_normal_network0, R.drawable.icons_normal_update0, R.drawable.icons_normal_app0, R.drawable.icons_normal_stb0, R.drawable.icons_normal_other0, R.drawable.icons_normal_wallpaper0, R.drawable.icons_normal_meida0 }; private final int[] colors = { R.color.network_text_color, R.color.update_text_color, R.color.app_text_color, R.color.stb_text_color, R.color.other_text_color, R.color.wallpaper_text_color, R.color.media_text_color, R.color.text_color_white }; // private final int[] names = { R.string.STR_NETWORK, // R.string.STR_UPDATE, R.string.STR_APP, R.string.STR_STB, // R.string.STR_OTHER, R.string.STR_WALLPAPER, R.string.STR_MEDIA }; public ImageAdapter(Context c) { mContext = c; } public void setImages(TypedArray array, TypedArray names) { Drawable[] drawables = new Drawable[array.length()]; mImages = new CarouselItem[array.length()]; for (int i = 0; i < array.length(); i++) { drawables[i] = array.getDrawable(i); Bitmap originalImage = ((BitmapDrawable) drawables[i]).getBitmap(); CarouselItem item = new CarouselItem(mContext); item.setIndex(i); item.setImageBitmap(originalImage); if (names != null) { item.setText(names.getString(i)); } if (i == mSelectedItemIndex || (i + 6) % 7 == mSelectedItemIndex || (i + 1) % 7 == mSelectedItemIndex) { item.setVisiblity(1); } else { item.setVisiblity(0); } mImages[i] = item; } } public int getCount() { if (mImages == null) { return 0; } else { return mImages.length; } } public Object getItem(int position) { return position; } public long getItemId(int position) { return position; } public View getView(int position, View convertView, ViewGroup parent) { if (position == mSelectedItemIndex || (position + 6) % 7 == mSelectedItemIndex || (position + 1) % 7 == mSelectedItemIndex) { mImages[position].setVisiblity(1); } else { mImages[position].setVisiblity(0); } if (position == mSelectedItemIndex && isFocus) { mImages[position].setImage(lightImages[position]); mImages[position].setTextColor(colors[position]); } else { mImages[position].setImage(normalImages[position]); mImages[position].setTextColor(colors[7]); } Log.d("GETVIEW", position + ":getView"); return mImages[position]; } } @SuppressLint("FloatMath") private void Calculate3DPosition(CarouselItem child, int diameter, float angleOffset) { angleOffset = angleOffset * (float) (Math.PI / 180.0f); float x = -(float) (diameter / 2 * android.util.FloatMath.sin(angleOffset) * 1.05) + diameter / 2 - child.getWidth() / 2; float z = diameter / 2 * (1.0f - (float) android.util.FloatMath.cos(angleOffset)); float y = -getHeight() / 2 + (float) (z * android.util.FloatMath.sin(mTheta)) + 120; child.setItemX(x); child.setItemZ(z); child.setItemY(y); } /** * Figure out vertical placement based on mGravity * * @param child * Child to place * @return Where the top of the child should be */ private int calculateTop(View child, boolean duringLayout) { int myHeight = duringLayout ? getMeasuredHeight() : getHeight(); int childHeight = duringLayout ? child.getMeasuredHeight() : child.getHeight(); int childTop = 0; switch (mGravity) { case Gravity.TOP: childTop = mSpinnerPadding.top; break; case Gravity.CENTER_VERTICAL: int availableSpace = myHeight - mSpinnerPadding.bottom - mSpinnerPadding.top - childHeight; childTop = mSpinnerPadding.top + (availableSpace / 2); break; case Gravity.BOTTOM: childTop = myHeight - mSpinnerPadding.bottom - childHeight; break; } return childTop; } private void makeAndAddView(int position, float angleOffset) { Log.d("ORDER", "makeAndAddView"); CarouselItem child; if (!mDataChanged) { child = (CarouselItem) mRecycler.get(position); if (child != null) { // Position the view setUpChild(child, child.getIndex(), angleOffset); } else { // Nothing found in the recycler -- ask the adapter for a view child = (CarouselItem) mAdapter.getView(position, null, this); Log.d("GETVIEW", "makeAndAddView1"); // Position the view setUpChild(child, child.getIndex(), angleOffset); } return; } // Nothing found in the recycler -- ask the adapter for a view child = (CarouselItem) mAdapter.getView(position, null, this); Log.d("GETVIEW", "makeAndAddView2"); // Position the view setUpChild(child, child.getIndex(), angleOffset); } private void onFinishedMovement() { if (mSuppressSelectionChanged) { mSuppressSelectionChanged = false; super.selectionChanged(); } checkSelectionChanged(); invalidate(); } /** * Brings an item with nearest to 0 degrees angle to this angle and sets it * selected */ private void scrollIntoSlots(int direction) { Log.d("ORDER", "scrollIntoSlots"); float angle; int position; ArrayListarr = new ArrayList (); for (int i = 0; i < getAdapter().getCount(); i++) { arr.add(((CarouselItem) getAdapter().getView(i, null, null))); Log.d("GETVIEW", "scrollIntoSlots"); } Collections.sort(arr, new Comparator () { public int compare(CarouselItem c1, CarouselItem c2) { int a1 = (int) c1.getCurrentAngle(); if (a1 > 180) { a1 = 360 - a1; } int a2 = (int) c2.getCurrentAngle(); if (a2 > 180) { a2 = 360 - a2; } return (a1 - a2); } }); angle = arr.get(0).getCurrentAngle(); if (angle > 180.0f) { angle = -(360.0f - angle); } if (Math.abs(angle) > 0.5f) { mFlingRunnable.startUsingDistance(-angle, 1, direction); } else { position = arr.get(0).getIndex(); setSelectedPositionInt(position); onFinishedMovement(); } } public int getIndex() { return mSelectedItemIndex; } private void resetIndex() { if (mSelectedItemIndex == 7) { mSelectedItemIndex = 0; } if (mSelectedItemIndex == -1) { mSelectedItemIndex = 6; } } public void toNextRightItem() { mSelectedItemIndex = mSelectedItemIndex - 1; resetIndex(); scrollToChild(mSelectedItemIndex, RIGHT); setSelectedPositionInt(mSelectedItemIndex); } public void toNextLeftItem() { mSelectedItemIndex = mSelectedItemIndex + 1; resetIndex(); scrollToChild(mSelectedItemIndex, LEFT); setSelectedPositionInt(mSelectedItemIndex); } void scrollToChild(int i, int v) { Log.d("ORDER", "scrollToChild"); CarouselItem view = (CarouselItem) getAdapter().getView(i, null, null); Log.d("GETVIEW", "scrollToChild"); float angle = view.getCurrentAngle(); Log.d("selectCurrentAngle", "Angle:" + angle); if (angle == 0) { return; } if (angle > 180.0f) { angle = 360.0f - angle; } else { angle = -angle; } mFlingRunnable.startUsingDistance(angle, 0, v); } public void setGravity(int gravity) { if (mGravity != gravity) { mGravity = gravity; requestLayout(); } } private void setUpChild(CarouselItem child, int index, float angleOffset) { Log.d("ORDER", "setUpChild"); // Ignore any layout parameters for child, use wrap content addViewInLayout(child, -1 /* index */, generateDefaultLayoutParams()); child.setSelected(index == mSelectedPosition); int h; int w; int d; if (mInLayout) { w = child.getMeasuredWidth(); h = child.getMeasuredHeight(); d = getMeasuredWidth(); } else { w = child.getMeasuredWidth(); h = child.getMeasuredHeight(); d = getWidth(); } child.setCurrentAngle(angleOffset); child.measure(w, h); int childLeft; int childTop = calculateTop(child, true); childLeft = 0; child.layout(childLeft, childTop - 45, w, h); Calculate3DPosition(child, d, angleOffset); } /** * Tracks a motion scroll. In reality, this is used to do just about any * movement to items (touch scroll, arrow-key scroll, set an item as * selected). */ void trackMotionScroll(float deltaAngle, float deg) { Log.d("ORDER", "trackMotionScroll"); for (int i = 0; i < getAdapter().getCount(); i++) { CarouselItem child = (CarouselItem) getAdapter().getView(i, null, null); Log.d("GETVIEW", "trackMotionScroll"); float angle = child.getCurrentAngle(); angle += deltaAngle; while (angle > 360.0f) { angle -= 360.0f; } while (angle < 0.0f) { angle += 360.0f; } child.setCurrentAngle(angle); child.setDegY(deg); Calculate3DPosition(child, getWidth(), angle); } mRecycler.clear(); invalidate(); } private void updateSelectedItemMetadata() { View oldSelectedChild = mSelectedChild; View child = mSelectedChild = getChildAt(mSelectedPosition - mFirstPosition); if (child == null) { return; } child.setSelected(true); child.setFocusable(true); if (hasFocus()) { child.requestFocus(); } if (oldSelectedChild != null) { oldSelectedChild.setSelected(false); oldSelectedChild.setFocusable(false); } } }
Demo测试类AndroidActivity.java
package com.john.carousel.test; import com.john.carousel.lib.Carousel; import com.john.carousel.lib.Carousel.CarouselItemClickListener; import com.john.carousel.lib.CarouselAdapter; import com.john.carousel.lib.CarouselAdapter.cOnItemClickListener; import com.john.carousel.lib.Constants; import com.john.carousel.lib.R; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnKeyListener; import android.widget.LinearLayout; public class AndroidActivity extends Activity implements CarouselItemClickListener, Constants { private Carousel carousel; private final String TAG = AndroidActivity.class.getSimpleName(); private LinearLayout layoutMain = null; private final int NETWORK = 0; private final int UPDATE = 1; private final int APK = 2; private final int STB = 3; private final int OTHER = 4; private final int WALLPAPER = 5; private final int MEDIA = 6; private int initSelection = 2; private long lastClickTime, currClickTime; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); View mainView = LayoutInflater.from(this).inflate(R.layout.activity_android, null); setContentView(mainView); if (getIntent() != null) { initSelection = getIntent().getExtras().getInt("selection", 2); } if (initSelection >= 6 || initSelection <= 0) { initSelection = initSelection % 7; } buildView(); } private void buildView() { carousel = (Carousel) findViewById(R.id.carousel); layoutMain = (LinearLayout) findViewById(R.id.layoutMain); layoutMain.setBackground(getResources().getDrawable(R.drawable.main_back00)); carousel.setDrawingCacheEnabled(true); carousel.setGravity(Gravity.TOP); carousel.setFocusFlag(true); carouselGetFocus(); carousel.setSelected(initSelection); carousel.setCarouselItemClickCallBack(this); carousel.setOnItemClickListener(new cOnItemClickListener() { @Override public void onItemClick(CarouselAdapter> parent, View view, int position, long id) { onItemClickOrCallback(position); } }); carousel.setOnKeyListener(new OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (event.equals(KeyEvent.ACTION_DOWN)) { switch (keyCode) { case KEY_LEFT: carousel.toNextLeftItem(); break; case KEY_RIGHT: carousel.toNextRightItem(); break; case KEY_OK: case KEY_CENTER: onItemClickOrCallback(carousel.getIndex()); break; } } carouselGetFocus(); return true; } }); } private void onItemClickOrCallback(int position) { switch (position) { case NETWORK: break; case UPDATE: break; case APK: break; case STB: break; case OTHER: break; case WALLPAPER: break; case MEDIA: break; default: break; } } @Override public void CarouselClickCallBack(int itemPosition) { onItemClickOrCallback(itemPosition); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KEY_OK: case KEY_CENTER: onItemClickOrCallback(carousel.getIndex()); return true; case KEY_LEFT: if (carousel.getFocusFlag()) { currClickTime = System.currentTimeMillis(); if (currClickTime - lastClickTime > 200) { lastClickTime = currClickTime; carousel.toNextLeftItem(); Log.d("selectedItemIndex", carousel.getIndex() + ""); return true; } else { return true; } } break; case KEY_RIGHT: if (carousel.getFocusFlag()) { currClickTime = System.currentTimeMillis(); if (currClickTime - lastClickTime > 200) { lastClickTime = currClickTime; carousel.toNextRightItem(); Log.d("selectedItemIndex", carousel.getIndex() + ""); return true; } else { return true; } } break; case KEY_UP: carousel.setFocusFlag(false); carousel.clearFocus(); carousel.setFocusable(false); carousel.setSelected(false); return true; case KEY_DOWN: if (!carousel.getFocusFlag()) { Log.e(TAG, "KEY_DOWN"); carouselGetFocus(); } return true; case KEY_EXIT: return true; case KEY_VOLDOWN: case KEY_VOLUP: case KEY_MUTE: case KEY_VOLUME_MUTE: return true; } return super.onKeyDown(keyCode, event); } private void carouselGetFocus() { carousel.setFocusFlag(true); carousel.requestFocus(); carousel.setFocusable(true); } }