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. Moyuru Aizawa Software Engineer of Catlog, RABO. Previously at Azit,

    CyberAgent, and Eureka. Love Metal, Hardcore and EDM. MoyuruAizawa
  2. ‣ Jetpack ComposeͰ࢖͑ΔImage Cropper͕ཉ͍͠ ‣ ArthurHub/Android-Image-Cropper ‣ ViewͷੈքͰ͓ੈ࿩ʹͳͬͯͨ ‣ Compose

    + AndroidViewͰ࢖͑ͳ͍ (ಉ྅͕ݕূͨ݁͠Ռͦ͏ݴͬͯͨΑ) ‣ SmartToolFactory/Compose-Cropper ‣ Android-Image-Cropperͱૢ࡞ײ͕ҟͳΔ ‣ BitmapͷαϯϓϦϯάಡΈࠐΈʹରԠ͍ͯ͠ͳ͍ Motivation
  3. Canvas(modifier = modifier) { drawRect(option.backgroundColor) drawImage( image = bitmap, dstSize

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

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

    = size.toInt(), dstOffset = offset.toInt(), ) } CanvasʹBitmapΛFitCenterʹͳΔΑ͏ʹඳը
  6. 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ͷ্ʹΫϩοϓϑϨʔϜΛඳը͢Δ
  7. 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ͰϚεΫ͢Δ
  8. 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”
  9. 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Λઃఆ͓ͯ͘͠
  10. 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) }
 } ϑϨʔϜΛඳը͢Δ
  11. ‣ 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)) }
  12. modifier .pointerInput(bitmap, option.frameAspectRatio) { detectDragGestures( onDragStart = { … },

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

    onDragEnd = { … }, onDrag = { … } ) } ϑϨʔϜͷͲ͜Λ৮ͬͨͷ͔Λ൑ผ͢Δ
  14. 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Λܭࢉ
  15. modifier .pointerInput(bitmap, option.frameAspectRatio) { detectDragGestures( onDragStart = { … },

    onDragEnd = { … }, onDrag = { … } ) } ϑϨʔϜͷ֦ॖ/ҠಈΛߦ͏
  16. onDrag = { change, dragAmount -> touchRegion?.let { when (it)

    { is TouchRegion.Vertex -> state.scaleFrameRect(…) TouchRegion.Inside -> state.translateFrameRect(…) } change.consume() } } TouchRegionͱdragAmountΛΈͯϑϨʔϜΛ֦ॖ/Ҡಈ
  17. 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͔Βը૾Λ੾Γൈ͘