Android 动画插值器
前言
插值器定义了如何根据动画当前已播放时间来计算属性的值。例如,您可以指定动画在整个动画中线性播放,即动画在整个过程中均匀移动,也可以指定动画使用非线性时间,例如在动画开始时加速,在动画结束时减速。
Android 动画插值器
Android 提供了多种内置的插值器,可以帮助你实现各种不同的动画效果。这些插值器皆是 TimeInterpolator接口的实现。
package android.animation; /** * TimeInterpolator 定义动画的变化率。这允许动画以具有诸如加速和减速之类的非线性运动。 */public interface TimeInterpolator { /** * 动画进度映射到插值计算公式,导出当前动画播放时间的动画值 * @param input:输入在 [0,1], 0 代表开始,1 代表结束 * @return 基于插值计算公式求得的值 */ float getInterpolation(float input);}
插值器定义动画的变化率。这允许对基本动画效果(alpha、缩放、平移、旋转)进行加速、减速、重复等操作。这些内置插值器说明如下
| 类/接口 | 说明 |
|---|---|
| AccelerateDecelerateInterpolator | 该插值器的变化率在开始和结束时缓慢,但在中间会加快。 |
| AccelerateInterpolator | 该插值器的变化率在开始时较为缓慢,然后会加快。 |
| AnticipateInterpolator | 该插值器先反向变化,然后再急速正向变化。 |
| AnticipateOvershootInterpolator | 该插值器先反向变化,再急速正向变化并超过目标值,然后最终回到最终值。 |
| BounceInterpolator | 该插值器的变化会跳过结尾处。 |
| CycleInterpolator | 该插值器的动画会在指定数量的周期内重复。 |
| DecelerateInterpolator | 该插值器的变化率开始时很快,然后减慢。 |
| LinearInterpolator | 该插值器的变化率恒定不变。 |
| OvershootInterpolator | 该插值器的变化先急速正向,再超过最终值,然后返回结果。 |
| PathInterpolator | 该插值器让动画按照构造出的曲线来执行。 |
| TimeInterpolator | 该接口用于实现您自己的插值器。 |
示例程序
下面通过一个示例,演示这些内置插值器的特性,示例代码如下
/** * 演示常用插值器的运动轨迹 */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 插值器的特点是两端速度较慢,中间加速


源码中的实现
float AccelerateDecelerateInterpolator::interpolate(float input) { return (float)(cosf((input + 1) * M_PI) / 2.0f) + 0.5f;}AccelerateInterpolator
AccelerateInterpolator 插值器的特点是先慢后快


源码中的实现
float AccelerateInterpolator::interpolate(float input) { if (mFactor == 1.0f) { return input * input; } else { return pow(input, mDoubleFactor); }}AnticipateInterpolator
AnticipateInterpolator 运动曲线先向后超过临界值,再快速向前,像一个回荡的秋千。这种效果使得动画呈现出回弹的视觉效果。


源码中的实现
float AnticipateInterpolator::interpolate(float t) { return t * t * ((mTension + 1) * t - mTension);}AnticipateOvershootInterpolator
AnticipateOvershootInterpolator 运动曲线先向后超过临界值,然后快速向前,最后回到最终值。这种效果使得动画呈现出回弹的视觉效果。


源码中的实现
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 运动曲线快速运动到临界值后,进行几次回跳,类似一个从高空坠落篮球的运动曲线。

源码中的实现
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 运动曲线会重复指定的循环次数,类似正弦波的形状。

源码中的实现
// 上图中 mCycles = 1ffloat CycleInterpolator::interpolate(float input) { return sinf(2 * mCycles * M_PI * input);}DecelerateInterpolator
DecelerateInterpolator 运动曲线从快速开始,然后逐渐减速。你可以通过设置 factor 参数来调整它的行为。

源码中的实现
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 运动曲线是一条直线。

源码中的实现
class LinearInterpolator : public Interpolator {public: virtual float interpolate(float input) override { return input; }};OvershootInterpolator
OvershootInterpolator 运动曲线先加速超过临界值 1.0,然后慢慢又回落到 1.0,呈现出回弹的视觉效果。

源码中的实现
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) 为控制点的贝塞尔曲线。

源码中的实现
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 mathematica 、geogebra 在图形、几何、代数、3D、统计和概率等方面的强大的表达能力,随着使用的增多,和相关知识积累,结合 procressing 代码生成艺术,这个想法或可一试。