今天要正式加入可以操作的表單元件,練習使用 TextInputEditText 接收文字與數字、使用 RadioButton 選擇單一項目、使用 CheckBox 選擇多個項目,最後完成一個會計算 BMI 並顯示對應圖片
今天會完成以下內容:
RadioGroup 與 RadioButton 建立單選題。CheckBox 建立複選題。TextInputEditText 接收姓名、年齡、身高與體重。Toast 顯示輸入錯誤訊息。toDoubleOrNull() 安全轉換數字。setCompoundDrawables() 在文字旁顯示圖片。參考資料:Android Developers-RadioButton|Android Developers-CheckBox

Android Emulator 可以模擬不同型號、螢幕尺寸與 Android 版本的裝置,讓我們不需要準備每一種實體手機,也能測試 App。參考資料:Android Developers-Android Emulator
這次使用 Android Studio 內建的 Pixel 6 模擬器。下圖是模擬器已經成功啟動,但 Gradle 還在同步的狀態。

畫面上方顯示:
Gradle project sync in progress...
此時要等待 Gradle 同步與專案建置完成,不要連續按下執行按鈕。Gradle 同步完成後,Android Studio 才能正確解析相依套件、SDK 版本與專案設定。參考資料:Android Developers-Build your app
成功執行後,Build Output 會顯示:
BUILD SUCCESSFUL
模擬器中則會顯示 App 的畫面:

看到 BUILD SUCCESSFUL 代表專案已成功編譯,但仍要觀察模擬器畫面是否符合預期;「成功建置」和「功能完全正確」是兩個不同層次的檢查。參考資料:Android Developers-Build and run your app
Android 傳統 View 系統通常使用 XML 描述畫面。開啟 res/layout/activity_main.xml 後,可以使用 Android Studio 的 Layout Editor,以拖放方式加入 TextView、Button、LinearLayout、ImageView 等元件。參考資料:Android Developers-Layout Editor

Layout Editor 主要分為以下區域:
| 區域 | 功能 |
|---|---|
| Palette | 提供可以加入畫面的 UI 元件 |
| Component Tree | 顯示目前畫面的元件階層 |
| Design | 顯示接近實際 App 的預覽 |
| Blueprint | 顯示 Constraint 與元件邊界 |
| Attributes | 設定元件 ID、尺寸、文字及背景 |
| Project | 顯示 Kotlin、XML、Drawable 等專案檔案 |
參考資料:Android Developers-Layout Editor overview
參考畫面在 ConstraintLayout 中加入一個水平 LinearLayout,準備將多個選項水平排列。LinearLayout 會依照 orientation 指定的方向,逐一排列內部元件。參考資料:Android Developers-LinearLayout

水平排列:
android:orientation="horizontal"
垂直排列:
android:orientation="vertical"
如果 LinearLayout 放在 ConstraintLayout 中,設定 layout_width="0dp" 通常代表寬度由左右 Constraint 決定;如果沒有設定完整 Constraint,元件可能在執行時移位。參考資料:Android Developers-ConstraintLayout
<LinearLayout
android:id="@+id/linearLayoutOptions"
android:layout_width="0dp"
android:layout_height="80dp"
android:orientation="horizontal"
android:gravity="center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/textViewTitle">
<!-- 選項元件放在這裡 -->
</LinearLayout>
RadioButton 適合用於互斥選項,例如性別、付款方式或圖片種類。多個 RadioButton 應放在同一個 RadioGroup 中,Android 才能自動確保一次只選中一個項目。參考資料:Android Developers-Radio buttons
<RadioGroup
android:id="@+id/radioGroup_Flower"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/radioButton_f1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="第一張花朵" />
<RadioButton
android:id="@+id/radioButton_f2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="第二張花朵" />
<RadioButton
android:id="@+id/radioButton_f3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="第三張花朵" />
</RadioGroup>
RadioGroup 可以透過 checkedRadioButtonId 取得目前被選中的 RadioButton ID。若尚未選擇任何項目,結果會是 View.NO_ID。參考資料:Android Developers-RadioGroup
when (radioGroupFlower.checkedRadioButtonId) {
R.id.radioButton_f1 -> {
textViewResult.text = radioButton1.text
}
R.id.radioButton_f2 -> {
textViewResult.text = radioButton2.text
}
R.id.radioButton_f3 -> {
textViewResult.text = radioButton3.text
}
else -> {
textViewResult.text = "尚未選擇花朵"
}
}
CheckBox 適合用於彼此獨立的複選項目,例如興趣、配料、功能或同意事項。它與 RadioButton 不同,同一畫面中的多個 CheckBox 可以同時勾選。參考資料:Android Developers-Checkboxes
<CheckBox
android:id="@+id/checkBox_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="紅色" />
<CheckBox
android:id="@+id/checkBox_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="黃色" />
<CheckBox
android:id="@+id/checkBox_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="藍色" />
Kotlin 可以透過 isChecked 判斷 CheckBox 是否被選中:
if (checkBox1.isChecked) {
textViewResult.append(checkBox1.text)
}
由於每個 CheckBox 都具有自己的勾選狀態,所以需要分別檢查。參考資料:Android Developers-CheckBox API
第一個完整範例會先讀取使用者選擇的花朵,再將所有勾選的附加選項顯示在 TextView。參考資料:Android Developers-Input controls
package com.example.widget_10b
import android.os.Bundle
import android.widget.Button
import android.widget.CheckBox
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
class MainActivity : AppCompatActivity() {
// 顯示使用者選擇結果的 TextView。
private lateinit var textViewResult: TextView
// 按下後讀取所有選項。
private lateinit var buttonOK: Button
// 三個可以同時選取的 CheckBox。
private lateinit var checkBox1: CheckBox
private lateinit var checkBox2: CheckBox
private lateinit var checkBox3: CheckBox
// 三個只能選擇一個的 RadioButton。
private lateinit var radioButton1: RadioButton
private lateinit var radioButton2: RadioButton
private lateinit var radioButton3: RadioButton
// 管理三個互斥 RadioButton 的 RadioGroup。
private lateinit var radioGroupFlower: RadioGroup
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 啟用 Edge-to-Edge 顯示。
enableEdgeToEdge()
// 載入 activity_main.xml。
setContentView(R.layout.activity_main)
// 加入系統狀態列與導覽列的安全邊距,
// 避免畫面內容被系統列遮住。
ViewCompat.setOnApplyWindowInsetsListener(
findViewById(R.id.main)
) { view, insets ->
val systemBars =
insets.getInsets(
WindowInsetsCompat.Type.systemBars()
)
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
insets
}
// 取得 RadioGroup。
radioGroupFlower =
findViewById(R.id.radioGroup_Flower)
// 取得三個 RadioButton。
radioButton1 =
findViewById(R.id.radioButton_f1)
radioButton2 =
findViewById(R.id.radioButton_f2)
radioButton3 =
findViewById(R.id.radioButton_f3)
// 取得三個 CheckBox。
checkBox1 =
findViewById(R.id.checkBox_1)
checkBox2 =
findViewById(R.id.checkBox_2)
checkBox3 =
findViewById(R.id.checkBox_3)
// 取得確認按鈕與結果 TextView。
buttonOK =
findViewById(R.id.button)
textViewResult =
findViewById(R.id.textView)
// 清除 TextView 的預設文字。
textViewResult.text = ""
buttonOK.setOnClickListener {
// 使用 StringBuilder 組合完整結果,
// 避免反覆直接修改 TextView。
val result = StringBuilder()
// 判斷 RadioGroup 中被選擇的 RadioButton。
when (radioGroupFlower.checkedRadioButtonId) {
R.id.radioButton_f1 -> {
result.append(radioButton1.text)
}
R.id.radioButton_f2 -> {
result.append(radioButton2.text)
}
R.id.radioButton_f3 -> {
result.append(radioButton3.text)
}
else -> {
result.append("尚未選擇花朵")
}
}
result.append("\n")
// 使用集合保存所有勾選的 CheckBox 文字。
val checkedItems = mutableListOf<String>()
if (checkBox1.isChecked) {
checkedItems.add(
checkBox1.text.toString()
)
}
if (checkBox2.isChecked) {
checkedItems.add(
checkBox2.text.toString()
)
}
if (checkBox3.isChecked) {
checkedItems.add(
checkBox3.text.toString()
)
}
// joinToString() 可以避免最後多出一個逗號。
if (checkedItems.isEmpty()) {
result.append("未勾選其他項目")
} else {
result.append(
checkedItems.joinToString(", ")
)
}
// 一次將完整結果顯示到畫面上。
textViewResult.text = result.toString()
}
}
}
參考資料:Android Developers-RadioGroup|Android Developers-CheckBox
原始參考程式使用多次 append():
textViewResult.append("\n")
if (checkBox1.isChecked) {
textViewResult.append("${checkBox1.text} ,")
}
if (checkBox2.isChecked) {
textViewResult.append("${checkBox2.text} ,")
}
if (checkBox3.isChecked) {
textViewResult.append("${checkBox3.text} ,")
}
這樣能正常顯示結果,但最後一個項目後方仍會留下逗號。改用清單及 joinToString(", "),可以產生格式較乾淨的結果。參考資料:Kotlin-joinToString
val checkedItems = mutableListOf<String>()
if (checkBox1.isChecked) {
checkedItems.add(checkBox1.text.toString())
}
if (checkBox2.isChecked) {
checkedItems.add(checkBox2.text.toString())
}
if (checkBox3.isChecked) {
checkedItems.add(checkBox3.text.toString())
}
textViewResult.append(
checkedItems.joinToString(", ")
)
BMI App 需要輸入姓名、年齡、身高與體重,因此使用 Material Components 的 TextInputEditText。它通常放在 TextInputLayout 裡,能提供浮動提示、錯誤顯示與較完整的無障礙支援。參考資料:Android Developers-TextInputEditText
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="姓名">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName" />
</com.google.android.material.textfield.TextInputLayout>
數字欄位應設定適合的 inputType,讓手機顯示相對應的數字鍵盤。身高與體重可能包含小數,因此可以使用 numberDecimal。參考資料:Android Developers-Specify the input method type
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etHeight"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="身高(公分)"
android:inputType="numberDecimal" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etWeight"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="體重(公斤)"
android:inputType="numberDecimal" />
BMI 中文稱為身體質量指數,計算方式是體重公斤數除以身高公尺數的平方。參考資料:衛生福利部國民健康署-成人健康體位標準
BMI = 體重(公斤)÷ 身高(公尺)²
使用者輸入的身高單位是公分,所以程式必須先除以 100,轉換成公尺。參考資料:衛生福利部國民健康署-成人健康體重對照表
private fun calculateBMI(
height: Double,
weight: Double
): Double {
// 將公分換算成公尺。
val heightInMeters = height / 100
// BMI=體重÷身高平方。
return weight /
(heightInMeters * heightInMeters)
}
例如身高 168 公分、體重 79.2 公斤:
身高:168 ÷ 100 = 1.68 公尺
BMI:79.2 ÷ (1.68 × 1.68)
≈ 28.06
依台灣成人體位標準,此結果屬於 BMI 大於等於 27 的肥胖範圍。BMI 只能作為成人健康體位的初步參考,不能單獨作為疾病診斷。參考資料:衛生福利部國民健康署-成人健康體位標準
參考程式使用的是台灣成人體位標準。參考資料:衛生福利部國民健康署-BMI 維持 18~24
| BMI 範圍 | 體位分類 |
|---|---|
BMI < 18.5 |
過輕 |
18.5 ≤ BMI < 24 |
正常 |
24 ≤ BMI < 27 |
過重 |
BMI ≥ 27 |
肥胖 |
程式可使用沒有參數的 when,依條件由上往下判斷。參考資料:Kotlin-when expression
private fun getBMICategory(
bmi: Double
): String {
return when {
bmi < 18.5 -> "過輕"
bmi < 24.0 -> "正常"
bmi < 27.0 -> "過重"
else -> "肥胖"
}
}
參考資料準備了四張不同體位的圖片,可以依 BMI 結果顯示對應圖像。
| 體位 | 圖片 |
|---|---|
| 肥胖 | fat_1.jpg |
| 過重 | fat_2.jpg |
| 過輕 | fat_3.png |
| 正常 | fat_4.jpg |




在 Android 專案中,圖片應放入:
app/src/main/res/drawable/
Drawable 資源檔名只能使用小寫英文字母、數字與底線,不能使用空白、連字號或大寫字母。參考資料:Android Developers-Drawable resources
以下依照上傳的 MainActivity(20260815-010725).kt 整理,保留所有功能並加強欄位驗證、性別檢查、命名與註解。參考資料:Android Developers-Text fields
package com.example.ex2
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.widget.Button
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.google.android.material.textfield.TextInputEditText
class MainActivity : AppCompatActivity() {
// 姓名輸入欄位。
private lateinit var editTextName: TextInputEditText
// 年齡輸入欄位。
private lateinit var editTextAge: TextInputEditText
// 性別單選群組。
private lateinit var radioGroupGender: RadioGroup
// 男性與女性選項。
private lateinit var radioButtonMale: RadioButton
private lateinit var radioButtonFemale: RadioButton
// 身高與體重輸入欄位。
private lateinit var editTextHeight: TextInputEditText
private lateinit var editTextWeight: TextInputEditText
// 清除與計算按鈕。
private lateinit var buttonCancel: Button
private lateinit var buttonBMI: Button
// 顯示個人資料、BMI 與體位結果。
private lateinit var textViewResult: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 讓畫面延伸至系統狀態列及導覽列區域。
enableEdgeToEdge()
// 載入 activity_main.xml。
setContentView(R.layout.activity_main)
// 取得系統列範圍並設定 Padding,
// 避免表單內容被狀態列或導覽列遮住。
ViewCompat.setOnApplyWindowInsetsListener(
findViewById(R.id.main)
) { view, insets ->
val systemBars =
insets.getInsets(
WindowInsetsCompat.Type.systemBars()
)
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
insets
}
// 初始化所有 XML 畫面元件。
initViews()
// 設定兩個按鈕的點擊事件。
setupClickListeners()
}
/**
* 使用 findViewById() 取得 activity_main.xml 中的元件。
*/
private fun initViews() {
editTextName =
findViewById(R.id.etName)
editTextAge =
findViewById(R.id.etAge)
radioGroupGender =
findViewById(R.id.rgGender)
radioButtonMale =
findViewById(R.id.rbMale)
radioButtonFemale =
findViewById(R.id.rbFemale)
editTextHeight =
findViewById(R.id.etHeight)
editTextWeight =
findViewById(R.id.etWeight)
buttonCancel =
findViewById(R.id.btnCancel)
buttonBMI =
findViewById(R.id.btnBMI)
textViewResult =
findViewById(R.id.tvResult)
}
/**
* 設定清除按鈕與 BMI 計算按鈕。
*/
private fun setupClickListeners() {
buttonCancel.setOnClickListener {
// 清除所有文字輸入欄位。
editTextName.text?.clear()
editTextAge.text?.clear()
editTextHeight.text?.clear()
editTextWeight.text?.clear()
// 清除 RadioGroup 的性別選擇。
radioGroupGender.clearCheck()
// 清除 BMI 結果文字。
textViewResult.text = ""
// 清除 TextView 四個方向的圖片。
textViewResult.setCompoundDrawables(
null,
null,
null,
null
)
// 將輸入焦點移回姓名欄位。
editTextName.requestFocus()
}
buttonBMI.setOnClickListener {
// 讀取所有輸入內容並移除頭尾空白。
val nameString =
editTextName.text
?.toString()
?.trim()
.orEmpty()
val ageString =
editTextAge.text
?.toString()
?.trim()
.orEmpty()
val heightString =
editTextHeight.text
?.toString()
?.trim()
.orEmpty()
val weightString =
editTextWeight.text
?.toString()
?.trim()
.orEmpty()
// 檢查是否有空白欄位。
if (
nameString.isEmpty() ||
ageString.isEmpty() ||
heightString.isEmpty() ||
weightString.isEmpty()
) {
showToast("請輸入所有欄位")
return@setOnClickListener
}
// 年齡應該是有效的正整數。
val ageValue =
ageString.toIntOrNull()
if (ageValue == null || ageValue <= 0) {
showToast("年齡格式錯誤")
return@setOnClickListener
}
// 身高與體重允許輸入小數,
// 使用 toDoubleOrNull() 避免格式錯誤造成閃退。
val heightValue =
heightString.toDoubleOrNull()
val weightValue =
weightString.toDoubleOrNull()
if (
heightValue == null ||
heightValue <= 0 ||
weightValue == null ||
weightValue <= 0
) {
showToast("身高或體重格式錯誤")
return@setOnClickListener
}
// 確認使用者已選擇性別。
if (
radioGroupGender.checkedRadioButtonId ==
-1
) {
showToast("請選擇性別")
return@setOnClickListener
}
// 計算 BMI。
val bmi =
calculateBMI(
height = heightValue,
weight = weightValue
)
// 取得 BMI 分類結果。
val bmiCategory =
getBMICategory(bmi)
// 取得目前選擇的性別文字。
val gender =
when (
radioGroupGender
.checkedRadioButtonId
) {
R.id.rbMale -> "男"
R.id.rbFemale -> "女"
else -> "未指定"
}
// 顯示個人資料與 BMI 計算結果。
textViewResult.text =
"""
姓名:$nameString
年齡:$ageValue
性別:$gender
身高:$heightValue 公分
體重:$weightValue 公斤
BMI:${String.format("%.2f", bmi)}
BMI 結果:$bmiCategory
""".trimIndent()
// 根據性別及 BMI 顯示對應圖片。
updateResultDrawable(bmi)
}
}
/**
* 計算 BMI。
*
* @param height 身高,單位為公分
* @param weight 體重,單位為公斤
* @return BMI 計算結果
*/
private fun calculateBMI(
height: Double,
weight: Double
): Double {
// 將身高由公分轉換成公尺。
val heightInMeters = height / 100.0
// BMI=體重÷身高平方。
return weight /
(heightInMeters * heightInMeters)
}
/**
* 依照台灣成人健康體位標準分類 BMI。
*/
private fun getBMICategory(
bmi: Double
): String {
return when {
bmi < 18.5 -> "過輕"
bmi < 24.0 -> "正常"
bmi < 27.0 -> "過重"
else -> "肥胖"
}
}
/**
* 根據性別與 BMI 選擇結果圖片,
* 再顯示於 TextView 左側。
*/
private fun updateResultDrawable(
bmi: Double
) {
val imageResource =
if (radioButtonMale.isChecked) {
when {
bmi < 18.5 ->
R.drawable.men_fat3
bmi < 24.0 ->
R.drawable.men_fat4
bmi < 27.0 ->
R.drawable.men_fat2
else ->
R.drawable.men_fat1
}
} else if (
radioButtonFemale.isChecked
) {
when {
bmi < 18.5 ->
R.drawable.woman_fat3
bmi < 24.0 ->
R.drawable.woman_fat4
bmi < 27.0 ->
R.drawable.woman_fat2
else ->
R.drawable.woman_fat1
}
} else {
null
}
if (imageResource != null) {
// 將 Drawable 資源 ID 轉換成 Drawable。
val drawable: Drawable? =
ContextCompat.getDrawable(
this,
imageResource
)
// 將 120dp 轉換成目前裝置使用的像素值。
val imageSize =
(
120 *
resources.displayMetrics.density
).toInt()
// Compound Drawable 必須先設定顯示邊界。
drawable?.setBounds(
0,
0,
imageSize,
imageSize
)
// 在 TextView 左側顯示圖片。
textViewResult.setCompoundDrawables(
drawable,
null,
null,
null
)
// 增加圖片與文字之間的距離。
textViewResult.compoundDrawablePadding =
(
12 *
resources.displayMetrics.density
).toInt()
} else {
// 沒有選擇性別時清除圖片。
textViewResult.setCompoundDrawables(
null,
null,
null,
null
)
}
}
/**
* 集中顯示短時間 Toast 訊息。
*/
private fun showToast(
message: String
) {
Toast.makeText(
this,
message,
Toast.LENGTH_SHORT
).show()
}
}
參考資料:Android Developers-TextInputEditText|Android Developers-Toast
如果直接呼叫:
val height = heightString.toDouble()
當使用者輸入英文字母、空字串或錯誤格式時,程式會產生 NumberFormatException。使用 toDoubleOrNull() 時,無法轉換就會回傳 null,程式可以自行顯示錯誤訊息。參考資料:Kotlin-toDoubleOrNull
val height = heightString.toDoubleOrNull()
if (height == null || height <= 0) {
Toast.makeText(
this,
"身高格式錯誤",
Toast.LENGTH_SHORT
).show()
return@setOnClickListener
}
這種寫法可以避免 App 因為使用者輸入錯誤而直接閃退。輸入資料永遠不應被程式預設為正確,特別是數字、帳號、密碼與網路傳回的資料。參考資料:Android Developers-Improve your app’s reliability
Toast 是短暫出現在畫面上的提示訊息,適合顯示「請輸入所有欄位」或「身高格式錯誤」等簡短通知。參考資料:Android Developers-Toasts
Toast.makeText(
this,
"請輸入所有欄位",
Toast.LENGTH_SHORT
).show()
三個參數分別代表:
| 參數 | 說明 |
|---|---|
this |
顯示 Toast 使用的 Context |
"請輸入所有欄位" |
要顯示的訊息 |
Toast.LENGTH_SHORT |
顯示時間 |
錯誤若只對應某一個輸入欄位,實務上也可以使用 TextInputLayout.error,讓錯誤訊息直接顯示在欄位附近,比單獨顯示 Toast 更容易理解。參考資料:Android Developers-TextInputLayout
TextView 除了文字之外,還能在左、上、右、下四個方向設定 Compound Drawable。參考程式將 BMI 圖片顯示在結果文字左側。參考資料:Android Developers-TextView.setCompoundDrawables
textViewResult.setCompoundDrawables(
drawable, // 左
null, // 上
null, // 右
null // 下
)
使用 setCompoundDrawables() 時,需要先呼叫 setBounds() 設定 Drawable 的顯示尺寸:
drawable?.setBounds(
0,
0,
imageSize,
imageSize
)
如果不想自行設定 Bounds,也可以改用 setCompoundDrawablesWithIntrinsicBounds(),讓 Drawable 使用原始尺寸;但圖片原始大小不一致時,畫面容易出現尺寸差異。參考資料:Android Developers-TextView API
Android XML 通常使用 dp 指定元件尺寸,但 Drawable.setBounds() 接收的是像素。因此程式使用螢幕密度將 120dp 轉換為實際像素。參考資料:Android Developers-Support different pixel densities
val imageSize =
(
120 *
resources.displayMetrics.density
).toInt()
不同裝置的像素密度不一樣,直接使用固定的 120px,可能在高密度手機上看起來很小;使用 dp 換算後,圖片的視覺尺寸會比較一致。參考資料:Android Developers-Density independence
BMI App 的清除按鈕不只要清空 TextInputEditText,還要取消 RadioGroup 選擇、清除結果文字與圖片。參考資料:Android Developers-RadioGroup.clearCheck
buttonCancel.setOnClickListener {
editTextName.text?.clear()
editTextAge.text?.clear()
editTextHeight.text?.clear()
editTextWeight.text?.clear()
radioGroupGender.clearCheck()
textViewResult.text = ""
textViewResult.setCompoundDrawables(
null,
null,
null,
null
)
editTextName.requestFocus()
}
完整重設可以避免上一筆資料的性別、結果圖片或計算內容殘留在畫面上。最後使用 requestFocus() 將游標移回姓名欄位,讓使用者可以直接開始輸入下一筆資料。參考資料:Android Developers-View.requestFocus
這次上傳的素材還包含剪刀、石頭、布圖片,可以運用今天學到的 RadioGroup、RadioButton、ImageView、Button 和 when,製作猜拳遊戲。



遊戲流程可以設計成:
0..2。TextView 顯示勝負結果。ImageView 顯示電腦出拳圖片。Kotlin 可以使用 (0..2).random() 產生三種結果。參考資料:Kotlin-Random
val computerChoice = (0..2).random()
val computerText =
when (computerChoice) {
0 -> "剪刀"
1 -> "石頭"
else -> "布"
}
猜拳勝負可以使用 when 判斷:
val result =
when {
playerChoice == computerChoice ->
"平手"
playerChoice == 0 &&
computerChoice == 2 ->
"玩家獲勝"
playerChoice == 1 &&
computerChoice == 0 ->
"玩家獲勝"
playerChoice == 2 &&
computerChoice == 1 ->
"玩家獲勝"
else ->
"電腦獲勝"
}
這個延伸練習能把今天的單選按鈕、圖片切換、亂數與條件判斷整合成一個小型遊戲。參考資料:Kotlin-Conditions and loops
若新版 Android Emulator 在特定電腦出現相容性問題,可以依照參考文件,暫時改用官方封存的 Emulator 版本,例如 35.5.10 Stable。不過降級屬於疑難排解手段,應先確認 SDK Tools、虛擬化功能、顯示卡驅動程式及 AVD 設定。參考資料:Android Developers-Emulator download archives
在 Android Studio 開啟:
Tools
→ SDK Manager
→ Android SDK Location
Windows 常見位置:
C:\Users\User\AppData\Local\Android\Sdk
實際路徑會依 Windows 帳號名稱與自訂安裝位置不同,應以 Android Studio 顯示的 Android SDK Location 為準。參考資料:Android Developers-Update IDE and SDK tools
操作檔案前,先關閉:
Android Studio
Android Emulator
adb
至少要確定模擬器已完全關閉,避免 emulator 資料夾中的檔案仍被程序占用。參考資料:Android Developers-Emulator troubleshooting
官方封存下載頁:
Android Emulator Download Archives
依作業系統下載對應版本,例如:
Android Emulator 35.5.10 Stable
Windows
只應從 Android Developers 官方網站下載 Emulator,不建議使用來源不明的壓縮檔。參考資料:Android Developers-Emulator archive
進入 Android SDK 目錄:
C:\Users\User\AppData\Local\Android\Sdk
將原本的:
emulator
更名為:
emulator_bck
保留備份後,若降級版本無法使用,還能刪除新資料夾並將 emulator_bck 改回 emulator。參考資料:Android Developers-Manually install an Emulator version
將下載的 Emulator 壓縮檔解壓縮,確認解壓縮後的資料夾名稱為:
emulator
再將整個資料夾放入:
C:\Users\User\AppData\Local\Android\Sdk
完成後應形成:
C:\Users\User\AppData\Local\Android\Sdk\emulator
不要變成雙層路徑:
C:\Users\User\AppData\Local\Android\Sdk\emulator\emulator
參考資料:Android Developers-Emulator download archives
依照上傳文件的做法,可將備份資料夾:
emulator_bck\package.xml
複製到新的:
emulator\package.xml
但官方說明指出,手動安裝特定 Emulator 版本時,需要讓 package.xml 內記錄的 Emulator 版本與實際安裝版本一致。因此不能只複製後完全不檢查版本內容,否則 Android Studio 可能誤判已安裝版本。參考資料:Android Developers-Manually install a select Emulator version
重新開啟 Android Studio 後,進入:
Tools
→ Device Manager
再啟動原本的 Pixel 6 AVD。如果仍無法啟動,可以嘗試:
Cold Boot Now
若 AVD 快取已損壞,再考慮:
Wipe Data
Wipe Data 會清除模擬器中的 App 與測試資料,執行前應先確認模擬器內沒有需要保留的內容。參考資料:Android Developers-Create and manage virtual devices
| 範例 | 主要元件 | 練習重點 |
|---|---|---|
| 花朵選擇 | RadioGroup、RadioButton |
單選項目 |
| 附加選項 | CheckBox |
多選項目 |
| BMI 計算器 | TextInputEditText、RadioGroup、Button、TextView |
表單驗證、計算與圖片顯示 |
| 猜拳延伸 | RadioButton、ImageView、亂數 |
條件判斷與遊戲流程 |
| 模擬器排錯 | Android Emulator | 開發環境維護 |
參考資料:Android Developers-Views UI
今天透過選項表單和 BMI 計算器,完成了以下 Android 開發練習:
LinearLayout 水平或垂直排列元件。RadioGroup 管理互斥選項。checkedRadioButtonId 取得單選結果。CheckBox.isChecked 取得複選結果。TextInputEditText 接收使用者輸入。trim() 移除輸入內容頭尾空白。toIntOrNull() 與 toDoubleOrNull() 安全轉換數字。Toast 顯示輸入錯誤。ContextCompat.getDrawable() 取得圖片。setBounds() 設定 Drawable 尺寸。setCompoundDrawables() 在文字旁顯示圖片。clearCheck() 清除 RadioGroup 選擇。參考資料:Android Developers-UI components|衛生福利部國民健康署-成人健康體位標準
今天的 BMI App 已經具備一個實用表單的基本流程:接收資料、驗證輸入、執行計算、判斷結果,再使用文字與圖片回應使用者。這些技巧不只適用於 BMI,也能延伸到註冊表單、問卷、購物選項、健康紀錄及個人資料設定。參考資料:Android Developers-Text fields
之後有篇幅可以再繼續學習 Spinner、Switch、等 與日期時間選擇器,讓表單支援下拉選單、開關、數值範圍及日期輸入。參考資料:Android Developers-Common widgets