mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 06:28:27 +02:00
fix(glass): swell navbar glass without resizing icons
This commit is contained in:
1 parent
a56f206c43
commit
224961d68d
4 files changed
+111
-54
No files matched your search
@@ -92,7 +92,10 @@ void main() {
|
||||
return builder.animation as CurvedAnimation;
|
||||
}
|
||||
|
||||
Future<void> capture(String name) async {
|
||||
Future<void> capture(
|
||||
String name, {
|
||||
Map<String, Object?> details = const {},
|
||||
}) async {
|
||||
await tester.pump();
|
||||
// Android can acquire the previously displayed image before the new
|
||||
// Flutter frame reaches its image view. Prime that view, then capture the
|
||||
@@ -122,6 +125,7 @@ void main() {
|
||||
// ignore: invalid_use_of_protected_member
|
||||
visit(tester.binding.renderViews.first.layer!);
|
||||
poses[name] = {
|
||||
...details,
|
||||
'collapseProgress': fold().value,
|
||||
'controllerProgress': fold().parent.value,
|
||||
'shaderBackdrops': histogram['_ImpellerShaderBackdropLayer'] ?? 0,
|
||||
@@ -240,6 +244,41 @@ void main() {
|
||||
);
|
||||
final home = tab('Home');
|
||||
final search = tab('Search');
|
||||
Rect capsule() => tester.getRect(
|
||||
find.descendant(
|
||||
of: find.byType(MornyeTabBar),
|
||||
matching: find.byType(MornyeGlass),
|
||||
),
|
||||
);
|
||||
Map<String, Rect> foreground() => {
|
||||
for (final entry in const {
|
||||
'Home': Icons.home,
|
||||
'Library': Icons.music_note,
|
||||
'Repo': Icons.grid_view,
|
||||
'Search': Icons.search,
|
||||
}.entries) ...{
|
||||
'${entry.key}_icon': tester.getRect(
|
||||
find.descendant(
|
||||
of: find.byType(MornyeTabBar),
|
||||
matching: find.byIcon(entry.value),
|
||||
),
|
||||
),
|
||||
'${entry.key}_label': tester.getRect(
|
||||
find.descendant(
|
||||
of: find.byType(MornyeTabBar),
|
||||
matching: find.text(entry.key),
|
||||
),
|
||||
),
|
||||
},
|
||||
};
|
||||
List<double> bounds(Rect rect) => [
|
||||
rect.left,
|
||||
rect.top,
|
||||
rect.width,
|
||||
rect.height,
|
||||
];
|
||||
final restingCapsule = capsule();
|
||||
final restingForeground = foreground();
|
||||
final touch = await tester.startGesture(home);
|
||||
try {
|
||||
await wait(320);
|
||||
@@ -257,11 +296,37 @@ void main() {
|
||||
await wait(100);
|
||||
}
|
||||
expect(tester.widget<MornyeLiquidBackdrop>(lens).progress, 1);
|
||||
await capture('${prefix}_held_halfway');
|
||||
final heldCapsule = capsule();
|
||||
expect(heldCapsule.width, closeTo(restingCapsule.width * 1.025, 0.01));
|
||||
expect(heldCapsule.height, closeTo(restingCapsule.height * 1.06, 0.01));
|
||||
expect(foreground(), restingForeground);
|
||||
final glass = tester.widget<MornyeLiquidBackdrop>(
|
||||
find.descendant(
|
||||
of: find.byType(MornyeTabBar),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is MornyeLiquidBackdrop && !widget.interaction,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(glass.refractionDepth, closeTo(0.60, 0.001));
|
||||
await capture(
|
||||
'${prefix}_held_halfway',
|
||||
details: {
|
||||
'restingCapsule': bounds(restingCapsule),
|
||||
'heldCapsule': bounds(heldCapsule),
|
||||
'fixedForegroundBounds': {
|
||||
for (final entry in restingForeground.entries)
|
||||
entry.key: bounds(entry.value),
|
||||
},
|
||||
'heldRefractionDepth': glass.refractionDepth,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
await touch.cancel();
|
||||
}
|
||||
await wait(300);
|
||||
expect(capsule(), restingCapsule);
|
||||
expect(foreground(), restingForeground);
|
||||
expect(tester.state(find.byType(MiniPlayer)), same(player));
|
||||
expect(tester.takeException(), isNull);
|
||||
}
|
||||
|
||||
@@ -120,6 +120,7 @@ class MornyeGlass extends ConsumerWidget {
|
||||
this.tintOpacity,
|
||||
this.tintColor,
|
||||
this.highlightBorder = false,
|
||||
this.refractionDepth = 0.30,
|
||||
}) : firstInGroup = true,
|
||||
lastInGroup = true,
|
||||
samplesBackdrop = true;
|
||||
@@ -136,6 +137,7 @@ class MornyeGlass extends ConsumerWidget {
|
||||
this.tintColor,
|
||||
this.samplesBackdrop = true,
|
||||
this.highlightBorder = false,
|
||||
this.refractionDepth = 0.30,
|
||||
});
|
||||
|
||||
// Quantized and shared across surfaces. Avoid composed color filters and
|
||||
@@ -157,6 +159,7 @@ class MornyeGlass extends ConsumerWidget {
|
||||
|
||||
/// Paint the edge above the fill so tinted popovers retain a visible rim.
|
||||
final bool highlightBorder;
|
||||
final double refractionDepth;
|
||||
|
||||
/// Plain-page surfaces keep their tint without filtering a uniform backdrop.
|
||||
final bool samplesBackdrop;
|
||||
@@ -282,6 +285,7 @@ class MornyeGlass extends ConsumerWidget {
|
||||
borderRadius: shape,
|
||||
clarity: clarity,
|
||||
blurSigma: _blurSigmas[filterIndex],
|
||||
refractionDepth: refractionDepth,
|
||||
child: surface,
|
||||
)
|
||||
: sampleBackdrop
|
||||
@@ -511,8 +515,8 @@ class MornyeTabBar extends StatelessWidget {
|
||||
blurEnabled: blurEnabled,
|
||||
liquidGlass: liquidGlass,
|
||||
);
|
||||
final content = FadeTransition(
|
||||
opacity: contentOpacity,
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: floating ? 8 : 0),
|
||||
child: MornyeSelectionPill(
|
||||
selectionColor: scheme.primary,
|
||||
maskItemForeground: false,
|
||||
@@ -525,6 +529,28 @@ class MornyeTabBar extends StatelessWidget {
|
||||
horizontal: floating ? 6 : 5,
|
||||
vertical: 5,
|
||||
),
|
||||
interactionBuilder: (context, progress, child) => Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
// Only the capsule projects forward. Icons, labels and touch
|
||||
// coordinates stay fixed, and collapse never fades the backdrop.
|
||||
child: Transform.scale(
|
||||
scaleX: 1 + 0.025 * progress,
|
||||
scaleY: 1 + 0.06 * progress,
|
||||
child: MornyeGlass.navigation(
|
||||
blurEnabled: blurEnabled,
|
||||
strongTint: true,
|
||||
tintOpacity: MornyeTheme.navigationOpacity(context),
|
||||
refractionDepth: 0.45 + 0.15 * progress,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// The selection lens can swell beyond the capsule's clip.
|
||||
FadeTransition(opacity: contentOpacity, child: child),
|
||||
],
|
||||
),
|
||||
itemBuilder: (context, index, selected) => ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 54),
|
||||
child: Padding(
|
||||
@@ -566,23 +592,5 @@ class MornyeTabBar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: floating ? 8 : 0),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: MornyeGlass.navigation(
|
||||
blurEnabled: blurEnabled,
|
||||
strongTint: true,
|
||||
tintOpacity: MornyeTheme.navigationOpacity(context),
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
// Keep the held lens above the bar's clip so it can swell outward.
|
||||
content,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ class MornyeLiquidBackdrop extends StatelessWidget {
|
||||
required this.child,
|
||||
this.interaction = false,
|
||||
this.progress = 1,
|
||||
this.refractionDepth = 0.30,
|
||||
});
|
||||
|
||||
final BorderRadius borderRadius;
|
||||
@@ -19,6 +20,7 @@ class MornyeLiquidBackdrop extends StatelessWidget {
|
||||
final Widget child;
|
||||
final bool interaction;
|
||||
final double progress;
|
||||
final double refractionDepth;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -45,7 +47,7 @@ class MornyeLiquidBackdrop extends StatelessWidget {
|
||||
refraction: LiquidGlassRefraction(
|
||||
refractionType: OpticalRefraction(
|
||||
refraction: 1.7,
|
||||
depth: 0.30 * effect,
|
||||
depth: refractionDepth * effect,
|
||||
refractionWidth: 20,
|
||||
),
|
||||
chromaticAberration: 0.002 * clarity * effect,
|
||||
|
||||
@@ -25,6 +25,7 @@ class MornyeSelectionPill extends StatefulWidget {
|
||||
this.liquidInteraction = false,
|
||||
this.selectionColor,
|
||||
this.maskItemForeground = true,
|
||||
this.interactionBuilder,
|
||||
});
|
||||
|
||||
final List<String> labels;
|
||||
@@ -41,6 +42,11 @@ class MornyeSelectionPill extends StatefulWidget {
|
||||
final Color? selectionColor;
|
||||
final bool maskItemForeground;
|
||||
|
||||
/// Lets the bar animate its glass behind the fixed-size foreground using the
|
||||
/// same press/release animation as the selection lens.
|
||||
final Widget Function(BuildContext context, double progress, Widget child)?
|
||||
interactionBuilder;
|
||||
|
||||
@override
|
||||
State<MornyeSelectionPill> createState() => _MornyeSelectionPillState();
|
||||
}
|
||||
@@ -141,7 +147,8 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill>
|
||||
ui.ImageFilter.isShaderFilterSupported &&
|
||||
!MediaQuery.disableAnimationsOf(context) &&
|
||||
!MediaQuery.highContrastOf(context);
|
||||
return Listener(
|
||||
final pressProgress = lensEnabled ? _pressAnimation.value : 0.0;
|
||||
final content = Listener(
|
||||
// An accepted drag can end with a PointerCancelEvent. Do not navigate
|
||||
// when the OS interrupts the gesture (e.g. opening system controls).
|
||||
onPointerDown: (event) {
|
||||
@@ -258,11 +265,11 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill>
|
||||
),
|
||||
),
|
||||
if (lensEnabled && selected >= 0 && selected < count)
|
||||
_interactionLens(selected, count, _pressAnimation.value),
|
||||
_interactionLens(selected, count, pressProgress),
|
||||
_foreground(
|
||||
selected,
|
||||
count,
|
||||
lensEnabled ? _pressAnimation.value : 0,
|
||||
pressProgress,
|
||||
Row(
|
||||
key: _foregroundKey,
|
||||
children: [
|
||||
@@ -289,27 +296,7 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill>
|
||||
: Colors.transparent,
|
||||
onTap: () => widget.onChanged(index),
|
||||
child: ExcludeSemantics(
|
||||
child: AnimatedScale(
|
||||
scale:
|
||||
lensEnabled && _pressed && selected >= 0
|
||||
? 1 +
|
||||
0.18 *
|
||||
_coverage(
|
||||
index,
|
||||
selected,
|
||||
count,
|
||||
)
|
||||
: 1,
|
||||
duration:
|
||||
_dragAlignment != null ||
|
||||
MediaQuery.disableAnimationsOf(
|
||||
context,
|
||||
)
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: _item(context, index, selected),
|
||||
),
|
||||
child: _item(context, index, selected),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -324,6 +311,8 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill>
|
||||
),
|
||||
),
|
||||
);
|
||||
return widget.interactionBuilder?.call(context, pressProgress, content) ??
|
||||
content;
|
||||
}
|
||||
|
||||
Widget _item(BuildContext context, int index, int selected) {
|
||||
@@ -337,13 +326,6 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill>
|
||||
: child;
|
||||
}
|
||||
|
||||
double _coverage(int index, int selected, int count) {
|
||||
final position = _dragAlignment == null
|
||||
? selected.toDouble()
|
||||
: (_dragAlignment! + 1) * (count - 1) / 2;
|
||||
return (1 - (index - position).abs()).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
Widget _foreground(
|
||||
int selected,
|
||||
int count,
|
||||
|
||||
Reference in new issue
Block a user