Android动画总结

动画

动画主要分为补间(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_layoutend_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 布局动画

ViewGroupLayoutAnimation动画,只能对Item第一次进场的时候有效,后续隐藏退出不会触发动画效果。
1.1.4.1 XML布局方式
LayoutAnimationXML方式需要两个XML文件。XML文件A是具体的动画效果(缩放、透明度、旋转、平移),而B的XML根部局是layoutAnimation,其具体属性含义情况下面代码:

A XML
//B XML是具体的Item动画

写好XML后在ViewGroup中引用



1.1.4.2 JAVA代码方式
1.LayoutAnimationJava代码方式通过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方法中,而ViewViewParentViewGroup,现在我们去看看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树测量、摆放和绘制的过程中进行的,而这个过程都由ViewRootImplperformTraversals方法来控制。通过阅读源码我们可以跳过前两个过程直接看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这些等等。在ViewGroupdispatchDraw中最终走到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-listitem为一组图片中对应的每张图的信息,android:drawable具体图片资源IDandroid: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方法。而CallbackView.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,调用PropertyValuesHoldersetupSetterAndGetter方法,而这个方法则是最终会走到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方法判断是否执行结束,如果结束调用endAnimation方法。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

使用时依据自己情况选择合适的插值器,如都不满足则考虑自定义,下面我们看一看自定义插值器:
首先我们来参考下LinearInterpolatorDecelerateInterpolator的插值器做了哪些操作

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

结语:

动画相关的知识点大体上已经梳理完成,可能不是太过详细,希望各位海涵。

给自己定个目标 下一篇梳理事件相关的知识点,加油!

撒花~~~
撒花~~~
撒花~~~
撒花~~~

你可能感兴趣的:(Android动画总结)