[Android Compose] 이미지 가져와서 표시하는 법
Compose에서 이미지를 가져와 리스트 형태로 표시하는 예제를 다룬 영상이 있어 공부한 후 포스팅한다. 링크는 아래 남긴다.
https://www.youtube.com/watch?v=uHX5NB6wHao&t=102s
이미지 처리 라이브러리는 Coil을 사용한다. 오늘 날짜인 23.08.13 기준 최신 버전은 아래와 같으니 참고한다.
implementation("io.coil-kt:coil-compose:2.4.0")
그리고 에뮬레이터로 안드로이드 12, 13 기기에서 어떻게 보이는지 확인했기 때문에 무료 이미지 몇 장을 준비해서 에뮬레이터에 옮겨둔 다음 진행했다. 에뮬레이터에 사진 파일을 넣는 방법은 아래 순서대로 따라하면 된다.
이 과정은 당연히 하나의 에뮬레이터에만 적용되며 한 에뮬레이터에 옮겼다고 다른 에뮬레이터에도 적용되지는 않는다. 여러 에뮬레이터에서 확인할 거라면 아래 과정들을 일일이 반복해줘야 한다.
- 안드로이드 스튜디오의 View > Tool Windows > Device Explorer 순으로 클릭
- storage > emulated > 0 > Pictures 패키지에 옮기길 원하는 이미지들을 드래그 앤 드랍으로 이동
별 거 없다. 아래는 1번 과정을 캡쳐한 사진이다.
아래는 2번 과정을 캡쳐한 사진이다.
이제 밑준비가 끝났으니 코드를 작성한다.
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import coil.compose.AsyncImage
import com.example.composeprac.ui.theme.ComposePracTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposePracTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
var selectedImageUri by remember {
mutableStateOf<Uri?>(null)
}
var selectedImageUris by remember {
mutableStateOf<List<Uri>>(emptyList())
}
val singlePhotoPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
selectedImageUri = uri
}
)
val multiplePhotoPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickMultipleVisualMedia(),
onResult = { uris ->
selectedImageUris = uris
}
)
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround
) {
Button(onClick = {
singlePhotoPickerLauncher.launch(
PickVisualMediaRequest(
ActivityResultContracts.PickVisualMedia.ImageOnly
)
)
}) {
Text("사진 1장 선택")
}
Button(onClick = {
multiplePhotoPickerLauncher.launch(
PickVisualMediaRequest(
ActivityResultContracts.PickVisualMedia.ImageOnly
)
)
}) {
Text("사진 여러 장 선택")
}
}
}
item {
AsyncImage(
model = selectedImageUri,
contentDescription = null,
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.Crop
)
}
items(selectedImageUris) { uri ->
AsyncImage(
model = uri,
contentDescription = null,
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.Crop
)
}
}
}
}
}
}
}
이걸 작동시키면 안드로이드 12 기기에선 아래와 같이 작동한다. 갤러리의 사진들이 아니라 다운로드한 이미지들을 에뮬레이터에 넣었기 때문에 갤러리 기준으로 표시되지는 않으며, 여러 사진들을 선택하려면 아무 사진이나 꾹 누르면 된다. 잠시 후 각 사진들의 우상단에 빈 원이 표시되면 여러 장을 선택할 수 있다.
안드로이드 13 기기에선 아래와 같이 표시된다.
이제 코드 분석을 해보자. 먼저 uri 형태로 가져온 이미지들을 담을 변수와 이미지를 가져올 때 사용할 런처를 만든다.
var selectedImageUri by remember {
mutableStateOf<Uri?>(null)
}
var selectedImageUris by remember {
mutableStateOf<List<Uri>>(emptyList())
}
val singlePhotoPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
selectedImageUri = uri
}
)
val multiplePhotoPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickMultipleVisualMedia(),
onResult = { uris ->
selectedImageUris = uris
}
)
Compose 없이 startActivityForResult() 이후 결과를 가져올 때 사용할 수 있는 새 API인 registerForActivityResult()가 있는데 이것의 Compose 버전이라고 생각하면 된다. Contract를 설정한 다음 선택한 사진이 들어오는 onResult 콜백 함수를 통해 단일 사진 또는 여러 사진의 uri를 앞서 만든 변수에 각각 담는다.
PickVisualMedia, PickMultipleVisualMedia는 각각 Compose에서 사진 선택기를 열기 위해 필요한 open class다. 관련 디벨로퍼 공식문서는 아래를 참고하며 두 클래스의 설명이 비슷하기 때문에 PickVisualMedia 링크만 가져왔다.
사진 선택기를 써서 단일 이미지, 동영상 또는 기타 시각적 미디어를 선택하는 ActivityResultContract다. 이 계약은 사용 가능한 경우 MediaStore.ACTION_PICK_IMAGES를 통해 사용할 수 있는 Photo Picker가 제공하는 시스템 프레임워크를 항상 선호하지만, 모든 안드로이드 API 레벨 19 이상의 기기에서 일관된 API를 보장하기 위해 사용할 수 없는 기기에 대한 폴백(=유사한 기능을 갖고 있어서 대체 가능한 솔루션)도 제공한다...(중략)...input은 PickVisualMediaRequest다. output은 유저가 미디어를 선택한 경우 uri고 아무것도 선택하지 않았다면 null이다. Photo Picker가 리턴한 URI는 쓸 수 없다. super.createIntent()에 의해 생성된 인텐트에 추가 엑스트라를 전달하려면 createIntent()를 재정의하기 위해 상속될 수 있다
@RequiresApi(value = 19)
public class ActivityResultContracts.PickVisualMedia extends ActivityResultContract
API 레벨 19부터 지원하기 때문에 최소 API 레벨 21이 필요한 Compose를 사용해 앱을 개발한다면 문제없이 사용할 수 있는 API다.
이 이상의 자세한 정보나 상세 코드는 필요하다면 알아서 찾아보고, 다음 코드를 확인한다.
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround
) {
Button(onClick = {
singlePhotoPickerLauncher.launch(
PickVisualMediaRequest(
ActivityResultContracts.PickVisualMedia.ImageOnly
)
)
}) {
Text("사진 1장 선택")
}
Button(onClick = {
multiplePhotoPickerLauncher.launch(
PickVisualMediaRequest(
ActivityResultContracts.PickVisualMedia.ImageOnly
)
)
}) {
Text("사진 여러 장 선택")
}
}
}
버튼을 2개 만들고 하나는 단일 사진을 선택하기 위한 버튼, 다른 하나는 여러 사진을 선택하기 위한 버튼으로 설정한다.
그리고 각 버튼의 클릭 리스너에 앞서 설정한 런처를 통해 launch()를 호출한다. 각 계약 클래스에서 요구하는 Request 클래스를 만들어서 서 launch()의 매개변수로 넘긴다.
위 코드에선 이미지만 선택하도록 ImageOnly를 사용했는데, 이외에도 사용할 수 있는 object가 2개 더 있다.
영상도 같이 선택해야 하거나 영상만 선택해야 한다면 다른 걸 사용하면 되겠다.
item {
AsyncImage(
model = selectedImageUri,
contentDescription = null,
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.Crop
)
}
items(selectedImageUris) { uri ->
AsyncImage(
model = uri,
contentDescription = null,
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.Crop
)
}
그 다음 코드의 AsyncImage는 Coil 라이브러리에서 제공하는 요소다.
https://coil-kt.github.io/coil/compose/#asyncimage
AsyncImage는 이미지 요청을 비동기식으로 실행하고 결과를 렌더링하는 컴포저블이다. 표준 이미지 컴포저블과 같은 인수를 지원하며 추가로 placeholder / error / fallback 및 onLoading / onSuccess / onError 콜백 설정을 지원한다
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data("https://example.com/image.jpg")
.crossfade(true)
.build(),
placeholder = painterResource(R.drawable.placeholder),
contentDescription = stringResource(R.string.description),
contentScale = ContentScale.Crop,
modifier = Modifier.clip(CircleShape)
)
단일 이미지를 선택할 때마다 그 이미지의 uri를 가져와서 가로로 꽉 차도록 AsyncImage 안에 렌더링한다. 기존에 선택한 이미지가 있다면 그것을 덮어씌운다. 여러 이미지를 선택했다면 선택한 이미지들을 가져와서 기존의 이미지들을 없애고 새로 선택한 이미지들을 덮어씌운다.
이 예시를 실제로 사용하려면 여러 예외처리와 로직 커스텀이 필요하겠지만 이를 바탕으로 간단한 이미지 앱에 활용할 수 있을 것이다.