Xây dựng ViewPager2 – Tablayout – Carousel

Introduction

Google đã giới thiệu ViewPager2 phiên bản chính thức từ November 20, 2019. Tuy nhiên gần đây android studio bản mới nó mới @Deprecated nên mình cũng thử chuyển qua dùng.
ViewPager2 là một bản mới được viết lại hoàn toàn để thay thế ViewPager cũ với nhiều điểm yếu.

What New?

implementation "androidx.viewpager2:viewpager2:1.0.0"

ViewPager2 sử dụng RecyclerView, một trong những component được sử dụng nhiều nhất trong Android. Đây là một thay đổi lớn đối với API ViewPager.

Sử dụng tất cả các chức năng của RecyclerView, ViewPager 2 cung cấp những cải tiến sau:

– Hỗ trợ phân trang dọc bằng cách sử dụng LinearLayoutManager. Điều này có nghĩa là bạn có thể dễ dàng chuyển đổi giữa các horizontal and vertical.
– Nó cung cấp hỗ trợ cho các layout từ  right-to-left hoặc RTL.
– Nó hỗ trợ việc sử dụng DiffUtil theo mặc định, vì nó là RecyclerView.

Example

Ở ví dụ này chúng ta sẽ kết hợp một vài thứ như ViewPager2 – Tablayout – Carousel và thêm hiệu ứng transform cho nó,kết quả như sau.

Bắt đầu nào 😀

Project  demo này được code trên android studio 4.1.1 dùng kotlin.

build.gradle nhớ thêm 2 thằng này và upadte plugins nhé các bạn

plugins {
    ..
    ..
    id 'kotlin-android-extensions'
    id 'kotlin-kapt'
}
..
..
implementation 'com.google.android.material:material:1.2.1'
implementation "androidx.viewpager2:viewpager2:1.0.0"

Step 1: Tạọ model FoodEntity.kt để chứa dữ liệu demo.

class FoodEntity(var name: String, var image: Int) : Serializable

Step 2: Cập nhật code layout activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    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="wrap_content"
        android:layout_alignParentTop="true" />

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tabLayout"
        android:layout_alignParentBottom="true" />
</RelativeLayout>

Step 3 : Giờ mình cần 1 cái adapter để đổ dữ liệu vào Viewpager.

Step 3.1 Tạo 1 class CardAdapter.kt y như dùng RecyclerView:

class CardAdapter(var arrFoods: List<FoodEntity>) : RecyclerView.Adapter<MyViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        return MyViewHolder(
            LayoutInflater.from(parent.context).inflate(R.layout.item_food_card, parent, false)
        )
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.image.setImageResource(arrFoods[position].image)
    }

    override fun getItemCount(): Int {
        return arrFoods.size
    }
}

class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    var image: ImageView = itemView.findViewById(R.id.image)
}

Layout item_food_card.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_marginLeft="@dimen/pageMarginAndoffset"
    android:layout_marginRight="@dimen/pageMarginAndoffset"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/image"
        tools:src="@drawable/image_1"
        android:adjustViewBounds="true"
        android:scaleType="centerCrop"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>


</RelativeLayout>

Giờ thử set adapter ở MainActivity.kt xem như nào nào.

   val foods = mutableListOf<FoodEntity>()
   foods.add(FoodEntity("Food 1", R.drawable.image_1))
   foods.add(FoodEntity("Food 2", R.drawable.image_2))
   foods.add(FoodEntity("Food 3", R.drawable.image_3))
   foods.add(FoodEntity("Food 4", R.drawable.image_4))
   foods.add(FoodEntity("Food 5", R.drawable.image_1))
viewPager.apply {
    orientation = ViewPager2.ORIENTATION_HORIZONTAL
    adapter = CardAdapter(foods)
}

Quá đơn giản luôn chúng ta có thể set orientation cho viewPager theo chiều ngang – dọc.

Cách trên là theo dạng RecyclerViewAdapter.

Step 3.2 Giờ mình muốn nó qua dạng FragmentAdapter để có thể xử lý nhiều logic hơn thì như nào?

Tạo 1 FoodCardAdapter.kt sử dụng FragmentStateAdapter.

class FoodCardAdapter(var arrFoods: List<FoodEntity>, activity: FragmentActivity) :
    FragmentStateAdapter(activity) {

    override fun getItemCount(): Int {
        return arrFoods.size
    }

    override fun createFragment(position: Int): Fragment {
        return FoodCardFragment.newInstance(arrFoods[position])
    }

}

Tiếp tục tạo 1 FoodCardFragment.kt như là 1 item của recyclerview.

class FoodCardFragment : Fragment() {
    companion object {
        fun newInstance (foodEntity: FoodEntity) :FoodCardFragment{
            val fragment = FoodCardFragment()
            val bundle = Bundle()
            bundle.putSerializable("Food_Arg",foodEntity)
            fragment.arguments = bundle
            return fragment
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.item_food_card, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val food  = arguments?.getSerializable("Food_Arg") as FoodEntity
        image.setImageResource(food.image)
    }
}

Oke đã xong giờ quay lại MainActivity thử cái Adapter này xem sao.

viewPager.apply {
   orientation = ViewPager2.ORIENTATION_HORIZONTAL
   adapter = FoodCardAdapter(foods, this@MainActivity)
   offscreenPageLimit = 3
}

Kết quả là y như nhau luôn tuỳ theo nhu cầu mà sử dụng nha các bạn.

Step 4: Sử dụng Transformer và Carousel

Để kết hợp nhiều Transforme thì chúng ta sẽ sử dụng: CompositePageTransformer

  val compositePageTransformer = CompositePageTransformer()
  compositePageTransformer.addTransformer(MarginPageTransformer(40))
  compositePageTransformer.addTransformer { page, position ->
            val absPosition = abs(position)
            val MAX_ALPHA = 1.0f
            val MIN_ALPHA = 0.7f
            page.alpha = MAX_ALPHA - (MAX_ALPHA - MIN_ALPHA) * absPosition
  }
 viewPager.setPageTransformer(compositePageTransformer)

Code ở trên bao gồm:

  • Margin giữa các item
  • Hiệu ứng alpha khi slide

Tiếp tục tạo hiệu ứng Carousel.

Cái này chúng ta sẽ cần chia ra 2 loại khi dùng RecyclerViewAdapter và FragmentStateAdapter

Nếu dùng theo cách CardAdapter.kt ở trên thì phần code để set hiệu ứng Carousel sẽ như sau:

val pageMargin = resources.getDimensionPixelOffset(R.dimen.pageMargin).toFloat()
val pageOffset = resources.getDimensionPixelOffset(R.dimen.pagerOffset).toFloat()       
compositePageTransformer.addTransformer { page, position ->
            val offset = position * -(2 * pageOffset + pageMargin)
            if (viewPager.orientation == ViewPager2.ORIENTATION_HORIZONTAL) {
                if (ViewCompat.getLayoutDirection(viewPager) == ViewCompat.LAYOUT_DIRECTION_RTL) {
                    page.translationX = -offset
                } else {
                    page.translationX = offset
                }
            } else {
                page.translationY = offset
            }
        }

Tuy nhiên phần transformer này sẽ không hoạt động nếu như bạn dùng FoodCardAdapter.kt

Sau khi research thì mình có đoạn code như sau cho trường hợp trên:

  val rv : RecyclerView = viewPager.getChildAt(0) as RecyclerView
  rv.setPadding(200,0,200,0)
  rv.clipToPadding = false

Tuyệt vời ông mặt trời, nó đã hoạt động  <3

Step 5 : Mải mê quá quay lại kết hợp Viewpager2 vào Tablayout nào.

Chúng ta sẽ sử dụng: TabLayoutMediator

  TabLayoutMediator(tabLayout, viewPager
        ) { tab, position ->
            tab.text = foods[position].name
        }.attach()

Đoạn code ở trên sẽ set title cho các tab.

Nếu bạn muốn lắng nghe sự kiện page change thì sẽ dùng sự kiện bên dưới.

 viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
   override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
               
  }
   override fun onPageSelected(position: Int) {
                
  }
 })

Xong rồi demo nho nhỏ đến đây là kết thúc. 🙂

Source code github.

Nguyễn Linh

Chia sẻ để cùng tiến bộ...