ML-6 Add support for dynamic colors on A12+ (#8)

* added dynamic colors

* made dynamic colors enabled/disabled

* fixed tests
This commit is contained in:
Vadim 2023-01-22 22:30:29 +03:00 committed by GitHub
parent 9be413752a
commit 9477f80ada
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 176 additions and 62 deletions

View file

@ -30,20 +30,21 @@ class Application extends StatelessWidget {
return MultiProvider( return MultiProvider(
providers: [ providers: [
Provider(create: (_) => UserPreferencesService(snapshot.data!)), Provider(create: (_) => UserPreferencesService(snapshot.data!)),
Provider(create: (_) => HapticsService()), Provider(create: (_) => const HapticsService()),
Provider(create: (_) => PermissionsService()),
Provider.value(value: evSource), Provider.value(value: evSource),
], ],
child: Provider( child: StopTypeProvider(
create: (_) => PermissionsService(), child: ThemeProvider(
child: StopTypeProvider( builder: (context, _) {
child: ThemeProvider( final systemIconsBrightness =
initialPrimaryColor: const Color(0xFF2196f3), ThemeData.estimateBrightnessForColor(context.watch<ThemeData>().colorScheme.onSurface);
builder: (context, child) => AnnotatedRegion( return AnnotatedRegion(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent, statusBarColor: Colors.transparent,
statusBarIconBrightness: statusBarIconBrightness: systemIconsBrightness,
ThemeData.estimateBrightnessForColor(context.watch<ThemeData>().colorScheme.onSurface),
systemNavigationBarColor: context.watch<ThemeData>().colorScheme.surface, systemNavigationBarColor: context.watch<ThemeData>().colorScheme.surface,
systemNavigationBarIconBrightness: systemIconsBrightness,
), ),
child: MaterialApp( child: MaterialApp(
theme: context.watch<ThemeData>(), theme: context.watch<ThemeData>(),
@ -64,8 +65,8 @@ class Application extends StatelessWidget {
"settings": (context) => const SettingsScreen(), "settings": (context) => const SettingsScreen(),
}, },
), ),
), );
), },
), ),
), ),
); );

View file

@ -1,6 +1,8 @@
import 'package:vibration/vibration.dart'; import 'package:vibration/vibration.dart';
class HapticsService { class HapticsService {
const HapticsService();
Future<void> quickVibration() async => _tryVibrate(duration: 25, amplitude: 96); Future<void> quickVibration() async => _tryVibrate(duration: 25, amplitude: 96);
Future<void> responseVibration() async => _tryVibrate(duration: 50, amplitude: 128); Future<void> responseVibration() async => _tryVibrate(duration: 50, amplitude: 128);

View file

@ -0,0 +1 @@
enum DynamicColorsState { unavailable, enabled, disabled }

View file

@ -10,6 +10,7 @@ class UserPreferencesService {
static const _hapticsKey = "haptics"; static const _hapticsKey = "haptics";
static const _themeTypeKey = "themeType"; static const _themeTypeKey = "themeType";
static const _dynamicColorsKey = "dynamicColors";
final SharedPreferences _sharedPreferences; final SharedPreferences _sharedPreferences;
@ -26,4 +27,7 @@ class UserPreferencesService {
ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(_themeTypeKey) ?? 0]; ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(_themeTypeKey) ?? 0];
set themeType(ThemeType value) => _sharedPreferences.setInt(_themeTypeKey, value.index); set themeType(ThemeType value) => _sharedPreferences.setInt(_themeTypeKey, value.index);
bool get dynamicColors => _sharedPreferences.getBool(_dynamicColorsKey) ?? false;
set dynamicColors(bool value) => _sharedPreferences.setBool(_dynamicColorsKey, value);
} }

View file

@ -20,6 +20,7 @@
"haptics": "Haptics", "haptics": "Haptics",
"theme": "Theme", "theme": "Theme",
"chooseTheme": "Choose theme", "chooseTheme": "Choose theme",
"dynamicColors": "Dynamic colors",
"themeLight": "Light", "themeLight": "Light",
"themeDark": "Dark", "themeDark": "Dark",
"themeSystemDefault": "System default", "themeSystemDefault": "System default",

View file

@ -1,5 +1,7 @@
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:material_color_utilities/material_color_utilities.dart'; import 'package:material_color_utilities/material_color_utilities.dart';
@ -7,13 +9,9 @@ import 'package:material_color_utilities/material_color_utilities.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class ThemeProvider extends StatefulWidget { class ThemeProvider extends StatefulWidget {
final Color initialPrimaryColor;
final Widget? child;
final TransitionBuilder? builder; final TransitionBuilder? builder;
const ThemeProvider({ const ThemeProvider({
required this.initialPrimaryColor,
this.child,
this.builder, this.builder,
super.key, super.key,
}); });
@ -27,53 +25,50 @@ class ThemeProvider extends StatefulWidget {
} }
class ThemeProviderState extends State<ThemeProvider> { class ThemeProviderState extends State<ThemeProvider> {
late ThemeType _themeType; late final _themeTypeNotifier = ValueNotifier<ThemeType>(context.read<UserPreferencesService>().themeType);
late Color _primaryColor = widget.initialPrimaryColor; late final _dynamicColorsNotifier = ValueNotifier<bool>(context.read<UserPreferencesService>().dynamicColors);
late final _primaryColorNotifier = ValueNotifier<Color>(const Color(0xFF2196f3));
@override @override
void initState() { void dispose() {
super.initState(); _themeTypeNotifier.dispose();
_themeType = context.read<UserPreferencesService>().themeType; _dynamicColorsNotifier.dispose();
_primaryColorNotifier.dispose();
super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Provider.value( return ValueListenableBuilder(
value: _themeType, valueListenable: _themeTypeNotifier,
child: Provider.value( builder: (_, themeType, __) => Provider.value(
value: _themeFromColor( value: themeType,
_primaryColor, child: ValueListenableBuilder(
_mapThemeTypeToBrightness(_themeType), valueListenable: _dynamicColorsNotifier,
builder: (_, useDynamicColors, __) => _DynamicColorsProvider(
useDynamicColors: useDynamicColors,
themeBrightness: _themeBrightness,
builder: (_, dynamicPrimaryColor) => ValueListenableBuilder(
valueListenable: _primaryColorNotifier,
builder: (_, primaryColor, __) => _ThemeDataProvider(
primaryColor: dynamicPrimaryColor ?? primaryColor,
brightness: _themeBrightness,
builder: widget.builder,
),
),
),
), ),
builder: widget.builder,
child: widget.child,
), ),
); );
} }
void setThemeType(ThemeType themeType) { void setThemeType(ThemeType themeType) {
if (themeType == _themeType) { _themeTypeNotifier.value = themeType;
return;
}
setState(() {
_themeType = themeType;
});
context.read<UserPreferencesService>().themeType = themeType; context.read<UserPreferencesService>().themeType = themeType;
} }
void setPrimaryColor(Color color) { Brightness get _themeBrightness {
if (color == _primaryColor) { switch (_themeTypeNotifier.value) {
return;
}
setState(() {
_primaryColor = color;
});
}
Brightness _mapThemeTypeToBrightness(ThemeType themeType) {
switch (themeType) {
case ThemeType.light: case ThemeType.light:
return Brightness.light; return Brightness.light;
case ThemeType.dark: case ThemeType.dark:
@ -83,9 +78,85 @@ class ThemeProviderState extends State<ThemeProvider> {
} }
} }
ThemeData _themeFromColor(Color color, Brightness brightness) { void enableDynamicColors(bool enable) {
final scheme = brightness == Brightness.light ? Scheme.light(color.value) : Scheme.dark(color.value); _dynamicColorsNotifier.value = enable;
final colorScheme = ColorScheme( context.read<UserPreferencesService>().dynamicColors = enable;
}
}
class _DynamicColorsProvider extends StatelessWidget {
final bool useDynamicColors;
final Brightness themeBrightness;
final Widget Function(BuildContext context, Color? primaryColor) builder;
const _DynamicColorsProvider({
required this.useDynamicColors,
required this.themeBrightness,
required this.builder,
});
@override
Widget build(BuildContext context) {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
late final DynamicColorsState state;
late final Color? dynamicPrimaryColor;
if (lightDynamic != null && darkDynamic != null) {
if (useDynamicColors) {
dynamicPrimaryColor = (themeBrightness == Brightness.light ? lightDynamic : darkDynamic).primary;
state = DynamicColorsState.enabled;
} else {
dynamicPrimaryColor = null;
state = DynamicColorsState.disabled;
}
} else {
dynamicPrimaryColor = null;
state = DynamicColorsState.unavailable;
}
return Provider.value(
value: state,
child: builder(context, dynamicPrimaryColor),
);
},
);
}
}
class _ThemeDataProvider extends StatelessWidget {
final Color primaryColor;
final Brightness brightness;
final TransitionBuilder? builder;
const _ThemeDataProvider({
required this.primaryColor,
required this.brightness,
required this.builder,
});
@override
Widget build(BuildContext context) {
return Provider.value(
value: _themeFromColorScheme(_colorSchemeFromColor()),
builder: builder,
);
}
ThemeData _themeFromColorScheme(ColorScheme scheme) {
return ThemeData(
useMaterial3: true,
bottomAppBarColor: scheme.surface,
brightness: scheme.brightness,
colorScheme: scheme,
dialogBackgroundColor: scheme.surface,
dialogTheme: DialogTheme(backgroundColor: scheme.surface),
scaffoldBackgroundColor: scheme.surface,
toggleableActiveColor: scheme.primary,
);
}
ColorScheme _colorSchemeFromColor() {
final scheme = brightness == Brightness.light ? Scheme.light(primaryColor.value) : Scheme.dark(primaryColor.value);
return ColorScheme(
brightness: brightness, brightness: brightness,
primary: Color(scheme.primary), primary: Color(scheme.primary),
onPrimary: Color(scheme.onPrimary), onPrimary: Color(scheme.onPrimary),
@ -102,9 +173,5 @@ class ThemeProviderState extends State<ThemeProvider> {
surfaceVariant: Color.alphaBlend(Color(scheme.primary).withOpacity(0.5), Color(scheme.background)), surfaceVariant: Color.alphaBlend(Color(scheme.primary).withOpacity(0.5), Color(scheme.background)),
onSurfaceVariant: Color(scheme.onSurfaceVariant), onSurfaceVariant: Color(scheme.onSurfaceVariant),
); );
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
);
} }
} }

View file

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/theme.dart';
class DynamicColorsListTile extends StatelessWidget {
const DynamicColorsListTile({super.key});
@override
Widget build(BuildContext context) {
return SwitchListTile(
secondary: const Icon(Icons.colorize),
title: Text(S.of(context).dynamicColors),
value: context.watch<DynamicColorsState>() == DynamicColorsState.enabled,
onChanged: ThemeProvider.of(context).enableDynamicColors,
);
}
}

View file

@ -2,10 +2,9 @@ import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/theme.dart'; import 'package:lightmeter/res/theme.dart';
import 'package:lightmeter/screens/settings/components/shared/widget_dialog_picker.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'shared/widget_dialog_picker.dart';
class ThemeTypeListTile extends StatelessWidget { class ThemeTypeListTile extends StatelessWidget {
const ThemeTypeListTile({super.key}); const ThemeTypeListTile({super.key});

View file

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
import 'package:provider/provider.dart';
import 'components/widget_list_tile_dynamic_colors.dart';
import 'components/widget_list_tile_theme_type.dart';
class ThemeSettings extends StatelessWidget {
const ThemeSettings({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
const ThemeTypeListTile(),
if (context.read<DynamicColorsState>() != DynamicColorsState.unavailable) const DynamicColorsListTile(),
],
);
}
}

View file

@ -4,7 +4,7 @@ import 'package:lightmeter/res/dimens.dart';
import 'components/haptics/provider_list_tile_haptics.dart'; import 'components/haptics/provider_list_tile_haptics.dart';
import 'components/widget_list_tile_fractional_stops.dart'; import 'components/widget_list_tile_fractional_stops.dart';
import 'components/widget_list_tile_theme_type.dart'; import 'components/theme/widget_settings_theme.dart';
import 'components/widget_label_version.dart'; import 'components/widget_label_version.dart';
class SettingsScreen extends StatelessWidget { class SettingsScreen extends StatelessWidget {
@ -13,7 +13,6 @@ class SettingsScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
body: SafeArea( body: SafeArea(
top: false, top: false,
child: CustomScrollView( child: CustomScrollView(
@ -42,7 +41,7 @@ class SettingsScreen extends StatelessWidget {
[ [
const StopTypeListTile(), const StopTypeListTile(),
const HapticsListTileProvider(), const HapticsListTileProvider(),
const ThemeTypeListTile(), const ThemeSettings(),
], ],
), ),
), ),

View file

@ -9,6 +9,7 @@ environment:
dependencies: dependencies:
camera: 0.10.0+4 camera: 0.10.0+4
exif: 3.1.2 exif: 3.1.2
dynamic_color: 1.5.4
flutter: flutter:
sdk: flutter sdk: flutter
flutter_bloc: 8.1.1 flutter_bloc: 8.1.1
@ -16,7 +17,7 @@ dependencies:
sdk: flutter sdk: flutter
intl: 0.17.0 intl: 0.17.0
intl_utils: 2.8.1 intl_utils: 2.8.1
material_color_utilities: 0.2.0 material_color_utilities: 0.1.5
package_info_plus: 3.0.2 package_info_plus: 3.0.2
permission_handler: 10.2.0 permission_handler: 10.2.0
provider: 6.0.4 provider: 6.0.4
@ -28,10 +29,10 @@ dev_dependencies:
flutter_launcher_icons: 0.11.0 flutter_launcher_icons: 0.11.0
flutter_lints: 2.0.0 flutter_lints: 2.0.0
flutter_native_splash: 2.2.16 flutter_native_splash: 2.2.16
test: 1.21.6 test: 1.22.2
dependency_overrides: dependency_overrides:
material_color_utilities: 0.2.0 test_api: 0.4.12
flutter: flutter:
uses-material-design: true uses-material-design: true