mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-10 18:04:06 +02:00
fix(mornye): preserve navbar blur during collapse animations
This commit is contained in:
1 parent
d1eba29a4a
commit
fe30871e36
3 files changed
+323
-159
No files matched your search
@@ -61,7 +61,7 @@ class MornyeChromeController extends ValueNotifier<bool> {
|
|||||||
|
|
||||||
/// A single mini-player survives the transition, preserving its artwork Hero,
|
/// A single mini-player survives the transition, preserving its artwork Hero,
|
||||||
/// playback controls and swipe-to-dismiss state as the tabs fold away.
|
/// playback controls and swipe-to-dismiss state as the tabs fold away.
|
||||||
class MornyeBottomBar extends ConsumerWidget {
|
class MornyeBottomBar extends ConsumerStatefulWidget {
|
||||||
const MornyeBottomBar({
|
const MornyeBottomBar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.collapsed,
|
required this.collapsed,
|
||||||
@@ -82,7 +82,59 @@ class MornyeBottomBar extends ConsumerWidget {
|
|||||||
final bool blurEnabled;
|
final bool blurEnabled;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<MornyeBottomBar> createState() => _MornyeBottomBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MornyeBottomBarState extends ConsumerState<MornyeBottomBar>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final _controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 380),
|
||||||
|
value: widget.collapsed ? 1 : 0,
|
||||||
|
);
|
||||||
|
late final _collapse = CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: Curves.easeInOutCubic,
|
||||||
|
);
|
||||||
|
late final _tabOpacity = ReverseAnimation(_collapse);
|
||||||
|
|
||||||
|
void _updateCollapse() {
|
||||||
|
final target = widget.collapsed ? 1.0 : 0.0;
|
||||||
|
if (MediaQuery.disableAnimationsOf(context)) {
|
||||||
|
_controller.value = target;
|
||||||
|
} else {
|
||||||
|
_controller.animateTo(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_updateCollapse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(MornyeBottomBar oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.collapsed != widget.collapsed) _updateCollapse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_collapse.dispose();
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final collapsed = widget.collapsed;
|
||||||
|
final destinations = widget.destinations;
|
||||||
|
final selectedIndex = widget.selectedIndex;
|
||||||
|
final onSelected = widget.onSelected;
|
||||||
|
final onHome = widget.onHome;
|
||||||
|
final onSearch = widget.onSearch;
|
||||||
|
final blurEnabled = widget.blurEnabled;
|
||||||
final hasPlayer = ref.watch(
|
final hasPlayer = ref.watch(
|
||||||
currentMediaItemProvider.select((item) => item.value != null),
|
currentMediaItemProvider.select((item) => item.value != null),
|
||||||
);
|
);
|
||||||
@@ -95,14 +147,18 @@ class MornyeBottomBar extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
final tabGap = glassTabs ? 0.0 : 8.0;
|
final tabGap = glassTabs ? 0.0 : 8.0;
|
||||||
// These contents do not depend on animation progress. Retain their widget
|
// These contents do not depend on animation progress. Retain their widget
|
||||||
// instances so folding only updates size/opacity wrappers each frame.
|
// instances so folding only updates geometry and foreground opacity. A
|
||||||
Widget sideSurface() => RepaintBoundary(
|
// backdrop inside a fading layer loses access to the page behind the bar.
|
||||||
child: MornyeGlass.navigation(
|
Widget sideSurface() => ScaleTransition(
|
||||||
blurEnabled: blurEnabled,
|
scale: _collapse,
|
||||||
strongTint: true,
|
child: RepaintBoundary(
|
||||||
tintOpacity: MornyeTheme.navigationOpacity(context),
|
child: MornyeGlass.navigation(
|
||||||
radius: 26,
|
blurEnabled: blurEnabled,
|
||||||
child: const SizedBox.expand(),
|
strongTint: true,
|
||||||
|
tintOpacity: MornyeTheme.navigationOpacity(context),
|
||||||
|
radius: 26,
|
||||||
|
child: const SizedBox.expand(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final leadingSurface = sideSurface();
|
final leadingSurface = sideSurface();
|
||||||
@@ -122,6 +178,7 @@ class MornyeBottomBar extends ConsumerWidget {
|
|||||||
index == destinations.length - 1 ? onSearch() : onSelected(index),
|
index == destinations.length - 1 ? onSearch() : onSelected(index),
|
||||||
blurEnabled: blurEnabled,
|
blurEnabled: blurEnabled,
|
||||||
liquidGlass: liquidGlass,
|
liquidGlass: liquidGlass,
|
||||||
|
contentOpacity: _tabOpacity,
|
||||||
hiddenIconIndices: hideMovingIcons
|
hiddenIconIndices: hideMovingIcons
|
||||||
? {leadingIndex, destinations.length - 1}
|
? {leadingIndex, destinations.length - 1}
|
||||||
: const {},
|
: const {},
|
||||||
@@ -158,13 +215,10 @@ class MornyeBottomBar extends ConsumerWidget {
|
|||||||
(constraints.maxWidth - tabInset * 2) /
|
(constraints.maxWidth - tabInset * 2) /
|
||||||
destinations.length *
|
destinations.length *
|
||||||
(index + 0.5);
|
(index + 0.5);
|
||||||
return TweenAnimationBuilder<double>(
|
return AnimatedBuilder(
|
||||||
tween: Tween(end: collapsed ? 1 : 0),
|
animation: _collapse,
|
||||||
duration: MediaQuery.disableAnimationsOf(context)
|
builder: (context, _) {
|
||||||
? Duration.zero
|
final amount = _collapse.value;
|
||||||
: const Duration(milliseconds: 380),
|
|
||||||
curve: Curves.easeInOutCubic,
|
|
||||||
builder: (context, amount, _) {
|
|
||||||
Widget movingIcon({
|
Widget movingIcon({
|
||||||
required bool leading,
|
required bool leading,
|
||||||
required Widget surface,
|
required Widget surface,
|
||||||
@@ -188,9 +242,7 @@ class MornyeBottomBar extends ConsumerWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(child: surface),
|
||||||
child: Opacity(opacity: amount, child: surface),
|
|
||||||
),
|
|
||||||
Material(
|
Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
@@ -269,10 +321,7 @@ class MornyeBottomBar extends ConsumerWidget {
|
|||||||
ignoring: amount > 0.5,
|
ignoring: amount > 0.5,
|
||||||
child: ExcludeSemantics(
|
child: ExcludeSemantics(
|
||||||
excluding: amount > 0.5,
|
excluding: amount > 0.5,
|
||||||
child: Opacity(
|
child: amount == 0 ? fullTabs : foldingTabs,
|
||||||
opacity: 1 - amount,
|
|
||||||
child: amount == 0 ? fullTabs : foldingTabs,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
+147
-135
@@ -612,6 +612,7 @@ class MornyeTabBar extends StatelessWidget {
|
|||||||
required this.blurEnabled,
|
required this.blurEnabled,
|
||||||
this.liquidGlass = true,
|
this.liquidGlass = true,
|
||||||
this.hiddenIconIndices = const {},
|
this.hiddenIconIndices = const {},
|
||||||
|
this.contentOpacity = const AlwaysStoppedAnimation(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Shader capsule and travelling pill; otherwise the frosted row below.
|
/// Shader capsule and travelling pill; otherwise the frosted row below.
|
||||||
@@ -638,6 +639,10 @@ class MornyeTabBar extends StatelessWidget {
|
|||||||
// The active and Search icons move independently while the capsule folds.
|
// The active and Search icons move independently while the capsule folds.
|
||||||
final Set<int> hiddenIconIndices;
|
final Set<int> hiddenIconIndices;
|
||||||
|
|
||||||
|
/// Fade icons and labels while the capsule folds. Its backdrop must stay
|
||||||
|
/// outside the fade layer so it can still sample the page during motion.
|
||||||
|
final Animation<double> contentOpacity;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final scheme = Theme.of(context).colorScheme;
|
final scheme = Theme.of(context).colorScheme;
|
||||||
@@ -673,79 +678,83 @@ class MornyeTabBar extends StatelessWidget {
|
|||||||
child: const SizedBox.expand(),
|
child: const SizedBox.expand(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
NativeGlassMetrics(
|
FadeTransition(
|
||||||
child: LiquidGlassTabBar.withImpeller(
|
opacity: contentOpacity,
|
||||||
width: constraints.maxWidth,
|
child: NativeGlassMetrics(
|
||||||
height: 64,
|
child: LiquidGlassTabBar.withImpeller(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
width: constraints.maxWidth,
|
||||||
selectedIndex: selectedIndex < 0 ? 0 : selectedIndex,
|
height: 64,
|
||||||
onChanged: onSelected,
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
style: LiquidGlassTabBar.defaultStyle.copyWith(
|
selectedIndex: selectedIndex < 0 ? 0 : selectedIndex,
|
||||||
// The frosted base owns the subtle outline. Disable the
|
onChanged: onSelected,
|
||||||
// package's default specular rim around the whole capsule.
|
style: LiquidGlassTabBar.defaultStyle.copyWith(
|
||||||
shape: const LiquidGlassShape.continuousRoundedRectangle(
|
// The frosted base owns the subtle outline. Disable the
|
||||||
cornerRadius: 32,
|
// package's default specular rim around the whole capsule.
|
||||||
borderWidth: 0,
|
shape:
|
||||||
lightIntensity: 0,
|
const LiquidGlassShape.continuousRoundedRectangle(
|
||||||
),
|
cornerRadius: 32,
|
||||||
// The sibling surface already supplies tint and blur. Keep
|
borderWidth: 0,
|
||||||
// the lens clear so the moving pill can refract the icons.
|
lightIntensity: 0,
|
||||||
appearance: const LiquidGlassAppearance(),
|
|
||||||
refraction: const LiquidGlassRefraction(
|
|
||||||
distortion: 0,
|
|
||||||
chromaticAberration: 0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
itemStyle: LiquidGlassTabItemStyle(
|
|
||||||
selectedColor: scheme.primary,
|
|
||||||
unselectedColor: scheme.onSurface,
|
|
||||||
iconSize: 25,
|
|
||||||
labelFontSize: 11,
|
|
||||||
),
|
|
||||||
pillStyle: LiquidGlassTabPillStyle(
|
|
||||||
mode: LiquidGlassPillMode.impellerOnly,
|
|
||||||
show: selectedIndex >= 0,
|
|
||||||
color: selectionFill,
|
|
||||||
animated: true,
|
|
||||||
// Keep the moving refractive pill, without stacking the
|
|
||||||
// package's second magnifier lens beneath it.
|
|
||||||
magnifierPill: const LiquidGlassTabMagnifierPillStyle(
|
|
||||||
enabled: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
items: [
|
|
||||||
for (final (index, destination) in destinations.indexed)
|
|
||||||
LiquidGlassTabBarItem(
|
|
||||||
label: destination.label,
|
|
||||||
iconBuilder: (context, icon) => IconTheme(
|
|
||||||
data: IconThemeData(
|
|
||||||
size: icon.size,
|
|
||||||
color: icon.selected && selectedIndex >= 0
|
|
||||||
? scheme.primary
|
|
||||||
: inactiveIconColor,
|
|
||||||
),
|
),
|
||||||
child: Opacity(
|
// The sibling surface already supplies tint and blur. Keep
|
||||||
opacity: hiddenIconIndices.contains(index)
|
// the lens clear so the moving pill can refract the icons.
|
||||||
? 0
|
appearance: const LiquidGlassAppearance(),
|
||||||
: 1,
|
refraction: const LiquidGlassRefraction(
|
||||||
child: destination.icon,
|
distortion: 0,
|
||||||
),
|
chromaticAberration: 0,
|
||||||
),
|
|
||||||
labelBuilder: (context, label) => Text(
|
|
||||||
destination.label,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context).textTheme.labelSmall
|
|
||||||
?.copyWith(
|
|
||||||
fontSize: label.textStyle.fontSize,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: selectedIndex < 0
|
|
||||||
? scheme.onSurface
|
|
||||||
: label.textStyle.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
itemStyle: LiquidGlassTabItemStyle(
|
||||||
|
selectedColor: scheme.primary,
|
||||||
|
unselectedColor: scheme.onSurface,
|
||||||
|
iconSize: 25,
|
||||||
|
labelFontSize: 11,
|
||||||
|
),
|
||||||
|
pillStyle: LiquidGlassTabPillStyle(
|
||||||
|
mode: LiquidGlassPillMode.impellerOnly,
|
||||||
|
show: selectedIndex >= 0,
|
||||||
|
color: selectionFill,
|
||||||
|
animated: true,
|
||||||
|
// Keep the moving refractive pill, without stacking the
|
||||||
|
// package's second magnifier lens beneath it.
|
||||||
|
magnifierPill: const LiquidGlassTabMagnifierPillStyle(
|
||||||
|
enabled: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
for (final (index, destination) in destinations.indexed)
|
||||||
|
LiquidGlassTabBarItem(
|
||||||
|
label: destination.label,
|
||||||
|
iconBuilder: (context, icon) => IconTheme(
|
||||||
|
data: IconThemeData(
|
||||||
|
size: icon.size,
|
||||||
|
color: icon.selected && selectedIndex >= 0
|
||||||
|
? scheme.primary
|
||||||
|
: inactiveIconColor,
|
||||||
|
),
|
||||||
|
child: Opacity(
|
||||||
|
opacity: hiddenIconIndices.contains(index)
|
||||||
|
? 0
|
||||||
|
: 1,
|
||||||
|
child: destination.icon,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
labelBuilder: (context, label) => Text(
|
||||||
|
destination.label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.labelSmall
|
||||||
|
?.copyWith(
|
||||||
|
fontSize: label.textStyle.fontSize,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: selectedIndex < 0
|
||||||
|
? scheme.onSurface
|
||||||
|
: label.textStyle.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// With no selected tab the package still needs an internal
|
// With no selected tab the package still needs an internal
|
||||||
@@ -784,77 +793,80 @@ class MornyeTabBar extends StatelessWidget {
|
|||||||
blurEnabled: blurEnabled,
|
blurEnabled: blurEnabled,
|
||||||
strongTint: true,
|
strongTint: true,
|
||||||
tintOpacity: MornyeTheme.navigationOpacity(context),
|
tintOpacity: MornyeTheme.navigationOpacity(context),
|
||||||
child: Padding(
|
child: FadeTransition(
|
||||||
padding: const EdgeInsets.all(5),
|
opacity: contentOpacity,
|
||||||
child: Row(
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.all(5),
|
||||||
for (var index = 0; index < destinations.length; index++)
|
child: Row(
|
||||||
Expanded(
|
children: [
|
||||||
child: Semantics(
|
for (var index = 0; index < destinations.length; index++)
|
||||||
selected: index == selectedIndex,
|
Expanded(
|
||||||
button: true,
|
child: Semantics(
|
||||||
label: destinations[index].label,
|
selected: index == selectedIndex,
|
||||||
excludeSemantics: true,
|
button: true,
|
||||||
child: Material(
|
label: destinations[index].label,
|
||||||
color: Colors.transparent,
|
excludeSemantics: true,
|
||||||
child: InkWell(
|
child: Material(
|
||||||
borderRadius: BorderRadius.circular(28),
|
color: Colors.transparent,
|
||||||
onTap: () => onSelected(index),
|
child: InkWell(
|
||||||
child: AnimatedContainer(
|
borderRadius: BorderRadius.circular(28),
|
||||||
duration: MediaQuery.disableAnimationsOf(context)
|
onTap: () => onSelected(index),
|
||||||
? Duration.zero
|
child: AnimatedContainer(
|
||||||
: const Duration(milliseconds: 220),
|
duration: MediaQuery.disableAnimationsOf(context)
|
||||||
constraints: const BoxConstraints(minHeight: 54),
|
? Duration.zero
|
||||||
padding: const EdgeInsets.symmetric(
|
: const Duration(milliseconds: 220),
|
||||||
horizontal: 4,
|
constraints: const BoxConstraints(minHeight: 54),
|
||||||
vertical: 6,
|
padding: const EdgeInsets.symmetric(
|
||||||
),
|
horizontal: 4,
|
||||||
decoration: BoxDecoration(
|
vertical: 6,
|
||||||
color: index == selectedIndex
|
),
|
||||||
? selectionFill
|
decoration: BoxDecoration(
|
||||||
: Colors.transparent,
|
color: index == selectedIndex
|
||||||
borderRadius: BorderRadius.circular(28),
|
? selectionFill
|
||||||
),
|
: Colors.transparent,
|
||||||
child: Column(
|
borderRadius: BorderRadius.circular(28),
|
||||||
mainAxisSize: MainAxisSize.min,
|
),
|
||||||
children: [
|
child: Column(
|
||||||
IconTheme(
|
mainAxisSize: MainAxisSize.min,
|
||||||
data: IconThemeData(
|
children: [
|
||||||
size: 25,
|
IconTheme(
|
||||||
color: index == selectedIndex
|
data: IconThemeData(
|
||||||
? scheme.primary
|
size: 25,
|
||||||
: inactiveIconColor,
|
color: index == selectedIndex
|
||||||
|
? scheme.primary
|
||||||
|
: inactiveIconColor,
|
||||||
|
),
|
||||||
|
// Keep tab selection quiet, as in Mornye. Badges
|
||||||
|
// stay live without the Material bounce/spin.
|
||||||
|
child: Opacity(
|
||||||
|
opacity: hiddenIconIndices.contains(index)
|
||||||
|
? 0
|
||||||
|
: 1,
|
||||||
|
child: destinations[index].icon,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
// Keep tab selection quiet, as in Mornye. Badges
|
const SizedBox(height: 2),
|
||||||
// stay live without the Material bounce/spin.
|
Text(
|
||||||
child: Opacity(
|
destinations[index].label,
|
||||||
opacity: hiddenIconIndices.contains(index)
|
maxLines: 1,
|
||||||
? 0
|
overflow: TextOverflow.ellipsis,
|
||||||
: 1,
|
style: Theme.of(context).textTheme.labelSmall
|
||||||
child: destinations[index].icon,
|
?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: index == selectedIndex
|
||||||
|
? scheme.primary
|
||||||
|
: scheme.onSurface,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
const SizedBox(height: 2),
|
),
|
||||||
Text(
|
|
||||||
destinations[index].label,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context).textTheme.labelSmall
|
|
||||||
?.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: index == selectedIndex
|
|
||||||
? scheme.primary
|
|
||||||
: scheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ void main() {
|
|||||||
WidgetTester tester, {
|
WidgetTester tester, {
|
||||||
bool withPlayer = true,
|
bool withPlayer = true,
|
||||||
bool blur = false,
|
bool blur = false,
|
||||||
|
bool liquidGlass = true,
|
||||||
Brightness brightness = Brightness.light,
|
Brightness brightness = Brightness.light,
|
||||||
Color? backdrop,
|
Color? backdrop,
|
||||||
Color? chromeSurface,
|
Color? chromeSurface,
|
||||||
@@ -67,6 +68,7 @@ void main() {
|
|||||||
playbackStateProvider.overrideWith((ref) => const Stream.empty()),
|
playbackStateProvider.overrideWith((ref) => const Stream.empty()),
|
||||||
lowEndDeviceProvider.overrideWithValue(!blur),
|
lowEndDeviceProvider.overrideWithValue(!blur),
|
||||||
backdropBlurEnabledProvider.overrideWithValue(blur),
|
backdropBlurEnabledProvider.overrideWithValue(blur),
|
||||||
|
mornyeLiquidGlassProvider.overrideWithValue(liquidGlass),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
theme: MornyeTheme.build(
|
theme: MornyeTheme.build(
|
||||||
@@ -135,6 +137,91 @@ void main() {
|
|||||||
|
|
||||||
const albumBlue = Color(0xff464566);
|
const albumBlue = Color(0xff464566);
|
||||||
|
|
||||||
|
for (final liquidGlass in [false, true]) {
|
||||||
|
testWidgets(
|
||||||
|
'navbar keeps its backdrop blurred throughout motion (liquid: $liquidGlass)',
|
||||||
|
(tester) async {
|
||||||
|
await pumpShell(
|
||||||
|
tester,
|
||||||
|
blur: true,
|
||||||
|
liquidGlass: liquidGlass,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
glassClarity: 1,
|
||||||
|
body: const SizedBox.expand(
|
||||||
|
child: CustomPaint(painter: _StripedBackdrop()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> expectBlur(Offset center, String phase) async {
|
||||||
|
final variation = await tester.runAsync(() async {
|
||||||
|
final boundary = tester.renderObject<RenderRepaintBoundary>(
|
||||||
|
find.byKey(capture),
|
||||||
|
);
|
||||||
|
final image = await boundary.toImage();
|
||||||
|
final bytes = (await image.toByteData(
|
||||||
|
format: ui.ImageByteFormat.rawRgba,
|
||||||
|
))!;
|
||||||
|
final values = <int>[];
|
||||||
|
for (var x = -9; x <= 9; x++) {
|
||||||
|
final offset =
|
||||||
|
(center.dy.floor() * image.width + center.dx.floor() + x) * 4;
|
||||||
|
values.add(bytes.getUint8(offset));
|
||||||
|
}
|
||||||
|
image.dispose();
|
||||||
|
values.sort();
|
||||||
|
return values.last - values.first;
|
||||||
|
});
|
||||||
|
expect(variation, lessThan(20), reason: phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
const capsuleSample = Offset(196, 745);
|
||||||
|
await expectBlur(capsuleSample, 'Expanded capsule');
|
||||||
|
final gesture = await tester.startGesture(
|
||||||
|
tester.getCenter(find.text('Home').first),
|
||||||
|
);
|
||||||
|
await gesture.moveBy(const Offset(100, 0));
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
await expectBlur(capsuleSample, 'Dragging the selected tab');
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
for (final collapsed in [true, false]) {
|
||||||
|
chrome.value = collapsed;
|
||||||
|
await tester.pump();
|
||||||
|
if (collapsed) {
|
||||||
|
await tester.pump(const Duration(milliseconds: 190));
|
||||||
|
await expectBlur(
|
||||||
|
capsuleSample,
|
||||||
|
'Capsule halfway to collapsed=$collapsed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Sample below the icons once the growing circles are large enough to
|
||||||
|
// expose their backdrop; the page stripes must stay blurred while moving.
|
||||||
|
await tester.pump(const Duration(milliseconds: 95));
|
||||||
|
for (final key in [
|
||||||
|
'mornye-compact-leading',
|
||||||
|
'mornye-compact-search',
|
||||||
|
]) {
|
||||||
|
final center = tester.getCenter(find.byKey(ValueKey(key)));
|
||||||
|
await expectBlur(
|
||||||
|
center + const Offset(0, 18),
|
||||||
|
'$key moving to collapsed=$collapsed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!collapsed) {
|
||||||
|
await tester.pump(const Duration(milliseconds: 95));
|
||||||
|
await expectBlur(
|
||||||
|
capsuleSample,
|
||||||
|
'Capsule halfway to collapsed=$collapsed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for (final blur in [false, true]) {
|
for (final blur in [false, true]) {
|
||||||
testWidgets('active edge icons and tabs stay synchronized (glass: $blur)', (
|
testWidgets('active edge icons and tabs stay synchronized (glass: $blur)', (
|
||||||
tester,
|
tester,
|
||||||
@@ -718,3 +805,19 @@ void main() {
|
|||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _StripedBackdrop extends CustomPainter {
|
||||||
|
const _StripedBackdrop();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint();
|
||||||
|
for (var x = 0.0; x < size.width; x += 12) {
|
||||||
|
paint.color = (x / 12).round().isEven ? Colors.white : Colors.black;
|
||||||
|
canvas.drawRect(Rect.fromLTWH(x, 0, 12, size.height), paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_StripedBackdrop oldDelegate) => false;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user