From 6005f016e36e2087b63cc75978647a337aac4a31 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 3 Oct 2022 17:00:08 +0200 Subject: [PATCH] caption change effect --- lib/theme/durations.dart | 3 + lib/widgets/common/basic/animated_text.dart | 86 +++++++++++++++++++ lib/widgets/common/basic/circle.dart | 8 +- .../common/basic/multi_cross_fader.dart | 2 +- lib/widgets/common/identity/aves_caption.dart | 42 +++++++++ lib/widgets/common/search/page.dart | 2 +- lib/widgets/dialogs/tile_view_dialog.dart | 8 +- lib/widgets/settings/common/tiles.dart | 3 +- .../privacy/file_picker/crumb_line.dart | 2 +- 9 files changed, 142 insertions(+), 14 deletions(-) create mode 100644 lib/widgets/common/basic/animated_text.dart create mode 100644 lib/widgets/common/identity/aves_caption.dart diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index 34bbf9fe8..cb291c8f1 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -96,6 +96,7 @@ class DurationsData { // common animations final Duration expansionTileAnimation; final Duration formTransition; + final Duration formTextStyleTransition; final Duration chartTransition; final Duration iconAnimation; final Duration staggeredAnimation; @@ -112,6 +113,7 @@ class DurationsData { const DurationsData({ this.expansionTileAnimation = const Duration(milliseconds: 200), this.formTransition = const Duration(milliseconds: 200), + this.formTextStyleTransition = const Duration(milliseconds: 800), this.chartTransition = const Duration(milliseconds: 400), this.iconAnimation = const Duration(milliseconds: 300), this.staggeredAnimation = const Duration(milliseconds: 375), @@ -126,6 +128,7 @@ class DurationsData { // as of Flutter v2.5.1, `ExpansionPanelList` throws if animation duration is zero expansionTileAnimation: const Duration(microseconds: 1), formTransition: Duration.zero, + formTextStyleTransition: Duration.zero, chartTransition: Duration.zero, iconAnimation: Duration.zero, staggeredAnimation: Duration.zero, diff --git a/lib/widgets/common/basic/animated_text.dart b/lib/widgets/common/basic/animated_text.dart new file mode 100644 index 000000000..80c7c04a9 --- /dev/null +++ b/lib/widgets/common/basic/animated_text.dart @@ -0,0 +1,86 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +class AnimatedText extends StatefulWidget { + final String data; + final TextStyle style, changedStyle; + final Curve curve; + final Duration duration; + + const AnimatedText( + this.data, { + super.key, + required this.style, + required this.changedStyle, + this.curve = Curves.linear, + required this.duration, + }); + + @override + State createState() => _AnimatedTextState(); +} + +class _AnimatedTextState extends State with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _style; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: widget.duration, + vsync: this, + ) + ..value = 1 + ..addListener(() => setState(() {})); + _style = _ShadowedTextStyleTween(begin: widget.changedStyle, end: widget.style).animate(CurvedAnimation( + parent: _controller, + curve: widget.curve, + )); + } + + @override + void didUpdateWidget(AnimatedText oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.data != widget.data) { + _controller + ..value = 0 + ..forward(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Text( + widget.data, + style: _style.value, + ); + } +} + +class _ShadowedTextStyleTween extends Tween { + _ShadowedTextStyleTween({super.begin, super.end}); + + @override + TextStyle lerp(double t) { + final textStyle = TextStyle.lerp(begin, end, t)!; + final beginShadows = begin!.shadows; + final endShadows = end!.shadows; + if (beginShadows != null && endShadows != null && beginShadows.length == endShadows.length) { + return textStyle.copyWith( + shadows: beginShadows.mapIndexed((i, a) { + final b = endShadows[i]; + return Shadow.lerp(a, b, t)!; + }).toList(), + ); + } else { + return textStyle; + } + } +} diff --git a/lib/widgets/common/basic/circle.dart b/lib/widgets/common/basic/circle.dart index d829c6993..9bde9fb65 100644 --- a/lib/widgets/common/basic/circle.dart +++ b/lib/widgets/common/basic/circle.dart @@ -29,13 +29,13 @@ class _CircularIndicatorState extends State { child: Stack( alignment: Alignment.center, children: [ - Circle( + _Circle( radius: widget.radius, lineWidth: widget.lineWidth, percent: 1.0, color: widget.background, ), - Circle( + _Circle( radius: widget.radius, lineWidth: widget.lineWidth, percent: widget.percent, @@ -48,11 +48,11 @@ class _CircularIndicatorState extends State { } } -class Circle extends StatelessWidget { +class _Circle extends StatelessWidget { final double radius, lineWidth, percent; final Color color; - const Circle({ + const _Circle({ super.key, required this.radius, required this.lineWidth, diff --git a/lib/widgets/common/basic/multi_cross_fader.dart b/lib/widgets/common/basic/multi_cross_fader.dart index ba3d01c01..17a07ad9b 100644 --- a/lib/widgets/common/basic/multi_cross_fader.dart +++ b/lib/widgets/common/basic/multi_cross_fader.dart @@ -33,7 +33,7 @@ class _MultiCrossFaderState extends State { @override void didUpdateWidget(covariant MultiCrossFader oldWidget) { super.didUpdateWidget(oldWidget); - if (_first == oldWidget.child) { + if (oldWidget.child == _first) { _second = widget.child; _fadeState = CrossFadeState.showSecond; } else { diff --git a/lib/widgets/common/identity/aves_caption.dart b/lib/widgets/common/identity/aves_caption.dart new file mode 100644 index 000000000..ce40356bd --- /dev/null +++ b/lib/widgets/common/identity/aves_caption.dart @@ -0,0 +1,42 @@ +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/basic/animated_text.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AvesCaption extends StatelessWidget { + final String data; + + const AvesCaption( + this.data, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final subtitleStyle = theme.textTheme.caption!; + final subtitleChangeShadowColor = theme.colorScheme.onPrimary; + return AnimatedText( + // provide key to refresh on theme brightness change + key: ValueKey(subtitleChangeShadowColor), + data, + style: subtitleStyle.copyWith( + shadows: [ + Shadow( + color: subtitleChangeShadowColor.withOpacity(0), + blurRadius: 0, + ) + ], + ), + changedStyle: subtitleStyle.copyWith( + shadows: [ + Shadow( + color: subtitleChangeShadowColor, + blurRadius: 3, + ) + ], + ), + duration: context.read().formTextStyleTransition, + ); + } +} diff --git a/lib/widgets/common/search/page.dart b/lib/widgets/common/search/page.dart index 9c21eb8d4..15ac0ccdd 100644 --- a/lib/widgets/common/search/page.dart +++ b/lib/widgets/common/search/page.dart @@ -38,7 +38,7 @@ class _SearchPageState extends State { @override void didUpdateWidget(covariant SearchPage oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.delegate != oldWidget.delegate) { + if (oldWidget.delegate != widget.delegate) { _unregisterWidget(oldWidget); _registerWidget(widget); } diff --git a/lib/widgets/dialogs/tile_view_dialog.dart b/lib/widgets/dialogs/tile_view_dialog.dart index 0e72b5f2c..e00c57d50 100644 --- a/lib/widgets/dialogs/tile_view_dialog.dart +++ b/lib/widgets/dialogs/tile_view_dialog.dart @@ -3,6 +3,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/basic/text_dropdown_button.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/aves_caption.dart'; import 'package:aves/widgets/common/identity/highlight_title.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -76,12 +77,7 @@ class _TileViewDialogState extends State> with _selectedSort = v; _reverseSort = false; }, - bottom: _selectedSort != null - ? Text( - widget.sortOrder(_selectedSort as S, _reverseSort), - style: Theme.of(context).textTheme.caption, - ) - : null, + bottom: _selectedSort != null ? AvesCaption(widget.sortOrder(_selectedSort as S, _reverseSort)) : null, ), AnimatedSwitcher( duration: context.read().formTransition, diff --git a/lib/widgets/settings/common/tiles.dart b/lib/widgets/settings/common/tiles.dart index 13b3d606b..ceab95463 100644 --- a/lib/widgets/settings/common/tiles.dart +++ b/lib/widgets/settings/common/tiles.dart @@ -1,5 +1,6 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/identity/aves_caption.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -103,7 +104,7 @@ class SettingsSelectionListTile extends StatelessWidget { selector: selector, builder: (context, current, child) => ListTile( title: Text(tileTitle), - subtitle: Text(getName(context, current)), + subtitle: AvesCaption(getName(context, current)), onTap: () => showSelectionDialog( context: context, builder: (context) => AvesSelectionDialog( diff --git a/lib/widgets/settings/privacy/file_picker/crumb_line.dart b/lib/widgets/settings/privacy/file_picker/crumb_line.dart index e38838d64..0c6a12ad9 100644 --- a/lib/widgets/settings/privacy/file_picker/crumb_line.dart +++ b/lib/widgets/settings/privacy/file_picker/crumb_line.dart @@ -25,7 +25,7 @@ class _CrumbLineState extends State { @override void didUpdateWidget(covariant CrumbLine oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.directory.relativeDir.length > oldWidget.directory.relativeDir.length) { + if (oldWidget.directory.relativeDir.length < widget.directory.relativeDir.length) { // scroll to show last crumb WidgetsBinding.instance.addPostFrameCallback((_) { final extent = _controller.position.maxScrollExtent;