perf: 限制 WebP 动画重复次数并简化入场动画,减少帧丢失

Perfetto trace 分析显示 75.6% 帧延迟,根因为 250x250 WebP
持续以 11-14 FPS 无限循环解码。每次解码周期导致 4-26 帧延迟。

- AnimatedGif: repeatCount(2) 限制动画播放 3 次后停止
- AnimatedGif: 移除两阶段弹跳动画,改为 150ms 淡入
- README: 添加构建命令和产物路径
This commit is contained in:
meyou 2026-05-25 22:43:45 +08:00
parent a84e1b9528
commit 80ab328906
No known key found for this signature in database
2 changed files with 47 additions and 18 deletions

View File

@ -19,4 +19,30 @@
- 双模块:`:shared`(UI + 逻辑) · `:androidApp`(薄壳)
- iOS 入口为 `MainViewController.kt`,Xcode 工程位于 `iosApp/`
## 构建
```bash
# Debug
./gradlew :app:assembleDebug # 构建 debug APK
./gradlew :app:installDebug # 安装 debug APK 到设备
# Release
./gradlew :app:assembleRelease # 构建 release APK
./gradlew :app:installBenchmark # 安装 benchmarkrelease + 可调试APK
# 测试
./gradlew :core:testDebugUnitTest # 运行全部测试
./gradlew :core:testDebugUnitTest --tests "plus.rua.project.ui.CalendarUtilsTest" # 运行单个测试
# Baseline Profile需要连接设备
./gradlew :macrobenchmark:updateBaselineProfile # 一键生成 + 自动复制到 :core
./gradlew :macrobenchmark:connectedBenchmarkAndroidTest # 仅运行基准测试
# 性能 Profiling需要连接设备
./scripts/profile.sh # 默认 8 秒
./scripts/profile.sh 15 # 自定义时长
```
构建产物位于 `app/build/outputs/apk/<variant>/` 目录。
线条小狗表情包来自 https://www.douban.com/group/topic/264788645/?_i=9181692phrDzjR,9241256phrDzjR

View File

@ -2,8 +2,6 @@ package plus.rua.project.ui
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -11,6 +9,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import com.github.panpf.sketch.AsyncImage
import com.github.panpf.sketch.rememberAsyncImageState
import com.github.panpf.sketch.request.ImageOptions
import com.github.panpf.sketch.request.repeatCount
import plus.rua.project.getWebpUri
/**
@ -18,12 +19,16 @@ import plus.rua.project.getWebpUri
*/
private val WEBP_FILES = (1..152).map { "${it.toString().padStart(3, '0')}.webp" }
private const val REPEAT_COUNT = 2
/**
* 显示动画 GIF 图片切换日期时随机选择一个
* 显示动画 WebP 图片切换日期时随机选择一个
*
* 动画播放 3 1 + [REPEAT_COUNT]后停止避免持续解码导致的帧丢失
*
* @param modifier 应用于图片的 Modifier
* @param contentDescription 无障碍描述
* @param seed 用于控制重新随机时机的 key变化时重新选择 GIF
* @param seed 用于控制重新随机时机的 key变化时重新选择 WebP
*/
@Composable
fun AnimatedGif(
@ -33,28 +38,26 @@ fun AnimatedGif(
) {
val webpFile = remember(seed) { WEBP_FILES.random() }
val uri = remember(webpFile) { getWebpUri(webpFile) }
val scale = remember { Animatable(0f) }
val alpha = remember { Animatable(0f) }
LaunchedEffect(seed) {
scale.snapTo(0f)
scale.animateTo(
targetValue = 1.1f,
animationSpec = tween(250, easing = FastOutSlowInEasing),
)
scale.animateTo(
alpha.snapTo(0f)
alpha.animateTo(
targetValue = 1f,
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
animationSpec = tween(150, easing = FastOutSlowInEasing),
)
}
val state = rememberAsyncImageState(
options = remember { ImageOptions { repeatCount(REPEAT_COUNT) } }
)
AsyncImage(
uri = uri,
contentDescription = contentDescription,
modifier = modifier
.graphicsLayer {
scaleX = scale.value
scaleY = scale.value
alpha = scale.value.coerceIn(0f, 1f)
},
state = state,
modifier = modifier.graphicsLayer {
this.alpha = alpha.value
},
)
}