flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
166.25k stars 27.51k forks source link

[Android] Streamline Edge-to-Edge and Navigation Bar behavior across Android versions #90098

Closed TheJulianJES closed 2 years ago

TheJulianJES commented 3 years ago

Background

https://github.com/flutter/flutter/pull/81303 added support for the Edge to Edge fullscreen mode introduced in Android 10 (SDK 29). https://github.com/flutter/engine/pull/28616 limited the support for Edge to Edge to Android 10 (SDK 29) and higher.

In the following, "Android 10+" will refer to "Android 10 (SDK 29) and upwards".

Expectations

The guidelines over at https://developer.android.com/training/gestures/edge-to-edge show the following: image With Android 10+, apps should basically always set a transparent navigation bar color. Setting a SystemUiOverlayStyle in Flutter with systemNavigationBarColor: Colors.transparent and systemNavigationBarContrastEnforced: true (the enforced contrast already defaults to on) enables Android 10+ to (dynamically) show a scrim behind the navigation bar to make the buttons clearly visible. This can be seen on the left image.

When the user chooses gesture navigations, it's expected that apps do not show a a color/overlay behind the bar. This can be seen on the right image. (This is automatically done by Android.)

In both images, the transparent navigation bar color takes effect. In the first image, the white scrim is created by Android (as the transparent navigation bar color possibly couldn't provide enough contrast). In the second image (with gesture navigation), no overlay is shown (as the transparent navigation bar color fully takes effect and no overlay is needed with only the "gesture bar").

Reality

Currently, Flutter apps do the following by default: (Taken from: flutter/gallery#643) image This looks especially weird on devices with rounded corners when the user has gesture navigations on.

SDK >= 29

Here, darkMode defines if the user has currently selected a dark theme.

SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); // Enable Edge-to-Edge on Android 10+
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
  systemNavigationBarColor: Colors.transparent, // Setting a transparent navigation bar color
  systemNavigationBarContrastEnforced: true, // Default
  systemNavigationBarIconBrightness: darkMode ? Brightness.light : Brightness.dark, // This defines the color of the scrim
));

When this is called on Android 10+, the behavior is perfect and as expected:

However, when this is called on Android versions below 10 (below SDK 29), the following happens: While the Edge-to-Edge fullscreen mode will be ignored because of https://github.com/flutter/engine/pull/28616, it still causes the following issues:

SDK < 29

For everything older than Android 10 (below SDK 29), the following code produces the expected behavior: Here, darkMode defines if the user has currently selected a dark theme.

SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
  systemNavigationBarColor: darkMode ? Colors.black : Colors.white,
  systemNavigationBarIconBrightness: darkMode ? Brightness.light : Brightness.dark,
));

Like expected, if a light theme is shown, the user sees a white navigation bar with dark buttons (and the other way around). For Android 10+, the user gets the weird "navigation bar overlay" even when using gesture navigations (because edge-to-edge is not enabled). (-> the current "default" behavior in Flutter)

Combining?

One could make the assumption that the code can be combined without doing any Android version checking in Dart:

SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); // Enable Edge-to-Edge on Android 10+
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
  systemNavigationBarColor: Colors.transparent, // Setting a transparent navigation bar color
  systemNavigationBarContrastEnforced: true, // Default
  systemNavigationBarIconBrightness: darkMode ? Brightness.light : Brightness.dark, // This defines the color of the scrim
));

However, this causes issues on everything older than Android 10 (below SDK 29):

Correct Usage

The following code should produce expected behavior on all Android verisons.

Future<void> redoSystemStyle(bool darkMode) async {
  if (Platform.isAndroid) {
    final AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo;
    final bool edgeToEdge = androidInfo.version.sdkInt != null && androidInfo.version.sdkInt! >= 29;

    // The commented out check below isn't required anymore since https://github.com/flutter/engine/pull/28616 is merged
    // if (edgeToEdge)
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);

    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
      statusBarColor: Colors.transparent, // Not relevant to this issue
      systemNavigationBarColor: edgeToEdge
          ? Colors.transparent
          : darkMode
              ? Colors.black
              : Colors.white,
      systemNavigationBarContrastEnforced: true,
      systemNavigationBarIconBrightness: darkMode ? Brightness.light : Brightness.dark,
    ));
  } else {
    SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
      statusBarColor: Colors.transparent, // Not relevant to this issue
    ));
  }

Caveats:

Use case

Make it easier for users to implement the new Edge-to-Edge fullscreen behavior in Android 10 and onwards while maintaining proper (navigation bar) behavior on older SDK versions without much effort. Basically, it shouldn't be required to check the Android SDK version in Dart logic for the expected/default Android 10+ behavior (and not break on older Android versions).

Proposal

There are different possibilities to solve this issue:

Notes

Maintainers, feel free to edit/update this issue with with your ideas. To everyone else, I'll try to update this issue with your ideas/feedback to collect the "main ideas".

cc @Piinks

rydmike commented 3 years ago

Thank you @TheJulianJES this is an excellent summary of the current situation and improvement suggestions needed to make it even easier for Flutter developers to make apps that follow the Android guidelines for both current and older API levels. It covers all and then some topics I had noticed as well. πŸ‘πŸ» πŸ’™

droplet-js commented 3 years ago

it is work for me

        final SystemUiOverlayStyle light = SystemUiOverlayStyle(
          statusBarColor: Colors.transparent, // 23
          statusBarIconBrightness: Brightness.dark, // 23
          systemNavigationBarColor: Colors.transparent, // 27
          systemNavigationBarDividerColor: Colors.transparent.withAlpha(1) /* δΈθƒ½η”¨ε…¨ι€ζ˜Ž */, // 28
          systemNavigationBarIconBrightness: Brightness.dark, // 27
          systemNavigationBarContrastEnforced: false, // 29
        );
        SystemChrome.setSystemUIOverlayStyle(light);
rydmike commented 2 years ago

@v7lin I agree, that worked well for me up to Android10 API29 too.

However, in Android 11 API30 when I do basically what you do above, it get this:

The status bar and system nav bar icons are light and not dark as they should be.

Code for above example ```dart void main() { runApp(const MyApp()); } final AppBarTheme appBarLight = AppBarTheme( backgroundColor: Colors.white.withAlpha(0xEE), foregroundColor: Colors.black, elevation: 0.5, systemOverlayStyle: SystemUiOverlayStyle.dark.copyWith( statusBarColor: Colors.transparent, ), ); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Annotated Region Issue', theme: ThemeData.from(colorScheme: const ColorScheme.light()) .copyWith(appBarTheme: appBarLight), home: const MyHomePage(title: 'Annotated Region Issue'), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override Widget build(BuildContext context) { final MediaQueryData media = MediaQuery.of(context); final double topPadding = media.padding.top + kToolbarHeight; final double bottomPadding = media.padding.bottom; SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); return AnnotatedRegion( value: SystemUiOverlayStyle( systemNavigationBarColor: Colors.transparent, systemNavigationBarDividerColor: Colors.transparent.withAlpha(1), systemNavigationBarIconBrightness: Brightness.dark, systemNavigationBarContrastEnforced: false, ), child: Scaffold( extendBody: true, extendBodyBehindAppBar: true, appBar: AppBar( title: Text(title), ), body: GridView.builder( padding: EdgeInsets.fromLTRB(10, topPadding + 10, 10, bottomPadding + 10), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, mainAxisSpacing: 8, crossAxisSpacing: 8, childAspectRatio: 2, ), itemCount: 100, itemBuilder: (context, index) => Card( color: Colors.primaries[index % Colors.primaries.length][800]!, elevation: 2, child: Center( child: Text( 'Tile nr ${index + 1}', style: const TextStyle(color: Colors.white, fontSize: 16), ), ), ), ), ), ); } } ```

If I in this sample comment the line:

//        systemNavigationBarDividerColor: Colors.transparent.withAlpha(1),

I get the correct brightness on both the system navigation bar icons and the status bar icons also on Android11 API30. The impact on the status bars is very is very odd since the status bar icons are not even touched in this example in the AnnotatedRegion. The status bar is setup in this example via the AppBarTheme, as it should preferably be, imo.

This is quite strange and annoying. As mentioned, the above code works correctly on Android 10, API29 producing the dark status bar icons and system navigation bar icons even with the systemNavigationBarDividerColor line present.

I did not yet test how how Android12 API31 behaves in this case. I'm assuming it is like A11 API30, but the way these things changes, it is maybe not a safe assumption.

I should probably open a separate issue with this finding, but I feel a bit Flutter issue fatigue at the moment. I'll probably get back to it later...


EDIT: Made an own issue of it here: https://github.com/flutter/flutter/issues/100027

ping: @TheJulianJES @Piinks

droplet-js commented 2 years ago

@rydmike This is best practice

    <style name="Theme.App" parent="@android:style/Theme.Black.NoTitleBar">
        <item name="android:windowDrawsSystemBarBackgrounds" tools:targetApi="lollipop">true</item>
        <item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="p">shortEdges</item>
        <!-- SystemUiOverlayStyle -->
        <item name="android:statusBarColor" tools:targetApi="m">@android:color/transparent</item>
        <item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
        <item name="android:enforceStatusBarContrast" tools:targetApi="q">false</item>
        <item name="android:navigationBarColor" tools:targetApi="o_mr1">@android:color/transparent</item>
        <item name="android:navigationBarDividerColor" tools:targetApi="o_mr1">#01000000</item>
        <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>
        <item name="android:enforceNavigationBarContrast" tools:targetApi="q">false</item>
    </style>

    <style name="Theme.App.Dark" parent="Theme.App">
        <!-- SystemUiOverlayStyle -->
        <item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
        <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
    </style>

    <style name="LaunchTheme" parent="Theme.App">
        <!-- SplashScreen -->
        <item name="android:windowSplashScreenBackground" tools:targetApi="s">@android:color/white</item>
        <item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@android:color/transparent</item>
        <item name="android:windowSplashScreenBrandingImage" tools:targetApi="s">@drawable/branding_icon</item>

        <!-- Show a splash screen on the activity. Automatically removed when
             Flutter draws its first frame -->
        <item name="android:windowBackground">@drawable/launch_background</item>
    </style>

    <style name="NormalTheme" parent="Theme.App">
        <item name="android:windowBackground">@android:color/white</item>
    </style>
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.Window;

import androidx.annotation.Nullable;

import io.flutter.embedding.android.FlutterActivity;

public class MainActivity extends FlutterActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final Window window = getWindow();
        final View decorView = window.getDecorView();
        decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);// edgeToEdge
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            window.setStatusBarColor(Color.TRANSPARENT);
            decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
            window.setNavigationBarColor(Color.TRANSPARENT);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                window.setNavigationBarDividerColor(Color.parseColor("#01000000"));// δΈθƒ½η”¨ε…¨ι€ζ˜Ž
            }
            decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                window.setNavigationBarContrastEnforced(false);
            }
        }
    }
}
  runZonedGuarded(
    () async {
      WidgetsFlutterBinding.ensureInitialized();
      final SystemUiOverlayStyle dark = SystemUiOverlayStyle(
        statusBarColor: Colors.transparent /*Android=23*/,
        statusBarBrightness: Brightness.light /*iOS*/,
        statusBarIconBrightness: Brightness.dark /*Android=23*/,
        systemStatusBarContrastEnforced: false /*Android=29*/,
        systemNavigationBarColor: Colors.transparent /*Android=27*/,
        systemNavigationBarDividerColor: Colors.transparent.withAlpha(1) /*Android=28,δΈθƒ½η”¨ε…¨ι€ζ˜Ž */,
        systemNavigationBarIconBrightness: Brightness.dark /*Android=27*/,
        systemNavigationBarContrastEnforced: false /*Android=29*/,
      );
      SystemChrome.setSystemUIOverlayStyle(dark);
      await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
      runApp(Root());
    },
    (Object error, StackTrace stack) {
      // TODO
    },
    zoneSpecification: ZoneSpecification(
      print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
        // TODO
      },
    ),
  );
devchenli commented 2 years ago

Any update about SystemUiMode.edgeToEdge ? I want it to be the default option for android 10+. Thanks.

camsim99 commented 2 years ago

To address the components of the issue individually:

Any update about SystemUiMode.edgeToEdge ? I want it to be the default option for android 10+.

Making SystemUiMode.edgeToEdge the default mode for API 10+ would be a breaking change because it involves not only making the system bars transparent by default, but also having developers lay out their apps to accommodate the insets that will now be drawn in front of some of the app content.

This migration should be made to be consistent with Android and to avoid difficulty in using this mode for particular Android versions, though. In the meantime, I think it could be useful to provide guidance on how to achieve behavior that emulates the Android behavior across versions. @Piinks any thoughts on this?

SDK 26-28: Because the transparent navigation bar is still set, it will show a completely black navigation bar with black navigation bar buttons (which are completely invisible) if light mode is selected. This is behavior that needs to be fixed. (comment)

I'll take a look at this issue and update here.

This is quite strange and annoying. As mentioned, the above code works correctly on Android 10, API29 producing the dark status bar icons and system navigation bar icons even with the systemNavigationBarDividerColor line present. (comment)

This issue with systemNavigationBarDividerColor was fixed with PR #32167.

Piinks commented 2 years ago

provide guidance on how to achieve behavior that emulates the Android behavior across versions. @Piinks any thoughts on this?

There are probably some best practices here, but I do not know them myself. The framework does not distinguish between versions of TargetPlatform, and this API is not specific to Android either. I affects iOS as well IIRC.

I did see recent discussion on a similar issue that a plugin is probably the best approach. It could avoid the breaking change by having users decide to add it, and from there it can provide the automatic defaults that are being requested here.

camsim99 commented 2 years ago

There are two components included in this issue: one concerning setting edge-to-edge mode by default for API 29+ and the other concerning hidden navigation bar icons. I split them into two issues to continue discussion (see links above!) and am closing this issue.

github-actions[bot] commented 2 years ago

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.