注册

Jetpack Compose 自定义 Loading

自学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()是无限循环动画,但是没有对动画Restart、 start、 end暴露监听接口 同时差值器提供的也不能满足需求,只能通过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

0 个评论

要回复文章请先登录注册