Android Animation in Kotlin: Onboarding screen with Lottie Animation, Navigation Component and ViewPager2

kulloveth

Loveth Nwokike

Posted on November 7, 2020

Android Animation in Kotlin: Onboarding screen with Lottie Animation, Navigation Component and ViewPager2

Animation in android is a way to make your app lively while passing information to the user. There are different APIs available in android you can use to create animations read more about it here. For this post we will be using a library called Lottie which makes it easier to add animations to your application. What you will learn

  • Creating an onboarding screen
  • Using Navigation Component
  • Animations with Lottie

Alt Text

Onboarding screen is mainly used to explain the features of an app using images, text or animations. To begin add the following dependencies to your app module build.gradle file

//navigation component
Implementation "androidx.navigation:navigation-fragment-ktx:2.3.1"
implementation "androidx.navigation:navigation-ui-ktx:2.3.1"
//viewpager2
implementation "androidx.viewpager2:viewpager2:1.0.0"
//indicator
implementation 'me.relex:circleindicator:2.1.4'
//lottie
implementation "com.airbnb.android:lottie:3.4.1"
//datastore
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha02"
Enter fullscreen mode Exit fullscreen mode

We need to enable safe args to ensure type-safety by adding the plugin to gradle. Add the following to the top level build.gradle file in the dependencies block

classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.1"
Enter fullscreen mode Exit fullscreen mode

then apply the plugin in the app module build.gradle file

apply plugin: 'androidx.navigation.safeargs.kotlin'
Enter fullscreen mode Exit fullscreen mode

We will be navigating into the onboarding screen from the splash screen.
The flow of navigation is Splash screen -> Onboarding Screen -> Main Page. Following the single activity principle the project will have only one activity(Main Activity) and three(3) fragments.

  • Create a new project with an empty template
  • Right click on res folder and select New -> Android Resource File
  • Enter a name for your navigation graph file, select Navigation as Resource type and click ok

nav_graph.xml

<navigation
    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/nav_graph"
    app:startDestination="@id/splashFragment">

    <fragment
        android:id="@+id/splashFragment"
        android:name="com.wellnesscity.health.ui.intro.SplashFragment"
        android:label="fragment_splash"
        tools:layout="@layout/fragment_splash" >
        <action
            android:id="@+id/action_splashFragment_to_welcomeFragment"
            app:destination="@id/welcomeFragment"
            app:popUpTo="@id/splashFragment"
            app:popUpToInclusive="true" />
        <action
            android:id="@+id/action_splashFragment_to_onboardingFragment"
            app:destination="@id/onboardingFragment"
            app:popUpTo="@id/splashFragment"
            app:popUpToInclusive="true" />
    </fragment>
    <fragment
        android:id="@+id/onboardingFragment"
        android:name="com.wellnesscity.health.ui.intro.OnboardingFragment"
        android:label="fragment_onboarding"
        tools:layout="@layout/fragment_onboarding" >
        <action
            android:id="@+id/action_onboardingFragment_to_welcomeFragment"
            app:destination="@id/welcomeFragment"
            app:popUpTo="@id/onboardingFragment"
            app:popUpToInclusive="true" />
    </fragment>
    <fragment
        android:id="@+id/welcomeFragment"
        android:name="com.wellnesscity.health.ui.welcome.WelcomeFragment"
        android:label="fragment_welcome"
        tools:layout="@layout/fragment_welcome" >
    </fragment>

</navigation>
Enter fullscreen mode Exit fullscreen mode

The nav_graph contains splash fragment which is the start destination, onboarding fragment and finally the welcome fragment. The first fragment added is set as the start destination which you can change later if need be by changing the id in the app:startDestination which is an attribute in the <navigation> tag to a different fragment id that you choose.

  • To add the fragments; in the design view at the top left corner click on the add button to see a list of fragments to choose from.
  • To add an action that connects the SplashFragment to the OnboardingFragment; hover on the SplashFragment and drag the circular connection point that shows up to the OnboardingFragment
  • Repeat same for OnboardingFragment to WelcomeFragment
  • The action tag represents possible path to a destination
  • The app:destination contains the id to the destination fragment
  • The app:popUpTo contains the id to the fragment you want directly in your back stack .
  • The app:popUpToInclusive="true" indicates that the popUpTo fragment should also be removed from the back stack

Connecting the nav_graph to the MainActivity

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".ui.activity.MainActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:navGraph="@navigation/nav_graph"
        app:defaultNavHost="true" />

</androidx.constraintlayout.widget.ConstraintLayout>
Enter fullscreen mode Exit fullscreen mode
  • The android:name attribute indicates the type of navigation been used
  • The app:navGraph attribute connects the layout to a navigation graph
  • The app:defaultNavHost="true" intercepts the system back button

The nav-graph is all setup, attached to the MainActivity and ready for use

fragment_splash.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
    tools:context=".ui.intro.SplashFragment">

    <!-- TODO: Update blank fragment layout -->
    <ImageView
        android:id="@+id/splash_iv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:srcCompat="@drawable/splash_screen" />

</FrameLayout>
Enter fullscreen mode Exit fullscreen mode

N:B

Dagger Hilt is used as a dependency injection library in the full project and you will be seeing some of its annotations.

SplashFragment.kt

@AndroidEntryPoint
class SplashFragment : Fragment() {
    private var binding: FragmentSplashBinding? = null
    @Inject lateinit var prefs:DataStore<Preferences>
    private val handler = Handler()
    private val runnable = Runnable {

//saves the onboarding screen to datastore the first its viewed and ensures it only shows up ones after installation
        lifecycleScope.launch {
        prefs.data.collectLatest {
            if (it[preferencesKey<Boolean>("onBoard")] == true)
                requireView().findNavController()
                    .navigate(SplashFragmentDirections.actionSplashFragmentToWelcomeFragment())
            else
                requireView().findNavController()
                    .navigate(SplashFragmentDirections.actionSplashFragmentToOnboardingFragment())
        }
    }}

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        binding = FragmentSplashBinding.inflate(layoutInflater)
        return binding?.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        requireActivity().window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN)
    }

    override fun onResume() {
        super.onResume()
        handler.postDelayed(runnable,3000)
    }

    override fun onPause() {
        super.onPause()
        handler.removeCallbacks(runnable)
    }
}
Enter fullscreen mode Exit fullscreen mode

In the SplashFragment we check if the onboarding has been displayed before, if yes then we navigate to the welcome screen if not we navigate into the OnboardingFragment because we want the onboarding to be displayed only when the app is first installed. To achieve this we used the preference datastore to save a boolean value to true on first installation.

The OnboardingFragment contains three item describing the content of the app, these items will be displayed with ViewPager2 and a RecyclerAdapter. You can either create your own animations with after effect and export as a json file or choose from lottie website

To add the json files to the asset folder;

  • Right click on your app folder
  • select New->folder->AssetsFolder
  • copy and paste them in the asset folder

slide_item_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="15dp">

    <com.airbnb.lottie.LottieAnimationView
        android:id="@+id/imageSlideIcon"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        app:layout_constraintTop_toTopOf="parent"
        app:lottie_autoPlay="true"
        app:lottie_fileName="diet.json"
        app:lottie_loop="true" />

    <TextView
        android:id="@+id/textTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="Title Text"
        android:textColor="@color/colorPrimary"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintTop_toBottomOf="@id/imageSlideIcon" />

    <TextView
        android:id="@+id/textDescription"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginEnd="10dp"
        android:gravity="center"
        android:text="Description Text"
        android:textColor="@color/colorTextSecondary"
        android:textSize="15sp"
        app:layout_constraintTop_toBottomOf="@id/textTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
Enter fullscreen mode Exit fullscreen mode

Lottie files can be referenced from two folders: raw and asset folder and for this post the json files are in the later. Notice the Lottie view widget in the item layout above;

  • The app:lottie_autoPlay="true" starts the animation as soon the screen is in view
  • The app:lottie_fileName="diet.json" attribute is used to add file name when the json file is in asset folder
  • The app:lottie_loop="true" attribute keeps the animation active until user exits the screen

IntroSlide

data class IntroSlide (
    val title: String,
    val description: String,
    val icon: String
)
Enter fullscreen mode Exit fullscreen mode

The above model class is used to represent each item present in the onboarding page

IntroSliderAdapter.kt

class IntroSliderAdapter(private val introSlides: List<IntroSlide>)
    : RecyclerView.Adapter<IntroSliderAdapter.IntroSlideViewHolder>(){
//for adding text to speech listener in the onboarding fragment
    var onTextPassed: ((textView:TextView) -> Unit)? = null

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

    override fun getItemCount(): Int {
        return introSlides.size
    }

    override fun onBindViewHolder(holder: IntroSlideViewHolder, position: Int) {
        holder.bind(introSlides[position])
    }

    inner class IntroSlideViewHolder(private val binding: SlideItemContainerBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(introSlide: IntroSlide) {
            binding.textTitle.text = introSlide.title
            binding.textDescription.text = introSlide.description
            binding.imageSlideIcon.imageAssetsFolder = "images";
            binding.imageSlideIcon.setAnimation(introSlide.icon)
            onTextPassed?.invoke(binding.textTitle)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The above adapter is a normal recyclerView adapter containing a viewHolder with an item layout. In the viewHolder we direct Lottie to where the images are stored by calling .imageAssetsFolder on the Lottie view and setting it to "images" and then call setAnimation on the view as well.

fragment_onboarding.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".ui.intro.OnboardingFragment">

    <!-- TODO: Update blank fragment layout -->
    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@id/indicator"/>

    <me.relex.circleindicator.CircleIndicator3
        android:id="@+id/indicator"
        android:layout_width="wrap_content"
        android:layout_height="@dimen/indicator_height"
        app:ci_drawable="@drawable/indicator_active"
        app:ci_drawable_unselected="@drawable/indicator_inactive"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toTopOf="@id/buttonNext" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/buttonNext"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="@dimen/standard_padding"
        android:text="@string/next"
        android:textColor="@android:color/white"
        android:textStyle="bold"
        app:backgroundTint="@color/colorPrimaryDark"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
Enter fullscreen mode Exit fullscreen mode

The onboarding fragment layout contains the relex indicator which is a library that automatically controls indicators with view pagers.

OnboardingFragment.kt

@AndroidEntryPoint
class OnboardingFragment : Fragment() {
    private var binding: FragmentOnboardingBinding? = null
    @Inject
    lateinit var prefs: DataStore<Preferences>

//the items are added to the adapter 
    private val introSliderAdapter = IntroSliderAdapter(
        listOf(
            IntroSlide(
                "Health Tips / Advice",
                "Discover tips and advice to help you to help maintain transform and main your health",
                "exercise.json"
            ),
            IntroSlide(
                "Diet Tips / Advice",
                "Find out basics of health diet and good nutrition, Start eating well and keep a balanced diet",
                "diet.json"
            ),
            IntroSlide(
                "Covid 19 Symptoms/Prevention tips",
                "Get regular Reminders of Covid-19 prevention tips ensuring you stay safe",
                "covid19.json"
            )
        )
    )

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        binding = FragmentOnboardingBinding.inflate(layoutInflater)
        return binding?.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
//set the adapter to the viewpager2
        binding?.viewPager?.adapter = introSliderAdapter
//sets the viewpager2 to the indicator
        binding?.indicator?.setViewPager(binding?.viewPager)

binding?.viewPager?.registerOnPageChangeCallback(
            object : ViewPager2.OnPageChangeCallback() {

                override fun onPageSelected(position: Int) {
                    super.onPageSelected(position)

/*
*check if its the last page, change text on the button
*from next to finish and set the click listener to
*to navigate to welcome screen else set the text to next
* and click listener to move to next page
*/
   if (position == introSliderAdapter.itemCount - 1) {
//this animation is added to the finish button
                        val animation = AnimationUtils.loadAnimation(
                            requireActivity(),
                            R.anim.app_name_animation
                        )
                        binding?.buttonNext?.animation = animation
                        binding?.buttonNext?.text = "Finish"
                        binding?.buttonNext?.setOnClickListener {
                            lifecycleScope.launch {
                                saveOnboarding()
                            }
                            requireView().findNavController()
                                .navigate(OnboardingFragmentDirections.actionOnboardingFragmentToWelcomeFragment())
                        }
                    } else {
                        binding?.buttonNext?.text = "Next"
                        binding?.buttonNext?.setOnClickListener {
                            binding?.viewPager?.currentItem?.let {
                                binding?.viewPager?.setCurrentItem(it + 1, false)
                            }
                        }
                    }
                }
            })
    }

//suspend function to save the onboarding to datastore
    suspend fun saveOnboarding() {
        prefs.edit {
            val oneTime = true
            it[preferencesKey<Boolean>("onBoard")] = oneTime
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

In the onboardingFragment the IntroSLideAdapter is setup with the list of Items for the Onboarding page, connects the adapter to ViewPager2 and setup the indicator with viewPager2. In the onPageChangeCallback we check if the currentPage is the last one and change the text on the button to finish and navigate into the WelcomeFragment if not the text remains Next and when clicked moves to the next page, finally we have

Alt Text

This sample code is part of a community project WellnessCity, you can check out just the onboarding branch here

If you have any suggestions or question kindly drop them in the comment section. Thank you

💖 💪 🙅 🚩
kulloveth
Loveth Nwokike

Posted on November 7, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related