android属性动画实践及原理

动画

首先明确啥是动画?你可以理解成若干不同图片随着时间不断交替成为焦点的过程,说简单点就是"量子阅读法",你还可以把动画理解成使用某多种手段将一张图片进行各种变换。那么这两种理解方式对应android里的动画就是帧动画和属性动画,当然帧动画和补间动画属于视图动画,所以android动画可以分为两大类:视图动画和属性动画

View动画操作对象是view,而属性对象操作的是任何对象甚至是无对象。
View动画基本上只支持4中动画效果,也就是最简单的平移、旋转、缩放、透明度。当然如果你不满足这4中基本动画效果,你也可以自定义view效果,基本思路就是继承Animation抽象性类,重写两个方法:initialize和applyTransformation方法,在applyTransformation中进行相关矩阵变换,为了简化matrix变化,使用camera简化变换过程。总体来说自定义View动画不是很难,而且套路比较固定。
当然view动画也不是一无是处,他在viewGroup中控制子view的出场退场效果,他在Activity中实现Activity之间的切换动画。

引用一个大佬对view动画的总结:

View动画还有一个致命的缺陷,就是它只是改变了View的显示效果而已,而不会真正去改变View的属性。什么意思呢?比如说,现在屏幕的左上角有一个按钮,然后我们通过补间动画将它移动到了屏幕的右下角,现在你可以去尝试点击一下这个按钮,点击事件是绝对不会触发的,因为实际上这个按钮还是停留在屏幕的左上角,只不过补间动画将这个按钮绘制到了屏幕的右下角而已。

下面我们重点介绍属性动画,它在实际中使用比较多。

属性动画

字面上的意思就是动态改变对象的属性值来实现动画效果,比如,让某个对象的宽度在10秒内增大50dp。别看这句话很短,但是实现起来需要考虑一下几个问题:

  1. 判断对象是否有这个”宽度“这个属性?若有怎么获取?若没有又怎么解决?
  2. 如何控制对象的宽度从原始宽度在10秒之内遍到指定宽度?匀速?变速?
  3. 如何检测感知对象的宽度变化了?

下面我们带着这几个思考继续开始学习吧
常用的几种属性动画有:ValueAnimator、ObjectAnimator、AnimatorSet类。其中ObjectAnimator继承了ValueAnimator。ValueAnimator是属性动画的核心类

ValueAnimator

属性动画的运行机制是通过不断地对属性值进行操作来实现的,而初始值和结束值之间的动画过渡就是由ValueAnimator这个类来负责计算的。它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,我们只需要将初始值和结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,那么ValueAnimator就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果。除此之外,ValueAnimator还负责管理动画的播放次数、播放模式、以及对动画设置监听器等,确实是一个非常重要的类。
ValueAnimator使用起来十分简单:
比如我想将一个值从0变到1,时长5000ms,就可以这样写:

ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
anim.setDuration(5000);
anim.start();

这里我们可以回答开头提出来的第2点思考,如何控制这个速度,如果我不想他变化很快或者太慢。android提供了一个叫做插值器的东东Interpolator,他用来控制改变属性值得变化速率。常见的有AccelerateDecelerateInterpolator(开始和结束变化慢、中间变化快)、LinearInterpolator(匀速变化)、BounceInterpolator(结束的时候出现反弹变化效果) 。说白了,插值器通过数学函数将一段时间段进行(非)线性流逝,从而实现渐变的动画效果。

anim.setInterpolator(new BounceInterpolator());//反弹插值器

接下来我们可以回答开头提出来的第3点思考,如何感知对象属性变化了?只有感知变化了我才好决定去做变化依赖的操作,比如我要实现一个button的宽度从0变化到100dp,ValueAnimator类不断set这个button的宽度属性,而没有进行其他改变button宽度的行为,所以逻辑时先得监听到属性值发生变化了然后去在ui层面上改变button的宽度。
好在android可以在添加监听器AnimatorUpdateListener实现对属性值的监听:

anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
      	//获取当前width变化到的值
        curWidth= (int) anim.getAnimatedValue();
        //更新ui
        invalidate();
      }
    });

所以ValueAnimator只需要我们提供起始值、终点值已经自己选取一个插值器(也可以自己定义,android已经做到完全不需要我们自己定义了),最后提供一个自定义的监听器实现属性值改变后的后操作。

ObjectAnimator

相比ValueAnimator,ObjectAnimator可能是我们开发中最常使用到的,因为ValueAnimator提供的ObjectAnimator都能实现,ObjectAnimator可以对任何对象直接做任何操作,这就是他强大之处。
比如我要对一个对象向右平移一个对象宽度:

ObjectAnimator.ofFloat(targetObj,"translationX",targetObj.getWidth()).setDuration(5000).setInterpolator(new BounceInterpolator()).start();

是不是感觉代码十分清爽,ObjectAnimator也不需要像ObjectAnimator需要设置还设置什么属性监听器,他做到了对对象属性值改变、动画时长、插值器设置链式构造。
当然ObjectAnimator也提供了用于监听动画播放过程的监听器AnimatorListener/AnimatorAdaptor,用于对动画开始、结束、取消等一些状态的回调。
下面我们可以回答第2点思考,ObjectAnimator是如何对对象进行属性赋值的?答案是通过调用该对象的getter和setter方法,getter方法可以不用提供,但是如何用户没有提供对象的初始值,那么系统就会使用getter方法找对象的初始值,若没getter方法就会报错。

所以这就可以解释得通为啥button并没有width这个属性值,但还是可以使用动画作用于width上,那是因为他有getWidth和setWidth方法。
我们借用《Android开发艺术探索》中的例子,改变button的宽度:点击button,改变button宽度:

@Override
  public void onClick(View v) {
    switch (v.getId()){
      case R.id.bt_button:
        changeButton();
        break;
    }
  }

  private void changeButton() {
    ObjectAnimator.ofInt(bt,"width",500).setDuration(5000).start();
  }

运行后点击button并没有反应,那是咋会事?
因为button提供的setWidth方法并不是改变的button宽度的直接方法,所以 ObjectAnimator.ofInt(bt,"width",500).setDuration(5000).start();显然不对(直接调用setWidth方法)。那么遇到这种摸不着头脑的情况,官方给出了3中方法:

  1. 给你的object直接设置getter和setter,前提你有权限
  2. 用一个包装类,间接提供getter和setter
  3. 用ValueAnimator的属性监听器,手动set属性值

显然大部分时第一种方法并不靠谱,最简单的就是第二种方式,这里给出:

private void changeButton() {
    ViewWapper wapper=new ViewWapper(bt);
    ObjectAnimator.ofInt(wapper,"width",500).setDuration(5000).start();
  }
  //定义一个button的包装类
  class ViewWapper{
    private View mView;
    public ViewWapper(View mView){
      this.mView=mView;
    }
    public int getWidth(){
      return mView.getLayoutParams().width;
    }
    public void setWidth(int width){
      mView.getLayoutParams().width=width;
      mView.requestLayout();
    }
  }

看懂了ValueAnimator原理,我们也可以用监听器实现:

private void changeButton() {
	 ValueAnimator animator = ValueAnimator.ofInt(bt.getLayoutParams().width, bt.getLayoutParams().width+500);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        bt.getLayoutParams().width=(int) animation.getAnimatedValue();
        bt.requestLayout();
      }
    });
    animator.setDuration(5000);
    animator.setInterpolator(new LinearInterpolator());
    animator.start();
}

至此,动画如何操作对象属性的流程及原理我们都弄清楚了。

AnimatorSet

最后一个类,他是提供对ObjectAnimator的集合,实现对目标各种不同的操作方式。

AnimatorSet set=new AnimatorSet();
set.playTogether(
	ObjectAnimator.ofInt(obj,"rotationX",0,360),
	ObjectAnimator.ofInt(obj,"rotationY",0,360),
	ObjectAnimator.ofInt(obj,"translationX",0,100),
	ObjectAnimator.ofInt(obj,"translationY",0,100),
	...
	);
set.setDuration(3000).start();

源码解析

所有的动画类都是通过.start()方法开始的,我们以ObjectAnimator.ofInt(wapper,"width",500).setDuration(5000).start()
为例,进入ObjectAnimator中的start()方法:

#ObjectAnimator
public void start() {
		//将与当前动画相同的动画取消掉
        AnimationHandler.getInstance().autoCancelBasedOn(this);
        //下面不用管,都是日志操作
        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));
            }
        }
        //调用ValueAnimator的start方法
        super.start();
    }
#ValueAnimator
private void start(boolean playBackwards) {
		//这里表示属性动画得运行在looper线程中,否则抛异常
        if (Looper.myLooper() == null) {
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        mReversing = playBackwards;
        mSelfPulse = !mSuppressSelfPulseRequested;
        // Special case: reversing from seek-to-0 should act as if not seeked at all.
        if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
            if (mRepeatCount == INFINITE) {
                // Calculate the fraction of the current iteration.
                float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
                mSeekFraction = 1 - fraction;
            } else {
                mSeekFraction = 1 + mRepeatCount - mSeekFraction;
            }
        }
        mStarted = true;
        mPaused = false;
        mRunning = false;
        mAnimationEndRequested = false;
        // Resets mLastFrameTime when start() is called, so that if the animation was running,
        // calling start() would put the animation in the
        // started-but-not-yet-reached-the-first-frame phase.
        mLastFrameTime = -1;
        mFirstFrameTime = -1;
        mStartTime = -1;
        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);
            }
        }
    }

代码最好调用了setCurrentPlayTime或者是setCurrentFraction方法,区别就是是否立刻进行动画操作还是延时一下,

public void setCurrentFraction(float fraction) {
		//初始化
        initAnimation();
        fraction = clampFraction(fraction);
        mStartTimeCommitted = true; // do not allow start time to be compensated for jank
        if (isPulsingInternal()) {
            long seekTime = (long) (getScaledDuration() * fraction);
            long currentTime = AnimationUtils.currentAnimationTimeMillis();
            // Only modify the start time when the animation is running. Seek fraction will ensure
            // non-running animations skip to the correct start time.
            mStartTime = currentTime - seekTime;
        } else {
            // If the animation loop hasn't started, or during start delay, the startTime will be
            // adjusted once the delay has passed based on seek fraction.
            mSeekFraction = fraction;
        }
        mOverallFraction = fraction;
        final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);
        animateValue(currentIterationFraction);
    }

首先进行初始化,对每一个mValues元素初始化,mValues是一个PropertyValuesHolder对象

void initAnimation() {
        if (!mInitialized) {
            int numValues = mValues.length;
            for (int i = 0; i < numValues; ++i) {
                mValues[i].init();
            }
            mInitialized = true;
        }
    }

在PropertyValuesHolder中

#PropertyValuesHolder
void init() {
        if (mEvaluator == null) {
            // We already handle int and float automatically, but not their Object
            // equivalents
            mEvaluator = (mValueType == Integer.class) ? sIntEvaluator :
                    (mValueType == Float.class) ? sFloatEvaluator :
                    null;
        }
        if (mEvaluator != null) {
            // KeyframeSet knows how to evaluate the common types - only give it a custom
            // evaluator if one has been set on this class
            mKeyframes.setEvaluator(mEvaluator);
        }
    }

就是初始化计算器Evaluator,如果是动画参数是Integer就将计算器设置为IntEvaluator,若是float型就设置FloatEvaluator,若都不是就让用户自己去定义计算器。
初始化完毕后,最后落到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);
            }
        }
    }

后面给对象属性赋值的逻辑很乱,但是可以知道,之后通过反射机制来调用对象的getter和setter方法

最后

通过属性动画控制对象动画十分简洁,并没有特别难的思想在里面,唯一一个就是监听对象属性值得变化而已。

你可能感兴趣的:(android学习)