15 May 2019

รู้จักกับ ViewPager2 ที่จะมาแทน ViewPager แบบเดิมๆ

Updated on


หลังจากที่ ViewPager ถูกใช้งานมาอย่างยาวนานพร้อมกับข้อจำกัดบางอย่างที่ไม่สามารถทำได้ ในตอนนี้ทีมแอนดรอยด์ก็ได้สร้าง ViewPager2 ขึ้นมาเพื่อใช้แทน ViewPager ตัวเก่าแล้ว

เมื่อใดก็ตามที่อยากจะทำให้ View สามารถแสดงข้อมูลได้หลายๆชุด แล้วอยากจะให้แสดงทีละตัวโดยผู้ใช้สามารถปัดไปทางซ้ายและขวาเมื่อดูข้อมูลทีละตัวได้ ก็คงไม่พ้นเจ้า ViewPager นี่แหละ


ถึงแม้ว่า ViewPager จะดูคล้ายคลึงกับ RecyclerView อยู่ไม่น้อย แต่ก็ยังมีความแตกต่างกันอยู่พอสมควร โดย View ที่แสดงข้อมูลจะถูก Snap อยู่บนหน้าจอเสมอ ซึ่งกรณีของ RecyclerView จะต้องใช้ SnapHelper เข้ามาช่วยถึงจะทำแบบนี้ได้

และ ViewPager ก็มี Adapter เป็นของตัวเองที่ชื่อว่า PageAdapter โดยแบ่งออกเป็น 2 คลาสด้วยกันคือ FragmentPagerAdapter และ FragmentStatePagerAdapter นั่นหมายความว่าสิ่งที่จะแสดงอยู่ใน ViewPager จะเป็น Fragment นั่นเอง (จริงๆสามารถแสดงเป็น View ได้นะ แต่ต้องเขียนโค้ดเพิ่มเข้าไปเล็กน้อย)

แต่เอาเข้าจริงก็ต้องยอมรับว่า RecyclerView นั้นมีลูกเล่นที่แพรวพราวกว่า ViewPager อยู่ดี จึงทำให้ทีมแอนดรอยด์ตัดสินใจสร้าง ViewPager2 ขึ้นมาโดยเบื้องหลังการทำงานของมันก็คือ RecyclerView นั่นเอง

ในขณะที่เขียนบทความนี้นี้ ViewPager2 เป็นเวอร์ชัน 1.0.0-alpha04 อยู่ และอาจจะมีการเพิ่มลูกเล่นต่างๆเข้ามามากกว่าที่เนื้อหาในบทความนี้พูดถึงในเวอร์ชันที่ใหม่กว่า

เริ่มต้นใช้งาน ViewPager2

โดยเพิ่ม Dependency ของ ViewPager2 เข้าไปในโปรเจคดังนี้

// build.gradle
implementation 'androidx.viewpager2:viewpager2:1.0.0'

การใช้งานใน Layout XML

ViewPager2 สามารถใช้งานได้เหมือนกัน View ทั่วๆไปได้เลย โดยจะมี Custom Attribute อยู่เพียง 1 ตัวเท่านั้น มีชื่อว่า android:orientation เอาไว้กำหนดทิศทางในการเลื่อนข้อมูลว่าอยากจะให้เป็น Horizontal หรือ Vertical

<androidx.viewpager2.widget.ViewPager2
    android:id="@+id/viewPager"
    ...
    android:orientation="horizontal" />

Adapter ของ ViewPager2

แสดง View บน ViewPager2

ViewPager2 รองรับ Adapter ของ RecyclerView ได้เลย จึงสามารถใช้รูปแบบโค้ดที่เป็น ViewHolder ของ RecyclerView ใน ViewPager2 ได้ทันที ไม่ต้องเรียนรู้อะไรเพิ่มเติมเลย

// AwesomePagerAdapter.kt
class AwesomePagerAdapter : RecyclerView.Adapter<AwesomeViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AwesomeViewHolder =
             AwesomeViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.view_holder_awesome, parent, false))

    override fun getItemCount(): Int = ...

    override fun onBindViewHolder(holder: AwesomeViewHolder, position: Int) {
        ...
    }
}

// AwesomeViewHolder.kt
class AwesomeViewHolder(override val containerView: View): RecyclerView.ViewHolder(containerView), LayoutContainer {
    ...
}

เพียงเท่านี้ก็สามารถนำ Adapter ไปกำหนดให้กับ ViewPager2 ได้แล้ว

// ใช้งานใน Activity หรือ Fragment
val adapter = AwesomePagerAdapter()
viewPager2.adapter = adapter

ง่ายใช่มั้ยล่ะ อยากจะทำ ViewHolder แบบ Multiple Type ก็ไม่ใช่เรื่องยากเช่นกัน

แสดง Fragment บน ViewPager2

ในกรณีที่ต้องการแสดงเป็น Fragment แทนก็ให้เปลี่ยนไปใช้ FragmentStateAdapter ของ ViewPager2 แทน

// AwesomePagerAdapter.kt
class AwesomePagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fragmentManager, lifecycle) {
    override fun createFragment(position: Int): Fragment = when (position) {
        0 -> ...
        1 -> ...
        else -> ...
    }
    
    override fun getItemCount(): Int = ...
}

คุ้นๆมั้ย? เหมือน FragmentPagerAdapter กับ FragmentStatePagerAdapter เลยใช่มั้ยล่ะ

จึงทำให้ผู้ที่หลงเข้ามาอ่านสามารถเปลี่ยนจาก Adapter ของ ViewPager มาใช้เป็น FragmentStateAdapter ได้ไม่ยาก เพราะว่าโครงสร้างข้างในเหมือนกันเป๊ะๆ

แต่ที่อยากให้สังเกตก็คือ Constructor ของ FragmentStateAdapter นอกจากจะต้องโยน FragmentManager เข้าไป จะต้องส่ง Lifecycle เข้าไปด้วย เพื่อให้ ViewPager2 สามารถแสดง Fragment ได้ถูกต้องตาม Lifecycle นั่นเอง

// ใช้งานใน Activity
val adapter = AwesomePagerAdapter(supportFragmentManager, lifecycle)
viewPager2.adapter = adapter

// ใช้งานใน Fragment
val adapter = AwesomePagerAdapter(fragmentManager, lifecycle)
viewPager2.adapter = adapter

แบบนี้ก็สามารถใช้กับ RecyclerView ได้ด้วย?

ใช่ครับ ถ้าอยากจะให้ RecyclerView แสดงข้อมูลอยู่ใน Fragment ก็สามารถใช้ FragmentStateAdapter ได้เลย

การอัปเดตข้อมูลที่อยู่ใน Adapter ของ ViewPager2

เนื่องจากเป็น Adapter ตัวเดียวกับใน RecyclerView จึงทำให้มีลูกเล่นตอนอัปเดตข้อมูลมากกว่า ViewPager แบบเดิม

adapter.notifyDataSetChanged()
adapter.notifyItemInserted(...)
adapter.notifyItemRangeInserted(...)
adapter.notifyItemChanged(...)
adapter.notifyItemRangeChanged(...)
adapter.notifyItemMoved(...)
adapter.notifyItemRemoved(...)
adapter.notifyItemRangeRemoved(...)

และถ้าเอา DiffUtil มาใช้ด้วยก็จะทำให้การสั่ง Notify เป็นเรื่องง่ายขึ้นไปอีก

มาดูกันที่ ViewPager2 บ้าง

คำสั่งต่างๆที่มีให้ใช้งานใน ViewPager2 จะมีดังนี้

viewPager2.adapter = AwesomePagerAdapter()
val adapter = viewPager2.adapter

viewPager2.orientation = ViewPager2.ORIENTATION_HORIZONTAL
val orientation = viewPager2.orientation

viewPager2.currentItem = 2
val currentItem = viewPager2.currentItem

viewPager2.isUserInputEnabled = true
val isUserInputEnabled = viewPager2.isUserInputEnabled

viewPager2.offscreenPageLimit = 3
val offscreenPageLimit = viewPager2.offscreenPageLimit

viewPager2.setPageTransformer(AwesomePageTransformer())

viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { ... })
viewPager2.unregisterOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { ... })

viewPager2.beginFakeDrag()
viewPager2.fakeDragBy(1000.toFloat())
viewPager2.endFakeDrag()

จะเห็นว่าคำสั่งที่มีให้ใช้งานค่อนข้างใกล้เคียงกับ ViewPager ตัวเดิมเลย และมีการตัดพวก ItemAnimator, ItemDecoration และ LayoutManager ออกไปด้วย เพราะจะให้ ViewPager2 จัดการเองทั้งหมด (ไม่แน่ใจว่าในอนาคตจะมีการเพิ่มให้รองรับมั้ย)

Page Transformer ใน ViewPager2

จุดเด่นของ ViewPager ที่เจ้าของบล็อกชอบก็คือ Page Transformer ที่สามารถกำหนด Transition เวลาที่ผู้ใช้เลื่อน ViewPager เพื่อเปลี่ยนจาก View ตัวหนึ่งไปเป็น View อีกตัวหนึ่ง


ซึ่ง ViewPager2 ก็ใส่ Page Transformer เตรียมไว้ให้เรียบร้อยแล้ว

// AwesomePageTransformer.kt
class AwesomePageTransformer:  ViewPager2.PageTransformer {
    override fun transformPage(page: View, position: Float) {
        ...
    }
}

จะเห็นว่ารูปแบบการ Implement ของ Page Transformer บน ViewPager2 นั้นเหมือนกับของเก่าเลย เพียงแค่ว่าไม่สามารถใช้ตัวเดิมได้ ต้องสร้างขึ้นมาใหม่จาก ViewPager2.PageTransformer แทน ส่วนโค้ดที่ใช้ข้างในนั้นสามารถใช้เหมือนของเดิมได้แบบเป๊ะๆ

Page Indicator สำหรับ ViewPager2

สิ่งหนึ่งที่จะเป็นปัญหาสำหรับการใช้งาน ViewPager2 ก็คงจะเป็น Page Indicator จาก 3rd Party Library ที่เขียนให้ผูกไว้กับ ViewPager โดยเฉพาะ

ยกตัวอย่างเช่น android-viewpager-indicator ของ Ravindu Wijewickrama (ravindu1024)

val viewPager: ViewPager = ...
val indicator: ViewPagerIndicator = ....
indicator.setPager(viewPager)

ถ้าในกรณีนี้ต้องบอกเลยว่ายังใช้กับ ViewPager2 ไม่ได้จ้า

แต่ถ้าเป็น Page Indicator อย่าง PageIndicatorView ของ Roman Danylyk (romandanylyk) ที่ไม่ได้บังคับว่าจะต้องกำหนด ViewPager เสมอไป แต่มีสามารถเรียก onPageSelected(...), onPageScrollStateChanged(...) และ onPageScrolled(...) โดยตรงได้

val viewPager2: ViewPager2 = ...
val indicator: PageIndicatorView = ...

viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
    override fun onPageSelected(position: Int) {
        indicator.onPageSelected(position)
    }

    override fun onPageScrollStateChanged(state: Int) {
        indicator.onPageScrollStateChanged(state)
    }

    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
        indicator.onPageScrolled(position, positionOffset, positionOffsetPixels)
    }
})

จึงทำให้เจ้าของบล็อกเปลี่ยนมาใช้วิธีกำหนดค่าผ่านคำสั่งดังกล่าวไว้ใน ViewPager2.OnPageChangeCallback ได้เลย

สรุป

บางครั้งเจ้าของบล็อกต้องมานั่งเขียน RecyclerView ให้เหมือนกับ ViewPager เพราะต้องการความสามารถบางอย่างใน RecyclerView ดังนั้นการมาของ ViewPager2 จะตอบโจทย์ให้กับเจ้าของบล็อกได้เป็นอย่างดี และจากที่ลองเล่นเจ้า ViewPager2 ก็ต้องบอกเลยว่าจุดเด่นของ ViewPager2 มีดังนี้

• สามารถ Migrate จาก ViewPager ได้ง่ายมาก
• ทำงานอยู่บน RecyclerView
• ใช้ Adapter ตัวเดียวกับของ RecyclerView
• รองรับคำสั่ง Notify ในรูปแบบต่างๆได้เหมือนกับ Adapter ของ RecyclerView
• สามารถใช้ DiffUtil ใน Adapter ได้
• Adapter รองรับทั้งแบบ Fragment และ View
• รองรับการแสดงแบบ Right-to-Left (RTL)
• สามารถกำหนดได้ว่าจะให้เลื่อนในแนวตั้งหรือแนวนอน

จริงๆแล้วการทำงานของ ViewPager ก็แทบจะไม่ต่างอะไรไปจาก RecyclerView ดังนั้นการเปลี่ยนมาใช้ ViewPager2 ที่ทำงานอยู่บน RecyclerView แทนก็จะช่วยให้ทีมแอนดรอยด์สามารถเพิ่มฟีเจอร์ใหม่ๆเข้าไปใน RecyclerView และรองรับกับ ViewPager2 ได้ง่ายๆ ไม่ต้องเสียเวลาเขียน 2 ที่ให้ยุ่งยากอีกต่อไป