보통 이미지피커는 라이브러리를 사용했었다 ( 편하니까.. )
클라이언트가 아래와 같은 UI를 원한다.
처음에 전체 이미지들이 나오고
전체보기를 눌렀을때 앨범들이 나온다
앨범을 누르면 그 앨범에 해당하는 이미지들이 나온다
( 그러면 다시 전체보기를 할수 없다. 전체보기 앨범이 없기 때문에 ) 시안대로.
사실 커서로 이미지를 가져온다는 개념은 알고있었으나
해보지 않았기 때문에 미지의 영역이었다.
처음엔 사용했었던 이미지피커를 import 해서 UI만 바꾸려고 했으나
이번기회에 학습해보고 싶어 밑바닥부터 구현했다.
일단 참고한 라이브러리는 TedImagePicker, Pickle
페이징 | 쿼리 | 리스트 | DataBinding | |
TedImagePicker | X | 쿼리 미사용 ( 모든 이미지 항목들에 대해 필터링 ) |
Sequence 처리 | 사용 |
Picker | 페이징3 라이브러리 사용 | 쿼리 사용 (LIMIT,OFFSET은 사용하지 않음) |
List 처리 | 미사용 ( 직접 연결 ) |
( 쿼리 미사용이란 의미는 WHERE, GROUP등의 조건절을 사용하지 않았다는 뜻이다 )
페이징이 필요한 이유
초기 로딩 시간때문 : 이미지가 10,000장이 넘어가는 경우
쿼리를 하면서 리스트에 담는 동안 딜레이 된다 ( 5초정도 걸리는것 같다)
-> 처음 x개를 불러오고 스크롤하면 x개를 불러와서 합치는 방법
(특이한 점은 TedImagePicker는 페이징처리를 하지 않는데 단지 Sequence 처리만으로 딜레이시간을 줄일수 있다고??
테스트는 아직 해보지 못함. )
기본 지식
- 안드로이드10 이상인 경우에는 이미지의 URI는 id, contentUri를 통해 가져오고, 미만인 경우 DATA 필드에서 가져온다
- 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"
구글링을 하고 난 후 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 |