将进酒

名不正则言不顺

Android酷炫动画效果之3D星体旋转效果

  在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类并自定义一些方法,具体实现和使用请参照http://www.cnblogs.com/zealotrouge/p/3380682.html

  本文要实现的3D星体旋转效果也是从这个CoverFlow演绎而来,不过CoverFlow只是对图像进行转动,我这里要实现的效果是要对所有的View进行类似旋转木马的转动,并且CoverFlow还存在很多已知bug,所以我这里需要重写一些类,并且将Scroller类用Rotator类替代,使界面看起来具有滚动效果,实际上是在转动一组图像。


首先我们需要自定义控件的一些属性,我们将控件取名Carousel,需要设置子项的最小个数和最大个数、当前选中项以及定义旋转角度等,attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="Carousel">
        <attr name="android:gravity" />
        <attr name="android:animationDuration" />
        <attr name="UseReflection" format="boolean" />
        <attr name="Items" format="integer" />
        <attr name="SelectedItem" format="integer" />
        <attr name="maxTheta" format="float" />
        <attr name="minQuantity" format="integer" />
        <attr name="maxQuantity" format="integer" />
        <attr name="Names" format="string" />
    </declare-styleable>

</resources>


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<CarouselImageView>
{

    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<CarouselItem>
{

    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>
{

    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 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 != 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<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();
        }
    }
}


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;
		ArrayList<CarouselItem> arr = new ArrayList<CarouselItem>();
		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<CarouselItem>()
		{

			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);
	}

}

效果展示

http://v.youku.com/v_show/id_XMTcyMDY3ODUxMg==.html


阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/johnWcheung/article/details/52496652
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

Android酷炫动画效果之3D星体旋转效果

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭