26 December 2017

ItemTouchHelper ใน Recycler View ที่จะช่วยให้การ Drag และ Swipe เป็นเรื่องที่โคตรง่าย

Updated on


        ย้อนไปเมื่อหลายปีที่แล้ว ในตอนที่ List View กำลังเป็นที่นิยม ในตอนนั้นการจะทำ Swipe To Dismiss หรือ Drag & Drop ให้กับ List View จะต้องมานั่งเขียนโค้ดใส่ลงไปเอง ซึ่งโค้ดก็จะดูรกหน่อยๆ เพราะต้องมีการคำนวณนู่นนั่นนี่เต็มไปหมด

        จนมาถึงยุคของ Recycler View ที่ถูกออกแบบมาให้มีลักษณะยืดหยุ่นต่อการนำไปใช้งาน อยากจะให้ทำอะไรเพิ่มเติมก็จะมีคลาสที่คอยจัดการหน้าที่นั้นๆให้อยู่แล้ว ที่ต้องทำก็แค่เรียกใช้งานให้ถูกต้อง ซึ่งการทำ Swipe To Dismiss หรือ Drag & Drop บน Recycler View นั้นจะเป็นเรื่องที่ง่ายดายโคตรๆ ด้วยคลาสที่มีชื่อว่า ItemTouchHelper ที่ผู้ที่หลงเข้ามาอ่านหลายๆคนอาจจะไม่เคยได้ยินมาก่อน

เตรียม Recycler View แบบรวบรัด

         เพื่อเน้นการทำงานของคลาส ItemTouchHelper เป็นหลัก ดังนั้นจึงขอข้ามขั้นตอนการสร้าง Recycler View ไปแบบไวๆนะจ๊ะ


รู้จักและเรียกใช้งานคลาส ItemTouchHelper

        ItemTouchHelper เป็นคลาสที่ถูกสร้างขึ้นมาเพื่อทำหน้าที่ควบคุมการ Swipe To Dismiss และการ Drag & Drop ใน Recycler View โดยเฉพาะ ซึ่งถูกเพิ่มเข้ามาตั้งแต่ RecyclerView เวอร์ชัน 24.1.0 ขึ้นไป นั่นหมายความว่าตอนนี้ผู้ที่หลงเข้ามาอ่านสามารถใช้ ItemTouchHelper ได้หายห่วงแน่นอน ซึ่งเบื้องหลังของคลาส ItemTouchHelper คือสืบทอดมาจาก ItemDecoration ของ Recycler View นั่นเอง

        ในการเรียกใช้งาน ItemTouchHelper จะเรียกผ่าน ItemTouchHelper.Callback โดยมีเงื่อนไขว่าผู้ที่หลงเข้ามาอ่านจะต้อง สร้าง Callback สำหรับ ItemTouchHelper ขึ้นมากำหนดรูปแบบการทำงานตามต้องการเอง โดย ItemTouchHelper.Callback จะมี Method ที่บังคับให้ประกาศไว้ทั้งหมด 3 ด้วยดังนี้

// CustomItemTouchHelperCallback.kt

import android.support.v7.widget.RecyclerView
import android.support.v7.widget.helper.ItemTouchHelper

class CustomItemTouchHelperCallback : ItemTouchHelper.Callback() {
    override fun getMovementFlags(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?): Int {
        ...
    }

    override fun onMove(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?, target: RecyclerView.ViewHolder?): Boolean {
        ...
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder?, direction: Int) {
        ...
    }
}

        • getMovementFlags(...) มีไว้กำหนดทิศทางที่จะให้ Swipe หรือ Drag

        • onMove(...) ทำงานเมื่อผู้ใช้ Drag & Drop ซึ่งจะรู้ได้ว่าผู้ใช้ลาก Item ไหนไปไว้ที่ตำแหน่งไหน

        • onSwiped(...) ทำงานเมื่อผู้ใช้ Swipe To Dismiss ซึ่งสามารถกำหนดได้ว่าจะให้ Item นั้นๆทำงานอะไร

        ซึ่งใน ItemTouchHelper.Callback ผู้ที่หลงเข้ามาอ่านสามารถกำหนดได้ตามใจชอบเลยว่าเมื่อ Drag & Drop หรือ Swipe To Dismiss จะให้แสดงผลหรือเกิดอะไรขึ้น

        และนอกจากนี้ยังมี Override Method อื่นๆอีกมากมายที่ใช้กำหนดรูปแบบการทำงานของ ItemTouchHelper ด้วย

// CustomItemTouchHelperCallback.kt

class CustomItemTouchHelperCallback : ItemTouchHelper.Callback() {
    ...

    override fun isItemViewSwipeEnabled(): Boolean {
        // Swipe To Dismiss ได้หรือไม่
        return true
    }

    override fun isLongPressDragEnabled(): Boolean {
        // กดค้างเพื่อ Drag & Drop ได้หรือไม่
        return true
    }
}

        ทีนี้กลับมาดูที่ getMovementFlags(...) กันต่อ เพราะ Method นี้คือตัวกำหนดว่าอยากจะให้ Drag & Drop และ Swipe To Dismiss ทิศทางไหนบ้าง

        ยกตัวอย่างเช่น Drag & Drop ด้วยการลากขึ้นและลง แต่ถ้าอยาก Swipe To Dismiss ให้ปัดซ้ายหรือขวา ก็ให้กำหนด Flag แบบนี้

// CustomItemTouchHelperCallback.kt

class CustomItemTouchHelperCallback(private var listener: ItemTouchHelperListener) : ItemTouchHelper.Callback() {
    override fun getMovementFlags(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?): Int {
        val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
        val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
        return ItemTouchHelper.Callback.makeMovementFlags(dragFlags, swipeFlags)
    }

    ...
}

         หรือถ้าอยากให้ Swipe To Dismiss เท่านั้น ก็ให้ Flag ของ Drag & Drop มีค่าเป็น 0 แล้วกำหนด isLongPressDragEnabled เป็น False ซะ

// CustomItemTouchHelperCallback.kt

class CustomItemTouchHelperCallback(private var listener: ItemTouchHelperListener) : ItemTouchHelper.Callback() {
    override fun getMovementFlags(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?): Int {
        val dragFlags = 0
        val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
        return ItemTouchHelper.Callback.makeMovementFlags(dragFlags, swipeFlags)
    }

    override fun isLongPressDragEnabled(): Boolean {
        return false
    }

    ...
}

         ส่วน onMove กับ onSwipe ที่เอาไว้กำหนดว่าจะให้ทำอะไรเมื่อ Drag & Drop หรือ Swipe To Dismiss เดิมทีจะต้องไปสั่ง Adapter อีกทีว่ามีการย้าย Item หรือ Item ถูกลบออก เพื่อให้โค้ดไม่ปนกับ Adapter จนเกินไป ควรสร้าง Interface เพื่อเป็นตัวกลางให้ CustomItemTouchHelperCallback ไปสั่ง Adapter อีกที

// CustomItemTouchHelperListener.kt

interface CustomItemTouchHelperListener {
    fun onItemMove(fromPosition: Int, toPosition: Int) : Boolean

    fun onItemDismiss(position: Int)
}

        ดังนั้น CustomItemTouchHelperCallback ก็ต้องเพิ่ม CustomItemTouchHelperListener เข้าไปด้วย

// จากเดิม

class CustomItemTouchHelperCallback : ItemTouchHelper.Callback() {
    ...
}

// กลายเป็น
class CustomItemTouchHelperCallback(private var listener: CustomItemTouchHelperListener) : ItemTouchHelper.Callback() {
    ...
}

        เวลา Drag & Drop หรือ Swipe To Dismiss ก็ให้เรียกผ่าน CustomItemTouchHelperListener เพื่อบอก Adapter ให้อัพเดท Item ของตัวเองใหม่

// CustomItemTouchHelperCallback.kt

class CustomItemTouchHelperCallback(private var listener: CustomItemTouchHelperListener) : ItemTouchHelper.Callback() {
    ...
    override fun onMove(recyclerView: RecyclerView?, source: RecyclerView.ViewHolder?, target: RecyclerView.ViewHolder?): Boolean {
        if (source?.itemViewType != target?.itemViewType) {
            return false
        }
        if (source != null && target != null) {
            return listener.onItemMove(source.adapterPosition, target.adapterPosition)
        }
        return false
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder?, direction: Int) {
        viewHolder?.let {
            listener.onItemDismiss(viewHolder.adapterPosition)
        }
    }
}

         กลับมาที่ฝั่ง Adapter ของ Recycler View ก็อย่าลืมเพิ่ม CustomitemTouchHelperListener เข้าไปด้วย แล้ว Override Method ทั้ง 2 ตัวที่เตรียมไว้

// AndroidInfoAdapter.kt

class AndroidInfoAdapter(private val androidList: MutableList<Android>?) : RecyclerView.Adapter<AndroidInfoViewHolder>(), CustomItemTouchHelperListener {
    ...

    override fun onItemMove(fromPosition: Int, toPosition: Int) : Boolean {
        // เมื่อ Item ถูก Drag & Drop
    }

    override fun onItemDismiss(position: Int) {
        // เมื่อ Item ถูก Swipe To Dismiss
    }
}

        สิ่งที่ต้องทำในนี้คือเมื่อมีการ Drag & Drop จะต้องย้าย Item (ในที่นี้คือ MutableList<Android>) ตามที่มีการ Drag & Drop ซึ่งเจ้าของบล็อกจะเอา Collections เข้ามาช่วยเพื่อให้ย้ายตำแหน่งของ Item ได้ง่ายขึ้น จากนั้นก็สั่ง notifyItemMoved(...) เพื่อให้ Recycler View แสดง Animation ให้ผู้ใช้เห็น

        สำหรับ Swipe To Dismiss ก็จะต้องลบ Item ที่ถูกเลือกออกไป แล้วสั่ง notifyItemRemoved(...) เพื่อให้ Recycler View แสดง Animation ให้ผู้ใช้เห็นว่ามีการลบ Item นั้นออกไปจริงๆ

// AndroidInfoAdapter.kt

class AndroidInfoAdapter(private val androidList: MutableList<Android>?) : RecyclerView.Adapter<AndroidInfoViewHolder>(), CustomItemTouchHelperListener {
    ..

    override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
        Collections.swap(androidList, fromPosition, toPosition)
        notifyItemMoved(fromPosition, toPosition)
        return true
    }

    override fun onItemDismiss(position: Int) {
        androidList?.removeAt(position)
        notifyItemRemoved(position)
    }
}

        สรุปความสัมพันธ์ระหว่าง Adapter, CustomItemTouchHelperListener และ CustomItemTouchHelperCallback ได้ดังนี้


        เมื่อ CustomItemTouchHelperCallback พร้อมใช้งานแล้ว ก็อย่าลืมกำหนดให้กับ Recycler View ด้วยล่ะ โดยจะต้องเอาไปกำหนดในคลาส ItemTouchHelper อีกทีหนึ่ง ซึ่งในคลาสดังกล่าวจะมี Method ที่ชื่อว่า attachToRecyclerView(...) เพื่อกำหนด Recycler View ที่ต้องการ

// MainActivity.kt

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.helper.ItemTouchHelper
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        initRecyclerView()
    }

    private fun initRecyclerView() {
        val adapter = AndroidInfoAdapter(...)
        rvAndroidVersion.layoutManager = LinearLayoutManager(this)
        rvAndroidVersion.adapter = adapter
        val callback = CustomItemTouchHelperCallback(adapter)
        val itemTouchHelper = ItemTouchHelper(callback)
        itemTouchHelper.attachToRecyclerView(rvAndroidVersion)
    }
}

        เพียงเท่านี้ Recycler View ก็รองรับ Drag & Drop และ Swipe To Dismiss แล้ว


สรุป

        ถ้าผู้ที่หลงเข้ามาอ่านเคยเขียน Drag & Drop หรือ Swipe To Dismiss บน List View มาก่อน ก็จะรู้ว่าต้องเขียนโค้ดเข้าไปใน List View เยอะพอสมควร ซึ่งทำให้มีโค้ดต่างๆนานาปนอยู่ในนั้นเต็มไปหมด แต่สำหรับ Recycler View นั้นมีการออกแบบให้แยกกับทำงานแต่ละส่วนออกจากกัน จึงเห็นว่าการทำ Drag & Drop หรือ Swipe To Dismiss จะมีโค้ดที่แยกออกมาจาก Adapter ของ Recycler View โดยตรง ที่ต้องเพิ่มเข้าไปก็แค่คำสั่งย้ายหรือลบ Item เท่านั้น แถมยังสามารถกำหนดรูปแบบการทำงานต่างๆใน ItemTouchHelper ได้ตามใจชอบอีกด้วย

        สำหรับตัวอย่างโค้ดในบทความนี้สามารถตามไปดูตามไปเสพย์กันได้ที่ Android-RecyclerViewItemTouchHelper [GitHub]

แหล่งข้อมูลอ้างอิง

        • Drag and Swipe with RecyclerView [Medium]
        • ItemTouchHelper [Android Developers]