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.

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.

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

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.


