mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-02 22:26:52 +02:00
perf(mornye): adapt glass and player effects to device capabilities
This commit is contained in:
1 parent
b11e925ee4
commit
9ebf98ff6c
20 files changed
+500
-81
No files matched your search
@@ -701,9 +701,7 @@ class HeaderCircleButton extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
final blurEnabled =
|
||||
!ref.watch(lowEndDeviceProvider) ||
|
||||
ref.watch(backdropBlurEnabledProvider);
|
||||
final blurEnabled = ref.watch(mornyeBlurEnabledProvider);
|
||||
final button = IconButton(
|
||||
onPressed: onPressed,
|
||||
tooltip: tooltip,
|
||||
|
||||
@@ -106,9 +106,7 @@ class AppSearchField extends StatelessWidget {
|
||||
return Consumer(
|
||||
builder: (context, ref, child) => MornyeGlass.navigation(
|
||||
radius: 28,
|
||||
blurEnabled:
|
||||
!ref.watch(lowEndDeviceProvider) ||
|
||||
ref.watch(backdropBlurEnabledProvider),
|
||||
blurEnabled: ref.watch(mornyeBlurEnabledProvider),
|
||||
child: child!,
|
||||
),
|
||||
child: field,
|
||||
|
||||
@@ -50,8 +50,8 @@ class AppSwitch extends StatelessWidget {
|
||||
enabled &&
|
||||
!MediaQuery.disableAnimationsOf(context) &&
|
||||
!MediaQuery.highContrastOf(context) &&
|
||||
(!ref.watch(lowEndDeviceProvider) ||
|
||||
ref.watch(backdropBlurEnabledProvider));
|
||||
// One shader lens per switch; settings pages show many at once.
|
||||
ref.watch(mornyeLiquidGlassProvider);
|
||||
final inactive = scheme.brightness == Brightness.dark
|
||||
? const Color(0xff39393d)
|
||||
: const Color(0xff8e8e93);
|
||||
|
||||
@@ -36,9 +36,7 @@ class ExtensionRepoCard extends ConsumerWidget {
|
||||
padding: margin,
|
||||
child: MornyeGlass.navigation(
|
||||
radius: 24,
|
||||
blurEnabled:
|
||||
!ref.watch(lowEndDeviceProvider) ||
|
||||
ref.watch(backdropBlurEnabledProvider),
|
||||
blurEnabled: ref.watch(mornyeBlurEnabledProvider),
|
||||
child: Material(color: Colors.transparent, child: child),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -63,7 +63,13 @@ class _MiniPlayerState extends ConsumerState<MiniPlayer> {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final mornye = context.isMornye;
|
||||
final compact = mornye && widget.compact;
|
||||
if (mornye && !MediaQuery.disableAnimationsOf(context)) {
|
||||
// Frosted and flat glass levels run on constrained Android hardware. They
|
||||
// skip keeping a paused video decoder for the full player's motion cover,
|
||||
// and stop the title marquee after a few passes: every marquee frame
|
||||
// re-filters the blurred chrome around it.
|
||||
final fullEffects = !mornye || ref.watch(mornyeLiquidGlassProvider);
|
||||
final marqueeCycles = fullEffects ? null : 2;
|
||||
if (mornye && fullEffects && !MediaQuery.disableAnimationsOf(context)) {
|
||||
final artwork = ref
|
||||
.watch(
|
||||
playerMotionArtworkProvider((
|
||||
@@ -156,6 +162,7 @@ class _MiniPlayerState extends ConsumerState<MiniPlayer> {
|
||||
children: [
|
||||
OverflowMarquee(
|
||||
resetKey: (mediaItem.id, mediaItem.title),
|
||||
maxCycles: marqueeCycles,
|
||||
child: ExplicitTrackTitle(
|
||||
title: mediaItem.title,
|
||||
explicit:
|
||||
@@ -171,6 +178,7 @@ class _MiniPlayerState extends ConsumerState<MiniPlayer> {
|
||||
),
|
||||
OverflowMarquee(
|
||||
resetKey: (mediaItem.id, mediaItem.artist),
|
||||
maxCycles: marqueeCycles,
|
||||
child: Text(
|
||||
mediaItem.artist ?? '',
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
@@ -244,9 +252,7 @@ class _MiniPlayerState extends ConsumerState<MiniPlayer> {
|
||||
child: MornyeGlass.navigation(
|
||||
strongTint: true,
|
||||
tintOpacity: MornyeTheme.chromeOpacity(context),
|
||||
blurEnabled:
|
||||
!ref.watch(lowEndDeviceProvider) ||
|
||||
ref.watch(backdropBlurEnabledProvider),
|
||||
blurEnabled: ref.watch(mornyeBlurEnabledProvider),
|
||||
child: player,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -359,8 +359,8 @@ class _ArtistCollapsingArtwork extends ConsumerWidget {
|
||||
final blur = 18 * Curves.easeOut.transform(collapse);
|
||||
final blurEnabled =
|
||||
!MediaQuery.disableAnimationsOf(context) &&
|
||||
(!ref.watch(lowEndDeviceProvider) ||
|
||||
ref.watch(backdropBlurEnabledProvider));
|
||||
// The collapse blur and masked backdrop re-filter on every scroll frame.
|
||||
ref.watch(mornyeLiquidGlassProvider);
|
||||
|
||||
return ClipRect(
|
||||
child: Stack(
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/music_player_provider.dart';
|
||||
import 'package:spotiflac_android/providers/runtime_profile_provider.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
import 'package:spotiflac_android/widgets/mini_player.dart';
|
||||
import 'package:spotiflac_android/widgets/mornye_chrome.dart';
|
||||
@@ -85,9 +86,11 @@ class MornyeBottomBar extends ConsumerWidget {
|
||||
final hasPlayer = ref.watch(
|
||||
currentMediaItemProvider.select((item) => item.value != null),
|
||||
);
|
||||
final liquidGlass = ref.watch(mornyeLiquidGlassProvider);
|
||||
// Animated glass tabs already reserve 8px above their visible capsule.
|
||||
final glassTabs =
|
||||
blurEnabled &&
|
||||
liquidGlass &&
|
||||
!MediaQuery.disableAnimationsOf(context) &&
|
||||
!MediaQuery.highContrastOf(context);
|
||||
final tabGap = glassTabs ? 0.0 : 8.0;
|
||||
@@ -118,6 +121,7 @@ class MornyeBottomBar extends ConsumerWidget {
|
||||
onSelected: (index) =>
|
||||
index == destinations.length - 1 ? onSearch() : onSelected(index),
|
||||
blurEnabled: blurEnabled,
|
||||
liquidGlass: liquidGlass,
|
||||
hiddenIconIndices: hideMovingIcons
|
||||
? {leadingIndex, destinations.length - 1}
|
||||
: const {},
|
||||
|
||||
+119
-48
@@ -25,10 +25,14 @@ class MornyeSegmentedControl extends ConsumerWidget {
|
||||
final scheme = theme.colorScheme;
|
||||
final blur =
|
||||
!MediaQuery.highContrastOf(context) &&
|
||||
(!ref.watch(lowEndDeviceProvider) ||
|
||||
ref.watch(backdropBlurEnabledProvider));
|
||||
ref.watch(mornyeBlurEnabledProvider);
|
||||
final animate = !MediaQuery.disableAnimationsOf(context);
|
||||
final height = MediaQuery.textScalerOf(context).scale(15) + 36;
|
||||
if (!blur || !animate || !ref.watch(mornyeLiquidGlassProvider)) {
|
||||
// The Impeller tab bar ignores its pill mode and always runs its shader
|
||||
// passes. Without blur or motion, draw the resting segments directly.
|
||||
return _plainSegments(context, blur: blur, height: height);
|
||||
}
|
||||
final rtl = Directionality.of(context) == TextDirection.rtl;
|
||||
final visualLabels = rtl ? labels.reversed.toList() : labels;
|
||||
int logicalIndex(int index) => rtl ? labels.length - 1 - index : index;
|
||||
@@ -74,11 +78,9 @@ class MornyeSegmentedControl extends ConsumerWidget {
|
||||
chromaticAberration: 0,
|
||||
),
|
||||
),
|
||||
pillStyle: LiquidGlassTabPillStyle(
|
||||
mode: blur && animate
|
||||
? LiquidGlassPillMode.impellerOnly
|
||||
: LiquidGlassPillMode.none,
|
||||
animated: animate,
|
||||
pillStyle: const LiquidGlassTabPillStyle(
|
||||
mode: LiquidGlassPillMode.impellerOnly,
|
||||
animated: true,
|
||||
),
|
||||
itemStyle: LiquidGlassTabItemStyle(
|
||||
selectedColor: scheme.onSurface,
|
||||
@@ -117,6 +119,66 @@ class MornyeSegmentedControl extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _plainSegments(
|
||||
BuildContext context, {
|
||||
required bool blur,
|
||||
required double height,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
final selectionFill = scheme.onSurface.withValues(
|
||||
alpha: scheme.brightness == Brightness.dark ? 0.12 : 0.08,
|
||||
);
|
||||
final radius = BorderRadius.circular(height / 2);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
child: _MornyeGlassSurface(
|
||||
blurEnabled: blur,
|
||||
radius: height / 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Row(
|
||||
children: [
|
||||
for (var index = 0; index < labels.length; index++)
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: index == selectedIndex,
|
||||
child: Material(
|
||||
color: index == selectedIndex
|
||||
? selectionFill
|
||||
: Colors.transparent,
|
||||
borderRadius: radius,
|
||||
child: InkWell(
|
||||
borderRadius: radius,
|
||||
onTap: () => onChanged(index),
|
||||
child: Center(
|
||||
child: Text(
|
||||
labels[index],
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: scheme.onSurface,
|
||||
fontWeight: index == selectedIndex
|
||||
? FontWeight.w600
|
||||
: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The bounded, translucent chrome used by Mornye's tab accessory. Blur is
|
||||
@@ -165,8 +227,7 @@ class MornyeGlassPanel extends ConsumerWidget {
|
||||
final blur =
|
||||
blurEnabled &&
|
||||
!MediaQuery.highContrastOf(context) &&
|
||||
(!ref.watch(lowEndDeviceProvider) ||
|
||||
ref.watch(backdropBlurEnabledProvider));
|
||||
ref.watch(mornyeBlurEnabledProvider);
|
||||
return MornyeGlass.navigation(
|
||||
radius: radius,
|
||||
firstInGroup: firstInGroup,
|
||||
@@ -231,10 +292,14 @@ class MornyeGlass extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final dark = scheme.brightness == Brightness.dark;
|
||||
final useGlass =
|
||||
_useLens && blurEnabled && !MediaQuery.highContrastOf(context);
|
||||
Widget lens(Widget child) => Consumer(
|
||||
// Mid-range Android keeps the frosted surface but not the shader lens.
|
||||
builder: (context, ref, child) =>
|
||||
ref.watch(mornyeLiquidGlassProvider) ? _liquidLens(child!) : child!,
|
||||
child: child,
|
||||
);
|
||||
return _MornyeGlassSurface(
|
||||
blurEnabled: blurEnabled,
|
||||
radius: radius,
|
||||
@@ -244,41 +309,45 @@ class MornyeGlass extends StatelessWidget {
|
||||
tintOpacity: tintOpacity,
|
||||
tintColor: tintColor,
|
||||
backdropFilter: backdropFilter,
|
||||
child: useGlass
|
||||
? 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: useGlass ? lens(child) : child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _liquidLens(Widget child) => Builder(
|
||||
builder: (context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final dark = scheme.brightness == Brightness.dark;
|
||||
return 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),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Keep the capsule legible independently of the shader renderer. The liquid
|
||||
@@ -464,10 +533,7 @@ class MornyeFilterChip extends ConsumerWidget {
|
||||
}
|
||||
return MornyeGlass.navigation(
|
||||
radius: 24,
|
||||
blurEnabled:
|
||||
blurEnabled &&
|
||||
(!ref.watch(lowEndDeviceProvider) ||
|
||||
ref.watch(backdropBlurEnabledProvider)),
|
||||
blurEnabled: blurEnabled && ref.watch(mornyeBlurEnabledProvider),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
@@ -480,9 +546,13 @@ class MornyeTabBar extends StatelessWidget {
|
||||
required this.selectedIndex,
|
||||
required this.onSelected,
|
||||
required this.blurEnabled,
|
||||
this.liquidGlass = true,
|
||||
this.hiddenIconIndices = const {},
|
||||
});
|
||||
|
||||
/// Shader capsule and travelling pill; otherwise the frosted row below.
|
||||
final bool liquidGlass;
|
||||
|
||||
final List<NavigationDestination> destinations;
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onSelected;
|
||||
@@ -498,6 +568,7 @@ class MornyeTabBar extends StatelessWidget {
|
||||
);
|
||||
final inactiveIconColor = scheme.onSurface;
|
||||
if (blurEnabled &&
|
||||
liquidGlass &&
|
||||
!MediaQuery.disableAnimationsOf(context) &&
|
||||
!MediaQuery.highContrastOf(context)) {
|
||||
return LayoutBuilder(
|
||||
|
||||
@@ -316,10 +316,10 @@ class _LandscapeLyricsViewport extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Three stacked backdrop passes re-filter the moving lyrics every frame.
|
||||
final blur =
|
||||
!MediaQuery.highContrastOf(context) &&
|
||||
(!ref.watch(lowEndDeviceProvider) ||
|
||||
ref.watch(backdropBlurEnabledProvider));
|
||||
ref.watch(mornyeLiquidGlassProvider);
|
||||
return ClipRect(
|
||||
key: const ValueKey('landscape-lyrics-viewport'),
|
||||
child: TweenAnimationBuilder<double>(
|
||||
|
||||
@@ -29,8 +29,7 @@ class MornyePlayerDetailsSheet extends ConsumerWidget {
|
||||
final item = mediaItem;
|
||||
final blur =
|
||||
!MediaQuery.highContrastOf(context) &&
|
||||
(!ref.watch(lowEndDeviceProvider) ||
|
||||
ref.watch(backdropBlurEnabledProvider));
|
||||
ref.watch(mornyeBlurEnabledProvider);
|
||||
return MornyeGlass.navigation(
|
||||
blurEnabled: blur,
|
||||
child: SafeArea(
|
||||
|
||||
@@ -8,11 +8,16 @@ class OverflowMarquee extends StatefulWidget {
|
||||
super.key,
|
||||
required this.resetKey,
|
||||
required this.child,
|
||||
this.maxCycles,
|
||||
});
|
||||
|
||||
final Object resetKey;
|
||||
final Widget child;
|
||||
|
||||
/// Stops at the start after this many passes; null scrolls indefinitely.
|
||||
/// A new [resetKey], width or lifecycle resume starts a fresh count.
|
||||
final int? maxCycles;
|
||||
|
||||
@override
|
||||
State<OverflowMarquee> createState() => _OverflowMarqueeState();
|
||||
}
|
||||
@@ -25,6 +30,7 @@ class _OverflowMarqueeState extends State<OverflowMarquee>
|
||||
final _contentKey = GlobalKey();
|
||||
Timer? _pause;
|
||||
int _generation = 0;
|
||||
int _cycles = 0;
|
||||
bool _restartQueued = false;
|
||||
bool _motionEnabled = false;
|
||||
bool _appActive = true;
|
||||
@@ -53,7 +59,8 @@ class _OverflowMarqueeState extends State<OverflowMarquee>
|
||||
@override
|
||||
void didUpdateWidget(OverflowMarquee oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.resetKey != oldWidget.resetKey) {
|
||||
if (widget.resetKey != oldWidget.resetKey ||
|
||||
widget.maxCycles != oldWidget.maxCycles) {
|
||||
_queueRestart();
|
||||
}
|
||||
}
|
||||
@@ -67,6 +74,7 @@ class _OverflowMarqueeState extends State<OverflowMarquee>
|
||||
void _queueRestart() {
|
||||
_pause?.cancel();
|
||||
_generation++;
|
||||
_cycles = 0;
|
||||
if (_restartQueued) return;
|
||||
_restartQueued = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -113,6 +121,9 @@ class _OverflowMarqueeState extends State<OverflowMarquee>
|
||||
// The next copy is now exactly where the first started, so resetting
|
||||
// the offset changes neither the visible text nor its direction.
|
||||
_scroll.jumpTo(0);
|
||||
_cycles++;
|
||||
final maxCycles = widget.maxCycles;
|
||||
if (maxCycles != null && _cycles >= maxCycles) return;
|
||||
_scheduleCycle(generation);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user