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 回傳 iddata 時,我需要呈獻相對應的圖片給使用者,因此這邊可以採用 Enum Class 達到這個目的

1. 將圖片加入 drawable 中

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

2. 建立 PirateImage Enum Class

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

3. Pirate Id 映射到 Pirate Image

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

  1. PirateImage 有兩個屬性是 pirateIdpirateImage
  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() 取得圖片內容,最後在 setCardViewImage
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寫APP30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言