Hanbit the Developer
[Kotlin] Android Custom Gallery Image Picker 만들기 본문
배경
안드로이드에서 Multiple Image Picker를 구현하려면 직접 구현해야만 합니다. TedPicker 같이 매우 좋은 라이브러리가 이미 나와있으나, 저 같은 경우에는 진행중인 프로젝트의 디자인 통일성을 위해 구현의 길을 택하게 되었습니다😅
이 글에서 이러한 커스텀 ImagePicker의 구현을 핵심 위주로 설명하고자 합니다. 미리 보는 결과물은 다음과 같습니다.
기본적으로 MVVM 아키텍처 및 Jetpack Navigation을 사용하였습니다.
구현
해야할 것은 대략 다음과 같습니다.
0. 권한 요청 처리
1. ImagePickerFragment
2. Item 정의
3. ViewModel 구현(⭐️)
4. RecyclerView 구현 및 ViewModel 연결
5. 선택한 사진을 외부 프래그먼트에 전달
ImagePickerFragment에 Image RecyclerView를 구현하고, ViewModel에서 ContentResolver를 통해 이미지를 불러오는 로직을 구현하는 구현한 뒤, RecyclerView와 ViewModel을 연결시키면 됩니다.
여기서 굵게 표시된 내용들을 설명하겠습니다.(모두가 같은 디자인패턴을 사용하는 것이 아니므로 무의미한 설명이 될 것 같기 때문입니다.)
Item 정의
ImageItem.kt
data class ImageItem(
var uri: Uri,
var isChecked: Boolean
)
item_image.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="137dp"
android:layout_height="137dp">
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<CheckBox
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
[⭐️] ViewModel 구현(중요)
ImagePickerViewModel.kt
import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.customimagepicker.data.ImageItem
import java.io.File
private const val INDEX_MEDIA_ID = MediaStore.MediaColumns._ID
private const val INDEX_MEDIA_URI = MediaStore.MediaColumns.DATA
private const val INDEX_ALBUM_NAME = MediaStore.Images.Media.BUCKET_DISPLAY_NAME
private const val INDEX_DATE_ADDED = MediaStore.MediaColumns.DATE_ADDED
class ImagePickerViewModel: ViewModel() {
val imageItemList = MutableLiveData<MutableList<ImageItem>>(mutableListOf())
@SuppressLint("Range")
fun fetchImageItemList(context: Context) {
val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(
INDEX_MEDIA_ID,
INDEX_MEDIA_URI,
INDEX_ALBUM_NAME,
INDEX_DATE_ADDED
)
val selection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) MediaStore.Images.Media.SIZE + " > 0"
else null
val sortOrder = "$INDEX_DATE_ADDED DESC"
val cursor = context.contentResolver.query(uri, projection, selection, null, sortOrder)
cursor?.let {
while(cursor.moveToNext()) {
val mediaPath = cursor.getString(cursor.getColumnIndex(INDEX_MEDIA_URI))
imageItemList.value!!.add(
ImageItem(Uri.fromFile(File(mediaPath)), false)
)
}
}
cursor?.close()
}
fun getCheckedImageUriList(): MutableList<String> {
val checkedImageUriList = mutableListOf<String>()
imageItemList.value?.let {
for(imageItem in imageItemList.value!!) {
if(imageItem.isChecked) checkedImageUriList.add(imageItem.uri.toString())
}
}
return checkedImageUriList
}
}
이 글의 핵심 코드이며, ContentResolver에서 제공하는 쿼리를 통해 갤러리 내 사진들을 긁어올 수 있습니다. 긁어온 데이터를 실시간으로 LiveData에 추가하게 되고, 이 데이터를 observe하는 RecyclerView가 자동으로 아이템을 추가하게 되는 구조입니다.
ContentResolver의 구체적인 구현에 대한 설명은 래퍼런스를 남깁니다.
https://developer.android.com/guide/topics/providers/content-provider-basics?hl=ko
추가로 유의해야할 점은, 체크박스를 클릭했을 때 ImagePickerViewModel의 imageItemList의 값 또한 변해야 한다는 점입니다. 이를 위해 리스너 처리를 해주어야 합니다. 저는 onCreateViewHolder 내부에서 아래와 같이 처리를 해주었습니다.
binding.checkbox.setOnCheckedChangeListener { _, isChecked ->
parentViewModel.imageItemList.value?.let {
val position = holder.absoluteAdapterPosition
it[position].isChecked = isChecked
}
}
parentViewModel은 ImageAdapter의 생성자를 통해 받은 ImagePickerViewModel입니다.
기타 구현
HomeFragment: 선택한 데이터를 받는 로직이 중요합니다.
https://github.com/hanbikan/custom-image-picker/blob/main/app/src/main/java/com/example/customimagepicker/HomeFragment.kt
ImagePickerFragment: 선택한 데이터를 보내는 로직이 중요합니다.
https://github.com/hanbikan/custom-image-picker/blob/main/app/src/main/java/com/example/customimagepicker/ImagePickerFragment.kt
ImageAdapter: 체크박스를 리스닝하여 뷰모델 값을 갱신시키는 로직이 중요합니다.
https://github.com/hanbikan/custom-image-picker/blob/main/app/src/main/java/com/example/customimagepicker/adapter/ImageAdapter.kt
마무리
스크롤, 영상, 카메라, 선택 개수 표현 등 추가할 내용들이 매우 많지만, 컴팩트하게 기본만을 담은 가벼운 repo를 만들고자 하였습니다.
해당 프로젝트를 담은 repository 링크는 다음과 같습니다.
https://github.com/hanbikan/custom-image-picker
참고
https://github.com/ParkSangGwon/TedPicker
'Android' 카테고리의 다른 글
[Trouble shooting] TabLayout customView에 setTypeface를 주면 indicator의 값이 잠시 0으로 리셋되는 문제 (0) | 2022.08.02 |
---|---|
[Kotlin] Custom Bottom Navigation With Animation (0) | 2022.08.01 |
[Kotlin] BottomNavigationView with multiple navigation (0) | 2022.07.19 |
Android AAR Library 추가 방법 (0) | 2022.07.07 |
[Kotlin] Multiple View Types를 지원하는 RecyclerView 구현 (0) | 2022.07.02 |