RecyclerView with Cursor Adapter

Show Photos in Album

Fragment with RecylerView

class ImportPhotos2Fragment : Fragment() {    private lateinit var adapter: LocalAdapter    override fun onCreateView(        inflater: LayoutInflater, container: ViewGroup?,        savedInstanceState: Bundle?    ): View? {        return inflater.inflate(R.layout.importphotos2, container, false)    }      private fun initList() {        val layoutManager = GridLayoutManager(context, 3)        list.layoutManager = layoutManager        // true if adapter changes cannot affect the size of the RecyclerView         list.setHasFixedSize(true)        if (!::adapter.isInitialized) {            adapter = LocalAdapter(viewModel.gallery)        }        list.adapter = adapter            val gallery = Gallery2(requireContext())        return gallery.getCameraBucketId()?.let { bucketId ->            gallery.query(bucketId)?.also { cursor ->              adapter.submitCursor(cursor)            }        }    }}

ViewItem

sealed class ViewItem(open val id: String, val resource: Int) {    data class PhotoItem(override val id: String, val uri: Uri) : ViewItem(id, R.layout.importphotos2_item)}

Adapter

class LocalAdapter(private val helper: Gallery2) : RecyclerView.Adapter<LocalAdapter.ViewHolder>() {    private var cursor: Cursor? = null    private var resizeOptions: ResizeOptions? = null    init {        // since every photos have an unique id        setHasStableIds(true)    }    fun requireCursor(): Cursor {        return cursor            ?: throw IllegalStateException("Cursor is not initialized.")    }    override fun getItemCount() = requireCursor().count    override fun getItemId(position: Int): Long {        val cursor = requireCursor()        cursor.moveToPosition(position)        return helper.getKey(cursor)    }    fun submitCursor(cursor: Cursor) {        val oldCursor = this.cursor        this.cursor = cursor        notifyDataSetChanged()        oldCursor?.close()    }    fun getItem(position: Int): ViewItem {        val cursor = requireCursor()        cursor.moveToPosition(position)        val data = helper.toItem(cursor)        val mediaId = data.get(Image.DEVICE_MEDIA_ID) as Long        val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mediaId)        return ViewItem.PhotoItem(id = mediaId.toString(), uri = uri)    }    /*    fun getItemPositionByKey(key: String): Int {        // TODO: consider using map for key to position        var position = 0        val cursor = requireCursor()        if (cursor.moveToFirst()) {            do {                if (helper.getKey(cursor).toString() == key) {                    return position                }                position += 1            } while (cursor.moveToNext())        }        return RecyclerView.NO_POSITION    }    override fun getItemViewType(position: Int): Int {        // return getItem(position).resource        return R.layout.importphotos2_item    }     */    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {        val view = LayoutInflater.from(parent.context)            // .inflate(viewType, parent, false)            .inflate(R.layout.importphotos2_item, parent, false)        return ViewHolder(view)    }    override fun onBindViewHolder(holder: ViewHolder, position: Int) {        val item = getItem(position)        when(val item = getItem(position)) {            is ViewItem.PhotoItem -> bindPhoto(item, holder)        }    }    fun bindPhoto(item: ViewItem.PhotoItem, holder: ViewHolder) {        holder.apply {            // optimize loading of images by knowing the container size            val localResizeOptions = if (resizeOptions == null) {                holder.draweeView.doOnLayout {                    resizeOptions = ResizeOptions(holder.draweeView.width, holder.draweeView.height)                }                // temporary size                // - you can estimate size by dividing screen width by 3 (columns)                // - don't specify a size, where we load the full image                // - estimate a size                ResizeOptions(300, 300)            }            else resizeOptions            // load image with Fresco            // draweeView.setImageURI(uri)            draweeView.setImageURISize(item.uri, localResizeOptions)        }    }    inner class ViewHolder(override val containerView: View) : RecyclerView.ViewHolder(        containerView    ),        LayoutContainer {    }}

R.layout.importphotos2

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    >

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

R.layout.importphoto2_item

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- square view -->
    <com.facebook.drawee.view.SimpleDraweeView
        android:id="@+id/draweeView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:gravity="center"
        app:layout_constraintDimensionRatio="H,1:1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="Test">
    </com.facebook.drawee.view.SimpleDraweeView>
</androidx.constraintlayout.widget.ConstraintLayout>

NOTE: Refer Android RecyclerView Get Item View Width and Height - Load Image with Fresco

class Gallery2(val context: Context) {    private val contentResolver by lazy {        context.contentResolver    }    private var cameraBucketId: Long? = null    @Suppress("DEPRECATION")    fun getCameraBucketId(): Long? {        if (cameraBucketId == null) {            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {                val bucketPath = Environment.getExternalStoragePublicDirectory("/DCIM/Camera").toString()                cameraBucketId = bucketPath.toLowerCase().hashCode().toLong()                return cameraBucketId            }            else {                    for (item in findAlbums()) {                        Timber.d("${item.name}=${item.count}, ${item.id}")                        if (item.name == "Camera") {                            cameraBucketId = item.id                            return cameraBucketId                        }                    }                }            }        return cameraBucketId    }    class Album(val id: Long, val name: String, var count: Long = 0, var lastImageUri: Uri? = null)    fun findAlbums(): List<Album> {        val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI        // display name might not be unique        val projections = arrayOf(            // "DISTINCT ${MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME}"            MediaStore.Images.ImageColumns.BUCKET_ID,            MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,            "COUNT(" + MediaStore.Images.ImageColumns._ID + ") AS image_count"        )        val groupBy = "1) GROUP BY ${MediaStore.Images.ImageColumns.BUCKET_ID}, (${MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME}"        val albums = mutableListOf<Album>()        contentResolver.query(contentUri, projections, groupBy, null, null)?.use { cursor ->            if (cursor.moveToFirst()) {                val bucketIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_ID)                val nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME)                val countIndex = cursor.getColumnIndexOrThrow("image_count")                do {                    val bucketId = cursor.getLong(bucketIdIndex)                    val bucketName = cursor.getString(nameIndex)                    val imageCount = cursor.getLong(countIndex)                    val album = Album(id = bucketId, name = bucketName, count = imageCount)                    albums.add(album)                    // MAYBE can be optimized further?                    val projections = arrayOf(                        MediaStore.Images.ImageColumns._ID,                        MediaStore.Images.ImageColumns.DATA                    )                    val selection = "${MediaStore.Images.ImageColumns.BUCKET_ID} == ?"                    val selectionArgs = arrayOf<String>(                        bucketId.toString()                    )                    val sortOrder = "${MediaStore.Images.ImageColumns.DATE_TAKEN} DESC LIMIT 1"                    contentResolver.query(contentUri, projections, selection, selectionArgs, sortOrder)?.use { imageCursor ->                        if (imageCursor.moveToFirst()) {                            val imageUriIndex = imageCursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA)                            val imageUri = imageCursor.getString(imageUriIndex)                            album.lastImageUri = Uri.parse(imageUri)                        }                        // imageCursor.close()                    }                } while (cursor.moveToNext())            }            // cursor.close()        }        return albums    }    fun query(albumId: Long): Cursor? {        val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI        val projections = arrayOf(            MediaStore.Images.ImageColumns._ID,            // MediaStore.Images.ImageColumns.DATA,            MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,            MediaStore.Images.ImageColumns.DATE_TAKEN,            MediaStore.Images.ImageColumns.DISPLAY_NAME,            MediaStore.Images.ImageColumns.ORIENTATION,            MediaStore.Images.ImageColumns.WIDTH,            MediaStore.Images.ImageColumns.HEIGHT,            MediaStore.Images.ImageColumns.SIZE,            @Suppress("DEPRECATION")            MediaStore.Images.ImageColumns.LATITUDE,            @Suppress("DEPRECATION")            MediaStore.Images.ImageColumns.LONGITUDE        )        val selection = "${MediaStore.Images.ImageColumns.BUCKET_ID} == ?"        val selectionArgs = arrayOf<String>(            albumId.toString()        )        val cursor = contentResolver.query(            contentUri,            projections,            selection,            selectionArgs,            "${MediaStore.Images.ImageColumns.DATE_TAKEN} ASC"        )        cursor?.also {            if (cursor.moveToFirst()) {                initIndex(cursor)            }        }        return cursor    }    var idIndex = -1    var dateTakenIndex = -1    var displayNameIndex = -1    var orientationIndex = -1    var widthIndex = -1    var heightIndex = -1    var sizeIndex = -1    var latIndex = -1    var lonIndex = -1    fun initIndex(cursor: Cursor) {        // if (idIndex == -1) {        idIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID)        Timber.d("initIndex, id=$idIndex")        // val uriIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA)        // val bucketDisplayName = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME)        dateTakenIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN)        displayNameIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DISPLAY_NAME)        orientationIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.ORIENTATION)        widthIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.WIDTH)        heightIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.HEIGHT)        sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.SIZE)        @Suppress("DEPRECATION")        latIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.LATITUDE)        @Suppress("DEPRECATION")        lonIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.LONGITUDE)        // }    }    fun getKey(cursor: Cursor) = cursor.getLong(idIndex)    fun toItem(cursor: Cursor): MutableMap<String, Any> {        val mediaId = cursor.getLong(idIndex)        // val uri = Uri.parse(cursor.getString(uriIndex))        // val filepath = cursor.getString(uriIndex)        // val filepath = cursor.getString(bucketDisplayName)        // val filename = File(uri.path).name        val filename = cursor.getString(displayNameIndex)        val millis = cursor.getLong(dateTakenIndex)        val orientation = cursor.getInt(orientationIndex)        val width = cursor.getInt(widthIndex)        val height = cursor.getInt(heightIndex)        val size = cursor.getLong(sizeIndex)        var lat = cursor.getDoubleOrNull(latIndex)        var lon = cursor.getDoubleOrNull(lonIndex)        var uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mediaId)        // seems to only warn in IDE, but compile OK        // NoSuchMethodError: No static method setRequireOriginal        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {            // https://developer.android.com/reference/kotlin/android/provider/MediaStore#setrequireoriginal            uri = MediaStore.setRequireOriginal(uri)            if (lat == null || lon == null) {                // UnsupportedOperationException: Caller must hold ACCESS_MEDIA_LOCATION permission to access original                contentResolver.openInputStream(uri).use { stream ->                    stream?.also {                        ExifInterface(stream).run {                            lat = latLong?.get(0)                            lon = latLong?.get(1)                        }                    }                }            }        }        val timezone = ZoneId.systemDefault()        val date = LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), timezone)        val data = mutableMapOf(            Image.DEVICE_MEDIA_ID to mediaId,            // Image.DEVICE_FILE_PATH to filepath,            // TODO: store device bucket instead?            // use UTC to store exact time            Image.CREATED to date!!.toTimestamp(ZoneOffset.UTC),            Image.FILENAME to filename,            Image.META to mapOf(                Image.Meta.ORIENTATION to orientation,                Image.Meta.WIDTH to width,                Image.Meta.HEIGHT to height,                Image.Meta.SIZE to size,                Image.Meta.LAT to lat,                Image.Meta.LON to lon            )        )        return data    }}

❤️ Is this article helpful?

Buy me a coffee ☕ or support my work via PayPal to keep this space 🖖 and ad-free.

Do send some 💖 to @d_luaz or share this article.

✨ By Desmond Lua

A dream boy who enjoys making apps, travelling and making youtube videos. Follow me on @d_luaz

👶 Apps I built

Travelopy - discover travel places in Malaysia, Singapore, Taiwan, Japan.