我正在尝试在 Jetpack Compose 位图上实现将像素擦除为透明颜色。但是当我加载位图以在画布上显示时,已擦除的像素显示为黑色。这是我的可组合项:
@RequiresApi(Build.VERSION_CODES.Q)
@Composable
fun EraseByHandCanvasContent(
modifier: Modifier = Modifier,
eraseByHandViewModel: EraseByHandViewModel = koinViewModel()
) {
val drawingByHandState = eraseByHandViewModel.drawingByHandState.collectAsState()
val bitmap = eraseByHandViewModel.pickedImage.collectAsState()
val paint = remember {
derivedStateOf {
Paint().apply {
isAntiAlias = true
color = Color.Transparent.toArgb()
style = Paint.Style.STROKE
strokeWidth = drawingByHandState.value.eraseBrushSize
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}
}
}
Box(
modifier = modifier.fillMaxWidth(),
contentAlignment = Alignment.TopCenter,
) {
Canvas(modifier = Modifier
.fillMaxSize()
.onPlaced { layoutCoordinates ->
eraseByHandViewModel.setCanvasSize(layoutCoordinates.size)
}
.pointerInput(Unit) {
detectDragGestures(
onDrag = { change, _ ->
eraseByHandViewModel.onDrawingAction(
DrawingByHandAction.onDraw(
offset = change.position
)
)
change.consume()
},
onDragStart = { offset ->
eraseByHandViewModel.onDrawingAction(
DrawingByHandAction.onNewPathDraw(
offset = offset
)
)
},
onDragEnd = {
eraseByHandViewModel.onDrawingAction(
DrawingByHandAction.onPathEnd()
)
}
)
}) {
val currentBitmap =
drawingByHandState.value.tempBitmap
?: bitmap.value?.copy(Bitmap.Config.ARGB_8888, true)?.apply {
isPremultiplied = true
setHasAlpha(true)
}
currentBitmap?.let { baseBitmap ->
val outputBitmap = Bitmap.createScaledBitmap(
baseBitmap,
size.width.toInt(),
size.height.toInt(),
false,
)
val combinedCanvas = android.graphics.Canvas(outputBitmap)
combinedCanvas.drawPath(
drawingByHandState.value.currentPath.asAndroidPath(),
paint.value,
)
eraseByHandViewModel.setTempBitmap(outputBitmap)
drawImage(
image = outputBitmap.asImageBitmap(),
)
}
}
}
}
同样出于调试目的,我检查了下面的位图像素,我得到的 countTransparent 值始终为 0,因此绘画过程也存在问题:
private fun countTransparentPixels(bitmap: Bitmap) {
var countTransparent = 0
var countNonTransparent = 0
val copyBitmap = bitmap.copy(Bitmap.Config.ARGB_8888,true).apply {
setHasAlpha(true)
isPremultiplied = true
}
for (x in 0 until copyBitmap.width) {
for (y in 0 until copyBitmap.height) {
val pixel = copyBitmap.getPixel(x, y)
if (pixel == androidx.compose.ui.graphics.Color.Transparent.toArgb()){
countTransparent++
} else {
countNonTransparent++
}
}
}
Log.d("COUNTERS", "$countTransparent---$countNonTransparent")
}
我还使用 Coil 在我的应用程序上显示位图图像,当我在图库上加载已擦除的位图时(已擦除的像素在图库中显示为白色),SubcomposeAsyncImage 也将位图已擦除的像素显示为黑色,就像我的 EraseByHandCanvasContent 可组合项一样。我无法理解,我在绘制对象和 drawImage() 方法上尝试了不同的 BlendMode 组合,但我无法获得透明位图。
val copyBitmap = selectedImageState.value?.copy(Bitmap.Config.ARGB_8888,true)
copyBitmap?.apply {
setHasAlpha(true)
isPremultiplied = true
}
SubcomposeAsyncImage(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.clickable {
onPickImage()
},
model = copyBitmap,
contentDescription = "",
alignment = Alignment.TopCenter,
contentScale = ContentScale.Crop,
)
当您从实际位图中擦除时,您需要使用
androidx.compose.ui.graphics.Canvas
而不是 androidx.compose.foundation.Canvas
,后者是可组合的。
下面的代码片段是我针对另一个问题发布的代码片段,用于检查删除了哪个百分比的图像。
完整代码和演示可在存储库中找到。
@Composable
fun EraseBitmapSample(imageBitmap: ImageBitmap, modifier: Modifier) {
var matchPercent by remember {
mutableFloatStateOf(100f)
}
BoxWithConstraints(modifier) {
// Path used for erasing. In this example erasing is faked by drawing with canvas color
// above draw path.
val erasePath = remember { Path() }
var motionEvent by remember { mutableStateOf(MotionEvent.Idle) }
// This is our motion event we get from touch motion
var currentPosition by remember { mutableStateOf(Offset.Unspecified) }
// This is previous motion event before next touch is saved into this current position
var previousPosition by remember { mutableStateOf(Offset.Unspecified) }
val imageWidth = constraints.maxWidth
val imageHeight = constraints.maxHeight
val drawImageBitmap: ImageBitmap = remember(imageBitmap) {
Bitmap.createScaledBitmap(
imageBitmap.asAndroidBitmap(),
imageWidth,
imageHeight,
false
)
.asImageBitmap()
}
// Pixels of scaled bitmap, we scale it to composable size because we will erase
// from Composable on screen
val originalPixels: IntArray = remember {
val buffer = IntArray(imageWidth * imageHeight)
drawImageBitmap
.readPixels(
buffer = buffer,
startX = 0,
startY = 0,
width = imageWidth,
height = imageHeight
)
buffer
}
val erasedBitmap: ImageBitmap = remember {
Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.ARGB_8888).asImageBitmap()
}
val canvas: Canvas = remember {
Canvas(erasedBitmap)
}
val paint = remember {
Paint()
}
val erasePaint = remember {
Paint().apply {
blendMode = BlendMode.Clear
this.style = PaintingStyle.Stroke
strokeWidth = 50f
}
}
LaunchedEffect(key1 = currentPosition) {
snapshotFlow {
currentPosition
}
.map {
compareBitmaps(
originalPixels,
erasedBitmap,
imageWidth,
imageHeight
)
}
.onEach {
matchPercent = it
}
.flowOn(Dispatchers.Default)
.launchIn(this)
}
canvas.apply {
val nativeCanvas = this.nativeCanvas
val canvasWidth = nativeCanvas.width.toFloat()
val canvasHeight = nativeCanvas.height.toFloat()
when (motionEvent) {
MotionEvent.Down -> {
erasePath.moveTo(currentPosition.x, currentPosition.y)
previousPosition = currentPosition
}
MotionEvent.Move -> {
erasePath.quadraticTo(
previousPosition.x,
previousPosition.y,
(previousPosition.x + currentPosition.x) / 2,
(previousPosition.y + currentPosition.y) / 2
)
previousPosition = currentPosition
}
MotionEvent.Up -> {
erasePath.lineTo(currentPosition.x, currentPosition.y)
currentPosition = Offset.Unspecified
previousPosition = currentPosition
motionEvent = MotionEvent.Idle
}
else -> Unit
}
with(canvas.nativeCanvas) {
drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
drawImageRect(
image = drawImageBitmap,
dstSize = IntSize(canvasWidth.toInt(), canvasHeight.toInt()),
paint = paint
)
drawPath(
path = erasePath,
paint = erasePaint
)
}
}
val canvasModifier = Modifier.pointerMotionEvents(
Unit,
onDown = { pointerInputChange ->
motionEvent = MotionEvent.Down
currentPosition = pointerInputChange.position
pointerInputChange.consume()
},
onMove = { pointerInputChange ->
motionEvent = MotionEvent.Move
currentPosition = pointerInputChange.position
pointerInputChange.consume()
},
onUp = { pointerInputChange ->
motionEvent = MotionEvent.Up
pointerInputChange.consume()
},
delayAfterDownInMillis = 20
)
Image(
modifier = canvasModifier
.clipToBounds()
.drawBehind {
val width = this.size.width
val height = this.size.height
val checkerWidth = 10.dp.toPx()
val checkerHeight = 10.dp.toPx()
val horizontalSteps = (width / checkerWidth).toInt()
val verticalSteps = (height / checkerHeight).toInt()
for (y in 0..verticalSteps) {
for (x in 0..horizontalSteps) {
val isGrayTile = ((x + y) % 2 == 1)
drawRect(
color = if (isGrayTile) androidx.compose.ui.graphics.Color.LightGray else androidx.compose.ui.graphics.Color.White,
topLeft = Offset(x * checkerWidth, y * checkerHeight),
size = Size(checkerWidth, checkerHeight)
)
}
}
}
.matchParentSize()
.border(2.dp, androidx.compose.ui.graphics.Color.Green),
bitmap = erasedBitmap,
contentDescription = null,
contentScale = ContentScale.FillBounds
)
}
Text("Original Bitmap")
Image(
modifier = modifier,
bitmap = imageBitmap,
contentDescription = null,
contentScale = ContentScale.FillBounds
)
Text(
"Bitmap match ${matchPercent.toInt()}%",
color = androidx.compose.ui.graphics.Color.Red,
fontSize = 22.sp
)
}