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

如何在非Composable类中用Animatable实现Rect回边界动画?

问题描述

我正在开发一款图片裁剪器,使用Rect绘制动态遮罩层。当遮罩层超出图片边界时,希望在指针抬起时让它动画过渡回有效边界。

已实现内容

  1. 定义overlayRect变量:
open var overlayRect: Rect =
    Rect(offset = Offset.Zero, size = Size(size.width.toFloat(), size.height.toFloat()))
  1. 通过moveIntoBounds函数计算遮罩层的有效边界位置:
internal fun moveIntoBounds(rectBounds: Rect, rectCurrent: Rect): Rect {
    var width = rectCurrent.width
    var height = rectCurrent.height

    if (width > rectBounds.width) {
        width = rectBounds.width
    }

    if (height > rectBounds.height) {
        height = rectBounds.height
    }

    var rect = Rect(offset = rectCurrent.topLeft, size = Size(width, height))

    if (rect.left < rectBounds.left) {
        rect = rect.translate(rectBounds.left - rect.left, 0f)
    }

    if (rect.top < rectBounds.top) {
        rect = rect.translate(0f, rectBounds.top - rect.top)
    }

    if (rect.right > rectBounds.right) {
        rect = rect.translate(rectBounds.right - rect.right, 0f)
    }

    if (rect.bottom > rectBounds.bottom) {
        rect = rect.translate(0f, rectBounds.bottom - rect.bottom)
    }

    return rect
}
  1. 指针抬起时直接赋值:
override fun onUp(change: PointerInputChange) {
    touchRegion = TouchRegion.None

    overlayRect = moveIntoBounds(rectBounds, overlayRect)

    // Calculate crop rectangle
    cropRect = calculateRectBounds()
    rectTemp = overlayRect.copy()
}

疑问

如何为这个Rect添加动画过渡到有效边界?能否使用Animatable来实现Rect动画?

我查阅了官方动画文档,文档建议用transition及transition.animateRect在状态间过渡,但我的场景没有固定状态,是从动态当前值过渡到动态目标值,且代码位于非Composable类DynamicCropState中(该类继承自类似ZoomStateImpl的类),需要使用Animatable或非Composable动画API实现。


解决方案

可以使用Animatable实现Rect的动画过渡,只需为Rect实现对应的TwoWayConverter,将其转换为动画系统可处理的向量类型即可。具体实现步骤如下:

1. 为Rect创建双向转换器

Rect由left、top、right、bottom四个浮点值决定,我们需要将它与AnimationVector4D做双向转换:

private val rectToVectorConverter = TwoWayConverter<Rect, AnimationVector4D>(
    convertToVector = { rect ->
        AnimationVector4D(rect.left, rect.top, rect.right, rect.bottom)
    },
    convertFromVector = { vector ->
        Rect(
            left = vector.v1,
            top = vector.v2,
            right = vector.v3,
            bottom = vector.v4
        )
    }
)

2. 在DynamicCropState中初始化Animatable

在你的状态类中添加Animatable实例,初始值与overlayRect保持一致:

private val coroutineScope = MainScope()
private val animatableRect = Animatable(overlayRect, rectToVectorConverter)

3. 修改onUp方法启动动画

在指针抬起时,计算目标Rect后启动动画,并监听动画过程更新overlayRect:

override fun onUp(change: PointerInputChange) {
    touchRegion = TouchRegion.None

    val targetRect = moveIntoBounds(rectBounds, overlayRect)

    // 启动动画,这里使用tween动画,可替换为spring实现弹性效果
    coroutineScope.launch {
        animatableRect.animateTo(
            targetValue = targetRect,
            animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
        )
    }

    // 监听动画值变化,同步更新UI相关变量
    animatableRect.addUpdateListener {
        overlayRect = it.value
        cropRect = calculateRectBounds()
        rectTemp = overlayRect.copy()
        // 如果需要通知Compose重组,调用对应状态更新方法(如notifyStateChanged())
    }
}

4. 同步拖拽过程中的Rect值

在拖拽修改overlayRect时,同步更新animatableRect的当前值,避免动画起点错误:

// 示例:拖拽时更新overlayRect的方法
fun updateOverlayRect(newRect: Rect) {
    overlayRect = newRect
    animatableRect.snapTo(newRect)
}

关键说明

  • Animatable依赖协程作用域,这里使用MainScope()确保在主线程执行动画;
  • 如果需要Composable自动感知overlayRect变化,建议将其改为MutableState<Rect>类型;
  • 动画时长、缓动效果可根据需求调整,比如用spring(dampingRatio = 0.8f)实现更自然的弹性回弹效果。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 17:09:32