ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Flutter] Android 15의 Edge-to-Edge 완벽 정복기
    IT 2025. 6. 23. 20:59

    최신 Android 15 업데이트와 함께 새롭게 도입된 Edge-to-Edge 기능은 몰입감 높은 사용자 경험을 제공할 수 있는 강력한 도구입니다. 하지만 이 기능을 제대로 길들이지 않으면 오히려 UI가 깨지는 골치 아픈 문제들을 마주하게 됩니다.

    저희 팀은 이번 업데이트에 대응하면서 상태 표시줄(Status Bar)과 네비게이션 바(Navigation Bar) 영역에서 여러 UI 문제를 겪었고, 이를 해결하기 위한 여정을 거쳤습니다. 이 글에서는 저희가 겪었던 문제들을 분석하고, 9단계에 걸쳐 체계적으로 해결한 과정과 그 최종 결과를 공유하고자 합니다.

     

    🚨 문제 상황: Edge-to-Edge가 던진 도전 과제

    Android 15의 Edge-to-Edge 기능을 활성화하자 다음과 같은 네 가지 주요 UI 문제가 발생했습니다.

    • 상태 표시줄과 콘텐츠 겹침: 홈 화면의 제목 텍스트가 상태 표시줄 아이콘들과 겹쳐 가독성이 심각하게 저하되었습니다.
    • 네비게이션 바 영역 침범: 화면 하단의 핵심 버튼들이 시스템 네비게이션 바 영역과 겹치면서 터치가 어렵거나 불가능해졌습니다.
    • BottomSheet UI 가려짐: 모달 다이얼로그와 바텀시트의 하단 버튼들이 제스처 네비게이션 영역에 가려져 사용자가 상호작용할 수 없는 문제가 있었습니다.
    • 상태 표시줄 아이콘 가시성: 투명한 상태 표시줄을 적용했을 때, 배경과 아이콘 색상이 비슷해져 아이콘이 거의 보이지 않는 문제가 발생했습니다.



    🔧 해결 과정: 9단계로 완성하는 UI 최적화

    이러한 문제들을 해결하기 위해 네이티브 코드부터 Flutter 위젯까지, 앱의 여러 계층을 아우르는 체계적인 접근 방식을 사용했습니다.

     

    1단계: 네이티브 스타일 기본 설정

    가장 먼저, Android 네이티브 레벨에서 Edge-to-Edge를 위한 기본 스타일을 styles.xml 파일에 정의했습니다.

    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
        <item name="android:windowBackground">?android:colorBackground</item>
        <item name="android:statusBarColor">@android:color/transparent</item>
        <item name="android:navigationBarColor">@android:color/transparent</item>
        <item name="android:windowLightStatusBar">false</item>
        <item name="android:windowLightNavigationBar">true</item>
        <item name="android:enforceStatusBarContrast">false</item>
        <item name="android:enforceNavigationBarContrast">false</item>
    </style>
    

    설명: statusBarColor와 navigationBarColor를 투명하게 만들어 앱 콘텐츠가 시스템 바 영역까지 확장될 수 있는 기반을 마련했습니다. 또한, enforce...Contrast 속성을 false로 설정하여 Android 15에서 강제로 대비를 조정하는 기능을 비활성화했습니다.

     

    2단계: MainActivity에서 Edge-to-Edge 명시적 활성화

    다음으로 MainActivity.kt 파일에서 Edge-to-Edge 모드를 코드로 직접 활성화하여 시스템 설정을 더욱 견고하게 만들었습니다.

    // android/app/src/main/kotlin/com/silverslab/ddallaemi/MainActivity.kt
    private fun setupEdgeToEdge() {
        try {
            // Edge-to-Edge 모드 활성화
            WindowCompat.setDecorFitsSystemWindows(window, false)
            
            // 상태 표시줄과 네비게이션 바를 투명하게 설정
            window.statusBarColor = android.graphics.Color.TRANSPARENT
            window.navigationBarColor = android.graphics.Color.TRANSPARENT
            
            // Android 15에서 강제 대비 비활성화
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                window.isStatusBarContrastEnforced = false
                window.isNavigationBarContrastEnforced = false
            }
            
            // 시스템 바 아이콘 색상 설정
            val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
            windowInsetsController.isAppearanceLightStatusBars = true
            windowInsetsController.isAppearanceLightNavigationBars = true
            
            println("📱 Android: Edge-to-Edge 설정 완료")
        } catch (e: Exception) {
            println("🚨 Android Edge-to-Edge 설정 실패: ${e.message}")
        }
    }
    

    설명: WindowCompat.setDecorFitsSystemWindows(window, false) 호출은 앱이 전체 화면을 사용하도록 만드는 핵심 코드입니다. WindowInsetsController를 사용해 시스템 아이콘의 색상을 제어할 수 있도록 준비했습니다.

     

    3단계: Flutter 시스템 UI 초기 설정

    앱이 시작되는 main.dart에서 Flutter 레벨의 시스템 UI 스타일을 정의하여 앱 전반에 일관된 정책을 적용했습니다.

    // lib/main.dart
    import 'package:flutter/services.dart';
    
    void main() async {
      WidgetsFlutterBinding.ensureInitialized();
    
      // Android 15 Edge-to-Edge 대응을 위한 시스템 UI 설정
      if (Platform.isAndroid) {
        try {
          SystemChrome.setSystemUIOverlayStyle(
            const SystemUiOverlayStyle(
              statusBarColor: Colors.transparent,
              statusBarIconBrightness: Brightness.dark,
              statusBarBrightness: Brightness.light,
              systemNavigationBarColor: Colors.transparent,
              systemNavigationBarIconBrightness: Brightness.dark,
              systemNavigationBarDividerColor: Colors.transparent,
            ),
          );
    
          // 시스템 UI 모드 설정 (Edge-to-Edge 활성화)
          SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
          
          print('🎵 Android 시스템 UI 및 오디오 설정 완료');
        } catch (e) {
          print('🚨 Android 시스템 설정 실패: $e');
        }
      }
      
      runApp(MyApp());
    }
    

    설명: SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge)를 통해 Flutter 앱에 Edge-to-Edge 모드를 명시적으로 활성화하고, setSystemUIOverlayStyle로 기본 아이콘 밝기 등을 설정했습니다.

     

    4단계: 핵심 화면에 SafeArea 적용

    콘텐츠가 시스템 UI와 겹치는 문제를 해결하기 위해, 메인 화면의 Scaffold를 SafeArea 위젯으로 감쌌습니다.

    // lib/screen/main/main_screen.dart
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        backgroundColor: Colors.white,
        body: SafeArea(
          top: true,    // 상단 SafeArea 활성화
          bottom: true, // 하단 SafeArea 활성화
          child: Stack(
            // ... 나머지 UI 구성 ...
          ),
        ),
      );
    }
    

    설명: SafeArea는 운영체제가 사용하는 영역(상태 표시줄, 노치, 네비게이션 바 등)을 자동으로 피해서 UI를 배치해주는 가장 확실하고 간단한 해결책입니다. top과 bottom을 true로 설정하여 상하단 영역을 모두 보호했습니다.

     

    5단계: 모든 BottomSheet에 SafeArea 적용

    하단에서 올라오는 모든 UI 요소(BottomSheet, Dialog) 또한 SafeArea로 감싸주어 네비게이션 바와의 충돌을 방지했습니다.

    // 예시: lib/screen/home/utils/home_alert_bottom_sheet.dart
    @override
    Widget build(BuildContext context) {
      return SafeArea(
        child: Container(
          // ... BottomSheet 내용 ...
        ),
      );
    }
    

    설명: 이를 통해 바텀시트의 버튼들이 제스처 네비게이션 영역에 가려지지 않고, 사용자가 모든 버튼을 정상적으로 터치할 수 있게 되었습니다.

     

    6단계: 홈 화면 구조 개선으로 브랜딩 강화

    홈 화면 상단의 보라색 배경이 상태 표시줄까지 자연스럽게 이어지도록 SafeArea의 구조를 개선했습니다.

    // lib/screen/home/home_screen.dart
    @override
    Widget build(BuildContext context) {
      return Container(
        color: Color(0xFF7959F8), // 보라색 배경
        child: SafeArea(
          bottom: false, // 하단은 메인 Scaffold에서 처리
          child: Container(
            color: Color(0xFFF4F4F4), // 콘텐츠 배경
            child: SingleChildScrollView(
              // ... 홈 화면 콘텐츠 ...
            ),
          ),
        ),
      );
    }
    

    설명: 배경색을 가진 Container를 SafeArea의 바깥에 배치하여, 상태 표시줄 영역까지 앱의 브랜드 컬러가 확장되도록 했습니다. SafeArea는 그 안의 콘텐츠만 보호하여 디자인과 기능성을 모두 잡았습니다.

     

    7단계: 페이지 전환 시 동적 상태 표시줄 관리

    각 탭 페이지의 배경색에 맞춰 상태 표시줄 아이콘 색상이 동적으로 변경되도록 관리 시스템을 구현했습니다.

    // lib/screen/main/main_screen.dart
    void _updateStatusBarForPage(int page) {
      // 홈화면(page 0)은 자체 스크롤 로직으로 관리하므로 제외
      if (page == 0) return;
      
      // 다른 모든 화면 (흰색 배경) -> 검은색 아이콘
      const SystemUiOverlayStyle style = SystemUiOverlayStyle(
        statusBarColor: Colors.transparent,
        statusBarIconBrightness: Brightness.dark,
        // ... 기타 설정 ...
      );
      
      SystemChrome.setSystemUIOverlayStyle(style);
    }
    

    설명: 탭이 변경될 때마다 _updateStatusBarForPage 함수를 호출하여, 해당 페이지의 배경에 맞는 SystemUiOverlayStyle을 동적으로 적용했습니다.

     

    8단계: 스크롤 기반 실시간 상태 표시줄 변경

    홈 화면에서는 스크롤 위치에 따라 배경색(보라색/흰색)이 바뀌므로, 그에 맞춰 상태 표시줄 아이콘 색상(흰색/검은색)이 실시간으로 변경되도록 구현했습니다.

    // lib/screen/home/home_screen.dart
    void _onScroll() {
      final double purpleAreaHeight = 268 - MediaQuery.of(context).padding.top;
      final double scrollOffset = widget.scrollController!.offset;
      
      // 스크롤 위치가 보라색 영역을 벗어났는지 확인
      bool shouldBeInWhiteArea = scrollOffset > purpleAreaHeight - 50;
      
      // 상태가 변경된 경우에만 UI 업데이트 (성능 최적화)
      if (_isInPurpleArea == shouldBeInWhiteArea) {
        _isInPurpleArea = !shouldBeInWhiteArea;
        _updateStatusBar(); // 아이콘 색상 변경 함수 호출
      }
    }
    
    void _updateStatusBar() {
      // _isInPurpleArea 값에 따라 흰색 또는 검은색 아이콘 스타일 적용
      final style = _isInPurpleArea
          ? const SystemUiOverlayStyle(statusBarIconBrightness: Brightness.light) // 보라 배경 -> 흰 아이콘
          : const SystemUiOverlayStyle(statusBarIconBrightness: Brightness.dark);  // 흰 배경 -> 검은 아이콘
      
      SystemChrome.setSystemUIOverlayStyle(style);
    }
    

    설명: ScrollController의 리스너를 활용해 스크롤 오프셋을 감지하고, 현재 배경색에 따라 적절한 상태 표시줄 스타일을 적용했습니다. 상태가 변경될 때만 setState와 유사한 SystemChrome을 호출하여 불필요한 리소-스 낭비를 막았습니다.

     

    9단계: 독립 화면 개별 설정

    탭 뷰에 속하지 않은 독립적인 화면(예: 채팅 화면)은 build 메서드 내에서 직접 상태 표시줄 스타일을 설정했습니다.

    // lib/screen/chat/chat_screen.dart
    @override
    Widget build(BuildContext context) {
      // 채팅 화면 진입 시 흰색 배경에 검은색 아이콘 설정
      SystemChrome.setSystemUIOverlayStyle(
        const SystemUiOverlayStyle(
          statusBarColor: Colors.white,
          statusBarIconBrightness: Brightness.dark,
          // ... 기타 설정 ...
        ),
      );
      
      return Container(
          // ... 채팅 화면 UI ...
      );
    }
    

    설명: 각 화면의 특성에 맞는 스타일을 build 시점에 적용하여, 화면에 진입하는 순간부터 일관성 있는 UI를 제공하도록 했습니다.

     

    ✅ 최종 결과 및 UI 개선 효과

    이러한 단계적인 해결 과정을 통해 모든 UI 문제를 해결하고, 완벽한 Edge-to-Edge 경험을 구현했습니다.

    화면상태 표시줄 배경아이콘 색상동작 방식
    홈 (보라색 영역) 투명 흰색 스크롤 기반 동적 변경
    홈 (흰색 영역) 투명 검은색 스크롤 기반 동적 변경
    일정, 와우보드, MY 투명 검은색 탭 변경 시 설정
    채팅 화면 흰색 검은색 화면 진입 시 설정
     

    주요 개선 결과:

    • 완벽한 Edge-to-Edge 구현: 모든 화면에서 앱 콘텐츠가 시스템 바 영역까지 자연스럽게 확장됩니다.
    • 안전한 UI 영역 보장: SafeArea 적용으로 모든 UI 요소가 터치 가능한 안전한 영역에 위치합니다.
    • 최적의 가시성 확보: 배경색에 따른 동적 아이콘 색상 변경으로 항상 최적의 가독성을 제공합니다.
    • 일관된 사용자 경험: 화면 전환 시에도 자연스럽고 일관된 상태 표시줄 동작을 구현했습니다.

     

    🎯 기대 효과: 단순한 버그 수정을 넘어서

    이번 개선 작업은 단순히 UI 버그를 수정하는 것을 넘어 다음과 같은 긍정적인 효과를 가져왔습니다.

    • 사용자 경험 향상: Edge-to-Edge 디자인은 사용자에게 더욱 현대적이고 몰입감 높은 경험을 제공하며, 모든 상호작용 요소가 안전한 터치 영역에 위치하여 편의성이 크게 향상되었습니다.
    • 기술적 이점: 최신 Android 버전에 완벽히 대응하여 미래 호환성을 확보했으며, 체계적인 관리 시스템 덕분에 향후 유지보수가 용이해졌습니다.
    • 브랜딩 효과: 앱의 메인 컬러가 상태 표시줄까지 확장되어 브랜드 아이덴티티가 강화되었고, 최신 디자인 트렌드를 반영한 세련된 인터페이스는 앱의 품질 인식을 높였습니다.

    이러한 종합적인 개선을 통해 Android 15의 새로운 기능을 완벽히 활용하면서도 사용자에게 최적의 경험을 제공하는 앱을 완성할 수 있었습니다.

     

    https://developer.android.com/develop/ui/views/layout/edge-to-edge?hl=ko

     

    뷰에서 더 넓은 화면에 콘텐츠 표시  |  Views  |  Android Developers

    이 페이지는 Cloud Translation API를 통해 번역되었습니다. 뷰에서 더 넓은 화면에 콘텐츠 표시 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose 사용해 보기

    developer.android.com

     

Designed by Tistory.