mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-01 21:59:43 +02:00
perf(mornye): share chrome backdrops and scale blur to clarity
This commit is contained in:
1 parent
718e13f24e
commit
e200879d14
4 files changed
+186
-71
No files matched your search
@@ -283,50 +283,55 @@ class _MornyeBottomBarState extends ConsumerState<MornyeBottomBar>
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (hasPlayer || amount > 0)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
// With no track, introducing the row at full height would
|
||||
// make the bar jump taller on the first animation frame.
|
||||
heightFactor: hasPlayer ? 1 : amount,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: hasPlayer
|
||||
? tabGap + (8 - tabGap) * amount
|
||||
: 8,
|
||||
// Only the non-overlapping player and tab surfaces share a
|
||||
// backdrop. Moving side capsules stay outside this group: they
|
||||
// cross the tab bar while folding and need separate samples.
|
||||
BackdropGroup(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (hasPlayer || amount > 0)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
// With no track, introducing the row at full height would
|
||||
// make the bar jump taller on the first animation frame.
|
||||
heightFactor: hasPlayer ? 1 : amount,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: hasPlayer
|
||||
? tabGap + (8 - tabGap) * amount
|
||||
: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 60 * amount,
|
||||
height: hasPlayer ? 48 + 4 * amount : 52,
|
||||
),
|
||||
Expanded(child: player),
|
||||
SizedBox(
|
||||
width: 60 * amount,
|
||||
height: hasPlayer ? 48 + 4 * amount : 52,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 60 * amount,
|
||||
height: hasPlayer ? 48 + 4 * amount : 52,
|
||||
),
|
||||
Expanded(child: player),
|
||||
SizedBox(
|
||||
width: 60 * amount,
|
||||
height: hasPlayer ? 48 + 4 * amount : 52,
|
||||
),
|
||||
],
|
||||
),
|
||||
ClipRect(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
heightFactor: 1 - amount,
|
||||
child: IgnorePointer(
|
||||
ignoring: amount > 0.5,
|
||||
child: ExcludeSemantics(
|
||||
excluding: amount > 0.5,
|
||||
child: amount == 0 ? fullTabs : foldingTabs,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ClipRect(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
heightFactor: 1 - amount,
|
||||
child: IgnorePointer(
|
||||
ignoring: amount > 0.5,
|
||||
child: ExcludeSemantics(
|
||||
excluding: amount > 0.5,
|
||||
child: amount == 0 ? fullTabs : foldingTabs,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
movingIcon(leading: true, surface: leadingSurface),
|
||||
movingIcon(leading: false, surface: searchSurface),
|
||||
|
||||
@@ -357,36 +357,42 @@ class MornyeGlass extends StatelessWidget {
|
||||
|
||||
/// Keep the capsule legible independently of the shader renderer. The liquid
|
||||
/// lens adds refraction above this frosted base, never above bare page text.
|
||||
class _MornyeGlassSurface extends StatelessWidget {
|
||||
static final _backdropBlur = ImageFilter.blur(sigmaX: 18, sigmaY: 18);
|
||||
class _MornyeGlassSurface extends ConsumerWidget {
|
||||
// Reuse a small set of filters rather than constructing a new blur for every
|
||||
// slider frame. Nearly opaque glass needs a much smaller sampling radius.
|
||||
static final _backdropBlurs = [
|
||||
for (final sigma in [4.0, 8.0, 12.0, 18.0])
|
||||
ImageFilter.blur(sigmaX: sigma, sigmaY: sigma),
|
||||
];
|
||||
// Reduce luminance without flattening the backdrop's color differences.
|
||||
// Subtract 35% of Rec.709 luma from every channel: white is bounded at 0.65,
|
||||
// while colored artwork stays visible without per-frame pixel readback.
|
||||
static final _darkBackdrop = ImageFilter.compose(
|
||||
outer: const ColorFilter.matrix([
|
||||
0.92559,
|
||||
-0.25032,
|
||||
-0.02527,
|
||||
0,
|
||||
0,
|
||||
-0.07441,
|
||||
0.74968,
|
||||
-0.02527,
|
||||
0,
|
||||
0,
|
||||
-0.07441,
|
||||
-0.25032,
|
||||
0.97473,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
]),
|
||||
inner: _backdropBlur,
|
||||
);
|
||||
static const _darken = ColorFilter.matrix([
|
||||
0.92559,
|
||||
-0.25032,
|
||||
-0.02527,
|
||||
0,
|
||||
0,
|
||||
-0.07441,
|
||||
0.74968,
|
||||
-0.02527,
|
||||
0,
|
||||
0,
|
||||
-0.07441,
|
||||
-0.25032,
|
||||
0.97473,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
]);
|
||||
static final _darkBackdrops = [
|
||||
for (final blur in _backdropBlurs)
|
||||
ImageFilter.compose(outer: _darken, inner: blur),
|
||||
];
|
||||
|
||||
const _MornyeGlassSurface({
|
||||
required this.child,
|
||||
@@ -413,12 +419,21 @@ class _MornyeGlassSurface extends StatelessWidget {
|
||||
final bool samplesBackdrop;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final clarity = MornyeTheme.glassClarityOf(context);
|
||||
final useBlur =
|
||||
blurEnabled && clarity > 0 && !MediaQuery.highContrastOf(context);
|
||||
final dark = scheme.brightness == Brightness.dark;
|
||||
final frosted =
|
||||
ref.watch(mornyeGlassLevelProvider) == MornyeGlassLevel.frosted;
|
||||
final filterIndex = clarity <= 0.25
|
||||
? 0
|
||||
: frosted || clarity <= 0.50
|
||||
? 1
|
||||
: clarity <= 0.75
|
||||
? 2
|
||||
: 3;
|
||||
final shape = BorderRadius.vertical(
|
||||
top: firstInGroup ? Radius.circular(radius) : Radius.zero,
|
||||
bottom: lastInGroup ? Radius.circular(radius) : Radius.zero,
|
||||
@@ -434,6 +449,10 @@ class _MornyeGlassSurface extends StatelessWidget {
|
||||
baseTint,
|
||||
)
|
||||
: scheme.surfaceContainerHigh;
|
||||
// On mid-range GPUs an almost opaque tint hides the blur anyway. Keep its
|
||||
// translucency and rim without paying for a backdrop pass at low clarity.
|
||||
final sampleBackdrop =
|
||||
useBlur && samplesBackdrop && (!frosted || tint.a < 0.95);
|
||||
final surface = DecoratedBox(
|
||||
decoration: BoxDecoration(color: tint, borderRadius: shape),
|
||||
child: child,
|
||||
@@ -463,10 +482,13 @@ class _MornyeGlassSurface extends StatelessWidget {
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: shape,
|
||||
child: useBlur && samplesBackdrop
|
||||
? BackdropFilter(
|
||||
child: sampleBackdrop
|
||||
? BackdropFilter.grouped(
|
||||
filter:
|
||||
backdropFilter ?? (dark ? _darkBackdrop : _backdropBlur),
|
||||
backdropFilter ??
|
||||
(dark
|
||||
? _darkBackdrops[filterIndex]
|
||||
: _backdropBlurs[filterIndex]),
|
||||
child: surface,
|
||||
)
|
||||
: surface,
|
||||
|
||||
@@ -137,6 +137,44 @@ void main() {
|
||||
|
||||
const albumBlue = Color(0xff464566);
|
||||
|
||||
testWidgets(
|
||||
'player and navbar share a backdrop while moving capsules remain isolated',
|
||||
(tester) async {
|
||||
await pumpShell(tester, blur: true, liquidGlass: false);
|
||||
void expectSharedBackdrop() {
|
||||
final filters = find.descendant(
|
||||
of: find.byType(MornyeBottomBar),
|
||||
matching: find.byType(BackdropFilter),
|
||||
);
|
||||
final renderers = filters
|
||||
.evaluate()
|
||||
.map(
|
||||
(element) => element.findRenderObject()! as RenderBackdropFilter,
|
||||
)
|
||||
.toList();
|
||||
final shared = renderers
|
||||
.where((filter) => filter.backdropKey != null)
|
||||
.toList();
|
||||
expect(shared, hasLength(2));
|
||||
expect(shared[0].backdropKey, same(shared[1].backdropKey));
|
||||
// Side capsules overlap the folding tabs and must not join their group.
|
||||
expect(
|
||||
renderers.where((filter) => filter.backdropKey == null),
|
||||
hasLength(2),
|
||||
);
|
||||
}
|
||||
|
||||
expectSharedBackdrop();
|
||||
chrome.value = true;
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 190));
|
||||
expectSharedBackdrop();
|
||||
await tester.pumpAndSettle();
|
||||
expectSharedBackdrop();
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
for (final liquidGlass in [false, true]) {
|
||||
testWidgets(
|
||||
'navbar keeps its backdrop blurred throughout motion (liquid: $liquidGlass)',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -115,6 +117,54 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'low clarity uses a small blur and mid-range glass caps its radius',
|
||||
(tester) async {
|
||||
Future<ui.ImageFilter?> filterFor(double clarity, bool liquid) async {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
key: UniqueKey(),
|
||||
overrides: [
|
||||
mornyeGlassLevelProvider.overrideWithValue(
|
||||
liquid ? MornyeGlassLevel.liquid : MornyeGlassLevel.frosted,
|
||||
),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: MornyeTheme.build(Brightness.light, glassClarity: clarity),
|
||||
home: const MornyeGlass.navigation(
|
||||
blurEnabled: true,
|
||||
child: SizedBox(width: 200, height: 50),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final filters = tester.widgetList<BackdropFilter>(
|
||||
find.byType(BackdropFilter),
|
||||
);
|
||||
return filters.isEmpty ? null : filters.single.filter;
|
||||
}
|
||||
|
||||
expect(await filterFor(0, false), isNull);
|
||||
expect(await filterFor(0.1, false), isNull);
|
||||
expect(
|
||||
await filterFor(0.25, false),
|
||||
ui.ImageFilter.blur(sigmaX: 4, sigmaY: 4),
|
||||
);
|
||||
expect(
|
||||
await filterFor(0.1, true),
|
||||
ui.ImageFilter.blur(sigmaX: 4, sigmaY: 4),
|
||||
);
|
||||
expect(
|
||||
await filterFor(1, false),
|
||||
ui.ImageFilter.blur(sigmaX: 8, sigmaY: 8),
|
||||
);
|
||||
expect(
|
||||
await filterFor(1, true),
|
||||
ui.ImageFilter.blur(sigmaX: 18, sigmaY: 18),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
for (final clarity in [0.0, 0.5]) {
|
||||
testWidgets('liquid tab and segment pills follow clarity ($clarity)', (
|
||||
tester,
|
||||
|
||||
Reference in new issue
Block a user