前幾天已經逐步進入 Android App 的實作,今天我們來做一個非常適合練習 圖片資源、GridView、Adapter 與點擊事件 的小作品——「花卉圖片選擇器」。
這次我直接從實際執行的 App 錄影畫面來看成果。
App 上方排列了 6 張不同的花卉圖片,當使用者點選其中一張圖片後,下方的大型 ImageView 就會立即換成被選取的圖片,同時顯示對應的花名。
今天主要會碰到:
GridView
ImageView
TextView
Adapter
setOnItemClickListener
drawable 圖片資源先看看今天完成的 App。

畫面上半部是一個 2 × 3 的花卉縮圖選擇區。
可以看到其中包含照片以及插畫風格的花卉圖片。
當我點選其中一張圖片,例如「牡丹」後:
ImageView 換成選取的圖片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 處理。
例如 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 │
└────────┴────────┴────────┘
這與影片中的實際執行畫面一致。

這裡開始進入今天最重要的 Android 觀念:
GridView 本身只負責:
「我要用格子的方式排列東西。」
但是它並不知道:
「每一格到底要放什麼?」
所以我們需要一個 Adapter 當作中間人。
概念可以想成:
圖片資料
↓
Adapter
↓
GridView
↓
手機畫面
Adapter 的工作,就是把我們的:
flowerImages
轉換成一個個:
ImageView
最後交給 GridView 顯示。
可以建立一個 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 的核心。
首先:
override fun getCount(): Int {
return images.size
}
假設:
images.size
等於:
6
Adapter 就會告訴 GridView:
我總共有 6 個 Item 要顯示。
所以 GridView 才知道需要建立六格。
真正負責建立畫面的地方是:
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。
今天的六張圖片比例並不完全一樣。
有些是橫向照片、有些是花卉插畫。
如果全部直接塞進相同大小的格子,很可能會產生:
所以可以設定:
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
CENTER_CROP 會:
保持圖片比例,同時裁切超出 ImageView 的部分。
因此很適合:
回到 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
假設畫面是:
0 1 2
🌸 🌹 💐
3 4 5
🌿 🌼 🌷
當使用者點第三張:
position = 2
因此:
flowerImages[position]
實際就是:
flowerImages[2]
同時:
flowerNames[position]
就是:
flowerNames[2]
如此一來圖片與名稱就可以一起切換。
真正換大圖的是:
imageView.setImageResource(
flowerImages[position]
)
例如使用者點到第六張:
position = 5
程式就相當於:
imageView.setImageResource(
flowerImages[5]
)
於是大型 ImageView 就會顯示海芋。
影片中的實際結果:

畫面下方同時顯示:
海芋
圖片切換之後,名稱也需要跟著變。
因此:
textView.text = flowerNames[position]
假設:
position = 5
那麼:
flowerNames[5]
就是:
海芋
所以我們只使用一個 position,就同時完成:
position
│
├── flowerImages[position]
│ ↓
│ ImageView
│
└── flowerNames[position]
↓
TextView
這就是今天這個範例真正值得理解的地方。
將前面的觀念整合起來,可以寫成:
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]
}
}
}
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
}
}
雖然今天的 App 很小,但仔細拆解,其實已經可以看到 Android App 常見的架構概念。
資料:
flowerImages
flowerNames
畫面:
GridView
ImageView
TextView
中間負責把資料放進 GridView:
ImageAdapter
使用者操作:
setOnItemClickListener
最後形成:
使用者點擊
↓
取得 position
↓
查詢 Array
↓
更新 ImageView
↓
更新 TextView
這也是之後學習:
ListView
RecyclerView
ViewPager
Spinner
Jetpack Compose LazyColumn
LazyVerticalGrid
非常重要的基礎。
初學時可能會想寫:
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 元件對應大量資料,而不需要為每個項目寫一份重複程式。
今天從單純「把圖片顯示出來」,進一步做到:
多張圖片
↓
GridView
↓
Adapter
↓
點擊 Item
↓
取得 position
↓
切換 ImageView
↓
更新 TextView
雖然現在只有六朵花,但把資料換掉之後,同一套概念馬上可以變成:
🍜 美食選單
🏨 飯店相簿
🚗 汽車型錄
👕 商品列表
🐶 寵物圖鑑
📷 手機相簿
這也是 Android 開發中很常看到的 UI 模式。
下一步如果把今天的 GridView + BaseAdapter 升級成 RecyclerView,就會開始接近現在 Android App 更常見的列表開發方式。
DAY 16 完成!🌸