Compose 带动画的待办清单列表页

动画列表页

页面动画拆分

  1. 背景动画
  2. 标题动画
  3. 水波纹动画
  4. ToDo列表动画

背景动画 AnimateBackgroundView

使用 Canvas Path 绘制图形

rememberInfiniteTransition开始动画

scss 复制代码
@Composable
fun AnimateBackgroundView(modifier: Modifier = Modifier) {

    val transition = rememberInfiniteTransition("AnimateBackgroundView")
    val rotate by transition.animateFloat(
        0f, 1f,
        animationSpec =
            infiniteRepeatable(
                tween(16000, easing = LinearEasing),
                RepeatMode.Restart
            )
    )
    //
    val periods = remember { intArrayOf(3, 4, 5) }
    val colors = remember {
        arrayOf(
            Color.Green,
            Color.Cyan,
            Color.Magenta,
        )
    }

    Box(modifier.fillMaxSize()) {
        Canvas(modifier = Modifier.fillMaxSize()) {
            val infos = arrayOf(
                Triple(
                    size.width * 0.25f,
                    size.width * 0.2f,
                    size.height * 0.25f
                ),
                Triple(
                    size.width * 0.18f,
                    size.width * 0.8f,
                    size.height * 0.29f
                ),
                Triple(
                    size.width * 0.28f,
                    size.width * 0.72f,
                    size.height * 0.76f
                )
            )
            infos.forEachIndexed { index, triple ->
                val (radius, centerX, centerY) = triple
                drawCustomPath(rotate, radius, centerX, centerY,
                    colors[index], periods[index])
            }
        }
    }
}

private fun DrawScope.drawCustomPath(
    rotate: Float,
    radius: Float,
    centerX: Float,
    centerY: Float,
    color: Color,
    period: Int
) {
    val path = Path().apply {
        for (i in 0 until 360 step 5) {
            //角度转化为弧度
            val rad = i * PI.toFloat() / 180f
            //正弦函数,period 周期
            //完整的0-2π,出现period个周期, period越大,波形越瘦,
            //0.2振幅,越大 波形越高
            val wave = 0.2f * sin(rotate * PI.toFloat() * 2f + rad * period)
            val adjustedRadius = radius * (1f + wave)
            val x = centerX + adjustedRadius * cos(rad)
            val y = centerY + adjustedRadius * sin(rad)
            if (i == 0) {
                moveTo(x, y)
            } else {
                lineTo(x, y)
            }
        }
        close()
    }
    drawPath(path, color = color.copy(alpha = 0.4f))
}

标题动画 ToDoListTitle

标题从上往下移动

ini 复制代码
@Composable
private fun ToDoListTitle() {

    val d = LocalDensity.current
    val titleY = remember {
        with(d) {
            50.dp.toPx()
        }
    }
    //Animatable控制动画
    val animate = remember { Animatable(-titleY) }

    LaunchedEffect(Unit) {
        delay(200)
        animate.animateTo(
            0f,
            animationSpec = spring(dampingRatio = 0.28f, stiffness = 300f),
        )
    }

    val date = remember { LocalDate.now() }
    Column(modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp)) {
        Text(
            buildAnnotatedString {
                withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
                    append(date.monthValue.toString())
                }
                withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.tertiary)) {
                    append(".")
                }
                withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.tertiary)) {
                    append(date.dayOfMonth.toString())
                }
            },
            color = Color.Gray.copy(alpha = 0.8f),
            fontSize = 24.sp,
            fontWeight = FontWeight.SemiBold,
            modifier = Modifier.graphicsLayer {
                translationY = animate.value
            }
        )
        Spacer(Modifier.height(5.dp))
        Text(
            "Today",
            fontSize = 28.sp,
            fontWeight = FontWeight.SemiBold,
            color = MaterialTheme.colorScheme.secondary,
            modifier = Modifier.graphicsLayer {
                translationY = animate.value
            })
    }
}

列表动画ToDoList

AnimatedVisibility 控制动画出现消失 使用list存储当前item是否显示和隐藏

ini 复制代码
@Composable
private fun ToDoList() {

    val toDoViewModel = koinViewModel<ToDoViewModel>()

    val lists by toDoViewModel.todoList.collectAsStateWithLifecycle()
    val animatedIndices = toDoViewModel.animatedIndices

    LaunchedEffect(lists) {
        for ((index, model) in lists.withIndex()) {
            if (!animatedIndices.contains(model.id)) {
                delay(50 * index.toLong())
                animatedIndices.add(model.id)
            }
        }
    }

    Box(modifier = Modifier.fillMaxSize()) {
        LazyColumn {
            itemsIndexed(lists, key = { index, model -> model.id }) { index, model ->
                val animate by remember(index) {
                    derivedStateOf { animatedIndices.contains(model.id) }
                }
      
                AnimatedVisibility(
                    visible = animate,
                    enter =
                        slideInVertically(
                            initialOffsetY = { -it },
                            animationSpec = tween(
                                durationMillis = 300,
                                delayMillis = 100,
                                easing = LinearEasing
                            )
                        ) + fadeIn(tween(300, delayMillis = 100, easing = LinearEasing)),
                    exit = slideOutVertically(
                        targetOffsetY = { -it/2 },
                        animationSpec = tween(
                            durationMillis = 300,
                            easing = LinearEasing
                        )
                    ) + fadeOut(tween(300, easing = LinearEasing))
                ) {
                    ListItemView(
                        model.title, model.content, model.color,
                        modifier = Modifier
                            .graphicsLayer {
                                //                            translationY = offsetY
                            })
                }
            }
        }

        Column(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(10.dp),
            verticalArrangement = Arrangement.spacedBy(10.dp)
        ) {
            IconButton(
                modifier = Modifier
                    .clip(CircleShape)
                    .background(color = Color.Gray),
                onClick = {
                    toDoViewModel.remove()
                }) {
                Icon(
                    imageVector = Icons.Default.Remove,
                    tint = Color.White,
                    contentDescription = "Remove"
                )
            }

            IconButton(
                modifier = Modifier
                    .clip(CircleShape)
                    .background(color = Color.Gray),
                onClick = {
                    toDoViewModel.add()
                }) {
                Icon(
                    imageVector = Icons.Default.Add,
                    tint = Color.White,
                    contentDescription = "Add"
                )
            }
        }
    }
}


@Composable
private fun ListItemView(
    title: String,
    content: String,
    color: Color = Color.Gray,
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier
            .padding(horizontal = 20.dp)
            .padding(top = 10.dp)
            .clip(RoundedCornerShape(10.dp))
            .graphicsLayer {
//                shadowElevation = 8.dp.toPx()
//                shape = RoundedCornerShape(8.dp)
//                clip = false
//                ambientShadowColor = Color(0x80000000)
//                spotShadowColor = Color(0x80000000)
            }
            .fillMaxWidth()
            .background(color)
            .padding(horizontal = 10.dp, vertical = 15.dp),
        verticalArrangement = Arrangement.Center
    ) {
        Column {
            Text(title, fontSize = 14.sp)
            Spacer(Modifier.height(5.dp))
            Text(content, fontSize = 12.sp)
        }
    }
}

ToDoViewModel

kotlin 复制代码
class ToDoViewModel : ViewModel() {

    private val _todoList = MutableStateFlow(emptyList<ToDoModel>())
    val todoList: StateFlow<List<ToDoModel>>
        get() = _todoList

    val animatedIndices = mutableStateSetOf<String>()

    init {
        _todoList.value = arrayListOf(
            ToDoModel(uuid(), "洗衣服", "好多衣服1", Color.Gray.copy(alpha = 0.8f)),
            ToDoModel(uuid(), "洗衣服", "好多衣服2", Color.Green.copy(alpha = 0.8f)),
            ToDoModel(uuid(), "洗衣服", "好多衣服3", Color.Magenta.copy(alpha = 0.8f)),
            ToDoModel(uuid(), "洗衣服", "好多衣服4", Color.Green),
        )
    }
    
    //删除时,先使 item 消失,在 刷新lazyColumn
    fun remove(index: Int = 0) {
       viewModelScope.launch {
           val list = _todoList.value.toMutableList()
           if (list.isNotEmpty()) {
               val id = list.removeAt(index).id
               animatedIndices.remove(id)
               delay(200)
               _todoList.update {
                   list
               }
           }
       }
    }

    fun add() {
        _todoList.update {
            it + ToDoModel(
                uuid(),
                "洗衣服",
                "好多衣服1",
                arrayListOf(Color.Green, Color.Magenta, Color.Gray).random()
            )
        }
    }
}

fun uuid(): String {
    return UUID.randomUUID().toString().take(8)
}

水波纹动画

scss 复制代码
@Composable
private fun MountainsRiversView() {
    var shanVisible by remember { mutableStateOf(false) }

    LaunchedEffect(Unit) {
        delay(800)
        shanVisible = true
    }

    AnimatedVisibility(
        visible = shanVisible,
        enter = slideInVertically(
            initialOffsetY = { -it - it / 2 }
        ) + slideInHorizontally(
            initialOffsetX = { it + it / 2 }
        ) + fadeIn() + scaleIn(initialScale = 0.5f)
    ) {
        EnhancedWaterRippleWidget()
    }
}

EnhancedWaterRippleWidget

ini 复制代码
@Composable
fun EnhancedWaterRippleWidget() {

    SideEffect {
        val runtime = Runtime.getRuntime()
        val usedMem = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024)
        "Memory ,Used memory: ${usedMem}MB".loge()
    }

    val infiniteTransition = rememberInfiniteTransition()

    val (phase1, phase2) = infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 2f * PI.toFloat(),
        animationSpec = infiniteRepeatable(
            animation = tween(1600, easing = LinearEasing),
            repeatMode = RepeatMode.Restart
        )
    ) to infiniteTransition.animateFloat(
        initialValue = PI.toFloat(),
        targetValue = 3f * PI.toFloat(),
        animationSpec = infiniteRepeatable(
            animation = tween(1600, easing = LinearEasing),
            repeatMode = RepeatMode.Restart
        )
    )

    val (alpha, alphaFlash) = infiniteTransition.animateFloat(
        initialValue = 0.2f,
        targetValue = 0.4f,
        animationSpec = infiniteRepeatable(
            animation = tween(2000, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        )
    ) to infiniteTransition.animateFloat(
        initialValue = 0.1f,
        targetValue = 0.6f,
        animationSpec = infiniteRepeatable(
            animation = tween(1000),
            repeatMode = RepeatMode.Reverse
        )
    )


    val animatable = remember { Animatable(0f) }

    LaunchedEffect(Unit) {
        animatable.animateTo(
            targetValue = 1f,
            animationSpec = tween(800, delayMillis = 400, easing = FastOutSlowInEasing)
        )
    }

    val cloudDx by infiniteTransition.animateValue(
        initialValue = 108.dp,
        targetValue = (-8).dp,
        animationSpec = infiniteRepeatable(
            animation = tween(8000, easing = LinearEasing),
            repeatMode = RepeatMode.Restart
        ),
        typeConverter = Dp.VectorConverter,
        label = "cloudDx"
    )

    SideEffect {
        "重组--- Ripple".loge()
    }

    Box(
        modifier = Modifier
            .width(100.dp)
            .height(100.dp)
            .clip(CircleShape)
            .background(Color.Blue)
            .clipToBounds()
            .drawWithCache {
                onDrawWithContent {
                    //绘制水波纹
                    waterDraw(phase1 = { phase1.value }, phase2 = { phase2.value })
                    //绘制云朵
                    cloudDraw(dx = { cloudDx })
                    //绘制闪电
                    drawFlash(alpha = { alphaFlash.value })
                    //绘制太阳
                    val x = { animatable.value * 35.dp.toPx() }
                    val y = { animatable.value * 20.dp.toPx() }
                    glowingSun(x = x, y = y, alpha = { alpha.value })
                }
            }
    )
}

水波纹 waterDraw

scss 复制代码
fun DrawScope.waterDraw(
    path1: Path = Path(),
    path2: Path = Path(),
    phase1: () -> Float,
    phase2: () -> Float
) {
    val waveWidth = size.width
    val baseHeight = size.height * 0.68f
    val amplitude = size.height * 0.1f
    // 主波纹(红色)
    path1.apply {
        reset()
        moveTo(0f, baseHeight)
        for (x in 0..waveWidth.toInt() step 5) {
            val ratio = x.toFloat() / waveWidth
            val y = amplitude * 0.4f * sin(phase1() + ratio * 2f * PI.toFloat())
            lineTo(x.toFloat(), baseHeight - y)
        }
        lineTo(waveWidth, size.height)
        lineTo(0f, size.height)
        close()
    }

    // 次波纹(青色,半透明)
    path2.apply {
        reset()
        moveTo(0f, baseHeight)
        for (x in 0..waveWidth.toInt() step 5) {
            val ratio = x.toFloat() / waveWidth
            val y = amplitude * 0.4f * sin(phase2() + ratio * 2.2f * PI.toFloat()) // 不同频率
            lineTo(x.toFloat(), baseHeight - y)
        }
        lineTo(waveWidth, size.height)
        lineTo(0f, size.height)
        close()
    }

    // 绘制双波纹(后绘制的在上层)
    drawPath(
        path = path1,
        color = Color(0xFFE53935), // 红色
    )
    drawPath(
        path = path2,
        color = Color(0xFF4DD0E1), // 青色
    )

    // 添加波光粼粼效果(可选)
    for (i in 0..3) {
        val sparkleX = waveWidth * ((phase1() / (2f * PI) + i * 0.25f) % 1f)
        if (sparkleX in 0f..waveWidth) {
            val sparkleY = baseHeight - amplitude * 0.4f *
                    sin(phase1() + sparkleX / waveWidth * 4f * PI.toFloat())
            drawCircle(
                color = Color.White.copy(alpha = 0.8f),
                radius = 2.dp.toPx(),
                center = Offset(sparkleX.toFloat(), sparkleY.toFloat())
            )
        }
    }
}

绘制太阳

ini 复制代码
fun DrawScope.glowingSun(x: () -> Float, y: () -> Float, alpha: () -> Float) {

    val center = Offset(x(), y())
    val baseRadius = 10.dp.toPx()

    // 在Canvas中添加光芒射线
    with(drawContext.canvas) {
        val spikeCount = 12
        repeat(spikeCount) { i ->
            val angle = i * (360f / spikeCount)
            val spikeLength = baseRadius + 2.dp.toPx()
            val endX = center.x + spikeLength * cos(angle * PI / 180).toFloat()
            val endY = center.y + spikeLength * sin(angle * PI / 180).toFloat()
            drawLine(
                start = center,
                end = Offset(endX, endY),
                color = Color.Red,
                strokeWidth = 1.dp.toPx(),
                colorFilter = ColorFilter.tint(
                    color = Color.Yellow.copy(alpha = 0.6f),
                    blendMode = BlendMode.Screen
                )
            )
        }
    }

    drawCircle(
        color = Color.Red, // 橙黄色
        radius = baseRadius,
        center = center,
    )

    // 4. 核心太阳(纯色不透明)
    drawCircle(
        color = Color.Red, // 橙黄色
        radius = baseRadius,
        center = center,
        colorFilter = ColorFilter.tint(
//            color = Color.Yellow.copy(alpha = alpha()),
            color = Color.Yellow.copy(alpha = 0.6f),
            blendMode = BlendMode.SrcOver
        )
    )

    // 5. 高光点(可选)
    drawCircle(
        color = Color.White,
        radius = baseRadius * 0.2f,
        center = Offset(
            center.x + baseRadius * 0.3f,
            center.y - baseRadius * 0.3f
        ),
        colorFilter = ColorFilter.tint(
            color = Color.White.copy(alpha = 0.6f),
            blendMode = BlendMode.Plus
        )
    )
}

绘制云朵

scss 复制代码
/**
 * 绘制云朵
 */
fun DrawScope.cloudDraw(
    path: Path = Path(),
    path2: Path = Path(),
    isRainLine: Boolean = true,
    dx: () -> Dp
) {
//    val center = Offset(78.dp.toPx(), 20.dp.toPx())
    val center = Offset(dx().toPx(), 20.dp.toPx())
    val radius = 4.dp.toPx()
    path.apply {
        reset()
        moveTo(center.x, center.y)
        addArc(
            Rect(
                offset = Offset(center.x - radius, center.y - radius),
                size = Size(radius * 2f, radius * 2f)
            ),
            -180f, 180f
        )

        moveTo(center.x + radius, center.y + radius)
        addArc(
            Rect(
                offset = Offset(center.x, center.y),
                size = Size(radius * 2f, radius * 2f)
            ), -90f, 180f
        )
        moveTo(center.x + radius, center.y + radius * 2)
        lineTo(center.x - radius, center.y + 2 * radius)
        moveTo(center.x - radius, center.y + radius)
        addArc(
            Rect(
                offset = Offset(center.x - radius * 2, center.y),
                size = Size(radius * 2f, radius * 2f)
            ), 90f, 180f
        )
        addRect(
            Rect(
                offset = Offset(center.x - radius, center.y),
                size = Size(radius * 2f, radius * 2f)
            )
        )

        //绘制底部线条
        if (isRainLine) {
            val lineCenter = Offset(center.x, center.y + 2 * radius)
            val xLineSpace = radius / 2
            val xSpace = 0.8f * radius * cos(Math.toRadians(60.0)).toFloat()
            val ySpace = 0.8f * radius * sin(Math.toRadians(60.0)).toFloat()
            path2.apply {
                reset()
                for (i in -1..1) {
                    moveTo(lineCenter.x + (i * xLineSpace), lineCenter.y)
                    lineTo(lineCenter.x + (i * xLineSpace) - xSpace, lineCenter.y + ySpace)
                }
            }
            drawPath(
                path2,
                color = Color.White.copy(alpha = 0.4f),
                style = Stroke(width = 2f)
            )
        }

    }
    drawPath(path, color = Color.White.copy(alpha = 0.4f))
}

绘制闪电

scss 复制代码
/**
 * 绘制闪电
 */
fun DrawScope.drawFlash(path: Path = Path(), alpha: () -> Float) {
//        val x = (10..(size.width - 10f).toInt()).random().toFloat()
//        val y = (10..(size.height / 2).toInt()).random().toFloat()
//    val x = if(alpha()>0.32) 40.dp.toPx() else 70.dp.toPx()
//    val y = if(alpha()>0.32) 40.dp.toPx() else 32.dp.toPx()
    val x = 70.dp.toPx()
    val y = 28.dp.toPx()
    val center = Offset(x, y)
    path.apply {
        reset()
        moveTo(center.x, center.y)
        lineTo(center.x, center.y + 5.dp.toPx())
        lineTo(center.x + 1.2.dp.toPx(), center.y + 5.dp.toPx())
        lineTo(center.x + 1.2.dp.toPx(), center.y + 10.dp.toPx())
        lineTo(center.x + 5.dp.toPx(), center.y + 4.dp.toPx())
        lineTo(center.x + 5.dp.toPx(), center.y + 4.dp.toPx())
        lineTo(center.x + 4.dp.toPx(), center.y + 4.dp.toPx())
        lineTo(center.x + 5.dp.toPx(), center.y)
        close()
    }
    drawPath(path, color = Color.White.copy(alpha = alpha()))
}
相关推荐
婵鸣空啼3 小时前
GD图像处理与SESSiON
android
sunly_4 小时前
Flutter:导航固定背景图,滚动时导航颜色渐变
android·javascript·flutter
ljt27249606614 小时前
Compose笔记(二十六)--DatePicker
笔记·android jetpack
用户2018792831674 小时前
简单了解android.permission.MEDIA_CONTENT_CONTROL权限
android
_一条咸鱼_4 小时前
Android Runtime类卸载条件与资源回收策略(29)
android·面试·android jetpack
顾林海4 小时前
Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理
android·面试·性能优化
砖厂小工4 小时前
Now In Android 精讲 8 - Gradle build-logic 现代构建逻辑组织方式
android
玲小珑4 小时前
Auto.js 入门指南(八)高级控件与 UI 自动化
android·前端
vocal5 小时前
我的安卓第一课:四大组件之一Activity及其组件RecyclerView
android