ViewPager2와 TabLayout를 이용해서 위 영상과 같이 만들어 보려고 합니다. 영상 처럼 좌우로 스와이프도 되고, 특히나 2번째 페이지에서는 RecyclerView를 이용해서 수직으로 아이템을 출력 시켜줍니다.
1. ViewPager2
ViewPager2의 어떤 Adapter를 붙이는지에 따라서 Fragment, RecyclerView.ViewHolder의 뷰를 적용 할 수 있습니다.
ViewPager |
ViewPager2 |
Pager 아이템 |
PagerAdapter |
RecyclerView.Adapter |
RecyclerView.ViewHolder |
FragmentStatePagerAdapter |
FragmentStateAdapter |
Fragment |
ViewPager2는 ViewPager와 다르게 RecyclerView를 기반으로 만들어 졌습니다.
public final class ViewPager2 extends ViewGroup {
private void initialize(Context context, AttributeSet attrs) {
mRecyclerView = new RecyclerViewImpl(context);
mLayoutManager = new LinearLayoutManagerImpl(context);
mRecyclerView.setLayoutManager(mLayoutManager);
mPagerSnapHelper = new PagerSnapHelperImpl();
mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
. . .
}
}
ViewPager2의 initialize 메소드를 확인하면, RecyclerView를 초기화 하고, PagerSnapHelper를 사용하여 Pager들의 애니메이션을 구현하고 있는것을 확인 할수 있습니다.
따라서, ViewPager2의 큰 장점들은 아래와 같습니다.
-
RTL (right to left) layout support
-
Vertical orientation support
-
Reliable Fragment support
-
Dataset change animations
이번 포스팅에서는 FragmentStateAdapter를 이용해서 Fragment를 ViewPager의 아이템으로 구현해 보겠습니다.
1. MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// binding SetUp
val binding = (DataBindingUtil.setContentView(
this, R.layout.activity_main) as ActivityMainBinding)
.apply {
lifecycleOwner = this@MainActivity
}
// 1.FragmentStateAdapter 초기화
val pagerAdapter = PagerFragmentStateAdapter(this)
.apply {
addFragment(FirstFragment())
addFragment(SecondFragment())
addFragment(ThirdFragment())
}
// 2.ViewPager2의 Adapter 설정
val viewPager: ViewPager2 = binding.pager.apply {
adapter = pagerAdapter
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
Log.d("ViewPagerFragment", "Page ${position+1}")
}
})
}
// 3.TabLayout과 ViewPager 연결
TabLayoutMediator(binding.tabLayout, viewPager) { tab, position ->
tab.text = "Tab ${position + 1}"
}.attach()
}
}
1. FragmentStateAdapter를 초기화 해줍니다. (PagerFragmentStateAdapter는 FragmentStateAdapter를 상속받습니다)
2. ViewPager2의 Adapter를 설정해 줍니다. registerOnPageChangeCallbakc() 또한 설정해 주어, page가 변결될때 이벤트를 받을수 있습니다.
3. TabLayout과 ViewPager 연결 시켜 줍니다. TabLayoutMediator를 사용하여, 연결 시켜 줍니다. 그리고 attach() 함수를 호출시켜 주어야 합니다.
2. activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
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"
>
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorTabLayout"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@+id/tabLayout"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
ViewPager2와 TabLayout을 원하는 위치에 넣어 줍니다. 여기서 저는 제일 많이 헤맸습니다. ViewPager2는 [width / height]가 [match_parent] 이여야 합니다. 하지만 위와 같이 height를 0dp로 넣어도 저는 문제 없이 잘됩니다. 하지만 저의 다른 프로젝트의 ViewPager2는 위와 같이 height는 0dp가 되면 이상한 버그가 생깁니다. 그 프로젝트의 경우에는 height도 match_parent로 변경하고, 상위 layout을 LinearLayout으로 변경해주니깐 버그가 사라졌습니다.
3. PagerFragmentStateAdapter.kt
class PagerFragmentStateAdapter(fragmentActivity: FragmentActivity)
: FragmentStateAdapter(fragmentActivity) {
private var fragments : ArrayList<Fragment> = ArrayList()
override fun getItemCount(): Int {
return fragments.size
}
override fun createFragment(position: Int): Fragment {
return fragments[position]
}
fun addFragment(fragment: Fragment) {
fragments.add(fragment)
notifyItemInserted(fragments.size - 1)
}
fun removeFragment() {
fragments.removeAt(fragments.size - 1)
notifyItemRemoved(fragments.size - 1)
}
}
PagerFragmentStateAdapter는 FragmentStateAdapter를 상속 받아 만든 Pager Adapter 입니다. ArrayList<Fragment>를 멤버 변수로 가지고 있고, 여기에 Fragment들을 Pager가 출력시켜주고, 스와이프 및 애니메이션을 지원해 줍니다.
4. SecondFragment.kt
class SecondFragment : Fragment(){
private lateinit var viewModel: SecondItemViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return FragmentSecondBinding.inflate(
inflater,
container,
false
).apply {
lifecycleOwner = viewLifecycleOwner
viewModel = requireActivity().obtainViewModel(SecondItemViewModel::class.java)
vm = viewModel
recyclerView.apply {
setHasFixedSize(true)
adapter = SecondRecyclerViewAdapter(arrayListOf())
layoutManager = LinearLayoutManager(requireActivity())
}
}.root
}
override fun onStart() {
super.onStart()
viewModel.onStart()
}
fun <T : ViewModel> FragmentActivity.obtainViewModel(viewModelClass: Class<T>) =
ViewModelProvider(viewModelStore,
ViewModelFactory.getInstance(
application
)
).get(viewModelClass)
}
SecondFragment는 영상에서 보셨듯이, 하단의 수직 RecyclerView를 가지고 있습니다. RecyclerView를 초기화 시켜주고, lifecycleOwner도 초기화 시켜 줍니다.
5. SecondRecyclerViewAdapter.kt
class SecondRecyclerViewAdapter(private var items: List<Item>)
:RecyclerView.Adapter<SecondRecyclerViewAdapter.VerticalViewHolder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VerticalViewHolder {
return VerticalViewItemBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
.run {
VerticalViewHolder(this)
}
}
override fun getItemCount(): Int {
return items.size
}
override fun onBindViewHolder(holder: VerticalViewHolder, position: Int) {
holder.bind(items[position])
}
fun setItem(itemList: List<Item>) {
items = itemList
notifyDataSetChanged()
}
// ViewHolder
class VerticalViewHolder(private val binding: VerticalViewItemBinding)
:RecyclerView.ViewHolder(binding.root){
fun bind(item: Item) {
binding.item = item
}
}
}
SecondRecyclerViewAdapter에서도 특별한건 없습니다. 다른 RecyclerView.Adapter와 같은 형태로 만들어 줍니다. ViewHolder도 만들어 줍니다.
6. Item.kt
data class Item (
val title: String = "sample title",
val content: String = "sample content"
)
SecondRecyclerView에서 사용할 data class도 만들어 줍니다.
7. 참고
8. 위 Github 소스코드
'Android' 카테고리의 다른 글
안드로이드 해시키 구하는 방법, 카카오 API (2) | 2021.04.12 |
---|---|
BaseObservable을 이용한 inverseBinding (0) | 2021.02.11 |
Android ConstraintLayout 사용법 (0) | 2020.10.21 |
Android 프로젝트 복제 (0) | 2020.10.08 |
Android의 Handler 와 Looper (0) | 2020.10.06 |