Android 属性动画(Property Animation) 源码解析 深入了解其内部实现
1、概述
Android中想做很炫酷的动画效果,相信在很多时候你都可以选择使用属性动画,关于属性动画如何使用,我们已经很详细的写过两篇博客讲解。如果你还不了解,请参考:
Android 属性动画(Property Animation) 完全解析 (上)
Android 属性动画(Property Animation) 完全解析 (下)
本篇博客将分析属性动画的实现源码,带你深入的了解Android属性动画的内部实现机制。如果你经常用属性动画,但又一直没有去查看其源码实现,没关系,请往下看。
2、分析前的猜想
在源码分析之前,我们需要有一个明确的思路,例如:源码的入口的选择、甚至对其实现进行简单的猜测,源码分析相当于一个验证的过程,带着一个目标去看源码,这样的话,分析和理解起来更为方便。
对于实现属性动画,最常用的类就是ObjectAnimator了,只需要简单的设置目标view,属性,以及目标值等必要属性,调用一下start();我们的动画就完成了。
类似如下代码:
[java] view plain copy
- ObjectAnimator
- .ofInt(target,propName,values[])
- .setInterpolator(LinearInterpolator)
- .setEvaluator(IntEvaluator)
- .setDuration(500)
- .start();
上述代码很好理解吧,设置动画作用的view,作用的属性,动画开始、结束、以及中间的任意个属性值;
然后是设置插值器,当然了插值器这个词比较难理解,我要是说例如:AccelerateInterpolator、LinearInterpolator
然后设置估值算法,这个看名字挺高端,其实内部实现尤其简单: return (int)(startInt + fraction * (endValue - startInt)); 开始值,加上当前的属性改变的百分比*(结束-开始)
当然了,这个百分比是fraction ,其实就是上面的插值器算出来的。比如线性插值器:fraction值就是currentTime - mStartTime) / mDuration,动画的运行时间/总设置时间。
然后是设置动画事件,
最后start()。
好了,现在我想问个问题,根据上面这些参数,如果我要你设计个属性动画框架,你怎么做?
这个嘛,好整,拿到上述参数之后,start()中,开启一个定时器,去执行一个任务;在任务内部,根据Interpolator计算出来的fraction,交给Evaluator,得到属性当前应该设置的值,然后反射设置tagert的指定属性,ok,奏事这么简单。嗯,大体上应该就是这样,当然了,源码的实现肯定复杂很多,但是万变不离其宗,所以接下来的源码阅读,就是去验证我们的这个答案。
3、源码分析
好了,猜想完了,我们就得进入验证阶段了~~
那么,我们源码的入口就是上述代码了,不过貌似上述代码调用了好几个方法,but,我觉得start之前的代码,无法是初始化实例,设置一些成员变量。
首先我们看ofInt,这里为了简单,我们的ofInt中的values参数,默认就一个,类似.ofInt(view, "translationX", 300) ;
1、ofInt
[java] view plain copy
- publicstaticObjectAnimatorofInt(Objecttarget,StringpropertyName,int...values){
- ObjectAnimatoranim=newObjectAnimator(target,propertyName);
- anim.setIntValues(values);
- returnanim;
- }
首先调用ObjectAnimator的构造方法传入了一个target和propName,估计就是创建对象,然后旧路下target和propName,简单看下
[java] view plain copy
- privateObjectAnimator(Objecttarget,StringpropertyName){
- mTarget=target;
- setPropertyName(propertyName);
- }
- publicvoidsetPropertyName(StringpropertyName){
- //...
- mPropertyName=propertyName;
- mInitialized=false;
- }
记录完成target,propName以后,调用setIntValues
[java] view plain copy
- @Override
- publicvoidsetIntValues(int...values){
- setValues(PropertyValuesHolder.ofInt(mPropertyName,values));
- }
可以看到,把我们的propName,和values传入到了一个PropertyValuesHolder的ofInt方法中,去构造一个PropertyValuesHolder对象,这个对象是干什么的呢?
从字面上看,是保存view在动画期间的属性和值,记住是动画期间的。继续往下看:
[java] view plain copy
- publicstaticPropertyValuesHolderofInt(StringpropertyName,int...values){
- returnnewIntPropertyValuesHolder(propertyName,values);
- }
- publicIntPropertyValuesHolder(StringpropertyName,int...values){
- mPropertyName=propertyName;
- setIntValues(values);
- }
- @Override
- publicvoidsetIntValues(int...values){
- mValueType=int.class;
- mKeyframeSet=KeyframeSet.ofInt(values);
- mIntKeyframeSet=(IntKeyframeSet)mKeyframeSet;
- }
可以看到在IntPropertyValuesHolder内部存储了我们的propertyName;,然后又调用了setIntValues,存储了我们的mValueType ,此外还存了一个mIntKeyframeSet。
这里又出现一个新名词,叫做mKeyframeSet,这个是由KeyframeSet.ofInt(values);得到的。
那么这个KeyframeSet是什么呢?单纯的理解是,Keyframe的集合,而Keyframe叫做关键帧,为一个动画保存time/value(时间与值)对。
那么我们去看看它是如何通过KeyframeSet.ofInt(values);去构造与保存的:
[java] view plain copy
- publicstaticKeyframeSetofInt(int...values){
- intnumKeyframes=values.length;
- IntKeyframekeyframes[]=newIntKeyframe[Math.max(numKeyframes,2)];
- if(numKeyframes==1){
- keyframes[0]=(IntKeyframe)Keyframe.ofInt(0f);
- keyframes[1]=(IntKeyframe)Keyframe.ofInt(1f,values[0]);
- }else{
- //...
- }
- returnnewIntKeyframeSet(keyframes);
- }
- publicIntKeyframeSet(IntKeyframe...keyframes){
- mNumKeyframes=keyframes.length;
- mKeyframes=newArrayList<Keyframe>();
- mKeyframes.addAll(Arrays.asList(keyframes));
- mFirstKeyframe=mKeyframes.get(0);
- mLastKeyframe=mKeyframes.get(mNumKeyframes-1);
- mInterpolator=mLastKeyframe.getInterpolator();
- }
这里代码跳跃比较大,部分代码我来解释:
根据我们的values的长度,构造了keyframes数组,然后分别通过Keyframe的ofInt方法,去构造keyframe对象,其实在内部:
[java] view plain copy
- IntKeyframe(floatfraction,intvalue){
- mFraction=fraction;
- mValue=value;
- mValueType=int.class;
- mHasValue=true;
- }
- IntKeyframe(floatfraction){
- mFraction=fraction;
- mValueType=int.class;
- }
就简单存了一下fraction,和value;当然了,我们这里values只有一个值,所以构造了两个Keyframe。
拿到初始化完成的keyframes数组以后,将其传入了KeyframeSet的构造方法,初始化了KeyframeSet内部的一些成员变量。
[java] view plain copy
- publicIntKeyframeSet(IntKeyframe...keyframes){
- mNumKeyframes=keyframes.length;
- mKeyframes=newArrayList<Keyframe>();
- mKeyframes.addAll(Arrays.asList(keyframes));
- mFirstKeyframe=mKeyframes.get(0);
- mLastKeyframe=mKeyframes.get(mNumKeyframes-1);
- mInterpolator=mLastKeyframe.getInterpolator();
- }
存了有多少关键帧,开始帧,结束帧,以及插值器。
到此,我们的(PropertyValuesHolder.ofInt在彻底返回,可以看到这个过程中,我们成功的为PropertyValuesHolder对象赋值了propName,valueType,keyframeSet .
keyframeset中存了Keyframe集合,keyframe中存储了(fraction , valuetype , value , hasValue)。
最后,叫PropertyValuesHolder 交给我们的 ObjectAnimator的setValues方法。
[java] view plain copy
- publicvoidsetValues(PropertyValuesHolder...values){
- intnumValues=values.length;
- mValues=values;
- mValuesMap=newHashMap<String,PropertyValuesHolder>(numValues);
- for(inti=0;i<numValues;++i){
- PropertyValuesHoldervaluesHolder=values[i];
- mValuesMap.put(valuesHolder.getPropertyName(),valuesHolder);
- }
- //Newproperty/values/targetshouldcausere-initializationpriortostarting
- mInitialized=false;
- }
好了,到此我们的ofInt结束了,晕否,其实还好。如果你晕了,我帮你总结下:ofInt就是记录了target,propName,values(是将我们传入的int型values,辗转转化成了PropertyValuesHolder),以及一个mValueMap,这个map的key是propName,value是PropertyValuesHolder,在PropertyValuesHolder内部又存储了proprName, valueType , keyframeSet等等。
好了,接下来会轻松点,按照顺序到setInterpolator了:
2、setInterpolator
[java] view plain copy
- @Override
- publicvoidsetInterpolator(TimeInterpolatorvalue){
- if(value!=null){
- mInterpolator=value;
- }else{
- mInterpolator=newLinearInterpolator();
- }
- }
没撒说的,记录下插值器,我们这里也线性插值器,默认也是~~
然后是setEvaluator。
3、setEvaluator
[java] view plain copy
- publicvoidsetEvaluator(TypeEvaluatorvalue){
- if(value!=null&&mValues!=null&&mValues.length>0){
- mValues[0].setEvaluator(value);
- }
- }
记得我们这里的mValue吧,在ofInt里面初始化的,类型是PropertyValuesHolder。然后调用了PropertyValuesHolder.setEvalutor
[java] view plain copy
- publicvoidsetEvaluator(TypeEvaluatorevaluator){
- mEvaluator=evaluator;
- mKeyframeSet.setEvaluator(evaluator);
- }
记录了一下估值算法,然后再将其传给KeyframeSet对象:
[java] view plain copy
- publicvoidsetEvaluator(TypeEvaluatorevaluator){
- mEvaluator=evaluator;
- }
可以看到,我们把估值算法,交给了PropertyValuesHolder以及KeyframeSet。
接下来,最后一个属性,duration
4、setDuration
[java] view plain copy
- //Howlongtheanimationshouldlastinms
- privatelongmDuration=(long)(300*sDurationScale);
- privatelongmUnscaledDuration=300;
- privatestaticfloatsDurationScale=1.0f;
- publicObjectAnimatorsetDuration(longduration){
- if(duration<0){
- thrownewIllegalArgumentException("Animatorscannothavenegativeduration:"+
- duration);
- }
- mUnscaledDuration=duration;
- mDuration=(long)(duration*sDurationScale);
- returnthis;
- }
就是简单在mDuration中记录了一下动画的持续时间,这个sDurationScale默认为1,貌似是用于调整,观察动画的,比如你可以调整为10,动画就会慢10倍的播放。
好了,到此该设置的设置完成了,小小总结一下:
ofInt中实例化了一个ObjectAnimator对象,然后设置了target,propName,values(PropertyValuesHolder) ;然后分别在setInterpolator,setDuration设置了Interpolator和duration。其中setEvaluator是给values[0],以及keyframeSet设置估值算法。
PropertyValueHolder实际上是IntPropertyValueHolder类型对象,包含propName,valueType,keyframeSet .
keyframeset中存了Keyframe集合,keyframe中存储了(fraction , valuetype , value , hasValue)。
以上都比较简单,关键就是看start()方法中,如何将这些属性进行合理的处理调用神马的。
5、start
喝杯水,小憩一下,准备征战start()方法。
[java] view plain copy
- @Override
- publicvoidstart(){
- super.start();
- }
- ValueAnimator
- @Override
- publicvoidstart(){
- start(false);
- }
- ValueAnimator
- privatevoidstart(booleanplayBackwards){
- if(Looper.myLooper()==null){
- thrownewAndroidRuntimeException("AnimatorsmayonlyberunonLooperthreads");
- }
- mPlayingBackwards=playBackwards;
- mCurrentIteration=0;
- mPlayingState=STOPPED;
- mStarted=true;
- mStartedDelay=false;
- mPaused=false;
- AnimationHandleranimationHandler=getOrCreateAnimationHandler();
- animationHandler.mPendingAnimations.add(this);
- if(mStartDelay==0){
- //Thissetstheinitialvalueoftheanimation,priortoactuallystartingitrunning
- setCurrentPlayTime(0);
- mPlayingState=STOPPED;
- mRunning=true;
- notifyStartListeners();
- }
- animationHandler.start();
- }
15-20行:设置了关于动画的一些标志位,mPlayingBackwards 表示动画是否reverse;mCurrentIteration 记录当前的动画的执行次数(与setRepeatCount有关);mPlayingState 动画的状态为STOPPED;还有些其他的标志位;
21行:生成一个AnimationHandler对象,getOrCreateAnimationHandler就是在当前线程变量ThreadLocal中取出来,没有的话,则创建一个,然后set进去。
AnimationHandler中包含一些List集合用于存储各种状态的ValueAnimator。
22行:将当前ValueAnimator对象,加入 animationHandler.mPendingAnimations 集合。
23行:未设置mStartDelay,默认为0,则进入循环;
24行: setCurrentPlayTime(0);一会需要细说
25-26行:设置些状态。
27行:回调监听动画的接口AnimatorListener的onAnimationStart方法,如果你设置了回调监听,此时就会进行回调;
最后30行:调用animationHandler.start();需要细说;
好了,有两个方法需要细说,首先看setCurrentPlayTime(0)
[java] view plain copy
- publicvoidsetCurrentPlayTime(longplayTime){
- initAnimation();
- longcurrentTime=AnimationUtils.currentAnimationTimeMillis();
- if(mPlayingState!=RUNNING){
- mSeekTime=playTime;
- mPlayingState=SEEKED;
- }
- mStartTime=currentTime-playTime;
- doAnimationFrame(currentTime);
- }
首先初始化动画,然后得到当前的系统开始到现在的时间currentTime;设置mSeekTime,设置当前状态为SEEKED;然后使用mSeekTime-playTime得到动画现在需要执行的时间;最后调用doAnimationFrame(currentTime),稍后看其代码;
关于initAnimation(),实际就是去设置我们ValueAnimator中存储的mValues,也就是IntPropertyValueHolder的mEvaluator;
[java] view plain copy- voidinitAnimation(){
- if(!mInitialized){
- intnumValues=mValues.length;
- for(inti=0;i<numValues;++i){
- mValues[i].init();
- }
- mInitialized=true;
- }
PropertyValuesHolder的init方法:
[java] view plain copy- voidinit(){
- if(mEvaluator==null){
- //Wealreadyhandleintandfloatautomatically,butnottheirObject
- //equivalents
- mEvaluator=(mValueType==Integer.class)?sIntEvaluator:
- (mValueType==Float.class)?sFloatEvaluator:
- null;
- }
- if(mEvaluator!=null){
- //KeyframeSetknowshowtoevaluatethecommontypes-onlygiveitacustom
- //evaluatorifonehasbeensetonthisclass
- mKeyframeSet.setEvaluator(mEvaluator);
- }
- }
接下来应该看doAnimationFrame(currentTime);了
[java] view plain copy
- finalbooleandoAnimationFrame(longframeTime){
- finallongcurrentTime=Math.max(frameTime,mStartTime);
- returnanimationFrame(currentTime);
- }
内部调用了:animationFrame(currentTime);
[java] view plain copy
- booleananimationFrame(longcurrentTime){
- booleandone=false;
- switch(mPlayingState){
- caseRUNNING:
- caseSEEKED:
- floatfraction=mDuration>0?(float)(currentTime-mStartTime)/mDuration:1f;
- if(fraction>=1f){
- //...
- }
- if(mPlayingBackwards){
- fraction=1f-fraction;
- }
- animateValue(fraction);
- break;
- }
- returndone;
- }
这里通过判断当前动画的状态,给出fraction,默认传入的就是(float)(currentTime - mStartTime) / mDuration,动画执行的时间除以总的时间比值;
接下来调用了animateValue(fraction)
在animateValue的内部,会将传入的fraction,交给mInterpolator.getInterpolation(fraction);方法,获得插值器处理后的fraction;然后在将fraction交给估值算法mEvaluator.evaluate(fraction, firstValue, lastValue)).intValue();进行计算得到当前时间点,属性应该的值;最后会反射对我们设置的属性进行设置。
终于看到,对我们的属性的值进行设置了,偶也~~当然了,动画如果没结束,应该每隔一定的帧数,再次调用,嗯,的确是这样的,你看到animationFrame最后是不是有个返回值,这个值会在fraction>=1的时候返回true;
我们还是先看看animateValue方法:
[java] view plain copy
- voidanimateValue(floatfraction){
- fraction=mInterpolator.getInterpolation(fraction);
- mCurrentFraction=fraction;
- intnumValues=mValues.length;
- for(inti=0;i<numValues;++i){
- mValues[i].calculateValue(fraction);
- }
- if(mUpdateListeners!=null){
- intnumListeners=mUpdateListeners.size();
- for(inti=0;i<numListeners;++i){
- mUpdateListeners.get(i).onAnimationUpdate(this);
- }
- }
- intnumValues=mValues.length;
- for(inti=0;i<numValues;++i){
- mValues[i].setAnimatedValue(mTarget);
- }
- }
首先将fraction交给给mInterpolator.getInterpolation(fraction);得到计算后的fraction;
然后for循环遍历调用IntPropertyValueHolder的calculateValue方法:
[java] view plain copy
- voidcalculateValue(floatfraction){
- mAnimatedValue=mKeyframeSet.getValue(fraction);
- }
在其内部,调用了mKeyframeSet的getValue,这里注意我们的IntKeyFrameSet,千万不要看错方法了。
[java] view plain copy
- @Override
- publicObjectgetValue(floatfraction){
- returngetIntValue(fraction);
- }
- publicintgetIntValue(floatfraction){
- if(mNumKeyframes==2){
- if(firstTime){
- firstTime=false;
- firstValue=((IntKeyframe)mKeyframes.get(0)).getIntValue();
- lastValue=((IntKeyframe)mKeyframes.get(1)).getIntValue();
- deltaValue=lastValue-firstValue;
- }
- if(mInterpolator!=null){
- fraction=mInterpolator.getInterpolation(fraction);
- }
- if(mEvaluator==null){
- returnfirstValue+(int)(fraction*deltaValue);
- }else{
- return((Number)mEvaluator.evaluate(fraction,firstValue,lastValue)).intValue();
- }
- }
- //...省略了很多代码
- }
在其内部,因为我们只设置了一个目标属性值,所以只有两个关键帧;
然后16-20行,调用估值算法的mEvaluator.evaluate方法,可以看到如果mEvaluator == null直接调用了firstValue + (int)(fraction * deltaValue);其实这个就是IntEvaluator的默认实现。
好了,for循环结束了,经过我们插值器和估值算法得出的值,最终给了IntPropertyValueHolder的mIntAnimatedValue属性;
回到animateValue方法:在animateValue的8-12行,继续回调动画监听onAnimationUpdate(this);方法;
animateValue的15-18行:循环拿到(其实我们就只有一个属性)我们的IntPropertyValueHolder调用setAnimatedValue,进行反射为我们的属性设置值,反射需要一些东西,比如target,propname,以及该属性应该设置的值;这三个参数在哪呢?target作为参数传入了,propName初始化的时候就设置了,至于该属性应该设置的值,上面有一句:“ 好了,for循环结束了,经过我们插值器和估值算法得出的值,最终给了IntPropertyValueHolder的mIntAnimatedValue属性” 。是不是全了~~反射的代码就不贴了。
好了,到此,我们属性动画,设置的各种值,经过重重的计算作用到了我们的属性上,反射修改了我们的属性。到此我们已经完成了一大半,但是貌似还少了个,每隔多少帧调用一次~~
嗯,的确是的,跨度好大,现在回到我们的start方法,最后一行:调用animationHandler.start();这个还没细说呢~~
animationHandler我们上面已经介绍了,存储在当前线程的ThreadLocal里面,里面放了一些集合用于存储各种状态的ObjectAnimator,我们当前的ObjectAnimator对象也存储在其mPendingAnimations的集合中(上面提到过~~)。
[java] view plain copy
- /**
- *Startanimatingonthenextframe.
- */
- publicvoidstart(){
- scheduleAnimation();
- }
- privatevoidscheduleAnimation(){
- if(!mAnimationScheduled){
- mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION,this,null);
- mAnimationScheduled=true;
- }
- }
start内部最终调用了mChoreographer.postCallback,其中有一个参数是this;至于什么是Choreographer,暂时不用管;但是你需要知道一件事,其实我们的animationHandler是Runnable的子类,而mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);类似与handler发送消息,最终执行这个Runnable的run方法。
说这么多,其实就是一句话,这里调用了animationHandler的 run方法。
[java] view plain copy
- publicvoidrun(){
- mAnimationScheduled=false;
- doAnimationFrame(mChoreographer.getFrameTime());
- }
- privatevoiddoAnimationFrame(longframeTime){
- while(mPendingAnimations.size()>0){
- ArrayList<ValueAnimator>pendingCopy=
- (ArrayList<ValueAnimator>)mPendingAnimations.clone();
- mPendingAnimations.clear();
- intcount=pendingCopy.size();
- for(inti=0;i<count;++i){
- ValueAnimatoranim=pendingCopy.get(i);
- //IftheanimationhasastartDelay,placeitonthedelayedlist
- if(anim.mStartDelay==0){
- anim.startAnimation(this);
- }else{
- mDelayedAnims.add(anim);
- }
- }
- }
- //...省略了一些代码
- //Nowprocessallactiveanimations.ThereturnvaluefromanimationFrame()
- //tellsthehandlerwhetheritshouldnowbeended
- intnumAnims=mAnimations.size();
- for(inti=0;i<numAnims;++i){
- mTmpAnimations.add(mAnimations.get(i));
- }
- for(inti=0;i<numAnims;++i){
- ValueAnimatoranim=mTmpAnimations.get(i);
- if(mAnimations.contains(anim)&&anim.doAnimationFrame(frameTime)){
- mEndingAnims.add(anim);
- }
- }
- mTmpAnimations.clear();
- if(mEndingAnims.size()>0){
- for(inti=0;i<mEndingAnims.size();++i){
- mEndingAnims.get(i).endAnimation(this);
- }
- mEndingAnims.clear();
- }
- //Iftherearestillactiveordelayedanimations,scheduleafuturecallto
- //onAnimatetoprocessthenextframeoftheanimations.
- if(!mAnimations.isEmpty()||!mDelayedAnims.isEmpty()){
- scheduleAnimation();
- }
- }
6-20行:while循环,遍历所有在mPendingAnimations中的ObjectAnimator,依次调用anim.startAnimation(this);
在anim.startAnimation(this);内部其实主要就一行代码:handler.mAnimations.add(this); 将当前动画加入animationHandler的mAnimations集合;
26-29行:将animationHandler的mAnimations集合中的每个anim,加入到mTmpAnimations中;
30-35行:依次调用mTmpAnimations中的anim,anim.doAnimationFrame(frameTime)
doAnimationFrame(frameTime)上面已经分析过了,如果返回true,即doAnimationFrame的done为true,则将该动画加入到结束动画集合。
37-43行:循环调用mEndingAnims,mEndingAnims.get(i).endAnimation(this);内部,会将动画移除mAnimations,回调动画监听接口onAnimationEnd;以及重置各种标志变量。 46-48行:如果mAnimations不为null,则再次调用scheduleAnimation(); 哈哈,终于终于发现了,每隔多少帧调用一次动画的地方了~~尼玛这个scheduleAnimation,不就是animationHandler的 run方法调用的么~~前面已经描述过animationHandler的 run方法中通过计算属性应该的值,反射设置;加上我们这里的动画没结束,就会再次调用该run方法内部一致的方法~~~
搜噶,到此~~我们的属性动画的流程已经完美跑通了~~~
对了,看完以后,和我们文章开始的预期符合么,其实我觉得差不多~~
4、总结
其实看源码的目的,最终就是为了总结,尼玛这么长的代码谁也记不住。。。所以看完记得总结:
ofInt中实例化了一个ObjectAnimator对象,然后设置了target,propName,values(PropertyValuesHolder) ;然后分别在setInterpolator,setDuration设置了Interpolator
和duration。其中setEvaluator是给PropertyValuesHolder,以及keyframeSet设置估值算法。
PropertyValueHolder实际上是IntPropertyValueHolder类型对象,包含propName,valueType,keyframeSet .
keyframeset中存了Keyframe集合,keyframe中存储了(fraction , valuetype , value , hasValue)。
上述其实都是设置各种值什么的。真正核心要看start~
start()中:
首先,步骤1:更新动画各种状态,然后初步计算fraction为(currentTime - mStartTime) / mDuration;然后将这个fraction交给我们的插值器计算后得到新的fraction,再将新的fraction交给我们的估值算法,估值算法根据开始、结束、fraction得到当前属性(动画作用的属性)应该的值,最大调用反射进行设置;
当然了:start中还会根据动画的状态,如果没有结束,不断的调用scheduleAnimation();该方法内部利用mChoreographer不断的去重复我们的上述步骤1。
好了,顺便说一句,在看源码的时候,一定要注意,你点进去的有可能不是真正运行时调用的,记得查看该方法子类,比如我们查看ObjectAnimator的方法,可能我们某个方法会跟到其父类ValueAnimator的方法,但是记得查看ObjectAnimator是否复写了该方法~~如果复写了,你该看的应该是ObjectAnimator的方法~~~
源码,嗯?木有源码点击下载了~~~
更多相关文章
- android textview xml 属性设置
- android的一些属性
- android RelativeLayout属性和使用, 实现上面view叠加在下面view
- android RelativeLayout属性及demo
- Android的Activity屏幕切换动画||GestureDetector、OnGestureLis
- RelativeLayout属性和使用, 实现上面view叠加在下面view之上的效
- 用fastboot大刷Android ~换个方法刷android手机
- Android多进程总结一:生成多进程(android:process属性)