iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 28
1

  今天的主題是點擊項目後的動作,首先我們要知道點到的是清單中的哪個項目,因此要擴充一下 groupRecycleAdapter

  首先在類別建構子加上第三個參數,命名為 itemClickEvent,型態是接收 FoodGrouplambda 表示式 (新上車的讀者可以參考 Day 11 的主題),將 val itemClickEvent: (FoodGroup) -> Unit,加到先前的程式碼之中,類別上方其他部分原封不動。

class GroupRecycleAdapter(val context: Context, val groups: List<FoodGroup>, val itemClickEvent: (FoodGroup) -> Unit)
   : RecyclerView.Adapter<GroupRecycleAdapter.Holder>() { ... }

  接續到內部類別 HolderbindGroup() 方法,加入熟悉的 setOnClickListener { },大括號中使用上面的 itemClickEvent 物件並傳入 group 項目。

itemView.setOnClickListener { itemClickEvent(group) }

  這裡提供截圖讓讀者方便確認有調整到的部分:

https://ithelp.ithome.com.tw/upload/images/20181111/20111944t3DWgfUtZd.png


  下一步打開 MainActivity,在 generateListView 中,將原本的 adapter = GroupRecycleAdapter... 後方加上 { } ,使用 lambda。可在此處加入 Log.v 執行模擬機查看效果,當點擊清單項目時,就會觸發印出 Log,並且程式都能知道是哪一個項目被點擊。

adapter = GroupRecycleAdapter(this, DataService.groups) {
   Log.v("Test", it.name)
}

  是時候移動到第二個頁面了,讓我們新增一個 ItemsActivity,接著要進行頁面的切換與傳值,切換回 MainActivity 剛剛設計的 lambda 中,移除 Log.v,並加入先前章節常用的換頁語法:

var itemIntent = Intent(this, ItemsActivity::class.java)
startActivity(itemIntent)

  現在執行程式就能成功跳至第二頁面,緊接著進行傳值前的準備工作,到 Models 資料夾內建立一個 FoodItem 的資料類別,主要存放第二層的資料,同時也需要增加實作 Parcelable

https://ithelp.ithome.com.tw/upload/images/20181111/201119448KQbVKuFZ6.png

  接著到 Models > FoodGroup,先加入第三參數 list 用來存放 FoodItem 型態資料,下一步一樣增加實作 Parcelable 介面:

data class FoodGroup(val name: String, val image: String, val list: List<FoodItem>?): Parcelable

  檢查一下建構子writeToParcel 這兩段程式是否有如下列完整產生,特別是各自的第三行。

constructor(parcel: Parcel) : this(
   parcel.readString(),
   parcel.readString(),
   parcel.createTypedArrayList(FoodItem)  // 檢查
)

override fun writeToParcel(parcel: Parcel, flags: Int) {
   parcel.writeString(name)
   parcel.writeString(image)
   parcel.writeTypedList(list)  // 檢查
}

  檢查沒問題後接著切換至 Services > DataService,在原本的 FoodGroup 加入第三個參數,這裡作者拿水果當作示範,其餘的指定為空值 null

var fruits = listOf(
   FoodItem("蘋果", "img_apple"),
   FoodItem("香蕉", "img_banana"),
   FoodItem("鳳梨", "img_pineapple"),
   FoodItem("芭樂", "img_guava"),
   FoodItem("芒果", "img_mango"),
   FoodItem("蓮霧", "img_wax_apple")
)

val groups = listOf(
   FoodGroup("五穀", "img_various_grains", null),
   FoodGroup("蔬菜", "img_vegetables", null),
   FoodGroup("水果", "img_fruits", fruits),
   FoodGroup("奶類", "img_dairy", null),
   FoodGroup("肉類", "img_meats", null)
)

  回到 MainActivity,現在能加入 putExtra 傳送選到的 group 到其它頁面。

fun generateListView() {
   adapter = GroupRecycleAdapter(this, DataService.groups) {
       var itemIntent = Intent(this, ItemsActivity::class.java)
       itemIntent.putExtra(EXTRA_GROUP, it)
       startActivity(itemIntent)
   }
   groupRecyclerView.adapter = adapter
   groupRecyclerView.layoutManager = LinearLayoutManager(this)
}

  第二個頁面用 getParcelableExtra 將物件取出,可以利用 Log.vtoString() 觀察值是否有成功傳遞。

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_item)
   val group = intent.getParcelableExtra<FoodGroup>(EXTRA_GROUP)
   Log.v("LogInfo", group.toString())
}

  以上範例是為了結合示範使用 Parcelable 中的 list 型態傳遞,可以跟 Day 24 的設計方法比較,一般這種資料讀取情境應該會自 MainActivity 傳遞點選的群組Id,再用此單一識別碼於第二頁面,透過 API Service 從資料庫讀取項目出來,因為資料量龐大的情境下,不會在一開始就將所有階層都讀取出來占用資源。


  下一步:於 res > layout 新增 item_list_item.xml,並加入兩個元件:ImageViewTextViewImageView 命名為 itemImagelayout 皆設定為 match,四面邊界設為 8dp,接著設定 ratio1:1,注意 ratio 必須多按幾下左上角的三角形,切換到如同下圖的模式:

https://ithelp.ithome.com.tw/upload/images/20181111/20111944Bvg7sfBlqH.png

  XML 的部分提供參照:

<ImageView
       app:srcCompat="@drawable/img_various_grains"
       android:id="@+id/itemImage"
       app:layout_constraintTop_toTopOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintBottom_toBottomOf="parent"
       android:scaleType="centerCrop"
       android:layout_width="0dp"
       android:layout_height="0dp"
       android:layout_marginTop="8dp"
       android:layout_marginStart="8dp"
       android:layout_marginEnd="8dp"
       android:layout_marginBottom="8dp"
       app:layout_constraintVertical_bias="1.0"
       app:layout_constraintDimensionRatio="w,1:1"/>

  TexeView 設定四邊與 ImageView 的四邊約束,並調整垂直到底部 90% 的部分。

https://ithelp.ithome.com.tw/upload/images/20181111/20111944TSGjWatYkW.png


  接著來到 Adapters 的部分,可用滑鼠左鍵點一下 group_list_item.xml,再按鍵盤 Ctrl + CCtrl + V 複製檔案,命名為 ItemRecycleAdapter

https://ithelp.ithome.com.tw/upload/images/20181111/20111944pepf3Sc5Fp.png
https://ithelp.ithome.com.tw/upload/images/20181111/20111944LF90ozbW97.png

  下一步將所有 FoodGroup 字眼換成 FoodItemR.layout 換成新建立的樣式表,R.id 修改讀取item 的目標元件,並且將用到 group 關鍵字的地方也一併更改,再將不需要的部分刪除,如:接收 List 參數和 onClick,以下為修改後的程式碼。

class ItemRecycleAdapter(val context: Context, val items: List<FoodItem>)
   : RecyclerView.Adapter<ItemRecycleAdapter.Holder>() {
   override fun onCreateViewHolder(parent: ViewGroup, position: Int): Holder {
       val view = LayoutInflater
           .from(context)
           .inflate(R.layout.item_list_item, parent, false)

       return Holder(view)
   }

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

   override fun onBindViewHolder(holder: Holder, position: Int) {
       holder.bindGroup(items[position], context)
   }

   inner class Holder(itemView: View): RecyclerView.ViewHolder(itemView) {
       val itemImage: ImageView = itemView.findViewById(R.id.itemImage)
       val itemName: TextView = itemView.findViewById(R.id.itemName)

       fun bindGroup(item: FoodItem, context: Context) {
           val resourceId = context.resources.getIdentifier(item.image, "drawable", context.packageName)
           itemImage.setImageResource(resourceId)
           itemName.text = item.name
       }
   }
}

  最後步驟是到 ItemsActivity,將 lateinit var adapter 參數於上方宣告。onCreate() 中加入 titleText.text = group.name (Line: 26),讓第二頁標題顯示首頁選擇的群組名稱,產生資料清單的部分多傳入一個清單,這個清單會是 List<FoodItem> 型態,可以透過 Alt + Enter 快速產生函式。

https://ithelp.ithome.com.tw/upload/images/20181111/201119443CFmZRYrBk.png

  就快完成了,在 generateListView() 中,先判斷一下 list 有值時才執行 Adapter 程式段,再把 list 傳入 ItemRecycleAdapter,最後我們將 layoutManager 更換使用 GridLayout,第二個參數可以指定項目要幾個為一排,這裡設定為 2

class ItemsActivity : AppCompatActivity() {

   lateinit var adapter: ItemRecycleAdapter

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_item)
       val group = intent.getParcelableExtra<FoodGroup>(EXTRA_GROUP)
       titleText.text = group.name
       // 產生資料清單
       generateListView(group.list)
   }

   private fun generateListView(list: List<FoodItem>?) {
       if (list != null) {
           adapter = ItemRecycleAdapter(this, list)
           groupRecyclerView.adapter = adapter
           groupRecyclerView.layoutManager = GridLayoutManager(this, 2)
       }
   }
}

  執行成果:

https://ithelp.ithome.com.tw/upload/images/20181111/20111944loTTjg8sPV.png

  最後留給讀者練習:如何改善 ItemName 顯示對比?如何調整顯示項目的排列形狀?讀者可以自行探索一下主版頁面與清單頁面之間的約束影響。RecyclerView 將在此告一段落,接下來會介紹一些獨立的小主題,是作者在之前主題想延伸介紹但與主軸沒有相關,因此就留在後面介紹,我們明天見!


資料參考

Kotlin for Android: Beginner to Advanced | Udemy
https://www.udemy.com/devslopes-android-kotlin/
Create a List with RecyclerView-Android Developers
https://developer.android.com/guide/topics/ui/layout/recyclerview


上一篇
Day 27. Android RecyclerView - 1/2
下一篇
Day 29. Kotlin for Android Configuration | Dialog
系列文
Kotlin for Android30

尚未有邦友留言

立即登入留言