How to Create an Onboarding Screen with Animation in Android? (#ViewPager2, #Animation, #PageIndicators)

We are going to learn that how we can Customize the animation using PageTransformer to Onboarding Screen to our android app using kotlin,

 so that we can provide a better user experience to the user of the application.

Overview:

  • Customize the animation using PageTransformer
  • Zoom-out page transformer
  • Swipe Tabs Example with TabLayout & ViewPager2

What is Onboarding Screen?

  • The onboarding screen can be understood as a virtual unboxing of an application. 
  • Users go through a series of screens which finally directs users to the application interface. 

Goals or purposes of Onboarding screen:

  • Welcomes user and excite them about application ahead.
  • Tell the features or functions of the application.
  • Allow users to register or log in.
  • Collect information about the interests of the user(for example – when we open the Google application for the first time it asks the user to select singers which he/she likes).

..

Let us start Onboarding Screens with ViewPager2 Android:

ViewPager2 is an improved version of the ViewPager library that offers enhanced functionality and addresses common difficulties when using ViewPager. 

TabLayout is google Material tab layout component, we are going to use custom page indicator.

..

First onboarding screen

Now create a new activity for first onboarding screen. 

And design for the first onboarding screen

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_centerHorizontal="true"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/intro_image"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"
        android:layout_weight="1"
        android:padding="35dp"
        tools:srcCompat="@tools:sample/backgrounds/scenic" />

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/intro_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:textColor="@android:color/black"
        android:textStyle="bold"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium"
        tools:text="@tools:sample/lorem" />

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/intro_description"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"
        android:textAlignment="center"
        android:textAppearance="@style/TextAppearance.AppCompat.Small"
        tools:text="@tools:sample/lorem/random"
        android:maxLines="3"
        android:ellipsize="end" />

</androidx.appcompat.widget.LinearLayoutCompat>

..

Now create another xml to create an item container layout for the onboarding screen

<?xml version="1.0" encoding="utf-8"?>
<!--Add ViewPage2, TabLayout & 2 MaterialButton to your layout.-->
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="@android:color/white"
    android:fitsSystemWindows="true"
    android:id="@+id/dialogInfo"
    android:orientation="vertical">

    <!--ViewPager2-->
    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager2"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <!--indicator-->
    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tabLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="@color/white"
        app:tabBackground="@drawable/intro_tab_selector"
        app:tabGravity="center"
        app:tabIndicatorHeight="0dp" />


    <!--Next-->
    <com.google.android.material.button.MaterialButton
        android:id="@+id/btnNext"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:text="@string/intro_next"
        android:textAllCaps="true"
        android:backgroundTint="#356DF1"
        android:textAppearance="@style/TextAppearance.AppCompat.Small"
        android:textColor="@android:color/white"
        android:textStyle="bold"
        app:cornerRadius="30dp" />
    <!--SKIP-->
    <com.google.android.material.button.MaterialButton
        android:id="@+id/btnSkip"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="@string/intro_skip"
        app:cornerRadius="30dp"
        android:textAllCaps="true"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:textAppearance="@style/TextAppearance.AppCompat.Small"
        android:textColor="@android:color/darker_gray"
        android:textStyle="bold" />

</androidx.appcompat.widget.LinearLayoutCompat>

..

Next, add the functionality (in the fragment)

package com.boltuix.androidpreferences

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.boltuix.androidpreferences.AppIntro.Companion.MAX_STEP
import com.boltuix.androidpreferences.databinding.IntroAppContentBinding
import com.boltuix.androidpreferences.databinding.IntroAppDesignBinding
import com.google.android.material.tabs.TabLayoutMediator

class AppIntro : Fragment() {
    private var _binding: IntroAppContentBinding? = null

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        //return inflater.inflate(R.layout.intro_app_content, container, false)
        _binding = IntroAppContentBinding.inflate(inflater, container, false)
        return binding.root
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        //............................................................
        binding.viewPager2.adapter = AppIntroViewPager2Adapter()

        //............................................................
        TabLayoutMediator(binding.tabLayout, binding.viewPager2) { tab, position ->
        }.attach()

        //............................................................
        binding.viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)

                if(position== MAX_STEP -1){
                    binding.btnNext.text                =   getString(R.string.intro_get_started)//"Get Started"
                    binding.btnNext.contentDescription  =   getString(R.string.intro_get_started)//"Get Started"
                }else{
                    binding.btnNext.text                  = getString(R.string.intro_next)//"Next"
                    binding.btnNext.contentDescription    = getString(R.string.intro_next)//"Next"
                }
            }
        })


        //............................................................
        binding.btnSkip.setOnClickListener {
            findNavController().navigateUp()
        }

        //............................................................
        binding.btnNext.setOnClickListener {
            if(binding.btnNext.text.toString()==getString(R.string.intro_get_started)){
                findNavController().navigateUp()
            }
            else{
                // to change current page - on click "Next BUTTON"
                val current = (binding.viewPager2.currentItem) + 1
                binding.viewPager2.currentItem = current

                // to update button text
                if(current>= MAX_STEP -1){
                    binding.btnNext.text                =   getString(R.string.intro_get_started)//"Get Started"
                    binding.btnNext.contentDescription  =   getString(R.string.intro_get_started)//"Get Started"

                }else{
                    binding.btnNext.text                =   getString(R.string.intro_next)//"Next"
                    binding.btnNext.contentDescription  =   getString(R.string.intro_next)//"Next"
                }
            }
        }
    }
    companion object {
        const val MAX_STEP = 3
    }
}
//...............................................................................


//................................................................................
class AppIntroViewPager2Adapter : RecyclerView.Adapter<PagerVH2>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagerVH2 {
        return PagerVH2(
            IntroAppDesignBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        )
    }

    //get the size of color array
    override fun getItemCount(): Int = MAX_STEP // Int.MAX_VALUE

    //binding the screen with view
    override fun onBindViewHolder(holder: PagerVH2, position: Int) = holder.itemView.run {

        with(holder) {
            if (position == 0) {
                bindingDesign.introTitle.text = context.getString(R.string.intro_title_1)
                bindingDesign.introDescription.text = context.getString(R.string.intro_description_1)
                bindingDesign.introImage.setImageResource(R.drawable.intro_ic_a_day_at_the_park)
            }
            if (position == 1) {
                bindingDesign.introTitle.text = context.getString(R.string.intro_title_2)
                bindingDesign.introDescription.text = context.getString(R.string.intro_description_2)
                bindingDesign.introImage.setImageResource(R.drawable.intro_ic_directions)
            }
            if (position == 2) {
                bindingDesign.introTitle.text = context.getString(R.string.intro_title_3)
                bindingDesign.introDescription.text = context.getString(R.string.intro_description_3)
                bindingDesign.introImage.setImageResource(R.drawable.intro_ic_hang_out)
            }
        }
    }
}
class PagerVH2(val bindingDesign: IntroAppDesignBinding) : RecyclerView.ViewHolder(bindingDesign.root)

..

Customize the animation using PageTransformer

When you have an implementation of a PageTransformer, call setPageTransformer() with your implementation to apply your custom animations. 

For example, if you have a PageTransformer named ZoomOutPageTransformer, you can set your custom animations like this:

val viewPager: ViewPager2 = findViewById(R.id.pager)
...
viewPager.setPageTransformer(ZoomOutPageTransformer())

..

Zoom-out page transformer

  • This page transformer shrinks and fades pages when scrolling between adjacent pages. 
  • As a page gets closer to the center, it grows back to its normal size and fades in.

package com.boltuix.androidpreferences

import android.view.View
import androidx.viewpager2.widget.ViewPager2
import kotlin.math.abs


private const val MIN_SCALE = 0.85f
private const val MIN_ALPHA = 0.5f

class ZoomOutPageTransformer : ViewPager2.PageTransformer {

    override fun transformPage(view: View, position: Float) {
        view.apply {
            val pageWidth = width
            val pageHeight = height
            when {
                position < -1 -> { // [-Infinity,-1)
                    // This page is way off-screen to the left.
                    alpha = 0f
                }
                position <= 1 -> { // [-1,1]
                    // Modify the default slide transition to shrink the page as well
                    val scaleFactor = MIN_SCALE.coerceAtLeast(1 - abs(position))
                    val vertMargin = pageHeight * (1 - scaleFactor) / 2
                    val horzMargin = pageWidth * (1 - scaleFactor) / 2
                    translationX = if (position < 0) {
                        horzMargin - vertMargin / 2
                    } else {
                        horzMargin + vertMargin / 2
                    }

                    // Scale the page down (between MIN_SCALE and 1)
                    scaleX = scaleFactor
                    scaleY = scaleFactor

                    // Fade the page relative to its size.
                    alpha = (MIN_ALPHA +
                            (((scaleFactor - MIN_SCALE) / (1 - MIN_SCALE)) * (1 - MIN_ALPHA)))
                }
                else -> { // (1,+Infinity]
                    // This page is way off-screen to the right.
                    alpha = 0f
                }
            }
        }
    }
}

..

Swipe Tabs Example with TabLayout & ViewPager2

We need to create a TabLayoutMediator to connect the TabLayout to ViewPager2.

TabLayoutMediator(binding.tabLayout, binding.viewPager2) { tab, position ->
        }.attach()

Run the app. Appreciate navigating with your new tab labels and swiping between fragments.

..

GET source code on Github:

..

..

Comments