Android Compose - Coil 로 불러온 이미지의 비율 알아내기
Android Compose - Coil Image aspect ratio
비율 계산 코드 : 이동
문제 상황

이미지를 클릭하면 다이얼로그로 크롭되지 않은 원본 비율의 사진을 보여줘야 한다.
하지만 이미지가 불러와지기 전에 화면을 가득 채우는 현상이 있었다. (그 와중에 태극기는 바로 불러와지는 ㅋㅋ 🇰🇷 펄럭)
문제 상황
Dialog(onDismissRequest = { }) {  
    Surface(  
        modifier = Modifier.fillMaxWidth().wrapContentHeight(),  // <--
        shape = RoundedCornerShape(8.dp),  
        color = MaterialTheme.colorScheme.background,  
    ) {  
        PhotoDetail(  
            photo = photo,
            // ... 
        )  
    }  
}
다이얼로그를 띄우는 부분이다. Surface 로 width를 가득 채우고, height는 wrapContentHeight 로 설정하였다.
Column(  
    modifier = modifier  
        .wrapContentHeight()  
        .padding(bottom = 30.dp),  
    horizontalAlignment = Alignment.CenterHorizontally,  
) {  
    Box(  
        modifier = Modifier  
            .fillMaxWidth()  
            .wrapContentHeight(),  
    ) {
	    // 사진이 들어가는 부분
	    AsyncImage(  
		    model = photo.loadUrlMedium,  
		    contentDescription = "Photo detail : ${photo.title}",  
		    contentScale = ContentScale.FillWidth,  
		    modifier = Modifier.fillMaxWidth(),  
		)
	}
	// 텍스트와 버튼 그리고 공백
	
Column 을 wrapContentHeight 로 만들었다.
사진이 로드되기 전 wrapContentHeight 로 높이를 계산하지 못해 생기는 문제였다
해결 방법
- 사진 목록을 표시할 때 이미 한 번 사진을 로드한다. 이때 비율을 계산할 수 있다
- 비율을 계산한다
- 클릭 이벤트에서 계산된 비율이 반영된 모델을 넘겨준다
- 넘겨 받은 비율로 다이얼로그의 사진 부분의 비율을 미리 설정한다
그 결과 불필요한 계산 작업(wrapContentHeight)을 하지 않고도 깔끔하게 다이얼로그를 띄울 수 있다
Photo 모델에 ratio 프로퍼티 추가
data class PhotoUIModel(  
    val id: String = "",  
    val owner: String = "",  
    val secret: String = "",  
    val server: String = "",  
    val title: String = "",  
) {  
    val loadUrlSmall: String // 검색화면 표시용  
        get() = "https://live.staticflickr.com/$server/${id}_${secret}_n.jpg"  
    val loadUrlMedium: String // 다운로드 확인용  
        get() = "https://live.staticflickr.com/$server/${id}_$secret.jpg"  
    val loadUrlOriginal: String // 다운로드용  
        get() = "https://live.staticflickr.com/$server/${id}_${secret}_o.jpg"  
    val photoId: String  
        get() = "$owner/$id"  
  
    var ratio: Float = 1f  // <-- 새로 추가함
}
ratio 를 프로퍼티로 추가했고, 1f로 비율을 초기화하였다.
Coil 로 이미지 로드
Coil에서 이미지 UI를 표시하기 위해 보통 AsyncImage 컴포저블을 사용한다. 이렇게 하면 자체적으로 이미지를 표시할 수 있기 때문에 편리하지만 이미지의 속성이나 로딩 상태를 컨트롤 할 수 없다.
그래서 내부적으로 사용하는 rememberAsyncImagePainter 를 사용하게 되었다
단, rememberAsyncImagePainter 는 low-level API이기 때문에 변경될 가능성이 있다
비율 계산
val painter = rememberAsyncImagePainter(  
    model = photo.loadUrlSmall,  
    placeholder = painterResource(id = R.drawable.charlezzicon),  
)  
val imageRatio = remember(painter.state) {  
    val imageSize = painter.intrinsicSize  
    imageSize.width / imageSize.height  
}
rememberAsyncImagePainter 의 반환 값은 Coil의 AsyncImagePainter 타입이며, 이는 컴포즈의 Painter 클래스의 하위 타입이다.

AsyncImagePainter 는 이미지 로딩 State 를 가지고 있으며, State 는 다음의 속성들을 가지고 있다
imageRatio 에서는 remember 의 key 로 patiner.state 를 사용했다. 즉, 이미지 로딩이 완료되면 state가 Success 가 되고, 이때 imageRatio 를 계산한 값으로 업데이트 되기 때문에 최신의 비율을 받아볼 수 있게 된다.
Coil AsyncImagePainter 의 State
sealed class State {  
  
    /** The current painter being drawn by [AsyncImagePainter]. */  
    abstract val painter: Painter?  
  
    /** The request has not been started. */  
    object Empty : State() {  
        override val painter: Painter? get() = null  
    }  
  
    /** The request is in-progress. */  
    data class Loading(  
        override val painter: Painter?,  
    ) : State()  
  
    /** The request was successful. */  
    data class Success(  
        override val painter: Painter,  
        val result: SuccessResult,  
    ) : State()  
  
    /** The request failed due to [ErrorResult.throwable]. */  
    data class Error(  
        override val painter: Painter?,  
        val result: ErrorResult,  
    ) : State()  
}
이미지 표시하기 및 비율 반영
Box(  
    modifier = Modifier  
        .width(minSize)  
        .aspectRatio(1f)  
        .fillMaxSize(),  
) {  
    Image(  
        painter = painter,  
        contentDescription = null,  
        contentScale = ContentScale.Crop,  
        modifier = Modifier  
            .fillMaxSize()  
            .clickable { onClick(photo.apply { ratio = imageRatio }) },  
    )  
}
AsyncImagePainter 는 앞서 말했듯이 컴포즈의 Painter 타입이기 때문에 Image 컴포저블의 painter 파라미터로 사용할 수 있다.
클릭이 일어날 때 해당 모델의 ratio 속성으로 앞서 계산한 imageRatio 값을 적용하고 넘겨주게 된다.
다이얼로그 비율 설정하기
Column(  
    modifier = modifier  
        .padding(bottom = 30.dp)  
        .wrapContentHeight(),  
    horizontalAlignment = Alignment.CenterHorizontally,  
) {  
    Box(  
        modifier = Modifier.aspectRatio(photo.ratio),  
    ) { 
		// 사진이 들어가는 부분
        AsyncImage(  
            model = photo.loadUrlMedium,  
            contentDescription = "Photo detail : ${photo.title}",  
            contentScale = ContentScale.FillWidth,  
            modifier = Modifier.fillMaxWidth(),  
        )
	}
	// 텍스트와 버튼 그리고 공백
}
사진을 감싸는 Box 의 Modifier 에 aspectRatio 를 photo.ratio 로 지정한다.

그러면 위와 같이 아직 사진이 불러와지지 않은 상황에서도 불러와질 사진의 비율대로 미리 다이얼로그의 크기가 설정되어 표시된다.