Hanbit the Developer

[Kotlin] Android Custom Gallery Image Picker 만들기 본문

Mobile/Android

[Kotlin] Android Custom Gallery Image Picker 만들기

hanbikan 2022. 7. 23. 15:14

배경

안드로이드에서 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 

 

콘텐츠 제공자 기본 사항  |  Android 개발자  |  Android Developers

콘텐츠 제공자 기본 사항 콘텐츠 제공자는 중앙 저장소로의 데이터 액세스를 관리합니다. 제공자는 Android 애플리케이션의 일부이며, 대개 데이터 작업을 위한 고유의 UI를 제공합니다. 그러나

developer.android.com

 

추가로 유의해야할 점은, 체크박스를 클릭했을 때 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

 

GitHub - hanbikan/custom-image-picker: A simple image picker.

A simple image picker. Contribute to hanbikan/custom-image-picker development by creating an account on GitHub.

github.com

 

참고

https://github.com/ParkSangGwon/TedPicker

 

GitHub - ParkSangGwon/TedPicker: Multiple image select library for Android. Take a picture or Select from gallary

Multiple image select library for Android. Take a picture or Select from gallary - GitHub - ParkSangGwon/TedPicker: Multiple image select library for Android. Take a picture or Select from gallary

github.com