Material Motion Part 2: Container Transform

Yazdığınız uygulamanın kullanıcı sayısının artmasını sağlayan en önemli konulardan biri, kullanıcı deneyimi yüksek ve dikkat çekici tasarıma sahip olmasıdır.

Android uygulama da UI öğeleri arasındaki geçişleri farklı animasyon görünümleri ile sağlayan çeşitli Material motion özellikleri bulunmaktadır. Bu özelliklerden biri olan Shared Axis hakkındaki örnek projeyi  “Material Motion Part 1: Shared Axis” adlı makalemden ulaşabilirsiniz.

Bu makale serisinde, Android uygulamada iki farklı arayüz elementinin birbirleri arasında animasyonlu bir şekilde geçişini sağlayan Material motion Container Transform özelliğini örnekleyeceğim.

Container Transform

Bir arayüz elementinin başka bir arayüz elementine animasyonlu bir şekilde geçişini sağlayan özelliktir. Böylelikle kullanıcının mobil tasarım öğeleriyle etkileşime girmesini sağlamış oluruz. Bu durum projenin uzun vadede kullana bilirliğini artırır.

Makalemizde anlatacağım örnek uygulamanın görüntüsü;

Projemin kodlarına github linkinden hızlıca ulaşabilirsiniz.

Bu örneği uygulayabilmek için işlemleri adım adım yapalım.

1-Gerekli Kütüphanelerin Yüklenmesi

Projemin app dizinin altındaki build.gradle dosyasını açıyoruz. Dependencies kod bloklarının arasına aşağıdaki kodları yerleştirerek navigation ve material kütüphanelerini yüklüyoruz.

implementation('androidx.navigation:navigation-fragment-ktx:2.3.1')
implementation('androidx.navigation:navigation-ui-ktx:2.3.1')
implementation('com.google.android.material:material:1.3.0-alpha03')

2- Kotlin ve tasarım kodları

İlk önce Note sınıfından bahsetmek istiyorum. Tüm not başlıklarını, içeriklerini ve not kutularının renklerini atadığım ve bu değerleri bir listOf array yapısında barındırdığım sınıfdır.

import androidx.annotation.ColorRes
data class Note(
    val id: Int,
    val title: String,
    val body: String,
    @ColorRes val colorRes: Int
)

val note1 = Note(
    id = 1,
    title = "ToDo 1",
    body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus scelerisque placerat nisl, nec semper felis ullamcorper vel. Nullam egestas ante nec tortor egestas mattis. Duis ut diam nec nibh sodales commodo at at diam. Nunc tempor eu lectus ut feugiat. Etiam eget ullamcorper est, at scelerisque lectus. Aliquam erat volutpat. Maecenas est urna, vestibulum non eros non, dignissim feugiat mi. Cras sit amet ex hendrerit, accumsan dolor in, bibendum erat. Maecenas ullamcorper ut risus eget congue. Vestibulum aliquam ipsum ut turpis efficitur, vel malesuada neque aliquam. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean egestas justo id rutrum blandit. Vestibulum id orci libero. Aliquam pharetra sed mauris ac vehicula.",
    colorRes = R.color.blue300
)

val note2 = Note(
    id = 2,
    title = "ToDo 2",
    body = "Fusce hendrerit enim in eros congue, sed pharetra libero tempus. Integer accumsan euismod nibh non vestibulum. Curabitur finibus imperdiet nunc vel ornare. Ut maximus fringilla sapien in viverra. Aenean a nulla feugiat, hendrerit risus et, congue erat. Ut venenatis lorem sit amet volutpat sollicitudin. Donec ac lorem auctor sem mattis faucibus non ac ante. Phasellus id sem non ante bibendum porta non in tellus. Etiam pellentesque porta luctus.",
    colorRes = R.color.amber300
)

val note3 = Note(
    id = 3,
    title = "ToDo 3",
    body = "Praesent interdum dictum magna quis pretium. Suspendisse at cursus ante, id rutrum nunc. Nullam at lacinia nibh, nec gravida lectus. Quisque maximus vulputate leo, et sollicitudin turpis luctus pellentesque. Nullam vehicula sagittis magna, consectetur congue neque ullamcorper vel. Praesent sed vulputate nunc. Ut ligula lorem, lobortis tristique interdum at, mollis a dolor. Ut sed fringilla urna, id suscipit justo. Praesent ut tortor pharetra, laoreet metus non, iaculis mi. Duis vel lacus fermentum, porta neque id, ultricies arcu. Nunc interdum, est ac sodales tristique, elit urna feugiat dui, quis venenatis ante lorem ullamcorper mi. Nulla id condimentum lorem. Nunc ac scelerisque felis. Nam semper, mi et ultrices rutrum, neque odio molestie tortor, ac iaculis ante arcu eu elit. Phasellus imperdiet tortor quis aliquam ornare.",
    colorRes = R.color.green300
)

val notes = listOf(note1, note2, note3)

 

MainActivity sınıfımızda ise, notların grid şeklinde gösterilmesi sağlayan ve not kutucuklarına tıklandığı anda yapılan geçiş animasyonunu tetiklemesini yaptık. Detaylı açıklamalar kodlar arasında bulunmaktadır.

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.MenuItem
import androidx.core.app.ActivityOptionsCompat
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.card.MaterialCardView
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setRecyclerView()
    }
    private fun setRecyclerView() {
        //Note data class da tanımlanan başlık ve içerikler NotesAdapter sınıfına gönderiliyor
        val adapter = NotesAdapter(notes)
        //Not kutularına tıklanma eventi
        adapter.noteClickListener = object : NotesAdapter.NoteClickListener {
            override fun onNoteClick(id: Int, noteCard: MaterialCardView) {

                val intent = Intent(this@MainActivity, NoteDetailActivity::class.java)
                //Not kutusuna tıkladığı anda yapılan geçiş animasyonunu tetiklediğimiz bölüm
                val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
                    this@MainActivity, noteCard, id.toString()
                )
                //İlgili içeriğin id'si NoteDetailActivity sınıfına gönderiliyor
                intent.putExtra("noteId", id)
                startActivity(intent, options.toBundle())
            }
        }
        //Notları grid şeklinde gösterilmesi sağlayan bölüm
        recycler_view.layoutManager = GridLayoutManager(this, 2)
        recycler_view.adapter = adapter
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            android.R.id.home -> {
                onBackPressed()
                true
            }
            else -> {
                true
            }
        }
    }

    override fun onBackPressed() {
        finish()
    }
}

Notların grid şeklinde gösterilmesi için activity_main xml dosyasında RecyclerView arayüz elementi kullanıldı.

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
    android:fitsSystemWindows="true"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        style="@style/Widget.MaterialComponents.AppBarLayout.Surface"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <com.google.android.material.textview.MaterialTextView
                style="?attr/textAppearanceHeadline6"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:textColor="@color/material_on_surface_emphasis_medium"
                android:text="My notes" />
        </com.google.android.material.appbar.MaterialToolbar>
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

NotesAdapter sınıfının işlevi, MainActivity sınıfında kullanılan RecyclerView içinde tüm not item’larını oluşturmaktır. Bu item kutucuklarında da Note sınıfından gelen notlarla ilgili içerikleri item_note xml dosyasına aktarmasıyla tasarımda gösterilmesidir.

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.smality.materialcontainertransform.databinding.ItemNoteBinding
import com.google.android.material.card.MaterialCardView

class NotesAdapter(private val items: List<Note>) : RecyclerView.Adapter<NotesAdapter.ViewHolder>() {
    lateinit var noteClickListener: NoteClickListener

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        ViewHolder(
            ItemNoteBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            )
        )

    override fun getItemCount() = items.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position])

    inner class ViewHolder(val binding: ItemNoteBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(note: Note) {
            binding.note = note
            binding.noteCard.transitionName = note.id.toString()
            binding.noteCard.setOnClickListener {
                noteClickListener.onNoteClick(note.id, binding.noteCard)
            }
        }
    }

    interface NoteClickListener {
        fun onNoteClick(id: Int, noteCard: MaterialCardView)
    }
}

item_note xml dosyasında MaterialCardView arayüz elementini notları kutucuk şeklinde göstermek için kullandık. MaterialCardView içinde de MaterialTextView arayüz elementine not başlık ve içeriklerini atadık.

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

    <data>
        <variable
            name="note"
            type="com.smality.materialcontainertransform.Note" />
    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.card.MaterialCardView
            android:id="@+id/note_card"
            android:layout_gravity="center"
            android:clickable="true"
            android:focusable="true"
            android:layout_margin="8dp"
            app:cardBackgroundColor="@{context.resources.getColor(note.colorRes)}"
            android:layout_width="match_parent"
            android:layout_height="200dp">
            <LinearLayout
                android:padding="16dp"
                android:orientation="vertical"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <com.google.android.material.textview.MaterialTextView
                    style="?attr/textAppearanceBody1"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="@{note.title}" />

                <com.google.android.material.textview.MaterialTextView
                    style="?attr/textAppearanceCaption"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:ellipsize="end"
                    android:maxLines="10"
                    android:layout_marginTop="8dp"
                    android:text="@{note.body}" />
            </LinearLayout>
        </com.google.android.material.card.MaterialCardView>
    </FrameLayout>
</layout>

Son olarak, NoteDetailActivity sınıfını kullandık. Bu sınıf, not kutularına tıklandığında notların detaylandırının gösterildiği sayfadır. Diğer bir yandan bu sınıfta, RecyclerView nesnesinden MaterialTextView nesnesine geciş anının süresinin belirlendiği kod sayfasıdır. Detaylı açıklamalar kodlar arasında bulunmaktadır.

import android.os.Bundle
import android.view.MenuItem
import android.view.Window
import androidx.appcompat.app.AppCompatActivity
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import com.google.android.material.transition.platform.MaterialContainerTransform
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
import com.smality.materialcontainertransform.databinding.NoteDetailActivityBinding

class NoteDetailActivity : AppCompatActivity() {
    private lateinit var binding: NoteDetailActivityBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
        binding = NoteDetailActivityBinding.inflate(layoutInflater)
        //MainActivity sınıfından gelen noteId değerini aldık
        val noteId = intent.getIntExtra("noteId", 0)
        //Id bilgisine göre Note data class daki not içeriğini alıp, note nesnesine atadık
        val note = notes.find { it.id == noteId }
        binding.note = note
        //
        binding.coordinator.transitionName = noteId.toString()
        setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback())
        //buildContainerTransform metodunda oluturulan özelliklerin geciş esnasında uygulanması için
        //yapılan atama
        window.sharedElementEnterTransition = buildContainerTransform()
        window.sharedElementReturnTransition = buildContainerTransform()
        setContentView(binding.root)
        super.onCreate(savedInstanceState)
        setSupportActionBar(binding.toolbar)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        supportActionBar?.setDisplayShowTitleEnabled(false)
    }
    //RecyclerView nesnesinden MaterialTextView nesnesine geciş anının süresini belirkeyen metod
    private fun buildContainerTransform() =
        MaterialContainerTransform().apply {
            addTarget(binding.coordinator)
            duration = 300
            //geçiş anındaki animasyon hızını ayarlayan metod
            interpolator = FastOutSlowInInterpolator()
            fadeMode = MaterialContainerTransform.FADE_MODE_IN
        }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            android.R.id.home -> {
                onBackPressed()
                true
            }
            else -> {
                true
            }
        }
    }
}

note_detail_activity xml dosyasında ise, MaterialTextView arayüz elementi kullanılarak seçilen notun detaylarının gösterildi.

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

    <data>
        <variable
            name="note"
            type="com.smality.materialcontainertransform.Note" />
    </data>

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:id="@+id/coordinator"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@{context.resources.getColor(note.colorRes)}">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true">

            <com.google.android.material.textview.MaterialTextView
                style="?attr/textAppearanceHeadline6"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:text="@{note.title}"
                android:textColor="@color/material_on_surface_emphasis_medium" />
        </com.google.android.material.appbar.MaterialToolbar>

        <androidx.core.widget.NestedScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="16dp"
            android:layout_marginTop="?actionBarSize">

            <com.google.android.material.textview.MaterialTextView
                style="?attr/textAppearanceBody2"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:ellipsize="end"
                android:maxLines="10"
                android:layout_marginTop="8dp"
                android:text="@{note.body}" />
        </androidx.core.widget.NestedScrollView>
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

Dilerseniz projemin kodlarına github linkinden hızlıca ulaşabilirsiniz.