RecyclerView와 같은 ViewPager2 Scroll View 만들기
- ViewPager2를 처음 배웠을 때는 Fragment 전환 시에만 사용하는 기능인 줄 알았는데 RecyclerView를 기반으로 한 것으로 RecyclerView와 같이 Adapter와 ViewHolder를 통해서 리스트뷰 형식으로의 기능도 할 수 있다는 사실을 알게 되어서 해당 기능에 대해서 알아보기로 했다.
- 이전 글에서 ViewPager2에 대해서 RecyclerView와 비교를 하며 알아 보았는데 간략하게 정리해보면, ViewPager2를 통해 RecyclerView와 같이 재활용 되는 Scroll ItemView를 생성하게 될 경우 다음과 같다.
- Scroll 자동 중앙 포커스
- 스크롤 시 해당 페이지의 중앙에 맞춰 스크롤이 되도록 하는 Snapping이 자동으로 적용이 되어 있어 별도로 지정을 해주지 않아도 된다.
- state 저장 및 복원
- 상태 저장 및 복원을 기본적으로 제공함으로 화면 방향 전환등과 같은 구성변경 상황에서도 이전 상태를 유지할 수 있다.
- RecyclerView에 비해 적은 유연성.
- ViewPager2는 ViewType을 나눠 각기 다른 ViewHolder를 하나의 Adapter에서 처리하는것이 불가능하다.
- ViewPager2는 Scroll View가 각각의 페이지로 이루어져 있기 때문에 RecyclerView와 같이 다양한 옵션의 Item UI 커스터마이징 처럼 디테일한 커스텀에는 제약이 있다.
- Scroll 자동 중앙 포커스
[ ViewPager2 순환 Scroll View 간단한 예제 만들기 ]
예제 완성결과 미리보기
[ 1 ] build.gradle , Manifest 설정
- ViewPager2는 androidx에서 기본적으로 제공을 해주기 때문에 별도로 등록해주지 않아도 된다.
- 예제에서는 ViewBinding과 Coil 을 통해서 List의 웹 이미지를 View에 적용시킬 것 이므로 INTERNET 퍼미션과 coil 라이브러리를 추가. ViewBinding 적용
Manifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
// INTERNET 퍼미션 추가
<uses-permission android:name="android.permission.INTERNET"/>
<application
...
</application>
</manifest>
build.gradle
android {
...
buildFeatures {
viewBinding = true
}
}
dependencies {
// url 이미지 로딩 coil Library
implementation("io.coil-kt:coil:2.4.0")
...
}
[ 2 ] item_view 만들기, xml에 ViewPager2 추가하기,
ViewPager2 / item_view 생성 시 주의사항
ViewPager2에 들어갈 item_view.xml을 작성할 때, 주의해야 될 점은 ViewPager2 는 페이지를 스크롤 하는 것이므로, ItemView의 사이즈가 ViewPager2크기와 같아야 한다. 때문에 item_view.xml 의 최상위 레이아웃의 사이즈는 ( width와 height ) 각각 match_parent로 지정해주어야 한다. match_parent가 아닐 경우 아래와 같은 오류 발생.
banner_item_view.xml
<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:background="@color/light_grey">
<ImageView
android:id="@+id/banner_item_img"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="70dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_launcher_background" />
<TextView
android:id="@+id/banner_item_tv_title"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="120dp"
android:layout_marginBottom="4dp"
android:paddingStart="8dp"
android:text="TextView"
android:maxLines="1"
android:ellipsize="marquee"
android:background="@color/white"
android:gravity="center_vertical"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/banner_item_img" />
<TextView
android:id="@+id/banner_item_tv_count"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="4dp"
android:text="1/4"
android:background="@color/white"
android:gravity="center"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/banner_item_tv_title"
app:layout_constraintTop_toBottomOf="@+id/banner_item_img" />
</androidx.constraintlayout.widget.ConstraintLayout>
activity_main.xml
<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">
<TextView
android:id="@+id/main_tv_events"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginTop="60dp"
android:gravity="center"
android:text="Event Banner"
android:textStyle="bold"
android:background="@color/sky"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager2"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="30dp"
android:layout_marginBottom="320dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/main_tv_events" />
</androidx.constraintlayout.widget.ConstraintLayout>
[ 3 ] Adapter 및 ViewHolder 지정
Event.kt
- ViewPager2의 List에 들어갈 데이터 형식을 지정해주는 data class 생성해주기.
data class Event(
val name : String,
val url : String,
)
BannerAdapter.kt
class BannerAdapter(private val eventList: List<Event>) : RecyclerView.Adapter<BannerAdapter.EventViewHolder>(){
inner class EventViewHolder(binding: BannerItemViewBinding) : RecyclerView.ViewHolder(binding.root){
val bannerImgUrl: ImageView = binding.bannerItemImg
val bannerTitle: TextView = binding.bannerItemTvTitle
val bannerCount : TextView = binding.bannerItemTvCount
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventViewHolder {
return EventViewHolder(BannerItemViewBinding.inflate(LayoutInflater.from(parent.context),parent,false))
}
override fun getItemCount(): Int {
return eventList.size
}
override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
holder.apply {
bannerImgUrl.load(eventList[position].url)
bannerTitle.text = eventList[position].name
bannerCount.text = "${position+1} / ${eventList.size}"
}
}
}
- RecyclerView.Adapter를 상속받기 때문에 기존에 RecyclerView.Adapter를 상속받아 만들던 RecyclerView의 메서드와 동일하게 Adapter를 생성해주면 된다.
[ 4 ] View, List, Adapter 연결 , 순환 Scroll 적용
- 기본적으로 Scroll은 양방향 Scroll을 지원하지만, Scroll View의 처음 아이템과 마지막 아이템의 양방향 스크롤은 기본적으로 적용되지 않아 ViewPager2.OnpageChangeCallback()을 통해 처음과 마지막 각각의 아이템에서 스크롤 시 해당 Page로 이동하는 로직으로 순환이 가능한 Scroll View를 생성해줄 수 있다.
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val viewPager: ViewPager2 = binding.viewPager2
// List에 아이템 지정
val eventList = listOf<Event>(
Event(
"테스트용",
"https://i.namu.wiki/i/BNF5rip9jXf6eC8yKb8MZrvDKFAyA-KsrV2ftUcHBrmvy6Fqis7lPxVRCaYyGHZ3iCKNgIi5oSyCszPp9QyrEA.webp"
)
...
)
val adapter = BannerAdapter(eventList)
viewPager.adapter = adapter
// 순환 ViewPager2
// onPageSelected 이벤트를 사용하여 마지막 페이지에서 첫 번째 페이지로 자연스럽게 이동
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
var currentState = 0
var currentPos = 0
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
if (currentState == ViewPager2.SCROLL_STATE_DRAGGING && currentPos == position ){
if (currentPos == 0) binding.viewPager2.currentItem = eventList.size -1
else if (currentPos == eventList.size - 1) binding.viewPager2.currentItem = 0
}
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
}
override fun onPageSelected(position: Int) {
currentPos = position
super.onPageSelected(position)
}
override fun onPageScrollStateChanged(state: Int) {
currentState = state
super.onPageScrollStateChanged(state)
}
})
// 초기 위치 설정
viewPager.setCurrentItem(0, false)
}
}
[오늘 복습한 내용]
1. RecyclerView
[오류,에러 등등]
1. ViewPager2에 ScrollView에 해당하는 Item은 Match Parent여야 한다.
해결방법 : ViewPager2에 들어가는 Item View의 사이즈를 match_parent로 변경해줌.
[느낀 점]
1. 많은 키워드를 접해보는 것이 중요한 것 같다.
2. 집중 하는 시간을 조금 더 늘려야겠다.
3. flow, firebase, data store, dagger 등등.. 다 언제 배우지
[Reference]
// ViewPager2
https://developer.android.com/guide/navigation/navigation-swipe-view-2?hl=ko
// 순환 ViewPager2
'TIL' 카테고리의 다른 글
[TIL] Kotlin 화면 세로 고정 / App Icon, splash 변경 (0) | 2023.10.07 |
---|---|
[TIL] kotlin Youtube Data API 3 - [ 3 ] 단일 Retrofit으로 여러 EndPoint호출하기 (0) | 2023.10.05 |
[TIL] Kotlin ViewPager2 [ 1 ] Fragment 전환 하기, 선택된 탭 아이콘 색상이 바뀌지 않는 오류 (0) | 2023.10.03 |
[TIL] kotlin Room Database 개념 (0) | 2023.10.01 |
[TIL] kotlin Youtube Data API 3 - [ 2 ] videos:List 받아와 Trending 10 Thumbnails 만들기 (2) | 2023.09.30 |