본문 바로가기

[Android/Kotlin] 안드로이드 ViewPager2 인트로 화면 with Navigation Component

꿈꾸는블로그왕 2021. 2. 6.

안녕하세요. 오늘은 ViewPager2를 사용하여 Intro 화면을 만드는 방법에 대해서 알아보겠습니다.

 

인트로 화면은 앱을 처음 실행할때 앱의 간단한 사용에 대해서 설명해주는 페이지입니다.

 

최초 실행시만 보여지고, 그 이후에는 사용자가 볼 수 없도록 구현해 보도록 하겠습니다.

 

완성된 모습은 아래와 같습니다.

 

인트로 화면을 구현하기 위해서 Navigation Component를 사용했습니다.

 

프로젝트의 전반적인 구조는 아래와 같습니다.

액티비티는 MainActivity 하나만 존재합니다. 메인 액티비티 안에서 Fragment 간의 상호작용이 발생하고,

이는 Navigation Component를 사용하여 구현할 예정입니다.

 

SplashFragment 에서 Splash화면을 로딩해주고,

만약 최초 실행이라면 OnBoardingFragment로 이동하여 인트로 화면을 보여줄 예정입니다.

그리고 최초 실행이 아니라면 SplashFragment에서 바로 HomeFragment로 이동을 합니다.

 

전반적인 흐름은 아래 이미지를 참고해주세요.

 

STEP01. Fragment 만들기

SplashFragment / OnBoardingFragment / FirstFragment / SecondFragment / ThirdFragment / HomeFragment 를 만들어줍니다.

 

STEP02. Navigation 만들기

res >Andorid Resource Directory를 클릭하여, navigation 폴더를 생성합니다.

그리고 naviagtion 폴더가 생성되면, 해당 폴더에 nav_graph.xml 파일을 만들어 줍니다.

nav_graph 파일을 여시고 녹색 플러스 아이콘을 클릭하시면 아래와 같이 Fragment를 추가할 수 있습니다.

더블클릭으로 추가하시고, Fragment간 관계를 설정해 줍니다.

관계는 위 그림을 참고해주세요.

설정이 끝나면 아래와 같이 코드가 생성이 됩니다.

[nav_graph.xml]

<?xml version="1.0" encoding="utf-8"?>
<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.reachfree.viewpageronboardingexample.SplashFragment"
        android:label="splash_fragment"
        tools:layout="@layout/splash_fragment" >
        <action
            android:id="@+id/action_splashFragment_to_onBoardingFragment"
            app:destination="@id/onBoardingFragment"
            // Splash Backstack 제거
            app:popUpTo="@id/splashFragment"
            app:popUpToInclusive="true"/>
        <action
            android:id="@+id/action_splashFragment_to_homeFragment"
            app:destination="@id/homeFragment" />
    </fragment>
    <fragment
        android:id="@+id/onBoardingFragment"
        android:name="com.reachfree.viewpageronboardingexample.OnBoardingFragment"
        android:label="OnBoardingFragment" >
        <action
            android:id="@+id/action_onBoardingFragment_to_homeFragment"
            app:destination="@id/homeFragment" />
    </fragment>
    <fragment
        android:id="@+id/homeFragment"
        android:name="com.reachfree.viewpageronboardingexample.HomeFragment"
        android:label="home_fragment"
        tools:layout="@layout/home_fragment" />
</navigation>

여기서 popUpTopupUptoInclusive 속성을 사용하여 BackStack을 제거했습니다.

해당 속성이 없으면 인트로 화면에서 뒤로가기 버튼을 클릭하면 SplashFragment로 이동을 합니다. 하지만 해당 속성으로 인해 뒤로가기 버튼을 클릭하면 앱이 종료됩니다.

 

메인 액티비티 레이아웃에 아래와 같이 연결을 해줍니다.

[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=".MainActivity">

    <fragment
        android:id="@+id/fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/nav_graph" />

</androidx.constraintlayout.widget.ConstraintLayout>

이제 액티비티와 프레그먼트가 연결되었습니다.

 

STEP03. SpalshFragment 

[SpalshFragment.kt]

class SplashFragment : Fragment() {

    private var _binding: SplashFragmentBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = SplashFragmentBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        //1
        Handler(Looper.getMainLooper()).postDelayed({
            if (isOnBoardingFinished()) {
                findNavController().navigate(R.id.action_splashFragment_to_homeFragment)
            } else {
                findNavController().navigate(R.id.action_splashFragment_to_onBoardingFragment)
            }
        }, 1500)
    }

    //2
    private fun isOnBoardingFinished(): Boolean {
        val prefs = requireActivity().getSharedPreferences("onBoarding", Context.MODE_PRIVATE)
        return prefs.getBoolean("finished", false)
    }

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

}

1. Splash화면은 Hanlder()를 사용하여 1.5초의 딜레이를 주었습니다. isOnBoardingFinished() 메소드를 호출하여 최초실행여부를 판단해줍니다.

2. isOnBoardingFinished() 메소드는 SharedPreferences에 저장된 최초 실행여부를 불러오기 합니다.

 

최초 실행이기 때문에 action_splashFragment_to_onBoardingFragment 에 의해서 OnBoardingFragment로 이동합니다.

 

[splash_fragment.xml]

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".SplashFragment">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="SPLASH SCREEN"
        android:textSize="40sp"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

STEP04. OnBoardingFragment

OnBoardingFragment는 ViewPager2를 구현합니다. ViewPager2에 사용할 Adapter 클래스 만들어 줍니다.

[ViewPagerAdapter.kt]

class ViewPagerAdapter(
    list: ArrayList<Fragment>,
    fm: FragmentManager,
    lifecycle: Lifecycle
) : FragmentStateAdapter(fm, lifecycle) {

    private val fragmentList = list

    override fun getItemCount() = fragmentList.size

    override fun createFragment(position: Int) = fragmentList[position]
}

 

[OnBoardingFragment.kt]

class OnBoardingFragment : Fragment() {

    private var _binding: OnBoardingFragmentBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = OnBoardingFragmentBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //1
        setupViewPager()
    }

    private fun setupViewPager() {
        val fragmentList = arrayListOf(
            FirstFragment.newInstance(),
            SecondFragment.newInstance(),
            ThirdFragment.newInstance()
        )

        val adapter = ViewPagerAdapter(
            fragmentList,
            requireActivity().supportFragmentManager,
            lifecycle
        )

        binding.viewPager.adapter = adapter
        //2
        binding.viewPager.isUserInputEnabled = false
    }

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

}

1. setupViewPager()메소드에서 viewPager와 adapter를 연결해줍니다.

2. isUserInputEnabled = false 로 하여 Swipe action을 막습니다. 사용자는 이전 또는 다음 버튼을 통해서만 Fragment 간 이동이 가능합니다.

 

[on_boarding_fragment.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">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

STEP05. FirstFragment

특별한 코드는 없습니다. 다음 버튼을 클릭하면 SecondFragment로 이동시킵니다.

[FirstFragment.xml]

class FirstFragment : Fragment() {

    private var _binding: FirstFragmentBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FirstFragmentBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val viewPager = activity?.findViewById<ViewPager2>(R.id.viewPager)

        binding.txtNext.setOnClickListener {
            viewPager?.currentItem = 1
        }
    }

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

    companion object {
        fun newInstance() = FirstFragment()
    }

}

 

[first_Fragment.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">


    <ImageView
        android:id="@+id/imageView"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_marginTop="150dp"
        android:src="@drawable/ic_launcher_background"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:text="First Screen Title"
        android:textSize="30sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView" />

    <TextView
        android:id="@+id/description"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="50dp"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="50dp"
        android:gravity="center"
        android:text="First Screen Subtitle"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/title" />

    <TextView
        android:id="@+id/txt_next"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="40dp"
        android:layout_marginBottom="40dp"
        android:text="다음"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="22sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

STEP06. SecondFragment

다음 버튼을 클릭하면 ThirdFragment로 이동시킵니다. 이전 버튼을 클릭하면 FirstFragment로 이동시킵니다.

[SecondFragment.xml]

class SecondFragment : Fragment() {

    private var _binding: SecondFragmentBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = SecondFragmentBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val viewPager = activity?.findViewById<ViewPager2>(R.id.viewPager)

        binding.txtPrevious.setOnClickListener {
            viewPager?.currentItem = 0
        }
        binding.txtNext.setOnClickListener {
            viewPager?.currentItem = 2
        }
    }
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    companion object {
        fun newInstance() = SecondFragment()
    }

}

 

[second_Fragment.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">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_marginTop="150dp"
        android:src="@drawable/ic_launcher_background"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:text="Second Screen Title"
        android:textSize="30sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView" />

    <TextView
        android:id="@+id/description"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="50dp"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="50dp"
        android:gravity="center"
        android:text="Second Screen Subtitle"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/title" />

    <TextView
        android:id="@+id/txt_previous"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="40dp"
        android:layout_marginBottom="40dp"
        android:text="이전"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="22sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <TextView
        android:id="@+id/txt_next"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="40dp"
        android:layout_marginBottom="40dp"
        android:text="다음"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="22sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

STEP07. ThirdFragment

이제 시작 버튼을 클릭하면 HomeFragment로 이동시킵니다. 

또한 onBoardingFinished()를 호출하여 SharedPrefereces에 finished 값을 true로 저장합니다.

 

[ThirdFragment.xml]

class ThirdFragment : Fragment() {

    private var _binding: ThirdFragmentBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = ThirdFragmentBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val viewPager = activity?.findViewById<ViewPager2>(R.id.viewPager)

        binding.txtPrevious.setOnClickListener {
            viewPager?.currentItem = 1
        }
        binding.txtStart.setOnClickListener {
            findNavController().navigate(R.id.action_onBoardingFragment_to_homeFragment)
            onBoardingFinished()
        }
    }

    private fun onBoardingFinished() {
        val prefs = requireActivity().getSharedPreferences("onBoarding", Context.MODE_PRIVATE)
        prefs.edit().putBoolean("finished", true).apply()
    }

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

    companion object {
        fun newInstance() = ThirdFragment()
    }
}

 

[third_Fragment.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">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_marginTop="150dp"
        android:src="@drawable/ic_launcher_background"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:text="Third Screen Title"
        android:textSize="30sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView" />

    <TextView
        android:id="@+id/description"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="50dp"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="50dp"
        android:gravity="center"
        android:text="Third Screen Subtitle"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/title" />

    <TextView
        android:id="@+id/txt_previous"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="40dp"
        android:layout_marginBottom="40dp"
        android:text="이전"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="22sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <TextView
        android:id="@+id/txt_start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="40dp"
        android:layout_marginBottom="40dp"
        android:text="시작"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="22sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

HomeFragment 생략하겠습니다.

댓글