fix(photo-editor): 修复旋转照片过程中的错位黑边

- 根因:isSwapped 原先直接基于目标旋转角度 state.rotationDegrees 判定,导致在旋转动画起始阶段(如 0° 向 90° 动画时)提前将布局 measure 尺寸转置,Fit 适配产生了内缩黑边 Padding。
- 修复:基于当前动画渲染角度 displayRotation.value 动态计算最近的 90° 基准角 (baseRotation),使布局尺寸转置与旋转动画平滑同步,彻底消除错位黑边。
- 测试:在 RotationGeometryTest 中补充 baseRotation、offsetDegrees、isSwapped 动态角度过渡断言。
This commit is contained in:
xfy 2026-07-24 11:09:33 +08:00
parent b13e018503
commit c39cecf343
3 changed files with 53 additions and 2 deletions

View File

@ -25,6 +25,26 @@ object RotationGeometry {
fun stableAspect(srcAspect: Float, rotationDegrees: Int): Float =
if (rotationDegrees % 180 == 0) srcAspect else 1f / srcAspect
/**
* 计算当前动画角度 [angle] 对应的最近 90° 倍数基准角度
*
* 动态基准角度避免了在动画刚开始时以终点角度直接切换转置状态导致的错位黑边
*/
fun baseRotation(angle: Float): Int =
kotlin.math.round(angle / 90f).toInt() * 90
/**
* 当前动画角度 [angle] 相对最近基准角度 [baseRotation] 的偏移角度 [-45°, 45°]
*/
fun offsetDegrees(angle: Float): Float =
angle - baseRotation(angle)
/**
* 判断在给定的 90° 倍数基准角度下宽高尺寸是否转置
*/
fun isSwapped(baseRotation: Int): Boolean =
baseRotation % 180 != 0
/**
* 与容器同比例的图片矩形旋转 [offsetDegrees] 后仍完整覆盖容器所需的最小缩放因子
*

View File

@ -380,14 +380,15 @@ private fun EditableImage(
}
) {
val angle = displayRotation.value
val offset = angle - state.rotationDegrees
val baseRotation = RotationGeometry.baseRotation(angle)
val isSwapped = RotationGeometry.isSwapped(baseRotation)
val offset = RotationGeometry.offsetDegrees(angle)
val scale = RotationGeometry.coverScale(offset, containerAspect)
Image(
bitmap = state.sourceBitmap.asImageBitmap(),
contentDescription = "编辑中的照片",
modifier = Modifier
.layout { measurable, constraints ->
val isSwapped = (state.rotationDegrees % 180 != 0)
val targetWidth = if (isSwapped) constraints.maxHeight else constraints.maxWidth
val targetHeight = if (isSwapped) constraints.maxWidth else constraints.maxHeight
val placeable = measurable.measure(

View File

@ -7,6 +7,7 @@ import kotlin.math.sin
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse
/**
* [RotationGeometry] 单元测试
@ -101,4 +102,33 @@ class RotationGeometryTest {
)
}
}
@Test
fun baseRotation_and_isSwapped_dynamicAngleTransition() {
assertEquals(0, RotationGeometry.baseRotation(0f))
assertEquals(0, RotationGeometry.baseRotation(20f))
assertEquals(0, RotationGeometry.baseRotation(44.9f))
assertEquals(90, RotationGeometry.baseRotation(45.1f))
assertEquals(90, RotationGeometry.baseRotation(80f))
assertEquals(90, RotationGeometry.baseRotation(134f))
assertEquals(180, RotationGeometry.baseRotation(135.1f))
assertEquals(270, RotationGeometry.baseRotation(270f))
assertEquals(-90, RotationGeometry.baseRotation(-45.1f))
assertEquals(-180, RotationGeometry.baseRotation(-135.1f))
assertFalse(RotationGeometry.isSwapped(0))
assertTrue(RotationGeometry.isSwapped(90))
assertFalse(RotationGeometry.isSwapped(180))
assertTrue(RotationGeometry.isSwapped(270))
assertTrue(RotationGeometry.isSwapped(-90))
assertFalse(RotationGeometry.isSwapped(-180))
assertEquals(0f, RotationGeometry.offsetDegrees(0f), 1e-5f)
assertEquals(20f, RotationGeometry.offsetDegrees(20f), 1e-5f)
assertEquals(-44.9f, RotationGeometry.offsetDegrees(45.1f), 1e-3f)
assertEquals(-10f, RotationGeometry.offsetDegrees(80f), 1e-5f)
assertEquals(0f, RotationGeometry.offsetDegrees(90f), 1e-5f)
}
}