สวัสดีทุกคน. ฉันตัดสินใจยกหัวข้อรายการที่ซับซ้อนใน Android หัวข้อนี้ทำให้ฉันงงในตอนแรก ดูเหมือนยากสำหรับฉันมาก แต่จริงๆ แล้วทุกอย่างง่ายกว่า และฉันคิดว่าใครก็ตามที่ประสบปัญหานี้อยู่จะพบว่าบทความนี้มีประโยชน์ ตัวอย่างทั้งหมดเขียนด้วยภาษา Kotlin ฉันพยายามเขียนความคิดเห็นทุกที่และทำให้ชัดเจนที่สุดสำหรับผู้ที่เขียนด้วยภาษา Java นี่คือโครงสร้างชั้นเรียนของเรา:
MainActivity
- นี่คือชั้นเรียนหลัก ListAdapter
- ที่นี่เราผูกรายการของเราเพื่อดูองค์ประกอบ ViewHolder
(มีหลายรายการ) - นี่คือมาร์กอัปของเราสำหรับองค์ประกอบแต่ละประเภท คลาสไอเท็มคือข้อมูลสำหรับรายการ (pojo) ไฟล์ XML คือมาร์กอัป ( activity_main
สำหรับหน้าจอหลัก ส่วนที่เหลือสำหรับองค์ประกอบรายการแต่ละประเภท) สิ่งแรกที่เราต้องทำคือเพิ่มการขึ้นต่อกันเพื่อให้เราสามารถใช้รายการได้ (ด้วยเหตุผลบางประการ การขึ้นต่อกันนี้หายไปเมื่อสร้างโปรเจ็กต์) ในไฟล์build.gradle
ในบล็อกdependencies
เราเพิ่มบรรทัด:
implementation "androidx.recyclerview:recyclerview:1.1.0"
ต่อไปเราจะค้นหาไฟล์activity_main.xml
: ที่นี่มีเพียงองค์ประกอบของเราสำหรับรายการ - RecyclerView
. เราเพิ่มการเยื้องและการจัดรูปแบบตามรสนิยม BaseViewHolder
ต่อไปเราจะ สร้างคลาส มันจะเป็นนามธรรม เนื่องจากคลาสทั้งหมดของเราจะสืบทอดจากViewHolder
มัน ค่อนข้างเป็นไปได้ที่จะทำโดยไม่มีมัน แต่ฉันยังคงแนะนำให้ตรวจสอบให้แน่ใจว่าคลาสที่สืบทอดทั้งหมดนั้นมีโครงสร้างเหมือนกัน (ใน Kotlin extends
และimplement
ถูกใช้ แทน :
. เช่นเดียวกับใน Java คุณสามารถสืบทอดจากคลาสเดียวและใช้งานอินเทอร์เฟซจำนวนมากได้) ต่อไป เราจะสร้างคลาสข้อมูลของเรา: ItemTitle
, ItemGoogle
และItemApple
. ใน Java คลาสเหล่านี้เป็นคลาสธรรมดาที่มี Constructor และ Getters
data class ItemGoogle(
val name: String,
val product: String,
val version: String,
val isUse: Boolean
) : ListMarker
data class ItemApple(
val name: String,
val country: String,
val year: Int
) : ListMarker
data class ItemTitle(
val title: String,
val amount: Int
) : ListMarker
มาเน้นที่อินเทอร์เฟซที่ListMarker
นี่
//это наш маркер. Его должны реализовать все айтемы, которые будут
//отображаться в конечном итоге в списке
interface ListMarker
คลาสข้อมูลทั้งหมดของเราใช้อินเทอร์เฟซนี้เพื่อส่งให้เป็นประเภทเดียวกัน เราจะต้องการสิ่งนี้ในภายหลังสำหรับรายการของเรา ต่อไป เราจะสร้างมุมมองของเราสำหรับแต่ละองค์ประกอบของรายการ อย่าลืมว่าแต่ละอันสืบทอดมาจากBaseViewHolder
. ในคลาสเหล่านี้ เราระบุว่าองค์ประกอบใดview
ที่สอดคล้องกับฟิลด์จากคลาสข้อมูล
abstract class BaseViewHolder<t>(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun bind(item: T)
}
class GoogleViewHolder(view: View) : BaseViewHolder<itemgoogle>(view) {
override fun bind(item: ItemGoogle) {
itemView.tvName.text = item.name
itemView.tvProduct.text = item.product
itemView.tvVersion.text = item.version
if (item.isUse) {
itemView.tvProduct.visibility = View.GONE
} else {
itemView.tvProduct.visibility = View.VISIBLE
}
}
}
class AppleViewHolder(view: View) : BaseViewHolder<itemapple>(view) {
override fun bind(item: ItemApple) {
//можем делать так
itemView.tvName.text = item.name
itemView.tvCountry.text = item.country
itemView.tvYear.text = item.year.toString()
/*----сверху и снизу два идентичных блока----*/
//а можем сделать такой блок и не использовать в каждой строке itemView
with(itemView) {
tvName.text = item.name
tvCountry.text = item.country
tvYear.text = item.year.toString()
}
}
}
class TitleViewHolder(view: View) : BaseViewHolder<itemtitle>(view) {
override fun bind(item: ItemTitle) {
itemView.tvTitle.text = item.title
itemView.tvAmount.text = item.amount.toString()
}
}
ไม่มีตรรกะใดที่นี่ ทุกอย่างง่ายมาก นอกจากนี้ ผู้ชมแต่ละคนก็มีเค้าโครงของตัวเอง: list_item_title.xml
list_item_google.xml
list_item_apple.xml
ตอนนี้เรามาดูส่วนที่ยากที่สุดกันดีกว่า - ListAdapter
.
class ListAdapter : RecyclerView.Adapter<baseviewholder<*>>() {
companion object {
//задаем константы для каждого типа айтема
private const val TYPE_TITLE = 0
private const val TYPE_GOOGLE = 1
private const val TYPE_APPLE = 2
}
//здесь можно использовать обычный ArrayList
//сюда добавляются все айтемы, которые реализовали интерфейс ListMarker
//How вариант можно было сделать mutableListOf<any>() и обойтись без интерфейса
private val items = mutableListOf<listmarker>()
internal fun swapData(list: List<listmarker>) {
items.clear()
items.addAll(list)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
return when(viewType) {
//задаем разметку для каждого типа айтема
TYPE_TITLE -> TitleViewHolder(parent.inflate(R.layout.list_item_title))
TYPE_GOOGLE -> GoogleViewHolder(parent.inflate(R.layout.list_item_google))
TYPE_APPLE -> AppleViewHolder(parent.inflate(R.layout.list_item_apple))
else -> throw IllegalArgumentException("Invalid view type")
}
}
override fun getItemViewType(position: Int): Int {
return when (items[position]) {
is ItemTitle -> TYPE_TITLE
is ItemGoogle -> TYPE_GOOGLE
is ItemApple -> TYPE_APPLE
else -> throw IllegalArgumentException("Invalid type of item $position")
}
}
override fun getItemCount(): Int {
//этот метод определяет размер списка
return items.size
}
override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
val element = items[position]
when (holder) {
//отправляем каждый айтем к своему ViewHolder
is TitleViewHolder -> holder.bind(element as ItemTitle)
is GoogleViewHolder -> holder.bind(element as ItemGoogle)
is AppleViewHolder -> holder.bind(element as ItemApple)
else -> throw IllegalArgumentException()
}
}
}
</listmarker></listmarker></any></baseviewholder<*>
เมื่อเราสืบทอดคลาสของเราจากRecyclerView.Adapter
เราจะต้องแทนที่สามวิธี: onCreateViewHolder
, getItemCount
, onBindViewHolder
. onCreateViewHolder
- ที่นี่เราเริ่มต้นชั้นเรียนของViewHolder
เรา getItemCount
— วิธีการนี้จะรับผิดชอบขนาดของรายการ onBindViewHolder
- ที่นี่เราส่งองค์ประกอบรายการไปยังชั้นเรียนของViewHolder
เรา สำหรับรายการปกติที่มีองค์ประกอบประเภทเดียว นี่ก็เพียงพอแล้ว แต่สำหรับประเภทต่างๆ เรายังจำเป็นต้องกำหนดวิธีการใหม่getItemViewType
(สำหรับสิ่งนี้ เราใช้ค่าคงที่ที่อยู่ด้านบนสุดของคลาสอะแดปเตอร์ของเรา ใน Java คุณสามารถใช้final
ตัวแปรสำหรับสิ่งนี้) นอกจากนี้ใน Java onBindViewHolder
แทนที่จะใช้นิพจน์when
คุณสามารถใช้ไฟล์if
. และสุดท้าย เรามาดูคลาสหลักของเรากันดีกว่า - MainActivity
.
class MainActivity : AppCompatActivity() {
private val adapter = ListAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initRecyclerView()
}
private fun initRecyclerView() {
rvList.layoutManager = LinearLayoutManager(this)
//здесь мы задаем разделитель между айтемами, чтоб они не сливались друг с другом
val divider = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
rvList.addItemDecoration(divider)
rvList.adapter = adapter
stubData()
}
private fun stubData() {
val list = mutableListOf<listmarker>()
list.add(ItemTitle("title1", 4))
list.add(ItemGoogle("android", "product1", "17.0v", true))
list.add(ItemGoogle("no name", "product2", "3.1v", false))
list.add(ItemApple("macOs", "USA", 2005))
list.add(ItemApple("iOs", "China", 2007))
list.add(ItemTitle("title2", 2))
list.add(ItemGoogle("map", "product3", "23.0v", true))
list.add(ItemApple("car", "England", 2018))
list.add(ItemTitle("title3", 0))
//отправляем все данные в адаптер
adapter.swapData(list)
}
}
</listmarker>
RecyclerView
นี่คือที่ของเรา และ เริ่มListAdapter
ต้น นี่เป็นโค้ดสำเร็จรูปที่หลายๆ คนเคยเจอ ในวิธีการนี้stubData
ฉันกรอกรายการด้วยข้อมูล (ดังที่คุณเห็น ข้อมูลที่มีองค์ประกอบต่างกัน) และส่งรายการนี้ไปยังอะแดปเตอร์ ต่อไป เราเปิดตัวแอปพลิเคชันของเรา และเราควรเห็นสิ่งนี้บนหน้าจอของเราอย่างที่คุณเห็น มีองค์ประกอบที่แตกต่างกันในรายการเดียว ซึ่งเป็นเป้าหมายของเรา ป.ล. ลืมบอกไปว่าExtension
.. นี่คือสิ่งที่ดูเหมือน:
//это расширение для класса ViewGroup. Теперь мы можем по всему проекту использовать
//короткое inflate instead of длинного LayoutInflater.from(context).inflate
fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View =
LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
นี่ไม่ใช่คลาส แต่เป็นไฟล์ ซึ่งเป็นสาเหตุที่ไม่มีชื่ออยู่ข้างใน แต่ตอนนี้เราสามารถใช้มันในอะแดปเตอร์inflate
แทนโครงสร้างที่ยาวได้ ขออภัย สิ่งนี้ไม่มีให้สำหรับ Java ดังนั้นให้เขียนLayoutInflater.from(context).inflate
. นั่นคือทั้งหมดที่ ไว้คราวหน้า :) ลิงก์ไปยังGitHub
GO TO FULL VERSION