Android์์ BottomAppBar + FloatingActionButton์ ํ์ฉํ์ฌ ๋๋ณด์ด๋ ์ค์ ํญ ๋ฉ๋ด๋ฅผ ๊ตฌํํ๋ ๋ฐฉ๋ฒ
๐ ๋ชฉ์ฐจ
๊ฐ์
์ด ์ธ๋ฏธ๋์์๋ ์ผ๋ฐ ํญ ๋ฉ๋ด์ ์กฐํ๋ฅผ ์ด๋ฃจ๋ฉด์๋ ํน๋ณํ ๋๋ณด์ด๋ ์ค์ ํ๋กํ ํญ์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ํ์ตํฉ๋๋ค.
๐จ ๊ตฌํ ๊ฒฐ๊ณผ๋ฌผ
- 4๊ฐ์ ์ผ๋ฐ ํญ (๋ด์ค, ํ, ๊ธฐ๋ก, ์ค์ )
- 1๊ฐ์ ํ๋กํ ์ค์ ํญ (๋ง์ดํฌ – ๋ค์จ ๊ทธ๋ฆฐ/๋ธ๋ฃจ ์์)
- BottomAppBar์ Cradle(์ค๋ชฉํ ํ) ํจ๊ณผ
์ ์ฒด ์ํคํ ์ฒ
graph TB
subgraph Layout["activity_main.xml"]
A[CoordinatorLayout] --> B[BottomAppBar]
A --> C[FloatingActionButton]
B --> D[LinearLayout - ํญ ์ปจํ
์ด๋]
D --> E[๋ด์ค ํญ]
D --> F[ํ ํญ]
D --> G["๋น ๊ณต๊ฐ (FAB ์๋ฆฌ)"]
D --> H[๊ธฐ๋ก ํญ]
D --> I[์ค์ ํญ]
end
subgraph Kotlin["MainActivity.kt"]
J[setupNavTabs] --> K[ํญ ํด๋ฆญ ๋ฆฌ์ค๋ ์ค์ ]
L[setupFabMic] --> M[FAB ์์ ๋ฐ ํด๋ฆญ ์ค์ ]
N[updateTabSelection] --> O[์ ํ ์ํ UI ์
๋ฐ์ดํธ]
end
C -.->|layout_anchor| B
ํต์ฌ ์ฝ๋ ๋ถ์
1. ๋ ์ด์์ ๊ตฌ์กฐ (activity_main.xml)
๐ BottomAppBar + FAB Cradle ํจ๊ณผ
<!-- Bottom App Bar with cradle for FAB -->
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottomAppBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:backgroundTint="?attr/colorSurface"
app:fabAlignmentMode="center"
app:fabAnimationMode="slide"
app:fabCradleMargin="8dp"
app:fabCradleRoundedCornerRadius="16dp"
app:fabCradleVerticalOffset="4dp"
app:hideOnScroll="false"
app:elevation="8dp">
[!IMPORTANT]
Cradle ํจ๊ณผ ํต์ฌ ์์ฑ๋ค
fabAlignmentMode="center"– FAB๋ฅผ ์ค์์ ๋ฐฐ์นfabCradleMargin="8dp"– FAB์ ๋ฐ ์ฌ์ด ๊ฐ๊ฒฉfabCradleRoundedCornerRadius="16dp"– ์ค๋ชฉํ ๊ณก์ ๋ฐ๊ฒฝfabCradleVerticalOffset="4dp"– FAB ์์ง ๋์ถ ์ ๋
๐ ํญ ์ปจํ ์ด๋ ๊ตฌ์กฐ (weightSum ํ์ฉ)
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:weightSum="4.8">
<!-- ๋ด์ค ํญ - weight 1 -->
<LinearLayout
android:id="@+id/navNews"
android:layout_width="0dp"
android:layout_weight="1"
... >
</LinearLayout>
<!-- ํ ํญ - weight 1 -->
<LinearLayout android:id="@+id/navHome" android:layout_weight="1" ... />
<!-- FAB ๊ณต๊ฐ - weight 0.8 (์๊ฒ ์ค์ ) -->
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.8" />
<!-- ๊ธฐ๋ก ํญ - weight 1 -->
<LinearLayout android:id="@+id/navHistory" android:layout_weight="1" ... />
<!-- ์ค์ ํญ - weight 1 -->
<LinearLayout android:id="@+id/navSettings" android:layout_weight="1" ... />
</LinearLayout>
[!TIP]
weightSum = 4.8 ๊ณ์ฐ
- ์ผ๋ฐ ํญ 4๊ฐ ร 1 = 4
- FAB ๊ณต๊ฐ = 0.8
- ์ดํฉ = 4.8
๐ FloatingActionButton ์ค์
<!-- Floating Action Button for MIC (center, elevated) -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabMic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_mic_neon_tab"
android:contentDescription="์์ฑ ์
๋ ฅ"
app:layout_anchor="@id/bottomAppBar"
app:tint="@null"
app:backgroundTint="#00FF88"
app:fabSize="normal"
app:elevation="12dp"
app:borderWidth="0dp" />
[!IMPORTANT]
FAB ํต์ฌ ์์ฑ
app:layout_anchor="@id/bottomAppBar"– BottomAppBar์ ๊ณ ์ app:tint="@null"– ์์ด์ฝ ์๋ณธ ์์ ์ ์ง (๋ค์จ ํจ๊ณผ)app:elevation="12dp"– ๋์ elevation์ผ๋ก ๋๋ณด์ด๊ฒ ํจ
2. ๋ค์จ ์์ด์ฝ ๋์์ธ
๐ res/drawable/ic_mic_neon_tab.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- Microphone body with neon gradient effect -->
<path
android:fillColor="#00FF88"
android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3z"/>
<!-- Microphone stand with neon blue -->
<path
android:fillColor="#00BFFF"
android:pathData="M17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
</vector>
[!TIP]
๋ค์จ ์ปฌ๋ฌ ์กฐํฉ
- ๋ง์ดํฌ ๋ณธ์ฒด:
#00FF88(๋ค์จ ๊ทธ๋ฆฐ)- ๋ง์ดํฌ ์คํ ๋:
#00BFFF(๋ค์จ ๋ธ๋ฃจ)- ์ด ์กฐํฉ์ผ๋ก ๋์ ๋๋ฉด์๋ ์ธ๋ จ๋ ๋๋ ์ฐ์ถ
3. Kotlin ๋ก์ง (MainActivity.kt)
๐ NavTab ๋ฐ์ดํฐ ํด๋์ค
// Navigation tabs
private data class NavTab(
val container: LinearLayout,
val icon: ImageView,
val label: TextView,
var destinationId: Int
)
private lateinit var navTabs: List<NavTab>
private var currentDestinationId: Int = R.id.navigation_home
// Neon colors for MIC FAB
private val neonGreen = Color.parseColor("#00FF88")
private val neonBlue = Color.parseColor("#00BFFF")
๐ ํญ ์ด๊ธฐํ ํจ์
private fun setupNavTabs() {
val homeDestinationId = if (isRealTimeMode)
R.id.navigation_dashboard else R.id.navigation_home
navTabs = listOf(
NavTab(binding.navNews, binding.iconNews,
binding.labelNews, R.id.navigation_news),
NavTab(binding.navHome, binding.iconHome,
binding.labelHome, homeDestinationId),
NavTab(binding.navHistory, binding.iconHistory,
binding.labelHistory, R.id.navigation_history),
NavTab(binding.navSettings, binding.iconSettings,
binding.labelSettings, R.id.navigation_notifications)
)
// ๊ฐ ํญ์ ํด๋ฆญ ๋ฆฌ์ค๋ ์ค์
navTabs.forEach { tab ->
tab.container.setOnClickListener {
navigateTo(tab.destinationId)
}
}
}
๐ FAB ์ค์ ํจ์
private fun setupFabMic() {
binding.fabMic.apply {
backgroundTintList = ColorStateList.valueOf(neonGreen)
imageTintList = ColorStateList.valueOf(Color.WHITE)
setOnClickListener {
navigateTo(R.id.navigation_dashboard)
}
}
}
๐ ์ ํ ์ํ ์ ๋ฐ์ดํธ (ํต์ฌ ๋ก์ง)
private fun updateTabSelection(selectedId: Int) {
currentDestinationId = selectedId
// ํ
๋ง์์ ์์ ๊ฐ์ ธ์ค๊ธฐ
val typedValue = android.util.TypedValue()
theme.resolveAttribute(android.R.attr.colorPrimary, typedValue, true)
val primaryColor = typedValue.data
theme.resolveAttribute(
com.google.android.material.R.attr.colorOnSurface,
typedValue, true)
val onSurfaceColor = typedValue.data
val grayColor = Color.argb(153,
Color.red(onSurfaceColor),
Color.green(onSurfaceColor),
Color.blue(onSurfaceColor))
// ์ผ๋ฐ ํญ ์ํ ์
๋ฐ์ดํธ
navTabs.forEach { tab ->
val isSelected = tab.destinationId == selectedId
val color = if (isSelected) primaryColor else grayColor
tab.icon.imageTintList = ColorStateList.valueOf(color)
tab.label.setTextColor(color)
}
// FAB ์ํ ์
๋ฐ์ดํธ (์ ํ ์ ํ๋ ํจ๊ณผ)
val isMicSelected = selectedId == R.id.navigation_dashboard
binding.fabMic.apply {
if (isMicSelected) {
backgroundTintList = ColorStateList.valueOf(neonGreen)
scaleX = 1.1f // 10% ํ๋
scaleY = 1.1f
} else {
backgroundTintList = ColorStateList.valueOf(
Color.parseColor("#00CC6A")) // ์ฝ๊ฐ ์ด๋์ด ํค
scaleX = 1.0f
scaleY = 1.0f
}
}
}
[!NOTE]
์ ํ ์ FAB ํจ๊ณผ
- ์ ํ ์: ๋ฐ์ ๋ค์จ ๊ทธ๋ฆฐ
#00FF88+ 1.1๋ฐฐ ํ๋- ๋น์ ํ ์: ์ฝ๊ฐ ์ด๋์ด
#00CC6A+ ์๋ ํฌ๊ธฐ
4. ์์ ๋ฐ ์คํ์ผ๋ง
๐ res/values/colors.xml (๋ผ์ดํธ ๋ชจ๋)
<!-- Bottom Nav Unselected Color -->
<color name="bottom_nav_icon_unselected">#43474E</color>
๐ res/values-night/colors.xml (๋คํฌ ๋ชจ๋)
<!-- Bottom Nav Unselected Color for Dark Theme -->
<color name="bottom_nav_icon_unselected">#C3C7CF</color>
๐ res/color/selector_bottom_nav_icon.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Selected state -->
<item android:color="?attr/colorPrimary" android:state_checked="true" />
<!-- Unselected state (default) -->
<item android:color="@color/bottom_nav_icon_unselected" />
</selector>
ํต์ฌ ๊ธฐ์ ํฌ์ธํธ
1๏ธโฃ CoordinatorLayout + BottomAppBar ์กฐํฉ
| ๊ตฌ์ฑ์์ | ์ญํ |
|---|---|
CoordinatorLayout | FAB์ BottomAppBar ๊ฐ์ ์ํธ์์ฉ ์กฐ์จ |
BottomAppBar | Cradle(์ค๋ชฉํ ํ) ํจ๊ณผ ์ ๊ณต |
FloatingActionButton | ์ค์์ ๋๋ณด์ด๋ ๋ฒํผ |
2๏ธโฃ weight ์์คํ ์ ํ์ฉํ ๊ท ๋ฑ ๋ฐฐ์น
weightSum = 4.8
โโโ ๋ด์ค (1.0) โโฌโ ํ (1.0) โโฌโ FAB๊ณต๊ฐ (0.8) โโฌโ ๊ธฐ๋ก (1.0) โโฌโ ์ค์ (1.0)
โ โ โ โ
20.8% 20.8% 16.7% 20.8% 20.8%
3๏ธโฃ ํ ๋ง ์ ์ํ ์์ ์ฒ๋ฆฌ
// ๋ฐํ์์ ํ
๋ง ์์ ๊ฐ์ ธ์ค๊ธฐ
theme.resolveAttribute(android.R.attr.colorPrimary, typedValue, true)
val primaryColor = typedValue.data
4๏ธโฃ ์ ํ ์ํ ์๊ฐ์ ํผ๋๋ฐฑ
| ์ํ | FAB ์์ | FAB ํฌ๊ธฐ |
|---|---|---|
| ์ ํ๋จ | #00FF88 (๋ฐ์ ๋ค์จ ๊ทธ๋ฆฐ) | 1.1x |
| ๋น์ ํ | #00CC6A (์ด๋์ด ๊ทธ๋ฆฐ) | 1.0x |
์ค์ต ๊ณผ์
๊ณผ์ 1: ๊ธฐ๋ณธ ๊ตฌํ
BottomAppBar + FAB ์กฐํฉ์ผ๋ก 5๊ฐ ํญ ๋ค๋น๊ฒ์ด์ ๊ตฌํํ๊ธฐ
๊ณผ์ 2: ์ ๋๋ฉ์ด์ ์ถ๊ฐ
FAB ์ ํ ์ ObjectAnimator๋ฅผ ํ์ฉํ์ฌ ๋ถ๋๋ฌ์ด ํ๋/์ถ์ ์ ๋๋ฉ์ด์
์ ์ฉ
๊ณผ์ 3: ๋ค์จ ํจ๊ณผ ํ์ฅ
- ์ ํ ์ FAB์ ๊ทธ๋ฆผ์ ์์ ๋ณ๊ฒฝ (
app:elevation+ ์ปค์คํ ๊ทธ๋ฆผ์) - ๋๋ ์ธ๊ณฝ์ (stroke) ๋ค์จ ํจ๊ณผ ์ถ๊ฐ
๐ ๊ด๋ จ ํ์ผ ๋ชฉ๋ก
| ํ์ผ ๊ฒฝ๋ก | ์ค๋ช |
|---|---|
| activity_main.xml | ๋ฉ์ธ ๋ ์ด์์ (BottomAppBar + FAB) |
| MainActivity.kt | ํญ ์ปจํธ๋กค ๋ก์ง |
| ic_mic_neon_tab.xml | ๋ค์จ ๋ง์ดํฌ ์์ด์ฝ |
| selector_bottom_nav_icon.xml | ํญ ์์ด์ฝ ์์ ์ ๋ ํฐ |
| colors.xml | ์์ ์ ์ |
| colors.xml (night) | ๋คํฌ๋ชจ๋ ์์ |
๋ต๊ธ ๋จ๊ธฐ๊ธฐ