Jetpack Compose DrawScope中渐变Alpha值出现异常相乘问题
看起来你在做纯绘制实现的渐隐标尺时,碰到了Alpha值异常衰减的问题——明明设置了25%宽度处保留60% opacity,但实际效果却接近透明,对吧?我帮你分析下问题出在哪,以及怎么解决。
问题根源分析
你的代码里有两个关键问题导致Alpha值被错误地多次相乘:
不必要的
graphicsLayerAlpha设置:你给Composable加了graphicsLayer { alpha = 0.99f },这个会给整个绘制图层套一层全局Alpha,之后再用BlendMode.DstIn的渐变遮罩时,相当于把渐变的Alpha和全局Alpha又做了一次相乘,进一步降低了透明度。渐变遮罩被重复绘制:你把
drawRect(brush = brush, blendMode = BlendMode.DstIn)放在了刻度绘制的for循环里面,这意味着每画一个刻度,就会给整个画布应用一次渐变遮罩。多次叠加后,边缘的Alpha值会被乘上N次(N是刻度数量),自然会变得几乎完全透明。
修复方案
针对这两个问题,我们可以做以下调整:
1. 移除全局Alpha的graphicsLayer设置
不需要通过graphicsLayer来添加Alpha通道,DrawScope里的混合模式已经能实现我们要的渐隐效果,直接去掉这部分代码即可。
2. 将渐变遮罩移到循环外,只绘制一次
把应用渐变遮罩的代码移到所有刻度和文字绘制完成之后,这样只会对整个标尺内容做一次Alpha渐变,不会重复叠加。
修改后的完整代码
BoxWithConstraints( modifier = Modifier .fillMaxWidth() .height(rulerHeightDp) .drag(state = state, onValueChanged = onValueChanged), contentAlignment = Alignment.TopCenter, ) { val textMeasurer = rememberTextMeasurer(cacheSize = numTicks) Box(modifier = Modifier .width(maxWidth) .height(rulerHeightDp) .drawWithCache { val barWidthPx = TickWidth.toPx() val minorTickHeightPx = MinorTickHeight.toPx() val majorTickHeightPx = MajorTickHeight.toPx() val textOffsetVerticalPx = majorTickHeightPx + 4.dp.toPx() val stepWidthPx = scale .stepWidthDp() .toPx() val halfStepsPerScreen = (constraints.maxWidth / stepWidthPx / 2f).toInt() val middleStep = state.currentValue / scale.pointsPerTick val startStep = (middleStep - halfStepsPerScreen) .toInt() .coerceAtLeast(0) val endStep = (middleStep + halfStepsPerScreen) .toInt() .coerceAtMost(maxPoints / scale.pointsPerTick) val centerOffset = constraints.maxWidth / 2f // content is visible where White, invisible where Transparent val colorStops = arrayOf( 0.0f to Color.White.copy(alpha = 0.15f), 0.25f to Color.White.copy(alpha = 0.6f), 0.5f to Color.White.copy(alpha = 1f), 0.75f to Color.White.copy(alpha = 0.6f), 1f to Color.White.copy(alpha = 0.15f), ) val brush = Brush.horizontalGradient(*colorStops) onDrawWithContent { drawContent() // 先绘制所有刻度和文字 for (step in startStep..endStep) { val tickValue = step * scale.pointsPerTick val offsetX = (step - middleStep) * stepWidthPx val tickHeightPx = if (tickValue % scale.pointsPerMajorTick == 0) { majorTickHeightPx } else { minorTickHeightPx } drawRect( color = Color.White, topLeft = Offset(offsetX, 0f), size = Size(barWidthPx, tickHeightPx), ) if (tickValue % scale.pointsPerLabel == 0) { // label val textLayoutResult = textMeasurer.measure( // textMeasurer caches the result text = tickValue.toString(), style = labelStyle, ) drawText( textLayoutResult = textLayoutResult, color = Color.White, topLeft = Offset( offsetX - textLayoutResult.size.width / 2, textOffsetVerticalPx, ), ) } } // 最后只应用一次渐变遮罩 drawRect( brush = brush, blendMode = BlendMode.DstIn, ) } }) { Box(Modifier .background(Color.Red) .fillMaxSize() ) { } } }
额外说明
如果之后你需要给整个标尺设置一个全局的透明度(比如整体80% opacity),可以直接在渐变的colorStops里调整每个颜色的Alpha值,或者在最后加一层半透明的矩形覆盖:
// 在drawRect(brush, BlendMode.DstIn)之后添加 drawRect( color = Color.Black.copy(alpha = 0.2f), blendMode = BlendMode.DstOver )
这样就能避免全局Alpha带来的混合问题啦~
备注:内容来源于stack exchange,提问作者Nino van Hooff

