• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Android面试题高级UI面试题——动画的分类以和区别属性动画的原理是怎么样的

武飞扬头像
前小小
帮助1

一、动画的分类以及区别

这道题想考察什么?

  1. 是否了解安卓的动画?

考察的知识点

  1. 动画的分类
  2. 动画的区别

考生应该如何回答

动画的种类

首先回答下安卓的动画种类,一共分为三种

  • 帧动画:由数张图片连续播放产生的动画效果。
  • 补间动画:对View的平移,旋转,缩放,透明产生效果;
  • 属性动画:动态的改变属性产生动画效果;

动画的区别

帧动画

帧动画其实就是简单的收集N张图片,然后依次显示这些图片。由于人眼"视觉暂留"的原因,会让我们造成动画的"错觉"。视觉暂留是指我们看到的画面,会在大脑中停留短暂的时间而不立即消失。

帧动画的特点:帧动画不会改变控件的属性,只是通过播放图片来达到动画效果,制作简单但效果单一,且占用空间较大。

补间动画

补间动画会先声明好两个关键帧,开始帧和结束帧。开始帧是用来描述动画开始时的状态,结束帧是用来描述动画结束时的状态,而动画中间如何由开始帧演变到结束帧是由系统计算而来。

补间动画的特点:安卓中补间动画只改变了View的显示效果,而未改变View的真正属性值。比如说,我们使用补间动画放大View的宽度,在动画运行中View宽度变大了,但仅仅只是显示效果上宽度变大了,并没有真正改变View宽度的值。

属性动画

属性动画与其他动画最大的不同是它会不断的更新View的属性值,从而产生动画效果。它不仅仅显示效果发生了改变,View的属性值也发生了变化。

属性动画和补间动画一样会声明关键帧,关键帧与关键帧之间的属性变化由系统计算而来。它们之间有个小的不同点是,属性动画可以声明一个、两个或者多个关键帧,而补间动画固定是开始帧和结束帧。


二、属性动画的原理是怎么样的?

这道题想考察什么?

这道题想考察同学对 动画 的理解。

考生应该如何回答

属性动画的原理很简单,可以用一句话描述:最开始先设定好动画的基本属性信息,然后属性的数值按照设定逻辑变化并刷新视图(变化的属性会带来视图效果的变化),接着会判断动画是否达到结束条件,如果没有达到则重复数值的变化和视图刷新直到结束。

具体的细节我们可以看后面的分析。

属性动画可以分解为以下五个步骤:

  1. 设置动画的基本信息,包括运行时间、动画效果、开始值、结束值。
  2. 设置属性值的变化逻辑,包括插值器、估值器。
  3. 根据变化的逻辑不断的改变值
  4. 值改变后,就赋给对象的属性
  5. 调用invalidate刷新视图,并且判断当前的数值是否为结束值。如果当前值为结束值,则动画结束。如果当前值不等于结束值则重复第4步。

学新通

属性动画原理详解

属性动画的重要类

关键帧KeyFrame

关键帧KeyFrame的注释是"This class holds a time/value pair for an animation",它包含时间和数值两个属性,用来描述动画运行中多个画面中的一个。KeyFrame的时间属性描述了这个画面发生在整个动画的什么时刻,keyFrame的数值属性描述了当前时刻属性的值。

在属性动画的创建过程中,我们会设定开始值和结束值。在框架内部,开始值和结束值最终会转化成为关键帧KeyFrame。

 public static KeyframeSet ofFloat(float... values) {
        boolean badValue = false;
        int numKeyframes = values.length;
        FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)];
        if (numKeyframes == 1) {
            keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
            keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]);
            if (Float.isNaN(values[0])) {
                badValue = true;
            }
        } else {
            keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]);
            for (int i = 1; i < numKeyframes;   i) {
                keyframes[i] =
                        (FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]);
                if (Float.isNaN(values[i])) {
                    badValue = true;
                }
            }
        }
        if (badValue) {
            Log.w("Animator", "Bad value (NaN) in float animator");
        }
        return new FloatKeyframeSet(keyframes);
    }

从上述代码过程中可以看到,参数values最终会转化为KeyFrame关键帧。这个里面会有两种情况,一种情况是values长度为一,另外一种是大于一。为一的情况,会生成开始帧和结束帧两个关键帧,开始帧数据都为0,结束帧的数据是传入的参数。不为一的情况,就是比较简单,values长度为多少就生成多少的关键帧。

PropertyValuesHolder

ObjectAnimator只能对单个属性进行操作,如果想实现比较复杂的效果就需要用到PropertyValuesHolder了。

PropertyValuesHolder的注释是"This class holds information about a property and the values that that property should take on during an animation",它包含了属性,以及该属性在动画运行期间的值。简单来讲就是,包含了属性名和多个关键帧。属性名表明了是哪个属性需要发生变化,关键帧表明动画该如何变化。 学新通

从上图看来,PropertyValuesHolder包含了属性名和多个关键帧。这里面属性是透明度,描述的是透明度的动画变化,在实际运用过程中可以换成其他属性。KeyFrame中的fraction代表的是时间、value表示数值,三个KeyFrame表示三个时间点的属性值。

动画基本信息设定

var objectAnimation: ObjectAnimator = ObjectAnimator.ofFloat(textView, "translationY", 0f, 400f)
objectAnimation.setDuration(5000)
objectAnimation.interpolator = DecelerateInterpolator()
objectAnimation.start()

如上面代码所示,我们设定了一个平移的动画。该动画作用于textview这个控件上,从0f的位置平移至400f的位置。动画的总共耗时5000ms,并且运用了DecelerateInterpolator动画变化速率越来越慢的插值器。这样一来我们把动画的目标控件、开始值、结束值、动画时间以及变化逻辑插值器都设定好了,包括了本章节开头提到的步骤一和步骤二的内容。

接下来我们研究一下代码内部的变化。首先通过ofFloat会生成一个ObjectAnimator对象,然后根据传入的属性值生成PropertyValuesHodler,接着根据传入的数值生成对应的关键帧,再设置相关的动画时间、插值器,整个动画的信息就设置完毕。

学新通

Start方法开启动画

调用start方法整个动画就开始运作起来,最终会进入了ValueAnimator的start方法。ValueAnimator#start方法中三件重要的事情:一是初始化估值器;二是计算属性值并设置到控件上;三是注册同步信号;

初始化估值器是通过调用startAnimation(),最后了会调用到initAnimation方法。在initAnimation方法里面会遍历PropertyValuesHolder,并逐个进行执行init。

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

其中mValues代表的是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);
        }
    }

系统会判断当前是否有估值器对象,如果没有会根据mValueType的类型默认分配一个,接着就是简单的设置。

计算属性值是通过调用setCurrentPlayTime方法,最终会调用到animateValue()。在animateValue()中,会先通过插值器mInterpolator拿到当前动画的完成度,然后把完成度给到每一个PropertyValuesHolder用于计算动画的属性值。

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

mValues[i].calculateValue最终会调用到KeyFrameSet的getValue方法,看完这个代码其实一切都很明了了。

public Object getValue(float fraction) {
    if (mNumKeyframes == 2) {
        if (mInterpolator != null) {
            fraction = mInterpolator.getInterpolation(fraction);
        }
        return mEvaluator.evaluate(fraction, mFirstKeyframe.getValue(),
                    mLastKeyframe.getValue());
    }
    ...
	for (int i = 1; i < mNumKeyframes;   i) {
            Keyframe nextKeyframe = mKeyframes.get(i);
            if (fraction < nextKeyframe.getFraction()) {
                final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
                final float prevFraction = prevKeyframe.getFraction();
                float intervalFraction = (fraction - prevFraction) /
                    (nextKeyframe.getFraction() - prevFraction);
                // Apply interpolator on the proportional duration.
                if (interpolator != null) {
                    intervalFraction = interpolator.getInterpolation(intervalFraction);
                }
                return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(),
                        nextKeyframe.getValue());
            }
            prevKeyframe = nextKeyframe;
        }
   ...
}

上述代码中分了两种情况,一种情况是关键帧数目为二,一种不为二。为二的情况会传入完成度fraction、开始帧的值、结束帧的值给到估值器,由估值器最终计算数据。不为二的情况会通过fraction完成度寻找最合适的前帧prevKeyframe和后帧nextKeyframe,之后进行的内容就和关键帧为二的情况十分类似,通过完成度和前帧数值、后帧数值计算当前的属性值。

设置属性值的代码在Object的animateValue方法:

int numValues = mValues.length;
        for (int i = 0; i < numValues;   i) {
            mValues[i].setAnimatedValue(target);
 }

上面代码其实是简单的遍历PropertyValuesHolder,然后给每一个PropertyValuesHolder设置动画数值。属性动画是通过反射的方式设置数值的,在具体代码中表现是先拿取到属性set方法的Method对象,当需要使用的时候直接invoke。

void setAnimatedValue(Object target) {
           ...
            if (mSetter != null) {
                try {
                    mTmpValueArray[0] = mIntAnimatedValue;
                    //反射设置属性
                    mSetter.invoke(target, mTmpValueArray);
                } catch (InvocationTargetException e) {
                    Log.e("PropertyValuesHolder", e.toString());
                } catch (IllegalAccessException e) {
                    Log.e("PropertyValuesHolder", e.toString());
                }
            }
            ...
        }

当调用ValueAnimator的start方法的时候,则会开始注册同步服务信号。注册同步服务信号其实就是向系统请求刷新屏幕。ValueAnimator的start方法最终会调用到MyFrameCallbackProvider的postFrameCallback方法,如下所示:

 public void postFrameCallback(Choreographer.FrameCallback callback) {
            mChoreographer.postFrameCallback(callback);
}

mChoreographer是编舞者,管理app端向系统端申请同步服务信号的事情。当系统执行同步,也就是刷新的时候,会回调callback。我们来看下callback中的代码。

 private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            doAnimationFrame(getProvider().getFrameTime());
            //判断动画是否结束
            if (mAnimationCallbacks.size() > 0) {
                getProvider().postFrameCallback(this);
            }
        }
    };

callback中的代码就只有两个内容:一个是执行doAnimationFrame,主要是执行新一轮的数值计算;另外一个是判断动画是否满足结束条件。

doAnimationFrame函数最终会调用到ValueAnimator#animateValue方法,其实也就是执行新一轮的动画的数值计算,先通过插值器、时间算出完成度,然后通过估值器、前后帧的值算出动画的数值,最后设置到控件上。

mAnimationCallbacks.size() > 0则说明动画未结束,继续申请一下次的同步服务信号,也就是需要继续刷新界面。不满足结束条件就继续刷新,执行回调的时候又判断是否需要继续刷新。类似于一种循环的方式,不断的推动动画的运行,直至满足条件动画结束。

总结

综上所述,你可以把动画的原理看中两个部分:一个部分与数据计算有关,另外一个部分与刷新界面有关。数据计算有关的部分,包括到关键帧、插值器、估值器、动画时间,它控制着动画运行过程中每个画面的数值。刷新界面有关就是不断的向系统申请刷新,当系统下发刷新的时候先运行动画的数据计算,然后判断动画是否结束。如果结束则不再想系统申请刷新,如果没有结束继续申请,直至满足条件结束。

详细关注公众号:Android老皮
还能解锁  《Android十大板块文档》 ,让学习更贴近未来实战。已形成PDF版

内容如下

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgiiffa
系列文章
更多 icon
同类精品
更多 icon
继续加载