iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 20
0
Mobile Development

30天,從0開始用Kotlin寫APP系列 第 20 篇

Day 20 | Kotlin 實作 Material Card View 與動態更換圖片

  • 分享至 

  • xImage
  •  

串聯 GetPirate Fragment 的 View Model 和 View

昨天完成了 PirateListViewModel 並且和他的 View ,今天就要接續完成 GetPirate Fragment 的部份,大致上的行為都和昨天會是一樣的,因此直接上 Code

GetPirateViewModel

首先一樣先在 viewmodel 下建立 GetPirateViewModel.kt,而pirateListLiveData 中會帶一個參數 - pirateId

class GetPirateViewModel(application: Application) :
    AndroidViewModel(application) {
    private val pirateDetailRepository: PirateDetailRepository by lazy { PirateDetailRepository() }

    fun pirateListLiveData(pirateId: Int): LiveData<PirateInfo> =
        liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
            emitSource(
                pirateDetailRepository.fetchPirateInfo(
                    pirateId = pirateId,
                    onSuccess = { },
                    onError = { }
                ).asLiveData()
            )
        }
}

GetPirateFragment

class GetPirateFragment : Fragment() {
    private val TAG: String = tag.toString()
    private val getPirateViewModel: GetPirateViewModel by lazy {
        ViewModelProvider(this).get(GetPirateViewModel::class.java)
    }
    
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_get_pirate, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setGetPirateViewModel()
    }
    
        private fun setGetPirateViewModel() {
            getPirateViewModel.pirateListLiveData((1 .. 10).random()).observe(viewLifecycleOwner, Observer { pirate ->

        })
    }

    companion object {
        fun newInstance(): GetPirateFragment = GetPirateFragment()
    }
}

這邊需要注意到在 pirateListLiveData() 中我採用 (1..10).random() 隨機產生 id,再撈資料回來

那完成到這裡後,就可以進到生成 海盜卡 的步驟了!!!

Main Activity UI 設計

接下來 Main Activity 會長成這個模樣(很醜我知道...QQ

那在這個 View 中包含了兩個 Component

  • Card View :用來顯示海賊圖片和相關數值
  • Button :按下去可以獲取海賊資料然後更新 Card View 內容

因此我們就一步步把這個 UI 建起來,首先先從 Card View 開始

Material Card View

那 Material Design 有出 CardView 元件,元件名稱就叫作 MaterialCardView,那我回希望這個 CardView 是可以被重複在很多地方利用的,因此我不會把這個 CardView 建立在特定的 Fragment 中,而是拉出來成為令一個 xml 檔

1. 建立 card_view_pirate.xml

首先先到 res/layout 下面新建一個 Layout Resource File,為他命名為 card_view_pirate

2. 建立 Card View

那 Parent Tag 的部份就選擇 com.google.android.material.card.MaterialCardView,那可以看到裏面還有很多奇怪的 attribute,例如允許使用者點擊 CardView,那其實是我為了後面的鋪陳暫時先留下來的坑,暫時先保密(拖稿的好藉口~

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/card_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="@dimen/margin_half"
    app:strokeWidth="1dp"
    app:strokeColor="@color/primaryDarkColor"
    app:cardElevation="0dp"
    android:clickable="true"
    android:focusable="true"
    android:checkable="true"
    app:cardBackgroundColor="@color/primaryLightColor">

</com.google.android.material.card.MaterialCardView>

3. 產生 Card View UI 結構

首先先來觀察一下 CardView 中會需要哪些元件

可以看到其實需要的元件不多,就只有

  • Image View * 1
  • Text View * 4

因此我們可以依照這樣的方式把 UI 結構產生出來,並擺到 MaterialCardView 中

    <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">
        
        <ImageView
            android:id="@+id/image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/aoe_arbalester"/>

        <TextView
            android:id="@+id/pirate_name"
            android:text="Arbalester"
            android:textSize="@dimen/item_text_size"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margin_half"
            android:layout_marginStart="@dimen/margin_half_plus_eight"
            android:textColor="@color/secondaryLightColor"/>

        <TextView
            android:id="@+id/pirate_height"
            android:text="100 cm"
            android:textSize="@dimen/item_text_size"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margin_half"
            android:layout_marginStart="@dimen/margin_half_plus_eight"
            android:textColor="@color/secondaryLightColor"/>

        <TextView
            android:id="@+id/pirate_weight"
            android:text="100 kg"
            android:textSize="@dimen/item_text_size"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margin_half"
            android:layout_marginStart="@dimen/margin_half_plus_eight"
            android:textColor="@color/secondaryLightColor"/>

        <TextView
            android:id="@+id/pirate_attack"
            android:text="Attack"
            android:textSize="@dimen/item_text_size"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margin_half"
            android:layout_marginStart="@dimen/margin_half_plus_eight"
            android:layout_marginBottom="@dimen/margin_half"
            android:textColor="@color/secondaryLightColor"/>
    </LinearLayout>

另外提一下,這邊的 ImageView 我有先塞一張圖片當作預設,所以如果照做會找不到圖片路徑是正常的

那我的圖片是在 AOE Wiki 上面抓下來的,抓了 10 張圖來當作海賊的圖片

MainActivity

在 activity_main.xml 中就要把剛剛完成的 CardView 加進來,並且要創建一個按鈕讓使用者按下去後更新 CardView 內容,因此佈局會長這樣

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/primaryColor"
    tools:context=".ui.fragment.GetPirateFragment">

    <include
        android:id="@+id/pirate_card"
        layout="@layout/card_view_pirate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:layout_marginBottom="@dimen/margin_base"
        android:backgroundTint="@color/primaryLightColor"
        android:text="Get Pirate"
        android:textColor="@color/secondaryColor"
        android:textSize="@dimen/sub_title_text_size" />


用 include 把元件加進來,就可以達到重複使用的目的

Pirate Image

那海賊的圖片並不會跟著 Api 資料回來,因此剛剛有提到我有先抓 10 張 AOE 的角色圖當作海賊的圖片

因此這邊要做到的是,當 Api 回傳 id 和 data 時,我需要呈獻相對應的圖片給使用者,因此這邊可以採用 Enum Class 達到這個目的

1. 將圖片加入 drawable 中

就把圖片加到 res/drawable 中,記得先 Rebuild Project 不然可能會找不到圖

2. 建立 PirateImage Enum Class

到 data 下建立一個 enum 的資料夾,之後這個資料夾都會放 enum class,那我們先建立一個 PirateImage.kt 的 enum class

3. Pirate Id 映射到 Pirate Image

那我們接下來就是要把回傳的 pirateId 映設到對應的 Image 上,這邊做個分段的講解

  1. PirateImage 有兩個屬性是 pirateId 和 pirateImage
  2. pirateId 對應到 ID
  3. pirateImage 對應到 drawable 位置
  4. 建立一個 static function 叫作 getPirateImage() ,當有人呼叫他和傳 pirateId 進來時,就會用 when-else 映射到相對應的 enum 並且回傳
enum class PirateImage(val pirateId: Int, val pirateImage: Int) {
    BULBASAUR(1, R.drawable.aoe_imperialskirmisher),
    IVYSAUR(2, R.drawable.aoe_petard),
    VENUSAUR(3, R.drawable.aoe_battle_elephant),
    CHARMANDER(4, R.drawable.aoe_arbalester),
    CHARMELEON(5, R.drawable.aoe_legionary),
    CHARIZARD(6, R.drawable.aoe_bombard_cannon),
    TORRENT(7, R.drawable.aoe_tarkan),
    WARTORTLE(8, R.drawable.aoe_camelrider),
    BLASTOISE(9, R.drawable.aoe_hand_cannoneer),
    CATERPIE(10, R.drawable.aoe_monk);

    companion object {
        fun getPirateImage(pirateId: Int): PirateImage {
            return when (pirateId) {
                1 -> BULBASAUR
                2 -> IVYSAUR
                3 -> VENUSAUR
                4 -> CHARMANDER
                5 -> CHARMELEON
                6 -> CHARIZARD
                7 -> TORRENT
                8 -> WARTORTLE
                9 -> BLASTOISE
                10 -> CATERPIE
                else -> BULBASAUR
            }
        }
    }
}

透過這樣的方式我們就完成了 1 to 1 mapping 了,接下來就只要在 pirateListLiveData() 時,更新 CardView 內容

更新 pirateInfoLiveData() 邏輯

那昨天有證明 Observer Pattern 會在 被觀察者 資料更新的時候打 Event 通知 觀察者,因此這邊我們就要去更新 pirateInfoLiveData() 裡面的邏輯,讓他可以達到我們希望的行為

1. 更新 CardView 內容

首先是打資料回來的時候我們要改變 CardView 的 UI 內容,那可以注意到

  • 在拿到 pirateId 的時候,會先去剛剛完成的 enum class 中將圖片的 drawable id 取回來 PirateImage.getPirateImage(pirate.id).pirateImage,在透過 ContextCompat.getDrawable() 取得圖片內容,最後在 set 到 CardView 的 Image 中
class GetPirateFragment : Fragment() {
    ...

    private fun setGetPirateViewModel() {
        getPirateViewModel.pirateInfoLiveData((1 .. 10).random()).observe(viewLifecycleOwner, Observer { pirate ->
            val pirateCard = pirate_card
            pirateCard.image.setImageDrawable(context?.let { ContextCompat.getDrawable(it, PirateImage.getPirateImage(pirate.id).pirateImage) })
            pirateCard.pirate_name.text = pirate.name
            pirateCard.pirate_height.text = pirate.getHeightString()
            pirateCard.pirate_weight.text = pirate.getWeightString()
            pirateCard.pirate_attack.text = pirate.getAttackString()
        })
    }

    ...
    
}

2. 用 Button 做到隨機取得資料和更新 UI

那上個步驟完成後接下來就很單純了,就只要幫按鈕設定 setOnClickListener 事件,當使用者按下去的時候呼叫 setGetPirateViewModel() ,接下來就交給 setGetPirateViewModel() 去更新 UI 即可

class GetPirateFragment : Fragment() {
    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    
        ...
        
        button.setOnClickListener {
            setGetPirateViewModel()
        }
    }

    ...
    
}

Demo

image alt

如果內容有任何問題或錯誤,歡迎提出與指教

Reference


上一篇
Day 19 | Kotlin 完成基礎 MVVM 架構
下一篇
Day 21 | Android Animator - 翻開一張海賊卡
系列文
30天,從0開始用Kotlin寫APP 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言