프로젝트 요구사항

  • 네이버 맵을 사용해서 달린 경로를 지도와 함께 보여줘야 함(포스팅 기능)

네이버 지도 API PathOverlay.setCoords() 를 활용해서 쉽게 구현 가능 하지만 지도 대신 사용자가 원하는 이미지로 배경 선택 가능 하도록 만들어야 한다.

아래 사진이 해당 기능 모습이다.

image




시도했던 방법…

각 아이탬마다 NaverMap 을 보여준다

아래 제공되는 API 들로 지도 객체를 하나의 아이탬으로 보고 배경 이미지, 경로 등을 그리는 방법이다.

PathOverlay.setCoords() // 경로 생성 가능
GroundOverlay.setImage() // 배경 이미지 선택 가능


장점 : 일관된 방식으로 지도, 사진을 모두 배경으로 사용할 수 있으며 경로 또한 일관된 방식으로 보여줄 수 있다.

단점 : 앱 성능 저하, 렌더링 과부화, 메모리 누수

아이탬마다 네이버 지도 객체를 생성하고 이미지와 경로를 다시 그려야 하기 때문에 성능, 메모리 누수 발생



채택된 개선 방안

네이버 지도 배경을 캡쳐해서 사용

달리기 결과를 확인하고 수정하는 페이지에서 지도 하나만을 사용, 포스팅할 땐 지도 스크린샷을 제공해서 저장한다.

아래 코드는 위 채택된 방법을 처음 구현했던 코드이다.

// The CompositionLocal containing the current Compose View.
val view = LocalView.current

// Composable의 위치 정보를 Rect로 받기
var composableBounds by remember {
    mutableStateOf<Rect?>(null)
}
Modifier.onGloballyPositioned {
    composableBounds = it.boundsInWindow()
}

LaunchedEffect(true) {
    val bmp = Bitmap.createBitmap(
        view.width,
        view.height,
        Bitmap.Config.ARGB_8888,
    ).applyCanvas {
        view.draw(this)
    }
}


  • 컴포저블 위치 정보를 Rect로 받는 법 OnGloballyPositionedModifier는 Modifier의 일종으로, 콘텐츠의 전역 포지션이 변경되었을 때 레이아웃의 최종 LayoutCoordinates와 함께 onGloballyPositioned 콜백을 호출한다. 좌표를 포함하고 있는 이 콜백은 Composition(구성)이 끝났을 때 호출 됨을 명심하자.
Column(
    Modifier.onGloballyPositioned { coordinates ->
        // Column의 사이즈
        coordinates.size
        // 애플리케이션 윈도우에 상대적인 Column의 포지션
        coordinates.positionInWindow()
        // 컴포즈 최상위에 상대적인 Column의 포지션
        coordinates.positionInRoot()
        // 레이아웃에 제공되는 정렬 라인 (Column의 경우 비어있음)
        coordinates.providedAlignmentLines
        //  Column의 부모에 해당하는 LayoutCoordinates 인스턴스
        coordinates.parentLayoutCoordinates
    }
) {
...
}



예외 발생

java.lang.IllegalArgumentException: Software rendering doesn’t support hardware bitmaps

스크린샷 에러


HARDWARE BITMAP??

Android 공식 문서를 확인해보면 Harware Bitmap은 오직 그래픽 메모리안에 픽셀정보가 저장되며 오직 화면에 띄울때만 최적화되었다고 나온다.


Special configuration, when bitmap is stored only in graphic memory. Bitmaps in this configuration are always immutable. It is optimal for cases, when the only operation with the bitmap is to draw it on a screen.


일반적으로 애플리케이션 메모리(픽셀 바이트 배열)에 픽셀 데이터 사본 하나와 그래픽 메모리(픽셀이 GPU에 업로드된 후)에 하나의 사본이 있습니다. 하드웨어 비트맵은 GPU에 업로드된 복사본만 유지합니다.

그래픽 메모리에 픽셀 데이터를 저장한다는 것은 픽셀 데이터에 쉽게 액세스할 수 없다는 것을 의미하며, 경우에 따라 예외가 발생할 수 있습니다.


Solution

Glide 공식 문서Software rendering doesn't support hardware bitmaps 예외를 발생시킬 수 있는 상황과 일부 대처 방법을 정의했고 이에 따라 코드를 수정했다.

스크린샷을 위해서는 Android 8.0 Oreo(API 26) 이상 버전에서 PixelCopy.request()를 사용하면 된다.

    composableBounds = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        it.boundsInWindow()
    } else {
        it.boundsInRoot()
    }
    
    // Android 8.0 Oreo(API 26) 버전 이후는 PixelCopy.request() API를 사용해야함
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        PixelCopy.request(
            (view.context as Activity).window,
            bounds.toAndroidRect(),
            bitmap,
            {},
            Handler(Looper.getMainLooper())
        )
    } else {
        val canvas = Canvas(bitmap)
            .apply {
                translate(-bounds.left, -bounds.top)
            }
        this.draw(canvas)
        canvas.setBitmap(null)
    }



캡쳐 라이브러리 배포

컴포저블로 만든 화면을 캡쳐하는 코드를 재활용하기 위해 모듈화, 배포를 준비했다.

  • CaptureReusult : sealed Class로 Initialized, Success, Error 의 하위 타입을 갖는다.
sealed class CaptureResult {
    object Initialized : CaptureResult()
    data class Success internal constructor(val bitmap: Bitmap) : CaptureResult()
    data class Error internal constructor(val exception: Exception) : CaptureResult()
}


  • CaptureState : CaptureReusult state 형태로 갖고있으며 capture() 함수 노출해 Capturecontent를 캡쳐함.
class CaptureState internal constructor() {

    val state = mutableStateOf<CaptureResult>(CaptureResult.Initialized)

    //...
    
    fun capture() {
        captureBlock?.invoke()
    }
}

@Composable
fun rememberCaptureState() = remember {
    CaptureState()
}


  • Capture : @Composable function으로 CaptureState를 매개변수로 받고 contents가 캡쳐 가능하다. 캡쳐된 화면은 Bitmap의 형태로 captureState.state : CaptureReusult.Success(bitmap: Bitmap)에서 사용 가능하다.



CaptureComposable Github Repo

Library for capturing Composable components

  • app module is demo app

  • capture module is Android Library for capturing composable contains and contains CaptureState, CaptureResult, @Composable Capture

How to

To get a Git project into your build:

Step 1. Add the JitPack repository to your build file

Add it in your root build.gradle at the end of repositories:

// root level settings.gradle.kts
repositories {
    // ...
    maven("https://jitpack.io")
}

Step 2. Add the dependency

// module level build.gradle.kts 
dependencies {
    val latestVersion = "1.0.2" 
    implementation("com.github.yonghanJu:CaptureComposable:$latestVersion")
}


Example Code

val captureState = rememberCaptureState()

Capture(
    modifier = Modifier
    captureState = captureState,
) {
    // @Composable content
}

// Captured
Button(onClick = { captureState.capture() })

// ...

// you can use these
captureState.bitmap // captured bitmap
captureState.state // capturedState(Initialized, Success(bitmap), Error(e))

😎 동작화면