Android

커스텀 이미지피커 [+페이징,앨범] (10, 11 대응)

그란. 2021. 9. 7. 17:33

보통 이미지피커는 라이브러리를 사용했었다 ( 편하니까.. )

 

클라이언트가 아래와 같은 UI를 원한다.

 

처음에 전체 이미지들이 나오고 

전체보기를 눌렀을때 앨범들이 나온다

앨범을 누르면 그 앨범에 해당하는 이미지들이 나온다

( 그러면 다시 전체보기를 할수 없다. 전체보기 앨범이 없기 때문에 ) 시안대로.

 

 

사실 커서로 이미지를 가져온다는 개념은 알고있었으나

해보지 않았기 때문에 미지의 영역이었다.

처음엔 사용했었던 이미지피커를 import 해서 UI만 바꾸려고 했으나 

이번기회에 학습해보고 싶어 밑바닥부터 구현했다.

 

일단 참고한 라이브러리는 TedImagePicker, Pickle 

  페이징 쿼리 리스트 DataBinding
TedImagePicker X 쿼리 미사용
( 모든 이미지 항목들에 대해 필터링 )
Sequence 처리 사용
Picker 페이징3 라이브러리 사용 쿼리 사용 
(LIMIT,OFFSET은 사용하지 않음)
List 처리 미사용 ( 직접 연결 )

( 쿼리 미사용이란 의미는 WHERE, GROUP등의 조건절을 사용하지 않았다는 뜻이다 ) 

 

페이징이 필요한 이유 

초기 로딩 시간때문 : 이미지가 10,000장이 넘어가는 경우

쿼리를 하면서 리스트에 담는 동안 딜레이 된다 ( 5초정도 걸리는것 같다)

-> 처음 x개를 불러오고 스크롤하면 x개를 불러와서 합치는 방법

(특이한 점은 TedImagePicker는 페이징처리를 하지 않는데 단지 Sequence 처리만으로 딜레이시간을 줄일수 있다고?? 

테스트는 아직 해보지 못함. ) 

 

 

기본 지식 

  1. 안드로이드10 이상인 경우에는 이미지의 URI는 id, contentUri를 통해 가져오고, 미만인 경우 DATA 필드에서 가져온다 
  2. bucketId 는 앨범을 식별하는 구분자다 (MediaStore.Files.FileColumns.BUCKET_ID)
private fun Cursor.getMediaUri(): Uri =
        if (DeviceUtil.isAndroid10Later()) {
            val id = getLong(getColumnIndex(MediaStore.MediaColumns._ID))
            ContentUris.withAppendedId(contentUri, id)
        } else {
            val mediaPath = getString(getColumnIndex(MediaStore.MediaColumns.DATA))
            Uri.fromFile(File(mediaPath))
        }

 

이미지 쿼리하기 

    private val contentUri = DeviceUtil.getContentUri()

    private val projection = arrayListOf(
        MediaStore.Files.FileColumns._ID,
        MediaStore.Files.FileColumns.DISPLAY_NAME,
        MediaStore.Files.FileColumns.MEDIA_TYPE,
        MediaStore.Files.FileColumns.MIME_TYPE,
        MediaStore.Files.FileColumns.DATE_MODIFIED,
        MediaStore.Files.FileColumns.DATE_ADDED,
        MediaStore.Files.FileColumns.DATA
    ).apply {
        if (DeviceUtil.isAndroid10Later()) {
            add(MediaStore.Files.FileColumns.RELATIVE_PATH)
        }
    }.toTypedArray()
    private val defaultSelection = "${MediaStore.Files.FileColumns.MEDIA_TYPE}=?"
    private val defaultSelectionArgs: String =
        MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString()


    override fun getMediaList(bucketId: Long?, start: Int, perPage: Int): Single<List<MediaData>> {
           return Single.create<List<MediaData>> { emitter ->
           
                val imageList = mutableListOf<MediaData>()

                val sortOrder =
                    "${MediaStore.Files.FileColumns.DATE_MODIFIED} DESC LIMIT $perPage OFFSET $start"

                var selection = defaultSelection
                val selectionArgs = mutableListOf(defaultSelectionArgs)

                if (bucketId != null) {
                    selection =
                        "($defaultSelection) AND ${MediaStore.Files.FileColumns.BUCKET_ID}=?"
                    selectionArgs.add("$bucketId")
                }

                val cursor: Cursor? = contentResolver.query(
                    contentUri,
                    projection,
                    selection,
                    selectionArgs.toTypedArray(),
                    sortOrder
                )

                cursor?.run {
                    while (moveToNext()) {
                        imageList.add(
                            MediaData(
                                uri = getMediaUri(),
                                id = getLong(getColumnIndex(MediaStore.Files.FileColumns._ID)),
                                name = getString(getColumnIndex(MediaStore.Files.FileColumns.DISPLAY_NAME)),
                                relativePath = getStringOrNull(getColumnIndex(MediaStore.Files.FileColumns.RELATIVE_PATH)),
                                dateAdded = getLongOrNull(getColumnIndex(MediaStore.Files.FileColumns.DATE_ADDED))
                            )
                        )
                    }
                    close()
                }
                
                 if (!emitter.isDisposed) {
                    emitter.onSuccess(imageList)
                }
                
         }

이렇게 하면 동작은 잘한다 ( 안드로이드 10이하에서는.. ) 

 

하지만 안드로이드 11에서는 이미지를 불러오지 못하는 이슈가 있다.

"Invalid token LIMIT" 

 

https://stackoverflow.com/questions/10390577/limiting-number-of-rows-in-a-contentresolver-query-function

구글링을 하고 난 후 LIMIT 쿼리가 문제란걸 알게되었다. ( 쿼리방법이 변경되었는데 LIMIT을 사용하지 않으면 또 문제가 생기지 않는다..) 

public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
            @Nullable String[] projection, @Nullable Bundle queryArgs,
            @Nullable CancellationSignal cancellationSignal)

안드로이드 11에서는 별도의 대응을 해줘야한다 ( 위의 query 함수를 사용한다 )

( 아예 안드로이드11방식으로 바꿔버리면 10이하에선 제대로 쿼리가 동작하지 않는 이슈가 있다 ) 

 

 

10과 11을 구분하여 커서를 반환하는 함수를 만들었다 

private fun generateCursor(bucketId: Long?, start: Int, perPage: Int): Cursor? {

        var selection = defaultSelection
        val selectionArgs = mutableListOf(defaultSelectionArgs)

        if (bucketId != null) {
            selection =
                "($defaultSelection) AND ${MediaStore.Files.FileColumns.BUCKET_ID}=?"
            selectionArgs.add("$bucketId")
        }

        if (DeviceUtil.isAndroid11Later()) {

            val selectionBundle = bundleOf(
                ContentResolver.QUERY_ARG_OFFSET to start,
                ContentResolver.QUERY_ARG_LIMIT to perPage,
                ContentResolver.QUERY_ARG_SORT_COLUMNS to arrayOf(MediaStore.Files.FileColumns.DATE_MODIFIED),
                ContentResolver.QUERY_ARG_SORT_DIRECTION to ContentResolver.QUERY_SORT_DIRECTION_DESCENDING,
                ContentResolver.QUERY_ARG_SQL_SELECTION to selection,
                ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to selectionArgs.toTypedArray()
            )

            return contentResolver.query(
                contentUri,
                projection,
                selectionBundle,
                null
            )
        } else {

            val sortOrder =
                "${MediaStore.Files.FileColumns.DATE_MODIFIED} DESC LIMIT $perPage OFFSET $start"

            return contentResolver.query(
                contentUri,
                projection,
                selection,
                selectionArgs.toTypedArray(),
                sortOrder
            )
        }
    }

 

 

generateCursor(bucketId, start, perPage)?.run {
                    while (moveToNext()) {
                        imageList.add(
                            MediaData(
                                uri = getMediaUri(),
                                id = getLong(getColumnIndex(MediaStore.Files.FileColumns._ID)),
                                name = getString(getColumnIndex(MediaStore.Files.FileColumns.DISPLAY_NAME)),
                                relativePath = getStringOrNull(getColumnIndex(MediaStore.Files.FileColumns.RELATIVE_PATH)),
                                dateAdded = getLongOrNull(getColumnIndex(MediaStore.Files.FileColumns.DATE_ADDED))
                            )
                        )
                    }
                    close()

 

 

'Android' 카테고리의 다른 글

카카오 로그인 Trouble Shooting  (0) 2021.09.29
어댑터 초기화후 데이터 넣기  (0) 2021.09.10
댓글 찾기  (0) 2021.08.19
데이터 조작 : flatMap 활용하기  (0) 2021.08.09
[Refactoring] 여러개의 CheckBox -> Rx 핸들링  (0) 2021.08.05