上一节课,我们讲了Demo中主界面那个滑动门效果的动画实现过程,这节课,我们要稍微深入一点,研究一些复杂一些的动画实现了。大家在网上随便百度一下,应该也可以搜到很多关于Android动画方面的知识,从中可以了解到Android动画的分类,有的说分为三种,分别是:补间动画、帧动画、属性动画,有的分为两种,分别是:Tween动画、Frame动画。其实分为几类都无所谓,我们从实现原理上来说明一下,一类的实现方式就是通过动画内容的不断改变产生的,就像播放电影一样,每帧的画面都不同,连接起来就是一个完整的动画了,应该这种也就叫帧动画吧;另一种呢,是画面内容不变,比如我们的Demo中的滑动门,整个动画过程中展示的都是一张图片,那么我们通过不断的改变这个View控件的属性,比如变换它的位置、放大、缩小、旋转、变形、透明度等等属性,使它在每一帧产生不同的界面,整个连起来产生的动画,这种应该就叫属性动画了。所以,Android动画从本质上讲,万变不离其宗,再如何改变都逃不出这两个范围,而复杂一些的动画,可能会把这两个方面的因素加在一起,产生一些更复杂的效果而已,但是实现上还是这两种。
好了,明白了Android动画的实现原理后,我们本节课呢,就选择一个比较简单的例子来展开我们的内容。这节课呢,我们要选取的例子是Demo当中的复杂动画的最后一个unzoom_out,大家可以先通过它的效果来大概猜一下它的实现过程。我们点击unzoom_out动画,界面的View以屏幕中心点为基准不断缩小,最后消失,动画完成后,View控件恢复成原样。没有平移,没有旋转,没有透明度,那么就是生成一个Animation类,然后在每次系统回调时,将我们生成好的Animation类的变化大小的数据传给framework,然后让它对当前视图重绘,最后连贯在一起,就成了我们看到的不断缩放的动画。理解这个过程呢,也要对Vsync信号在应用侧的Choreographer中的分发、执行有比较好的掌握,才能更好的理解它的原理,如果有哪位同学对Choreographer的执行原理还有不熟悉的,请回头学一下前面的课程:Android Choreographer源码分析,如果对基本的动画的框架性原理还有不理解的地方,请复习一下上一课的知识:Android动画全解析(一)。
好了,大概能猜出来的unzoom_out的动画的实现方式,我们就深入来分析一下它的实现。我们可以打开手机自带的绘制布局边界的功能,然后再点一下unzoom_out动画,可以看到,整个过程,中间的就只有一个View控件,我们对照代码来看一下,当前的复杂动画对应的是ComplexActivity类,它所加载的布局文件是activity_anim_complex,非常简单,就包含了一个ListView组件,unzoom_out动画的实现是通过xml方式来实现的,它的定义是unzoom_out.xml,我们把它的布局文件代码贴出来,方便后边的分析:
好,到这里呢,我们对工程当的基本的构成有了一个初步的认识,下面我们就来分析它的实现。我们操作unzoom_out动画的过程对应在代码中的,只有两句:Animation anim = AnimationUtils.loadAnimation(ComplexActivity.this, ContantValue.complex[position])、listView_anim_complex.startAnimation(anim),非常的简单,从这里也可以看到Android系统动画框架的强大,它把所有的工作都系统性的完成了,我们要使用它非常简单。当然这也有不好的地方,就是如果开发者不对它进行研究的话,那么根本看不到它的实现过程,就会对它的实现一点都不了解,而只会使用。我们搞开发的,不光要知其然,还要知其所以然,这样才能提高我们的能力,想一想,我们每次面试的时候,面试官一问动画,我们的回答都是会使用,那有什么竞争力??你会用,别人也会用啊!但是如果我们研究透了动画的实现原理,我们就可以在简历上清楚的写上精通Android动画框架,非常清楚它的系统层实现原理,那是一种什么概念,当然这样也不敢说怎么样,但是比大部分停留在应用层的人马上高出一个档次,如果有淘汰的话,那我们的命运肯定比他们长!
呵呵,又扯远了,我们现在就来研究一下unzoom_out的动画实现,两句代码,意思也非常清楚,第一句就是把我们定义好的xml文件转换成一个Animation对象,第二句就是把它应用在当前的View控件上。跟上一节课不一样的,上一节课,系统会不断的回调我们,所以我们在重写computeScroll()方法,在每次系统回调时,把我们的坐标数据传回给系统,这样达到改变View控件位置的目的。而这次,我们不需要重写任何方法,只需要一句startAnimation,从中我们也可以猜到,系统肯定是把生成的Animation对象保存在哪里了,每次回调时候,就不需要通知我们了,因为参数都定义好了,直接自己取就OK了。那我们来看一下,它是把Animation对象保存在哪里了,又是怎么取的呢?
整个过程我们就分成两步:1、生成Animation对象;2、调用startAnimation开始加载动画。
一、调用AnimationUtils.loadAnimation()方法成生Animation对象
这个方法的代码在AnimationUtils中:
/**
* Loads an {@link Animation} object from a resource
*
* @param context Application context used to access resources
* @param id The resource id of the animation to load
* @return The animation object reference by the specified id
* @throws NotFoundException when the animation cannot be loaded
*/
public static Animation loadAnimation(Context context, int id)
throws NotFoundException {
XmlResourceParser parser = null;
try {
parser = context.getResources().getAnimation(id);
return createAnimationFromXml(context, parser);
} catch (XmlPullParserException ex) {
NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
Integer.toHexString(id));
rnf.initCause(ex);
throw rnf;
} catch (IOException ex) {
NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
Integer.toHexString(id));
rnf.initCause(ex);
throw rnf;
} finally {
if (parser != null) parser.close();
}
}
private static Animation createAnimationFromXml(Context c, XmlPullParser parser)
throws XmlPullParserException, IOException {
return createAnimationFromXml(c, parser, null, Xml.asAttributeSet(parser));
}
private static Animation createAnimationFromXml(Context c, XmlPullParser parser,
AnimationSet parent, AttributeSet attrs) throws XmlPullParserException, IOException {
Animation anim = null;
// Make sure we are on a start tag.
int type;
int depth = parser.getDepth();
while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
&& type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
if (name.equals("set")) {
anim = new AnimationSet(c, attrs);
createAnimationFromXml(c, parser, (AnimationSet)anim, attrs);
} else if (name.equals("alpha")) {
anim = new AlphaAnimation(c, attrs);
} else if (name.equals("scale")) {
anim = new ScaleAnimation(c, attrs);
} else if (name.equals("rotate")) {
anim = new RotateAnimation(c, attrs);
} else if (name.equals("translate")) {
anim = new TranslateAnimation(c, attrs);
} else {
throw new RuntimeException("Unknown animation name: " + parser.getName());
}
if (parent != null) {
parent.addAnimation(anim);
}
}
return anim;
}
/**
* Constructor used when a ScaleAnimation is loaded from a resource.
*
* @param context Application context to use
* @param attrs Attribute set from which to read values
*/
public ScaleAnimation(Context context, AttributeSet attrs) {
super(context, attrs);
mResources = context.getResources();
TypedArray a = context.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.ScaleAnimation);
TypedValue tv = a.peekValue(
com.android.internal.R.styleable.ScaleAnimation_fromXScale);
mFromX = 0.0f;
if (tv != null) {
if (tv.type == TypedValue.TYPE_FLOAT) {
// This is a scaling factor.
mFromX = tv.getFloat();
} else {
mFromXType = tv.type;
mFromXData = tv.data;
}
}
tv = a.peekValue(
com.android.internal.R.styleable.ScaleAnimation_toXScale);
mToX = 0.0f;
if (tv != null) {
if (tv.type == TypedValue.TYPE_FLOAT) {
// This is a scaling factor.
mToX = tv.getFloat();
} else {
mToXType = tv.type;
mToXData = tv.data;
}
}
tv = a.peekValue(
com.android.internal.R.styleable.ScaleAnimation_fromYScale);
mFromY = 0.0f;
if (tv != null) {
if (tv.type == TypedValue.TYPE_FLOAT) {
// This is a scaling factor.
mFromY = tv.getFloat();
} else {
mFromYType = tv.type;
mFromYData = tv.data;
}
}
tv = a.peekValue(
com.android.internal.R.styleable.ScaleAnimation_toYScale);
mToY = 0.0f;
if (tv != null) {
if (tv.type == TypedValue.TYPE_FLOAT) {
// This is a scaling factor.
mToY = tv.getFloat();
} else {
mToYType = tv.type;
mToYData = tv.data;
}
}
Description d = Description.parseValue(a.peekValue(
com.android.internal.R.styleable.ScaleAnimation_pivotX));
mPivotXType = d.type;
mPivotXValue = d.value;
d = Description.parseValue(a.peekValue(
com.android.internal.R.styleable.ScaleAnimation_pivotY));
mPivotYType = d.type;
mPivotYValue = d.value;
a.recycle();
initializePivotPoint();
}
先调用父类的带两个参数的构造方法,因为在父类中要进行一些必要的初始化,我们就继续跟进去看一下父类的构造方法:
/**
* Creates a new animation whose parameters come from the specified context and
* attributes set.
*
* @param context the application environment
* @param attrs the set of attributes holding the animation parameters
*/
public Animation(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Animation);
setDuration((long) a.getInt(com.android.internal.R.styleable.Animation_duration, 0));
setStartOffset((long) a.getInt(com.android.internal.R.styleable.Animation_startOffset, 0));
setFillEnabled(a.getBoolean(com.android.internal.R.styleable.Animation_fillEnabled, mFillEnabled));
setFillBefore(a.getBoolean(com.android.internal.R.styleable.Animation_fillBefore, mFillBefore));
setFillAfter(a.getBoolean(com.android.internal.R.styleable.Animation_fillAfter, mFillAfter));
setRepeatCount(a.getInt(com.android.internal.R.styleable.Animation_repeatCount, mRepeatCount));
setRepeatMode(a.getInt(com.android.internal.R.styleable.Animation_repeatMode, RESTART));
setZAdjustment(a.getInt(com.android.internal.R.styleable.Animation_zAdjustment, ZORDER_NORMAL));
setBackgroundColor(a.getInt(com.android.internal.R.styleable.Animation_background, 0));
setDetachWallpaper(a.getBoolean(com.android.internal.R.styleable.Animation_detachWallpaper, false));
final int resID = a.getResourceId(com.android.internal.R.styleable.Animation_interpolator, 0);
a.recycle();
if (resID > 0) {
setInterpolator(context, resID);
}
ensureInterpolator();
}
/**
* Loads an {@link Interpolator} object from a resource
*
* @param context Application context used to access resources
* @param id The resource id of the animation to load
* @return The animation object reference by the specified id
* @throws NotFoundException
*/
public static Interpolator loadInterpolator(Context context, int id) throws NotFoundException {
XmlResourceParser parser = null;
try {
parser = context.getResources().getAnimation(id);
return createInterpolatorFromXml(context, parser);
} catch (XmlPullParserException ex) {
NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
Integer.toHexString(id));
rnf.initCause(ex);
throw rnf;
} catch (IOException ex) {
NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
Integer.toHexString(id));
rnf.initCause(ex);
throw rnf;
} finally {
if (parser != null) parser.close();
}
}
private static Interpolator createInterpolatorFromXml(Context c, XmlPullParser parser)
throws XmlPullParserException, IOException {
Interpolator interpolator = null;
// Make sure we are on a start tag.
int type;
int depth = parser.getDepth();
while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
&& type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
AttributeSet attrs = Xml.asAttributeSet(parser);
String name = parser.getName();
if (name.equals("linearInterpolator")) {
interpolator = new LinearInterpolator(c, attrs);
} else if (name.equals("accelerateInterpolator")) {
interpolator = new AccelerateInterpolator(c, attrs);
} else if (name.equals("decelerateInterpolator")) {
interpolator = new DecelerateInterpolator(c, attrs);
} else if (name.equals("accelerateDecelerateInterpolator")) {
interpolator = new AccelerateDecelerateInterpolator(c, attrs);
} else if (name.equals("cycleInterpolator")) {
interpolator = new CycleInterpolator(c, attrs);
} else if (name.equals("anticipateInterpolator")) {
interpolator = new AnticipateInterpolator(c, attrs);
} else if (name.equals("overshootInterpolator")) {
interpolator = new OvershootInterpolator(c, attrs);
} else if (name.equals("anticipateOvershootInterpolator")) {
interpolator = new AnticipateOvershootInterpolator(c, attrs);
} else if (name.equals("bounceInterpolator")) {
interpolator = new BounceInterpolator(c, attrs);
} else {
throw new RuntimeException("Unknown interpolator name: " + parser.getName());
}
}
return interpolator;
}
/**
* An interpolator where the rate of change is constant
*
*/
public class LinearInterpolator implements Interpolator {
public LinearInterpolator() {
}
public LinearInterpolator(Context context, AttributeSet attrs) {
}
public float getInterpolation(float input) {
return input;
}
}
/**
* Start the specified animation now.
*
* @param animation the animation to start now
*/
public void startAnimation(Animation animation) {
animation.setStartTime(Animation.START_ON_FIRST_FRAME);
setAnimation(animation);
invalidateParentCaches();
invalidate(true);
}
/**
* Utility function, called by draw(canvas, parent, drawingTime) to handle the less common
* case of an active Animation being run on the view.
*/
private boolean drawAnimation(ViewGroup parent, long drawingTime,
Animation a, boolean scalingRequired) {
Transformation invalidationTransform;
final int flags = parent.mGroupFlags;
final boolean initialized = a.isInitialized();
if (!initialized) {
a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
onAnimationStart();
}
final Transformation t = parent.getChildTransformation();
boolean more = a.getTransformation(drawingTime, t, 1f);
if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
if (parent.mInvalidationTransformation == null) {
parent.mInvalidationTransformation = new Transformation();
}
invalidationTransform = parent.mInvalidationTransformation;
a.getTransformation(drawingTime, invalidationTransform, 1f);
} else {
invalidationTransform = t;
}
if (more) {
if (!a.willChangeBounds()) {
if ((flags & (ViewGroup.FLAG_OPTIMIZE_INVALIDATE | ViewGroup.FLAG_ANIMATION_DONE)) ==
ViewGroup.FLAG_OPTIMIZE_INVALIDATE) {
parent.mGroupFlags |= ViewGroup.FLAG_INVALIDATE_REQUIRED;
} else if ((flags & ViewGroup.FLAG_INVALIDATE_REQUIRED) == 0) {
// The child need to draw an animation, potentially offscreen, so
// make sure we do not cancel invalidate requests
parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
parent.invalidate(mLeft, mTop, mRight, mBottom);
}
} else {
if (parent.mInvalidateRegion == null) {
parent.mInvalidateRegion = new RectF();
}
final RectF region = parent.mInvalidateRegion;
a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region,
invalidationTransform);
// The child need to draw an animation, potentially offscreen, so
// make sure we do not cancel invalidate requests
parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
final int left = mLeft + (int) region.left;
final int top = mTop + (int) region.top;
parent.invalidate(left, top, left + (int) (region.width() + .5f),
top + (int) (region.height() + .5f));
}
}
return more;
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mFromX = resolveScale(mFromX, mFromXType, mFromXData, width, parentWidth);
mToX = resolveScale(mToX, mToXType, mToXData, width, parentWidth);
mFromY = resolveScale(mFromY, mFromYType, mFromYData, height, parentHeight);
mToY = resolveScale(mToY, mToYType, mToYData, height, parentHeight);
mPivotX = resolveSize(mPivotXType, mPivotXValue, width, parentWidth);
mPivotY = resolveSize(mPivotYType, mPivotYValue, height, parentHeight);
}
float resolveScale(float scale, int type, int data, int size, int psize) {
float targetSize;
if (type == TypedValue.TYPE_FRACTION) {
targetSize = TypedValue.complexToFraction(data, size, psize);
} else if (type == TypedValue.TYPE_DIMENSION) {
targetSize = TypedValue.complexToDimension(data, mResources.getDisplayMetrics());
} else {
return scale;
}
if (size == 0) {
return 1;
}
return targetSize/(float)size;
}
/**
* Convert the information in the description of a size to an actual
* dimension
*
* @param type One of Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
* Animation.RELATIVE_TO_PARENT.
* @param value The dimension associated with the type parameter
* @param size The size of the object being animated
* @param parentSize The size of the parent of the object being animated
* @return The dimension to use for the animation
*/
protected float resolveSize(int type, float value, int size, int parentSize) {
switch (type) {
case ABSOLUTE:
return value;
case RELATIVE_TO_SELF:
return size * value;
case RELATIVE_TO_PARENT:
return parentSize * value;
default:
return value;
}
}
/**
* Gets the transformation to apply at a specified point in time. Implementations of this
* method should always replace the specified Transformation or document they are doing
* otherwise.
*
* @param currentTime Where we are in the animation. This is wall clock time.
* @param outTransformation A transformation object that is provided by the
* caller and will be filled in by the animation.
* @return True if the animation is still running
*/
public boolean getTransformation(long currentTime, Transformation outTransformation) {
if (mStartTime == -1) {
mStartTime = currentTime;
}
final long startOffset = getStartOffset();
final long duration = mDuration;
float normalizedTime;
if (duration != 0) {
normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /
(float) duration;
} else {
// time is a step-change with a zero duration
normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
}
final boolean expired = normalizedTime >= 1.0f;
mMore = !expired;
if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
if (!mStarted) {
fireAnimationStart();
mStarted = true;
if (USE_CLOSEGUARD) {
guard.open("cancel or detach or getTransformation");
}
}
if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
if (mCycleFlip) {
normalizedTime = 1.0f - normalizedTime;
}
final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
applyTransformation(interpolatedTime, outTransformation);
}
if (expired) {
if (mRepeatCount == mRepeated) {
if (!mEnded) {
mEnded = true;
guard.close();
fireAnimationEnd();
}
} else {
if (mRepeatCount > 0) {
mRepeated++;
}
if (mRepeatMode == REVERSE) {
mCycleFlip = !mCycleFlip;
}
mStartTime = -1;
mMore = true;
fireAnimationRepeat();
}
}
if (!mMore && mOneMoreTime) {
mOneMoreTime = false;
return true;
}
return mMore;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float sx = 1.0f;
float sy = 1.0f;
float scale = getScaleFactor();
if (mFromX != 1.0f || mToX != 1.0f) {
sx = mFromX + ((mToX - mFromX) * interpolatedTime);
}
if (mFromY != 1.0f || mToY != 1.0f) {
sy = mFromY + ((mToY - mFromY) * interpolatedTime);
}
if (mPivotX == 0 && mPivotY == 0) {
t.getMatrix().setScale(sx, sy);
} else {
t.getMatrix().setScale(sx, sy, scale * mPivotX, scale * mPivotY);
}
}
sx = mFromX + ((mToX - mFromX) * interpolatedTime);
}
if (mFromY != 1.0f || mToY != 1.0f) {
sy = mFromY + ((mToY - mFromY) * interpolatedTime);
}
/**
* An animation that controls the alpha level of an object.
* Useful for fading things in and out. This animation ends up
* changing the alpha property of a {@link Transformation}
*
*/
public class AlphaAnimation extends Animation {
private float mFromAlpha;
private float mToAlpha;
/**
* Constructor used when an AlphaAnimation is loaded from a resource.
*
* @param context Application context to use
* @param attrs Attribute set from which to read values
*/
public AlphaAnimation(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a =
context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.AlphaAnimation);
mFromAlpha = a.getFloat(com.android.internal.R.styleable.AlphaAnimation_fromAlpha, 1.0f);
mToAlpha = a.getFloat(com.android.internal.R.styleable.AlphaAnimation_toAlpha, 1.0f);
a.recycle();
}
/**
* Constructor to use when building an AlphaAnimation from code
*
* @param fromAlpha Starting alpha value for the animation, where 1.0 means
* fully opaque and 0.0 means fully transparent.
* @param toAlpha Ending alpha value for the animation.
*/
public AlphaAnimation(float fromAlpha, float toAlpha) {
mFromAlpha = fromAlpha;
mToAlpha = toAlpha;
}
/**
* Changes the alpha property of the supplied {@link Transformation}
*/
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float alpha = mFromAlpha;
t.setAlpha(alpha + ((mToAlpha - alpha) * interpolatedTime));
}
@Override
public boolean willChangeTransformationMatrix() {
return false;
}
@Override
public boolean willChangeBounds() {
return false;
}
/**
* @hide
*/
@Override
public boolean hasAlpha() {
return true;
}
}