在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()