我會以你這次最新上傳的程式重新定位 Day 21。這批內容已經不是前一版的 BMI,而是完整進入 Android 的 Menu、Dialog、Spinner、NumberPicker、WebView、Navigation Drawer、Date/Time Picker、Bottom Navigation;另外你新加入的三張貓咪圖片也實際用在 Menu 3 與 Bottom Navigation 的選項中。
前面幾天,我們已經從 Activity、Intent 慢慢做到可以互動的 Android App。
今天要進入另一個每天使用 App 都一定會遇到的東西:
手機畫面不可能把所有功能都塞成 Button。
例如:
⋮ 更多功能
☰ 側邊選單
🏠 首頁
🐱 Cat
🐶 Dog
📅 日期
🕐 時間
🌐 Web
這些其實都是 App 的「操作入口」。
今天的練習量相當大,從最基本的 Options Menu 開始,一路做到:
Options Menu
↓
SubMenu
↓
AlertDialog
↓
Intent / Activity
↓
Spinner / NumberPicker
↓
Custom Dialog
↓
WebView
↓
Navigation Drawer
↓
DatePicker / TimePicker
↓
Bottom Navigation
可以說今天是在練習:
Android App 到底要怎麼讓使用者「找到功能、選擇功能、切換功能」。
Android Menu / Navigation
│
┌───────────────────┼───────────────────┐
↓ ↓ ↓
Options Menu Selection UI Navigation UI
│ │ │
┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴──────┐
↓ ↓ ↓ ↓ ↓ ↓
SubMenu AlertDialog Spinner NumberPicker Drawer Bottom Nav
│ │ │ │
↓ ↓ ↓ ↓
Image Intent ☰ Menu 🐱🐶 Menu
Change │
↓
DisplayActivity
Custom Dialog
│
↓
Intent
│
↓
WebView
DatePicker
│
↓
TimeActivity
│
↓
TimePicker
今天不是單純做一個元件,而是一次把 Android 常見的互動方式串起來。
第一個範例沒有使用 XML Menu,而是直接利用 Kotlin 建立選單。
首先定義每一個 Menu ID:
companion object {
const val ChangPicture = Menu.FIRST
const val ChangeColor = Menu.FIRST + 1
const val ChangeText = Menu.FIRST + 2
const val Text1 = Menu.FIRST + 3
const val Text2 = Menu.FIRST + 4
const val About = Menu.FIRST + 5
}
接著覆寫:
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
super.onCreateOptionsMenu(menu)
menu?.add(
1,
ChangPicture,
Menu.NONE,
"Change picture"
)
menu?.add(
1,
ChangeColor,
Menu.NONE,
"Change color"
)
val subMenu = menu?.addSubMenu(
1,
ChangeText,
Menu.NONE,
"Change text"
)
subMenu?.add(
1,
Text1,
Menu.NONE,
"Text 1"
)
subMenu?.add(
1,
Text2,
Menu.NONE,
"Text 2"
)
menu?.add(
1,
About,
Menu.NONE,
"About"
)
return true
}
最後會形成:
⋮
├── Change picture
├── Change color
├── Change text >
│ ├── Text 1
│ └── Text 2
└── About
📸 【圖 1:Options Menu 與 SubMenu 展開畫面】
這裡最重要的是:
onCreateOptionsMenu()
負責「建立 Menu」。
而:
onOptionsItemSelected()
負責「使用者點下 Menu 後要做什麼」。
Menu 建好之後,我們可以讓它真正控制畫面。
程式使用:
private var picFlag = true
記錄現在是哪一張圖片。
接著:
ChangPicture -> {
Toast.makeText(
this,
"Change picture clicked",
Toast.LENGTH_SHORT
).show()
if (picFlag) {
imgView.setImageResource(
R.drawable.flower2
)
picFlag = false
} else {
imgView.setImageResource(
R.drawable.flower1
)
picFlag = true
}
}
流程就是:
Change picture
↓
picFlag ?
↙ ↘
true false
↓ ↓
flower2 flower1
↓ ↓
false true
同一個 Menu 就能讓兩張圖片來回切換。
另一個 Menu:
Change color
程式:
ChangeColor -> {
if (colorFlag) {
textViewData.setTextColor(
0x7FFF88FF.toInt()
)
colorFlag = false
} else {
textViewData.setTextColor(
0x7F2196F3.toInt()
)
colorFlag = true
}
}
這跟換圖片的概念非常像:
Boolean Flag
↓
目前狀態
↓
切換狀態
↓
更新 UI
這種 Flag 的概念在 Android 開發非常常見。
今天也第一次加入:
Change text
├── Text 1
└── Text 2
程式:
Text1 -> {
textViewData.text =
"There are lots of flowers"
}
Text2 -> {
textViewData.text =
"This is a pretty flower"
}
因此:
Menu
↓
Change text
↓
SubMenu
├── Text 1
└── Text 2
比全部塞在第一層 Menu 更容易整理。
點選:
About
不是單純 Toast,而是開啟:
AlertDialog.Builder
完整邏輯:
val builder =
AlertDialog.Builder(this@MainActivity)
builder.setTitle(
"About option menu"
)
builder.setIcon(
android.R.drawable.ic_dialog_info
)
builder.setMessage(
"This is alert dialog"
)
builder.setPositiveButton(
"ok"
) { dialog, which ->
textViewData.text =
"About dialog"
}
builder.setNegativeButton(
"cancel"
) { dialog, which ->
textViewData.text =
"Cancel dialog"
dialog?.dismiss()
}
builder.show()
畫面概念:
┌───────────────────────────┐
│ ⓘ About option menu │
│ │
│ This is alert dialog │
│ │
│ CANCEL OK │
└───────────────────────────┘
📸 【圖 2:About AlertDialog】
這就開始出現真正 App 常見的操作方式了。
第二版 Menu 不再全部使用:
menu.add()
而改成:
override fun onCreateOptionsMenu(
menu: Menu
): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(
R.menu.setup,
menu
)
return true
}
也就是:
res/
└── menu/
└── setup.xml
負責定義 Menu。
而:
MainActivity.kt
只負責處理點擊行為。
架構變成:
setup.xml
│
↓
Menu UI
│
↓
onOptionsItemSelected()
│
↓
Kotlin Logic
這種做法也讓 UI 與程式邏輯分工得更清楚。
Menu 2 又再進一步。
點擊:
About
首先:
val builder =
AlertDialog.Builder(this@MainActivity)
顯示:
Menu App
Would you like to change
to Display Activity?
OK
按下 OK:
builder.setPositiveButton("OK") {
dialog, _ ->
val intent = Intent(
this@MainActivity,
DisplayActivity::class.java
)
startActivity(intent)
dialog.dismiss()
}
因此流程變成:
MainActivity
↓
Options Menu
↓
About
↓
AlertDialog
↓
OK
↓
Intent
↓
DisplayActivity
這裡就把昨天學到的 Intent 和今天的 Menu 串起來了。
進入 DisplayActivity 後:
supportActionBar?.title =
"Display Page"
接著:
supportActionBar
?.setDisplayHomeAsUpEnabled(true)
這會讓 ActionBar 左邊出現:
← Display Page
再處理:
override fun onOptionsItemSelected(
item: MenuItem
): Boolean {
if (
item.itemId ==
android.R.id.home
) {
finish()
}
return super.onOptionsItemSelected(item)
}
流程:
MainActivity
│
↓
DisplayActivity
│
↓
← 返回
│
↓
finish()
│
↓
MainActivity
📸 【圖 3:DisplayActivity 左上角返回箭頭】
這次最新資料裡加入了:
Flower
Cat 1
Cat 2
Cat 3
Dog 1
Dog 2
Dog 3
使用者選擇不同 Menu,ImageView 就會立即換圖片。
例如:
R.id.menu_cat1 -> {
textViewData.text =
"It is Cat 1"
imageViewPic.setImageResource(
R.drawable.cat_1
)
}
Cat 2:
R.id.menu_cat2 -> {
textViewData.text =
"It is Cat 2"
imageViewPic.setImageResource(
R.drawable.cat_2
)
}
Cat 3:
R.id.menu_cat3 -> {
textViewData.text =
"It is Cat 3"
imageViewPic.setImageResource(
R.drawable.cat_3
)
}
【直接放今天上傳的 cat_1.jpg】
這張就是:
R.drawable.cat_1
點:
Cat 1
畫面:
┌────────────────────┐
│ It is Cat 1 │
│ │
│ [ CAT 1 ] │
│ │
└────────────────────┘
【直接放今天上傳的 cat_2.jpg】
對應:
R.drawable.cat_2
選擇:
Cat 2
執行:
textViewData.text =
"It is Cat 2"
imageViewPic.setImageResource(
R.drawable.cat_2
)
【直接放今天上傳的 cat_3.jpg】
對應:
R.drawable.cat_3
所以整個 Menu 3 可以理解為:
Menu
│
┌────────┼────────┐
↓ ↓ ↓
Cat 1 Cat 2 Cat 3
│ │ │
↓ ↓ ↓
cat_1.jpg cat_2.jpg cat_3.jpg
│ │ │
└────────┼────────┘
↓
ImageView
📸 【圖 4:Cat 1 / Cat 2 / Cat 3 三張圖片並排】
這個範例非常適合放在文章首圖附近,因為可以直接看出:
Menu 選了什麼,畫面就跟著變。
接下來是另外一種 Selection 元件:
Spinner
程式先取得:
spinner1 =
findViewById(R.id.spinner_1)
spinner2 =
findViewById(R.id.spinner_2)
資料來源:
val countryArray =
resources.getStringArray(
R.array.country
)
再建立:
val adapter =
ArrayAdapter.createFromResource(
this@MainActivity,
R.array.country,
R.layout.simple_spinner_item
)
設定展開畫面:
adapter.setDropDownViewResource(
R.layout.simple_spinner_dropdown_item
)
最後:
spinner1.adapter = adapter
spinner2.adapter = adapter
結構:
String Array
↓
ArrayAdapter
↓
Spinner
↓
使用者選擇
↓
onItemSelected()
↓
TextView
這個範例也很有意思。
第一個:
numberPicker1.minValue = 0
numberPicker1.maxValue = 100
numberPicker1.value = 5
就是:
4
[5]
6
但第二個 NumberPicker:
numberPicker2.minValue = 0
numberPicker2.maxValue =
countryArray.size - 1
numberPicker2.displayedValues =
countryArray
竟然可以顯示國家名稱。
也就是:
Taiwan
Japan
USA
Korea
...
當使用者改變:
numberPicker2
.setOnValueChangedListener {
picker,
oldVal,
newVal ->
textViewResult.text =
"picker 2 = " +
countryArray[newVal]
}
📸 【圖 5:Spinner + NumberPicker 選擇國家】
接著進入更實用的範例。
點:
LOGIN
不是直接換 Activity,而是:
val myBuilder =
Dialog(this@MainActivity)
myBuilder.setContentView(
R.layout.dialog_layout
)
也就是自己建立:
dialog_layout.xml
內容包含:
┌─────────────────────────┐
│ Name │
│ [____________________] │
│ │
│ Email │
│ [____________________] │
│ │
│ CANCEL OK │
└─────────────────────────┘
取得 Dialog 裡面的 View:
val editTextName =
myBuilder.findViewById<EditText>(
R.id.editText_dialogName
)
val editTextEmail =
myBuilder.findViewById<EditText>(
R.id.editText_dialogEmail
)
按 OK:
val name =
editTextName.text.toString()
val email =
editTextEmail.text.toString()
然後:
Toast.makeText(
this@MainActivity,
"name = $name , email = $email",
Toast.LENGTH_SHORT
).show()
按下 OK 後:
val intent =
Intent(
this@MainActivity,
WebActivity::class.java
)
startActivity(intent)
流程:
MainActivity
↓
LOGIN
↓
Custom Dialog
↓
Name + Email
↓
OK
↓
Intent
↓
WebActivity
↓
WebView
這就開始很像真正的登入流程。
WebActivity 裡使用:
private lateinit var webView: WebView
初始化:
webView =
findViewById(
R.id.webView_id
)
接著:
webView.settings.apply {
javaScriptEnabled = true
setSupportZoom(true)
builtInZoomControls = true
}
開啟 JavaScript:
javaScriptEnabled = true
支援縮放:
setSupportZoom(true)
builtInZoomControls = true
按下 Link:
buttonLink.setOnClickListener {
if (editTextIpAddr.length() == 0) {
ipAddr =
"https://tw.yahoo.com/"
} else {
ipAddr =
editTextIpAddr.text.toString()
}
webView.webViewClient =
WebViewClient()
webView.loadUrl(ipAddr)
}
所以:
網址欄位
│
├── 空白
│ ↓
│ Yahoo Taiwan
│
└── 有網址
↓
使用輸入網址
↓
WebView.loadUrl()
📸 【圖 6:WebView 載入網站畫面】
再來是現在很多 App 都會看到的:
☰
也就是 Navigation Drawer。
主要使用:
DrawerLayout
以及:
NavigationView
初始化:
myDrawer =
findViewById(
R.id.drawerlayout_id
)
navView =
findViewById(
R.id.navigationView
)
再建立:
drawerToggle =
ActionBarDrawerToggle(
this,
myDrawer,
R.string.open,
R.string.close
)
設定:
myDrawer.addDrawerListener(
drawerToggle
)
drawerToggle.syncState()
於是 ActionBar 左邊就會出現:
☰
選 Item:
navView
.setNavigationItemSelectedListener {
item ->
when (item.itemId) {
R.id.nav_item1 -> {
textViewData.text =
"Item 1"
}
R.id.nav_item2 -> {
textViewData.text =
"Item 2"
}
R.id.nav_item3 -> {
textViewData.text =
"Item 3"
}
}
myDrawer.closeDrawers()
true
}
注意最後:
myDrawer.closeDrawers()
使用者選完之後把 Drawer 收回。
流程:
☰
↓
Drawer 開啟
↓
Item 1
Item 2
Item 3
↓
選擇
↓
更新 TextView
↓
closeDrawers()
📸 【圖 7:Navigation Drawer 展開畫面】
接著做到日期。
首先:
now =
Calendar.getInstance()
取得:
val year =
now.get(Calendar.YEAR)
val month =
now.get(Calendar.MONTH)
val day =
now.get(Calendar.DAY_OF_MONTH)
初始化:
datePicker.init(
year,
month,
day
) { _, year, monthOfYear, dayOfMonth ->
textViewData.text =
"$selectPrefix$year/" +
"${monthOfYear + 1}/" +
"$dayOfMonth"
}
注意:
monthOfYear + 1
因為 Calendar.MONTH 是從:
0
開始計算。
Menu 選:
Date
執行:
val dateDialog =
DatePickerDialog(
this@MainActivity,
myDate,
now.get(Calendar.YEAR),
now.get(Calendar.MONTH),
now.get(Calendar.DAY_OF_MONTH)
)
dateDialog.setMessage(
"Please select date"
)
dateDialog.show()
📸 【圖 8:DatePickerDialog 日期選擇畫面】
選完後:
override fun onDateSet(
view: DatePicker?,
year: Int,
month: Int,
dayOfMonth: Int
) {
now.set(
Calendar.YEAR,
year
)
now.set(
Calendar.MONTH,
month
)
now.set(
Calendar.DAY_OF_MONTH,
dayOfMonth
)
textViewData.text =
"$selectPrefix$year/" +
"${month + 1}/" +
"$dayOfMonth"
datePicker.updateDate(
year,
month,
dayOfMonth
)
}
另一個 Menu:
Time
直接:
val intent =
Intent(
this@MainActivity,
TimeActivity::class.java
)
startActivity(intent)
所以:
Date Activity
↓
Menu
↓
Time
↓
Intent
↓
TimeActivity
TimeActivity 取得現在時間:
now =
Calendar.getInstance()
val hour =
now.get(
Calendar.HOUR_OF_DAY
)
val minute =
now.get(
Calendar.MINUTE
)
設定:
timePicker.setIs24HourView(
true
)
所以使用:
24 小時制
按下按鈕:
val timeDialog =
TimePickerDialog(
this@TimeActivity,
MyTime(),
hour,
minute,
true
)
timeDialog.setMessage(
"Select time"
)
timeDialog.show()
使用者選完:
override fun onTimeSet(
view: TimePicker?,
hourOfDay: Int,
minute: Int
) {
textViewData.text =
"Selected time: " +
"$hourOfDay:$minute"
}
📸 【圖 9:TimePickerDialog 時間選擇畫面】
最後做到另一個非常常見的 UI:
┌────────────────────────────┐
│ │
│ App Content │
│ │
├────────────────────────────┤
│ 🐱 🐱 + 🐱 🐶 ⋮ │
└────────────────────────────┘
使用:
BottomNavigationView
以及:
FloatingActionButton
取得:
val bottomNavigationView:
BottomNavigationView =
findViewById(
R.id.bottomAppBar_id
)
val floatingButton:
FloatingActionButton =
findViewById(
R.id.floatingActionButton_id
)
這次你提供的三張貓咪圖片,在這個範例也可以作為 Bottom Navigation 的圖示資源。
程式特別設定:
bottomNavigationView
.itemIconTintList = null
這一行很重要。
Android Material 元件預設可能會幫 Menu Icon 套上 Tint 顏色。
如果我們使用的是:
🐱 真實貓咪圖片
就不希望系統把整張圖片染成單色。
因此:
itemIconTintList = null
保留圖片原本的顏色。
另外:
bottomNavigationView
.isItemActiveIndicatorEnabled =
false
關閉預設 Active Indicator。
bottomNavigationView
.setOnItemSelectedListener {
item ->
when (item.itemId) {
R.id.menu_cat1 -> {
textViewData1.text =
"Cat 1"
}
R.id.menu_cat2 -> {
textViewData1.text =
"Cat 2"
}
R.id.menu_cat3 -> {
textViewData1.text =
"Cat 3"
}
R.id.menu_dog1 -> {
textViewData2.text =
"Dog 1"
}
R.id.menu_dog2 -> {
textViewData2.text =
"Dog 2"
}
R.id.menu_about -> {
textViewData2.text =
"About bottom menu"
}
}
true
}
因此:
🐱 Cat1 → "Cat 1"
🐱 Cat2 → "Cat 2"
🐱 Cat3 → "Cat 3"
🐶 Dog1 → "Dog 1"
🐶 Dog2 → "Dog 2"
ⓘ About → "About bottom menu"
📸 【圖 10:三隻貓咪作為 Bottom Navigation 圖示】
中間還有:
+
Floating Action Button。
事件:
floatingButton.setOnClickListener {
Toast.makeText(
this@MainActivity,
"Floating button is pressed",
Toast.LENGTH_SHORT
).show()
}
按下後:
Floating button is pressed
這就是 Android 常看到的:
+
FAB
今天看似做了很多不同元件,但其實核心概念只有一件事情:
可以整理成:
| UI | 適合用途 |
|---|---|
| Options Menu | 次要功能/設定 |
| SubMenu | 分類功能 |
| AlertDialog | 確認操作 |
| Custom Dialog | 自訂輸入 |
| Spinner | 下拉選擇 |
| NumberPicker | 數字/項目選擇 |
| Navigation Drawer | 大型功能分類 |
| Bottom Navigation | App 主要頁面 |
| DatePicker | 日期選擇 |
| TimePicker | 時間選擇 |
| FloatingActionButton | 主要快捷動作 |
| WebView | App 內顯示網頁 |
Android App
│
↓
Options Menu
│
┌────────────┼────────────┐
↓ ↓ ↓
Cat 1 Cat 2 Cat 3
│ │ │
↓ ↓ ↓
cat_1.jpg cat_2.jpg cat_3.jpg
│ │ │
└────────────┼────────────┘
↓
ImageView
↓
畫面立即切換圖片
而另一組 Navigation:
Bottom Navigation
│
┌────────────────┼────────────────┐
↓ ↓ ↓
🐱 🐶 ⓘ
Cats Dogs About
│
┌─────┼─────┐
↓ ↓ ↓
Cat1 Cat2 Cat3
這正好把今天最新加入的三張貓咪圖片實際帶進 Android Menu。
從最簡單的:
menu.add()
一路做到:
Options Menu
↓
SubMenu
↓
AlertDialog
↓
Intent
↓
Activity
再延伸:
Spinner
NumberPicker
Custom Dialog
WebView
最後進入真正 App 常用的:
Navigation Drawer
Bottom Navigation
FloatingActionButton
DatePicker
TimePicker
今天最大的收穫就是開始了解:
Android App 不只有 Activity,還需要一套好的 Navigation,才能讓使用者知道「我在哪裡、功能在哪裡、下一步要去哪裡」。
以前看到 Android App 左上角:
☰
或右上角:
⋮
以及下面:
🏠 🔍 + ❤️ 👤
只會覺得「這就是 App 的按鈕」。
但實際寫過之後,才發現背後其實分成:
Menu
SubMenu
NavigationView
DrawerLayout
BottomNavigationView
FloatingActionButton
而且這些元件最後都離不開我們前面學過的 Kotlin:
when
if / else
Boolean
Listener
Intent
Activity
Array
Adapter
所以 Kotlin 基礎不是學完就丟掉,而是開始一點一點組成真正的 Android App。
今天甚至把三張貓咪圖片:
cat_1.jpg
cat_2.jpg
cat_3.jpg
放進 Menu 的切換邏輯中。
從:
點 Cat 1
到:
R.id.menu_cat1
↓
when
↓
setImageResource()
↓
cat_1
↓
ImageView 顯示貓咪
這個過程,就是 Android UI「事件驅動」最直接的例子。
完成今天的 Menu 與 Navigation 後,接下來就可以把:
Menu
+
Activity
+
Intent
+
ListView / RecyclerView
+
實際資料
組成更完整的多頁 Android App。
Day 21 完成!🐱🐱🐱
這版是依你最新這一批程式重新整理:Menu 1 有程式動態建立 Options Menu、SubMenu、換圖、換色與 AlertDialog;Menu 2 改用 XML Menu,並從 Dialog 透過 Intent 進入 DisplayActivity。
另外,最新 Menu 3 確實有 cat_1、cat_2、cat_3 的選單切圖;後面的練習則一路涵蓋 Spinner/NumberPicker、Custom Dialog + WebView、Navigation Drawer、DatePicker/TimePicker 與 Bottom Navigation/FAB,所以我認為這次 Day 21 的主題用 **「Android 選單與 Navigation 大集合」**會比 BMI 更符合你今天真正的課程內容。