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) | 다크모드 색상 |