Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Jetpack Composeで画像クロップ機能を実装する

Jetpack Composeで画像クロップ機能を実装する

Moyuru Aizawa

July 14, 2023
Tweet

More Decks by Moyuru Aizawa

Other Decks in Programming

Transcript

  1. Jetpack ComposeͰ


    ը૾ΫϩοϓػೳΛ࣮૷͢Δ
    @MoyuruAizawa

    View Slide

  2. Moyuru Aizawa
    Software Engineer of Catlog, RABO.
    Previously at Azit, CyberAgent, and Eureka.
    Love Metal, Hardcore and EDM.
    MoyuruAizawa

    View Slide

  3. github.com/MoyuruAizawa/Cropify

    View Slide

  4. ‣ Jetpack ComposeͰ࢖͑ΔImage Cropper͕ཉ͍͠


    ‣ ArthurHub/Android-Image-Cropper


    ‣ ViewͷੈքͰ͓ੈ࿩ʹͳͬͯͨ


    ‣ Compose + AndroidViewͰ࢖͑ͳ͍ (ಉ྅͕ݕূͨ݁͠Ռͦ͏ݴͬͯͨΑ)


    ‣ SmartToolFactory/Compose-Cropper


    ‣ Android-Image-Cropperͱૢ࡞ײ͕ҟͳΔ


    ‣ BitmapͷαϯϓϦϯάಡΈࠐΈʹରԠ͍ͯ͠ͳ͍
    Motivation

    View Slide

  5. ࣗ࡞͢Δ͔…!!

    View Slide

  6. 1. αϯϓϦϯάͨ͠BitmapΛϩʔυ͢Δ


    2. CanvasʹBitmapΛඳը͢Δ


    3. Canvasͷ্ʹΫϩοϓϑϨʔϜΛඳը͢Δ


    4. Ϣʔβʔૢ࡞ʹैͬͯΫϩοϓϑϨʔϜΛҠಈ/֦ॖ͢Δ


    5. ϑϨʔϜͷ࠲ඪͱαΠζΛऔͬͯBitmap͔Βը૾Λൈ͖औΔ
    ࣮૷ͷ֓ཁ

    View Slide

  7. αϯϓϦϯάͨ͠BitmapΛϩʔ
    υ͢Δ

    View Slide

  8. ‣ Image/Canvas/ImageViewΑΓ΋ང͔ʹେ͖͍ը૾Λ
    ͦͷ··දࣔ͢Δͷ͸ϝϞϦͷແବ


    ‣ αϯϓϦϯάͯ͠ը૾ΛBitmapʹ͓ͤ͜͹ޮ཰త



    🔍 AndroidDevelopers ”Loading Large Bitmaps Efficiently”
    αϯϓϦϯάͨ͠BitmapΛϩʔυ͢Δ

    View Slide

  9. CanvasʹBitmapΛදࣔ͢Δ

    View Slide

  10. Canvas(modifier = modifier) {
    drawRect(option.backgroundColor)
    drawImage(
    image = bitmap,
    dstSize = size.toInt(),
    dstOffset = offset.toInt(),
    )
    }
    CanvasʹBitmapΛදࣔ͢Δ

    View Slide

  11. Canvas(modifier = modifier) {
    drawRect(option.backgroundColor)
    drawImage(
    image = bitmap,
    dstSize = size.toInt(),
    dstOffset = offset.toInt(),
    )
    }
    എܠΛCanvas͍ͬͺʹඳը

    View Slide

  12. Canvas(modifier = modifier) {
    drawRect(option.backgroundColor)
    drawImage(
    image = bitmap,
    dstSize = size.toInt(),
    dstOffset = offset.toInt(),
    )
    }
    CanvasʹBitmapΛFitCenterʹͳΔΑ͏ʹඳը

    View Slide

  13. Canvasͷ্ʹ


    ΫϩοϓϑϨʔϜΛඳը͢Δ

    View Slide

  14. ‣ ΫϩοϓϑϨʔϜΛඳը͢Δ


    ‣ ΫϩοϓϑϨʔϜͷ֎ଆ͸҉͘͢Δ


    ‣ PorterDuff
    Canvasͷ্ʹΫϩοϓϑϨʔϜΛඳը͢Δ

    View Slide

  15. Canvas(modifier = modifier) {
    with(drawContext.canvas.nativeCanvas) {
    val checkPoint = saveLayer(null, null)
    drawRect(
    color = option.maskColor,
    alpha = option.maskAlpha,
    )
    drawRect(
    color = Color.Transparent,
    topLeft = offset,
    size = size,
    blendMode = BlendMode.SrcOut
    )
    restoreToCount(checkPoint)
    drawFrame(offset, size, option)
    }

    }
    Canvasͷ্ʹΫϩοϓϑϨʔϜΛඳը͢Δ

    View Slide

  16. Canvas(modifier = modifier) {
    with(drawContext.canvas.nativeCanvas) {
    val checkPoint = saveLayer(null, null)
    drawRect(
    color = option.maskColor,
    alpha = option.maskAlpha,
    )
    drawRect(
    color = Color.Transparent,
    topLeft = offset,
    size = size,
    blendMode = BlendMode.SrcOut
    )
    restoreToCount(checkPoint)
    drawFrame(offset, size, option)
    }
    }
    CanvasશମΛdrawRectͰϚεΫ͢Δ

    View Slide

  17. Canvas(modifier = modifier) {
    with(drawContext.canvas.nativeCanvas) {
    val checkPoint = saveLayer(null, null)
    drawRect(
    color = option.maskColor,
    alpha = option.maskAlpha,
    )
    drawRect(
    color = Color.Transparent,
    topLeft = offset,
    size = size,
    blendMode = BlendMode.SrcOut
    )
    restoreToCount(checkPoint)
    drawFrame(offset, size, option)
    }

    }
    ϑϨʔϜͷ಺ଆ͚ͩSrcOutͰ͘Γൈ͘
    🔍 AndroidDevelopers “PorterDuff.Mode”


    🔍 AndroidDevelopers “BlendMode”

    View Slide

  18. Canvas(modifier = modifier) {
    with(drawContext.canvas.nativeCanvas) {
    val checkPoint = saveLayer(null, null)
    drawRect(
    color = option.maskColor,
    alpha = option.maskAlpha,
    )
    drawRect(
    color = Color.Transparent,
    topLeft = offset,
    size = size,
    blendMode = BlendMode.SrcOut
    )
    restoreToCount(checkPoint)
    drawFrame(offset, size, option)
    }
    }
    PorterDuffͰmask͚ͩΛ͘Γൈͨ͘ΊʹlayerΛઃఆ͓ͯ͘͠

    View Slide

  19. Canvas(modifier = modifier) {
    with(drawContext.canvas.nativeCanvas) {
    val checkPoint = saveLayer(null, null)
    drawRect(
    color = option.maskColor,
    alpha = option.maskAlpha,
    )
    drawRect(
    color = Color.Transparent,
    topLeft = offset,
    size = size,
    blendMode = BlendMode.SrcOut
    )
    restoreToCount(checkPoint)
    drawFrame(offset, size, option)
    }

    }
    ϑϨʔϜΛඳը͢Δ

    View Slide

  20. Ϣʔβʔૢ࡞ʹैͬͯ
    ΫϩοϓϑϨʔϜΛҠಈ/֦ॖ͢Δ

    View Slide

  21. ‣ Modifier#pointerInput


    ‣ PointerInputScope#detectDragGestures


    ‣ ͜ͷ2ͭΛ࢖ͬͯϢʔβʔͷδΣενϟʔΛ͞͹͘


    ‣ ϑϨʔϜͷ࠲ඪ/αΠζ Ŋ ը૾ͷ࠲ඪ/αΠζΛߟྀ͢Δඞཁ͕͋ΔŇ
    Stateͱ͓ͯ࣋ͬͯ͘͠Ň
    ΫϩοϓϑϨʔϜͷҠಈ/֦ॖ
    class CropifyState {
    internal var frameRect by mutableStateOf(Rect(0f, 0f, 0f, 0f))
    internal var imageRect by mutableStateOf(Rect(0f, 0f, 0f, 0f))
    }

    View Slide

  22. modifier
    .pointerInput(bitmap, option.frameAspectRatio) {
    detectDragGestures(
    onDragStart = { … },
    onDragEnd = { … },
    onDrag = { … }
    )
    }
    ΫϩοϓϑϨʔϜͷҠಈ/֦ॖ

    View Slide

  23. modifier
    .pointerInput(bitmap, option.frameAspectRatio) {
    detectDragGestures(
    onDragStart = { … },
    onDragEnd = { … },
    onDrag = { … }
    )
    }
    ϑϨʔϜͷͲ͜Λ৮ͬͨͷ͔Λ൑ผ͢Δ

    View Slide

  24. fun detectTouchRegion(
    tapPosition: Offset, frameRect: Rect, tolerance: Float
    ): TouchRegion? {
    return when {
    Rect(frameRect.topLeft, tolerance)

    .contains(tapPosition) -> TouchRegion.Vertex.TOP_LEFT
    // தུ
    Rect(frameRect.center, frameRect.width / 2 - tolerance)

    .contains(tapPosition) -> TouchRegion.Inside
    else -> null
    }
    }
    ϑϨʔϜͷ࠲ඪͱλον࠲ඪΛൺֱͯ͠TouchRegionΛܭࢉ

    View Slide

  25. modifier
    .pointerInput(bitmap, option.frameAspectRatio) {
    detectDragGestures(
    onDragStart = { … },
    onDragEnd = { … },
    onDrag = { … }
    )
    }
    ϑϨʔϜͷ֦ॖ/ҠಈΛߦ͏

    View Slide

  26. onDrag = { change, dragAmount ->
    touchRegion?.let {
    when (it) {
    is TouchRegion.Vertex -> state.scaleFrameRect(…)
    TouchRegion.Inside -> state.translateFrameRect(…)
    }
    change.consume()
    }
    }
    TouchRegionͱdragAmountΛΈͯϑϨʔϜΛ֦ॖ/Ҡಈ

    View Slide

  27. ‣ ϑϨʔϜ͸ը૾ͷ֎ʹग़ͯ͸͍͚ͳ͍


    ‣ ϑϨʔϜ֦ॖͰ௖఺࠲ඪΛҠಈ͢Δ৔߹
    ྡͷ௖఺Λ௒͑ͯ͸ͳΒͳ͍


    ‣ ΞεϖΫτൺݻఆͷϑϨʔϜ֦ॖͰ௖఺࠲ඪΛҠಈ͢Δ৔߹
    ͍͍ײ͡ʹΞεϖΫτൺΛҡ࣋ͯ͠௖఺࠲ඪΛಈ͔͢


    ϑϨʔϜͷ֦ॖ͸ͪΐͬͱͩΔ͍

    View Slide

  28. Bitmap͔Βը૾Λ੾Γൈ͘

    View Slide

  29. private suspend fun cropImage(
    bitmap: ImageBitmap,
    frameRect: Rect,
    imageRect: Rect,
    ): ImageBitmap {
    return withContext(Dispatchers.IO) {
    val scale = bitmap.width / imageRect.width
    Bitmap.createBitmap(
    bitmap.asAndroidBitmap(),
    ((frameRect.left - imageRect.left) * scale).roundToInt(),
    ((frameRect.top - imageRect.top) * scale).roundToInt(),
    (frameRect.width * scale).roundToInt(),
    (frameRect.height * scale).roundToInt(),
    ).asImageBitmap()
    }
    }
    Bitmap͔Βը૾Λ੾Γൈ͘

    View Slide

  30. ؆୯ʹImage Cropper࡞Εͨ🥰

    View Slide

  31. Thank you

    View Slide