음.. 위의 화면에서 Banner부분의 이미지는 Swipe 방식으로 좌우로 움직이면서 이미지가 바뀐다. 저런건 어떻게 구현하지,,? AndroidX에서 제공 해 주는 ViewPager2 라는 아주 유용한 위젯이 있다.
https://developer.android.com/guide/navigation/navigation-swipe-view-2?hl=ko
전체적인 흐름은 아래와 같다.
- 메인 레이아웃에 ViewPager2 추가 (fragment_home.xml)
- ViewPager2 에 하위 뷰를 삽입하기 위해 FragmentStateAdapter 에 연결 (HomeBannerVpAdapter.kt)
- ViewPager2 의 하위 뷰에 삽입될 Fragment 생성 (HomeBannerFragment.kt , fragment_home_banner.xml)
- 메인 Fragment에서 binding을 통한 Adapter 연결 (HomeFragment.kt)
1. 메인 레이아웃에 ViewPager2 추가
//fragment_home.xml
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/home_banner_viewpager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/home_today_music_scroll_view"/>
간단하게 xml 코드에 추가하면 된다. 여기서 추가적으로 ViewPager2 의 기본 Swipe 방향은 좌우 이다. 따라서 orientation의 default 값은 horizontal 이다. Swipe 방향을 상하로 하고 싶다면 android:orientation="vertical" 속성을 추가하면 된다.
2. FragmentStateAdapter 에 연결 (Adapter 생성)
ViewPager2 는 화면이 Swipe 되면서 하위 뷰 (Fragment) 들이 보여지는 위젯이다. 각 페이지를 나타내는 하위 뷰를 삽입하려면 이 레이아웃을 FragmentStateAdapter에 연결해야 한다. 직접 Adapter class 를 만들어서 FragmentStateAdapter 를 상속받으면 된다. 여기서 getItemCount 와 createFragment 는 필수 메소드이다.
//HomeBannerVpAdapter.kt
package com.example.flo
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
class HomeBannerVpAdapter(fragment : Fragment) :FragmentStateAdapter(fragment) {
private var fragmentlist : ArrayList<Fragment> = ArrayList()
override fun getItemCount(): Int = fragmentlist.size
override fun createFragment(position: Int): Fragment = fragmentlist[position]
fun addFragment(fragment: Fragment) {
fragmentlist.add(fragment)
notifyItemInserted(fragmentlist.size-1)
}
}
우리는 이 글 최상단의 gif 파일에서 보다시피 Home 화면의 배너에 사진이 Swipe 되면서 보여지는 형식이다. 이 사진은 갯수가 정해진 것이 아니라 추가적으로 add 될 수 있으므로 fragmentlist 를 직접 만들어서 사진이 들어가는 Fragment 를 담을것이다.
위에서 우리가 직접 만든 addFragment 메소드는 Banner에 새로운 사진파일이 추가될 때 마다 호출이 되는 메소드이다. fragmentlist.add(fragment) 를 통해 리스트에 add 해 주고, notifyItemInserted 를 통해 해당 Adapter 클래스에 새로운 fragment 가 add 되었다고 꼭 알려줘야 한다.
3. ViewPager2 의 하위 뷰에 삽입될 Fragment 생성
레이아웃 코드는 위와 같다. 그냥 주의해야 할 거는 height를 match_parent 가 아닌 wrap_content 로 하는 거 밖에 없다.
//HomeBannerFragment.kt
package com.example.flo
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.example.flo.databinding.FragmentHomeBannerBinding
class HomeBannerFragment(val imgres : Int) : Fragment() {
lateinit var binding : FragmentHomeBannerBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentHomeBannerBinding.inflate(inflater, container, false)
binding.homeBannerImgIv.setImageResource(imgres)
return binding.root
}
}
위의 Fragment는 ViewPager2 가 Swipe 되면서 보여지게 될 하위 뷰이다. 우리가 만들어야 할 Banner는 크기와 위치가 모두 일정하고 그냥 사진 src만 변경해 주면 되므로 Fragment 파일은 하나만 만들어도 무방하다. 따라서 위의 HomeBannerFragment.kt 는 imgres 라는 사진파일 src 를 인자로 받게된다. 사진소스의 형식은 Int 이다. 그 뒤에 그냥 뷰 바인딩을 이용해서 (binding.homeBannerImgIv.setImageResource(imgres) ) 사진파일을 변경 해 주면 된다. 그렇다면 사진은 src는 어디서 쏴(?) 주느냐??? 바로바로
.
.
.
.
4. 메인 Fragment에서 binding을 통한 Adapter 연결
그렇다. 여기서 사진 src를 쏴(?) 준다
val bannerAdapter = HomeBannerVpAdapter(this)
bannerAdapter.addFragment(HomeBannerFragment(R.drawable.img_home_viewpager_exp))
bannerAdapter.addFragment(HomeBannerFragment(R.drawable.img_home_viewpager_exp))
bannerAdapter.addFragment(HomeBannerFragment(R.drawable.img_home_viewpager_exp))
bannerAdapter.addFragment(HomeBannerFragment(R.drawable.img_home_viewpager_exp))
bannerAdapter.addFragment(HomeBannerFragment(R.drawable.img_home_viewpager_exp))
binding.homeBannerViewpager.adapter = bannerAdapter
아주 무식해 보이는 코드다. 아까 생성한 HomeBannerVpAdapter 클래스의 인스턴스를 생성 한 뒤에 사진 src 를 보내주면서 Fragment를 add 하는 방식이다. 마지막 줄에서 adapter 연결도 꼭 빠짐없이 하도록 하자.
다음으로는 아래와 같은 화면의 레이아웃을 짜야한다.
자세히 보면 전체의 큰 Fragment (AlbumFragment.kt) 에서 아래부분을 보면 [수록곡 , 상세정보 , 영상] 이 swipe 됨과 동시에 바뀌면서 아래부분의 뷰 전체가 바뀌는 것을 볼 수 있다. 사실 처음에 난 처음에 그냥 [수록곡 , 상세정보 , 영상] 에 해당하는 Fragment를 만든 뒤에 버튼을 누를때마다 Fragment가 바뀌는 식으로 코드를 짰다. 그런데 이 부분에도 위의 예시와 마찬가지로 ViewPager2 를 적용할 수 있다.
아래 그림을 보면 알 수 있다시피 Swipe를 통한 화면 전환에 유용한 ViewPager2 와, 가로의 탭을 추가할 수 있는 TabLayout을 활용해서 레이아웃 틀을 구성 해 보았다.
첫번째 예시에서는 이미지 src만 바뀌는 경우여서 ViewPager2 하위 뷰의 Fragment 를 하나만 생성했다. 하지만 이 경우엔 [수록곡 , 상세정보 , 영상] 세가지 다 다른 화면이다. 따라서 3개의 Fragment를 각각 만들어 줘야 한다. 그리고 첫번째 경우에는 Banner에 사진이 계속해서 add 될 수도 있으므로 하위 뷰의 수가 가변적이였다. 하지만 이 경우는 3개로 고정이다. 이 점을 염두에 두고 코드를 살펴보자.
//AlbumVpAdapter.kt
package com.example.flo
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
class AlbumVpAdapter (fragment:Fragment) : FragmentStateAdapter(fragment) {
override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment {
return when(position) {
1 -> AlbumDetailFragment()
2 -> AlbumVideoFragment()
else -> AlbumIncludedFragment()
}
}
}
필수메소드 getItemCount 와 createFragment 는 여전히 존재한다. createFragment 에서 position 값이 바뀜에 따라 하위 뷰들의 Fragment 가 정해지는 방식이다.
다음은 메인 코드의 Adapter 연결 부분을 보자.
val albumVpAdapter = AlbumVpAdapter(this)
val tabTextList = arrayListOf("수록곡", "상세정보", "영상")
TabLayoutMediator(binding.albumTabLayout, binding.albumViewPager) { tab, position ->
tab.text = tabTextList[position]
}.attach()
binding.albumViewPager.adapter = albumVpAdapter
뭔가 첫번째 경우랑 다르다. 새롭게 눈에 띄는놈이 있다. 바로 TabLayoutMediator. TabLayout를 ViewPager2에 연결하고 첨부하는 역할을 한다. TabLayout은 Tab 메뉴들을 담는 큰 틀이고, tabTextList 라는 리스트를 생성해서 각각의 탭에 대한 정보를 넣었다. 이 tabTextList 를 TabLayout에 연결한다. 코드는 그냥 스윽 봐도 직관적으로 이해가 가능하다.
추가적으로 하나 더 기록 하겠다. 상단이 Swipe 되는건 ViewPager2 를 통해 구현했다. 그런데 아래 TabLayout이 각각 Tab의 text 는 없고, Indicator 가 dot 형식으로 되어있다. 음... 어떻게 만들지??
모를땐 역시 stackoverflow...!!!!
drawable 폴더에 home_top_selected_dot.xml ,home_top_default_dot.xml , home_top_tab_selector.xml 파일을 추가해준다.
<!-- home_top_selected_dot.xml -->
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:shape="ring"
android:innerRadius="0dp"
android:thickness="4dp"
android:useLevel="false">
<solid android:color="#3F51B5"/>
</shape>
</item>
</layer-list>
<!-- home_top_default_dot.xml -->
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:shape="ring"
android:innerRadius="0dp"
android:thickness="4dp"
android:useLevel="false">
<solid android:color="#C3C3C3"/>
</shape>
</item>
</layer-list>
<!-- home_top_tab_selector.xml -->
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/home_top_selected_dot"
android:state_selected="true"/>
<item android:drawable="@drawable/home_top_default_dot"/>
</selector>
그리고 xml Layout에서 TabLayout에 아래 속성들을 추가 해 줘야한다.
app:tabBackground="@drawable/tab_selector"
app:tabGravity="center"
app:tabIndicatorHeight="0dp"
방법은 간단하다. <selector> 태그와 그 안에 2개의 <item> 태그를 활용해서 selected 되었을때와, default일때의 경우를 나눠서 처리하는 방식(?) 에 익숙해 지고 훈련하자. 느낌있다.
'안드로이드 > [Kotlin]' 카테고리의 다른 글
[Kotlin] Worker Thread(작업 스레드) & Handler를 활용한 UI 처리 (0) | 2021.10.29 |
---|---|
[Kotlin] registerForActivityResult() 사용법 (Feat. startActivityForResult 의 deprecated) (0) | 2021.10.25 |
CoordinatorLayout 적용 (Feat. Scroll 상단 고정) (0) | 2021.10.20 |
[Kotlin] ImageView 모서리 라운딩 (feat. CardView & Glide 라이브러리) (0) | 2021.10.05 |
[Kotlin] 상단의 Status Bar를 투명하게 만들어 보자. (0) | 2021.10.04 |