ViewPager2 View를 Paging 하는 매개체
즉, 사용하는 View를 슬라이드 쇼처럼 넘기게 할 수 있는 페이징 툴
ViewPager
ViewPager2와 ViewPager
ViewPager2는 AndroidX가 발표된 이후 새롭게 나온 ViewPager로 안드로이드 공식 문서에서도 아래와 같은 이유로 ViewPager보다 ViewPager2를 활용하여 페이징 하는 것을 권장
- Horizontal Paging에서 Vertical Paging도 지원 가능(orientation 속성 활용)
- RTl(Right To Left) 페이징 지원(layoutDirection 속성 활용)
- notifyDatasetChanged로 Mutable Fragment Collection을 활용하여 동적 페이징 구현
주로 배너광고, 최초 소개 페이지, 스와이프를 통한 메뉴 변경에 쓰이고 있다.
단, 스와이프를 통한 메뉴 변경은 구글 디자인 정책상 권장되지 않는 방법이다.
구글은 머티리얼 디자인 웹 사이트를 통해 “스와이프 등 왼쪽과 오른쪽으로 움직이는 가로 방향의 터치 동작은 기본적으로 콘텐츠를 세부적으로 탐색하기 위해 마련된 것이기 때문”이라고 설명했다.
이를테면 친구목록에서 스와이프를 하면 해당 위치에 있는 친구를 ‘숨김’하거나 ‘삭제’하는 등의 2단계 기능을 위해 마련된 것이지 다른 페이지로 넘어가도록 하는 형태는 금지한다는 뜻이다.
뷰 페이저를 통한 메뉴 전환은 '하지 말아야 할 기능'이라고 구글에서 명시
사용방법
사용방법은 RecyclerVIew와 유사, xml ViewPager2 추가
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:orientation="horizontal"
//xml에서 orientation 지정해줄 시 추가 horizontal / vertical
임시 DataClass (임시 값 표현 용, 필수 아님)
class DataPage(var color: Int, var title: String)
Adpater에 생성할 Item을 넘겨주고 Orientation을 지정
class ViewPager2Activity : AppCompatActivity() {
lateinit var binding: ActivityViewPager2Binding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_view_pager2)
//임시 데이터 생성
val list: ArrayList<DataPage> = ArrayList<DataPage>().let {
it.apply {
add(DataPage(android.R.color.holo_red_light, "1 Page"))
add(DataPage(android.R.color.holo_orange_dark, "2 Page"))
add(DataPage(android.R.color.holo_green_dark, "3 Page"))
add(DataPage(android.R.color.holo_blue_light, "4 Page"))
add(DataPage(android.R.color.holo_blue_bright, "5 Page"))
add(DataPage(android.R.color.black, "6 Page"))
}
}
binding.viewPager.adapter = ViewPagerAdapter(list)
binding.viewPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
}
}
Adpater는 RecyclerVIew와 같게 3가지 Override 작성
class ViewPagerAdapter(private val listData: ArrayList<DataPage>) : RecyclerView.Adapter<ViewHolderPage>() {
lateinit var binding: ItemViewpagerBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderPage {
binding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.item_viewpager, parent, false)
return ViewHolderPage(binding)
}
override fun onBindViewHolder(holder: ViewHolderPage, position: Int) {
val viewHolder: ViewHolderPage = holder
viewHolder.onBind(listData[position])
}
override fun getItemCount(): Int = listData.size
}
class ViewHolderPage(val binding: ItemViewpagerBinding) : RecyclerView.ViewHolder(binding.root) {
fun onBind(data: DataPage) {
binding.tvTitle.text = data.title
binding.rlLayout.setBackgroundResource(data.color)
}
}
사용할 Item Layout 작성 item_viewpager.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<RelativeLayout
android:id="@+id/rl_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="@android:color/white"
android:textSize="32sp"
tools:text="item" />
</RelativeLayout>
</layout>
기본적인 사용법은 이렇다.
배너 형식 만들기
광고 배너처럼 보이기 위해 오른쪽 아래 현재 배너의 위치를 표시할 것이다.
drawable 폴더에 gray_ellipse.xml 생성, 배너 위치의 backgroud UI를 만들 것이다.
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#59000000"/> <!--색 지정-->
<corners
android:bottomLeftRadius="15dp"
android:bottomRightRadius="15dp"
android:topLeftRadius="15dp"
android:topRightRadius="15dp"
/> <!--코너 둥글게-->
</shape>
ViewPager2와 겹치게 사용하려고 ConstraintLayout을 사용
TextView 하나를 ViewPager2 아래에 지정 후, background를 위에서 만든 gray_ellipse로 지정
Binding을 사용해 <layout> 태그로 감싸주었지만 예외해도 좋습니다.
<?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">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".viewpager2.ViewPager2Activity">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:0.6"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/txt_current_banner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="@drawable/gray_ellipse"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:textColor="@color/white"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="@+id/view_pager"
app:layout_constraintEnd_toEndOf="parent"
tools:text="1 / 3 모두보기" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
이제 이 TextView에 현재 배너의 위치(position) 값을 나타낼 것이다.
사용할 방법은 이전에 포스팅 한 string.xml을 사용
string.xml 에 표시할 텍스트 지정
<resources>
<string name="viewpager2_banner">%1$d / %2$d 모두보기</string>
</resources>
getString()으로 해당 폼을 불러와 처음 보여줄 Item 순서 1과 최대치 Item 값을 넣어줌
class ViewPager2Activity : AppCompatActivity() {
lateinit var binding: ActivityViewPager2Binding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_view_pager2)
// ...
binding.viewPager.adapter = ViewPagerAdapter(list)
binding.viewPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
// ...
binding.txtCurrentBanner.text = getString(R.string.viewpager2_banner, 1, list.size)
}
}
드래그하여 넘길 때 포지션 값을 변경해 주기 위해 드래그를 판단한 CallBack을 등록해야 한다.
그러면 position 값을 알아올 수 있는데 position index 값은 0부터 시작하기 때문에 + 1을 해주어 Text표시
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback(){
//사용자가 스크롤 했을때 position 수정
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
binding.txtCurrentBanner.text = getString(R.string.viewpager2_banner, position + 1, list.size)
}
})
Infinite Scroll (무한 스크롤)
앱들의 배너의 끝에 도달했을 때 페이지를 넘기면 다시 맨 처음부터 배너가 시작되는 배너를 볼 수 있다.
정확한 명칭은 없는 것 같고 무한 스크롤, Infinite Scroll으로 불린다고 한다.
구글링을 하여 찾아보았는데 지원하는 메소드는 따로 없고 편법을 이용한 구현이 있다.
class ViewPagerAdapter(private val listData: ArrayList<DataPage>) : RecyclerView.Adapter<ViewHolderPage>() {
lateinit var binding: ItemViewpagerBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderPage {
binding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.item_viewpager, parent, false)
return ViewHolderPage(binding)
}
override fun onBindViewHolder(holder: ViewHolderPage, position: Int) {
val viewHolder: ViewHolderPage = holder
viewHolder.onBind(listData[position % listData.size])
}
override fun getItemCount(): Int = Int.MAX_VALUE
}
Adapter에서 getItemCount() 받아온 DataList의 사이즈를 return 해주었는데 이 사이즈를 Int가 표현할 수 있는 Max인 억 자리 숫자를 return
그만큼 아이템 개수를 늘려 마치 무한으로 스크롤이 되는 것처럼 구현하는 방식
position 값은 DataList의 Item 개수만큼 ( listData.size ) 나눈 나머지 값을 사용
Item이 3개일 시
0 % 3 = 0 | 1 번 배너 |
1 % 3 = 1 | 2 번 배너 |
2 % 3 = 2 | 3 번 배너 |
(페이지 스크롤 시) | |
3 % 3 = 0 | 1 번 배너 |
4 % 3 = 1 | 2 번 배너 |
5 % 3 = 2 | 3 번 배너 |
페이지를 계속 넘김에 따라 position은 계속 늘어나지만 DataList의 Size만큼 나눈 나머지로 배너를 반복해서 사용 가능
class ViewPager2Activity : AppCompatActivity() {
lateinit var binding: ActivityViewPager2Binding
private var bannerPosition = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_view_pager2)
// ...
bannerPosition = Int.MAX_VALUE / 2 - ceil(list.size.toDouble() / 2).toInt()
binding.viewPager.setCurrentItem(bannerPosition, false)
}
}
따로 position을 저장할 변수를 하나 선언한 후
전 후로 스크롤하기 위해 Int.MAX_VALUE의 절반만큼 에서 - size/2 만큼 빼준다. ( 소수점 올림 - Math.ceil() )
절반만 해줄 시 index 가 중간 Item 데이터를 보여주기 때문
position값을 구한 후 ViewPager2의 setCurrentItem()을 호출해 position을 set 한다.
뒤 false 인자 값은 smoothScroll 유무
true - 포지션까지 스크롤되는 Animation이 출력되고
false - Animation 없이 바로 Set
처음 지정해주는 position값은 별도 animation 없이 바로 set 돼야 하기 때문에 False를 주었다.
추가로 아래 현재 배너 위치를 표시한 Text에도 position값을 List.size 만큼 수정해서 사용해야 한다.
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
//사용자가 스크롤 했을때 position 수정
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
bannerPosition = position
binding.txtCurrentBanner.text = getString(R.string.viewpager2_banner, (bannerPosition % list.size)+1, list.size)
}
})
이렇게 무한 스크롤 구현을 할 수 있다.
Auto Scroll (자동 스크롤)
광고 배너를 보면 사용자가 움직이지 않아도 일정 시간이 지나면 스스로 다음 페이지로 넘어가면서 배너를 보여준다.
- 사용자가 터치를 하지 않으면 자동으로 페이지가 넘어간다.
- 사용자가 배너를 터치해서 드래그하면 페이지가 움직이지 않는다.
- 사용자가 배너를 터치한 상태로 드래그를 하지 않으면 페이지가 넘어간다.
다른 글들을 보면 Handler를 사용하여 AutoScorll을 사용하지만 Coroutine을 사용하고 싶어 Coroutine Job으로 구현하였다.
ViewPager2의 OnPageChangeCallback()의 onPageScrollStateChanged 메소드에 들어오는 state 값을 이용하면 뷰 페이저의 상태를 알 수 있다.
이 상태에 따라 자동 스크롤을 시작할지, 중지할지 정해준다.
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { //사용자가 스크롤 했을때 position 수정
super.onPageSelected(position)
bannerPosition = position
binding.txtCurrentBanner.text = getString(R.string.viewpager2_banner, (bannerPosition % list.size)+1, list.size)
}
override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
when (state) {
ViewPager2.SCROLL_STATE_IDLE ->{}
ViewPager2.SCROLL_STATE_DRAGGING -> {}
ViewPager2.SCROLL_STATE_SETTLING -> {}
}
}
})
ViewPager2.SCROLL_STATE_IDLE | 멈춰있을 때 |
ViewPager2.SCROLL_STATE_DRAGGING | 드래그 될 때 |
ViewPager2.SCROLL_STATE_SETTLING | 스크롤이 양쪽 끝까지 갔을 때 |
SCROLL_STATE_SETTLING 은 무한 스크롤을 구현하였으면 오지 않는다.
(정확히는 안 오는 건 아니지만 몇십 억대 스크롤을 언제 다하는가)
Coroutine Job을 사용 위해 Job 선언
lateinit var job : Job
AutoScroll 함수 생성
lifecycleScope와 delay( 1.5 초)를 사용해 setCurrentItem()으로 이동해 줄 다음 position과 Animatior true 지정
fun scrollJobCreate() {
job = lifecycleScope.launchWhenResumed {
delay(1500)
binding.viewPager.setCurrentItem(++bannerPosition, true)
}
}
Activity나 Fragment의 onReume()에서 생성한 함수 호출
onPause()에서는 job.cancel()로 취소
override fun onResume() {
super.onResume()
scrollJobCreate()
}
override fun onPause() {
super.onPause()
job.cancel()
}
onPageScrollStateChanged()에서 SCROLL_STATE_IDLE 일 때 job이 Active 돼있는지 확인 후 안 돼있으면 만든 함수 scrollJobCreate()로 재생성해 AutoScroll 구현
SCROLL_STATE_DRAGGING 일 때 드래그 되었으니 job을 cancel() 해준다.
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
//...
}
override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
when (state) {
ViewPager2.SCROLL_STATE_IDLE ->{
if (!job.isActive) scrollJobCreate()
}
ViewPager2.SCROLL_STATE_DRAGGING -> job.cancel()
ViewPager2.SCROLL_STATE_SETTLING -> {}
}
}
})
이 방법이 Job을 계속해서 생성하여 좋은 방법인지는 모르겠지만 Job은 cancel()하면 재시작이 불가능해서 일단 계속 생성하고 취소하게 구현하였다.
AutoScroll 애니메이션이 빠를 때
setCurrentItem()을 하게 되면 Animation을 true로 해도 너무 빠르게 지나가게 되어 불편할 수 있다.
Viewpager2 자체는 duration을 지원하지 않아 확장 함수를 만든다.
fun ViewPager2.setCurrentItemWithDuration(
item: Int, duration: Long,
interpolator: TimeInterpolator = AccelerateDecelerateInterpolator(),
pagePxWidth: Int = width // ViewPager2 View 의 getWidth()에서 가져온 기본값
) {
val pxToDrag: Int = pagePxWidth * (item - currentItem)
val animator = ValueAnimator.ofInt(0, pxToDrag)
var previousValue = 0
animator.addUpdateListener { valueAnimator ->
val currentValue = valueAnimator.animatedValue as Int
val currentPxToDrag = (currentValue - previousValue).toFloat()
fakeDragBy(-currentPxToDrag)
previousValue = currentValue
}
animator.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator?) { beginFakeDrag() }
override fun onAnimationEnd(animation: Animator?) { endFakeDrag() }
override fun onAnimationCancel(animation: Animator?) { /* Ignored */ }
override fun onAnimationRepeat(animation: Animator?) { /* Ignored */ }
})
animator.interpolator = interpolator
animator.duration = duration
animator.start()
}
위 함수를 만들고 scrollJobCreate() 수정
setCurrentItem() 대신 사용하여 position과 Animation 시간 값을 준다.
fun scrollJobCreate() {
job = lifecycleScope.launchWhenResumed {
delay(1500)
binding.viewPager.setCurrentItemWithDuration(++bannerPosition, 2500)
}
}
꿀렁.. 꿀렁..
ViewPager Animation
https://todaycode.tistory.com/27?category=979455
https://joycehong0524.medium.com/android-kotlin-viewpager2-setcurrentitem-애니메이션-속도-지정-방법-viewpager2-slide-disable-하는-방법-3c5ad7a2671f
'Android > Reference' 카테고리의 다른 글
Context (0) | 2022.03.28 |
---|---|
ClipboardManager 클립보드에 복사 후 붙여넣기 (0) | 2022.03.16 |
권한 체크 Permission Check (0) | 2022.02.23 |
Fragment ViewModel 공유 (0) | 2022.02.22 |
Navigation, BottomNavigation 클릭시 Fragment 재생성 막기 (0) | 2022.02.17 |