How to quickly get album categories

The so-called album classification, in fact, is to process all media files in the media database, according to the folder name to distinguish, so that users can choose pictures according to the folder name quickly

For example, we can take a look at wechat’s picture selection:

You can add GROUP BY to the SQL statement on some older Android phones, but you can’t use this method on older Android phones.

You must manually handle the cursor of your raw data, such as when we query a media database:

The usual projection is as follows:

val PROJECTION = arrayOf(
    MediaStore.Files.FileColumns._ID,
    COLUMN_BUCKET_ID,
    COLUMN_BUCKET_DISPLAY_NAME,
    MediaStore.MediaColumns.MIME_TYPE
)
Copy the code

You can see that we only have 4 columns, respectively file ID, file folder ID, folder name, file type

How to quickly and conveniently convert the original 4 columns of CURSOR?

We can create a virtual cursor MatrixCursor to extend our previous original cursor

val MATRIX_COLUMNS = arrayOf(
    MediaStore.Files.FileColumns._ID, COLUMN_BUCKET_ID, COLUMN_BUCKET_DISPLAY_NAME, MediaStore.MediaColumns.MIME_TYPE, COLUMN_URI, COLUMN_COUNT
)

Copy the code

I added two new columns, one for the uri of the file and one for the number of images under the folder.

So we just need to pass the cursor into the Cursoradapter to quickly complete the list of album categories

So how do I convert? Let’s go to code:

{return withContext(dispatchers.default) {// Key is the folder id. Value is the folder id. Val bucketMap = hashMapOf<Long, Long>() while (cursor.moveToNext()) { val bucketId = cursor.getBucketId() if (bucketMap.containsKey(bucketId)) { val count = bucketMap[bucketId] bucketMap[bucketId] = count!! + 1} else {bucketMap[bucketId] = 1L}} val cursor = matrixColumns Val allAlbumCursor = MatrixCursor(MATRIX_COLUMNS) var allAlbumUri: Uri? = null var fileId: Long? = null if (cursor.moveToFirst()) {allAlbumUri = cursor.geturi (); fileId = cursor.getFileId()  Log.v("wuyue", "AllAlbumUri :$allAlbumUri") val bucketIdSet = hashSetOf<Long>() do (bucketIdSet.contains(cursor.getBucketId())) { continue } var bucketDisplayName = "" if (cursor.getType(cursor.getColumnIndex(COLUMN_BUCKET_DISPLAY_NAME)) == FIELD_TYPE_STRING) { bucketDisplayName = cursor.getBucketDisplayName() } bucketIdSet.add(cursor.getBucketId()) matrixCursor.addRow(arrayOf(cursor.getFileId().toString(), cursor.getBucketId().toString(), bucketDisplayName, cursor.getFileMimeType(), cursor.getUri().toString(), bucketMap[cursor.getBucketId()].toString())) } while (cursor.moveToNext()) } allAlbumCursor.addRow(arrayOf(fileId ? : "", "-1", "All", allAlbumUri? .toString() ? : "", "", cursor.count)) MergeCursor(arrayOf(allAlbumCursor, matrixCursor)) } }Copy the code

Brief thoughts’;

1. Calculate a map by using the original cursor. The key of the map is the ID of the folder and the value is the number of pictures under the folder

  1. Start walking through the original cursor, and then add the new row to our virtual matrixCursor, because we only need to show the cover image,

So for each album, we just need his latest picture and skip the rest

Processing of some abnormal situations

The main thing is that for some images, bucketDisplayName is not available

/ * * * get photo folder names * / fun Cursor. GetBucketDisplayName () : String = get String (getColumnIndex (COLUMN_BUCKET_DISPLAY_NAME))Copy the code

Take this picture for example:

For images stored directly in the phone’s root directory, the bucketDisplayName value is not available and will crash if you use the extension function

If the cursor Type is not string or null, just skip it. If the cursor Type is not string, just skip it. If the cursor Type is not string, just skip it.

Recycleview-CursorAdapter

CursorAdapter is not made for Recycleview at present, only the listView version, so we need to customize one if we want to use Recycleview

abstract class RecyclerViewCursorAdapter<VH : RecyclerView.ViewHolder? > internal constructor(c: Cursor?) : RecyclerView.Adapter<VH? >() { private var mCursor: Cursor? = null private var mRowIDColumn = 0 protected abstract fun onBindViewHolder(holder: VH, cursor: Cursor?) override fun onBindViewHolder(holder: VH, position: Int) { if (! isDataValid(mCursor)) { throw IllegalStateException("Cannot bind view holder when cursor is in invalid state.") } if (! mCursor!! .moveToPosition(position)) { throw IllegalStateException( "Could not move cursor to position " + position + " when trying to bind view holder" ) } onBindViewHolder(holder, mCursor) } override fun getItemViewType(position: Int): Int { if (! mCursor!! .moveToPosition(position)) { throw IllegalStateException( ("Could not move cursor to position " + position + " when trying to get item view type.") ) } return getItemViewType(position, mCursor) } protected abstract fun getItemViewType(position: Int, cursor: Cursor?) : Int override fun getItemCount(): Int { return if (isDataValid(mCursor)) { mCursor!! .count } else { 0 } } override fun getItemId(position: Int): Long { if (! isDataValid(mCursor)) { throw IllegalStateException("Cannot lookup item id when cursor is in invalid state.") } if (! mCursor!! .moveToPosition(position)) { throw IllegalStateException( ("Could not move cursor to position " + position + " when trying to get an item id") ) } return mCursor!! .getLong(mRowIDColumn) } fun swapCursor(newCursor: Cursor?) { if (newCursor === mCursor) { return } if (newCursor ! = null) { mCursor = newCursor mRowIDColumn = mCursor!! .getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID) // notify the observers about the new cursor notifyDataSetChanged() } else { notifyItemRangeRemoved(0, itemCount) mCursor = null mRowIDColumn = -1 } } fun getCursor(): Cursor? { return mCursor } private fun isDataValid(cursor: Cursor?) : Boolean { return cursor ! = null && ! cursor.isClosed } init { setHasStableIds(true) swapCursor(c) } }Copy the code

Is it still necessary to use LoaderManager

Most of the open source image selection is based on the LoaderManager, in fact, there is no need, now it is 2021, so difficult to use the LoaderManager has been abandoned by Google, why should I use it myself

In fact, just use the viewModel to put a cursor in the LiveData.