mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 13:19:44 +02:00
fix(ui): correct glass shader coordinates on scaled tablets
Use native window metrics for glass shaders while preserving scaled layout metrics for their content. Cover fullscreen tablet orientations and resizing to a compact window. Fixes #597
This commit is contained in:
1 parent
b40ba27cd6
commit
55a390d679
4 files changed
+292
-147
No files matched your search
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:liquid_glass_easy/liquid_glass_easy.dart';
|
||||
import 'package:spotiflac_android/providers/runtime_profile_provider.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
import 'package:spotiflac_android/widgets/native_glass_metrics.dart';
|
||||
|
||||
/// One switch style for settings, extension controls and editing sheets.
|
||||
/// Mornye keeps its wide oval thumb even when glass or motion is disabled.
|
||||
@@ -48,11 +49,13 @@ class AppSwitch extends StatelessWidget {
|
||||
if (useGlass) {
|
||||
control = Transform.flip(
|
||||
flipX: Directionality.of(context) == TextDirection.rtl,
|
||||
child: LiquidGlassSwitch(
|
||||
value: value,
|
||||
onChanged: onChanged!,
|
||||
activeColor: active,
|
||||
inactiveColor: inactive,
|
||||
child: NativeGlassMetrics(
|
||||
child: LiquidGlassSwitch(
|
||||
value: value,
|
||||
onChanged: onChanged!,
|
||||
activeColor: active,
|
||||
inactiveColor: inactive,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
||||
+153
-142
@@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:liquid_glass_easy/liquid_glass_easy.dart';
|
||||
import 'package:spotiflac_android/providers/runtime_profile_provider.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
import 'package:spotiflac_android/widgets/native_glass_metrics.dart';
|
||||
|
||||
class MornyeSegmentedControl extends ConsumerWidget {
|
||||
const MornyeSegmentedControl({
|
||||
@@ -53,59 +54,61 @@ class MornyeSegmentedControl extends ConsumerWidget {
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
LiquidGlassTabBar.withImpeller(
|
||||
width: constraints.maxWidth,
|
||||
height: height,
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
itemPadding: 4,
|
||||
selectedIndex: logicalIndex(selectedIndex),
|
||||
onChanged: (index) => onChanged(logicalIndex(index)),
|
||||
style: const LiquidGlassStyle(
|
||||
shape: LiquidGlassShape.continuousRoundedRectangle(
|
||||
cornerRadius: 32,
|
||||
borderWidth: 0,
|
||||
lightIntensity: 0,
|
||||
NativeGlassMetrics(
|
||||
child: LiquidGlassTabBar.withImpeller(
|
||||
width: constraints.maxWidth,
|
||||
height: height,
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
itemPadding: 4,
|
||||
selectedIndex: logicalIndex(selectedIndex),
|
||||
onChanged: (index) => onChanged(logicalIndex(index)),
|
||||
style: const LiquidGlassStyle(
|
||||
shape: LiquidGlassShape.continuousRoundedRectangle(
|
||||
cornerRadius: 32,
|
||||
borderWidth: 0,
|
||||
lightIntensity: 0,
|
||||
),
|
||||
appearance: LiquidGlassAppearance(),
|
||||
refraction: LiquidGlassRefraction(
|
||||
distortion: 0,
|
||||
chromaticAberration: 0,
|
||||
),
|
||||
),
|
||||
appearance: LiquidGlassAppearance(),
|
||||
refraction: LiquidGlassRefraction(
|
||||
distortion: 0,
|
||||
chromaticAberration: 0,
|
||||
pillStyle: LiquidGlassTabPillStyle(
|
||||
mode: blur && animate
|
||||
? LiquidGlassPillMode.impellerOnly
|
||||
: LiquidGlassPillMode.none,
|
||||
animated: animate,
|
||||
),
|
||||
),
|
||||
pillStyle: LiquidGlassTabPillStyle(
|
||||
mode: blur && animate
|
||||
? LiquidGlassPillMode.impellerOnly
|
||||
: LiquidGlassPillMode.none,
|
||||
animated: animate,
|
||||
),
|
||||
itemStyle: LiquidGlassTabItemStyle(
|
||||
selectedColor: scheme.onSurface,
|
||||
unselectedColor: scheme.onSurface,
|
||||
iconSize: 0,
|
||||
iconLabelGap: 0,
|
||||
labelFontSize: 15,
|
||||
),
|
||||
items: [
|
||||
for (final label in visualLabels)
|
||||
LiquidGlassTabBarItem(
|
||||
label: label,
|
||||
iconBuilder: (_, _) => const SizedBox.shrink(),
|
||||
labelBuilder: (context, state) => Text(
|
||||
label,
|
||||
textDirection: rtl
|
||||
? TextDirection.rtl
|
||||
: TextDirection.ltr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: scheme.onSurface,
|
||||
fontWeight: state.selected
|
||||
? FontWeight.w600
|
||||
: FontWeight.w400,
|
||||
itemStyle: LiquidGlassTabItemStyle(
|
||||
selectedColor: scheme.onSurface,
|
||||
unselectedColor: scheme.onSurface,
|
||||
iconSize: 0,
|
||||
iconLabelGap: 0,
|
||||
labelFontSize: 15,
|
||||
),
|
||||
items: [
|
||||
for (final label in visualLabels)
|
||||
LiquidGlassTabBarItem(
|
||||
label: label,
|
||||
iconBuilder: (_, _) => const SizedBox.shrink(),
|
||||
labelBuilder: (context, state) => Text(
|
||||
label,
|
||||
textDirection: rtl
|
||||
? TextDirection.rtl
|
||||
: TextDirection.ltr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: scheme.onSurface,
|
||||
fontWeight: state.selected
|
||||
? FontWeight.w600
|
||||
: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -235,32 +238,36 @@ class MornyeGlass extends StatelessWidget {
|
||||
tintOpacity: tintOpacity,
|
||||
tintColor: tintColor,
|
||||
child: useGlass
|
||||
? LiquidGlassLens(
|
||||
style: LiquidGlassStyle(
|
||||
shape: LiquidGlassShape.continuousRoundedRectangle(
|
||||
cornerRadius: radius,
|
||||
borderWidth: dark && tintOpacity == null ? 0 : 0.6,
|
||||
lightIntensity: dark && tintOpacity == null ? 0 : 0.18,
|
||||
),
|
||||
appearance: LiquidGlassAppearance(
|
||||
color: tintOpacity != null
|
||||
? Colors.transparent
|
||||
: dark
|
||||
? scheme.surfaceContainerHigh.withValues(alpha: 0.28)
|
||||
: Colors.white.withValues(
|
||||
alpha: strongTint ? 0.20 : 0.55,
|
||||
),
|
||||
// The surface already blurs the backdrop. Refracting that
|
||||
// frosted result needs no second Gaussian blur pass.
|
||||
blur: const LiquidGlassBlur(),
|
||||
),
|
||||
refraction: const LiquidGlassRefraction(
|
||||
distortion: 0.02,
|
||||
distortionWidth: 8,
|
||||
chromaticAberration: 0,
|
||||
? NativeGlassMetrics(
|
||||
child: LiquidGlassLens(
|
||||
style: LiquidGlassStyle(
|
||||
shape: LiquidGlassShape.continuousRoundedRectangle(
|
||||
cornerRadius: radius,
|
||||
borderWidth: dark && tintOpacity == null ? 0 : 0.6,
|
||||
lightIntensity: dark && tintOpacity == null ? 0 : 0.18,
|
||||
),
|
||||
appearance: LiquidGlassAppearance(
|
||||
color: tintOpacity != null
|
||||
? Colors.transparent
|
||||
: dark
|
||||
? scheme.surfaceContainerHigh.withValues(alpha: 0.28)
|
||||
: Colors.white.withValues(
|
||||
alpha: strongTint ? 0.20 : 0.55,
|
||||
),
|
||||
// The surface already blurs the backdrop. Refracting that
|
||||
// frosted result needs no second Gaussian blur pass.
|
||||
blur: const LiquidGlassBlur(),
|
||||
),
|
||||
refraction: const LiquidGlassRefraction(
|
||||
distortion: 0.02,
|
||||
distortionWidth: 8,
|
||||
chromaticAberration: 0,
|
||||
),
|
||||
),
|
||||
// Only the shader uses window metrics; responsive content and
|
||||
// decoded artwork retain the surrounding tablet layout scale.
|
||||
child: MediaQuery(data: MediaQuery.of(context), child: child),
|
||||
),
|
||||
child: child,
|
||||
)
|
||||
: child,
|
||||
);
|
||||
@@ -494,76 +501,80 @@ class MornyeTabBar extends StatelessWidget {
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
LiquidGlassTabBar.withImpeller(
|
||||
width: constraints.maxWidth,
|
||||
height: 64,
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
selectedIndex: selectedIndex < 0 ? 0 : selectedIndex,
|
||||
onChanged: onSelected,
|
||||
style: LiquidGlassTabBar.defaultStyle.copyWith(
|
||||
// The frosted base owns the subtle outline. Disable the
|
||||
// package's default specular rim around the whole capsule.
|
||||
shape: const LiquidGlassShape.continuousRoundedRectangle(
|
||||
cornerRadius: 32,
|
||||
borderWidth: 0,
|
||||
lightIntensity: 0,
|
||||
),
|
||||
// The sibling surface already supplies tint and blur. Keep
|
||||
// the lens clear so the moving pill can refract the icons.
|
||||
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(
|
||||
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,
|
||||
),
|
||||
),
|
||||
NativeGlassMetrics(
|
||||
child: LiquidGlassTabBar.withImpeller(
|
||||
width: constraints.maxWidth,
|
||||
height: 64,
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
selectedIndex: selectedIndex < 0 ? 0 : selectedIndex,
|
||||
onChanged: onSelected,
|
||||
style: LiquidGlassTabBar.defaultStyle.copyWith(
|
||||
// The frosted base owns the subtle outline. Disable the
|
||||
// package's default specular rim around the whole capsule.
|
||||
shape: const LiquidGlassShape.continuousRoundedRectangle(
|
||||
cornerRadius: 32,
|
||||
borderWidth: 0,
|
||||
lightIntensity: 0,
|
||||
),
|
||||
],
|
||||
// The sibling surface already supplies tint and blur. Keep
|
||||
// the lens clear so the moving pill can refract the icons.
|
||||
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(
|
||||
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
|
||||
// index; handle taps here so
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Gives glass shaders the actual window's coordinate system.
|
||||
///
|
||||
/// AdaptiveUiScaler changes MediaQuery's size and density for layout and image
|
||||
/// decoding. The glass renderer already includes that ancestor transform when
|
||||
/// positioning its shader, so using the scaled density again displaces the
|
||||
/// effect from its clip (especially near the bottom of a tablet screen).
|
||||
class NativeGlassMetrics extends StatelessWidget {
|
||||
const NativeGlassMetrics({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final view = View.of(context);
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
size: view.physicalSize / view.devicePixelRatio,
|
||||
devicePixelRatio: view.devicePixelRatio,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user