Jetpack Compose 自定义 Loading

1,422 阅读3分钟

好久没写文章了,打算来水一篇,希望能得到各位工友赞赏

自学Jetpack Compose 半月有余了,写了一个Loading加载动效

效果图

loading02.gif

实现思路拆分

  1. 将正方形均分为4份 确定4个符号的中心点位置

image.png

BoxWithConstraints(modifier = modifier) {
    val circleSizeDp = minOf(maxWidth, maxHeight)
    val density = LocalDensity.current.density
    val circleSizePx = circleSizeDp.value * density
    //均分4份
    val radius = circleSizePx / 4
    //right 和 bottom x,y
    val centerOffset = radius * 3
    
    //加号中心点
    var plusOffset by remember { mutableStateOf(Offset(radius, radius)) }
    //减号中心点
    var minusOffset by remember { mutableStateOf(Offset(centerOffset, radius)) }
    //乘号中心点
    var timesOffset by remember { mutableStateOf(Offset(centerOffset, centerOffset)) }
    //除号中心点
    var divOffset by remember { mutableStateOf(Offset(radius, centerOffset)) }
   
}   
  1. 根据4个符号的中心点绘制符号
     //符号长度
     val offset = radius / 2 + 15.dp.value
     Canvas(modifier = modifier.requiredSize(size = circleSizeDp)) {
           //加号
            drawLine(
                color = lineColor,
                start = Offset(plusOffset.x - offset, plusOffset.y),
                end = Offset(plusOffset.x + offset, plusOffset.y),
                strokeWidth = strokeWidth,
                cap = StrokeCap.Round,
            )

            drawLine(
                color = lineColor,
                start = Offset(plusOffset.x, plusOffset.y - offset),
                end = Offset(plusOffset.x, plusOffset.y + offset),
                strokeWidth = strokeWidth,
                cap = StrokeCap.Round,
            )
            
            //减号
            drawLine(
                color = lineColor,
                start = Offset(minusOffset.x - offset, minusOffset.y),
                end = Offset(minusOffset.x + offset, minusOffset.y),
                strokeWidth = strokeWidth,
                cap = StrokeCap.Round,
            )
            //乘号
            rotate(degrees = 45F, pivot = timesOffset) {
                drawLine(
                    color = lineColor,
                    start = Offset(timesOffset.x - offset, timesOffset.y),
                    end = Offset(timesOffset.x + offset, timesOffset.y),
                    strokeWidth = strokeWidth,
                    cap = StrokeCap.Round,
                )
            }
            rotate(degrees = 135F, pivot = timesOffset) {
                drawLine(
                    color = lineColor,
                    start = Offset(timesOffset.x - offset, timesOffset.y),
                    end = Offset(timesOffset.x + offset, timesOffset.y),
                    strokeWidth = strokeWidth,
                    cap = StrokeCap.Round,
                )
            }
	    //除号
            drawLine(
                color = lineColor,
                start = Offset(divOffset.x - offset, divOffset.y),
                end = Offset(divOffset.x + offset, divOffset.y),
                strokeWidth = strokeWidth,
                cap = StrokeCap.Round,
            )
            //除法2个圆点
            drawCircle(
                color = lineColor,
                style = Fill,
                radius = circleRadius,
                center = Offset(divOffset.x, divOffset.y - radius / 3)
            )
            drawCircle(
                color = lineColor,
                style = Fill,
                radius = circleRadius,
                center = Offset(divOffset.x, divOffset.y + radius / 3)
            )
        }

静态绘制效果
image.png

  1. 使用动画动起来 根据4个符号的中心点 构成一个正方形,每次偏移是正方形的边长

image.png

使用rememberInfiniteTransition() 无限循环动画 不断执行0到正方形的边长的动画运算 不断改变4个符号的中心点位置

//移动长度
val animateSize = radius * 2
//记录旋转次数 
var currentCount by remember { mutableStateOf(0) }
//rememberInfiniteTransition() 无限动画
val animateValue by rememberInfiniteTransition().animateFloat(
    initialValue = 0f,
    targetValue = animateSize,
    // keyframes 分时间分段计算返回
    // LinearEasing 平滑过渡
    animationSpec = infiniteRepeatable(
        animation = keyframes {
            durationMillis = 800
            0f at 80 with LinearEasing
            0.1f * animateSize at 150 with LinearEasing
            0.2f * animateSize at 200 with LinearEasing
            0.3f * animateSize at 250 with LinearEasing
            0.4f * animateSize at 300 with LinearEasing
            0.5f * animateSize at 400 with LinearEasing
            0.6f * animateSize at 500 with LinearEasing
            0.7f * animateSize at 600
            0.8f * animateSize at 700
            0.9f * animateSize at 750
            animateSize at 800
        },
        repeatMode = RepeatMode.Restart
    )
)
//监听动画结果变化 对4个断
LaunchedEffect(animateValue) {
    //根据animateValue ==0 来判断 动画的每次重新执行(无奈、没有相关监听接口)
    if (animateValue == 0f) {
        //每次重新开始就累加1
        currentCount += 1
        if (currentCount > 4) {
            currentCount = 1
        }
    }
    val plus = radius + animateValue
    val minus = centerOffset - animateValue
    // 根据 currentCount 标记出动画运行到哪个阶段
    when (currentCount) {
        1 -> {//加号从左往右
            plusOffset = Offset(plus, radius)
            minusOffset = Offset(centerOffset, plus)

            timesOffset = Offset(minus, centerOffset)
            divOffset = Offset(radius, minus)
        }
        2 -> {//加号从右往下
            plusOffset = Offset(centerOffset, plus)
            minusOffset = Offset(minus, centerOffset)

            timesOffset = Offset(radius, minus)
            divOffset = Offset(plus, radius)
        }
        3 -> {//加号从下往左
            plusOffset = Offset(minus, centerOffset)
            minusOffset = Offset(radius, minus)

            timesOffset = Offset(plus, radius)
            divOffset = Offset(centerOffset, plus)
        }
        4 -> {
            plusOffset = Offset(radius, minus)
            minusOffset = Offset(plus, radius)

            timesOffset = Offset(centerOffset, plus)
            divOffset = Offset(minus, centerOffset)
        }
    }
}

动画实现这个过程有点痛苦,目前Compose 在对动画细粒度监听上没有更好的支持,rememberInfiniteTransition()是无限循环动画,但是没有对动画Restartstartend暴露监听接口 同时差值器提供的也不能满足需求,只能通过keyframes 去一点一点的计算出来 如果有工友有好的方式 还望不要吝啬告知 到这里就基本上完成了

loading02.gif

扩展

使用 ModifierdrawWithContent实现未读消息红点提示

fun Modifier.redPoint(num: String): Modifier = drawWithContent {
    drawContent()
    drawIntoCanvas {
        val padding = 6.dp.toPx()
        val topPadding = 3.dp.toPx()

        val paint = Paint().apply {
            color = Color.Red
        }
        val paintTextSize= 14.sp.toPx()
        //绘制文本用FrameworkPaint 
        val textPaint = Paint().asFrameworkPaint().apply {
            isAntiAlias = true
            isDither = true
            color=Color.White.toArgb()
            textSize = paintTextSize
            typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)
            textAlign = android.graphics.Paint.Align.CENTER
        }
        //测量出文本的宽度
        val textWidth = textPaint.measureText(num)

        val radius =20.dp.toPx()
        val offset=(textWidth+padding*2)
        //绘制背景
        it.drawRoundRect(
            left = size.width-offset,
            top = 0f,
            right = size.width,
            bottom = radius,
            radiusX= 10.dp.toPx(),
            radiusY= 10.dp.toPx(),
            paint = paint
        )
        //绘制文本
        it.nativeCanvas.drawText(num, size.width-offset/2, radius-(radius-paintTextSize)/2-topPadding, textPaint)
    }
}

调用

@Composable
fun ImageDemo() {
        Image(
            painter = painterResource(id = R.drawable.message),
            contentDescription = "",
            modifier = Modifier
                .size(width = 56.dp, height = 56.dp)
                .redPoint("99"),
            contentScale = ContentScale.FillBounds, 
            alignment = Alignment.CenterEnd,
        )
}

image.png