Android RecyclerView Photo Grid Group by Date Header

September 26, 2019

I wanted to create the following layout.

Android Photo Grid with Header

Layout

Main Layout

<?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.GridLayoutManager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

Layout for Photo (home_item_image.xml)

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

    <com.facebook.drawee.view.SimpleDraweeView
        android:id="@+id/draweeView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        fresco:placeholderImage="@drawable/img_placeholder_100dp"
        fresco:viewAspectRatio="1"
        />
</FrameLayout>

NOTE: Using Fresco for image loading.

Layout for date header (home_item_date.xml)

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

    <TextView
        android:padding="16dp"
        android:id="@+id/dateTextView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
</FrameLayout>

RecyclerView

Activty/Fragment class variable

private lateinit var adapter: LocalAdapter

Class for RecyclerView object

sealed class ViewItem(val resource: Int) {
    class DateItem(val posted: LocalDateTime): ViewItem(R.layout.home_item_date)
    class ImageItem(val created: LocalDateTime, val uri: Uri): ViewItem(R.layout.home_item_image)
}

Setup RecyclerView

// use GridLayout with 3 columns
val layoutManager = GridLayoutManager(context, 3)
list.layoutManager = layoutManager

// assign adapter
adapter = LocalAdapter()
list.adapter = adapter

// expand 3 columns to single row to show date
layoutManager.spanSizeLookup = object: GridLayoutManager.SpanSizeLookup() {
    override fun getSpanSize(position: Int): Int {
        return when (adapter.getItemViewType(position)) {
            R.layout.home_item_date -> 3
            else -> 1
        }
    }
}

Load Data

val items = mutableListOf<ViewItem>()

// date
items.add(ViewItem.DateItem(posted = posted))

// photos
items.add(ViewItem.ImageItem(
    created = created,
    uri = Uri.fromFile(File(filepath))
))

...

// date
...

// photos
...

// load items into adapter
adapter.replaceItems(items)

Adapter

class LocalAdapter : RecyclerView.Adapter<LocalAdapter.ViewHolder>() {
    private var items = listOf<ViewItem>()

    override fun getItemViewType(position: Int): Int {
        return items[position].resource
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(viewType, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        when(val item = items[position]) {
            is ViewItem.DateItem -> {
                val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
                holder.dateTextView.text = item.posted.format(formatter)
            }
            is ViewItem.ImageItem -> {
                holder.draweeView.setImageURI(item.uri)
            }
        }
    }

    fun replaceItems(items: List<ViewItem>) {
        this.items = items
        notifyDataSetChanged()
    }

    override fun getItemCount(): Int {
        return items.size
    }

    inner class ViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView),
        LayoutContainer
}
This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.