[카테고리:] 미분류

  • 🎯 Floating Tab Menu 구현 세미나

    Android에서 BottomAppBar + FloatingActionButton을 활용하여 돋보이는 중앙 탭 메뉴를 구현하는 방법


    📋 목차

    1. 개요
    2. 전체 아키텍처
    3. 핵심 코드 분석
    1. 핵심 기술 포인트
    2. 실습 과제

    개요

    이 세미나에서는 일반 탭 메뉴와 조화를 이루면서도 특별히 돋보이는 중앙 플로팅 탭을 구현하는 방법을 학습합니다.

    🎨 구현 결과물

    • 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 조합

    구성요소역할
    CoordinatorLayoutFAB와 BottomAppBar 간의 상호작용 조율
    BottomAppBarCradle(오목한 홈) 효과 제공
    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)다크모드 색상