Skip to content

Commit 36def35

Browse files
authored
Add animated theme (#671)
* Refactor style_generator * Add FWidgetStateMap.lerp * WIP adding lerp method * Generate lerp function for styles * Add lerp generator * Add FAnimatedTheme * Commit from GitHub Actions (Forui Internal Gen Presubmit) * Prepare Forui for review * Update card * Fix failing test * Prepare Forui for review * Update windows-latest goldens * Fix failing internal gen build test * Increase timeout --------- Co-authored-by: Pante <[email protected]>
1 parent 28709d8 commit 36def35

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1811
-575
lines changed

.github/workflows/forui_internal_gen_build.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
with:
2828
flutter-version: ${{ matrix.flutter-version }}
2929
cache: true
30-
- run: flutter pub get
30+
- run: dart pub get
3131
- run: dart run build_runner build --delete-conflicting-outputs
32-
- run: flutter analyze
32+
- run: dart analyze
33+
- run: dart test

docs/app/docs/localization/page.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ MaterialApp(
1212
supportedLocales: [
1313
// Add locales supported by your application here.
1414
],
15-
builder: (context, child) => FTheme(
15+
builder: (context, child) => FAnimatedTheme(
1616
data: FThemes.zinc.light,
1717
child: child!,
1818
),
@@ -27,7 +27,7 @@ these instead of providing them manually if your application doesn't have any ot
2727
MaterialApp(
2828
localizationsDelegates: FLocalizations.localizationsDelegates,
2929
supportedLocales: FLocalizations.supportedLocales,
30-
builder: (context, child) => FTheme(
30+
builder: (context, child) => FAnimatedTheme(
3131
data: FThemes.zinc.light,
3232
child: child!,
3333
),

docs/app/docs/page.mdx

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,91 @@ flutter pub add forui_assets
4949
## Usage
5050

5151
To use Forui widgets in your Flutter app, import the Forui package and place the
52-
[`FTheme`](https://pub.dev/documentation/forui/latest/forui.theme/FTheme-class.html) widget underneath `CupertinoApp`,
53-
`MaterialApp`, or `WidgetsApp` at the root of the widget tree.
52+
[`FAnimatedTheme`](https://pub.dev/documentation/forui/latest/forui.theme/FAnimatedTheme-class.html) widget underneath
53+
`CupertinoApp`, `MaterialApp`, or `WidgetsApp` at the root of the widget tree.
5454

5555
To generate a basic Forui app structure in your project, run:
5656

5757
```bash filename="bash" copy
5858
dart run forui init
5959
```
6060

61+
Or copy & paste the following code snippet:
62+
```dart filename='main.dart' copy
63+
import 'package:flutter/material.dart';
64+
import 'package:forui/forui.dart';
65+
66+
void main() {
67+
runApp(const Application());
68+
}
69+
70+
class Application extends StatelessWidget {
71+
const Application({super.key});
72+
73+
@override
74+
Widget build(BuildContext context) {
75+
/// Try changing this and hot reloading the application.
76+
///
77+
/// To create a custom theme:
78+
/// ```shell
79+
/// dart forui theme create [theme template].
80+
/// ```
81+
final theme = FThemes.zinc.dark;
82+
83+
return MaterialApp(
84+
// TODO: replace with your application's supported locales.
85+
supportedLocales: FLocalizations.supportedLocales,
86+
// TODO: add your application's localizations delegates.
87+
localizationsDelegates: const [...FLocalizations.localizationsDelegates],
88+
// MaterialApp's theme is also animated by default with the same duration and curve.
89+
// See https://api.flutter.dev/flutter/material/MaterialApp/themeAnimationStyle.html for how to configure this.
90+
//
91+
// There is a known issue with implicitly animated widgets where their transition occurs AFTER the theme's.
92+
// See https://github.com/forus-labs/forui/issues/670.
93+
theme: theme.toApproximateMaterialTheme(),
94+
builder: (_, child) => FAnimatedTheme(data: theme, child: child!),
95+
// You can also replace FScaffold with Material Scaffold.
96+
home: const FScaffold(
97+
// TODO: replace with your widget.
98+
child: Example(),
99+
),
100+
);
101+
}
102+
}
103+
104+
class Example extends StatefulWidget {
105+
const Example({super.key});
106+
107+
@override
108+
State<Example> createState() => _ExampleState();
109+
}
110+
111+
class _ExampleState extends State<Example> {
112+
int _count = 0;
113+
114+
@override
115+
Widget build(BuildContext context) => Center(
116+
child: Column(
117+
mainAxisSize: MainAxisSize.min,
118+
spacing: 10,
119+
children: [
120+
Text('Count: $_count'),
121+
FButton(
122+
onPress: () => setState(() => _count++),
123+
suffix: const Icon(FIcons.chevronsUp),
124+
child: const Text('Increase'),
125+
),
126+
],
127+
),
128+
);
129+
}
130+
131+
```
132+
61133
It is perfectly fine to use Cupertino/Material widgets alongside Forui widgets!
62134

63135
```dart filename="main.dart" {3,12-18} copy
64-
import 'package:flutter/material.dart';
136+
import 'package:flutter/cupertino.dart';
65137
66138
import 'package:forui/forui.dart';
67139
@@ -72,8 +144,8 @@ void main() {
72144
class Application extends StatelessWidget {
73145
const Application({super.key});
74146
75-
Widget build(BuildContext context) => MaterialApp(
76-
builder: (context, child) => FTheme(
147+
Widget build(BuildContext context) => CupertinoApp(
148+
builder: (context, child) => FAnimatedTheme(
77149
data: FThemes.zinc.light,
78150
child: child!,
79151
),

docs/app/docs/themes/page.mdx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ to generate themes and styles that can be directly modified in your project.
1818

1919
<Callout type="info">
2020
Forui does not manage the theme brightness (light or dark) automatically.
21-
You need to specify the theme explicitly in `FTheme(...)`.
21+
You need to specify the theme explicitly in `FAnimatedTheme(...)`.
2222

2323
```dart filename="main.dart" {3} copy
2424
@override
25-
Widget build(BuildContext context) => FTheme(
25+
Widget build(BuildContext context) => FAnimatedTheme(
2626
data: FThemes.zinc.light, // or FThemes.zinc.dark
2727
child: FScaffold(...),
2828
);
@@ -51,8 +51,8 @@ Forui includes predefined themes that can be used out of the box. They are heavi
5151
title: Forui Theming System
5252
---
5353
classDiagram
54-
class FTheme {
55-
+FTheme(...)
54+
class FAnimatedTheme {
55+
+FAnimatedTheme(...)
5656
}
5757
5858
class FThemeData {
@@ -78,7 +78,7 @@ class FAccordionStyle {
7878
}
7979
note for FAccordionStyle "Each Forui widget has a corresponding style.\nWe use FAccordionStyle here as an example."
8080
81-
FTheme --> FThemeData
81+
FAnimatedTheme --> FThemeData
8282
FThemeData --> FColors
8383
FThemeData --> FTypography
8484
FThemeData --> FStyle
@@ -87,7 +87,7 @@ FThemeData --> FAccordionStyle
8787

8888
There are **5** core components in Forui's theming system.
8989

90-
- **[`FTheme`](https://pub.dev/documentation/forui/latest/forui.theme/FTheme-class.html)**: The root widget that provides the theme data to all widgets in the subtree.
90+
- **[`FAnimatedTheme`](https://pub.dev/documentation/forui/latest/forui.theme/FAnimatedTheme-class.html)**: The root widget that provides the theme data to all widgets in the subtree.
9191
- **[`FThemeData`](https://pub.dev/documentation/forui/latest/forui.theme/FThemeData-class.html)**: Main class that holds:
9292
- **[`FColors`](https://pub.dev/documentation/forui/latest/forui.theme/FColors-class.html)**: Color scheme including primary, foreground, and background colors.
9393
- **[`FTypography`](https://pub.dev/documentation/forui/latest/forui.theme/FTypography-class.html)**: Typography settings including font family and text styles.
@@ -173,7 +173,7 @@ method is provided to quickly scale all the font sizes.
173173

174174
```dart {5-7} copy
175175
@override
176-
Widget build(BuildContext context) => FTheme(
176+
Widget build(BuildContext context) => FAnimatedTheme(
177177
data: FThemeData(
178178
colors: FThemes.zinc.light.colors,
179179
typography: FThemes.zinc.light.typography.copyWith(
@@ -290,7 +290,7 @@ class Application extends StatelessWidget {
290290
return MaterialApp(
291291
localizationsDelegates: FLocalizations.localizationsDelegates,
292292
supportedLocales: FLocalizations.supportedLocales,
293-
builder: (_, child) => FTheme(data: theme, child: child!),
293+
builder: (_, child) => FAnimatedTheme(data: theme, child: child!),
294294
theme: theme.toApproximateMaterialTheme(),
295295
home: const FScaffold(
296296
// TODO: replace with your widget.

forui/CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
## 0.16.0 (Next)
2+
3+
### Better Generated Documentation
4+
5+
We've improved the generated documentation for styles. They should be much easier to navigate and understand.
6+
7+
8+
### `FThemeData`
9+
We've added support for animated theme transitions. This should make transitions between themes gradual instead of abrupt.
10+
11+
* Add `FThemeData.lerp(...)`.
12+
* Change `FThemeData.copyWith(...)` to accept style builder functions.
13+
14+
15+
### `FWidgetStateMap`
16+
* Add `FWidgetStateMap.lerpBoxDecoration(...)`.
17+
* Add `FWidgetStateMap.lerpColor(...)`.
18+
* Add `FWidgetStateMap.lerpIconThemeData(...)`.
19+
* Add `FWidgetStateMap.lerpTextStyle(...)`.
20+
* Add `FWidgetStateMap.lerpWhere(...)`.
21+
22+
123
## 0.15.1
224
* Fix CLI generating incorrect icon mappings.
325
* Fix CLI generating theme that references private constant.

forui/bin/commands/snippet/snippet.dart

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

forui/lib/foundation.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ export 'src/foundation/rendering.dart' hide Alignments, DefaultData, RenderBoxes
1212
export 'src/foundation/tappable.dart' hide AnimatedTappable, AnimatedTappableState;
1313
export 'src/foundation/time.dart';
1414
export 'src/foundation/typeahead_controller.dart';
15-
export 'src/foundation/widget_states_delta.dart';
1615
export 'src/foundation/portal/portal.dart';
1716
export 'src/foundation/portal/portal_constraints.dart' hide FixedConstraints;
1817
export 'src/foundation/portal/portal_shift.dart';

forui/lib/src/foundation/focused_outline.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:ui';
2+
13
import 'package:flutter/foundation.dart';
24
import 'package:flutter/rendering.dart';
35
import 'package:flutter/widgets.dart';
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import 'package:flutter/foundation.dart';
2+
import 'package:flutter/widgets.dart';
3+
4+
import 'package:forui/forui.dart';
5+
6+
/// An animated version of [FTheme] which automatically transitions the colors, typography, and other properties over a
7+
/// given duration whenever the provided [FThemeData] changes.
8+
///
9+
/// See:
10+
/// * [FTheme] which is a non-animated version of this widget.
11+
/// * [FThemeData] which describes the actual configuration of a theme.
12+
class FAnimatedTheme extends ImplicitlyAnimatedWidget {
13+
/// Motion-related properties for the animation.
14+
final FAnimatedThemeMotion motion;
15+
16+
/// The theme.
17+
final FThemeData data;
18+
19+
/// The widget below this widget in the tree.
20+
final Widget child;
21+
22+
/// Creates an animated theme.
23+
FAnimatedTheme({
24+
required this.data,
25+
required this.child,
26+
this.motion = const FAnimatedThemeMotion(),
27+
super.onEnd,
28+
super.key,
29+
}) : super(duration: motion.duration, curve: motion.curve);
30+
31+
@override
32+
AnimatedWidgetBaseState<FAnimatedTheme> createState() => _State();
33+
34+
@override
35+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
36+
super.debugFillProperties(properties);
37+
properties
38+
..add(DiagnosticsProperty('motion', motion))
39+
..add(DiagnosticsProperty('data', data));
40+
}
41+
}
42+
43+
/// The motion-related properties for [FAnimatedTheme].
44+
class FAnimatedThemeMotion {
45+
/// The animation's duration. Defaults to 200 milliseconds.
46+
final Duration duration;
47+
48+
/// The animation's curve. Defaults to [Curves.linear].
49+
///
50+
/// We recommend [Curves.linear], especially if only the theme's colors are changing.
51+
/// See https://pow.rs/blog/animation-easings/ for more information.
52+
final Curve curve;
53+
54+
/// Creates a [FAnimatedThemeMotion].
55+
const FAnimatedThemeMotion({this.duration = const Duration(milliseconds: 200), this.curve = Curves.linear});
56+
}
57+
58+
class _State extends AnimatedWidgetBaseState<FAnimatedTheme> {
59+
_Tween? _tween;
60+
61+
@override
62+
void forEachTween(TweenVisitor<dynamic> visitor) {
63+
_tween = visitor(_tween, widget.data, (value) => _Tween(begin: value as FThemeData))! as _Tween;
64+
}
65+
66+
@override
67+
Widget build(BuildContext context) => FTheme(data: _tween!.evaluate(animation), child: widget.child);
68+
}
69+
70+
class _Tween extends Tween<FThemeData> {
71+
_Tween({super.begin});
72+
73+
@override
74+
FThemeData lerp(double t) => FThemeData.lerp(begin!, end!, t);
75+
}

forui/lib/src/theme/colors.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,29 @@ final class FColors with Diagnosticable {
159159
assert(0.0 <= hoverDarken && hoverDarken <= 1.0, 'The hoverDarken must be between 0 and 1.'),
160160
assert(0 <= disabledOpacity && disabledOpacity <= 1, 'The disabledOpacity must be between 0 and 1.');
161161

162+
/// Creates a linear interpolation between two [FColors] using the given factor [t].
163+
factory FColors.lerp(FColors a, FColors b, double t) => FColors(
164+
brightness: t < 0.5 ? a.brightness : b.brightness,
165+
systemOverlayStyle: t < 0.5 ? a.systemOverlayStyle : b.systemOverlayStyle,
166+
barrier: Color.lerp(a.barrier, b.barrier, t)!,
167+
background: Color.lerp(a.background, b.background, t)!,
168+
foreground: Color.lerp(a.foreground, b.foreground, t)!,
169+
primary: Color.lerp(a.primary, b.primary, t)!,
170+
primaryForeground: Color.lerp(a.primaryForeground, b.primaryForeground, t)!,
171+
secondary: Color.lerp(a.secondary, b.secondary, t)!,
172+
secondaryForeground: Color.lerp(a.secondaryForeground, b.secondaryForeground, t)!,
173+
muted: Color.lerp(a.muted, b.muted, t)!,
174+
mutedForeground: Color.lerp(a.mutedForeground, b.mutedForeground, t)!,
175+
destructive: Color.lerp(a.destructive, b.destructive, t)!,
176+
destructiveForeground: Color.lerp(a.destructiveForeground, b.destructiveForeground, t)!,
177+
error: Color.lerp(a.error, b.error, t)!,
178+
errorForeground: Color.lerp(a.errorForeground, b.errorForeground, t)!,
179+
border: Color.lerp(a.border, b.border, t)!,
180+
hoverLighten: lerpDouble(a.hoverLighten, b.hoverLighten, t)!,
181+
hoverDarken: lerpDouble(a.hoverDarken, b.hoverDarken, t)!,
182+
disabledOpacity: lerpDouble(a.disabledOpacity, b.disabledOpacity, t)!,
183+
);
184+
162185
/// Generates a hovered variant of the given [color] by darkening light colors and lighting dark colors based on their
163186
/// HSL lightness.
164187
///

0 commit comments

Comments
 (0)