Android Animation动画原理分析

年代久远,仅供参考

动画在Android应用中是很常用的,而系统提供的Animation相关动画机制由于简单实用,经常是我们实现动画的首要选择。本文旨在分析Animation相关类实现动画的原理,目的是了解动画实现的主要流程,由于源码较为繁杂,故对一些与主干流程并不相关的代码进行了忽略。本文源代码基于4.0。

如果只是想对Animation实现有个简要了解,可忽略前两节,直接从第三节看起。

一、view基本层级结构

Animation动画实在view的draw()方法里实现的,因此,要想了解Animation的实现原理,不得不对View的相关结构有个大致的了解,这样才能在阅读源码的过程中按图索骥,有迹可循。

View、Activity、ViewRoot大致关系如下:

 

如上图,每个Activity都有一个对应的PhoneWindow类,PhoneWindow类封装了一个DecorView,就是说PhoneWindow内有一个类型为DecorView的属性。DecorView是Activity内所有View的根view,就是说它是最上层的View,所有的view都是它的子view。

另外Activity还有一个对应的ViewRootImp类。ViewRootImp是个很重要的类,它主要负责对View进行管理和驱动,比如在每次绘制工作之前,向本地Surface申请一个Canvas,然后将这个canvas传给DecorView,其后DecorView将Canvas一层层往下传,页面上的所有View便在这个Canvas画布上进行绘制。其次,ViewRootImp内部封装了一个基于主线程的Handler,每次View的invalidate()实际上是给这个handler发消息,由此驱动了这个Activity内View的重绘。

每个View都有一个ViewParent类型的mParent属性,一般情况下子view的mParent指向作为ViewGroup的父View,DecorView一般是最终的那个父View。而DecorView的mParent属性指向了ViewRootImp。虽然ViewRootImp并不是个View,但是它实现了ViewParent接口。同时,ViewRootImp也有个mView属性,它指向了DecorView。通过互相引用,DecorView和ViewRootImp之间就可以进行彼此调用了。

注意,这个图表达的类间的关系实际上并不严谨,PhoneWindow其实是Activity的一个名为mWindow的属性,而Activity并没有直接封装ViewRootImp,Activity有一个mWindowManager属性指向了WindowManagerImp,这个WindowManager创建并管理了管理了所有的Activity的ViewRootImp。对于本文而言,对DecorView和ViewRootImp有上图那样的大概印象就可以了。为了迎娶白富美,不要在意这些细节。

二、前传。

我们常用的使用Animation的方式是new一个Animation类型的实例,随后通过调用View的startAnimation()方法启动动画,我们就从这个方法开始跟踪动画的实现细节。Android提供了TranslateAnimation、ScaleAnimation、AlphaAnimation等等不同类型的Animation子类,这些动画在具体实现时有部分差别,为了简单明了起见,这里我们假设使用的是TranslateAnimation,也就是位移动画。

首先看源码View的startAnimation()方法:

public void startAnimation(Animation animation) {
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
        setAnimation(animation);
        invalidateParentCaches();
        invalidate(true);
}

首先,会将传入的animation对象的startTime属性设置为Animation.START_ON_FIRST_FRAME,实际上这个属性稍后会再次被设置,这里先不管。setAnimation(animation);这句话是将animation对象设置到View的mCurrentAnimation属性。如下:

public void setAnimation(Animation animation) {
        mCurrentAnimation = animation;

        if (animation != null) {
            if (mAttachInfo != null && !mAttachInfo.mScreenOn &&
                    animation.getStartTime() == Animation.START_ON_FIRST_FRAME) {
                animation.setStartTime(AnimationUtils.currentAnimationTimeMillis());
            }
            animation.reset();
        }
	}

这里发现animation的StartTime属性被重置为animation.setStartTime(AnimationUtils.currentAnimationTimeMillis()。看一下AnimationUtils.currentAnimationTimeMillis()发现其实就是当前时间。

回到startAnimation方法,最后一句是invalidate(true);这句很重要。它很像我们刷新页面时用的invalidate方法,只是多了个参数。那我们看一下我们常用的invalidate方法:

public void invalidate() {
        invalidate(true);
}

 没错,实际上调的也是这个方法。从这里大概可猜出,动画的实际上是view在本身的绘制过程中实现的。下面看一下这个方法的内容:

void invalidate(boolean invalidateCache) {
        if (skipInvalidate()) {
            return;
        }
        if (……) {
				.......
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;

				.......
			
            if (p != null && ai != null) {
                final Rect r = ai.mTmpInvalRect;
                r.set(0, 0, mRight - mLeft, mBottom - mTop);
                p.invalidateChild(this, r);
            }
        }
}

ViewParent p = mParent, 然后调用了p.invalidateChild(this, r);那么mParent是什么呢?一般实际应用中View是多层级的,也就是一个ViewGroup包含一个View或者ViewGroup,而ViewGroup的addView方法调用时会将子view的mParent赋为自身。也就是一般情况下,一个View的mParent是它的父view,当然,这个父view是个ViewGroup。那么我们就去ViewGroup里看看这个方法:

public final void invalidateChild(View child, final Rect dirty) {

            ViewParent parent = this;

	        ……
       do {
                View view = null;
                if (parent instanceof View) {
                    view = (View) parent;
                }

                ……

                parent = parent.invalidateChildInParent(location, dirty);
                ……
            } while (parent != null);
        }
}

在这个方法里,首先声明了一个局部变量parent并指向自己,随后进入一个while循环中。parent = parent.invalidateChildInParent(location, dirty);这句很关键,我们看一下invalidateChildInParent()方法的代码:

public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
	If (……){
	……
	return mParent;
	}
	Else {
	……
	return mParent;
	……
	}
}

这个方法里面除了做一些判断、标记重置等,就直接返回mParent了,好像什么也没做,由于ViewGroup继承于View,所以这个mParent实际上是从View继承来的,因此这个mParent也指向当前这个ViewGroup的父View。回到上个方法,while (parent != null)这个条件说明只要parent不是空,就会一直遍历所有父view的invalidateChildInParent方法。

但是这个方法里面除了做一些标识位处理和其他准备工作,好像没做跟绘制和动画有关的操作。但是别忘了,view体系的最顶层是DecorView,而前面讲过,DecorView的mParent是ViewRootImp,所以这段循环的最终会调用到ViewRootImp的invalidateChildInParent()方法,这才是关键!

马上跳去ViewRootImp的invalidateChildInParent()方法看看:

public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
……
if (!mWillDrawSoon) {
            scheduleTraversals();
        }

        return null;
}

忽略重重迷雾,最终看到scheduleTraversals();虽然这里说的是动画的实现流程,但上面也提到,这实际上也是View.invalidate()也就是View绘制的流程。那我们看scheduleTraversals()有什么:

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            scheduleConsumeBatchedInput();
        }
}

mChoreographer实际上封装了一个基于主线程的Handler,上面的代码中就是往这个handler里发送了mTraversalRunnable,mTraversalRunnable里调用了doTraversal()方法,而这个方法里主要执行了performTraversals()方法。

performTraversals()这个方法里面先后执行了performMeasure()、performMeasure、performDraw,是的,我们熟悉的View对应的measure、layout、draw方法就是从这个地方开始的。在这里暂时只关注performDraw。这个方法执行了ViewRootImp的draw(Boolean)->drawSoftware(),在drawSoftware里面,从本地Surface获取画布Canvas对象之后,便调用了DecorView的draw(Canvas)方法,并将canvas对象传入方法内。至此,就回到了我们熟悉的View类里面去了。

三、动画实现

public void draw(Canvas canvas) {

	……
	// Step 3, draw the content
	if (!dirtyOpaque) onDraw(canvas);
	……
	// Step 4, draw the children
	dispatchDraw(canvas);
	……
	// Step 6, draw decorations (scrollbars)
	onDrawScrollBars(canvas);
	
	if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(left, top, right, top + length, p);
        }

        if (drawBottom) {
            matrix.setScale(1, fadeHeight * bottomFadeStrength);
            matrix.postRotate(180);
            matrix.postTranslate(left, bottom);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(left, bottom - length, right, bottom, p);
        }

        if (drawLeft) {
            matrix.setScale(1, fadeHeight * leftFadeStrength);
            matrix.postRotate(-90);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(left, top, left + length, bottom, p);
        }

        if (drawRight) {
            matrix.setScale(1, fadeHeight * rightFadeStrength);
            matrix.postRotate(90);
            matrix.postTranslate(right, top);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(right - length, top, right, bottom, p);
        }

        canvas.restoreToCount(saveCount);

        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
}

在View的draw(Canvas canvas)里面先后调用了onDraw、dispatchDraw方法。dispatchDraw在ViewGroup中实现,用来将canvas分发给子View。dispatchDraw中调用了每个child的boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) 方法。这个方法很有料,真正使用了最开始我们设置到View里的mCurrentAnimation属性,就是我们指定的那个动画。除次之外还调用了computeScroll();这也是一种动画实现方式,我们暂不关注。

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
	……
	Transformation transformToApply = null;
	
	final Animation a = getAnimation();
	if (a != null) {
		more = drawAnimation(parent, drawingTime, a, scalingRequired);
		concatMatrix = a.willChangeTransformationMatrix();
		if (concatMatrix) {
			mPrivateFlags3 |= VIEW_IS_ANIMATING_TRANSFORM;
		}
		transformToApply = parent.mChildTransformation;
	} else {
		……
	}
	
		……
		// 先保存canvas状态
		restoreTo = canvas.save();
		……
		if (transformToApply != null) {
			if (concatMatrix) {
				if (useDisplayListProperties) {
					displayList.setAnimationMatrix(transformToApply.getMatrix());
				} else {
					// Undo the scroll translation, apply the transformation matrix,
					// then redo the scroll translate to get the correct result.
					canvas.translate(-transX, -transY);
					canvas.concat(transformToApply.getMatrix());
					canvas.translate(transX, transY);
				}
				parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
			}

			float transformAlpha = transformToApply.getAlpha();
			if (transformAlpha < 1) {
				alpha *= transformAlpha;
				parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
			}
		}

		……

		if (offsetForScroll) {
            canvas.translate(mLeft - sx, mTop - sy);
        } else {
            if (!useDisplayListProperties) {
                canvas.translate(mLeft, mTop);
            }
            if (scalingRequired) {
                if (useDisplayListProperties) {
                    // TODO: Might not need this if we put everything inside the DL
                    restoreTo = canvas.save();
                }
                // mAttachInfo cannot be null, otherwise scalingRequired == false
                final float scale = 1.0f / mAttachInfo.mApplicationScale;
                canvas.scale(scale, scale);
            }
        }

		……
	
		float alpha = useDisplayListProperties ? 1 : getAlpha();
		……
		if (alpha < 1 || (mPrivateFlags3 & VIEW_IS_ANIMATING_ALPHA) == VIEW_IS_ANIMATING_ALPHA) {

			final int multipliedAlpha = (int) (255 * alpha);
			……
			canvas.saveLayerAlpha(scrollX, scrollY, scrollX + mRight - mLeft,
	                                    scrollY + mBottom - mTop, multipliedAlpha, layerFlags);
		}

		// 这段也很重要
		if ((flags & ViewGroup.FLAG_CLIP_CHILDREN) == ViewGroup.FLAG_CLIP_CHILDREN &&
                !useDisplayListProperties) {
            if (offsetForScroll) {
                canvas.clipRect(sx, sy, sx + (mRight - mLeft), sy + (mBottom - mTop));
            } else {
                if (!scalingRequired || cache == null) {
                    canvas.clipRect(0, 0, mRight - mLeft, mBottom - mTop);
                } else {
                    canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
                }
            }
        }
		……
		
}

首先,final Animation a = getAnimation();而getAnimaion实际上取的是开始我们赋过值的那个mCurrentAnimation,很显然,这里a不为空。于是调用了drawAnimation方法。

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

        boolean more = a.getTransformation(drawingTime, parent.mChildTransformation, 1f);
		……
	if (more) {	
	parent.invalidate(mLeft, mTop, mRight, mBottom);
	}
	……
        return more;
}

调用了Animation的getTransfromation方法,并传入parent.mChildTransformation作为参数,这是一个参数-返回值类型的参数,就是说方法执行后的结果是保存在这个参数里的。

这个getTransfromation方法是重点,表明了Animation具体的执行方式:

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

首先我们看第一句,如果mStartTime属性为-1, mStartTime赋值为参数 currentTime,这个currentTime是从上面GroupView的dispatchDraw一直传过来的,实际上取的是View的属性mAttachInfo.mDrawingTime的值。由于在一开始我们startAnimation的时候将mStartTime赋值为当前时间,所以这个判断略过。

final long startOffset = getStartOffset();这句话是获取动画开始的延迟时间。如果在我们构建Animation的时候指定了startOffset,那么在这里将会体现这个延迟值。

normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) / (float) duration;

这一句算的是从动画开始的时间到当前时间的时间差,占整个动画时间的比例,换句话说就是在整个时长为duration(就是我们在构建Animation时指定的动画时长setDuration())的动画过程中,我们已经走了多少比例了。如果延迟值startOffset不为0,那么开始时间要从延迟到达后的点算起。

有了这个比例后,便能很容易换算出具体动画的运行程度。比如说,当前我算出的时间比是normalizedTime = 0.1, 就是走了整个动画时间的百分之10, 那么,如果当前动画是位移动画的话,只要将总的移动距离乘以这个0.1,就是当前应该移动的距离。同样如果是透明度动画,那当前的透明度就应该是总的透明度乘以0.1。

从上面的公式看出,normalizedTime的范围应该是0-1,因为当前时间减去开始时间等于duration时动画就应该结束了,这才符合我们的预期,这是会将expired即为true,标识本次动画已过期,不用再继续了。如果在0-1的范围内,说明动画正在进行过程中,我们往下看:

interpolatedTime = mInterpolator.getInterpolation(normalizedTime);

mInterpolator是一个插入器。每个Animation都有一个对应的插入器。在构建Animation的时候我们可以自己指定一个加速(AccelerateInterpolator)、减速(DecelerateInterpolator)等类型的插入器。如果没有指定,那么Animation会指定一个默认的AccelerateDecelerateInterpolator插入器。为简单期间。我们假设在构建Animation时指定的是加速类型的插入器AccelerateInterpolator。在这句话当中调用了插入器的getInterpolation方法。我们看看AccelerateInterpolator的getInterpolation的具体实现:

public float getInterpolation(float input) {
        if (mFactor == 1.0f) {
            return input * input;
        } else {
            return (float)Math.pow(input, mDoubleFactor);
        }
	}

就是这么简单。在我们手动构建Animation的情况下mFactor 一般等于1。所以这里返回的是这一句input * input;回到之前的代码,我们知道这个input就是我们之前算出的时间比例normalizedTime。所以,如果原来normalizedTime的增长方式是0.1,0.2, 那么在加速插入器处理过后,就变成了0.01,0.004,明显是一个加速的过程。这个插入器就是这么简单的实现了加速过程。同样,其他插入器也是通过对时间比例normalizedTime做相应的运算处理后,实现了相应的运动变化效果。从系统提供的几个插入器来看,无论对normalizedTime做如何运算,处理过的值都不会超出0-1这个范围,这也符合我们之前对normalizedTime的理解。

往下看:

applyTransformation(interpolatedTime, outTransformation);

在对interpolatedTime是刚刚插入器对normalizedTime处理后的值,outTransformation是方法调用者传进来的参数,也就是View的draw方法里的parent.mChildTransformation。而applyTransformation本身是一个空方法,由各类动画子类具体实现。这个方法是不同类型方法具体规定动画行为的,有了Animation这个父类计算后的interpolatedTime即当前时间点动画运行比例,就可以很轻松的规定具体动画应该变化到一个什么程度。所以在自定义动画时,一般都是重写Animation.applyTransformation的这个方法,因为时间计算Animation已经做好了,你只需要转化成具体行为就可以了。

还是以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);
}

我们看它对x坐标和y坐标的运行差值做了写处理,随后赋值给了传进来的Transformation 类型的参数t,保存在了t的matrix属性里。重复一下,这个t实际上就是之前view的Draw方法里的parent.mChildTransformation。

至此,动画相关计算已经完毕,并把相关值保存进了parent.mChildTransformation的matrix属性里。再会到getTransformation方法,看看后续的操作。

之前我们知道,当normalizedTime大于1说明动画已经过期,expired为true,这时会有一系列相应处理:如果指定了重复次数mRepeated,在没到指定次数之前会重置startTime,再走一边动画。并且如果动画需要反转(mRepeatMode == REVERSE)的话,比如第一次动画是从做走到右,那么下次动画要从右再回来到左,而不是再走一次从左到右,那么这时将设置标志位mCycleFlip,下次再计算normalizedTime的时候,会用1-normalizedTime,也就是如果走了百分之10-30的时间,那么会转换成走了百分之90-70,转换成距离的话就是从大到小,从动画效果上看就是从上一次的到达点一点点向原点往回走。

如果没有过期,或者repeatCount没有到指定数量,说明动画还在继续,那么getTransformation的返回值mMore将为true。

好了,这个方法已经完事,前言万语,实际上就是改变了parent.mChildTransformation的matrix属性而已。那么再回到上面drawAnimation方法,如果getTransformation返回的值为true,即more为true,说明动画还在执行阶段,并没有完事还需要下一次的计算和绘制,于是会调用parent.invalidate方法,也就是再往ViewRootImp的handler队列里放入一个消息。等本次绘制完成后,会再走一次这样的整个流程,于是随着动画相关参数的计算值不断改变、matrix不断变化、canvas绘制的内容也随之变化,这样就形成了动画效果。

继续往下走,再回到的View的draw(Canvas canvas, ViewGroup parent, long drawingTime)方法。

在调用了drawAnimation方法之后,parent.mChildTransformation已经有了设置好的值,这时将parent.mChildTransformation赋值给transformToApply,往下,将会走到这句:

 canvas.concat(transformToApply.getMatrix());

就是将刚刚动画计算过的matrix合并的当前canvas的matrix上。到这里,动画的处理基本完毕。后面还有一些对alpha属性,以及canvas的clip的处理,先跳过,稍后再讲。在将动画的matrix合入到canvas后, 下面就是view绘制的正常流程了。至此,Animation的实现流程基本完毕。

但有一个疑问:既然所有的view共用一个,并且动画计算后的matrix是合入cavas的,那怎么保证对指定view的动画操作只反映到这个view区域内,而不会干扰其他view区域?  我们回头去看draw(Canvas canvas, ViewGroup parent, long drawingTime) 方法。发现在动画合入canvas之前,分别对canvas做了save、translate、clip、restore方法,如果有alpha操作,还会新建一个layer层,这样就保证了所有的操作都局限在mTop、mLeft、mRight、mBottom这个范围内,也就是之前view经过了measure和layout后计算出的自身区域内。所以动画最终只反映到了指定view上。

四、总结

第三节的整个绘制流程大概可用下图表示:

细想一下上面的流程,其实我们发现,要实现系统动画主要是下面几个因素:

1. 驱动力: 通过不断的invalidate,不断走view的绘制流程。

2. 进度计算:在每次绘制时,都会通过对时间的计算,得出当前动画运行的进度是多少,也就是那个时间比例值,再将这个值用插入器处理一下,就可以有一个速度上的变化效果。

3. 不同形态的具体实现:有了当期的进度值,就可以换算出当前动画形态变化的程度。比如如果时间过了0.1,那么位移动画中的位置就应该移动到整个移动距离的0.1倍。

了解了以上流程,我们可以参考系统的实现方式,使用类似的算法、插入器机制,实现自己的动画。

从上面几个因素看,动画的显示效果和主线程对队列内消息处理的速度是息息相关的。因为每次invalidate实际上是向ViewRootImp里那个基于主线程的handler发消息,如果两个消息处理间隔过长,则相邻两次时间比例值会有较大差距(因为使用的是当前时间减去开始时间),这样具体动画,比如位移动画算出的相邻两个位置间的距离便会过大,这样便会出现卡顿的效果。

五、问题分析:

  1. 使用AlphaAnimation是view渐变消失后,为什么不能通过View.setAlpha()的方式显示回来?

在上面的draw(Canvas canvas, ViewGroup parent, long drawingTime) 方法中我们发现,透明效果是通过view在绘制过程中在canvas的layer栈中新建一层layer并指定layer的透明的实现,而透明值是setAlpha后的alpha值乘以AlphaAnimation计算后的matrix里的alpha值。也就是说,假设未设置过setAlpha,即alpha==1,然后做了个alphaAnimation动画,使透明度变为0.5, 那最终的透明度便是1 * 0.5;这时再通过setAlpha(1)是没有办法将不透明度变为1的。也就是最终的透明效果是这两个值共同作用的效果,只设置一个值是没用的。

  1. 为什么位移动画移除某一区域后,事件还保留在原处?

我们在使用TranslateAnimation时, 会发现当view移走之后, 点击原来的地方仍然可以触发事件。从上面的分析过程我们知道,动画实现方式改变view自身区域内canvas的矩阵参数,而不是view自身区域位置。也就是说变化的是画的内容,画板本身的形态位置没有变。而从android事件的传播机制我们看到,在父View向子View传递事件的过程中,这个子view是否响应事件的依据是,该事件是否在由mLeft,mTop,mRight, mBottom这四个值组成的view区域内发生的(参照ViewGroup的isTransformedTouchPointInView方法),在Animation动画过程中,很显然这四个参数没有变化过,也就是view区域没有变化,所以还会响应事件。

顺便提一下,从draw(Canvas canvas, ViewGroup parent, long drawingTime)方法里我们可以看到,  scrollTo的实现方式是通过canva.translate方式实现的,也就是同样没有改变mLeft,mTop,mRight, mBottom的值,所以通过scrollTo方式实现的位移在原位同样会相应事件。

暂时想到2个,了解动画的实现原理,类似的现象都可以得到解释。

就写到这里。如果本文分析过程中存在错误,还请指正,多谢。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值