หลังจากที่ Android 5.0 Lollipop ได้เปิดตัวอย่างเป็นทางการ ในฝั่งของนักพัฒนาก็ได้มีการเพิ่ม RecyclerView เข้ามาใหม่ ซึ่งถูกสร้างขึ้นมาเพื่อแทนที่ ListView และ GridView โดยมีจุดเด่นคือมีความยืดหยุ่นในการใช้งานมากกว่า

และความสามารถอย่างหนึ่งที่น่าสนใจใน RecyclerView ก็คือ LayoutManager ที่จะช่วยให้นักพัฒนาสามารถจัดรูปแบบในการแสดงผลได้ง่ายขึ้น

รู้จักกับ LayoutManager

LayoutManager จะคอยจัดการเรื่องขนาดและตำแหน่งของ Item View แต่ละตัวที่จะนำไปแสดงผลใน RecyclerView ว่าจะให้มีขนาดเท่าไร เรียงกันแบบไหน ซึ่งจะช่วยให้การแสดงข้อมูลบน RecyclerView มีรูปแบบที่หลากหลาย อยากจะเรียงข้อมูลเป็น List ก็ทำได้ อยากได้เป็นตารางแบบ Grid ก็ได้ หรืออยากจะได้รูปแบบนอกเหนือจากนั้นก็สร้างขึ้นมาเองได้ตามใจชอบ

โดยจะมี LayoutManager แบบพื้นฐานเพื่อให้นักพัฒนานำไปใช้งานได้ทันที อยู่ทั้งหมด 3 แบบด้วยกันคือ

  • LinearLayoutManager
  • GridLayoutManager
  • StaggeredGridLayoutManager

ในการกำหนดค่า LayoutManager ใด ๆ ให้กับ RecyclerView จะใช้คำสั่งแบบนี้

val recyclerView: RecyclerView = /* ... */
val layoutManager: LayoutManager = /* ... */
recyclerView.layoutManager = layoutManager

และในการใช้งาน RecyclerView จะบังคับให้นักพัฒนาต้องกำหนด LayoutManager ก่อนเสมอ ไม่ได้มีค่า Default ให้

ทีนี้มาดูกันว่า LayoutManager แต่ละตัวมีไว้ใช้ทำอะไรบ้าง

LinearLayoutManager

LinearLayout ทำงานยังไง LinearLayoutManager ก็ทำงานเช่นนั้นแหละครับ โดยจะทำหน้าที่จัด Item View ให้เรียงต่อกันในแนวตั้งหรือแนวนอนนั่นเอง

สำหรับวิธีใช้งานก็ง่ายมาก แค่สร้างขึ้นมาแล้วกำหนดให้ RecyclerView ได้เลย

val context: Context = /* ... */
val recyclerView: RecyclerView = /* ... */
val layoutManager = LinearLayoutManager(context)
recyclerView.layoutManager = layoutManager

ซึ่งคำสั่งแบบนี้จะได้ LinearLayoutManager เป็นแบบ Vertical ซึ่งเป็นค่า Default ในกรณีที่ไม่ได้กำหนดค่าใด ๆ เพิ่มเข้าไป

โดย LinearLayoutManager จะมี Overload Constructor ไว้ให้ 2 ตัวด้วยกัน

LinearLayoutManager(context: Context)
LinearLayoutManager(context: Context, orientation: Int, reverseLayout: Boolean)

ถ้าอยากได้เป็นแบบ Horizontal แทน ก็ให้สร้างขึ้นมาแบบนี้

val context: Context = /* ... */
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)

นอกจากนี้ยังสามารถกำหนดให้ LinearLayoutManager เรียงข้อมูลแบบ Reverse ได้ด้วย เพียงแค่กำหนดค่าใน reverseLayout เป็น true

val context: Context = /* ... */
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, true)

GridLayoutManager

ใช้สำหรับเรียง Item View ให้เป็นตาราง สามารถกำหนดได้ว่าจะให้เรียงในแนวตั้งหรือแนวนอนแบบเดียวกับ LinearLayoutManager เลย

และในการสร้าง GridLayoutManager ก็จะคล้าย ๆ กับ LinearLayoutManager เช่นกัน

GridLayoutManager(context: Context, spanCount: Int)
GridLayoutManager(context: Context, spanCount: Int, orientation: Int, reverseLayout: Boolean)

แต่จะมีค่าที่ต้องกำหนดเพิ่มเข้ามาอีกตัวหนึ่งที่ชื่อว่า spanCount เพื่อกำหนดว่าอยากจะให้ RecyclerView แสดงข้อมูลทั้งหมดกี่คอลัมน์

val context: Context = /* ... */
val recyclerView: RecyclerView = /* ... */
val layoutManager = GridLayoutManager(context, 4)
recyclerView.layoutManager = layoutManager

ถ้าไม่ได้กำหนด orientation จะได้ GridLayoutManager เป็นแบบ Vertical ดังนั้นถ้าอยากได้แบบ Horizontal ก็ให้กำหนดค่าเพิ่มเข้าไปแบบนี้

val context: Context = /* ... */
val layoutManager = GridLayoutManager(context, 4, GridLayoutManager.HORIZONTAL, false)

และสามารถกำหนดให้เรียงลำดับแบบ Reverse ได้ด้วยนะ

แต่พอพูดถึงตารางแล้ว ในบางครั้งนักพัฒนาก็อาจจะอยากให้ Item View บางตัวแสดงเต็มแถว และบางตัวแสดงตามคอลัมน์ที่กำหนดไว้ให้ ซึ่งจะต้องใช้สิ่งที่เรียกว่า SpanSizeLookup

โดย SpanSizeLookup จะช่วยให้นักพัฒนาสามารถกำหนดได้ว่า อยากจะให้ Item View ตัวไหนใช้พื้นที่ในการแสดงผลกี่คอลัมน์

val context: Context = /* ... */
val layoutManager = GridLayoutManager(context, 3)
layoutManager.spanSizeLookup = object: GridLayoutManager.SpanSizeLookup() {
    override fun getSpanSize(position: Int): Int {
        return /* Number of column */
    }
}

การกำหนดจำนวนคอลัมน์ให้กับ Item View มักจะเป็น RecyclerView ที่แสดงข้อมูลแบบ Multiple View Type ดังนั้นจึงต้องใช้ควบคู่กับ Adapter ของ RecyclerView ด้วย เพื่อเช็คว่าเป็น View Type แบบไหน

val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = /* ... */
val layoutManager = GridLayoutManager(this, 3)
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
    override fun getSpanSize(position: Int): Int {
        return when (adapter.getItemViewType(position)) {
            TYPE_HEADER, TYPE_BANNER -> 3
            TYPE_CARD, TYPE_MORE -> 1
            TYPE_RECOMMENDATION -> 2
            else -> 0
        }
    }
}

โดยการใช้ SpanSizeLookup นักพัฒนาจะต้องจัด Item View ให้ตรงกับจำนวนคอลัมน์ที่ต้องการด้วย เพราะมีโอกาสที่ Item View บางตัวจะตัดขึ้นแถวใหม่ได้ ถ้าผลรวมในคอลัมน์นั้นเกินจำนวนคอลัมน์ที่กำหนดไว้ใน GridLayoutManager

StaggeredGridLayoutManager

เป็นการต่อยอดจาก GridLayoutManager อีกทีหนึ่ง โดยจะเป็นการเรียงข้อมูลในลักษณะตาราง โดยที่ Item View แต่ละตัวจะมีขนาดไม่เท่ากัน (Vertical จะดูที่ความสูง ส่วน Horizontal จะดูที่ความกว้าง)

โดยปกติแล้ว เวลาแสดง Item View ที่มีขนาดไม่เท่ากันใน GridLayoutManager ข้อมูลในแต่ละแถวก็จะมีความสูงโดยอิงจาก Item View ที่สูงสุดในแถวนั้น

สำหรับ StaggeredGridLayoutManager นั้นจะเรียงข้อมูลต่อกัน โดยสนใจแค่ขนาดของ Item View ไม่มีการแบ่งข้อมูลเป็นแถว ๆ จึงทำให้ StaggeredGridLayoutManager เหมาะกับข้อมูลที่ Dynamic มาก ๆ

StaggeredGridLayoutManager(spanCount: Int, orientation: Int)

ในการใช้งาน StaggerdGridLayoutManager จะต้องกำหนดค่าทั้งหมด 2 ตัวด้วยกันคือ จำนวนคอลัมน์ที่ต้องการและทิศทางในการเรียงข้อมูล

val recyclerView: RecyclerView = /* ... */
val layoutManager = StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)
recyclerView.layoutManager = layoutManager

มี LayoutManager แบบอื่นอีกมั้ย?

จริง ๆ แล้ว LayoutManager พื้นฐานทั้ง 3 ตัวก็แทบจะครอบคลุมการใช้งานทั่วไปทั้งหมดแล้ว แต่ในกรณีที่ต้องการจัดข้อมูลในรูปแบบที่นอกเหนือจากนี้ก็สามารถสร้าง LayoutManager เองได้ หรือจะใช้ Library ของนักพัฒนาคนอื่นที่ทำไว้ก็ได้นะ

สรุป

LayoutManager เป็นหนึ่งในคุณสมบัติของ RecyclerView ที่จะแยกการจัดเรียง Item View ออกมาจากตัว RecyclerView เพื่อให้นักพัฒนาสามารถควบคุมและจัดการได้อย่างอิสระ ไม่ต้องกลัวว่าจะไปกระทบการทำงานส่วนอื่น ๆ ของ RecyclerView