Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cupertino in dark mode is too low-contrast and theme APIs are missing #80860

Open
mleonhard opened this issue Apr 21, 2021 · 11 comments
Open

Cupertino in dark mode is too low-contrast and theme APIs are missing #80860

mleonhard opened this issue Apr 21, 2021 · 11 comments
Labels
a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) f: cupertino flutter/packages/flutter/cupertino repository framework flutter/packages/flutter repository. See also f: labels. team-design Owned by Design Languages team triaged-design Triaged by Design Languages team

Comments

@mleonhard
Copy link
Contributor

mleonhard commented Apr 21, 2021

Use case

I'm writing an app with iOS style using Flutter's Cupertino library. I want the app to automatically switch between light and dark modes.

Proposal

  1. Fix the Cupertino widgets to be usable in dark mode.
  2. Add support to Flutter for dark mode themes.
  3. Provide a working example somewhere in the documentation.

Existing APIs with Missing or Undocumented Dark Mode Support

Mostly-Working Example

Problems in Dark Mode:

  • Disabled button text is unreadable.
  • Picker background is faint. It looks like a label, not a picker.
  • Slider track is very faint.
  • Edit box placeholder text is too faint to see.
  • Alert dialog border is invisible (Make CupertinoAlertDialog clearly visible in dark mode #80921)
  • Keyboard is too bright on Android.
code sample
// main.dart
import 'package:flutter/cupertino.dart'
    show
    showCupertinoDialog,
    CupertinoAlertDialog,
    CupertinoApp,
    CupertinoButton,
    CupertinoDatePicker,
    CupertinoDialogAction,
    CupertinoIcons,
    CupertinoNavigationBar,
    CupertinoPage,
    CupertinoPageScaffold,
    CupertinoPicker,
    CupertinoSlider,
    CupertinoSwitch,
    CupertinoTextField,
    CupertinoTheme,
    CupertinoThemeData;
import 'package:flutter/foundation.dart'
    show
    debugBrightnessOverride,
    kReleaseMode;
import 'package:flutter/widgets.dart'
    show
    runApp,
    Builder,
    BuildContext,
    Center,
    Color,
    Column,
    Container,
    DefaultTextStyle,
    EdgeInsets,
    Icon,
    Key,
    MainAxisSize,
    MediaQuery,
    MediaQueryData,
    Page,
    Row,
    Spacer,
    State,
    StatefulWidget,
    StatelessWidget,
    Text,
    TextAlign,
    TextEditingController,
    Navigator,
    Widget,
    WidgetsBinding,
    WidgetsBindingObserver;
import 'dart:ui' show Brightness;

// Copied from flutter/lib/src/widgets/app.dart:_MediaQueryFromWindow .
class MediaQueryFromWindow extends StatefulWidget {
  const MediaQueryFromWindow({Key key, this.child}) : super(key: key);

  final Widget child;

  @override
  MediaQueryFromWindowsState createState() => MediaQueryFromWindowsState();
}

class MediaQueryFromWindowsState extends State<MediaQueryFromWindow>
    with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void didChangeAccessibilityFeatures() {
    setState(() {});
  }

  @override
  void didChangeMetrics() {
    setState(() {});
  }

  @override
  void didChangeTextScaleFactor() {
    setState(() {});
  }

  @override
  void didChangePlatformBrightness() {
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    MediaQueryData data = MediaQueryData.fromWindow(
        WidgetsBinding.instance.window);
    if (!kReleaseMode) {
      data = data.copyWith(platformBrightness: debugBrightnessOverride);
    }
    return MediaQuery(
      data: data,
      child: widget.child,
    );
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
}

Page rootPage() =>
    CupertinoPage(
      title: 'Root Title',
      child: CupertinoPageScaffold(
        navigationBar: CupertinoNavigationBar(),
        child: Text('Root'),
      ),
    );

class CupertinoWidgetsPageWidget extends StatelessWidget {
  TextEditingController controller = new TextEditingController(
      text: "contents1");

  @override
  Widget build(BuildContext ctx) =>
      CupertinoPageScaffold(
        navigationBar: CupertinoNavigationBar(
          trailing: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              CupertinoButton(
                padding: EdgeInsets.symmetric(
                    horizontal: 4.0),
                child: Text(
                  'Disabled1',
                  textAlign: TextAlign.center,
                ),
              ),
              CupertinoButton(
                padding: EdgeInsets.symmetric(
                    horizontal: 4.0),
                onPressed: () => {},
                child: Text(
                  'Action1',
                  textAlign: TextAlign.center,
                ),
              ),

            ],
          ),
        ),
        child: Column(
          children: [
            Text('Text1'),
            Icon(
              CupertinoIcons.right_chevron,
              size: 24.0,
              semanticLabel: 'Chevron',
            ),
            CupertinoButton(
              child: Text('Enabled Button'),
              onPressed: () =>
                  showCupertinoDialog(
                    context: ctx,
                    builder: (BuildContext ctx) =>
                        CupertinoAlertDialog(
                          title: Text('Alert Title'),
                          content: Text('Content1'),
                          actions: [
                            CupertinoDialogAction(
                              isDefaultAction: false,
                              isDestructiveAction: false,
                              child: Text('Disabled1'),
                            ),
                            CupertinoDialogAction(
                              isDefaultAction: false,
                              isDestructiveAction: false,
                              onPressed: () =>
                                  Navigator.pop(
                                      ctx, false),
                              child: Text('Action1'),
                            ),
                            CupertinoDialogAction(
                              isDefaultAction: false,
                              isDestructiveAction: true,
                              onPressed: () =>
                                  Navigator.pop(
                                      ctx, false),
                              child: Text('Destructive1'),
                            ),
                            CupertinoDialogAction(
                              isDefaultAction: true,
                              isDestructiveAction: false,
                              onPressed: () =>
                                  Navigator.pop(
                                      ctx, false),
                              child: Text('Cancel'),
                            ),
                          ],
                        ),
                  ),
            ),
            CupertinoButton(
                child: Text('Disabled Button')),
            Row(children: [
              CupertinoSwitch(value: true),
              CupertinoSwitch(value: false),
              CupertinoSwitch(
                value: true,
                onChanged: (x) => {},
              ),
              CupertinoSwitch(
                value: false,
                onChanged: (x) => {},
              ),
            ]),
            CupertinoPicker(
              itemExtent: 30.0,
              onSelectedItemChanged: (x) => {},
              children: [
                Text('Item1'),
                Text('Item2'),
                Text('Item3'),
                Text('Item4'),
              ],
            ),
            CupertinoSlider(
              value: 0.0,
              divisions: 3,
              onChanged: (x) => {},
            ),
            Container(
              height: 100.0,
              child:
              CupertinoDatePicker(
                onDateTimeChanged: (x) => {},
              ),
            ),
            CupertinoTextField(
                placeholder: "placeholder1"),
            CupertinoTextField(
                controller: this.controller),
            Spacer(),
          ],
        ),
      );
}

Page cupertinoWidgetsPage() =>
    CupertinoPage(
        title: 'Title1',
        child: CupertinoWidgetsPageWidget());

class AppWidget extends StatelessWidget {
  @override
  Widget build(BuildContext ctx) {
    final isLight = MediaQuery
        .of(ctx)
        .platformBrightness == Brightness.light;
    final cupertinoThemeData = isLight ? CupertinoThemeData(
      // Light
      primaryColor: Color(0xff664600),
      barBackgroundColor: Color(0xffFFF0D0),
      scaffoldBackgroundColor: Color(0xffFFFFFF),
    ) : CupertinoThemeData(
      // Dark
      primaryColor: Color(0xffFFF0D0),
      barBackgroundColor: Color(0xff664600),
      // Has no effect on any Cupertino* widgets.
      primaryContrastingColor: Color(0xffFF0000),
    );
    return CupertinoApp(
      theme: cupertinoThemeData,
      title: 'App1',
      home: Builder(builder: (BuildContext ctx) =>
          DefaultTextStyle(
            style: CupertinoTheme
                .of(ctx)
                .textTheme
                .textStyle,
            child: Navigator(
              pages: [rootPage(), cupertinoWidgetsPage()],
              onPopPage: (route, result) => true,
            ),
          ),
      ),
    );
  }
}

void main() {
  runApp(MediaQueryFromWindow(child: AppWidget()));
}
iOS dark iOS light
Simulator Screen Shot - iPhone 11 - 2021-04-21 at 22 57 25 Simulator Screen Shot - iPhone 11 - 2021-04-21 at 22 58 17
Simulator Screen Shot - iPhone 11 - 2021-04-21 at 22 57 46 Simulator Screen Shot - iPhone 11 - 2021-04-21 at 22 58 23
Android dark Android light
Screenshot_1619071226 Screenshot_1619071175
Screenshot_1619071230 Screenshot_1619071188

EDIT 1: Mention MediaQuery.of(ctx).platformBrightness.

@TahaTesser TahaTesser added in triage Presently being triaged by the triage team d: cookbook d: examples Sample code and demos f: cupertino flutter/packages/flutter/cupertino repository framework flutter/packages/flutter repository. See also f: labels. and removed in triage Presently being triaged by the triage team labels Apr 21, 2021
@mleonhard
Copy link
Contributor Author

EDIT 2: Updated to use a copy of flutter/lib/src/widgets/app.dart:_MediaQueryFromWindow and then use a MediaQuery to pick CupertinoThemeData.

@HansMuller
Copy link
Contributor

CC @LongCatIsLooong

@mleonhard
Copy link
Contributor Author

I still need this.

@mleonhard mleonhard changed the title Document how to set up Cupertino light/dark colors Fix Cupertino widgets in dark mode or document workarounds Dec 14, 2021
@mleonhard mleonhard changed the title Fix Cupertino widgets in dark mode or document workarounds Make Cupertino widgets usable in dark mode or document workarounds Dec 14, 2021
@mleonhard
Copy link
Contributor Author

EDIT3: Change title and section headings to clearly state that this is a problem with the widget implementations, Flutter API, and missing documentation. Cupertino dark mode support is incomplete.

@workerbee22
Copy link

Very much support this, given iOS support for light and dark mode is no longer very new, Flutter should properly support iOS. Looking for feature parity between 'MaterialApp' and 'CupertinoApp' as well as the theme classes.

@synaptek-jhm
Copy link

Hoping this gets some attention. Would be very nice to have the same depth in CupertinoThemeData as we do in ThemeData.

@kevin-hv
Copy link

kevin-hv commented Sep 7, 2022

Same, would like to have this in Flutter

@synaptek-jhm
Copy link

@kevin-hv I've just resorted to forcing our app to light mode via the brightness:

static CupertinoThemeData get cupertinoLightTheme { return const CupertinoThemeData( brightness: Brightness.light,),}

Obviously not ideal, but saved me a ton of conditional logic and workarounds for the lack of Cupertino theme support.

@navaronbracke
Copy link
Contributor

@mleonhard Could we split this up into smaller subtasks and make this issue an umbrella issue?

I'd like to work on getting some of the things listed working if I can. (the first step would be to have a darkMode attribute in the CupertinoApp constructor)

@Hixie
Copy link
Contributor

Hixie commented Mar 24, 2023

This issue is very unlikely to get resolved in its current state because it is an umbrella issue and therefore diffuses responsibility. I recommend that people interested in seeing this resolved file distinct issues for each thing that needs to change, and then link to them from here.

@Hixie Hixie removed the d: cookbook label Mar 24, 2023
@gnprice gnprice changed the title Make Cupertino widgets usable in dark mode or document workarounds Cupertino in dark mode is too low-contrast and theme APIs are missing May 16, 2023
@gnprice gnprice added a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) and removed d: examples Sample code and demos documentation labels May 16, 2023
@gnprice
Copy link
Member

gnprice commented May 16, 2023

Adjusted title and labels because in general we fix bugs rather than document workarounds for them:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#lazy-programming
(It's not clear there even are workarounds for these that don't amount to just fixing the bug.)

In particular, all of the items under "Problems in Dark Mode" appear to be that something is too dark or too light. It's hard to evaluate without screenshots of iOS native widgets to compare to; but for one item (the alert dialog), #80921 makes clear that indeed the problem is too dark a dialog background causing too little contrast with the overall background.

(The next step remains, as @Hixie said above, for someone who wants to make progress on one or more of these issues to file them as separate issue threads.)

@flutter-triage-bot flutter-triage-bot bot added multiteam-retriage-candidate team-design Owned by Design Languages team labels Jul 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) f: cupertino flutter/packages/flutter/cupertino repository framework flutter/packages/flutter repository. See also f: labels. team-design Owned by Design Languages team triaged-design Triaged by Design Languages team
Projects
None yet
Development

No branches or pull requests

9 participants