目录介绍
- 1.Animation和Animator区别
-
2.Animation运行原理和源码分析
- 2.1 基本属性介绍
- 2.2 如何计算动画数据
- 2.3 什么是动画更新函数
- 2.4 动画数据如何存储
- 2.5 Animation的调用
-
3.Animator运行原理和源码分析
- 3.1 属性动画的基本属性
- 3.2 属性动画新的概念
- 3.3 PropertyValuesHolder作用
- 3.4 属性动画start执行流程
- 3.5 属性动画cancel和end执行流程
- 3.6 属性动画pase和resume执行流程
- 3.7 属性动画与View结合
好消息
- 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计47篇[近20万字],转载请注明出处,谢谢!
- 链接地址:https://github.com/yangchong2...
- 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!
- [01.动画机制总结]()
- 02.动画源码解析
1.Animation和Animator区别
-
对于 Animation 动画:
- 实现机制是,在每次进行绘图的时候,通过对整块画布的矩阵进行变换,从而实现一种视图坐标的移动,但实际上其在 View内部真实的坐标位置及其他相关属性始终恒定.
-
对于 Animator 动画:
- Animator动画的实现机制说起来其实更加简单一点,因为他其实只是计算动画开启之后,结束之前,到某个时间点得时候,某个属性应该有的值,然后通过回调接口去设置具体值,其实 Animator 内部并没有针对某个 view 进行刷新,来实现动画的行为,动画的实现是在设置具体值的时候,方法内部自行调取的类似 invalidate 之类的方法实现的.也就是说,使用 Animator ,内部的属性发生了变化
-
或者更简单一点说
- 前者属性动画,改变控件属性,(比如平移以后点击有事件触发)
- 后者补间动画,只产生动画效果(平移之后点无事件触发,前提是你fillafter=true)
2.Animation运行原理和源码分析
2.1 基本属性介绍
-
上一篇文章已经对补间动画做了详细的说明,不过这里还是需要重复说一下动画属性的作用
- mStartTime:动画实际开始时间
- mStartOffset:动画延迟时间
- mFillEnabled:mFillBefore及mFillAfter是否使能
- mFillBefore:动画结束之后是否需要进行应用动画
- mFillAfter:动画开始之前是否需要进行应用动画
- mDuration:单次动画运行时长
- mRepeatMode:动画重复模式(RESTART、REVERSE)
- mRepeatCount:动画重复次数(INFINITE,直接值)
- mInterceptor:动画插间器
- mBackgroundColor:动画背景颜色
- mListener:动画开始、结束、重复回调监听器
2.2 如何计算动画数据
-
首先进入Animation类,然后找到getTransformation方法,主要是分析这个方法逻辑,如图所示
- 那么这个方法中做了什么呢?Animation在其getTransformation函数被调用时会计算一帧动画数据,而上面这些属性基本都是在计算动画数据时有相关的作用。
- 第一步:若startTime为START_ON_FIRST_FRAME(值为-1)时,将startTime设定为curTime
-
第二步:计算当前动画进度:
- normalizedTime = (curTime - (startTime + startOffset))/duration
- 若mFillEnabled==false:将normalisedTime夹逼至[0.0f, 1.0f]
-
第三步:判断是否需要计算动画数据:
- 若normalisedTime在[0.0f, 1.0f],需计算动画数据
-
若normalisedTime不在[0.0f, 1.0f]:
- normalisedTime<0.0f, 仅当mFillBefore==true时才计算动画数据
- normalisedTime>1.0f, 仅当mFillAfter==true时才计算动画数据
-
第四步:若需需要计算动画数据:
- 若当前为第一帧动画,触发mListener.onAnimationStart
- 若mFillEnabled==false:将normalisedTime夹逼至[0.0f, 1.0f]
- 根据插间器mInterpolator调整动画进度:
- interpolatedTime = mInterpolator.getInterpolation(normalizedTime)
- 若动画反转标志位mCycleFlip为true,则
- interpolatedTime = 1.0 - normalizedTime
- 调用动画更新函数applyTransformation(interpolatedTime, transformation)计算出动画数据
-
第五步:若夹逼之前normalisedTime大于1.0f, 则判断是否需继续执行动画:
-
已执行次数mRepeatCount等于需执行次数mRepeated
- 若未触发mListener.onAnimationEnd,则触发之
-
已执行次数mRepeatCount不等于需执行次数mRepeated
- 自增mRepeatCount
- 重置mStartTime为-1
- 若mRepeatMode为REVERSE,则取反mCycleFlip
- 触发mListener.onAnimationRepeat
-
2.3 什么是动画更新函数
-
下面我们来看一下getTransformation方法中的这一行代码applyTransformation(interpolatedTime, outTransformation),然后进去看看这个方法。如下所示
- 这个方法的用途是干啥呢?从这个英文解释中可以得知:getTransform的助手。子类应该实现这一点,以应用给定的内插值来应用它们的转换。该方法的实现应该总是替换指定的转换或文档,而不是这样做的。
-
都知道Animation是个抽象类,接着我们这些逗比程序员可以看看它的某一个子类,比如看看ScaleAnimation中的applyTransformation方法吧。
-
是否设定缩放中心点:
- 若mPivotX==0 且 mPivotY==0:transformation.getMatrix().setScale(sx, sy)
- 否则:transformation.getMatrix().setScale(sx, sy, mPivotX, mPivotY)
-
- 介绍到这里还是没有讲明白它的具体作用,它是在什么情况下调用的。不要着急,接下来会慢慢分析的……
2.4 动画数据如何存储
-
可以看到applyTransformation(float interpolatedTime, Transformation t)这个方法中带有一个Transformation参数,那么这个参数是干啥呢?
-
实际上,Animation的动画函数getTransformation目的在于生成当前帧的一个Transformation,这个Transformation采用alpha以及Matrix存储了一帧动画的数据,Transformation包含两种模式:
- alpha模式:用于支持透明度动画
- matrix模式:用于支持缩放、平移以及旋转动画
-
同时,Transformation还提供了许多两个接口用于组合多个Transformation:
- compose:前结合(alpha相乘、矩阵右乘、边界叠加)
- postCompose:后结合(alpha相乘、矩阵左乘、边界叠加
-
2.5 Animation的调用
-
getTransformation这个函数究竟是在哪里调用的?计算得到的动画数据又是怎么被应用的?为什么Animation这个包要放在android.view下面以及Animation完成之后为什么View本身的属性不会被改变。慢慢看……
- 要了解Animation,先从要从Animation的基本使用View.startAnimation开始寻根溯源:如下所示
-
接着看看setStartTime这个方法,主要是设置一些属性。
-
接着看看setAnimation(animation)方法源码
- 设置要为此视图播放的下一个动画。如果希望动画立即播放,请使用{@link#startAnimation(android.view.animation.Animation)}代替此方法,该方法允许对启动时间和无效时间进行细粒度控制,但必须确保动画具有启动时间集,并且当动画应该启动时,视图的父视图(控制子视图上的动画)将失效。
public void setAnimation(Animation animation) {mCurrentAnimation = animation;if (animation != null) {if (mAttachInfo != null && mAttachInfo.mDisplayState == Display.STATE_OFF&& animation.getStartTime() == Animation.START_ON_FIRST_FRAME) {animation.setStartTime(AnimationUtils.currentAnimationTimeMillis());}animation.reset();} }
-
接着重点看一下invalidate(true)这个方法
- 通过invalidate(true)函数会触发View的重新绘制,那么在View.draw是怎么走到对Animation的处理函数呢?
View.draw(Canvas) —> ViewGroup.dispatchDraw(Canvas) —> ViewGroup.drawChild(Canvas, View, long) —> View.draw(Canvas, ViewGroup, long) —> View.applyLegacyAnimation(ViewGroup, long, Animation, boolean)
-
接着看看View中applyLegacyAnimation这个方法
private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,Animation a, boolean scalingRequired) {Transformation invalidationTransform;final int flags = parent.mGroupFlags;//判断Animation是否初始化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);//由父视图组调用,通知当前与此视图关联的动画的开始。如果重写此方法,则始终调用Super.on动画Start();onAnimationStart();}//获取Transformation对象final Transformation t = parent.getChildTransformation();//获取要在指定时间点应用的转换,这个方法最终调用了Animation中的getTransformation方法//调用getTransformation根据当前绘制事件生成Animation中对应帧的动画数据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) {parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;//调用ViewGroup.invalidate(int l, int t, int r, int b)设定绘制区域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);parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;final int left = mLeft + (int) region.left;final int top = mTop + (int) region.top;//调用ViewGroup.invalidate(int l, int t, int r, int b)设定绘制区域parent.invalidate(left, top, left + (int) (region.width() + .5f),top + (int) (region.height() + .5f));}}return more; }
-
View.applyLegacyAnimation就是Animation大显神通的舞台,其核心代码主要分三个部分
-
初始化Animation(仅初始化一次)
- 调用Animation.initialize(width, height, parentWidth, parentHeight),通过View及ParentView的Size来解析Animation中的相关数据;
- 调用Animation.initializeInvalidateRegion(left, top, right, bottom)来设定动画的初始区域,并在fillBefore为true时计算Animation动画进度为0.0f的数据
- 调用getTransformation根据当前绘制事件生成Animation中对应帧的动画数据
-
根据动画数据设定重绘制区域
- 若仅为Alpha动画,此时动画区域为View的当前区域,且不会产生变化
- 若包含非Alpha动画,此时动画区域需要调用Animation.getInvalidateRegion进行计算,该函数会根据上述生成动画数据Thransformation中的Matrix进行计算,并与之前的动画区域执行unio操作,从而获取动画的完整区域
- 调用ViewGroup.invalidate(int l, int t, int r, int b)设定绘制区域
-
-
-
当View.applyLegacyAnimation调用完成之后,View此次绘制的动画数据就构建完成,之后便回到View.draw(Canvas, ViewGroup, long)应用动画数据对视图进行绘制刷新,如下所示:
- 重点看到Animation产生的动画数据实际并不是应用在View本身的,而是应用在RenderNode或者Canvas上的,这就是为什么Animation不会改变View的属性的根本所在。另一方面,我们知道Animation仅在View被绘制的时候才能发挥自己的价值,这也是为什么插间动画被放在Android.view包内。
3.Animator运行原理和源码分析
3.1 属性动画的基本属性
-
属性动画跟补间动画一样会包含动画相关的属性,如动画时长、动画播放次数、延迟时间、插间器等等,为了后面分析动画运行流程时概念更加明确,这里仅仅写了部分ValueAnimator源码中的字段,并做了相应的注解
// 初始化函数是否被调用 boolean mInitialized = false; // 动画时长 private long mDuration = (long)(300 * sDurationScale); private long mUnscaledDuration = 300; // 动画延时 private long mStartDelay = 0; private long mUnscaledStartDelay = 0; // 动画重复模式及次数 private int mRepeatCount = 0; private int mRepeatMode = RESTART; // 插间器 private TimeInterpolator mInterpolator = sDefaultInterpolator; // 动画开始运行的时间点 long mStartTime; // 是否需要在掉帧的时候调整动画开始时间点 boolean mStartTimeCommitted; // 动画是否反方向运行,当repeatMode=REVERSE是会每个动画周期反转一次 private boolean mPlayingBackwards = false; // 当前动画在一个动画周期中所处位置 private float mCurrentFraction = 0f; // 动画是否延时 private boolean mStartedDelay = false; // 动画完成延时的时间点 private long mDelayStartTime; // 动画当前所处的状态:STOPPED, RUNNING, SEEKED int mPlayingState = STOPPED; // 动画是否被启动 private boolean mStarted = false; // 动画是否被执行(以动画第一帧被计算为界) private boolean mRunning = false; // 回调监听器 // 确保AnimatorListener.onAnimationStart(Animator)仅被调用一次 private boolean mStartListenersCalled = false; // start,end,cancel,repeat回调 ArrayList<AnimatorListener> mListeners = null; // pause, resume回调 ArrayList<AnimatorPauseListener> mPauseListeners = null; // value更新回调 ArrayList<AnimatorUpdateListener> mUpdateListeners = null;
3.2 属性动画新的概念
-
属性动画相对于插间动画来件引入了一些新的概念
- 可以暂停和恢复、可以调整进度,这些概念的引入,让动画的概念更加饱满起来,让动画有了视频播放的概念,主要有:
// 动画是否正在running private boolean mRunning = false; // 动画是否被开始 private boolean mStarted = false; // 动画是否被暂停 boolean mPaused = false; // 动画暂停时间点,用于在动画被恢复的时候调整mStartTime以确保动画能优雅地继续运行 private long mPauseTime; // 动画是否从暂停中被恢复,用于表明动画可以调整mStartTime private boolean mResumed = false; // 动画被设定的进度位置 float mSeekFraction = -1;
3.3 PropertyValuesHolder作用
-
PropertyValuesHolder是用来保存某个属性property对应的一组值,这些值对应了一个动画周期中的所有关键帧。
- 动画说到底是由动画帧组成的,将动画帧连续起来就成了动画呢。
- Animator可以设定并保存整个动画周期中的关键帧,然后根据这些关键帧计算出动画周期中任一时间点对应的动画帧的动画数据
- 而每一帧的动画数据里都包含了一个时间点属性fraction以及一个动画值mValue,从而实现根据当前的时间点计算当前的动画值,然后用这个动画值去更新property对应的属性
- Animator被称为属性动画的原因,因为它的整个动画过程实际上就是不断计算并更新对象的属性这个后面详细讲解。
-
那么保存property使用什么存储的呢?看代码可知:数组
-
PropertyValuesHolder由Property及Keyframes组成,其中Property用于描述属性的特征:如属性名以及属性类型,并提供set及get方法用于获取及设定给定Target的对应属性值;Keyframes由一组关键帧Keyframe组成,每一个关键帧由fraction及value来定量描述,于是Keyframes可以根据给定的fraction定位到两个关键帧,这两个关键帧的fraction组成的区间包含给定的fraction,然后根据定位到的两个关键帧以及设定插间器及求值器就可以计算出给定fraction对应的value。
-
PropertyValuesHolder的整个工作流程
- 首先通过setObjectValues等函数来初始化关键帧组mKeyframes,必要的情况下(如ObjectAnimator)可以通过setStartValue及setEndValue来设定第一帧及最末帧的value,以上工作只是完成了PropertyValuesHolder的初始化,
- 之后就可以由Animator在绘制动画帧的时候通过fraction来调用calculateValue计算该fraction对应的value(实际上是由mKeyframes的getValue方法做出最终计算),获得对应的value之后,一方面可以通过getAnimatedValue提供给Animator使用,
- 另一方面也可以通过setAnimatedValue方法直接将该值设定到相应Target中去,这样PropertyValuesHolder的职责也就完成呢。
-
3.4 属性动画start执行流程
-
首先看看start方法,默认是false,这个参数是干嘛的呢?这个参数是动画是否应该开始反向播放。
- 启动动画播放。这个版本的start()使用一个布尔标志,指示动画是否应该反向播放。该标志通常为false,但如果从反向()方法调用,则可以将其设置为true。通过调用此方法启动的动画将在调用此方法的线程上运行。这个线程应该有一个活套(如果不是这样的话,将抛出一个运行时异常)。另外,如果动画将动画化视图层次结构中对象的属性,那么调用线程应该是该视图层次结构的UI线程。
@Override public void start() {start(false); }private void start(boolean playBackwards) {if (Looper.myLooper() == null) {throw new AndroidRuntimeException("Animators may only be run on Looper threads");}mReversing = playBackwards;mSelfPulse = !mSuppressSelfPulseRequested;if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {if (mRepeatCount == INFINITE) {float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));mSeekFraction = 1 - fraction;} else {mSeekFraction = 1 + mRepeatCount - mSeekFraction;}}mStarted = true;mPaused = false;mRunning = false;mAnimationEndRequested = false;mLastFrameTime = -1;mFirstFrameTime = -1;mStartTime = -1;addAnimationCallback(0);if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {startAnimation();if (mSeekFraction == -1) {setCurrentPlayTime(0);} else {setCurrentFraction(mSeekFraction);}} }
-
然后接着看addAnimationCallback(0)这行代码,从字面意思理解是添加动画回调callback
- 可以看到通过getAnimationHandler()创建了一个AnimationHandler对象。
- 然后在看看addAnimationFrameCallback()这个方法,看命名应该是专门处理动画相关的。实际上里面的逻辑大概是:通过Choreographer向底层注册下一个屏幕刷新信号监听,然后将需要运行的动画添加到列表中,如果延迟时间大于0,则说明动画是一个延迟开始的动画,那么加入Delay队列里。
- 然后看看动画是用什么存储的呢?mAnimationCallbacks是一个ArrayList,每一项保存的是 AnimationFrameCallback 接口的对象,看命名这是一个回调接口
-
AnimationHandler的作用主要是什么呢?
- 是一个定时任务处理器,根据Choreographer的脉冲周期性地完成指定的任务,由于它是一个线程安全的静态变量,因此运行在同一线程中的所有Animator共用一个定时任务处理器,这样的好处在于:一方面可以保证Animator中计算某一时刻动画帧是在同一线程中运行的,避免了多线程同步的问题;另一方面,该线程下所有动画共用一个处理器,可以让这些动画有效地进行同步,从而让动画效果更加优雅。
-
然后在回到start(boolean playBackwards)方法中,查看startAnimation()源码。
- 内部调用,通过将动画添加到活动动画列表来启动动画。必须在UI线程上调用。
- 通过notifyStartListeners()这个方法,刷新动画listener,也就是通知动画开始呢。
-
接着看initAnimation()初始化动画操作逻辑
- 在处理动画的第一个动画帧之前立即调用此函数。如果存在非零
startDelay
,则在延迟结束后调用该函数,它负责动画的最终初始化步骤。
- 在处理动画的第一个动画帧之前立即调用此函数。如果存在非零
3.5 属性动画cancel和end执行流程
-
先看看cancel中的源码
-
可以得知,cancel只会处理那些正在运行或者等待开始运行的动画,大概的处理逻辑是这样的:
- 调用AnimatorListener.onAnimationCancel
-
然后调用Animator.endAnimation
- 通过removeAnimationCallback()把该动画从AnimationHandler的所有列表中清除
- 调用AnimatorListener.onAnimationEnd
- 复位动画所有状态:如mPlayingState = STOPPED、mRunning=false、mReversing = false、mStarted = false等等
-
-
再看看end中的源码
-
end相对于cancel来说有两个区别:一个是会处理所有动画;另一个是会计算最末一帧动画值。其具体的处理逻辑如下所示:
- 若动画尚未开始:调用Animatior.startAnimation让动画处于正常运行状态
- 计算最后一帧动画的动画值:animateValue(mPlayingBackwards ? 0f : 1f)
- 结束动画就调用endAnimation这个方法,上面已经分析了该方法的作用
-
3.6 属性动画pase和resume执行流程
-
先看看pause方法中的源码
- 先看在Animator中的pause方法,然后看ValueAnimator中的pause方法可知:
-
仅仅在动画已开始(isStarted()==true)且当前为非暂停状态时才进行以下处理
- 置位:mPaused = true
- 循环遍历调用AnimatorPauseListener.onAnimationPause
- 清空暂停时间:mPauseTime = -1
- 复位mResumed = false
//在ValueAnimator中
public void pause() {boolean previouslyPaused = mPaused;super.pause();if (!previouslyPaused && mPaused) {mPauseTime = -1;mResumed = false;}
}//在Animator中
public void pause() {if (isStarted() && !mPaused) {mPaused = true;if (mPauseListeners != null) {ArrayList<AnimatorPauseListener> tmpListeners =(ArrayList<AnimatorPauseListener>) mPauseListeners.clone();int numListeners = tmpListeners.size();for (int i = 0; i < numListeners; ++i) {tmpListeners.get(i).onAnimationPause(this);}}}
}
- 做完这些处理之后,等下一帧动画的到来,当doAnimationFrame被调用,此时若仍然处于暂停状态,就会做如下截击- 这样就阻止了动画的正常运行,并记录下来动画暂停的时间,确保恢复之后能让动画调整到暂停之前的动画点正常运行,具体怎么起作用就要看resume的作用。
- ![image](https://upload-images.jianshu.io/upload_images/4432347-3bffba42cdadef07.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
-
先看看resume方法中的源码
-
先看在ValueAnimator中的resume方法,然后看Animator中的resume方法可知:
- 置位:mResumed = true
- 复位:mPaused = false
- 调用AnimatorPauseListener.onAnimationResume
-
//在ValueAnimator中
@Override
public void resume() {if (Looper.myLooper() == null) {throw new AndroidRuntimeException("Animators may only be resumed from the same " +"thread that the animator was started on");}if (mPaused && !mResumed) {mResumed = true;if (mPauseTime > 0) {addAnimationCallback(0);}}super.resume();
}//在Animator中
public void resume() {if (mPaused) {mPaused = false;if (mPauseListeners != null) {ArrayList<AnimatorPauseListener> tmpListeners =(ArrayList<AnimatorPauseListener>) mPauseListeners.clone();int numListeners = tmpListeners.size();for (int i = 0; i < numListeners; ++i) {tmpListeners.get(i).onAnimationResume(this);}}}
}
- 当doAnimationFrame被调用,此时若处于恢复状态(mResume==true),就会做如下补偿处理- 这样就让暂停的时间从动画的运行过程中消除
- ![image](https://upload-images.jianshu.io/upload_images/4432347-1b43530b23fa712b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
3.7 属性动画与View结合
-
属性动画如何去实现View的变换?
- 是根据计算出来的动画值去修改View的属性,如alpha、x、y、scaleX、scaleY、translationX、translationY等等,这样当View重绘时就会产生作用,随着View连续不断地被重绘,就会产生绚烂多彩的动画。
-
接着看setTarget这个方法源码
- 如果是使用ValueAnimator类,那么直接通过mAnimator.setTarget(view)设置view
- 如果是使用ObjectAnimator,那么直接通过ObjectAnimator.ofFloat(view, type, start, end)设置view,最终还是会调用setTarget方法。注意ObjectAnimator实现了ValueAnimator类
- ObjectAnimator是可以在动画帧计算完成之后直接对Target属性进行修改的属性动画类型,相对于ValueAnimator来说更加省心省力
-
相比ValueAnimator类,ObjectAnimator还做了许多操作,ObjectAnimator与 ValueAnimator类的区别:
- ValueAnimator 类是先改变值,然后 手动赋值 给对象的属性从而实现动画;是 间接 对对象属性进行操作;
- ObjectAnimator 类是先改变值,然后 自动赋值 给对象的属性从而实现动画;是 直接 对对象属性进行操作;
- 个人感觉属性动画源码分析十分具有跳跃性。不过还好没有关系,只需要理解其大概运作原理就可以呢。
关于其他内容介绍
01.关于博客汇总链接
- 1.技术博客汇总
- 2.开源项目汇总
- 3.生活博客汇总
- 4.喜马拉雅音频汇总
- 5.其他汇总
02.关于我的博客
- 我的个人站点:www.yczbj.org,www.ycbjie.cn
- github:https://github.com/yangchong211
- 知乎:https://www.zhihu.com/people/...
- 简书:http://www.jianshu.com/u/b7b2...
- csdn:http://my.csdn.net/m0_37700275
- 喜马拉雅听书:http://www.ximalaya.com/zhubo...
- 开源中国:https://my.oschina.net/zbj161...
- 泡在网上的日子:http://www.jcodecraeer.com/me...
- 邮箱:yangchong211@163.com
- 阿里云博客:https://yq.aliyun.com/users/a... 239.headeruserinfo.3.dT4bcV
- segmentfault头条:https://segmentfault.com/u/xi...