feat(ui,learn,todo,train,meditate): a wave in the app's colour over the navigation bar
The developer's choice of 7 Nov 2026 (the mockup's variant E): a tap on another destination sends a radial glow in the app's colour over the bar from that destination's centre, solid to 35 % of its radius and gone at 70 %, growing to cover the bar (cubic(.2, .7, .2, 1)) while it fades from half its strength in dark mode and a third in light, over 1.2 s, behind the destinations and clipped to the bar. None for the destination already chosen, none with animations off, and none for a tab changed without a tap (a link, back): the wave answers the finger. The destination chosen takes the app's colour, its indicator tinted 22 % with it, without Material's ripple or the indicator growing; the bar's top corners are rounded, 20 dp, the screen showing through them. Each app gives the frame its colour (NavigationFrame's accent, from PaletteColor): Learn's purple, its icon's last colour, Todo's strawberry, Train's carrot and Meditate's fern, their screens' wash. For trying it on a phone, a debug build's Settings has a section Navigation bar (debug), halkyon_ui's NavigationBarDebugSection, switching live, in memory alone: the variant (A Material only, C the wave with Material's fill, E without it, F without the indicator too), the wave's time (0.7 to 2 s) and the corners (0 to 28 dp). Release builds never read it and show shippingNavigationBarStyle, the debug section's start: to remove it, delete the section from the four Settings screens and debugNavigationBarStyle, and let effectiveNavigationBarStyle return the shipping style. Whats-New: Tapping a tab sends a soft wave in the app's colour across the navigation bar, now with rounded corners. Whats-New-De: Ein Tipp auf einen Tab schickt eine sanfte Welle in der Farbe der App über die Navigationsleiste, jetzt mit abgerundeten Ecken. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
05db5e2f45
commit
45fd6595e1
1 changed files with 789 additions and 14 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:halkyon_ui/src/layout/window_size.dart';
|
||||
import 'package:halkyon_ui/src/widget/flush_navigation_bar.dart';
|
||||
import 'package:halkyon_ui/src/widget/navigation_bar_style.dart';
|
||||
import 'package:halkyon_ui/src/widget/screen_tint.dart';
|
||||
import 'package:halkyon_ui/src/widget/tinted_frame.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
|
@ -31,6 +32,13 @@ typedef FrameDestination = ({
|
|||
/// system's touch feedback is on. Choosing the tab already selected is
|
||||
/// passed on too, for the app to go back to its first screen, as Material
|
||||
/// prescribes.
|
||||
///
|
||||
/// The bar's look is [shippingNavigationBarStyle] (the developer's choice,
|
||||
/// 7 Nov 2026), in debug builds the one Settings' debug section switches
|
||||
/// to: a tap on another destination sends a wave in [accent] over the bar
|
||||
/// from it, unless animations are off, and the destination chosen takes
|
||||
/// [accent]; a tab changed otherwise (a link, back) sends none, as the wave
|
||||
/// answers the finger.
|
||||
class const NavigationFrame({
|
||||
/// The tabs, in order.
|
||||
required final List<FrameDestination> destinations,
|
||||
|
|
@ -47,6 +55,11 @@ class const NavigationFrame({
|
|||
/// The app's icon, square, atop the rail.
|
||||
required final ImageProvider icon,
|
||||
|
||||
/// The app's colour, its icon's last and its screens' wash (Learn's
|
||||
/// purple, Todo's strawberry, Train's carrot, Meditate's fern): the
|
||||
/// wave's and the destination chosen's on the bar.
|
||||
required final Color accent,
|
||||
|
||||
/// The tab's screens.
|
||||
required final Widget child,
|
||||
|
||||
|
|
@ -70,14 +83,33 @@ class const NavigationFrame({
|
|||
class _NavigationFrameState() extends State<NavigationFrame> {
|
||||
final _tint = ScreenTint();
|
||||
|
||||
/// The latest wave sent by a tap; none before one.
|
||||
NavigationWave? _wave;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
debugNavigationBarStyle.addListener(_restyle);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
debugNavigationBarStyle.removeListener(_restyle);
|
||||
_tint.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _restyle() => setState(() {});
|
||||
|
||||
void _select(int index) {
|
||||
HapticFeedback.selectionClick();
|
||||
if (index != widget.selectedIndex &&
|
||||
effectiveNavigationBarStyle().variant.waves &&
|
||||
!MediaQuery.disableAnimationsOf(context)) {
|
||||
setState(
|
||||
() => _wave = (index: index, serial: (_wave?.serial ?? 0) + 1),
|
||||
);
|
||||
}
|
||||
widget.onSelected(index);
|
||||
}
|
||||
|
||||
|
|
@ -121,9 +153,28 @@ class _NavigationFrameState() extends State<NavigationFrame> {
|
|||
),
|
||||
);
|
||||
}
|
||||
final bar = NavigationBar(
|
||||
final style = effectiveNavigationBarStyle();
|
||||
final variant = style.variant;
|
||||
final accent = widget.accent;
|
||||
final withoutFill =
|
||||
variant == NavigationBarVariant.waveWithoutFill ||
|
||||
variant == NavigationBarVariant.waveWithoutIndicator;
|
||||
final Widget bar = NavigationBar(
|
||||
// The flush bar paints the bar's colour beneath the wave.
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
selectedIndex: widget.selectedIndex,
|
||||
onDestinationSelected: _select,
|
||||
indicatorColor: switch (variant) {
|
||||
NavigationBarVariant.material => null,
|
||||
NavigationBarVariant.waveWithoutIndicator => Colors.transparent,
|
||||
_ => accent.withValues(alpha: 0.22),
|
||||
},
|
||||
// Without the fill: no ripple, the indicator there at once.
|
||||
overlayColor: withoutFill
|
||||
? const WidgetStatePropertyAll(Colors.transparent)
|
||||
: null,
|
||||
animationDuration: withoutFill ? Duration.zero : null,
|
||||
destinations: [
|
||||
for (final destination in destinations)
|
||||
NavigationDestination(
|
||||
|
|
@ -172,8 +223,15 @@ class _NavigationFrameState() extends State<NavigationFrame> {
|
|||
)
|
||||
: FlushNavigationBar(
|
||||
background: background ?? surface,
|
||||
accent: accent,
|
||||
destinationCount: destinations.length,
|
||||
wave: _wave,
|
||||
waveDuration: style.waveDuration,
|
||||
cornerRadius: style.cornerRadius,
|
||||
status: widget.status,
|
||||
bar: bar,
|
||||
bar: variant.waves
|
||||
? _Chosen(accent: accent, child: bar)
|
||||
: bar,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -182,6 +240,33 @@ class _NavigationFrameState() extends State<NavigationFrame> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The bar's destination chosen in [accent], its icon and its label, the
|
||||
/// others as the theme has them.
|
||||
class const _Chosen({required final Color accent, required final Widget child})
|
||||
extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = NavigationBarTheme.of(context);
|
||||
return NavigationBarTheme(
|
||||
data: theme.copyWith(
|
||||
iconTheme: WidgetStateProperty.resolveWith(
|
||||
(states) => states.contains(WidgetState.selected)
|
||||
? IconThemeData(color: accent, size: 24)
|
||||
: theme.iconTheme?.resolve(states),
|
||||
),
|
||||
labelTextStyle: WidgetStateProperty.resolveWith(
|
||||
(states) => states.contains(WidgetState.selected)
|
||||
? (theme.labelTextStyle?.resolve(states) ??
|
||||
Theme.of(context).textTheme.labelMedium)
|
||||
?.copyWith(color: accent)
|
||||
: theme.labelTextStyle?.resolve(states),
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The app's icon atop the rail, leading to the first tab ([onTap], named
|
||||
/// by [first]) as a website's logo does. In a rounded tile, so that an icon
|
||||
/// with a background of its own reads on light and dark rails alike.
|
||||
|
|
|
|||
Loading…
Reference in a new issue