# Android筆記--RecyclerView(循環視圖)(進階篇) --- - 接續 [Android筆記–RecyclerView(循環視圖)(基礎篇)](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/Android_RecyclerView) 再來是關於RecyclerView在各種地方的實務。 ---- ▼1.簡單的加上按鈕 --- >[!Note]參考:[[Day 9] Android in Kotlin: 簡單的 Recycler View](https://ithelp.ithome.com.tw/articles/10241713?sc=rss.iron) **效果**: ![image](https://hackmd.io/_uploads/HyLIrzLg0.png) :::success :::spoiler ※===詳細製作法===※ - 1.首先我們到Holder綁定的Layout增加按鈕 ![image](https://hackmd.io/_uploads/B1iL-MUlA.png) - 2.在Holder中將新增的Button綁定 - 3.改採用apply的標準函數,然後給按鈕設置監聽器(要注意==context參數要用`holder.itemView.context`來取得==) >[!Note]補充:[apply是甚麼?](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/kotlin_ScopeFunction) ![image](https://hackmd.io/_uploads/H1QBXMIe0.png) ::: --- ▼2.使用addItemDecoration()添加裝飾 --- RecyclerView 中的 addItemDecoration 用於在 RecyclerView 的項目之間添加裝飾,例如分割線、邊框、陰影等。要使用 addItemDecoration 方法,需要建立一個 ItemDecoration 類別。 :::info :::spoiler ```kotlin= //基本使用法: 呼叫RecyclerView之後再呼叫addItemDecoration(),通常配合apply使用。 bindingMainbinding.rv1ActivityMain.apply { addItemDecoration( ...(內部填入ItemDecoration物件) ) } //範例: bindingMainbinding.rv1ActivityMain.apply { layoutManager = LinearLayoutManager(this@MainActivity) setHasFixedSize(true) addItemDecoration(DividerItemDecoration(this@MainActivity, DividerItemDecoration.HORIZONTAL)) adapter = MyAdapter(MyList()) } ``` ### **添加分隔橫線**: "DividerItemDecoration.VERTICAL" ![image](https://hackmd.io/_uploads/ByTFAQOgR.png) ```kotlin= addItemDecoration(DividerItemDecoration(this@MainActivity, DividerItemDecoration.VERTICAL)) ``` ### **添加分隔直線**: "DividerItemDecoration.HORIZONTAL" ![image](https://hackmd.io/_uploads/SJMaCXOeA.png) ```kotlin= addItemDecoration(DividerItemDecoration(this@MainActivity, DividerItemDecoration.HORIZONTAL)) ``` - ::: --- ▼3.添加波紋效果 --- 效果:![1713018718806](https://hackmd.io/_uploads/rkIqwGdgR.gif) :::success :::spoiler ※===詳細製作法===※ ### 方法1: ![image](https://hackmd.io/_uploads/r1lwddPlC.png) ```kotlin= android:background="?attr/selectableItemBackground" ``` ### 方法2: ![image](https://hackmd.io/_uploads/HJqYw_weC.png) ```kotlin= android:foreground="?attr/selectableItemBackground" ``` - ==使用foreground和background的差別在於使用foreground可以變更該View的背景顏色== (因為foreground不會占用background屬性),而background不行。 - ==必須加上`android:clickable="true"`和`android:focusable="true"`屬性,否則波紋效果的設定有時會不起作用。== - 有`selectableItemBackground`(有邊界)和`selectableItemBackgroundBorderless`(無邊界)兩種版本 ::: :::success :::spoiler ※===參考===※ - ==★==[Android 波紋效果 Ripple Effect](https://medium.com/@waynechen323/android-%E6%B3%A2%E7%B4%8B%E6%95%88%E6%9E%9C-ripple-effect-c025940bf14c) - [android使用selectableItemBackground的一些坑](https://haldir65.github.io/2016/09/23/2016-09-23-selectableItemBackground-foreground/) - [android 按钮水波纹效果【背景色】](https://blog.csdn.net/fengyeNom1/article/details/105950836) - [[使用 ?attr/selectableItemBackground 作为背景时如何修改波纹颜色?]](https://segmentfault.com/q/1010000043160224) ::: --- ▼4.卡片式佈局 --- 將RecyclerView變成卡片式的,可以提升質感。 ![image](https://hackmd.io/_uploads/B1mm4adxR.png) :::info :::spoiler ※===詳細製作法===※ - 基本上就只是對View右鍵Convert View -> 轉換成CardView->再重放一個ConstraintLayout - ==可以在CardView的地方加上`app:cardCornerRadius="xxdp"`,來調整弧度,上面的示範圖為20dp== ![image](https://hackmd.io/_uploads/SJCVzTuxA.png) ![image](https://hackmd.io/_uploads/BkpLMaOeA.png) ![image](https://hackmd.io/_uploads/rJXofpdlR.png) ::: --- ▼5.載入圖片 --- ![image](https://hackmd.io/_uploads/HyEVauogC.png) :::success :::spoiler ※===詳細製作法===※ ### 1.將圖片放入resource裡 ![image](https://hackmd.io/_uploads/rJDdadixC.png) ### 2.將圖片位置的參照放到我們製作的串列裡,方便調用 ![image](https://hackmd.io/_uploads/HJ4np_sg0.png) ### 3.在ViewHolder中加上綁定、在onViewBinding中使用`setImageResource()`設置圖片,`setImageResource`內要填入位置參照。 ![image](https://hackmd.io/_uploads/r1uyTuil0.png) ::: ▼6.使用Glide載入圖片 --- >[!Note]參考:[Android筆記–Glide](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/android_Glide) 將Glide需求的參數填入即可,`with(holder.itemView.context)`,`load(圖片來源)`,`into(要放置的地方)`,onBindViewHolder()外的其他部分直接使用["▼5.載入圖片"](https://hackmd.io/ZxHCAtWdSYS_ZeFBMLF2iA?both#%E2%96%BC5%E8%BC%89%E5%85%A5%E5%9C%96%E7%89%87)的就可以了 ![image](https://hackmd.io/_uploads/Hyl5ri3lA.png) ▼7.強制禁止itemView隨大小自動調整 --- 只要在Adapter中的`onBindViewHolder`加上 ```kotlin= holder.myItemView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) ``` 就可以了 ![image](https://hackmd.io/_uploads/rJCEOiqNC.png) 使用前: ![Screenshot_1](https://hackmd.io/_uploads/BySSFo540.png) 使用後: ![image](https://hackmd.io/_uploads/rJYvYs540.png) ▼8.根據不同的狀況採用不同的ItemView --- ![1737284754681](https://hackmd.io/_uploads/SJkan85Pyx.gif =40%x) :::spoiler 詳細製作法 當 RecyclerView 的列表中需要顯示不同的佈局時,我們可以使用RecyclerView中的`getItemViewType()`來達到此效果(或是也可以直接手動改變) - `getItemViewType()`: 使用傳入的 position,返回一個整數類型的值 (viewType),用來區分不同的項目類型,回傳的值會在Adapter的[onCreateViewHolder()](https://hackmd.io/ErN8nQN6SsaSHYh7LejGHg?view=&stext=6427%3A7%3A0%3A1737285342%3AzTiINF)中被使用,也就是說,==我們必須在`getItemViewType()`實現一個判斷邏輯來判斷資料集的第position筆資料是屬於哪一類型==。 <本篇使用的範例中有使用DataBinding> :::spoiler 步驟1.製作多個ItemView ![image](https://hackmd.io/_uploads/SknNbtqDye.png) ::: :::spoiler 步驟2.宣告要用在getItemViewType()中的類型的代號 ```kotlin= //宣告要用在getItemViewType()中的類型的代號 companion object{ const val type_1 = 1 const val type_2 = 2 } ``` ::: :::spoiler 步驟3.製作對應的ViewHolder ```kotlin= //建構ItemView裡元件的綁定以及運作邏輯,有幾個類型就要幾個 inner class TaskRecyclerViewHolder_1(private val rvItem1Binding: RvItem1Binding): RecyclerView.ViewHolder(rvItem1Binding.root){ fun bind(task: TaskDataClass){ rvItem1Binding.tasks = task rvItem1Binding.checkBox1.setOnCheckedChangeListener(null) rvItem1Binding.checkBox1.isChecked = task.taskDone rvItem1Binding.checkBox1.setOnCheckedChangeListener { buttonView, isChecked -> if(task != null){ viewModel.updateTaskDone(task.taskID, isChecked) } } rvItem1Binding.executePendingBindings() } } inner class TaskRecyclerViewHolder_2(private val rvItem2Binding: RvItem2Binding): RecyclerView.ViewHolder(rvItem2Binding.root){ fun bind(task: TaskDataClass){ rvItem2Binding.tasks = task rvItem2Binding.checkBox2.setOnCheckedChangeListener(null) rvItem2Binding.checkBox2.isChecked = task.taskDone rvItem2Binding.checkBox2.setOnCheckedChangeListener { buttonView, isChecked -> if(task != null){ viewModel.updateTaskDone(task.taskID, isChecked) } } } } ``` ::: :::spoiler 步驟4.覆寫getItemViewType()方法,並在其中實現分類邏輯 ```kotlin= //在這裡實現分類邏輯,當taskDone為true(被勾選時),就會被分類到type_1 override fun getItemViewType(position: Int): Int { return when{ currentTaskList[position].taskDone == true -> type_1 else -> type_2 } } ``` ::: :::spoiler 步驟5.根據ViewType的種類數量,充填對應的ItemViewBinding ```kotlin= //getItemViewType()中回傳的類型代號(viewType)會傳入到onCreateViewHolder(),用來判斷需要創建哪 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { // 根據傳入的 viewType,選擇對應的佈局並初始化對應的 ViewHolder return when(viewType){ // 為 type_1 的項目充填對應的 RvItem1Binding,並回傳 TaskRecyclerViewHolder_1 type_1 -> { val rvItem1Binding = RvItem1Binding.inflate(LayoutInflater.from(parent.context), parent, false) TaskRecyclerViewHolder_1(rvItem1Binding) } // 為其他類型的項目充填對應的 RvItem2Binding,並回傳 TaskRecyclerViewHolder_2 else -> { val rvItem2Binding = RvItem2Binding.inflate(LayoutInflater.from(parent.context), parent, false) TaskRecyclerViewHolder_2(rvItem2Binding) } } } ``` ::: :::spoiler 步驟6.根據使用的Holder呼叫我們設計的Bind()方法 ```kotlin= override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when(holder){ is TaskRecyclerViewHolder_1 -> { holder.bind_1(currentTaskList[position]) } is TaskRecyclerViewHolder_2 -> { holder.bind_2(currentTaskList[position]) } } } ``` >[!Caution]GitHub專案: [Task_12](https://github.com/PudCheetah/Task_12) ::: ▼9.使用DataBinding讓資料自動綁定 --- >[!Note]參考:[Android筆記–DataBinding](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/android_dataBinding) 如果要在RecyclerView中使用DataBinding,在佈局文件的`variable`中的`type`,要填入`DataClass` :::spoiler 範例 ```kotlin= <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="tasks" type="com.example.task_12.TaskDataClass" /> </data> <com.google.android.material.card.MaterialCardView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#2196F3" app:cardCornerRadius="4dp" app:cardElevation="8dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#5103A9F4" android:orientation="vertical"> <TextView android:id="@+id/task_name_1" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{tasks.taskName}" /> <CheckBox android:id="@+id/checkBox_1" android:layout_width="match_parent" android:layout_height="wrap_content" android:checked="@{tasks.taskDone}" android:text="CheckBox" /> <TextView android:id="@+id/textView2" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Type_1" /> </LinearLayout> </com.google.android.material.card.MaterialCardView> </layout> ``` ```kotlin= //DataClass data class TaskDataClass( val taskID: Long = 0, var taskName: String = "", var taskDone: Boolean = false ) ``` ```kotlin= //RecyclerView class TaskRecyclerViewAdapter(private val viewModel: TaskViewModel): ListAdapter<TaskDataClass, RecyclerView.ViewHolder>(TaskDiffCallback()){ private var currentTaskList = listOf<TaskDataClass>() ... inner class TaskRecyclerViewHolder_1(private val rvItem1Binding: RvItem1Binding): RecyclerView.ViewHolder(rvItem1Binding.root){ fun bind_1(task: TaskDataClass){ //在這裡將傳入的task和xml中的tasks連結 rvItem1Binding.tasks = task rvItem1Binding.checkBox1.setOnCheckedChangeListener(null) rvItem1Binding.checkBox1.isChecked = task.taskDone rvItem1Binding.checkBox1.setOnCheckedChangeListener { buttonView, isChecked -> if(task != null){ viewModel.updateTaskDone(task.taskID, isChecked) } } rvItem1Binding.executePendingBindings() } } ... override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val task = getItem(position) when(holder){ is TaskRecyclerViewHolder_1 -> { holder.bind_1(currentTaskList[position]) } ... } } ``` ::: >[!Caution]GitHub專案: [Task_12](https://github.com/PudCheetah/Task_12) ▼10.使用DiffUtil高效更新 RecyclerView 的內容 --- DiffUtil 是 Android 提供的一個實用工具類別,用於高效更新 RecyclerView 的內容。它能夠比較兩個列表,計算出新增、刪除、修改等差異,並將這些變化高效地應用到 RecyclerView 中,而不需要完全重新刷新整個列表。 :::spoiler 詳細作法 - `areItemsTheSame()`:檢查收到的oldItem和newItem是否參考同一個項目(通常根據 ID) - `areContentsTheSame()`:檢查兩個物件有沒有相同的內容,當`areItemsTheSame()`是true時,才會檢查`areContentsTheSame()` - `ListAdapter`: 用於簡化和優化 RecyclerView 的數據處理過程。內部整合了 DiffUtil 的功能 - `getItem(position)`: 是一個 ListAdapter 提供的內建方法,用於從內部管理的數據集中,透過索引值獲取對應的資料項目。 - `submitList()`: 是 ListAdapter 提供的一個方法,用於將新的資料列表提交給 ListAdapter,並觸發 RecyclerView 的更新。 :::spoiler <u>步驟1.設計一個DataClass</u> ```kotlin= data class TaskDataClass( val taskID: Long = 0, var taskName: String = "", var taskDone: Boolean = false ) ``` ::: :::spoiler <u>步驟2.設計一個`DiffCallback`Class</u> 要繼承`DiffUtil.ItemCallback<xxxDataClass>()`,`<>`內填入要用的dataClass,並複寫`areItemsTheSame()`、`areContentsTheSame()`。</br>`areItemsTheSame()`通常是檢查ID是否相同,`areContentsTheSame()`則是檢查內容是否相同。[參考](https://ithelp.ithome.com.tw/articles/10261005#:~:text=ItemCallback%E7%9A%84class-,DiffUtil%E5%AF%A6%E4%BD%9C,-%E6%96%B0%E5%A2%9EDiffCallbackclass) ```kotlin= class TaskDiffCallback: DiffUtil.ItemCallback<TaskDataClass>() { override fun areItemsTheSame(oldItem: TaskDataClass, newItem: TaskDataClass): Boolean { return oldItem.taskID == newItem.taskID } override fun areContentsTheSame(oldItem: TaskDataClass, newItem: TaskDataClass): Boolean { return oldItem == newItem } } ``` ::: :::spoiler <u>步驟3.將原本的RecyclerViewAdapter改成ListAdapter,並變更內容</u> RecyclerViewAdapter中有四處需要變更 - 1.ListAdapter的泛型標記中填入要使用的DataClass和ViewHolder類型。[參考](https://ithelp.ithome.com.tw/articles/10261005#:~:text=DataClass%E4%BD%9C%E7%AF%84%E4%BE%8B)</br>例如: </br> - 原本: `RecyclerView.Adapter<RecyclerView.ViewHolder>()` - 改成:`ListAdapter<TaskDataClass, RecyclerView.ViewHolder>(TaskDiffCallback())` - 2.將原本使用資料集`currentTaskList = listOf<TaskDataClass>()`的地方改成直接使用`getItem(position)`。[參考](https://ithelp.ithome.com.tw/articles/10261005#:~:text=%E6%98%AF%E5%90%A6%E5%AE%8C%E5%85%A8%E7%9B%B8%E7%AD%89%E3%80%82-,%E6%9B%B4%E6%96%B0ListAdapter%E4%B8%AD%E7%9A%84data,-%E4%BB%A5%E5%BE%80%E5%8F%96%E5%80%BC) - 例如,原本是`holder.bind_1(currentTaskList[position])`改成`holder.bind_1(getItem(position))` - 3.將`getItemCount()`註解掉,ListAdapter 已內建實作</u> - 4.將原本用來更新RecyclerView的方法也註解掉,ListAdapter已內建能夠替代的`submitList()` ```kotlin= //原本用來更新RecyclerView的方法,將這段註解掉 fun submitNewTask(newTaskList: List<TaskDataClass>){ currentTaskList = newTaskList notifyDataSetChanged() } ``` ![image](https://hackmd.io/_uploads/Bk9PNkxuJg.png) ![image](https://hackmd.io/_uploads/Sy7GHyldJl.png) ![image](https://hackmd.io/_uploads/SyqaB1gdyg.png) ::: :::spoiler <u>步驟4.更改在Activity/Fragment中,用來更新RecyclerView的方法</u> 原本我們呼叫的是RecyclerViewAdapter中我們自己設計的`submitNewTask()`,現在改成使用ListAdapter中內建的`submitList()`。[參考](https://ithelp.ithome.com.tw/articles/10261005#:~:text=name%20%3D%20getItem(position)%0A%20%20%20%20%7D-,Data%E5%9C%A8ListAdapter%E7%9A%84%E4%BD%BF%E7%94%A8,-%E5%9C%A8ListAdapter%E4%B8%AD) ```kotlin= //Fragment中原本的 viewModel.tasks.observe(viewLifecycleOwner){ tasks -> recyclerViewadapter.submitNewTask(tasks) } //改用ListAdapter中內建的`submitList()` viewModel.tasks.observe(viewLifecycleOwner){ tasks -> recyclerViewadapter.submitList(tasks) } ``` ::: >[!Note]參考: [[Day4] Android - Kotlin筆記:RecyclerView Adapter - ListAdapter + DiffUtil](https://ithelp.ithome.com.tw/articles/10261005) ::: --- Q&A.<span id="portalA">為甚麼要在`onBindViewHolder`中加入`holder is xxxViewholder`的檢查?</span> --- Ans: 這是為了方便未來如果要同時使用不同的`itemView`時,能夠更方便的擴充。[參考: ▼8.根據不同的狀況採用不同的ItemView](https://hackmd.io/ZxHCAtWdSYS_ZeFBMLF2iA?view=&stext=4645%3A23%3A0%3A1737197109%3ARwj1yj)。 如果確定只使用一個itemView的話,可以直接將Adapter中所有用到`RecyclerView.Holder`的地方都改成要使用的類別名 :::spoiler 圖解 ![image](https://hackmd.io/_uploads/Sk96w-KDyl.png) ::: --- 上一篇:[Android筆記–RecyclerView(循環視圖)(基礎篇)](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/Android_RecyclerView) --- 參考資料: --- - [AndroidDeveloper--使用資訊卡顯示圖像清單 ](https://developer.android.com/codelabs/basic-android-kotlin-training-display-list-cards?hl=zh-tw#0) - [AndroidDeveloper--載入並顯示網際網路上的圖片]([https://](https://developer.android.com/codelabs/basic-android-kotlin-training-internet-images?hl=zh-tw#0)) - [[Google Course] Android Basics in Kotlin(第7篇) — Display the image and text with MaterialCardView](https://happyphoebe40090.medium.com/google-course-android-basics-in-kotlin-%E7%AC%AC7%E7%AF%87-display-the-image-and-text-with-materialcardview-317496863e1e) - [AndroidDeveloper-- 自訂動態清單](https://developer.android.com/develop/ui/views/layout/recyclerview-custom?hl=zh-tw) - [[Android 十全大補] RecyclerView as a Pro](https://ithelp.ithome.com.tw/articles/10220712) - [[Day 9] Android in Kotlin: 簡單的 Recycler View](https://ithelp.ithome.com.tw/articles/10241713?sc=rss.iron) - [Android Kotlin 實作 Day 6 : ImageList(RecyclerView + LayoutInflater)](https://ithelp.ithome.com.tw/articles/10203735) - :::spoiler Android Kotlin開發 -小嫩雞的30篇精選筆記系列 - [Android x Kotlin : RecyclerView(二)-項目的拖曳換位及左右滑動刪除](https://ithelp.ithome.com.tw/articles/10239304) - [Android x Kotlin : Recyclerview(三)-能上下滑又能左右滑的巢狀玩法](https://ithelp.ithome.com.tw/articles/10240046) - :::spoiler HTK線上教室--RecyclerView實務系列 - [Day 13:RecyclerView 基本資料列表顯示](https://ithelp.ithome.com.tw/articles/10263176?sc=rss.iron) - [Day 14:RecyclerView 進階項目佈局](https://ithelp.ithome.com.tw/articles/10263656) - [Day 15:RecyclerView 卡片式項目佈局](https://ithelp.ithome.com.tw/articles/10264141) - [Day 16:RecyclerView 跳頁&資料傳遞(1)](https://ithelp.ithome.com.tw/articles/10264909) - [Day 17:RecyclerView 跳頁&資料傳遞(2)](https://ithelp.ithome.com.tw/articles/10265804) - :::spoiler 碼農日常-進階RecyclerView 系列<JAVA> - [碼農日常-『Android studio』基本RecyclerView用法](https://thumbb13555.pixnet.net/blog/post/311803031) - [碼農日常-『Android studio』基本RecyclerView 用法-2 基本版下拉更新以及點擊事件](https://thumbb13555.pixnet.net/blog/post/312844960-android-studio-%e4%b9%8b%e5%9f%ba%e6%9c%acre) - [碼農日常-『Android studio』基本RecyclerView 用法-3 RecyclerView上下滑動排序與側滑刪除(RecyclerView Swipe)](https://thumbb13555.pixnet.net/blog/post/316420566-recyclerview-swipe) - [碼農日常-『Android studio』基本RecyclerView 用法-4 左滑顯示Button Menu](https://thumbb13555.pixnet.net/blog/post/322876604-swipereveallayout) - [碼農日常-『Android studio』進階RecyclerView 用法-5 RecyclerView item混合介面](https://thumbb13555.pixnet.net/blog/post/324929799-recyclerview_multipl) :::