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.

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, który posiada ładne animacje oraz wykorzystuje nowy ViewPager2. Dodatkowo będzie zawierał implementację modelu widoku, który to udostępni listę na podstawie której zbudujemy ekrany samouczka. Z czego skorzystamy? Poniżej znajdziesz mechanizmy, narzędzia oraz rozwiązania, z…

Read more

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 część pierwszą.

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. 

Newsletter

Zapisz się do mojego newslettera, aby nie przegapić nowych postów. Dodatkowo wyślę Ci darmowego ebooka mojego autorstwa zawierającego mnóstwo wskazówek dla programisty aplikacji mobilnych.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Wypełnij to pole
Wypełnij to pole
Proszę wprowadzić prawidłowy adres email.
You need to agree with the terms to proceed

Menu