Cursor blinking

Android 动画插值器

Android 基础|字数 1,485|阅读时长≈ 4 分钟

前言

插值器定义了如何根据动画当前已播放时间来计算属性的值。例如,您可以指定动画在整个动画中线性播放,即动画在整个过程中均匀移动,也可以指定动画使用非线性时间,例如在动画开始时加速,在动画结束时减速。

Android 动画插值器

Android 提供了多种内置的插值器,可以帮助你实现各种不同的动画效果。这些插值器皆是 TimeInterpolator接口的实现。

Code
package android.animation; /** * TimeInterpolator 定义动画的变化率。这允许动画以具有诸如加速和减速之类的非线性运动。 */public interface TimeInterpolator {     /**     * 动画进度映射到插值计算公式,导出当前动画播放时间的动画值     * @param input:输入在 [0,1], 0 代表开始,1 代表结束     * @return 基于插值计算公式求得的值     */    float getInterpolation(float input);}
截屏2024-05-13 13.54.29.png
截屏2024-05-13 13.54.29.png

插值器定义动画的变化率。这允许对基本动画效果(alpha、缩放、平移、旋转)进行加速、减速、重复等操作。这些内置插值器说明如下

类/接口说明
AccelerateDecelerateInterpolator该插值器的变化率在开始和结束时缓慢,但在中间会加快。
AccelerateInterpolator该插值器的变化率在开始时较为缓慢,然后会加快。
AnticipateInterpolator该插值器先反向变化,然后再急速正向变化。
AnticipateOvershootInterpolator该插值器先反向变化,再急速正向变化并超过目标值,然后最终回到最终值。
BounceInterpolator该插值器的变化会跳过结尾处。
CycleInterpolator该插值器的动画会在指定数量的周期内重复。
DecelerateInterpolator该插值器的变化率开始时很快,然后减慢。
LinearInterpolator该插值器的变化率恒定不变。
OvershootInterpolator该插值器的变化先急速正向,再超过最终值,然后返回结果。
PathInterpolator该插值器让动画按照构造出的曲线来执行。
TimeInterpolator该接口用于实现您自己的插值器。

示例程序

下面通过一个示例,演示这些内置插值器的特性,示例代码如下

Code
/** * 演示常用插值器的运动轨迹 */class InterpolatorFragment : BaseFragment(R.layout.fragment_interpolator) {    override val binding: FragmentInterpolatorBinding by viewBinding()     private var curInterpolator: Interpolator = LinearInterpolator()     // 动画持续时间,1 秒    private var duration = 1000L     // 持续时间倒数    private var durationReciprocal = 0f     // 动画开启的时间    private var startTime: Long = 0     // 编舞器,用来模拟动画    private val choreographer = Choreographer.getInstance()     override fun initViews() {        super.initViews()        durationReciprocal = 1 / duration.toFloat()    }     override fun bindListener() {        super.bindListener()        binding.btnStart.setOnClickListener {            startAnim()        }        binding.mcvCard.setOnClickListener {            showInterpolatorMenu(binding.tvName)        }    }     private fun showInterpolatorMenu(v: View) {        val popup = PopupMenu(v.context, v)        popup.menuInflater.inflate(R.menu.interpolator_menu, popup.menu)        popup.setOnMenuItemClickListener { menuItem: MenuItem ->            val menuItemTitle = menuItem.title.toString()            binding.tvName.text = menuItemTitle            curInterpolator = when (menuItemTitle) {                "AccelerateDecelerateInterpolator" -> AccelerateDecelerateInterpolator()                "AccelerateInterpolator" -> AccelerateInterpolator()                "AnticipateInterpolator" -> AnticipateInterpolator()                "AnticipateOvershootInterpolator" -> AnticipateOvershootInterpolator()                "BounceInterpolator" -> BounceInterpolator()                "CycleInterpolator" -> CycleInterpolator(1.0f)                "DecelerateInterpolator" -> DecelerateInterpolator()                "OvershootInterpolator" -> OvershootInterpolator()                "PathInterpolator" -> PathInterpolator(0.8f, 0.8f)                else -> LinearInterpolator()            }            return@setOnMenuItemClickListener true        }        popup.show()    }     /**     * 开启动画     */    private fun startAnim() {        startTime = AnimationUtils.currentAnimationTimeMillis()        binding.rgvRate.clearTrack()        postNextFrame()    }     /**     * 递归执行动画,直到时间走完     */    private fun postNextFrame() {        val timePassed: Int = (AnimationUtils.currentAnimationTimeMillis() - startTime).toInt()        if (timePassed > duration) {            return        }        // 插值值,当前进度[0,1],带入插值器求值公式求得        val output: Float = curInterpolator.getInterpolation(timePassed * durationReciprocal)        debug("timePassed=${timePassed} output=${output}")        binding.vMobile.translationY = 200.dp2px * output        binding.rgvRate.addTrackPoint(timePassed / duration.toFloat(), output)        choreographer.postFrameCallback {            postNextFrame()        }    }}

AccelerateDecelerateInterpolator

AccelerateDecelerateInterpolator 插值器的特点是两端速度较慢,中间加速

源码中的实现

Code
float AccelerateDecelerateInterpolator::interpolate(float input) {    return (float)(cosf((input + 1) * M_PI) / 2.0f) + 0.5f;}

AccelerateInterpolator

AccelerateInterpolator 插值器的特点是先慢后快

Interpolator-AccelerateInterpolator.gif
Interpolator-AccelerateInterpolator.gif
Interpolator-AccelerateInterpolator.png
Interpolator-AccelerateInterpolator.png

源码中的实现

Code
float AccelerateInterpolator::interpolate(float input) {    if (mFactor == 1.0f) {        return input * input;    } else {        return pow(input, mDoubleFactor);    }}

AnticipateInterpolator

AnticipateInterpolator 运动曲线先向后超过临界值,再快速向前,像一个回荡的秋千。这种效果使得动画呈现出回弹的视觉效果。

Interpolator-AnticipateInterpolator.gif
Interpolator-AnticipateInterpolator.gif
Interpolator-AnticipateInterpolator.png
Interpolator-AnticipateInterpolator.png

源码中的实现

Code
float AnticipateInterpolator::interpolate(float t) {    return t * t * ((mTension + 1) * t - mTension);}

AnticipateOvershootInterpolator

AnticipateOvershootInterpolator 运动曲线先向后超过临界值,然后快速向前,最后回到最终值。这种效果使得动画呈现出回弹的视觉效果。

源码中的实现

Code
static float a(float t, float s) {    return t * t * ((s + 1) * t - s);} static float o(float t, float s) {    return t * t * ((s + 1) * t + s);} float AnticipateOvershootInterpolator::interpolate(float t) {    if (t < 0.5f)        return 0.5f * a(t * 2.0f, mTension);    else        return 0.5f * (o(t * 2.0f - 2.0f, mTension) + 2.0f);}

BounceInterpolator

BounceInterpolator 运动曲线快速运动到临界值后,进行几次回跳,类似一个从高空坠落篮球的运动曲线。

Interpolator-BounceInterpolator.gif
Interpolator-BounceInterpolator.gif

源码中的实现

Code
float BounceInterpolator::interpolate(float t) {    t *= 1.1226f;    if (t < 0.3535f)        return bounce(t);    else if (t < 0.7408f)        return bounce(t - 0.54719f) + 0.7f;    else if (t < 0.9644f)        return bounce(t - 0.8526f) + 0.9f;    else        return bounce(t - 1.0435f) + 0.95f;}

CycleInterpolator

CycleInterpolator 运动曲线会重复指定的循环次数,类似正弦波的形状。

Interpolator-CycleInterpolator.gif
Interpolator-CycleInterpolator.gif

源码中的实现

Code
// 上图中 mCycles = 1ffloat CycleInterpolator::interpolate(float input) {    return sinf(2 * mCycles * M_PI * input);}

DecelerateInterpolator

DecelerateInterpolator 运动曲线从快速开始,然后逐渐减速。你可以通过设置 factor 参数来调整它的行为。

Interpolator-DecelerateInterpolator.gif
Interpolator-DecelerateInterpolator.gif

源码中的实现

Code
float DecelerateInterpolator::interpolate(float input) {    float result;    if (mFactor == 1.0f) {        result = 1.0f - (1.0f - input) * (1.0f - input);    } else {        result = 1.0f - pow((1.0f - input), 2 * mFactor);    }    return result;}

LinearInterpolator

LinearInterpolator 运动曲线是一条直线。

Interpolator-LinearInterpolator.gif
Interpolator-LinearInterpolator.gif

源码中的实现

Code
 class LinearInterpolator : public Interpolator {public:    virtual float interpolate(float input) override { return input; }};

OvershootInterpolator

OvershootInterpolator 运动曲线先加速超过临界值 1.0,然后慢慢又回落到 1.0,呈现出回弹的视觉效果。

Interpolator-OvershootInterpolator.gif
Interpolator-OvershootInterpolator.gif

源码中的实现

Code
float OvershootInterpolator::interpolate(float t) {    t -= 1.0f;    return t * t * ((mTension + 1) * t + mTension) + 1.0f;}

PathInterpolator

是 Android 5.0(API 21)中引入的一种插值器,它基于贝塞尔曲线或 Path对象。这个插值器允许你在一个 1x1 的正方形内指定一个动作曲线,其中定位点位于 (0,0) 和 (1,1),而控制点则使用构造函数参数指定。简单来说,通过控制点来构造出 (0,0) 到 (1,1) 之间的任意曲线,让动画按照构造出的曲线来执行。

下图以PathInterpolator(0f, 1f) 构造,运动曲线是以(0,0) 和 (1,1)为锚点,(0,1) 为控制点的贝塞尔曲线

Interpolator-PathInterpolator.gif
Interpolator-PathInterpolator.gif

源码中的实现

Code
float PathInterpolator::interpolate(float t) {    if (t <= 0) {        return 0;    } else if (t >= 1) {        return 1;    }    // Do a binary search for the correct x to interpolate between.    size_t startIndex = 0;    size_t endIndex = mX.size() - 1;     while (endIndex > startIndex + 1) {        int midIndex = (startIndex + endIndex) / 2;        if (t < mX[midIndex]) {            endIndex = midIndex;        } else {            startIndex = midIndex;        }    }     float xRange = mX[endIndex] - mX[startIndex];    if (xRange == 0) {        return mY[startIndex];    }     float tInRange = t - mX[startIndex];    float fraction = tInRange / xRange;     float startY = mY[startIndex];    float endY = mY[endIndex];    return startY + (fraction * (endY - startY));}

拓展

这篇博客内容准备没花费多少时间,但是写的时候遇到一些问题,特此记录一下。

  • 博客里面引入数学公式时,Notion 对此支持并不好,比如⁍ 显示上就有问题,之前写 使用四段三次 Bézier 曲线拟合圆 就遇到复杂数学公式无法录入的问题,后续找一找对符号、公式、推导过程支持比较好的工具(这样的工具一定存在,毕竟像线性代数书籍都能打印),可以开发一个对应的 Notion 插件。
  • 在做自定义 View、展示动画轨迹等场景时,需要绘制笛卡尔坐标系、网格等辅助线,后续整理起来,开发一个对应的视图组件(拓展一下,别的开发语言里面比较好用的图表库,嫁接到 Android 里面,比如 Python 里面的 Matplotlib)。
  • 导出 .gif 文件时,发现 Gifski (我能找到的 Mac 上最好用的视频转 gif 工具)不支持裁剪,我想导出方形的 .gif ,但实际只能导出屏幕尺寸的 .gif。另外这个工具也不支持一组图片转.gif。
  • 可视化数学概念、函数和算法,以前萌生过一个想法,把算法执行过程可视化(刷 leetcode 所带来痛苦后遗症),写这篇博客是发现 wolfram mathematicageogebra 在图形、几何、代数、3D、统计和概率等方面的强大的表达能力,随着使用的增多,和相关知识积累,结合 procressing 代码生成艺术,这个想法或可一试。

参考文档