在写程序的时候遇到了Tween动画几个问题:
1, 执行动画的时候点击事件仍然在动画开始的位置?
2, XXXAnimation的构造参数里面的值具体是什么意思?
3, 平移动画中fromXValue和toXValue旋转动画中fromDegrees和toDegrees取负值有什么不同??(相信很多人也有疑惑)
4, RotateAnimation的int pivotXType, float pivotXValue, int pivotYType, float pivotYValue四个参数是怎么确定旋转原点的?确定的旋转原点在哪里?
Android动画分为:
Tween Animation View动画也叫补间动画
Drawable Animation 也叫Frame 帧动画
Property Animation(3.0以后加入)
主要研究Tween动画
我在写程序的时候经常由于参数设置不当(主要是从多少度旋转为多少度,有时是负的度数)得不到想要的效果。因此打算把动画的实现框架研究一下。
研究之前请看这篇文章:Android中图像变换Matrix的原理 了解一下Matrix矩阵的相关知识。明白图形的各种转换就是要得到对应的变换矩阵。
首先说一下动画的大概绘制框架过程,不然由于我写的比较乱可能看晕了。
调用startAnimation会设置与View关联的animation,然后会重绘视图,重绘视图的时候调用到drawChild,这时获取与View绑定的Animation,不为null了,只要动画时间没有结束就会通过绘制的时间获得变换矩阵,然后将画布原点平移到视图的左上角?(是不是这样?)绘制新的一帧。绘制完又会重绘,然后获取新的一帧的转换矩阵…..循环下去,直到动画结束就不再重绘视图了。
回到View的onDraw函数里面,onDraw函数做了如下工作。
1. Draw the background
2. If necessary, save the canvas' layers toprepare for fading
3. Draw view's content
4. Draw children
5. If necessary, draw the fading edges andrestore layers
6. Draw decorations (scrollbars forinstance)
当是ViewGroup的时候会执行第四步,dispatchDraw(canvas);
- @Override
- protected void dispatchDraw(Canvas canvas) {
-
- final LayoutAnimationController controller = mLayoutAnimationController;
- ...
-
-
- mPrivateFlags &= ~DRAW_ANIMATION;
- mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
- boolean more = false;
- final long drawingTime = getDrawingTime();
- if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
- for (int i = 0; i < count; i++) {
- final View child = children[i];
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
- more |= drawChild(canvas, child, drawingTime);
- }
- }
- } else {
- for (int i = 0; i < count; i++) {
- final View child = children[getChildDrawingOrder(count, i)];
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
- more |= drawChild(canvas, child, drawingTime);
- }
- }
- }
- ...
- }
肯定会执行到drawChild(canvas, child, drawingTime); 在该函数顾名思义就是绘制子控件。
- protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
- boolean more = false;
-
- final int cl = child.mLeft;
- final int ct = child.mTop;
- final int cr = child.mRight;
- final int cb = child.mBottom;
-
- final int flags = mGroupFlags;
-
- Transformation transformToApply = null;
-
- final Animation a = child.getAnimation();
- boolean concatMatrix = false;
-
- if (a != null) {
- if (mInvalidateRegion == null) {
- mInvalidateRegion = new RectF();
- }
- final RectF region = mInvalidateRegion;
-
- final boolean initialized = a.isInitialized();
- if (!initialized) {
-
- a.initialize(cr - cl, cb - ct, getWidth(), getHeight());
- a.initializeInvalidateRegion(0, 0, cr - cl, cb - ct);
- child.onAnimationStart();
- }
-
- if (mChildTransformation == null) {
- mChildTransformation = new Transformation();
- }
-
-
-
- more = a.getTransformation(drawingTime, mChildTransformation);
- transformToApply = mChildTransformation;
-
- concatMatrix = a.willChangeTransformationMatrix();
-
- if (more) {
-
- if (!a.willChangeBounds()) {
- if ((flags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) ==
- FLAG_OPTIMIZE_INVALIDATE) {
- mGroupFlags |= FLAG_INVALIDATE_REQUIRED;
- } else if ((flags & FLAG_INVALIDATE_REQUIRED) == 0) {
- mPrivateFlags |= DRAW_ANIMATION;
-
- invalidate(cl, ct, cr, cb);
- }
- } else {
- a.getInvalidateRegion(0, 0, cr - cl, cb - ct, region, transformToApply);
- mPrivateFlags |= DRAW_ANIMATION;
- final int left = cl + (int) region.left;
- final int top = ct + (int) region.top;
-
- invalidate(left, top, left + (int) region.width(), top + (int) region.height());
- }
- }
- } else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) ==
- FLAG_SUPPORT_STATIC_TRANSFORMATIONS) {
- if (mChildTransformation == null) {
- mChildTransformation = new Transformation();
- }
- final boolean hasTransform = getChildStaticTransformation(child, mChildTransformation);
- if (hasTransform) {
- final int transformType = mChildTransformation.getTransformationType();
- transformToApply = transformType != Transformation.TYPE_IDENTITY ?
- mChildTransformation : null;
- concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
- }
- }
- ...
- child.computeScroll();
-
- final int sx = child.mScrollX;
- final int sy = child.mScrollY;
- ...
- final boolean hasNoCache = cache == null;
- final int restoreTo = canvas.save();
- if (hasNoCache) {
- canvas.translate(cl - sx, ct - sy);
- } else {
-
-
-
- canvas.translate(cl, ct);
- if (scalingRequired) {
-
- final float scale = 1.0f / mAttachInfo.mApplicationScale;
- canvas.scale(scale, scale);
- }
- }
- float alpha = 1.0f;
- if (transformToApply != null) {
- if (concatMatrix) {
- int transX = 0;
- int transY = 0;
- if (hasNoCache) {
- transX = -sx;
- transY = -sy;
- }
-
- canvas.translate(-transX, -transY);
-
-
- canvas.concat(transformToApply.getMatrix());
- canvas.translate(transX, transY);
- mGroupFlags |= FLAG_CLEAR_TRANSFORMATION;
- }
- ...
- if (alpha < 1.0f && hasNoCache) {
- final int multipliedAlpha = (int) (255 * alpha);
- if (!child.onSetAlpha(multipliedAlpha)) {
- canvas.saveLayerAlpha(sx, sy, sx + cr - cl, sy + cb - ct, multipliedAlpha,
- Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
- } else {
- child.mPrivateFlags |= ALPHA_SET;
- }
- }
- } else if ((child.mPrivateFlags & ALPHA_SET) == ALPHA_SET) {
- child.onSetAlpha(255);
- }
- ...
- return more;
- }
接下来看一下Animation类:动画类里面有两个重要的函数protected void applyTransformation(float interpolatedTime,Transformation t)
和public boolean getTransformation(long currentTime, TransformationoutTransformation);
Transformation类包含了一个变换矩阵和alpha值。
applyTransformation函数:传入一个差值时间,会填充一个Transformation类。会在getTransformation函数里面调用,Animation类的applyTransformation是个空实现,具体的XXXAnimation在继承自Animation时会实现applyTransformation函数。
getTransformation函数:会在drawChild函数里面调用。
- 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 {
-
- 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) {
- if (mListener != null) {
-
- mListener.onAnimationStart(this);
- }
- mStarted = true;
- }
-
- 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;
- if (mListener != null) {
- mListener.onAnimationEnd(this);
- }
- }
- } else {
- if (mRepeatCount > 0) {
- mRepeated++;
- }
-
- if (mRepeatMode == REVERSE) {
- mCycleFlip = !mCycleFlip;
- }
-
- mStartTime = -1;
- mMore = true;
-
- if (mListener != null) {
- mListener.onAnimationRepeat(this);
- }
- }
- }
-
- if (!mMore && mOneMoreTime) {
- mOneMoreTime = false;
- return true;
- }
-
-
- return mMore;
- }
applyTransformation函数在Animation里面默认是空实现,需要在子类中实现,也就是说自定义动画需要实现applyTransformation函数。
插值类也很简单,是一个接口,只有一个函数。
- public interface Interpolator {
- float getInterpolation(float input);
- }
常用的子类有:
AccelerateDecelerateInterpolator 在动画开始与结束的地方速率改变比较慢,在中间的时候加速
AccelerateInterpolator 在动画开始的地方速率改变比较慢,然后开始加速
AnticipateInterpolator 开始的时候向后然后向前甩
AnticipateOvershootInterpolator 开始的时候向后然后向前甩一定值后返回最后的值
BounceInterpolator 动画结束的时候弹起
CycleInterpolator 动画循环播放特定的次数,速率改变沿着正弦曲线
DecelerateInterpolator 在动画开始的地方快然后慢
LinearInterpolator 以常量速率改变
OvershootInterpolator 向前甩一定值后再回到原来位置
也可以自定义interpolator,只需要实现getInterpolation函数就可以了。
前面简单的介绍了一下动画绘制所涉及的一些函数,接下来以执行平移动化为例将动画的执行过程走一遍:
1.定义完XXXAnimation后执行View的startAnimation
- public void startAnimation(Animation animation) {
- animation.setStartTime(Animation.START_ON_FIRST_FRAME);
-
- setAnimation(animation);
-
- invalidate();
- }
setAnimation函数如下:
- public void setAnimation(Animation animation) {
-
- mCurrentAnimation = animation;
- if (animation != null) {
- animation.reset();
- }
- }
2.然后请求重绘视图会执行onDraw函数,最后必然会执行到dispatchDraw函数,又会执行到drawChild(canvas,child, drawingTime); drawingTime函数是这次绘制的时间毫秒值。drawChild函数前面解释过。
3.首先获取View所关联的Animation,然后调用Animation的初始化函数,在这面会解析Animation的各个参数。对不同的xy类型和值进行转换,initialize函数也会在Animation的子类中实现。TranslateAnimation中的initialize函数如下,在该函数里面直接调用resolveSize函数解析构造参数中的值。
- public void initialize(int width, int height, int parentWidth, int parentHeight) {
- super.initialize(width, height, parentWidth, parentHeight);
- mFromXDelta = resolveSize(mFromXType, mFromXValue, width, parentWidth);
- mToXDelta = resolveSize(mToXType, mToXValue, width, parentWidth);
- mFromYDelta = resolveSize(mFromYType, mFromYValue, height, parentHeight);
- mToYDelta = resolveSize(mToYType, mToYValue, height, parentHeight);
- }
resolveSize函数如下,根据是绝对坐标、相对控件自身还是相对父控件和具体的值解析出解析后的值,很容易看出绝对坐标,直接返回对应值,相对自身和相对父控件就是用相对值结余0到1之间的值乘以子控件或者父视图宽高的值。
- 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;
- }
- }
4.然后就根据重绘的时间毫秒值通过getTransformation函数获得对应的转换矩阵。在这个函数里面会先调用interpolatedTime = mInterpolator.getInterpolation(normalizedTime);根据时间获得插值时间。
然后调用applyTransformation函数,TranslateAnimation的该函数具体如下:
- protected void applyTransformation(float interpolatedTime, Transformation t) {
- float dx = mFromXDelta;
- float dy = mFromYDelta;
-
- if (mFromXDelta != mToXDelta) {
-
- dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime);
- }
- if (mFromYDelta != mToYDelta) {
- dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime);
- }
-
- t.getMatrix().setTranslate(dx, dy);
- }
只要该动画没有结束getTransformation函数会一直返回true。然后判断返回true又会调用invalidate函数,接下来就是重复2,3,4的步骤了,但是重复执行的时候绘制时间不一样,于是获得的转换矩阵不一样,得到的新的视图的位置就不一样。如果返回false说明动画执行完成了,就不在重绘视图了。绘制控件时前面说过在绘制视图的时候会调用canvas.translate(cl - sx, ct - sy);简单地说就是将画布的坐标体系从屏幕左上角移动至动画视图的左上角。然后每次动画的时候canvas.concat(transformToApply.getMatrix());对话不的矩阵转换操作,然后绘图就实现了对动画的转换。
整个过程稍微有点复杂,有些函数我还没看的很明白,不过大致的思路就是这样。
回头看开始提出的几个问题:
1, 执行动画的时候其实并不是该控件本身在改变,而是他的父View完成的。startAnimation(anim)其实是给这个View设置了一个animation,而不是进行实际的动画绘制。他的位置其实根本没有改变,还是有layout所指定的位置决定的。
2, 参数的意思在代码里面的注释解释过了。
3, fromXValue和toXValue旋转动画中fromDegrees和toDegrees取正负值是有区别的,具体要看代码里面,转换矩阵是怎么生成的。比如平移动化里面:
- dx = mFromXDelta + ((mToXDelta - mFromXDelta) *interpolatedTime);
插值时间从0变到1,假设现在时相对自身的类型,控件本身宽度为100,mFromXDelta这些值在初始化的时候已经解析成了实际值(从0~1.0这种相和相对自身还是相对父View变成了像素宽高度)
从0变到1.0f的效果是:从当前位置向右平移100,原因是第一帧图像的dx为0,最后一帧dx为100
从-1.0f变到0的效果是:从当前位置的左边100处向右平移到当前位置,原因是第一帧图像的dx为-100,最后一帧的dx为0
旋转动画中通过指定开始结束角度的正负实现顺时针和逆时针旋转是类似的道理。大家可以自行感悟一下下面四个动画的区别,来看一下正负始末值对旋转的影响。
- rotateAnimation= new RotateAnimation(0, 90, Animation.RELATIVE_TO_PARENT, -0.5f, Animation.RELATIVE_TO_PARENT,0);
- rotateAnimation= new RotateAnimation(90, 0, Animation.RELATIVE_TO_PARENT, -0.5f,Animation.RELATIVE_TO_PARENT, 0);
- rotateAnimation= new RotateAnimation(-90, 0, Animation.RELATIVE_TO_PARENT, -0.5f,Animation.RELATIVE_TO_PARENT, 0);
- rotateAnimation = new RotateAnimation(0, -90,Animation.RELATIVE_TO_PARENT, -0.5f, Animation.RELATIVE_TO_PARENT, 0);
4, RotateAnimation的int pivotXType, float pivotXValue, int pivotYType, float pivotYValue四个参数的问题:
由于前面说过了,动画的时候,画布平移到了View的左上角,因此对于旋转动画来说参考的坐标原点始终是View左上角。而旋转的旋转点是可以任意指定的,该旋转点参考的坐标应该是View左上角。
手机屏幕中心处有一个点,现在我们想以屏幕最左边的那一边的中点为圆心旋转360度,代码应该是:
- new RotateAnimation(0, 360, Animation.RELATIVE_TO_PARENT,-0.5f, Animation.RELATIVE_TO_PARENT, 0);
以屏幕左上角旋转:
- new RotateAnimation(0, 360, Animation.RELATIVE_TO_PARENT,-0.5f, Animation.RELATIVE_TO_PARENT, -0.5f);
以自己中心旋转:
- new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f,Animation.RELATIVE_TO_SELF, 0.5f);
以自己最下面的中点旋转:
- new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f,Animation.RELATIVE_TO_SELF, 1.0f);
其他旋转点以此类推。我以前只是大致看过这些参数,所以在写围绕屏幕左上角旋转的代码时,想当然写成了:
new RotateAnimation(0, 360, Animation.RELATIVE_TO_PARENT, 0,Animation.RELATIVE_TO_PARENT, 0);
我本来想相对于父View来说(0,0)就是屏幕的左上角那一个点。结果当然不正确,测试一下也可以看出这个时候的旋转点仍然是View的左上角,是不是和RELATIVE_TO_PARENT没有半点关系?
总结一下就是说:后面的四个参数只是能算出来相对于画布坐标的距离,仅此而已,并不能看到是相对父View的值就忘了画布的原点在哪。
我在分析的时候只是大致的将动画的绘制过程研究了一下,实际的代码很复杂的。如果那里解释的不对的地方欢迎大家和我交流。
测试动平移和旋转动画的参数Demo:点此下载