作った理由
AGSL
を使えばShader
を使ってエフェクトを書けることができ、下記の記事のようにAGSL
を使ってモザイクをかける処理を実装されている方もいます。
AGSLで遊んでみる
しかし、AGSL
はAndroid13以降でないと対応しておらず、プロジェクト作成時に確認できるデバイスの割合も33%程度は留まっており、別の方法で実装可能か確認がてら作ってみました。
実装例
今回はこちらのページを参考に、カスタムペインタとして実装しています。特筆すべきことも無いので、処理の内容はコメントを見てください。
カスタム ペインタ
MosaicPainter.kt
class MosaicPainter(
private val image: ImageBitmap,
private val blockSize: Int
) : Painter() {
override val intrinsicSize: Size get() = Size(image.width.toFloat(), image.height.toFloat())
override fun DrawScope.onDraw() {
// 画像と画面のサイズ比を取得
val scale = image.width / size.width
// ある程度ブロックサイズが大きい場合にモザイク処理を行う
if (blockSize > 5) {
for (x in 0 until size.width.roundToInt() step blockSize) {
for (y in 0 until size.height.roundToInt() step blockSize) {
// 画像内での値に変換
val imageX = (x * scale).roundToInt()
val imageY = (y * scale).roundToInt()
val imageBlockSize = (blockSize * scale).roundToInt()
// 読み込む範囲を画像サイズ内に収める
val blockWidth = if (imageX + imageBlockSize < image.width) {
imageBlockSize
} else {
image.width - imageX
}
val blockHeight = if (imageY + imageBlockSize < image.height) {
imageBlockSize
} else {
image.height - imageY
}
// BitmapImageから指定範囲を読み込み
val buffer = IntArray(blockWidth * blockHeight)
image.readPixels(
buffer = buffer,
startX = imageX,
startY = imageY,
width = blockWidth,
height = blockHeight
)
// 範囲内の色を平均化しブロックを描画
drawRect(
averagingColor(buffer),
Offset(x.toFloat(), y.toFloat()),
Size(blockSize.toFloat(), blockSize.toFloat())
)
}
}
} else {
// ブロックサイズが小さい場合は画像をそのまま描画
drawImage(
image = image,
dstSize = IntSize(
size.width.roundToInt(),
size.height.roundToInt()
)
)
}
}
}
/**
* 平均化したColorを返す
*/
private fun averagingColor(
buffer: IntArray
): Color {
var r = 0L
var g = 0L
var b = 0L
buffer.forEach {
r += (it shr 16) and 0xff
g += (it shr 8) and 0xff
b += it and 0xff
}
return Color(
(r / buffer.size).toInt(),
(g / buffer.size).toInt(),
(b / buffer.size).toInt())
}
呼び出し方
MainContent.kt
@Composable
fun MainContent() {
val bitmap = ImageBitmap.imageResource(R.drawable.gundam)
var blockSize by remember { mutableIntStateOf(0) }
val mosaicPainter = remember(blockSize) {
MosaicPainter(bitmap, blockSize)
}
Box() {
Image(
modifier = Modifier.align(Alignment.Center),
painter = mosaicPainter,
contentDescription = null
)
Box(
modifier = Modifier
.wrapContentSize()
.align(Alignment.BottomCenter)
.padding(all = 32.dp)
.clip(shape = RoundedCornerShape(50))
.background(color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f))
) {
Slider(
modifier = Modifier
.align(Alignment.Center)
.padding(horizontal = 16.dp),
value = blockSize.toFloat(),
valueRange = 0f..100f,
steps = 9,
onValueChange = { blockSize = it.roundToInt() }
)
}
}
}
実際の動作
画像はららぽーとで撮った𝛎ガンダムです。
作ってみて
ブロックが小さくなると若干もたつく感じはするものの、思ってたよりはスムーズに動作しました。ただ、カスタム ペインタのページに下記の注意書きがあるため、DrawModifierを使った実装も試してみたいと思います。
測定またはレイアウトに影響を与える必要がある場合は、Painter を使用する必要があります。指定の境界内でしかレンダリングを行わない場合は、DrawModifier を使用してください。