Android 动画深入分析(三)——属性动画的高级使用及工作原理

一.前言

前面已经介绍过了逐帧动画,补间动画,和一些属性动画的简单使用,接下来介绍一下属性动画的高级使用及工作原理。
 
前面的知识:
Android 动画深入分析(一)——逐帧动画,补间动画
 
Android 动画深入分析(二)——属性动画的简单使用

 
属性动画可以对任意对象进行动画操作,接下来我们就使用任意对象来实现动画。

二. 对任意属性做动画

给Button加一个动画,让button的宽度从当前值增加到500px,即对button的width属性做动画。
用上篇文章属性动画来实现:

ObjectAnimator.ofInt(button,"width",500).setDuration(5000).start();

但是发现没有效果。
 
先说明一下属性动画的原理:属性动画要求动画作用的对象提供该属性的get和set方法,属性动画根据外界传递的该属性的初始值和最终值,以动画的效果去多次调用set方法,每次传递给set方法的值都不一样,准切来说是随着时间的推移,所传递的值越来越接近最终值。如果动画的时候没有传递初始值,那么还要提供get方法,因为系统要去获取属性的初始值。总结一下,我们对object的属性abc做动画,如果想让动画生效,要同时满足两个条件:

  1. object必须要提供setAbc方法,如果动画的时候没有传递初始值,那么还要提供getAbc方法。
  2. object的setAbc对属性abc所做出的改变必须通过某种方法反应出来,比如会带来UI的改变之类的。

在button的内部虽然提供了getWidth和setWidth方法,但是这个setWidth方法并不改变视图的大小,即不满足条件2
 
解决方法:

  1. 给你的对象加上get和set方法,如果有条件的话
  2. 用一个类来包装原始对象,间接提供get和set方法
  3. 采用ValueAnimator,监听动画过程,自己实现属性的改变

使用上面的方法解决这个问题,第一种没有权限就不做解释
 

2.用一个类来包装原始对象,间接提供get和set方法

public class ViewWrapper {
    
    
    private View mTarget;

    public ViewWrapper(View mTarget) {
    
    
        this.mTarget = mTarget;
    }
    public int getWidth(){
    
    
        return mTarget.getLayoutParams().width;
    }
    public void setWidth(int width){
    
    
        mTarget.getLayoutParams().width = width;
        mTarget.requestLayout();
    }
}

属性动画调用:

 ObjectAnimator.ofInt(new ViewWrapper(button),"width",500).setDuration(5000).start();

解释:这里的操作对象时new ViewWrapper(button),在使用ObjectAnimator处理width属性时,会去多次调用ViewWrapper类中的set方法,而在set方法中我们对width做了处理,使其宽改变,最终达到效果。

3.采用ValueAnimator,监听动画过程,自己实现属性的改变

performAnimate(button,button.getLayoutParams().width,500);


private void performAnimate(View view, int start, int end) {
    
    
        ValueAnimator valueAnimator = ValueAnimator.ofInt(1,100);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    
    
        
            private IntEvaluator intEvaluator = new IntEvaluator();
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
    
    
                //获取当前动画的进度值
                int currentValue = (Integer) animation.getAnimatedValue();

                //获取当前动画占整个动画的比例
                float fraction = animation.getAnimatedFraction();
                //调用整型估值器,通过比例算出宽度,然后设给View,当然这里也可直接自己算
                view.getLayoutParams().width = intEvaluator.evaluate(fraction,start,end);
                view.requestLayout();
            }
        });
        valueAnimator.setDuration(5000).start();
    }

三.TypeEvaluator(估值器)

TypeEvaluator的作用是根据当前属性改变的百分比来计算改变后的属性值。
系统预设的有IntEvaluator(针对整型属性),FloatEvaluator(针对浮点型属性),和ArgbEvaluator(针对Color属性)。上面采用ValueAnimator,监听动画过程,自己实现属性的改变就采用了估值器
 

FloatEvaluator的内部实现机制

我们来看一下FloatEvaluator的代码实现:

public class FloatEvaluator implements TypeEvaluator<Number> {
    
    
    public Float evaluate(float fraction, Number startValue, Number endValue) {
    
    
        float startFloat = startValue.floatValue();
        return startFloat + fraction * (endValue.floatValue() - startFloat);
    }
}

可以看到,FloatEvaluator实现了TypeEvaluator接口,然后重写evaluate()方法。evaluate()方法当中传入了三个参数,第一个参数fraction非常重要,这个参数用于表示动画的完成度的,我们应该根据它来计算当前动画的值应该是多少,第二第三个参数分别表示动画的初始值和结束值。那么上述代码的逻辑就比较清晰了,用结束值减去初始值,算出它们之间的差值,然后乘以fraction这个系数,再加上初始值,那么就得到当前动画的值了。
 

自定义TypeEvaluator

ValueAnimator中还有一个ofObject()方法,是用于对任意对象进行动画操作的。但是相比于浮点型或整型数据,对象的动画操作明显要更复杂一些,因为系统将完全无法知道如何从初始对象过度到结束对象,因此这个时候我们就需要实现一个自己的TypeEvaluator来告知系统如何进行过度。
 
1.首先自定义一个类:

public class Point {
    private float x;
    private float y;
    public Point(float x, float y) {
        this.x = x;
        this.y = y;
    }
    public float getX() {
        return x;
    }
    public float getY() {
        return y;
    }
}

这个类有两个成员变量,x,y,我们的目标是让将Point1(实例对象)通过动画平滑过度到Point2(实例对象)
 
2.定义PointEvaluator,如下所示:

public class PointEvaluator implements TypeEvaluator{
    
    
 
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {
    
    
        Point startPoint = (Point) startValue;
        Point endPoint = (Point) endValue;
        float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
        float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
        Point point = new Point(x, y);
        return point;
    }
}

可以看到,PointEvaluator同样实现了TypeEvaluator接口并重写了evaluate()方法。其实evaluate()方法中的逻辑还是非常简单的,先是将startValue和endValue强转成Point对象,然后同样根据fraction来计算当前动画的x和y的值,最后组装到一个新的Point对象当中并返回。
 
3.使用

Point point1 = new Point(0, 0);
Point point2 = new Point(300, 300);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), point1, point2);
anim.setDuration(5000);
anim.start();

四.Interpolator(插值器)

它的主要作用是根据时间流逝的百分比来计算当前属性值的百分比。通过它,可以控制动画的变化速率,比如去实现一种非线性运动的动画效果。
 
之前的补间动画也支持这个功能,只不过在属性动画中新增了一个TimeInterpolator接口,这个接口是用于兼容之前的Interpolator的,这使得所有过去的Interpolator实现类都可以直接拿过来放到属性动画当中使用,那么我们来看一下现在TimeInterpolator接口的所有实现类,如下图所示:
在这里插入图片描述
常用的实现类:

说明
LinearInterpolator 匀速运动的Interpolator
AccelerateInterpolator 加速运动的Interpolator
DecelerateInterpolator 减速运动的Interpolator
AccelerateDecelerateInterpolator 加速减速插值器:动画中间快两头慢
BounceInterpolator 可以模拟物理规律,实现反复弹起效果的Interpolator。

 
使用属性动画时,系统默认的Interpolator其实就是一个先加速后减速的Interpolator,对应的实现类就是AccelerateDecelerateInterpolator。
 
当然我们也可以替换Interpolator,代码如下:

ObjectAnimator animator1 =  ObjectAnimator.ofFloat(textView,"translationX",0,1000F,0);
        animator1.setDuration(5000);
        //设置为匀速插值器
        animator1.setInterpolator(new LinearInterpolator());
        animator1.start();

Interpolator的内部实现机制:

首先看一下TimeInterpolator的接口定义,代码如下所示:

public interface TimeInterpolator {
    
    
    float getInterpolation(float input);
}

只有一个getInterpolation()方法,getInterpolation()方法中接收一个input参数,这个参数的值会随着动画的运行而不断变化,不过它的变化是非常有规律的,就是根据设定的动画时长匀速增加,变化范围是0到1。也就是说当动画一开始的时候input的值是0,到动画结束的时候input的值是1,而中间的值则是随着动画运行的时长在0到1之间变化的。
 
input和fraction的关系:,input的值决定了fraction的值。input的值是由系统经过计算后传入到getInterpolation()方法中的,然后我们可以自己实现getInterpolation()方法中的算法,根据input的值来计算出一个返回值,而这个返回值就是fraction了。
 
因此,最简单的情况就是input值和fraction值是相同的,这种情况由于input值是匀速增加的,因而fraction的值也是匀速增加的,所以动画的运动情况也是匀速的。系统中内置的LinearInterpolator就是一种匀速运动的Interpolator,那么我们来看一下它的源码是怎么实现的:

public class LinearInterpolator extends BaseInterpolator implements NativeInterpolator {
    
    

    public LinearInterpolator() {
    
    
    }

    public LinearInterpolator(Context context, AttributeSet attrs) {
    
    
    }

    public float getInterpolation(float input) {
    
    
        return input;
    }

    /** @hide */
    @Override
    public long createNativeInterpolator() {
    
    
        return NativeInterpolatorFactory.createLinearInterpolator();
    }
}

这里我们只看getInterpolation()方法,这个方法没有任何逻辑,就是把参数中传递的input值直接返回了,因此fraction的值就是等于input的值的,这就是匀速运动的Interpolator的实现方式。
 
再来看一下AccelerateDecelerateInterpolator的实现:

public class AccelerateDecelerateInterpolator extends BaseInterpolator
        implements NativeInterpolator {
    public float getInterpolation(float input) {
        return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
    }
}

它的返回值计算借助余弦函数来完成。经过了余弦运算之后,最终的结果不再是匀速增加的了,而是经历了一个先加速后减速的过程。
 

自定义一个先减速后加速的插值器:

public class DecelerateAccelerateInterpolator implements TimeInterpolator{
    
    
 
    @Override
    public float getInterpolation(float input) {
    
    
        float result;
        if (input <= 0.5) {
    
    
            result = (float) (Math.sin(Math.PI * input)) / 2;
        } else {
    
    
            result = (float) (2 - Math.sin(Math.PI * input)) / 2;
        }
        return result;
    }
 
}

这段代码是使用正弦函数来实现先减速后加速的功能的,因为正弦函数初始弧度的变化值非常大,刚好和余弦函数是相反的,而随着弧度的增加,正弦函数的变化值也会逐渐变小,这样也就实现了减速的效果。当弧度大于π/2之后,整个过程相反了过来,现在正弦函数的弧度变化值非常小,渐渐随着弧度继续增加,变化值越来越大,弧度到π时结束,这样从0过度到π,也就实现了先减速后加速的效果。

五.属性动画的工作原理

上面已经说过了,在说一遍:
属性动画要求动画作用的对象提供该属性的get和set方法,属性动画根据外界传递的该属性的初始值和最终值,以动画的效果去多次调用set方法,每次传递给set方法的值都不一样,准切来说是随着时间的推移,所传递的值越来越接近最终值。如果动画的时候没有传递初始值,那么还要提供get方法,因为系统要去获取属性的初始值
 
对于ObjectAnimator animator = ObjectAnimator.ofFloat(textview, “alpha”, 0f).start();我们现在来分析它的源码。
 
从ObjectAnimator.start方法开始。

 @Override
    public void start() {
    
    
    	/*
    	判断如果当前动画,等待的动画和延迟的动画中有和当前动画相同的动画,
    	就把相同的动画取消掉
    	*/
        AnimationHandler.getInstance().autoCancelBasedOn(this); 
        //log操作
        if (DBG) {
    
    
            Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
            for (int i = 0; i < mValues.length; ++i) {
    
    
                PropertyValuesHolder pvh = mValues[i];
                Log.d(LOG_TAG, "   Values[" + i + "]: " +
                    pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
                    pvh.mKeyframes.getValue(1));
            }
        }
        //调用父类的start方法
        super.start();
    }


在看看父类的start方法
ValueAnimator.start

private void start(boolean playBackwards) {
    
    
        if (Looper.myLooper() == null) {
    
    
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        .......
        addAnimationCallback(0);
         if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
    
    
            // If there's no start delay, init the animation and notify start listeners right away
            // to be consistent with the previous behavior. Otherwise, postpone this until the first
            // frame after the start delay.
            startAnimation();
            if (mSeekFraction == -1) {
    
    
                // No seek, start at play time 0. Note that the reason we are not using fraction 0
                // is because for animations with 0 duration, we want to be consistent with pre-N
                // behavior: skip to the final value immediately.
                setCurrentPlayTime(0);
            } else {
    
    
                setCurrentFraction(mSeekFraction);
            }
        }
}

//ValueAnimator.getAnimationHandler().addAnimationFrameCallback(this, delay);
 private void addAnimationCallback(long delay) {
    
    
        if (!mSelfPulse) {
    
    
            return;
        }
        getAnimationHandler().addAnimationFrameCallback(this, delay);
    }

首先,上段代码第一行代码说明动画是需要运行在线程中,紧接着调用了addAnimationCallback(0);方法为动画添加回调方法看一下其源码:
最终调用到了getAnimationHandler().addAnimationFrameCallback(this, delay);

public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
    
    
        if (mAnimationCallbacks.size() == 0) {
    
    
            getProvider().postFrameCallback(mFrameCallback);
        }
        if (!mAnimationCallbacks.contains(callback)) {
    
    
            mAnimationCallbacks.add(callback);
        }

        if (delay > 0) {
    
    
            mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
        }
    }

valueAnimation实现了AnimationFrameCallback,实际上传入的就是ValueAnimation的callback。最终加入到了mAnimationCallbacks队列中。

接下来调用到了startAnimation();方法

private void startAnimation() {
    
    
    	.....
        initAnimation();	//主要就是这个方法
      	.....
}
//ValueAnimator.initAnimation
 void initAnimation() {
    
    
        if (!mInitialized) {
    
    
            int numValues = mValues.length;
            for (int i = 0; i < numValues; ++i) {
    
    
                mValues[i].init();
            }
            mInitialized = true;
        }
    }      
 void init() {
    
    
        if (mEvaluator == null) {
    
    
       
            mEvaluator = (mValueType == Integer.class) ? sIntEvaluator :
                    (mValueType == Float.class) ? sFloatEvaluator :
                    null;
        }
        if (mEvaluator != null) {
    
    
       
            mKeyframes.setEvaluator(mEvaluator);
        }
    }	

其中 mValues[i].init(); mValues是PropertyValuesHolder[] mValues;数组。调用了PropertyValuesHolder的init方法
init()主要是为了设置估值器如果之前写的是ofInt这里setEvaluato的就是IntEvaluator 如果是ofFloat就会传入floatEvaluator会根据插值器和开始终止值来返回当前值。
接下来会调用setCurrentPlayTime()方法

public void setCurrentPlayTime(long playTime) {
    
    
        float fraction = mDuration > 0 ? (float) playTime / mDuration : 1;
        setCurrentFraction(fraction);
    }
 public void setCurrentFraction(float fraction) {
    
    
        initAnimation();
      ......
        animateValue(currentIterationFraction);
    }    

开始还是调用了initAnimation()方法,因为之前已经调用过里面的mInitialized = true;已经被调用所以初始化代码将不会被执行。最后通过调用animateValue(currentIterationFraction);方法来开始动画,首先会调用子类的方法

ObjectAnimation的animateValue方法

void animateValue(float fraction) {
    
    
      ........
        super.animateValue(fraction);	//首先调用父类的animateValue方法
        int numValues = mValues.length;
        
        for (int i = 0; i < numValues; ++i) {
    
    
            mValues[i].setAnimatedValue(target);
        }
    }


//  super.animateValue(fraction);即ValueAnimator.animateValue
void animateValue(float fraction) {
    
    
  		//获取插值器
        fraction = mInterpolator.getInterpolation(fraction);
        mCurrentFraction = fraction;
        int numValues = mValues.length;
        //通过插值器获取值给估值器计算出最终值
        for (int i = 0; i < numValues; ++i) {
    
    
            mValues[i].calculateValue(fraction);
        }
          //通知
        if (mUpdateListeners != null) {
    
    
            int numListeners = mUpdateListeners.size();
            for (int i = 0; i < numListeners; ++i) {
    
    
                mUpdateListeners.get(i).onAnimationUpdate(this);
            }
        }
    }

父类方法走完之后会记录走子类 void animateValue(float fraction) 方法的下面代码最终会走到子类的 mValues[i].setAnimatedValue(target);方法

void setAnimatedValue(Object target) {
    
    
        if (mProperty != null) {
    
    
            mProperty.set(target, getAnimatedValue());
        }
        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());
            }
        }
    }

这块就是 通过反射机制来修改属性,分析到这里,动画的一帧就结束了。那么接下来是怎么一帧一帧的调用的呢。之前设置的addAnimationCallback(0);方法。其中有一个方法是doAnimationFrame();来看一下它的实现是在父类当中实现的

public final boolean doAnimationFrame(long frameTime) {
    
    
        if (mStartTime < 0) {
    
    
           
            mStartTime = mReversing ? frameTime : frameTime + (long) (mStartDelay * sDurationScale);
        }
        if (mPaused) {
    
    
            mPauseTime = frameTime;
            removeAnimationCallback();
            return false;
        } else if (mResumed) {
    
    
            mResumed = false;
            if (mPauseTime > 0) {
    
    
                
                mStartTime += (frameTime - mPauseTime);
            }
        }
        if (!mRunning) {
    
    
            
            if (mStartTime > frameTime && mSeekFraction == -1) {
    
    
                
                return false;
            } else {
    
    
               
                mRunning = true;
                startAnimation();
            }
        }
        if (mLastFrameTime < 0) {
    
    
            if (mSeekFraction >= 0) {
    
    
                long seekTime = (long) (getScaledDuration() * mSeekFraction);
                mStartTime = frameTime - seekTime;
                mSeekFraction = -1;
            }
            mStartTimeCommitted = false; 
        }
        mLastFrameTime = frameTime;
      
        final long currentTime = Math.max(frameTime, mStartTime);
        boolean finished = animateBasedOnTime(currentTime);

        if (finished) {
    
    
            endAnimation();
        }
        return finished;
    }

直到动画结束之前都会不断地循环调用doAnimationFrame()来实现动画。

六.ViewPropertyAnimator的用法

在绝大多数情况下,属于动画的使用主要都还是对View进行动画操作的。
ViewPropertyAnimator这个机制可以为View的动画操作提供一种更加便捷的用法。
 
先体验一下:
比如我们想要让一个TextView从常规状态变成透明状态,就可以这样写:

ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "alpha", 0f);
animator.start();

使用ViewPropertyAnimator:

textview.animate().alpha(0f);

说明:textview.animate()这个方法的返回值是一个ViewPropertyAnimator对象,也就是说拿到这个对象之后我们就可以调用它的各种方法来实现动画效果了,这里我们调用了alpha()方法并转入0,表示将当前的textview变成透明状态。

 
除此之外,ViewPropertyAnimator还可以很轻松地将多个动画组合到一起,比如我们想要让textview运动到500,500这个坐标点上,就可以这样写:

textview.animate().x(500).y(500);

 
如果要添加插值器:

textview.animate().x(500).y(500).setDuration(5000)
		.setInterpolator(new BounceInterpolator());

 
关于ViewPropertyAnimator的几个细节:

  • 整个ViewPropertyAnimator的功能都是建立在View类新增的animate()方法之上的,这个方法会创建并返回一个ViewPropertyAnimator的实例,之后的调用的所有方法,设置的所有属性都是通过这个实例完成的。
  • 在使用ViewPropertyAnimator时,我们自始至终没有调用过start()方法,这是因为新的接口中使用了隐式启动动画的功能,只要我们将动画定义完成之后,动画就会自动启动。并且这个机制对于组合动画也同样有效,只要我们不断地连缀新的方法,那么动画就不会立刻执行,等到所有在ViewPropertyAnimator上设置的方法都执行完毕后,动画就会自动启动。当然如果不想使用这一默认机制的话,我们也可以显式地调用start()方法来启动动画。
  • ViewPropertyAnimator的所有接口都是使用连缀的语法来设计的,每个方法的返回值都是它自身的实例,因此调用完一个方法之后可以直接连缀调用它的另一个方法,这样把所有的功能都串接起来,我们甚至可以仅通过一行代码就完成任意复杂度的动画功能。

七.参考博客

Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法
Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法
属性动画的工作原理

八.demo

动画学习Demo

猜你喜欢

转载自blog.csdn.net/haazzz/article/details/114705351