본문 바로가기

TIL

[TIL] Kotlin ViewPager2 [ 2 ] 순환 Scroll 배너 만들기 ( Fragment전환이 아닌 Rcv 형태)

RecyclerView와 같은 ViewPager2 Scroll View 만들기

  • ViewPager2를 처음 배웠을 때는 Fragment 전환 시에만 사용하는 기능인 줄 알았는데 RecyclerView를 기반으로 한 것으로 RecyclerView와 같이 Adapter와 ViewHolder를 통해서 리스트뷰 형식으로의 기능도 할 수 있다는 사실을 알게 되어서 해당 기능에 대해서 알아보기로 했다.
 

[TIL] Kotlin ViewPager2 [ 1 ] Fragment 전환 하기, 선택된 탭 아이콘 색상이 바뀌지 않는 오류

[ 1 ] View Pager2 개념 ViewPager2 는 RecyclerView에 기반하여 만들어진 것이다. ViewPager2도 ViewHolder 패턴을 사용하여 화면에 보이지 않는 View를 재사용하고, 데이터만 업데이트하여 효율적인 메모리 사용

junes-daily.tistory.com

  • 이전 글에서 ViewPager2에 대해서 RecyclerView와 비교를 하며 알아 보았는데 간략하게 정리해보면, ViewPager2를 통해 RecyclerView와 같이 재활용 되는 Scroll ItemView를 생성하게 될 경우 다음과 같다.
    • Scroll 자동 중앙 포커스
      • 스크롤 시 해당 페이지의 중앙에 맞춰 스크롤이 되도록 하는 Snapping이 자동으로 적용이 되어 있어 별도로 지정을 해주지 않아도 된다.
    • state 저장 및 복원
      • 상태 저장 및 복원을 기본적으로 제공함으로 화면 방향 전환등과 같은 구성변경 상황에서도 이전 상태를 유지할 수 있다.
    • RecyclerView에 비해 적은 유연성.
      • ViewPager2는 ViewType을 나눠 각기 다른 ViewHolder를 하나의 Adapter에서 처리하는것이 불가능하다.
      • ViewPager2는 Scroll View가 각각의 페이지로 이루어져 있기 때문에 RecyclerView와 같이 다양한 옵션의 Item UI 커스터마이징 처럼 디테일한 커스텀에는 제약이 있다.

 

 

[ 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

https://notepad96.tistory.com/183