ViewPager2, MotionLayout i animacje na ekranie onboardingu. Część 2.

W tym artykule dowiesz się jak wykorzystać animacje, MotionLayout oraz ViewPager2 do stworzenia efektowanego ekranu onboardingu.

ViewPager2, MotionLayout i animacje na ekranie onboardingu. Część 2.

Ten wpis jest kontynuacją wpisu z lipca zeszłego roku, kiedy to rozpoczęliśmy prace nad ekranem onboardingu aplikacji przy użyciu kontrolki ViewPager2. Odsyłam Cię do niego, jeżeli nie miałeś okazji się z nim zapoznać. Koniecznie sprawdź! W tym wpisie rozszerzymy pomysł o wykorzystanie MotionLayout.

Co nowego w tej części?

Do już w zasadzie gotowego ekranu onboardingu dodamy dwie animacje. Pierwsza z nich będzie odpowiedzialna za płynną zmianę koloru tła pomiędzy stronami, a druga za pojawienie się przycisku pozwalającego zamknąć bieżący ekran.

Efekt końcowy
👌
Onboarding w Twojej aplikacji.

W tym artykule przycisk do zamknięcia onboardingu znajduje się dopiero na ostatniej stronie. Nie rób tego w swojej aplikacji! Nie każdy użytkownik ma ochotę na przeglądanie tego typu ekranów po uruchomieniu aplikacji, więc musisz pozwolić na pominięcie tego ekranu w każdej chwili.

W tym artykule rozwiązałem to w ten sposób tylko w celach demonstracyjnych.

Punktem wyjściowym będzie projekt aplikacji po zakończonej części pierwszej. Możesz skorzystać ze specjalnie przygotowanego repozytorium lub przerobić samodzielnie

ViewPager2 jako świetny sposób na samouczek. Część 1
Świetnym sposobem na przedstawienie możliwości aplikacji jest dodanie ekranu samouczka. W tym wpisie pokaże Ci w jaki sposób przygotować taki samouczek.
GitHub - krzbbaranowski/ViewPagerOnboardingSample
Contribute to krzbbaranowski/ViewPagerOnboardingSample development by creating an account on GitHub.

Konfiguracja

Potrzebujemy dostępu do dwóch nowych paczek. Dodaj je do pliku build.gradle.

implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta5
implementation 'com.google.android.material:material:1.1.0

Zwróć uwagę na wersję ConstraintLayout, potrzebujemy wersji drugiej ze względu na to, że w niej pojawia się jego ulepszona wersja, a mianowicie MotionLayout. Daje on znacznie więcej możliwości, jeżeli chodzi o tworzenie animacji pomiędzy widokami. Co prawda w tym artykule nie dowiesz się wyjątkowo dużo o jego możliwościach, ale przykład, który przedstawię, powinien Cię zaciekawić :)

Biblioteka Material przyda się nam do dodania przycisku MaterialButton.

Animacja tła

Na początek zajmiemy się płynną zamianą koloru tła podczas przewijania stron. W tym celu musimy gdzieś przechowywać kolor tła do konkretnej strony, dlatego zaktualizuj model OnboardingPageItem o backgroundColor.

data class OnboardingPageItem(
    @StringRes
    val title: Int,
    @StringRes
    val description: Int,
    @DrawableRes
    val image: Int,
    @ColorRes
    val backgroundColor: Int)

Dodaj nowe kolory w pliku z zasobami colors.xml.

<color name="firstColor">#00b8a9</color>
<color name="secondColor">#f8f3d4</color>
<color name="thirdColor">#f6416c</color>
<color name="fourthColor">#ffde7d</color>

Niezbędne zmiany w repozytorium onboardingu.

class OnboardingRepository {
    private val pages = ArrayList<OnboardingPageItem>()

    init {
        pages.add(
            OnboardingPageItem(
                R.string.onboarding_bar_title,
                R.string.onboarding_bar_description,
                R.drawable.ic_bar,
                R.color.firstColor
            )
        )
        pages.add(
            OnboardingPageItem(
                R.string.onboarding_island_title,
                R.string.onboarding_island_description,
                R.drawable.ic_island,
                R.color.secondColor
            )
        )
        pages.add(
            OnboardingPageItem(
                R.string.onboarding_compass_title,
                R.string.onboarding_compass_description,
                R.drawable.ic_compass,
                R.color.thirdColor
            )
        )
        pages.add(
            OnboardingPageItem(
                R.string.onboarding_tickets_title,
                R.string.onboarding_tickets_description,
                R.drawable.ic_boarding_pass,
                R.color.fourthColor
            )
        )
    }

    fun getOnboardingPages(): ArrayList<OnboardingPageItem> {
        return pages
    }
}

Świetnie, mamy już to czego trzeba jeżeli chodzi o model. Teraz musimy go wykorzystać w tworzeniu animacji, a a w tym celu skorzystamy z listenera onPageChangeCallback kontrolki PageView.

Posiada on metodę o następującej sygnaturze:

fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int)

Wykorzystamy drugi parametr positionOffset, który przyjmuje wartości od 0 do 1 i informuje o postępie przewijania strony.

Naszym celem jest stworzenie płynnej animacji, która wraz z postępem przewijania zmieni odpowiednio swoją wartość, a w konsekwencji kolor tła, dlatego positionOffset posłuży nam za parametr, dzięki któremu wyznaczymy odpowiedni kolor pomiędzy kolorem aktualnej i następnej strony. Dodaj poniższy kod do metody OnViewCreated w OnboardingFragment.

viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
    override fun onPageScrolled(
        position: Int,
        positionOffset: Float,
        positionOffsetPixels: Int
    ) {
        if (position < pages.count() - 1) {
            val pageItemData = pages[position]
            val nextPageItemData = pages[position + 1]

            val color = getPageColor(
                positionOffset,
                pageItemData.backgroundColor,
                nextPageItemData.backgroundColor
            )

            root_view.setBackgroundColor(color)
        }
    }
})

Poniżej znajduje się funkcja odpowiedzialna za zwrócenie interpolowanego koloru na podstawie wartości przesunięcia strony oraz dwóch kolorów.

private fun getPageColor(
    positionOffset: Float,
    @ColorRes currentPageColor: Int,
    @ColorRes nextPageColor: Int
): Int {
    return ArgbEvaluator().evaluate(
        positionOffset,
        ContextCompat.getColor(requireContext(), currentPageColor),
        ContextCompat.getColor(requireContext(), nextPageColor)) as Int
}

Uruchom aplikację. Powinieneś zobaczyć płynną zmianę tła na podstawie przesunięcia stron onboardingu.

Dobra robota! 🙂

MotionLayout i animowany przycisk

Kolejnym celem jest wyświetlenie przycisku pozwalającego na zamknięcie onboardingu, wtedy gdy użytkownik dotrze do ostatniej strony. Zajmiemy się tym, korzystając z MotionLayout czyli ConstraintLayout w wersji 2.0.

Musimy zaktualizować definicję widoku w pliku fragment_onboarding.xml. Zwróć uwagę na użycie nowego atrybutu w kontrolce MotionLayout o nazwie layoutDescription. Za chwilę to wyjaśnię 🙂

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout 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:id="@+id/root_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    app:layoutDescription="@xml/scene_01"
    tools:showPaths="true">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/viewPager_indicator"
        app:layout_constraintHeight_percent="0.85"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0" />

    <me.relex.circleindicator.CircleIndicator3
        android:id="@+id/viewPager_indicator"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_margin="8dp"
        app:ci_drawable="@drawable/pager_indicator_selected"
        app:ci_drawable_unselected="@drawable/pager_indicator_unselected"
        app:layout_constraintBottom_toTopOf="@id/finish"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/viewPager" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/finish"
        android:layout_width="180dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:backgroundTint="@color/firstColor"
        android:text="Finish"
        app:cornerRadius="150dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>

Utwórz w folderze z zasobami nowy folder o nazwie xml, umieść w nim plik o nazwie scene_01.xml i uzupełnij go następującą treścią:

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

    <Transition
        app:constraintSetEnd="@id/end"
        app:constraintSetStart="@id/start"
        app:motionInterpolator="easeInOut" />


    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintBottom_toTopOf="@id/viewPager_indicator"
            app:layout_constraintHeight_percent="0.85"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0" />

        <Constraint
            android:id="@+id/viewPager_indicator"
            android:layout_width="0dp"
            android:layout_height="48dp"
            android:layout_margin="8dp"
            app:layout_constraintBottom_toTopOf="@id/finish"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/viewPager" />

        <Constraint
            android:id="@+id/finish"
            android:layout_width="180dp"
            android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="parent" />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintBottom_toTopOf="@id/viewPager_indicator"
            app:layout_constraintHeight_percent="0.85"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0" />

        <Constraint
            android:id="@+id/viewPager_indicator"
            android:layout_width="0dp"
            android:layout_height="48dp"
            android:layout_margin="8dp"
            app:layout_constraintBottom_toTopOf="@id/finish"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/viewPager" />

        <Constraint
            android:id="@+id/finish"
            android:layout_width="180dp"
            android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
    </ConstraintSet>

</MotionScene>

Czym zajmuje się ten plik? Pozwala on na zdefiniowanie widoku początkowego oraz docelowego. Dodaliśmy dwa sety opatrzone identyfikatorami 'start’ oraz 'end’. Jak się zapewne domyślasz, pierwszy opisuje zależności widoku startowego, a drugi docelowego.

Zwróć uwagę na użycie następujących znaczników:

  • Transition,
  • ConstraintSet,
  • Constraint

Chcemy, aby przycisk zakończenia onboardingu był na początku ukryty na dole strony, co realizuje nam atrybut layout_constraintTop_toBottomOf=”parent” zawarty w pierwszym secie, ale potem ma się pojawić, dlatego w drugim secie zamieniamy go na app:layout_constraintBottom_toBottomOf=”parent”.

Element Transition pozwala na zdefiniowanie setu startowego oraz końcowego i m.in. użytego interpolatora.

Teraz musimy uruchomić to przejście, gdy użytkownik znajdzie się na ekranie końcowym. MotionLayout załatwi za nas robotę, jeżeli chodzi o stworzenie stosownej animacji.

Pokaż mi przycisk!

Ponownie skorzystamy z listenera onPageChangeCallback, ale tym razem z jego metody onPageSelected. Przeładuj ją we wspomnianym callbacku.

override fun onPageSelected(position: Int) {
    if (position < pages.count() - 1)
        root_view.transitionToState(R.id.start)
    else
        root_view.transitionToState(R.id.end)
}

O ile o niczym nie zapomnieliśmy, aplikacja powinna płynnie wyświetlić przycisk na ostatniej stronie.

ViewPager2 jako świetny sposób na samouczek. Część 1
Świetnym sposobem na przedstawienie możliwości aplikacji jest dodanie ekranu samouczka. W tym wpisie pokaże Ci w jaki sposób przygotować taki samouczek.
Manage motion and widget animation with MotionLayout | Android Developers
Browse thousands of Onboarding images for design inspiration | Dribbble
Explore thousands of high-quality onboarding images on Dribbble. Your resource to get inspired, discover and connect with designers worldwide.
GitHub - krzbbaranowski/ViewPagerOnboardingSample
Contribute to krzbbaranowski/ViewPagerOnboardingSample development by creating an account on GitHub.