Skip to content
This repository has been archived by the owner on Oct 2, 2024. It is now read-only.

Commit

Permalink
Migrate to Material Design 3 (Material You)
Browse files Browse the repository at this point in the history
  • Loading branch information
tsinis committed Oct 31, 2021
1 parent b5a5297 commit 7333eae
Show file tree
Hide file tree
Showing 31 changed files with 383 additions and 134 deletions.
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

Or download signed binaries for **all mainstream platforms** in the [Releases](https://github.com/tsinis/colors_ai/releases) section of this repository.

# Colors AI 🤖
# Colors AI 🎨🤖

## Table of content

Expand All @@ -20,7 +20,6 @@ Or download signed binaries for **all mainstream platforms** in the [Releases](h
* [Architecture](#Architecture)
* [Directory Structure](#Directory-Structure)
* [Tests](#Tests)
* [Packages](#Packages)
* [Accessibility](#Accessibility)
* [How to run it](#How-to-run-it)
* [UI/UX Design](#UI/UX-Design)
Expand Down Expand Up @@ -49,21 +48,22 @@ Application is **partially covered with Unit, Widget, and Integration tests**. M

## Accessibility

The app was originally designed to be accessible by **WCAG 2.1 AA standards at minimum, and AAA in particular**, although it was not intended to be used by the blind or by people with severe visual disabilities. All **texts have a contrast ratio at least of 4.5, images 3.0, touch target sizes of at least 48dp**. The application was audited physically on a first-generation iPhone SE (smallest iPhone at this moment) with maximum font size, thickness, contrast, and component magnification. The **audit was also performed on the macOS** version of the app, using the same Accessibility Inspector as on the iOS. The **report from the official Accessibility Scanner for Android** (with a tested device with screen size 640x320px and maximum UI and font scale) can be found at [resources/accessibility](./resources/accessibility/) folder. As you may see it will only complain about the small tap size of text links in the *About* app section and overall screen semantics (which is a [framework bug](https://github.com/flutter/flutter/issues/39531)). Also, the application is **translated into 4 languages and have a haptic feedback (vibration) on mobile devices**. The UI was also built to be **controllable via keyboard/input device/remote control/gamepad**, [here you will find a remote control showcase video](https://drive.google.com/file/d/15Ppuk3ELnP6MhUP6smwmrOS-LdbF0ji0/view?usp=sharing).
The app was originally designed to be accessible by **WCAG 2.1 AA standards at minimum, and AAA in particular**, although it was not intended to be used by the blind or by people with severe visual disabilities. All **texts have a contrast ratio at least of 4.5, images 3.0, touch target sizes of at least 48dp**. The application was audited physically on a first-generation iPhone SE (smallest iPhone at this moment) with maximum font size, thickness, contrast, and component magnification. The **audit was also performed on the macOS** version of the app, using the same Accessibility Inspector as on the iOS. The **report from the official Accessibility Scanner for Android** (with a tested device with screen size 640x320px and maximum UI and font scale) can be found at [resources/accessibility](./resources/accessibility/) folder. As you may see it will only complain about the small tap size of text links in the *About* app section and overall screen semantics (which is a [framework bug](https://github.com/flutter/flutter/issues/39531)). Also, the application is **translated into 4 languages, have a haptic feedback (vibration) on mobile devices and sound feedback on all platforms** (expect PCs at this moment of time). The UI was also built to be **controllable via keyboard/input device/remote control/gamepad**, [here you will find a remote control showcase video](https://drive.google.com/file/d/15Ppuk3ELnP6MhUP6smwmrOS-LdbF0ji0/view?usp=sharing).
However, in the future, I'm planning to improve the control via the input device.

## How to run it

Flutter version **2.6.0-12.0.pre.94** or higher is assumed to be installed. Please run this command from the project's folder, in your terminal:

```bash
flutter pub get
flutter gen-l10n
flutter run
```

## UI/UX Design

All animations here are made with pure Flutter. The application's UI is designed with "gesture-first" UX on mobile platforms and strictly following [Material Design Guidelines](https://material.io/design). Neutral grey color UI is used here to not disrupt user's color perception with highly contrasting light or dark themes.
All animations here are made with pure Flutter. The application's UI is designed with "gesture-first" UX on mobile platforms and strictly following [Material Design 3 (Material You) Guidelines](https://m3.material.io). Neutral grey color UI is used here to not disrupt user's color perception with highly contrasting light or dark themes.

## To-Do Section

Expand All @@ -75,7 +75,6 @@ All animations here are made with pure Flutter. The application's UI is designed
* [x] L10N.
* [ ] Add more keyboard shortcuts.
* [ ] Add Feedback.
* [ ] Map BLoC events to state via Freezed.
* [ ] Provide support for Favorites backup (online?).

## Licenses
Expand Down
7 changes: 7 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ dart_code_metrics:
metrics-exclude:
- test/**
rules:
- avoid-nested-conditional-expressions:
acceptable-level: 2
- prefer-correct-type-name
- prefer-last
- refer-correct-identifier-length
- prefer-first
- prefer-const-border-radius
- avoid-unused-parameters
- binary-expression-operand-order
- double-literal-format
Expand Down
2 changes: 1 addition & 1 deletion ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c

COCOAPODS: 1.11.0
COCOAPODS: 1.11.2
3 changes: 2 additions & 1 deletion lib/about/ui/view/about_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../../core/ui/constants.dart';
import '../../blocs/about_dialog/about_bloc.dart';
import '../widgets/app_icon.dart';
import '../widgets/material3_dialog.dart';

class AboutAppDialog extends StatelessWidget {
const AboutAppDialog();
Expand All @@ -15,7 +16,7 @@ class AboutAppDialog extends StatelessWidget {
final TextStyle? linkStyle =
Theme.of(context).textTheme.bodyText2?.copyWith(color: Theme.of(context).indicatorColor);

return AboutDialog(
return AboutDialogM3(
applicationName: appName,
applicationVersion: BlocProvider.of<AboutBloc>(context).state.appVersion,
applicationLegalese: '2021 © Roman Cinis',
Expand Down
2 changes: 1 addition & 1 deletion lib/about/ui/widgets/app_icon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class AppIcon extends StatelessWidget {
child: Material(
elevation: 4,
clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular(16),
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: ColoredBox(
color: const Color(0xff424242),
child: Transform.translate(
Expand Down
69 changes: 69 additions & 0 deletions lib/about/ui/widgets/material3_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';

import '../../../core/extensions/string.dart';

class AboutDialogM3 extends StatelessWidget {
const AboutDialogM3({
required this.applicationName,
required this.applicationVersion,
required this.applicationIcon,
this.applicationLegalese,
this.children,
Key? key,
}) : super(key: key);

static const double _textVerticalSeparation = 18;

final String applicationName;
final String applicationVersion;
final Widget applicationIcon;
final String? applicationLegalese;
final List<Widget>? children;

@override
Widget build(BuildContext context) => AlertDialog(
actionsPadding: const EdgeInsets.only(bottom: 8, right: 8),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(28))),
content: ListBody(
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
IconTheme(data: Theme.of(context).iconTheme, child: applicationIcon),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: ListBody(
children: <Widget>[
Text(applicationName, style: Theme.of(context).textTheme.headline5),
Text(applicationVersion, style: Theme.of(context).textTheme.bodyText2),
const SizedBox(height: _textVerticalSeparation),
Text(applicationLegalese ?? '', style: Theme.of(context).textTheme.caption),
],
),
),
),
],
),
...?children,
],
),
actions: <Widget>[
TextButton(
child: Text(MaterialLocalizations.of(context).viewLicensesButtonLabel.toBeginningOfSentenceCase()),
onPressed: () => showLicensePage(
context: context,
applicationName: applicationName,
applicationVersion: applicationVersion,
applicationIcon: applicationIcon,
applicationLegalese: applicationLegalese,
),
),
TextButton(
child: Text(MaterialLocalizations.of(context).closeButtonLabel.toBeginningOfSentenceCase()),
onPressed: () => Navigator.pop(context),
),
],
scrollable: true,
);
}
36 changes: 33 additions & 3 deletions lib/app/theme/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ class AppTheme {
return isDarkTheme ? _darkTheme : _lightTheme;
}

static const _visualDensity = VisualDensity.standard;
static const _material3FabSize = BoxConstraints.tightForFinite(height: 56);
static const _material3FabBorder = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16)));
static const _material3ButtonShape = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20)));

final ThemeData _lightTheme = ThemeData(
primarySwatch: Colors.grey,
primaryColor: Colors.grey[400],
Expand All @@ -26,20 +31,33 @@ class AppTheme {
dialogTheme: DialogTheme(backgroundColor: Colors.grey[100]),
radioTheme: RadioThemeData(fillColor: MaterialStateProperty.all<Color>(Colors.black)),
textButtonTheme: TextButtonThemeData(style: TextButton.styleFrom(primary: Colors.grey[850])),
elevatedButtonTheme: ElevatedButtonThemeData(style: ElevatedButton.styleFrom(primary: Colors.grey[200])),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
primary: Colors.grey[200],
shape: _material3ButtonShape,
visualDensity: _visualDensity,
),
),
appBarTheme: AppBarTheme(
elevation: 2,
shadowColor: Colors.black45,
iconTheme: const IconThemeData(color: _grey800),
backgroundColor: Colors.grey[400],
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
extendedPadding: const EdgeInsets.symmetric(horizontal: 16),
extendedSizeConstraints: _material3FabSize,
shape: _material3FabBorder,
backgroundColor: Colors.grey[100],
foregroundColor: Colors.grey[850],
focusElevation: 10,
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
primary: Colors.grey[850],
visualDensity: _visualDensity,
backgroundColor: Colors.transparent,
shape: _material3ButtonShape,
side: const BorderSide(color: _grey800),
),
),
Expand All @@ -64,10 +82,20 @@ class AppTheme {
indicatorColor: Colors.teal[200],
disabledColor: Colors.grey[600],
primaryIconTheme: IconThemeData(color: Colors.grey[350]),
radioTheme: RadioThemeData(fillColor: MaterialStateProperty.all<Color>(Colors.grey[400]!)),
appBarTheme: const AppBarTheme(shadowColor: Colors.black54, elevation: 2),
radioTheme: RadioThemeData(fillColor: MaterialStateProperty.all<Color?>(Colors.grey[400])),
textButtonTheme: TextButtonThemeData(style: TextButton.styleFrom(primary: Colors.grey[350])),
elevatedButtonTheme: ElevatedButtonThemeData(style: ElevatedButton.styleFrom(primary: Colors.grey[350])),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
shape: _material3ButtonShape,
primary: Colors.grey[350],
visualDensity: _visualDensity,
),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
extendedPadding: const EdgeInsets.symmetric(horizontal: 16),
extendedSizeConstraints: _material3FabSize,
shape: _material3FabBorder,
backgroundColor: Colors.grey[600],
foregroundColor: Colors.white,
disabledElevation: 0,
Expand All @@ -81,8 +109,10 @@ class AppTheme {
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
visualDensity: _visualDensity,
primary: Colors.grey[300],
backgroundColor: Colors.transparent,
shape: _material3ButtonShape,
side: BorderSide(color: Colors.grey[400]!),
),
),
Expand Down
4 changes: 2 additions & 2 deletions lib/color_generator/ui/view/gen_colors_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class ColorsGenerator extends StatelessWidget {
padding: const EdgeInsets.only(top: 20),
child: Text.rich(
TextSpan(
text: AppLocalizations.of(context).noConnectionTitle.toUpperCase(),
text: AppLocalizations.of(context).noConnectionTitle,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
children: <TextSpan>[
TextSpan(
Expand All @@ -55,7 +55,7 @@ class ColorsGenerator extends StatelessWidget {
padding: const EdgeInsets.only(top: 56, bottom: 24),
child: ElevatedButton.icon(
icon: const Icon(Icons.refresh_outlined, size: 20),
label: Text(AppLocalizations.of(context).returnButtonLabel.toUpperCase()),
label: Text(AppLocalizations.of(context).returnButtonLabel),
onPressed: () => BlocProvider.of<ColorsBloc>(context).add(const ColorsStarted()),
),
),
Expand Down
21 changes: 17 additions & 4 deletions lib/color_generator/ui/widgets/animated/animated_list_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ class AnimatedListItem extends StatefulWidget {
required this.child,
required this.length,
required this.size,
required Key key,
this.hoverIndex,
Key? key,
}) : super(key: key);

final Widget child;
final int index;
final int length;
final int? hoverIndex;
final BoxConstraints size;

@override
Expand All @@ -20,6 +22,7 @@ class AnimatedListItem extends StatefulWidget {

class _AnimatedListItemState extends State<AnimatedListItem> {
static const Duration duration = Duration(milliseconds: 300);
static const double hoverPadding = 48;
bool isAnimationDone = false;

@override
Expand All @@ -36,18 +39,28 @@ class _AnimatedListItemState extends State<AnimatedListItem> {
double get tileHeight => widget.size.maxHeight / widget.length;
double get tileWeight => widget.size.maxWidth / widget.length;

double get additionaHoverPadding {
if (widget.hoverIndex == null) {
return 0;
} else if (widget.index == widget.hoverIndex) {
return hoverPadding;
}

return -(hoverPadding / widget.length);
}

@override
Widget build(BuildContext context) => AnimatedContainer(
curve: Curves.decelerate,
curve: Curves.easeInOutCubicEmphasized,
height: isPortrait
? isAnimationDone
? tileHeight
? tileHeight + additionaHoverPadding
: (widget.index + 1) * tileHeight * widget.length
: widget.size.maxHeight,
width: isPortrait
? widget.size.maxWidth
: isAnimationDone
? tileWeight
? tileWeight + additionaHoverPadding
: (widget.index + 1) * tileWeight * widget.length,
duration: duration,
child: AnimatedOpacity(
Expand Down
Loading

0 comments on commit 7333eae

Please sign in to comment.