Skip to content

Instantly share code, notes, and snippets.

@SmartToolFactory
Created September 7, 2022 09:55
Show Gist options
  • Save SmartToolFactory/8d17cdbe74d83e5fde28a3295f02cf62 to your computer and use it in GitHub Desktop.
Save SmartToolFactory/8d17cdbe74d83e5fde28a3295f02cf62 to your computer and use it in GitHub Desktop.

Revisions

  1. SmartToolFactory created this gist Sep 7, 2022.
    342 changes: 342 additions & 0 deletions CanvasDemo.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,342 @@
    package com.smarttoolfactory.composeimagecropper.demo

    import android.graphics.Bitmap
    import android.graphics.PorterDuff
    import android.graphics.PorterDuffXfermode
    import androidx.compose.foundation.*
    import androidx.compose.foundation.layout.*
    import androidx.compose.material.Text
    import androidx.compose.runtime.*
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.draw.alpha
    import androidx.compose.ui.geometry.Offset
    import androidx.compose.ui.graphics.*
    import androidx.compose.ui.layout.ContentScale
    import androidx.compose.ui.platform.LocalContext
    import androidx.compose.ui.res.imageResource
    import androidx.compose.ui.unit.IntSize
    import androidx.compose.ui.unit.dp
    import androidx.compose.ui.unit.sp
    import com.smarttoolfactory.composeimagecropper.R
    import com.smarttoolfactory.gesture.pointerMotionEvents
    import com.smarttoolfactory.imagecropper.createPolygonPath
    import kotlin.math.roundToInt

    @Composable
    fun CanvasDemo() {
    Column(
    modifier = Modifier
    .fillMaxSize()
    .verticalScroll(rememberScrollState())
    ) {

    val imageBitmap1 = ImageBitmap.imageResource(
    LocalContext.current.resources,
    R.drawable.landscape1
    ).asAndroidBitmap().copy(Bitmap.Config.ARGB_8888, true).asImageBitmap()


    val imageBitmap2 = ImageBitmap.imageResource(
    LocalContext.current.resources,
    R.drawable.landscape1
    ).asAndroidBitmap().copy(Bitmap.Config.ARGB_8888, true).asImageBitmap()

    val aspectRatio1 = imageBitmap1.width / imageBitmap1.height.toFloat()
    val aspectRatio2 = imageBitmap2.width / imageBitmap2.height.toFloat()

    Text("Native Canvas Clipping")
    // NativeCanvasSample1(
    // imageBitmap = imageBitmap1,
    // modifier = Modifier
    // .fillMaxWidth()
    // .aspectRatio(aspectRatio1)
    // )

    // Spacer(modifier = Modifier.height(50.dp))
    Text("Native Canvas BlendMode Clear")
    NativeCanvasSample2(
    imageBitmap = imageBitmap2,
    modifier = Modifier
    .fillMaxWidth()
    .aspectRatio(aspectRatio2)
    )
    //
    // val dstBitmap = ImageBitmap.imageResource(id = R.drawable.landscape1)
    // val aspectRatioDst = dstBitmap.width / dstBitmap.height.toFloat()
    //
    // Spacer(modifier = Modifier.height(50.dp))
    // Text("Compose Canvas BlendMode Clear")
    // ComposeCanvasSample(
    // imageBitmap = dstBitmap,
    // modifier = Modifier
    // .fillMaxWidth()
    // .aspectRatio(aspectRatioDst)
    // )
    //
    // Spacer(modifier = Modifier.height(50.dp))
    // Text("Compose Canvas Path + BlendMode Clear")
    // ComposeCanvasSample2(
    // imageBitmap = dstBitmap,
    // modifier = Modifier
    // .fillMaxWidth()
    // .aspectRatio(aspectRatioDst)
    // )
    }
    }


    // FIXME This one does not refresh correctly, here is the question i asked on Stackoverflow
    // https://stackoverflow.com/questions/72168588/jetpack-compose-androidx-compose-ui-graphics-canvas-not-refreshing-correctly-for
    @Composable
    fun NativeCanvasSample2(imageBitmap: ImageBitmap, modifier: Modifier) {


    BoxWithConstraints(modifier) {

    val imageWidth = constraints.maxWidth
    val imageHeight = constraints.maxHeight

    val bitmapWidth = imageBitmap.width
    val bitmapHeight = imageBitmap.height

    var offset by remember {
    mutableStateOf(Offset(bitmapWidth / 2f, bitmapHeight / 2f))
    }


    val canvasModifier = Modifier.pointerMotionEvents(
    Unit,
    onDown = {
    val position = it.position
    val offsetX = position.x * bitmapWidth / imageWidth
    val offsetY = position.y * bitmapHeight / imageHeight
    offset = Offset(offsetX, offsetY)
    it.consume()
    },
    onMove = {
    val position = it.position
    val offsetX = position.x * bitmapWidth / imageWidth
    val offsetY = position.y * bitmapHeight / imageHeight
    offset = Offset(offsetX, offsetY)
    it.consume()
    },
    delayAfterDownInMillis = 20
    )

    val canvas: Canvas = remember {
    Canvas(imageBitmap)
    }

    val paint = remember {
    android.graphics.Paint().apply {
    color = android.graphics.Color.TRANSPARENT
    xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
    }
    }

    val paintBg2 = remember {
    android.graphics.Paint().apply {
    color = 0x55000000
    }
    }

    val paintClear = remember {
    android.graphics.Paint().apply {
    color = android.graphics.Color.RED
    xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
    }
    }

    val paintBg = remember {
    Paint().apply {
    color = Color(0x55000000)
    }
    }

    val paintImage = remember {
    Paint().apply {
    }
    }

    canvas.apply {
    with(canvas.nativeCanvas) {
    val checkPoint = saveLayer(null, null)
    // this.drawColor(android.graphics.Color.TRANSPARENT, PorterDuff.Mode.MULTIPLY)
    // this.drawBitmap(imageBitmap.asAndroidBitmap(), 0f, 0f, paint)
    // this.drawRect(0f, 0f, canvasWidth, canvasHeight, paintBg2)
    this.drawCircle(200f, 100f, 100f, paintClear)
    restoreToCount(checkPoint)
    }
    }

    Image(
    modifier = canvasModifier.alpha(.99f),
    bitmap = imageBitmap,
    contentDescription = null,
    contentScale = ContentScale.FillBounds
    )

    Text(
    "Offset: $offset",
    modifier = Modifier.align(Alignment.BottomEnd),
    color = Color.White,
    fontSize = 16.sp
    )

    }
    }

    @Composable
    fun ComposeCanvasSample(modifier: Modifier, imageBitmap: ImageBitmap) {

    BoxWithConstraints(modifier) {
    val dstBitmap = imageBitmap

    val imageWidth = constraints.maxWidth
    val imageHeight = constraints.maxHeight

    val bitmapWidth = imageBitmap.width
    val bitmapHeight = imageBitmap.height

    var offset by remember {
    mutableStateOf(Offset(bitmapWidth / 2f, bitmapHeight / 2f))
    }

    val canvasModifier = Modifier.pointerMotionEvents(
    Unit,
    onDown = {
    val position = it.position
    val offsetX = (position.x).coerceIn(0f, imageWidth.toFloat())
    val offsetY = (position.y).coerceIn(0f, imageHeight.toFloat())
    offset = Offset(offsetX, offsetY)
    it.consume()
    },
    onMove = {
    val position = it.position
    val offsetX = (position.x).coerceIn(0f, imageWidth.toFloat())
    val offsetY = (position.y).coerceIn(0f, imageHeight.toFloat())
    offset = Offset(offsetX, offsetY)
    it.consume()
    },
    delayAfterDownInMillis = 20
    )

    Canvas(modifier = canvasModifier.fillMaxSize()) {
    val canvasWidth = size.width.roundToInt()
    val canvasHeight = size.height.roundToInt()

    drawImage(
    image = dstBitmap,
    srcSize = IntSize(dstBitmap.width, dstBitmap.height),
    dstSize = IntSize(canvasWidth, canvasHeight)
    )

    with(drawContext.canvas.nativeCanvas) {
    val checkPoint = saveLayer(null, null)

    // Destination
    drawRect(Color(0x55000000))

    // Source
    drawCircle(
    center = offset,
    color = Color.Blue,
    radius = canvasHeight.coerceAtMost(canvasWidth) / 8f,
    blendMode = BlendMode.Clear
    )
    restoreToCount(checkPoint)
    }
    }
    }
    }

    @Composable
    fun ComposeCanvasSample2(modifier: Modifier, imageBitmap: ImageBitmap) {

    val path = remember {
    Path()
    }

    BoxWithConstraints(modifier) {
    val dstBitmap = imageBitmap

    val imageWidth = constraints.maxWidth
    val imageHeight = constraints.maxHeight

    val bitmapWidth = imageBitmap.width
    val bitmapHeight = imageBitmap.height

    var offset by remember {
    mutableStateOf(Offset(bitmapWidth / 2f, bitmapHeight / 2f))
    }

    val canvasModifier = Modifier.pointerMotionEvents(
    Unit,
    onDown = {
    val position = it.position
    val offsetX = (position.x).coerceIn(0f, imageWidth.toFloat())
    val offsetY = (position.y).coerceIn(0f, imageHeight.toFloat())
    offset = Offset(offsetX, offsetY)
    path.moveTo(offset.x, offset.y)
    it.consume()
    },
    onMove = {
    val position = it.position
    val offsetX = (position.x).coerceIn(0f, imageWidth.toFloat())
    val offsetY = (position.y).coerceIn(0f, imageHeight.toFloat())
    offset = Offset(offsetX, offsetY)
    path.lineTo(offset.x, offset.y)
    it.consume()
    },
    onUp = {
    val position = it.position
    val offsetX = (position.x).coerceIn(0f, imageWidth.toFloat())
    val offsetY = (position.y).coerceIn(0f, imageHeight.toFloat())
    offset = Offset(offsetX, offsetY)
    path.lineTo(offset.x, offset.y)
    path.close()
    },
    delayAfterDownInMillis = 20
    )

    println("⛺️ canvasModifier: $canvasModifier, offset: $offset")

    Canvas(modifier = canvasModifier.fillMaxSize()) {
    val canvasWidth = size.width.roundToInt()
    val canvasHeight = size.height.roundToInt()

    drawImage(
    image = dstBitmap,
    srcSize = IntSize(dstBitmap.width, dstBitmap.height),
    dstSize = IntSize(canvasWidth, canvasHeight)
    )


    with(drawContext.canvas.nativeCanvas) {
    val checkPoint = saveLayer(null, null)

    // Destination
    drawRect(Color(0x55000000))

    // Source
    // drawCircle(
    // center = offset,
    // color = Color.Blue,
    // radius = canvasHeight.coerceAtMost(canvasWidth) / 8f,
    // blendMode = BlendMode.Clear
    // )

    drawPath(
    color = Color.Green,
    path = path,
    // style = Stroke(width = 1.dp.toPx()),
    blendMode = BlendMode.Clear
    )
    restoreToCount(checkPoint)
    }


    }
    }
    }