RecyclerView i PagerSnapHelper. Centrowanie elementów z zachowaniem UX.

Dowiesz się, w jaki sposób podczas pracy z RecyclerView (bez scrollbar) wycentrować wybrany element przy użyciu SnapHelper.

RecyclerView i PagerSnapHelper. Centrowanie elementów z zachowaniem UX.

W tym artykule dowiesz się, w jaki sposób podczas pracy z RecyclerView (bez scrollbar) wycentrować wybrany element a dodatkowo zadbać o to, aby użytkownicy wiedzieli, że istnieją inne elementy na liście.

Co chcemy osiągnąć?

Nasz plan podzielimy na dwa podpunkty.

  1. Chcemy, aby podczas przewijania listy, element, który aktualnie jest najbardziej widoczny po puszczeniu palca, został wycentrowany. Zależy nam na efekcie 'przyklejenia’ go do środka ekranu.
  2. Nie chcemy używać pasków przewijania. Zamiast tego pokażemy krańce sąsiednich elementów.

Do it!

Krok pierwszy

Chcemy, aby podczas przewijania horyzontalnej listy wybrany jej element znalazł się na środku ekranu.

Domyślne zachowanie
😍
Pamiętaj!
Podczas projektowania interfejsu użytkownika pamiętaj o tym, że bardzo ważne jest zaprojektowanie go w taki sposób, aby użytkownik nie miał problemów ze zrozumieniem działania aplikacji.

Konfiguracja

W ramach projektu demo wykorzystamy bibliotekę Material oraz co nie jest zaskoczeniem – RecyclerView.

implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'

Stwórz widok, w którym osadzimy nasz RecyclerView oraz widok jego pojedynczego elementu.

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="150dp"
    android:layout_margin="8dp"
    app:cardCornerRadius="16dp"
    app:cardElevation="4dp">

    <View
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:background="#ff2674" />

    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="16dp"
        android:gravity="center"
        android:textAllCaps="true"
        android:textColor="#ffffff"
        android:textSize="24sp"
        android:textStyle="bold"
        tools:text="text" />

</androidx.cardview.widget.CardView>

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:text="List of items"
        android:textColor="#333333"
        android:textSize="20sp"
        android:textStyle="bold"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="8dp"
        android:orientation="horizontal"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/title"
        tools:listitem="@layout/item_rv" />

</androidx.constraintlayout.widget.ConstraintLayout>

Widoki skończone, więc płynnie przejdźmy do kodu. Adapter jest standardowy i nie jest celem artykułu, dlatego nie będę go opisywał. Zwróć uwagę na użycie klasy PagerSnapHelper. Dzięki niej podczas przewijania RecyclerView element, który jest aktualnie najbardziej widoczny, zostanie wyśrodkowany po oderwaniu palca od ekranu. Magia!

Przekonaj się.

class RVPagerSnapActivity : AppCompatActivity()
{
    override fun onCreate(savedInstanceState: Bundle?)
    {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_rv_snaper)

        val items = listOf("a", "b", "c", "d", "e", "f")

        rv.adapter = MyAdapter(items)

        val pagerSnapHelper = PagerSnapHelper()
        pagerSnapHelper.attachToRecyclerView(rv)
    }
}

class MyAdapter(private val items: List<String>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>()
{
    override fun getItemCount() = items.size

    override fun onBindViewHolder(holder: MyViewHolder, position: Int)
    {
        val text = items[position]
        holder.textView.text = text
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder
    {
        val view: View =
                LayoutInflater.from(parent.context).inflate(R.layout.item_rv, parent, false)

        return MyViewHolder(view)
    }

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
    {
        val textView = itemView.findViewById<TextView>(R.id.title)
    }
}

Czas na krok drugi

Poprzednie rozwiązanie posiada spory mankament. Kompletnie nie jest widoczne to, że mamy do czynienia z przewijalną listą. Naprawiając to, wcale nie musimy zaglądać do kodu. Wystarczą sztuczki z paddingami i obcinaniem zawartości.

Zachowanie dzięki, któremu użytkownik jest świadomy istnienia kolejnych elementów.

Wystarczy, że uzupełnisz deklarację RecyclerView o następujące atrybuty:

   android:clipToPadding="false"
   android:paddingStart="16dp"
   android:paddingEnd="16dp"

Dzięki temu, że dodaliśmy lewy i prawy padding w naszym RecyclerView, a do tego nakazaliśmy mu nieucinanie zawartości, która znajduje się poza jego obszarem (wyznaczonym przez padding), sąsiednie elementy są widoczne.

Jeśli uruchomisz teraz aplikację, zwróć uwagę na to, że zachowaliśmy poprzednio wypracowane rozwiązanie (przyklejanie się najbardziej widocznego elementu), a dodatkowo pomogliśmy zrozumieć użytkownikowi, z czym ma do czynienia. Prawda, że łatwe?

Pamiętaj, że to rozwiązanie zadziała, jeśli pojedynczy padding będzie większy od marginesu bocznego pojedynczego elementu listy.

Aktualnie margines elementy wynosi 8dp a padding listy 16dp. To pozwala na wyświetlenie krawędzi elementu sąsiedniego o długości 8dp.

SnapHelper | Android Developers