Android高级UI面试题汇总(含详细解析 十七)

本文详细解读了Android属性动画的工作原理,涉及关键帧、PropertyValuesHolder、ObjectAnimator的使用,以及动画设置、计算和刷新过程。同时提及了并发编程在面试中的重要性。

Android并发编程高级面试题汇总最全最细面试题讲解持续更新中👊👊
👀你想要的面试题这里都有👀
👇👇👇

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

这道题想考察什么?

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

考生应该如何回答

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

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

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

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

总结

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

由于面试题内容比较多,篇幅有限,资料已经被整理成了PDF文档,有需要2023年Android中高级最全面试真题答案 完整文档的可扫描下方卡片免费获取~

PS:(文末还有使用ChatGPT机器人小福利哦!!大家不要错过)

目录

img

第一章 Java方面

  • Java基础部分
  • Java集合
  • Java多线程
  • Java虚拟机

img

第二章 Android方面

  • Android四大组件相关
  • Android异步任务和消息机制
  • Android UI绘制相关
  • Android性能调优相关
  • Android中的IPC
  • Android系统SDK相关
  • 第三方框架分析
  • 综合技术
  • 数据结构方面
  • 设计模式
  • 计算机网络方面
  • Kotlin方面

img

第三章 音视频开发高频面试题

  • 为什么巨大的原始视频可以编码成很小的视频呢?这其中的技术是什么呢?
  • 怎么做到直播秒开优化?
  • 直方图在图像处理里面最重要的作用是什么?
  • 数字图像滤波有哪些方法?
  • 图像可以提取的特征有哪些?
  • 衡量图像重建好坏的标准有哪些?怎样计算?

img

第四章 Flutter高频面试题

  • Dart部分
  • Flutter部分

img

第五章 算法高频面试题

  • 如何高效寻找素数
  • 如何运用二分查找算法
  • 如何高效解决雨水问题
  • 如何去除有序数组的重复元素
  • 如何高效进行模幂运算
  • 如何寻找最长回文子串

img

第六章 Andrio Framework方面

  • 系统启动流程面试题解析
  • Binder面试题解析
  • Handler面试题解析
  • AMS面试题解析

img

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值