You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose自定义函数实现图片偏移动画示例求助

Jetpack Compose 基于数学函数的图片偏移动画实现

一、自定义AnimationSpec实现方案

如果你需要将数学函数直接集成到动画系统中,可以自定义AnimationSpec封装曲线逻辑,以下是针对Offset类型的基础示例:

1. 自定义MathFunctionAnimationSpec

这个Spec会将动画进度(0到1)传入指定数学函数,再映射为最终偏移值:

import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.VectorizedAnimationSpec
import androidx.compose.ui.geometry.Offset

class MathFunctionAnimationSpec(
    private val durationMillis: Int = 1000,
    private val curveFunction: (Float) -> Float
) : VectorizedAnimationSpec<Offset> {

    // 将Offset转换为动画系统可处理的向量
    override fun vectorize(value: Offset): AnimationVector2D {
        return AnimationVector2D(value.x, value.y)
    }

    // 核心计算逻辑:根据播放时间计算当前偏移
    override fun getValueFromNanos(
        playTimeNanos: Long,
        initialValue: AnimationVector2D,
        targetValue: AnimationVector2D,
        initialVelocity: AnimationVector2D
    ): AnimationVector2D {
        // 将纳秒转换为0-1的进度值
        val progress = (playTimeNanos / 1e9f).coerceIn(0f, 1f)
        // 用自定义数学函数映射进度
        val mappedProgress = curveFunction(progress)
        // 计算最终偏移量
        val deltaX = targetValue.v1 - initialValue.v1
        val deltaY = targetValue.v2 - initialValue.v2
        return AnimationVector2D(
            initialValue.v1 + deltaX * mappedProgress,
            initialValue.v2 + deltaY * mappedProgress
        )
    }

    // 返回动画总时长
    override fun getDurationNanos(
        initialValue: AnimationVector2D,
        targetValue: AnimationVector2D,
        initialVelocity: AnimationVector2D
    ): Long {
        return durationMillis * 1_000_000L
    }
}

2. 在Compose中使用该Spec

以下示例展示如何用sin(x)和exp(x)实现图片偏移:

import androidx.compose.animation.core.animateAsState
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.res.painterResource
import kotlin.math.exp
import kotlin.math.sin
import kotlin.math.PI

@Composable
fun MathCurveOffsetDemo() {
    var isAnimating by remember { mutableStateOf(false) }

    // 使用sin函数的偏移动画
    val sinOffset by animateAsState(
        targetValue = if (isAnimating) Offset(150f, 100f) else Offset(0f, 0f),
        animationSpec = MathFunctionAnimationSpec(
            durationMillis = 2500,
            curveFunction = { progress ->
                // sin(progress * PI) 实现先向前偏移再回弹的效果
                sin(progress * PI.toFloat())
            }
        ),
        label = "sin offset animation"
    )

    // 使用exp函数的偏移动画
    val expOffset by animateAsState(
        targetValue = if (isAnimating) Offset(100f, 150f) else Offset(0f, 0f),
        animationSpec = MathFunctionAnimationSpec(
            durationMillis = 2000,
            curveFunction = { progress ->
                // exp(progress * 3) - 1 实现先慢后快的加速偏移,归一化到0-1范围
                (exp(progress * 3f) - 1f) / (exp(3f) - 1f)
            }
        ),
        label = "exp offset animation"
    )

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Button(onClick = { isAnimating = !isAnimating }) {
            Text(text = "触发动画")
        }

        // 应用sin函数偏移的图片
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_foreground),
            contentDescription = "Sin曲线偏移示例",
            modifier = Modifier.offset(sinOffset.x, sinOffset.y)
        )

        // 应用exp函数偏移的图片
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_foreground),
            contentDescription = "Exp曲线偏移示例",
            modifier = Modifier.offset(expOffset.x, expOffset.y)
        )
    }
}

二、关于线性Float映射方案的疑问解答

你提到的“线性动画Float值再映射为函数计算后的状态”是完全可行的,且在绝大多数场景下是最优方案:

  • 优势:实现成本极低,代码直观,不需要自定义复杂的AnimationSpec,适合快速验证效果或单次使用的场景。
  • 性能:Compose动画的每帧计算本身就是轻量级操作,普通数学函数(如sin、exp)的计算量可以忽略,不会造成性能问题。只有当函数包含大量复杂运算时,才需要考虑优化(比如提前预计算曲线值缓存)。

该方案的代码示例:

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.res.painterResource
import kotlin.math.sin
import kotlin.math.PI

@Composable
fun MappedFloatAnimationDemo() {
    var isAnimating by remember { mutableStateOf(false) }

    // 线性动画0-1的进度值
    val linearProgress by animateFloatAsState(
        targetValue = if (isAnimating) 1f else 0f,
        animationSpec = tween(durationMillis = 2000),
        label = "linear progress"
    )

    // 将线性进度映射为sin函数曲线
    val mappedProgress = sin(linearProgress * PI.toFloat())
    // 计算最终偏移
    val offset = Offset(mappedProgress * 150f, mappedProgress * 100f)

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Button(onClick = { isAnimating = !isAnimating }) {
            Text(text = "触发动画")
        }

        Image(
            painter = painterResource(id = R.drawable.ic_launcher_foreground),
            contentDescription = "映射式偏移示例",
            modifier = Modifier.offset(offset.x, offset.y)
        )
    }
}

三、两种方案的选择建议

  • 若需要复用同一数学曲线动画(比如多个组件共用同一曲线),选择自定义AnimationSpec,更符合Compose动画的设计范式。
  • 若只是单次使用或快速验证效果,选择线性Float映射,代码更简洁高效。

内容的提问来源于stack exchange,提问作者dlw

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 21:36:00