From 5a641be96f665e5e5feec62a42df0e57533c8204 Mon Sep 17 00:00:00 2001 From: Ian VanSchooten Date: Thu, 23 Jan 2025 10:49:32 -0500 Subject: [PATCH] Switch to Material 3 for Android (#224) This exploration updates the Android app to use Google's Material 3 design, and changes up the colors a bit in the process. I used a source color of #5D23DD in https://material-foundation.github.io/material-theme-builder/, which generates a full set of material colors that I imported into `lib/services/theme.dart`. It should be easy to tweak any of the colors that we want, vs previously it was somewhat more difficult because they were being generated "behind the scenes". This doesn't necessarily need to be done now, but it does align better with more modern Android design patterns and Flutter has said at some point they will stop supporting Material 2. Note: this should not have any impact on iOS, since we use the default `CupertinoThemeData` there, without any custom theming. Here are some before/after comparisons of interesting screens: Adding a new site: |Dark Before|Dark After|Light Before|Light After| |---|---|---|--- |Android Studio 2025-01-22 10 43 55|Android Studio 2025-01-22 11 16 48|image|image| Confirmation dialog: |Before|After| |---|---| |image|Android Studio 2025-01-22 11 43 19| Settings page: |Dark Before|Dark After|Light Before|Light After| |---|---|---|---| |Android Studio 2025-01-22 11 46 01|Android Studio 2025-01-22 11 46 35|Android Studio 2025-01-22 11 46 09|Android Studio 2025-01-22 11 46 28| Site with errors: |Dark Before|Dark After|Light Before|Light After| |---|---|---|---| |Android Studio 2025-01-22 11 48 50|Android Studio 2025-01-22 11 48 06|image|Android Studio 2025-01-22 11 48 20| Main page / site list: |Light Before|Light After|Light Scrolled Before|Light Scrolled After| |---|---|---|---| |Android Studio 2025-01-22 11 53 07|Android Studio 2025-01-22 11 53 41|Android Studio 2025-01-22 11 53 22|Android Studio 2025-01-22 11 53 51| Certificate: |Dark Before|Dark After|Light Before|Light After| |---|---|---|---| |Android Studio 2025-01-22 11 57 25|Android Studio 2025-01-22 11 56 44|Android Studio 2025-01-22 11 57 01|Android Studio 2025-01-22 11 56 21| --- lib/components/DangerButton.dart | 4 +- lib/main.dart | 41 +--- lib/services/theme.dart | 389 +++++++++++++++++++++++++++++++ lib/services/utils.dart | 24 +- pubspec.lock | 8 + pubspec.yaml | 1 + 6 files changed, 426 insertions(+), 41 deletions(-) create mode 100644 lib/services/theme.dart diff --git a/lib/components/DangerButton.dart b/lib/components/DangerButton.dart index 4279c5e..52e30aa 100644 --- a/lib/components/DangerButton.dart +++ b/lib/components/DangerButton.dart @@ -15,7 +15,9 @@ class DangerButton extends StatelessWidget { return FilledButton( onPressed: onPressed, child: child, - style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error)); + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + foregroundColor: Theme.of(context).colorScheme.onError)); } else { // Workaround for https://github.com/flutter/flutter/issues/161590 final themeData = CupertinoTheme.of(context); diff --git a/lib/main.dart b/lib/main.dart index ad222c6..758e276 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,7 @@ import 'dart:async'; -import 'package:flutter/cupertino.dart' show CupertinoThemeData, DefaultCupertinoLocalizations, CupertinoColors; -import 'package:flutter/material.dart' - show BottomSheetThemeData, ColorScheme, Colors, DefaultMaterialLocalizations, ThemeData, ThemeMode; +import 'package:flutter/cupertino.dart' show CupertinoThemeData, DefaultCupertinoLocalizations; +import 'package:flutter/material.dart' show DefaultMaterialLocalizations, TextTheme, ThemeMode; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -11,6 +10,8 @@ import 'package:mobile_nebula/screens/MainScreen.dart'; import 'package:mobile_nebula/screens/EnrollmentScreen.dart'; import 'package:mobile_nebula/services/settings.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; +import 'package:mobile_nebula/services/theme.dart'; +import 'package:mobile_nebula/services/utils.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; Future main() async { @@ -85,36 +86,8 @@ class _AppState extends State { @override Widget build(BuildContext context) { - final ThemeData lightTheme = ThemeData( - useMaterial3: false, - colorScheme: ColorScheme.fromSwatch( - brightness: Brightness.light, - primarySwatch: Colors.blueGrey, - errorColor: CupertinoColors.systemRed.resolveFrom(context), - ), - primaryColor: Colors.blueGrey[900], - fontFamily: 'PublicSans', - //scaffoldBackgroundColor: Colors.grey[100], - scaffoldBackgroundColor: Colors.white, - bottomSheetTheme: BottomSheetThemeData( - backgroundColor: Colors.blueGrey[50], - ), - ); - - final ThemeData darkTheme = ThemeData( - useMaterial3: false, - colorScheme: ColorScheme.fromSwatch( - brightness: Brightness.dark, - primarySwatch: Colors.grey, - errorColor: CupertinoColors.systemRed.resolveFrom(context), - ), - primaryColor: Colors.grey[900], - fontFamily: 'PublicSans', - scaffoldBackgroundColor: Colors.grey[800], - bottomSheetTheme: BottomSheetThemeData( - backgroundColor: Colors.grey[850], - ), - ); + TextTheme textTheme = Utils.createTextTheme(context, "Public Sans", "Public Sans"); + MaterialTheme theme = MaterialTheme(textTheme); return PlatformProvider( settings: PlatformSettingsData(iosUsesMaterialWidgets: true), @@ -129,7 +102,7 @@ class _AppState extends State { material: (_, __) { return new MaterialAppData( themeMode: brightness == Brightness.light ? ThemeMode.light : ThemeMode.dark, - theme: brightness == Brightness.light ? lightTheme : darkTheme, + theme: brightness == Brightness.light ? theme.light() : theme.dark(), ); }, cupertino: (_, __) => CupertinoAppData( diff --git a/lib/services/theme.dart b/lib/services/theme.dart new file mode 100644 index 0000000..44f456a --- /dev/null +++ b/lib/services/theme.dart @@ -0,0 +1,389 @@ +import "package:flutter/material.dart"; + +// Originally generated by https://material-foundation.github.io/material-theme-builder/ +// from a source color of #5D23DD + +class MaterialTheme { + final TextTheme textTheme; + + const MaterialTheme(this.textTheme); + + static ColorScheme lightScheme() { + return const ColorScheme( + brightness: Brightness.light, + primary: Color(4284700303), + surfaceTint: Color(4284700303), + onPrimary: Color(4294967295), + primaryContainer: Color(4293451519), + onPrimaryContainer: Color(4283121270), + secondary: Color(4284570481), + onSecondary: Color(4294967295), + secondaryContainer: Color(4293385976), + onSecondaryContainer: Color(4282991704), + tertiary: Color(4286403169), + onTertiary: Color(4294967295), + tertiaryContainer: Color(4294957540), + onTertiaryContainer: Color(4284693322), + error: Color(4290386458), + onError: Color(4294967295), + errorContainer: Color(4294957782), + onErrorContainer: Color(4287823882), + surface: Color(4294834175), + onSurface: Color(4280032032), + onSurfaceVariant: Color(4282926414), + outline: Color(4286150015), + outlineVariant: Color(4291478735), + shadow: Color(4278190080), + scrim: Color(4278190080), + inverseSurface: Color(4281478965), + inversePrimary: Color(4291673599), + primaryFixed: Color(4293451519), + onPrimaryFixed: Color(4280225864), + primaryFixedDim: Color(4291673599), + onPrimaryFixedVariant: Color(4283121270), + secondaryFixed: Color(4293385976), + onSecondaryFixed: Color(4280097067), + secondaryFixedDim: Color(4291544028), + onSecondaryFixedVariant: Color(4282991704), + tertiaryFixed: Color(4294957540), + onTertiaryFixed: Color(4281405726), + tertiaryFixedDim: Color(4293834953), + onTertiaryFixedVariant: Color(4284693322), + surfaceDim: Color(4292729056), + surfaceBright: Color(4294834175), + surfaceContainerLowest: Color(4294967295), + surfaceContainerLow: Color(4294439674), + surfaceContainer: Color(4294110452), + surfaceContainerHigh: Color(4293715694), + surfaceContainerHighest: Color(4293321193), + ); + } + + ThemeData light() { + return theme(lightScheme()); + } + + static ColorScheme lightMediumContrastScheme() { + return const ColorScheme( + brightness: Brightness.light, + primary: Color(4282002788), + surfaceTint: Color(4284700303), + onPrimary: Color(4294967295), + primaryContainer: Color(4285686943), + onPrimaryContainer: Color(4294967295), + secondary: Color(4281873223), + onSecondary: Color(4294967295), + secondaryContainer: Color(4285557376), + onSecondaryContainer: Color(4294967295), + tertiary: Color(4283444025), + onTertiary: Color(4294967295), + tertiaryContainer: Color(4287455344), + onTertiaryContainer: Color(4294967295), + error: Color(4285792262), + onError: Color(4294967295), + errorContainer: Color(4291767335), + onErrorContainer: Color(4294967295), + surface: Color(4294834175), + onSurface: Color(4279373846), + onSurfaceVariant: Color(4281873725), + outline: Color(4283715930), + outlineVariant: Color(4285492085), + shadow: Color(4278190080), + scrim: Color(4278190080), + inverseSurface: Color(4281478965), + inversePrimary: Color(4291673599), + primaryFixed: Color(4285686943), + onPrimaryFixed: Color(4294967295), + primaryFixedDim: Color(4284042373), + onPrimaryFixedVariant: Color(4294967295), + secondaryFixed: Color(4285557376), + onSecondaryFixed: Color(4294967295), + secondaryFixedDim: Color(4283912807), + onSecondaryFixedVariant: Color(4294967295), + tertiaryFixed: Color(4287455344), + onTertiaryFixed: Color(4294967295), + tertiaryFixedDim: Color(4285679960), + onTertiaryFixedVariant: Color(4294967295), + surfaceDim: Color(4291478989), + surfaceBright: Color(4294834175), + surfaceContainerLowest: Color(4294967295), + surfaceContainerLow: Color(4294439674), + surfaceContainer: Color(4293715694), + surfaceContainerHigh: Color(4292926435), + surfaceContainerHighest: Color(4292202712), + ); + } + + ThemeData lightMediumContrast() { + return theme(lightMediumContrastScheme()); + } + + static ColorScheme lightHighContrastScheme() { + return const ColorScheme( + brightness: Brightness.light, + primary: Color(4281344857), + surfaceTint: Color(4284700303), + onPrimary: Color(4294967295), + primaryContainer: Color(4283252856), + onPrimaryContainer: Color(4294967295), + secondary: Color(4281215292), + onSecondary: Color(4294967295), + secondaryContainer: Color(4283123291), + onSecondaryContainer: Color(4294967295), + tertiary: Color(4282655023), + onTertiary: Color(4294967295), + tertiaryContainer: Color(4284824908), + onTertiaryContainer: Color(4294967295), + error: Color(4284481540), + onError: Color(4294967295), + errorContainer: Color(4288151562), + onErrorContainer: Color(4294967295), + surface: Color(4294834175), + onSurface: Color(4278190080), + onSurfaceVariant: Color(4278190080), + outline: Color(4281150259), + outlineVariant: Color(4283123793), + shadow: Color(4278190080), + scrim: Color(4278190080), + inverseSurface: Color(4281478965), + inversePrimary: Color(4291673599), + primaryFixed: Color(4283252856), + onPrimaryFixed: Color(4294967295), + primaryFixedDim: Color(4281739616), + onPrimaryFixedVariant: Color(4294967295), + secondaryFixed: Color(4283123291), + onSecondaryFixed: Color(4294967295), + secondaryFixedDim: Color(4281675843), + onSecondaryFixedVariant: Color(4294967295), + tertiaryFixed: Color(4284824908), + onTertiaryFixed: Color(4294967295), + tertiaryFixedDim: Color(4283180853), + onTertiaryFixedVariant: Color(4294967295), + surfaceDim: Color(4290557887), + surfaceBright: Color(4294834175), + surfaceContainerLowest: Color(4294967295), + surfaceContainerLow: Color(4294242295), + surfaceContainer: Color(4293321193), + surfaceContainerHigh: Color(4292400090), + surfaceContainerHighest: Color(4291478989), + ); + } + + ThemeData lightHighContrast() { + return theme(lightHighContrastScheme()); + } + + static ColorScheme darkScheme() { + return const ColorScheme( + brightness: Brightness.dark, + primary: Color(4291673599), + surfaceTint: Color(4291673599), + onPrimary: Color(4281608030), + primaryContainer: Color(4283121270), + onPrimaryContainer: Color(4293451519), + secondary: Color(4291544028), + onSecondary: Color(4281478721), + secondaryContainer: Color(4282991704), + onSecondaryContainer: Color(4293385976), + tertiary: Color(4293834953), + onTertiary: Color(4282983731), + tertiaryContainer: Color(4284693322), + onTertiaryContainer: Color(4294957540), + error: Color(4294948011), + onError: Color(4285071365), + errorContainer: Color(4287823882), + onErrorContainer: Color(4294957782), + surface: Color(4279505688), + onSurface: Color(4293321193), + onSurfaceVariant: Color(4291478735), + outline: Color(4287860633), + outlineVariant: Color(4282926414), + shadow: Color(4278190080), + scrim: Color(4278190080), + inverseSurface: Color(4293321193), + inversePrimary: Color(4284700303), + primaryFixed: Color(4293451519), + onPrimaryFixed: Color(4280225864), + primaryFixedDim: Color(4291673599), + onPrimaryFixedVariant: Color(4283121270), + secondaryFixed: Color(4293385976), + onSecondaryFixed: Color(4280097067), + secondaryFixedDim: Color(4291544028), + onSecondaryFixedVariant: Color(4282991704), + tertiaryFixed: Color(4294957540), + onTertiaryFixed: Color(4281405726), + tertiaryFixedDim: Color(4293834953), + onTertiaryFixedVariant: Color(4284693322), + surfaceDim: Color(4279505688), + surfaceBright: Color(4282005566), + surfaceContainerLowest: Color(4279176467), + surfaceContainerLow: Color(4280032032), + surfaceContainer: Color(4280295204), + surfaceContainerHigh: Color(4281018671), + surfaceContainerHighest: Color(4281742394), + ); + } + + ThemeData dark() { + return theme(darkScheme()); + } + + static ColorScheme darkMediumContrastScheme() { + return const ColorScheme( + brightness: Brightness.dark, + primary: Color(4293056255), + surfaceTint: Color(4291673599), + onPrimary: Color(4280884306), + primaryContainer: Color(4288055493), + onPrimaryContainer: Color(4278190080), + secondary: Color(4292991218), + onSecondary: Color(4280754998), + secondaryContainer: Color(4287925669), + onSecondaryContainer: Color(4278190080), + tertiary: Color(4294955230), + onTertiary: Color(4282194472), + tertiaryContainer: Color(4290020244), + onTertiaryContainer: Color(4278190080), + error: Color(4294955724), + onError: Color(4283695107), + errorContainer: Color(4294923337), + onErrorContainer: Color(4278190080), + surface: Color(4279505688), + onSurface: Color(4294967295), + onSurfaceVariant: Color(4292926181), + outline: Color(4290097339), + outlineVariant: Color(4287860377), + shadow: Color(4278190080), + scrim: Color(4278190080), + inverseSurface: Color(4293321193), + inversePrimary: Color(4283187063), + primaryFixed: Color(4293451519), + onPrimaryFixed: Color(4279501629), + primaryFixedDim: Color(4291673599), + onPrimaryFixedVariant: Color(4282002788), + secondaryFixed: Color(4293385976), + onSecondaryFixed: Color(4279438880), + secondaryFixedDim: Color(4291544028), + onSecondaryFixedVariant: Color(4281873223), + tertiaryFixed: Color(4294957540), + onTertiaryFixed: Color(4280550932), + tertiaryFixedDim: Color(4293834953), + onTertiaryFixedVariant: Color(4283444025), + surfaceDim: Color(4279505688), + surfaceBright: Color(4282794826), + surfaceContainerLowest: Color(4278716172), + surfaceContainerLow: Color(4280163618), + surfaceContainer: Color(4280887085), + surfaceContainerHigh: Color(4281610808), + surfaceContainerHighest: Color(4282334531), + ); + } + + ThemeData darkMediumContrast() { + return theme(darkMediumContrastScheme()); + } + + static ColorScheme darkHighContrastScheme() { + return const ColorScheme( + brightness: Brightness.dark, + primary: Color(4294241791), + surfaceTint: Color(4291673599), + onPrimary: Color(4278190080), + primaryContainer: Color(4291410427), + onPrimaryContainer: Color(4279107636), + secondary: Color(4294241791), + onSecondary: Color(4278190080), + secondaryContainer: Color(4291280856), + onSecondaryContainer: Color(4279044122), + tertiary: Color(4294962160), + onTertiary: Color(4278190080), + tertiaryContainer: Color(4293571782), + onTertiaryContainer: Color(4280091150), + error: Color(4294962409), + onError: Color(4278190080), + errorContainer: Color(4294946468), + onErrorContainer: Color(4280418305), + surface: Color(4279505688), + onSurface: Color(4294967295), + onSurfaceVariant: Color(4294967295), + outline: Color(4294242041), + outlineVariant: Color(4291215563), + shadow: Color(4278190080), + scrim: Color(4278190080), + inverseSurface: Color(4293321193), + inversePrimary: Color(4283187063), + primaryFixed: Color(4293451519), + onPrimaryFixed: Color(4278190080), + primaryFixedDim: Color(4291673599), + onPrimaryFixedVariant: Color(4279501629), + secondaryFixed: Color(4293385976), + onSecondaryFixed: Color(4278190080), + secondaryFixedDim: Color(4291544028), + onSecondaryFixedVariant: Color(4279438880), + tertiaryFixed: Color(4294957540), + onTertiaryFixed: Color(4278190080), + tertiaryFixedDim: Color(4293834953), + onTertiaryFixedVariant: Color(4280550932), + surfaceDim: Color(4279505688), + surfaceBright: Color(4283584341), + surfaceContainerLowest: Color(4278190080), + surfaceContainerLow: Color(4280295204), + surfaceContainer: Color(4281478965), + surfaceContainerHigh: Color(4282202689), + surfaceContainerHighest: Color(4282926668), + ); + } + + ThemeData darkHighContrast() { + return theme(darkHighContrastScheme()); + } + + ThemeData theme(ColorScheme colorScheme) => ThemeData( + useMaterial3: true, + brightness: colorScheme.brightness, + colorScheme: colorScheme, + textTheme: textTheme.apply( + bodyColor: colorScheme.onSurface, + displayColor: colorScheme.onSurface, + ), + scaffoldBackgroundColor: colorScheme.background, + canvasColor: colorScheme.surface, + ); + + List get extendedColors => []; +} + +class ExtendedColor { + final Color seed, value; + final ColorFamily light; + final ColorFamily lightHighContrast; + final ColorFamily lightMediumContrast; + final ColorFamily dark; + final ColorFamily darkHighContrast; + final ColorFamily darkMediumContrast; + + const ExtendedColor({ + required this.seed, + required this.value, + required this.light, + required this.lightHighContrast, + required this.lightMediumContrast, + required this.dark, + required this.darkHighContrast, + required this.darkMediumContrast, + }); +} + +class ColorFamily { + const ColorFamily({ + required this.color, + required this.onColor, + required this.colorContainer, + required this.onColorContainer, + }); + + final Color color; + final Color onColor; + final Color colorContainer; + final Color onColorContainer; +} diff --git a/lib/services/utils.dart b/lib/services/utils.dart index a5625bb..dbf7197 100644 --- a/lib/services/utils.dart +++ b/lib/services/utils.dart @@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:google_fonts/google_fonts.dart'; class Utils { /// Minimum size (width or height) of a interactive component @@ -82,12 +83,8 @@ class Utils { } static Widget trailingSaveWidget(BuildContext context, Function onPressed) { - return CupertinoButton( - child: Text('Save', - style: TextStyle( - fontWeight: FontWeight.bold, - //TODO: For some reason on android if inherit is the default of true the text color here turns to the background color - inherit: Platform.isIOS ? true : false)), + return PlatformTextButton( + child: Text('Save', style: TextStyle(fontWeight: FontWeight.bold)), padding: Platform.isAndroid ? null : EdgeInsets.zero, onPressed: () => onPressed()); } @@ -180,4 +177,19 @@ class Utils { final file = File(result.files.first.path!); return file.readAsString(); } + + static TextTheme createTextTheme(BuildContext context, String bodyFontString, String displayFontString) { + TextTheme baseTextTheme = Theme.of(context).textTheme; + TextTheme bodyTextTheme = GoogleFonts.getTextTheme(bodyFontString, baseTextTheme); + TextTheme displayTextTheme = GoogleFonts.getTextTheme(displayFontString, baseTextTheme); + TextTheme textTheme = displayTextTheme.copyWith( + bodyLarge: bodyTextTheme.bodyLarge, + bodyMedium: bodyTextTheme.bodyMedium, + bodySmall: bodyTextTheme.bodySmall, + labelLarge: bodyTextTheme.labelLarge, + labelMedium: bodyTextTheme.labelMedium, + labelSmall: bodyTextTheme.labelSmall, + ); + return textTheme; + } } diff --git a/pubspec.lock b/pubspec.lock index d055643..2c6a83b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -176,6 +176,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" http: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 868d677..385b841 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: flutter_platform_widgets: ^7.0.1 path_provider: ^2.0.11 file_picker: ^8.1.2 + google_fonts: ^6.2.1 uuid: ^4.4.2 package_info_plus: ^8.0.2 url_launcher: ^6.1.6