iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Software Development

Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app系列 第 17

Android 圖片選擇器實作:GridView 圖片清單、點擊事件與 ImageView 動態切換

  • 分享至 

  • xImage
  •  

前幾天已經逐步進入 Android App 的實作,今天我們來做一個非常適合練習 圖片資源、GridView、Adapter 與點擊事件 的小作品——「花卉圖片選擇器」。

這次我直接從實際執行的 App 錄影畫面來看成果。

App 上方排列了 6 張不同的花卉圖片,當使用者點選其中一張圖片後,下方的大型 ImageView 就會立即換成被選取的圖片,同時顯示對應的花名。

今天主要會碰到:

  • GridView
  • ImageView
  • TextView
  • Adapter
  • setOnItemClickListener
  • drawable 圖片資源
  • Array / 圖片 ID 對應
  • Android UI 與資料之間的互動

🌸 今天要完成什麼?

先看看今天完成的 App。

DAY16 App 執行畫面

畫面上半部是一個 2 × 3 的花卉縮圖選擇區

可以看到其中包含照片以及插畫風格的花卉圖片。

當我點選其中一張圖片,例如「牡丹」後:

  1. Android 接收到使用者的點擊事件
  2. 找出使用者點擊的是第幾張圖片
  3. 從圖片陣列取得對應的 drawable Resource ID
  4. 將大型 ImageView 換成選取的圖片
  5. TextView 顯示「牡丹」

這其實已經是一個很典型的 Android Master → Detail(選擇 → 顯示詳細內容) UI 概念。


一、先準備圖片資源

Android 專案裡面的圖片,一般可以放在:

app
└── src
    └── main
        └── res
            └── drawable

例如今天準備六張花卉圖片:

res/drawable/

flower1.png
flower2.png
flower3.png
flower4.png
flower5.png
flower6.png

Android 在編譯專案之後,會自動把它們轉換成可以從 Kotlin 使用的 Resource ID。

例如:

R.drawable.flower1
R.drawable.flower2
R.drawable.flower3
R.drawable.flower4
R.drawable.flower5
R.drawable.flower6

所以 Android 開發時,並不是直接寫:

"flower1.png"

而是透過:

R.drawable.flower1

取得圖片資源。


二、建立圖片陣列

六張圖片如果每一張都分開處理,程式很快就會變得又長又難維護。

因此可以先建立圖片 Resource ID 陣列:

val flowerImages = intArrayOf(
    R.drawable.flower1,
    R.drawable.flower2,
    R.drawable.flower3,
    R.drawable.flower4,
    R.drawable.flower5,
    R.drawable.flower6
)

這時候:

flowerImages[0] → flower1
flowerImages[1] → flower2
flowerImages[2] → flower3
flowerImages[3] → flower4
flowerImages[4] → flower5
flowerImages[5] → flower6

這個觀念很重要。

因為稍後 GridView 告訴我們:

你點了 position = 2

我們就可以直接:

flowerImages[2]

取得第三張圖片。

這就是 position 與 Array 搭配的好處。


三、花名也可以做成陣列

圖片有陣列,文字當然也可以。

例如:

val flowerNames = arrayOf(
    "波斯菊",
    "玫瑰",
    "牡丹",
    "馬醉木",
    "水仙",
    "海芋"
)

因此:

flowerImages[0] ↔ flowerNames[0]
flowerImages[1] ↔ flowerNames[1]
flowerImages[2] ↔ flowerNames[2]
...

只要兩個陣列的位置保持一致,我們就可以用同一個 position 同時取得:

flowerImages[position]
flowerNames[position]

也就是:

圖片與文字透過索引值產生對應關係。


四、使用 GridView 排列圖片

今天畫面最上方的六張圖片,不是一張一張手動擺放比較方便,而是可以交給 GridView 處理。

例如 XML:

<GridView
    android:id="@+id/gridView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:numColumns="3"
    android:horizontalSpacing="0dp"
    android:verticalSpacing="0dp" />

其中:

android:numColumns="3"

代表:

每一列顯示三個項目。

六張圖片自然就會變成:

┌────────┬────────┬────────┐
│ 圖片 1 │ 圖片 2 │ 圖片 3 │
├────────┼────────┼────────┤
│ 圖片 4 │ 圖片 5 │ 圖片 6 │
└────────┴────────┴────────┘

這與影片中的實際執行畫面一致。

GridView 六張圖片選擇區


五、GridView 為什麼需要 Adapter?

這裡開始進入今天最重要的 Android 觀念:

Adapter

GridView 本身只負責:

「我要用格子的方式排列東西。」

但是它並不知道:

「每一格到底要放什麼?」

所以我們需要一個 Adapter 當作中間人。

概念可以想成:

圖片資料
   ↓
Adapter
   ↓
GridView
   ↓
手機畫面

Adapter 的工作,就是把我們的:

flowerImages

轉換成一個個:

ImageView

最後交給 GridView 顯示。


六、自訂圖片 Adapter

可以建立一個 BaseAdapter

class ImageAdapter(
    private val context: Context,
    private val images: IntArray
) : BaseAdapter() {

    override fun getCount(): Int {
        return images.size
    }

    override fun getItem(position: Int): Any {
        return images[position]
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    override fun getView(
        position: Int,
        convertView: View?,
        parent: ViewGroup?
    ): View {

        val imageView: ImageView

        if (convertView == null) {

            imageView = ImageView(context)

            imageView.layoutParams = AbsListView.LayoutParams(
                300,
                200
            )

            imageView.scaleType = ImageView.ScaleType.CENTER_CROP

        } else {

            imageView = convertView as ImageView
        }

        imageView.setImageResource(images[position])

        return imageView
    }
}

這段程式就是今天圖片 GridView 的核心。


七、getCount()

首先:

override fun getCount(): Int {
    return images.size
}

假設:

images.size

等於:

6

Adapter 就會告訴 GridView:

我總共有 6 個 Item 要顯示。

所以 GridView 才知道需要建立六格。


八、getView() 才是 Adapter 的核心

真正負責建立畫面的地方是:

override fun getView(
    position: Int,
    convertView: View?,
    parent: ViewGroup?
): View

position 就是目前正在建立第幾格。

例如:

position = 0
position = 1
position = 2
position = 3
position = 4
position = 5

所以:

images[position]

就能取得每一格需要顯示的圖片。

最後:

imageView.setImageResource(images[position])

將圖片放進 ImageView


九、CENTER_CROP 是什麼?

今天的六張圖片比例並不完全一樣。

有些是橫向照片、有些是花卉插畫。

如果全部直接塞進相同大小的格子,很可能會產生:

  • 圖片變形
  • 圖片比例怪異
  • 留下大量白邊

所以可以設定:

imageView.scaleType = ImageView.ScaleType.CENTER_CROP

CENTER_CROP 會:

保持圖片比例,同時裁切超出 ImageView 的部分。

因此很適合:

  • 相簿
  • 商品列表
  • 旅遊照片
  • Avatar
  • 圖片 Grid
  • RecyclerView 圖片列表

十、把 Adapter 指定給 GridView

回到 MainActivity

val gridView = findViewById<GridView>(R.id.gridView)

gridView.adapter = ImageAdapter(
    this,
    flowerImages
)

這一句:

gridView.adapter = ...

就是正式把:

圖片資料
↓
ImageAdapter
↓
GridView

串接起來。


十一、接下來才是重頭戲:點圖片換大圖

影片中可以看到,點選上方不同花卉之後,下方的大圖會跟著改變。

例如影片中的「牡丹」:

點擊後切換為牡丹

這個功能需要:

setOnItemClickListener

例如:

gridView.setOnItemClickListener { parent, view, position, id ->

    imageView.setImageResource(
        flowerImages[position]
    )

    textView.text =
        flowerNames[position]
}

這裡最重要的就是:

position

十二、position 到底是什麼?

假設畫面是:

0        1        2
🌸       🌹       💐

3        4        5
🌿       🌼       🌷

當使用者點第三張:

position = 2

因此:

flowerImages[position]

實際就是:

flowerImages[2]

同時:

flowerNames[position]

就是:

flowerNames[2]

如此一來圖片與名稱就可以一起切換。


十三、ImageView 動態換圖

真正換大圖的是:

imageView.setImageResource(
    flowerImages[position]
)

例如使用者點到第六張:

position = 5

程式就相當於:

imageView.setImageResource(
    flowerImages[5]
)

於是大型 ImageView 就會顯示海芋。

影片中的實際結果:

選擇海芋後的大圖畫面

畫面下方同時顯示:

海芋

十四、TextView 同步更新

圖片切換之後,名稱也需要跟著變。

因此:

textView.text = flowerNames[position]

假設:

position = 5

那麼:

flowerNames[5]

就是:

海芋

所以我們只使用一個 position,就同時完成:

position
   │
   ├── flowerImages[position]
   │        ↓
   │    ImageView
   │
   └── flowerNames[position]
            ↓
         TextView

這就是今天這個範例真正值得理解的地方。


十五、完整 MainActivity 範例

將前面的觀念整合起來,可以寫成:

package com.example.flowerdemo

import android.os.Bundle
import android.widget.GridView
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    // 花卉圖片 Resource ID
    private val flowerImages = intArrayOf(
        R.drawable.flower1,
        R.drawable.flower2,
        R.drawable.flower3,
        R.drawable.flower4,
        R.drawable.flower5,
        R.drawable.flower6
    )

    // 花卉名稱
    // 注意:順序必須與 flowerImages 完全相同
    private val flowerNames = arrayOf(
        "波斯菊",
        "玫瑰",
        "牡丹",
        "馬醉木",
        "水仙",
        "海芋"
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        // 取得 XML 中的 View
        val gridView =
            findViewById<GridView>(R.id.gridView)

        val imageView =
            findViewById<ImageView>(R.id.imageView)

        val textView =
            findViewById<TextView>(R.id.textView)

        // 將圖片資料交給 Adapter
        gridView.adapter =
            ImageAdapter(
                this,
                flowerImages
            )

        // 設定 GridView 點擊事件
        gridView.setOnItemClickListener {
                parent,
                view,
                position,
                id ->

            // 根據 position 取得圖片
            imageView.setImageResource(
                flowerImages[position]
            )

            // 根據相同 position 取得花名
            textView.text =
                flowerNames[position]
        }
    }
}

十六、完整 ImageAdapter

package com.example.flowerdemo

import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.AbsListView
import android.widget.BaseAdapter
import android.widget.ImageView

class ImageAdapter(
    private val context: Context,
    private val images: IntArray
) : BaseAdapter() {

    // GridView 總共有幾個 Item
    override fun getCount(): Int {

        return images.size
    }

    // 取得指定位置的資料
    override fun getItem(
        position: Int
    ): Any {

        return images[position]
    }

    // 取得 Item ID
    override fun getItemId(
        position: Int
    ): Long {

        return position.toLong()
    }

    // 建立每一格所需要顯示的 ImageView
    override fun getView(
        position: Int,
        convertView: View?,
        parent: ViewGroup?
    ): View {

        val imageView: ImageView

        // convertView == null
        // 代表目前沒有可以重複使用的 View
        if (convertView == null) {

            imageView =
                ImageView(context)

            // 設定每一格圖片尺寸
            imageView.layoutParams =
                AbsListView.LayoutParams(
                    300,
                    200
                )

            // 維持圖片比例並填滿 ImageView
            imageView.scaleType =
                ImageView.ScaleType.CENTER_CROP

        } else {

            // 重複利用舊 View
            // 避免一直建立新的 ImageView
            imageView =
                convertView as ImageView
        }

        // 根據 position 顯示對應圖片
        imageView.setImageResource(
            images[position]
        )

        return imageView
    }
}

十七、今天其實已經碰到 MVC / 資料與 UI 分離的概念

雖然今天的 App 很小,但仔細拆解,其實已經可以看到 Android App 常見的架構概念。

資料:

flowerImages
flowerNames

畫面:

GridView
ImageView
TextView

中間負責把資料放進 GridView:

ImageAdapter

使用者操作:

setOnItemClickListener

最後形成:

使用者點擊
    ↓
取得 position
    ↓
查詢 Array
    ↓
更新 ImageView
    ↓
更新 TextView

這也是之後學習:

ListView
RecyclerView
ViewPager
Spinner
Jetpack Compose LazyColumn
LazyVerticalGrid

非常重要的基礎。


十八、為什麼不要寫六個 OnClickListener?

初學時可能會想寫:

image1.setOnClickListener {
    // 顯示花 1
}

image2.setOnClickListener {
    // 顯示花 2
}

image3.setOnClickListener {
    // 顯示花 3
}

一路寫到:

image6.setOnClickListener {
    // 顯示花 6
}

六張圖片還勉強可以。

如果今天變成:

100 張照片

難道要寫 100 個 setOnClickListener

當然不可能。

所以才會使用:

Array
+
Adapter
+
position

讓同一套程式處理所有圖片。


十九、影片實際操作結果

這次實際在 Android Emulator 上執行,可以看到上方六張圖片維持不動,下方則作為目前選擇結果的顯示區。

例如選擇牡丹:

牡丹顯示結果

選擇海芋:

海芋顯示結果

最大的重點並不是花的圖片,而是:

我們已經讓 Android UI 可以根據使用者的操作,動態改變畫面內容。

這就開始有真正「App」的感覺了。


二十、今天學到了什麼?

DAY 16 完成了一個簡單但非常實用的圖片選擇器。

今天主要學到:

1. drawable 圖片資源
2. ImageView
3. TextView
4. GridView
5. BaseAdapter
6. Array / IntArray
7. Resource ID
8. position
9. setOnItemClickListener
10. setImageResource()
11. 動態更新 UI
12. Adapter 與 View 的關係

其中我認為最值得記住的是:

flowerImages[position]

以及:

flowerNames[position]

只要掌握 position,我們就可以讓一組 UI 元件對應大量資料,而不需要為每個項目寫一份重複程式。


DAY 16 小結

今天從單純「把圖片顯示出來」,進一步做到:

多張圖片
    ↓
GridView
    ↓
Adapter
    ↓
點擊 Item
    ↓
取得 position
    ↓
切換 ImageView
    ↓
更新 TextView

雖然現在只有六朵花,但把資料換掉之後,同一套概念馬上可以變成:

🍜 美食選單
🏨 飯店相簿
🚗 汽車型錄
👕 商品列表
🐶 寵物圖鑑
📷 手機相簿

這也是 Android 開發中很常看到的 UI 模式。

下一步如果把今天的 GridView + BaseAdapter 升級成 RecyclerView,就會開始接近現在 Android App 更常見的列表開發方式。

DAY 16 完成!🌸

8 App 操作錄影
Yes


上一篇
打造 BMI 表單介面進美化——TextInputLayout、ConstraintLayout 與 Drawable 圖文實戰
下一篇
Android 學生資料表單:RadioButton、CheckBox 與表單互動實作
系列文
Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言