[Android 앱 개발 심화] 과제 - 이미지 검색 앱 (1) ViewPager2 & TabLayout

0
post-thumbnail

🍥구현 기능

  • ViewPager2와 TabLayout을 이용해 프래그먼트 전환 구현하기
  • TabLayout 커스텀하기

🍥구현하기

  • ViewPager2와 TabLayout을 이용해 프래그먼트 전환 구현하기

📌참고자료: ViewPager2 | Android Developers

  • build.gradle 파일에 AndroidX dependency 추가
dependencies {
    implementation("androidx.viewpager2:viewpager2:1.0.0")
}

📌참고자료: ViewPager2를 사용하여 탭으로 스와이프 뷰 만들기 | Android Developers

  • AndroidX의 ViewPager2 위젯을 사용하여 스와이프 뷰 만들기
    • (1) XML 레이아웃에 ViewPager2 요소 추가하기
      <androidx.viewpager2.widget.ViewPager2
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    • (2) ViewPager2 레이아웃에 FragmentStateAdapter 연결하기
      class CollectionDemoFragment : Fragment() {
        private lateinit var demoCollectionAdapter: DemoCollectionAdapter
        private lateinit var viewPager: ViewPager2
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            return inflater.inflate(R.layout.collection_demo, container, false)
        }
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            demoCollectionAdapter = DemoCollectionAdapter(this)
            viewPager = view.findViewById(R.id.pager)
            viewPager.adapter = demoCollectionAdapter
        }
      }
      class DemoCollectionAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
        override fun getItemCount(): Int = 100
        override fun createFragment(position: Int): Fragment {
            val fragment = DemoObjectFragment()
            fragment.arguments = Bundle().apply {
                putInt(ARG_OBJECT, position + 1)
            }
            return fragment
        }
      }
      private const val ARG_OBJECT = "object"
      // Instances of this class are fragments representing a single
      // object in our collection.
      class DemoObjectFragment : Fragment() {
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View {
            return inflater.inflate(R.layout.fragment_collection_object, container, false)
        }
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            arguments?.takeIf { it.containsKey(ARG_OBJECT) }?.apply {
                val textView: TextView = view.findViewById(android.R.id.text1)
                textView.text = getInt(ARG_OBJECT).toString()
            }
        }
      }
  • TabLayout을 사용하여 탭 추가하기
    • (1) XML 레이아웃에 TabLayout 요소 추가하기
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tab_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/pager"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1" />
      </LinearLayout>
    • (2) TabLayoutMediator를 만들어 TabLayout을 ViewPager2에 연결
      class CollectionDemoFragment : Fragment() {
        //...
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            val tabLayout = view.findViewById(R.id.tab_layout)
            TabLayoutMediator(tabLayout, viewPager) { tab, position ->
                tab.text = "OBJECT ${(position + 1)}"
            }.attach()
        }
        //...
      }

📌참고자료: FragmentStateAdapter | Android Developers

abstract class FragmentStateAdapter : RecyclerView.Adapter, StatefulAdapter
  • FragmentStatePagerAdapter와 유사한 Behavior를 가짐
  • RecyclerView 내 생명주기:
    • (1) RecyclerView.ViewHolder 초기 상태: 빈 FrameLayout
      (이후 Fragments들을 위한 re-usable container 역할)
    • (2) onBindViewHolder(): position에 해당하는 Fragment 요청
    • (3) onAttachedToWindow(): container에 Fragment를 attach()
    • (4) onViewRecycled(): remove, save state, destroy Fragment

📌참고자료: TabLayoutMediator | Android Developers

  • TabLayout과 ViewPager2를 연결하는 mediator(중재자)
  • listen to ViewPager2's OnPageChangeCallback
    -> adjust tab when ViewPager2 moves
  • listens to TabLayout's OnTabSelectedListener
    -> adjust VP2 when tab moves
  • listens to RecyclerView's AdapterDataObserver
    -> recreate tab content when dataset changes

activity_main.xml

  • ViewPager2 요소와 TabLayout 요소 추가
<?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">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/layout_tab"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/layout_tab"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.kt

  • ViewPager2에 FragmentStateAdapter 설정
  • TabLayoutMediator로 ViewPager와 TabLayout 연결
class MainActivity : AppCompatActivity() {
    private var _binding: ActivityMainBinding? = null
    private val binding get() = _binding!!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.viewpager.adapter = MyPagerAdapter(this)
        TabLayoutMediator(binding.layoutTab, binding.viewpager){tab, position ->
            tab.text = resources.getText(
                if(position==0)R.string.menu_search
                else R.string.menu_folder
            )
        }.attach()
    }

    class MyPagerAdapter(activity: AppCompatActivity): FragmentStateAdapter(activity) {
        override fun getItemCount(): Int = 2
        override fun createFragment(position: Int): Fragment {
            return if(position==0) SearchFragment() else FolderFragment()
        }

    }

}
  • 실행 화면:

  • TabLayout 커스텀하기

📌참고자료: How to make custom tabs with text & icons in android

📌참고자료: Android tab bottom indicator change shape (안드로이드 Tab Indicator 모양 변경 하기) | 꿀맛코딩

tab_selector.xml

  • TabLayout의 Indicator로 사용할 drawable 파일 추가
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="ring">
    <item android:gravity="center">
        <shape>
            <size
                android:width="3dp"
                android:height="3dp" />
            <corners android:radius="4dp" />
            <solid android:color="@color/white" />
        </shape>
    </item>
</layer-list>

activity_main.xml

  • TabLayout 요소의 속성으로 Indicator 커스텀하기
    • Indicator 모양 drawable 파일로 변경
    • Indicator 색상 @color/white로 변경
<?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">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/layout_tab"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/layout_tab"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/theme_secondary"
        app:layout_constraintBottom_toBottomOf="parent"
        app:tabIndicator="@drawable/tab_selector"
        app:tabIndicatorColor="@color/white"/>

</androidx.constraintlayout.widget.ConstraintLayout>

tab_custom_view.xml

  • TabLayout의 Tab의 커스텀 뷰로 사용할 레이아웃 추가
<?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="wrap_content"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/iv_tab_icon"
        android:layout_width="30dp"
        android:layout_height="26dp"
        android:layout_marginTop="8dp"
        android:src="@drawable/icon_search"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:tint="@color/gray" />

    <TextView
        android:id="@+id/tv_tab_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:layout_marginBottom="8dp"
        android:text="@string/menu_search"
        android:textColor="@color/gray"
        android:textSize="14sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/iv_tab_icon" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.kt

  • TabLayoutMediator로 TabLayout과 ViewPager2를 연결한 후, TabLayout 커스텀하기
  • setCustomTabView() 확장 함수 정의
    • TabLayout의 Tab 커스텀 뷰로 변경
  • setTabSelectedListener() 확장 함수 정의
    • TabLayout의 Tab이 선택됨/미선택됨에 따라 커스텀 뷰의 이미지&글자 색 변경
  • setCustomTabAnimation() 확장 함수 정의
    • TabLayout의 Tab이 선택될 때 표시되는 Ripple 색상 제거
class MainActivity : AppCompatActivity() {
    private var _binding: ActivityMainBinding? = null
    private val binding get() = _binding!!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.viewpager.adapter = MyPagerAdapter(this)

        TabLayoutMediator(binding.layoutTab, binding.viewpager) { tab, position ->
            tab.text = resources.getText(
                if (position == 0) R.string.menu_search
                else R.string.menu_folder
            )
        }.attach()
        
        //TabLayout 커스텀하기
        binding.layoutTab.run {
            setCustomTabView()
            setTabSelectedListener()
            setCustomTabAnimation()
        }
    }

    class MyPagerAdapter(activity: AppCompatActivity) : FragmentStateAdapter(activity) {
        override fun getItemCount(): Int = 2
        override fun createFragment(position: Int): Fragment {
            return if (position == 0) SearchFragment() else FolderFragment()
        }
    }
	
    private fun TabLayout.setCustomTabView() {
        TabCustomViewBinding.inflate(layoutInflater).apply {
            ivTabIcon.setImageResource(R.drawable.icon_search)
            tvTabName.setText(R.string.menu_search)

            setCustomViewColor(this.root, true)
        }.also {
            this.getTabAt(0)?.customView = it.root
        }

        TabCustomViewBinding.inflate(layoutInflater).apply {
            ivTabIcon.setImageResource(R.drawable.icon_folder)
            tvTabName.setText(R.string.menu_folder)
        }.also {
            this.getTabAt(1)?.customView = it.root
        }
    }

    private fun TabLayout.setTabSelectedListener() {
        addOnTabSelectedListener(object : OnTabSelectedListener {
            override fun onTabSelected(tab: TabLayout.Tab?) {
                setCustomViewColor(tab?.customView!!, true)
            }

            override fun onTabUnselected(tab: TabLayout.Tab?) {
                setCustomViewColor(tab?.customView!!, false)
            }

            override fun onTabReselected(tab: TabLayout.Tab?) {
            }
        })
    }

    private fun setCustomViewColor(customView: View, selected: Boolean) {
        customView.findViewById<ImageView>(R.id.iv_tab_icon)
            .imageTintList = ColorStateList.valueOf(
            resources.getColor(
                if (selected) R.color.theme_accent else R.color.gray
            )
        )
        customView.findViewById<TextView>(R.id.tv_tab_name)
            .setTextColor(
                resources.getColor(
                    if (selected) R.color.white else R.color.gray
                )
            )
    }

    private fun TabLayout.setCustomTabAnimation() {
        tabRippleColor = null
    }
}
  • 실행 화면:
profile
Be able to be vulnerable, in search of truth

0개의 댓글