动画
动画主要分为补间(Tween)动画 、逐帧(Frame )动画和属性动画。
补间(Tween)动画:对场景内的对象不断进行图像转变(平移、缩放、旋转、透明度)产生的图像效果,是一种渐变动画。
逐帧(Frame )动画:通过顺序播放事先放好的图片,达到画面转换的效果。
属性动画:通过改变对象的属性而达到的动画效果。
1.补间(Tween)动画
现在基本不使用此动画来做复杂的动画效果,3.0版本后逐渐被属性动画替代。目前的使用场景主要有Activity转场动画和布局动画(LayoutAnimation)效果。
1.1动画的使用
Tween动画的使用方式有两种,一种是XML文件方式结合AnimationUtils类实现,第二种是Java代码实现。
1.1.1 XML方式
一般在anim资源文件夹中新建Animation Resource file,假设资源名称为enter.xml,具体演示代码如下:
上述代码是一个set
集合,依次是透明度
、旋转
、缩放
和平移
动画,set中的属性有duration动画时间
、fillAfter
动画结束后是否保存状态、repeatMode
重复方式(restart
重头再来、reverse
倒序)、interpolate
插值器、shareInterpolator
是否共享插值器。
各自的动画没啥要说的,主要是区分下数值的含义:50 、50%和50%p。
50:当前View的左上角的坐标加上50px
50%:当前View左上角的坐标加上控件50%的宽或者高
50%p:当前View左上角的坐标加上父控件50%的宽或者高
代码引用:
Animation animation = AnimationUtils.loadAnimation(this, R.anim.enter);
//animView为要做动画的View对象
animView.startAnimation(animation);
1.1.2 JAVA方式
AnimationSet set = new AnimationSet(true);
Animation alpha = new AlphaAnimation(1, 0);
Animation rotate = new RotateAnimation(0, 30);
Animation scale = new ScaleAnimation(1, 1, 1.5f, 1.5f);
Animation translate = new TranslateAnimation(0, 0, 360, 1080);
set.addAnimation(alpha);
set.addAnimation(rotate);
set.addAnimation(scale);
set.addAnimation(translate);
set.setInterpolator(new AccelerateInterpolator());
set.setFillAfter(true);
set.setDuration(3000);
animView.startAnimation(set);
可以看到Java方式的写法和XML基本没区别,XML的元素和Java类一一对应,最终还是调用animView.startAnimation方法开启动画。
1.1.3 转场动画
1.1.3.1 style方式
通过设置style中对应的属性来达成界面进入退出的切换效果。下面列举一些比较常用的动画属性
//activity开启时进入动画
//activity开启时退出动画
//activity关闭时进入动画
//activity关闭时退出动画
等等
1.1.3.2 JAVA代码方式
常用的进入退出动画通常是通过Window设置setWindowAnimations来实现
window.setWindowAnimations(R.style.anim);
其中anim是设置上述style的属性的样式,列如:
Activity进入退出动画
通过overridePendingTransition
方法来实现,方法调用在startActivity
或者finish
后面。
参数依次为进入动画和退出动画
activity.overridePendingTransition(R.anim.enterAnim,R.anim.exitAnim);
5.0后Java方式:
//跳转界面方式变更:
Intent intent = new Intent(this,SecondActivity.class);
Bundle bundle = ActivityOptions.makeSceneTransitionAnimation(this).toBundle();
startActivity(intent,bundle);
Activity中 setContentView前设置:
//设置支持CONTENT_TRANSITIONS标识
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
//设置界面退出动画
getWindow().setExitTransition(new Fade());
//设置界面进入动画
getWindow().setEnterTransition(new Fade());
//返回时动画
getWindow().setReturnTransition(new Fade());
//再次进入时动画
getWindow().setReenterTransition(new Fade());
或者style中设置:
问题:设置了返回动画后,调用finish方法动画效果和点击返回键动画效果不一致。
解决:finishAfterTransition()替代finish方法。
共享元素动画
5.0之后的一种转场方式,效果非常的绚丽。
首先我们要在当前Activity
中设置
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
或者style设置
- true
其次在当前Activity
中需要共享的组件
上加上android:transitionName="标识"
属性。
之后在目标Activity
中需要共享的组件
上加上相同的android:transitionName="标识"
属性。并且设置
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
或者style设置
- true
设置共享元素动画 按需设置
getWindow().setSharedElementEnterTransition(new ChangeBounds());
getWindow().setSharedElementExitTransition(new ChangeBounds());
getWindow().setSharedElementReturnTransition(new ChangeBounds());
getWindow().setSharedElementReenterTransition(new ChangeBounds());
最后进行界面跳转
Intent intent = new Intent(this, Activity2.class);
// create the transition animation - the images in the layouts
// of both activities are defined with android:transitionName="标识"
ActivityOptions options = ActivityOptions
.makeSceneTransitionAnimation(this, androidRobotView, "标识");
// start the new activity
startActivity(intent, options.toBundle());
多个共享元素跳转
Intent intent = new Intent(this, Activity2.class);
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this,
Pair.create(view1, "agreedName1"),
Pair.create(view2, "agreedName2"));
startActivity(intent, options.toBundle());
此处可参考官方文档-共享元素
1.1.3.4 场景动画
Scene动画:
步骤:
1.创建Scene
2.创建Transition
3.调用TransitionManager.go(Scene ,Transition);
1.创建Scene
//首先我们来看布局层次
//activity布局
//first_layout布局
//end_layout布局
可以看到first_layout
和end_layout
布局id是一致的。后续动画是依据id来进行操作。
View view = LayoutInflater.from(this).inflate(R.layout.end_layout, null);
mEndScene = new Scene(mRootGroup,view);
mEnd2Scene = Scene.getSceneForLayout(mRootGroup, R.layout.end2_layout, this);
创建的方式有两种,一种是通过new Scene
方式,第一个参数为FrameLayout
也就是变动布局所在的父布局
的实例,第二个参数为变动结束布局
的实例。第二种方式是Scene.getSceneForLayout
方式,参数分别是父布局
实例、结束布局id
和上下文
。
2.创建Transition
1.xml方式
新建transition xml
transitionSet 组合亦可单独fade 等
代码引用
mTransition = TransitionInflater.from(this).inflateTransition(R.transition.transition_change);
2.java方式
mTransition = new Fade();
等等
//如果想针对组两个Scene变化中新增的组件做单独的动画可以通过在`Transition`中新增`target`的方式来操作
//,如下代码` android:targetId`中是要单独做动画的`id`。
3.调用TransitionManager.go(Scene ,Transition);
TransitionManager.go(mEndScene,mTransition);
//TransitionManager在xml中配置了,代码中直接inflate到TransitionManager实例
mManager2.transitionTo(mEnd2Scene);
只想改变其中某一个场景下的某一个View属性来实现过度动画。可以通过TransitionManager.beginDelayedTransition(ViewGroup sceneRoot)
的方式。
TransitionManager.beginDelayedTransition(mChangeViewRoot);
//需要变化的view
View square = mChangeViewRoot.findViewById(R.id.changeView);
....(属性变化)
Scene动画
可以参考官网介绍-Scene动画
1.1.4 布局动画
ViewGroup
的LayoutAnimation
动画,只能对Item第一次
进场的时候有效,后续隐藏退出不会触发动画效果。
1.1.4.1 XML布局方式
LayoutAnimation
XML方式需要两个XML文件。XML文件A
是具体的动画效果(缩放、透明度、旋转、平移),而B的XML根部局是layoutAnimation
,其具体属性含义情况下面代码:
A XML
//B XML是具体的Item动画
写好XML后在ViewGroup
中引用
1.1.4.2 JAVA代码方式
1.LayoutAnimation
Java代码方式通过LayoutAnimationController
类处理,设置方法和XML属性一一对应,具体代码如下:
//注意入参为Animation,通过loadAnimation来获取,获取时入参为`B`(具体动画效果XML的ID)
LayoutAnimationController animationController =
new LayoutAnimationController(AnimationUtils.loadAnimation(this, R.anim.B));
animationController.setDelay(0.5f);
animationController.setOrder(LayoutAnimationController.ORDER_REVERSE);
animationController.setInterpolator(new LinearInterpolator());
mList.setLayoutAnimation(animationController);
如果需要默认Order的三种方式不满足我们的要求,那么我们可以通过继承`LayoutAnimationController`
重写`getTransformedIndex`方法来满足子布局进入屏幕的动效顺序。
2. LayoutTransition方式
ViewGroup
布局更改中启用自动动画。
最简单的开启默认动画的方式:在ViewGroup的布局中设置如下属性,那么就会开启内嵌的布局动画。
android:animateLayoutChanges="true"
自定义布局方式:
LayoutTransition.APPEARING
代表子View添加到容器中时的过渡动画效果。
LayoutTransition.CHANGE_APPEARING
代表子View添加到容器中时,其他子View位置改变的过渡动画。
LayoutTransition.CHANGE_DISAPPEARING
代表子View从容器中移除时的过渡动画效果。
LayoutTransition.DISAPPEARING
代表子View从容器中移除时,其它子view位置改变的过渡动画
LayoutTransition.CHANGING
代表子View在容器中位置改变时的过渡动画,不涉及删除或者添加操作
LayoutTransition transition = new LayoutTransition();
ObjectAnimator appearing = ...;
ObjectAnimator disappearing = ...;
ObjectAnimator changing = ...;
ObjectAnimator change_appearing = ...;
ObjectAnimator change_disappearing = ...;
appearing.setDuration(transition.getDuration(LayoutTransition.APPEARING));
transition.setAnimator(LayoutTransition.APPEARING,appearing);
transition.setAnimator(LayoutTransition.DISAPPEARING,disappearing);
transition.setAnimator(LayoutTransition.CHANGING,changing);
transition.setAnimator(LayoutTransition.CHANGE_APPEARING,change_appearing);
transition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING,change_disappearing);
transition.setStagger(LayoutTransition.APPEARING,30);
mChangeViewRoot.setLayoutTransition(transition);
1.2动画的原理
讲了这么多补间动画的用法这时候我们心中也许有个疑问,这个动画是如何实现的呢?
下面我们带着这个疑问来分析下Animation的源码逻辑:
首先我们看到Animation
子类都重写了applyTransformation
方法,那么我们通过查看每个子类这个方法的实现,可以了解到这个方法是对应子类具体动画的实现,那么这个动画实现的方法如何和View相关联的呢,带着这个疑问我们去看一看View.startAnimation
方法:
public void startAnimation(Animation animation) {
animation.setStartTime(Animation.START_ON_FIRST_FRAME);
setAnimation(animation);
invalidateParentCaches();
invalidate(true);
}
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
通过查看startAnimation
方法我们发现此方法一共有4个步骤,查看源码我们可以发现第一步和第二步是对Animation进行了一些初始化工作,第三步是设置标志位暂且跳过目前与我们无关,那么我们继续看第四步里面的方法是如何操作的:
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
...
// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
...
}
上述代码省略部分都是进行一些标志位的判定,可以看到代码最终走到了ViewParent.invalidateChild
方法中,而View
的ViewParent
是ViewGroup
,现在我们去看看ViewGroup
中的invalidateChild
方法:
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
...
ViewParent parent = this;
if (attachInfo != null) {
...
final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0;
...
}
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
if (drawAnimation) {
if (view != null) {
view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
} else if (parent instanceof ViewRootImpl) {
((ViewRootImpl) parent).mIsAnimating = true;
}
}
// If the parent is dirty opaque or not dirty, mark it dirty with the opaque
// flag coming from the child that initiated the invalidate
if (view != null) {
...
if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
}
}
parent = parent.invalidateChildInParent(location, dirty);
if (view != null) {
// Account for transform on current parent
Matrix m = view.getMatrix();
....
}
} while (parent != null);
}
}
从上面的代码中,我们可以看到如果parent 不为空的情况下do {} while
一直执行invalidateChildInParent
方法,而这个方法做了一些条件处理后将当前View的mParent返回,最终得到View树的根布局的mParent,而我们知道View的根布局是DecorView,依据Activity相关知识我们可以知道DecorView的mParent是ViewRootImpl
,也就是说最终会走到ViewRootImpl.invalidateChildInParent
方法中。那么我们直接看一下ViewRootImpl.invalidateChildInParent
方法。
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
if (dirty == null) {
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
...
invalidateRectOnScreen(dirty);
return null;
}
上面的代码可以看到invalidateChildInParent
先检测线程,然后判断dirty是否为空,为空则初始化mDirty
,走到scheduleTraversals
方法,不为空则最后调用invalidateRectOnScreen
方法,在这个方法中最终也会走到scheduleTraversals
方法里面,下面我们来看一下scheduleTraversals
方法做了啥:
void scheduleTraversals() {
...
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
通过了解可以看到 这个方法中通过Choreographer
类监听屏幕刷新信号,然后走到了TraversalRunnable.run
方法中,而run
方法调用了doTraversal
方法,最终调用了 performTraversals
方法。这个方法主要走了测量
、摆放
和绘制
几个方法:
...
performMeasure
...
performLayout
...
performDraw
这个时候我们差不多可以得出结论了,动画执行过程应该是在View树测量、摆放和绘制的过程中进行的,而这个过程都由ViewRootImpl
的performTraversals
方法来控制。通过阅读源码我们可以跳过前两个过程直接看draw
的步骤:
ViewRootImpl.performDraw ->ViewRootImpl.draw->ThreadedRenderer.draw ->
ThreadedRenderer.updateRootDisplayList ->
ThreadedRenderer.updateViewTreeDisplayList->
View.updateDisplayListIfDirty ->
View.draw->ViewGroup.dispatchDraw->
ViewGroup.drawChild->View.draw
我们直接看View.draw的方法
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
在draw(canvas)中有这么一段注释,大体意思就是draw的流程:先绘制background
然后绘制View
,如果有子View
的话绘制子view
最后绘制scrollbars
这些等等。在ViewGroup
的dispatchDraw
中最终走到View.draw(canvas, this, drawingTime)
方法,在这个方法中我们终于找到了最开始设置的Animation
的调用!
public Animation getAnimation() {
return mCurrentAnimation;
}
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...
final Animation a = getAnimation();
if (a != null) {
more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
concatMatrix = a.willChangeTransformationMatrix();
...
transformToApply = parent.getChildTransformation();
}
...
if (transformToApply != null) {
if (concatMatrix) {
if (drawingWithRenderNode) {
renderNode.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);
}
...
从上面的代码中可以看到调用了applyLegacyAnimation
方法后,获取Animation变换矩阵,如果使用的是硬件绘制,将结果应用到 renderNode
,如果是软件绘制的话则将结果应用到canvas
。下面我们来看一下applyLegacyAnimation
方法:
private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
Animation a, boolean scalingRequired) {
Transformation invalidationTransform;
final int flags = parent.mGroupFlags;
final boolean initialized = a.isInitialized();
if (!initialized) {//未初始化 则初始化Animation
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();
//调用动画的getTransformation方法 最终走到Animation的applyTransformation方法中
boolean more = a.getTransformation(drawingTime, t, 1f);
...
//如果动画没执行完,会走到invalidate方法,遍历之前的操作。
if (more) {
if (!a.willChangeBounds()) {
if (...) {
...
} else if (...) {
parent.invalidate(mLeft, mTop, mRight, mBottom);
}
} else {
...
parent.invalidate(left, top, left + (int) (region.width() + .5f),
top + (int) (region.height() + .5f));
}
}
return more;
}
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;
}
...
//确保进度在0到1之间
if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
...
if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
...
//依据当前的进度通过插值器获取到实际动画的进度
final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
//应用动画效果
applyTransformation(interpolatedTime, outTransformation);
}
//如果取消或者执行进度大于1
if (expired) {
//重复次数达到设定的次数或者取消
if (mRepeatCount == mRepeated || isCanceled()) {
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;
}
我们来简单的理一下draw之后的逻辑,draw
会调用applyLegacyAnimation
方法,applyLegacyAnimation
方法调用getTransformation
方法获取到当前的动画进度然后应用动画效果applyTransformation
,将转变后的矩阵同步到画布上或者renderNode
上,如果动画执行未完成的话会继续调用invalidate
方法重新走之前绘制的流程,直到动画结束。
1.3小结
补间(Tween)动画目前还主要应用于转场动画、布局动画等场景。
原理:startAnimation
会调用View.invalidate
方法,之后会调用ViewRootImpl.invalidateChildInParent
方法,然后会调用scheduleTraversals
,通过监听屏幕刷新的帧信号,走到performTraversals
方法,最后会走到View.draw
方法中。在View.draw
方法中进行动画的操作,并且动画未结束时不断调用View.invalidate
方法,直到动画结束。
2.逐帧(Frame )动画
逐帧动画:通过连续播放一组图片达到的动画效果,主要适用于复杂的类似Gif等动画效果。
2.1动画的使用
2.1.1XML方式
在drawable
文件夹中新建xml
,根元素为animation-list
,item
为一组图片中对应的每张图的信息,android:drawable
为具体图片资源ID
,android:duration
为展示时间
。
-
-
...
AnimationDrawable drawable = (AnimationDrawable)ContextCompat.getDrawable(this, R.drawable.frame_list);
animView.setBackground(drawable);
drawable.start();
引用的时候获取到XML并转成AnimationDrawable
格式的对象,作为background
设置给ImageView
,调用start
开启动画。
2.1.2JAVA方式
AnimationDrawable drawable = new AnimationDrawable();
drawable.addFrame(ContextCompat.getDrawable(this,R.drawable.item1),250);
drawable.addFrame(ContextCompat.getDrawable(this,R.drawable.item2),250);
drawable.addFrame(ContextCompat.getDrawable(this,R.drawable.item3),250);
drawable.addFrame(ContextCompat.getDrawable(this,R.drawable.item4),250);
animView.setBackground(drawable);
drawable.start();
Java调用方式通过AnimationDrawable.addFrame
加载每一帧的图片,应用和开启动画方式和XML方式
一致。
2.2动画的原理
AnimationDrawable.start()
public void start() {
...
setFrame(0, false, mAnimationState.getChildCount() > 1
|| !mAnimationState.mOneShot);
}
setFrame
private void setFrame(int frame, boolean unschedule, boolean animate) {
...
scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
}
scheduleSelf
public void scheduleSelf(@NonNull Runnable what, long when) {
final Callback callback = getCallback();
if (callback != null) {
callback.scheduleDrawable(this, what, when);
}
}
public void setBackground(Drawable background) {
//noinspection deprecation
setBackgroundDrawable(background);
}
public void setBackgroundDrawable(Drawable background) {
...
background.setCallback(this);
...
}
我们可以看到调用start方法后会直接调用setFrame
方法,在这个方法里面会调用scheduleSelf
方法,最终调用了callback.scheduleDrawable
方法。而Callback
在View.setBackground
中传入,入参就是View本身
,也就是说最终调用到View.scheduleDrawable
方法这边,那么我们来看看View.scheduleDrawable
做了什么:
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
if (verifyDrawable(who) && what != null) {
final long delay = when - SystemClock.uptimeMillis();
if (mAttachInfo != null) {
mAttachInfo.mViewRootImpl.mChoreographer.postCallbackDelayed(
Choreographer.CALLBACK_ANIMATION, what, who,
Choreographer.subtractFrameDelay(delay));
} else {
// Postpone the runnable until we know
// on which thread it needs to run.
getRunQueue().postDelayed(what, delay);
}
}
}
上面的代码会发现是延时开启一个Runable
,而这个Runable
就是AnimationDrawable
本身,也就是说会走到AnimationDrawable.run
方法里面,在run
方法里面调用了的下一帧nextFrame
的方法,从而达到了播放的效果。
2.3小结
逐帧动画主要用于一些复杂效果的实现,类似Gif那种播放的效果。
优点:实现简单。
缺点:依赖于多张图片,容易OOM。
3.属性动画
Android3.0后新增的动画,通过改变对象的属性从而达到动画效果。
3.1动画的使用
ViewPropertyAnimator动画:针对View的属性动画,使用方式如下:
animView.animate().alpha(0.8f).scaleX(1.2f).scaleY(1.3f);
通过animate()方法获取到ViewPropertyAnimator对象,然后通过alpha等属性方法来做属性动画。
ObjectAnimator:通过更改对象的属性值来完成动画效果。代码使用方式如下:
//单个属性
ObjectAnimator scaleX = ObjectAnimator.ofFloat(animView, "scaleX", 1, 1.2f);
scaleX.setDuration(300);
scaleX.setInterpolator(new DecelerateInterpolator());
ObjectAnimator scaleY = ObjectAnimator.ofFloat(animView, "scaleY", 1, 1.2f);
scaleY.setDuration(300);
scaleY.setInterpolator(new DecelerateInterpolator());
//控制动画顺序
AnimatorSet set = new AnimatorSet();
set.playTogether(scaleX,scaleY);
set.play(scaleX).with(scaleY);
set.play(scaleX).after(scaleY);
set.play(scaleX).before(scaleY);
set.start();
//同时更改多个属性
PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX",1,1.2f);
PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY",1,1.2f);
ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(animView, scaleX, scaleY);
objectAnimator.start();
//将同一个属性拆分
//开始0
Keyframe start = Keyframe.ofFloat(0, 0);
//中间放大1.3
Keyframe center = Keyframe.ofFloat(0.5f,1.3f);
//结束回缩1.2
Keyframe end = Keyframe.ofFloat(1,1.2f);
PropertyValuesHolder scaleY = PropertyValuesHolder.ofKeyframe("scaleY",start,center,end);
ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(animView, scaleY);
objectAnimator.start();
ObjectAnimator的父类ValueAnimator
ValueAnimator valueAnimator = ValueAnimator.ofFloat(1, 1.3f);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue = (float) animation.getAnimatedValue();
animView.setScaleY(animatedValue);
}
});
valueAnimator.start();
3.2动画的原理
ObjectAnimator.ofxxx
方法中通过源码可以看到这其中就是一些初始化工作,并将propertyName
转换成PropertyValuesHolder
保存到mValues
中,而在真正初始化方法initAnimation
里面则会遍历mValues
,调用PropertyValuesHolder
的setupSetterAndGetter
方法,而这个方法则是最终会走到getPropertyFunction
方法里面,这个方法就是通过反射拿到set/get+propertyName的大写首字母+propertyName
的方法,进行设值。所以如果我们后续对自己的对象进行属性动画时需要提供对应的set/get
方法。
下面我们来看一下属性动画的start
方法:
可以看到ObjectAnimator调用的是super.start()方法,也就是说调用了ValueAnimator的方法。
public void start() {
start(false);
}
private void start(boolean playBackwards) {
...
addAnimationCallback(0);
...
}
private void addAnimationCallback(long delay) {
if (!mSelfPulse) {
return;
}
getAnimationHandler().addAnimationFrameCallback(this, delay);
}
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
if (mAnimationCallbacks.size() == 0) {
getProvider().postFrameCallback(mFrameCallback);
}
...
}
private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {
final Choreographer mChoreographer = Choreographer.getInstance();
@Override
public void postFrameCallback(Choreographer.FrameCallback callback) {
mChoreographer.postFrameCallback(callback);
}
@Override
public void postCommitCallback(Runnable runnable) {
mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
}
@Override
public long getFrameTime() {
return mChoreographer.getFrameTime();
}
@Override
public long getFrameDelay() {
return Choreographer.getFrameDelay();
}
@Override
public void setFrameDelay(long delay) {
Choreographer.setFrameDelay(delay);
}
}
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
doAnimationFrame(getProvider().getFrameTime());
if (mAnimationCallbacks.size() > 0) {
getProvider().postFrameCallback(this);
}
}
};
从上面的代码块中可以看到,start
方法最终是走到了MyFrameCallbackProvider.postFrameCallback
方法里面了,这个方法调用了Choreographer.postFrameCallback
的方法,而这个方法我们在Tween动画中已经简单的看过了,这是用来监听屏幕刷新的每一帧的,监听的回调中再调用doAnimationFrame
方法,然后再次监听从而达到监听每一帧的效果,所以这边我们应该可以得出结论,属性动画也是监听屏幕每一帧从而达到更改属性的效果的。doAnimationFrame最终会回调到ValueAnimator的doAnimationFrame方法中,下面我们来看一下这个方法的具体实现。
public final boolean doAnimationFrame(long frameTime) {
if (!mRunning) {
if (mStartTime > frameTime && mSeekFraction == -1) {
return false;
} else {
mRunning = true;
startAnimation();
}
}
boolean finished = animateBasedOnTime(currentTime);
if (finished) {
endAnimation();
}
return finished;
}
boolean animateBasedOnTime(long currentTime) {
boolean done = false;
if (mRunning) {
...
animateValue(currentIterationFraction);
}
return done;
}
void animateValue(float fraction) {
...
if (mUpdateListeners != null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
mUpdateListeners.get(i).onAnimationUpdate(this);
}
}
}
代码中逻辑是如果动画没有执行则执行,调用animateBasedOnTime
方法判断是否执行结束,如果结束调用endAnimatio
n方法。animateBasedOnTime
代码开始部分我们不关注,可以看到这边的代码会执行animateValue
方法,这个方法中会回调监听器的onAnimationUpdate
方法。这就是我们拿到动画执行进度的流程,而在ObjectAnimator
中重写了这个方法,在更新了监听器进度后,ObjectAnimator
还执行了mValues
成员的setAnimatedValue
方法,这个方法拿到我们之前拼接好的属性的set Method对象
进行设值,从而更新了数据。
void setAnimatedValue(Object target) {
...
if (mSetter != null) {
try {
mTmpValueArray[0] = getAnimatedValue();
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}
3.3总结
1.对比ValueAnimator
类 & ObjectAnimator
类,其实二者本质上是一致的:先改变值,然后 赋值 给对象的属性从而实现动画效果。
ValueAnimator
是值的变化,然后手动设置,而ObjectAnimator
是内部封装了set/get
的调用,自动更改值并且设置。
2.ObjectAnimator
更改的属性可以是任意属性
,但是必须要提供get/set
方法,否则会导致崩溃和动画不生效。
3.自定义属性变化如果界面效果不生效则有可能是invalidate
方法未调用。
4.插值器与估值器
4.1插值器
插值器作用是设置属性值从初始值到最终值的变化规律。
4.1.1场景
实现非线性的特殊动画效果
4.1.2使用方式
XML:
< .../>
JAVA:
AnimationSet set = new AnimationSet(true);
Animation alpha = new AlphaAnimation(1, 0);
...
//java方式
set.setInterpolator(new AccelerateInterpolator());
...
4.1.3内置的几种方式
作用 | id | JAVA类 |
---|---|---|
动画加速进行 | @android:anim/accelerate_interpolator | AccelerateInterpolator |
快速完成动画,超出再回到结束样式 | @android:anim/overshoot_interpolator | OvershootInterpolator |
先加速再减速 | @android:anim/accelerate_decelerate_interpolator | AccelerateDecelerateInterpolator |
先退后再加速前进 | @android:anim/anticipate_interpolator | AnticipateInterpolator |
先退后再加速前进,超出终点后再回终点 | @android:anim/anticipate_overshoot_interpolator | AnticipateOvershootInterpolator |
最后阶段弹球效果 | @android:anim/bounce_interpolator | BounceInterpolator |
周期运动 | @android:anim/cycle_interpolator | CycleInterpolator |
减速 | @android:anim/decelerate_interpolator | DecelerateInterpolator |
匀速 | @android:anim/linear_interpolator | LinearInterpolator |
使用时依据自己情况选择合适的插值器,如都不满足则考虑自定义,下面我们看一看自定义插值器:
首先我们来参考下LinearInterpolator
和 DecelerateInterpolator
的插值器做了哪些操作
DecelerateInterpolator
public float getInterpolation(float input) {
float result;
if (mFactor == 1.0f) {
result = (float)(1.0f - (1.0f - input) * (1.0f - input));
} else {
result = (float)(1.0f - Math.pow((1.0f - input), 2 * mFactor));
}
return result;
}
LinearInterpolator
public float getInterpolation(float input) {
return input;
}
可以看到在这两个插值器中getInterpolation
返回的算法不一样,LinearInterpolator
返回得就是值本身,而DecelerateInterpolator
幂次方,所以如果我们要自定义插值器的话可以直接继承TimeInterpolator
实现getInterpolation
方法,然后依据我们自己的算法返回对应的进度值。这就是插值器的工作。
4.2估值器
估值器是依据变化的进程得出具体的属性值。
4.2.1场景
协助插值器完成复杂的动画效果。
下面我们来看下内置的FloatEvaluator
是如何实现变化的。
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
可以看到 FloatEvaluator
重写了evaluate
方法,此方法中fraction
为动画进度,startValue
为开始值,endValue
为结束值。算法返回开始值加上总体值乘以动画进度的值,这个值就是当前进度对应的具体属性值。所以如果我们要自定义的话也需要继承TypeEvaluator
并重写evaluate
方法,在evaluate
方法中依据要求返回具体的值来达到我们的效果。下面代码是引用插值器的代码。
ObjectAnimator.ofObject(mRl,"scaleX",new FloatEvaluator(),0,1);
结语:
动画相关的知识点大体上已经梳理完成,可能不是太过详细,希望各位海涵。
给自己定个目标 下一篇梳理事件相关的知识点,加油!
撒花~~~
撒花~~~
撒花~~~
撒花~~~