Photo by LOLIONI on Unsplash

[Android] ทำ List ข้อมูลจาก RecyclerView + SnapHelper แบบง่ายๆ

Nattapong Cheewalertsakul
InsightEra
Published in
3 min readMay 7, 2020

--

สำหรับใครยังไม่รู้จักทั้งตัว RecyclerView และ SnapHelper ว่าคืออะไร อธิบายแบบง่ายๆ RecyclerView คือ ตัวที่ช่วย list ข้อมูลให้แสดงออกมาคล้ายๆ กับ ListView ที่มีมานั่นแหละครับ

แต่จะมีข้อแตกต่างอยู่ที่เขียน code และทำความเข้าใจได้ยากขึ้นมาหน่อย แต่ๆๆๆๆ มันทำให้เป็น pattern มากขึ้น ช่วยให้เราจัด layout และทำ event ต่างๆ ได้ง่าย ไม่ใช่แค่นั้นนะ RecyclerView ยังทำให้ performance การแสดงข้อมูลดีขึ้น โดยที่มันจะ recycle ตัว list ที่เราเลื่อนผ่านไปแล้วให้มาแสดงใหม่พร้อมกับข้อมูลของชิ้นถัดไป ซึ่งทำให้ไม่ต้องไป render ตัว list ทั้งหมดในกรณีที่ข้อมูลมีหลายตัว

ส่วน SnapHelper เป็นตัวที่ทำให้เวลา scroll list ข้อมูลแล้วจะหยุดอยู่ตรงตำแหน่งที่กำหนดพอดี (snap) ถ้ายังไม่เข้าใจเดี๋ยวดูตัวอย่างได้หลังทำนะครับ

เอาล่ะ มาเริ่มทำ RecyclerView กันแบบง่ายๆ กันก่อน

  1. ติดตั้ง RecyclerView ก่อนนะครับ

โดยการเพิ่ม

implementation "androidx.recyclerview:recyclerview:1.1.0"

ลงใน file Gradle ให้เรียบร้อยก่อน

2. ต่อมาเป็นฝั่งของ UI ให้จัด Layout ตามที่ต้องการแล้วตามด้วยใส่ RecyclerView เข้าไป

movie_recycler_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/movieRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>


<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorText"
android:layout_marginTop="20sp"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Detail movie"
/>
</LinearLayout>

</LinearLayout>

3. เพิ่ม file xml ที่จะจัด layout ให้กับตัว content ใน list

movie_list_content.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5sp"
>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<ImageView
android:id="@+id/poster"
android:layout_width="wrap_content"
android:layout_height="300sp"
android:src="@drawable/avengers1"
android:adjustViewBounds="true"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/colorPrimaryDark"
android:padding="10sp">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/movie_title"
android:textColor="@color/colorText"
android:textSize="15sp"
android:textStyle="bold"
/>
<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/movie_time"
android:textColor="@color/colorText"
android:textSize="10sp"
/>
</LinearLayout>

</LinearLayout>



</LinearLayout>

4. ทำ Model สำหรับไว้เป็นข้อมูลที่จะเอาไป list ออกมา

Movie.kt

data class Movie(
var id : Long = 0,
var name : String? = null,
var time : String? = null,
var poster : Int = 0
) {
}

Note : ที่ poster เป็น type Int เพราะเราใช้รูปจาก folder drawble ซึ่งจะคืนค่ามาเป็น Int

5. มาถึงส่วนสำคัญอย่างแรกคือการสร้าง ViewHolder ไว้เป็นตัว map ข้อมูลลงใน list โดยเราจะต้อง extend class RecyclerView.ViewHolder และ set ค่าจาก Model ที่สร้างลงส่วนต่างๆ ของ View ตาม id

MovieViewHolder.kt

class MovieViewHolder(movieRecyclerView: View) : RecyclerView.ViewHolder(movieRecyclerView) {
val title = movieRecyclerView.title
val time = movieRecyclerView.time
val poster = movieRecyclerView.poster
}

6. อีกหนึ่งส่วนที่สำคัญคือ Adapter โดย class นี้เราจะทำการ extend RecyclerView.Adapter<MovieViewHolder> และรับเป็น Type ViewHolder ที่เราสร้างไว้ หลังจากนั้น override function ตามนี้
onCreateViewHolder : เป็นการนำ layout ที่ทำไว้มาใส่ใน RecyclerView ซึ่งถ้าหากมีหลาย layout เราสามารถสร้าง condition เพื่อนำ view แบบอื่นๆ มาใส่แทน
getItemCount : บอกจำนวนของข้อมูล
onBindViewHolder : รับค่าจาก list ข้อมูลมาแสดงตาม field บน ViewHolder

MovieAdapter.kt

class MovieAdapter(val item : ArrayList<Movie>) : RecyclerView.Adapter<MovieViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val inflate = LayoutInflater.from(parent.context).inflate(R.layout.movie_list_content,parent,false)
return MovieViewHolder(inflate)
}

override fun getItemCount(): Int {
return item.count()
}

override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
holder.title.text = item[position].name
holder.time.text = item[position].time
holder.poster.setImageResource(item[position].poster)
}

override fun getItemId(position: Int): Long {
return item[position].id
}

}

ใน onCreateViewHolder นำ layout ที่สร้างไว้ไปใส่ไว้ใน RecyclerView (inflate) ในที่นี้คือ movie_list_content และใส่ไปที่ context ของ parent ที่จะมาเรียกใช้
ส่วนใน onBindViewHolder ก็ทำการ map ข้อมูลจาก model เข้า field เป็นอันจบ

7. มาถึงส่วนสุดท้ายที่เราจะทำให้มันแสดงผล นั่นคือ MainActivity หรือ Activity อื่นๆ ที่เราต้องการให้แสดงขึ้น

class MainActivity : AppCompatActivity() {

private val movies = arrayListOf(
Movie(101,"Avengers","1hr 25m",R.drawable.avengers1),
Movie(102,"Avengers2","1hr 45m",R.drawable.avengers2),
Movie(103,"Avengers3", "2 hr 20m",R.drawable.avengers3),
Movie(104,"Avengers4", "3hr 00m", R.drawable.avengers4),
Movie(105,"Ant Man", "1hr 20m", R.drawable.antman),
Movie(106,"Black Panther", "2hr 10m", R.drawable.blackpanther),
Movie(107,"Captain America", "2hr 40m", R.drawable.captainamerica),
Movie(108,"Captain Marvel", "2hr 30m", R.drawable.captainmarvel),
Movie(109,"Iron man", "1hr 50m", R.drawable.ironman),
Movie(110,"Spider Man", "2hr 30m", R.drawable.spiderman)
)


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.movie_recycler_view)

val linear = LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false)


var movieAdapter = MovieAdapter(movies)
movieRecyclerView.apply {
layoutManager = linear
adapter = movieAdapter
}

}

}

ผมเริ่มจาก mockup data ขึ้นมาแบบง่ายๆ ก่อนเพื่อที่จะเอาไปแสดงเป็น list หลังจากนั้น ใน function onCreate ก็ทำการประกาศตัวแปร 2 ตัว คือ layout ที่ต้องการใช้งาน ในที่นี้เป็น LinearLayout แบบ Horizontal และ adapter ที่เราเขียนขึ้นข้างต้นพร้อมกับใส่ parameter เป็นตัว item ที่เรา mock เอาไว้ สุดท้ายเราจะเรียก recyclerView โดยเรียกจาก ID ใน xml ที่เรากำหนดไว้ได้เลย ใส่ค่า layoutManager กับ adapter ให้กับ view

ใน setContentView ผมแก้จากที่ R.layout.activity_main เป็น R.layout.movie_recycler_view เพราะตอนแรกผมสร้างเป็น layout แยกอีกอัน

มาดูผลลัพธ์กัน

RecyclerView horizontal linear layout

8. ทีนี้จะเพิ่มลูกเล่นเข้าไปนิดนึงโดยการใช้ SnapHelper ช่วย เพียงเพิ่ม code ขึ้นมา 2 บรรทัดเท่านั้น ตามด้านล่าง

var movieAdapter = MovieAdapter(movies)
movieRecyclerView.apply {
layoutManager = linear
adapter = movieAdapter
}
val snap = LinearSnapHelper()
snap.attachToRecyclerView(movieRecyclerView)

ทีนี้ลองเทียบความแตกต่างในการ scroll ตัว item จะถูก snap ไว้ที่ตำแหน่งตรงกลางตลอด ยกเว้น item แรกและ item สุดท้าย

RecyclerView with snaphelper

ศึกษาเพิ่มเติมเกี่ยวกับ RecyclerView และ SnapHelper ได้ตามนี้เลยครับ

--

--