修复动画错误逻辑

This commit is contained in:
LanZhan-Harmony
2026-03-23 20:33:11 +08:00
parent 1a1a50377d
commit 3185de7023
3 changed files with 123 additions and 200 deletions

View File

@@ -1,8 +1,11 @@
using System.ComponentModel; using System.ComponentModel;
using System.Numerics;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Imaging;
using UntamedMusicPlayer.Controls; using UntamedMusicPlayer.Controls;
using UntamedMusicPlayer.Models; using UntamedMusicPlayer.Models;
using UntamedMusicPlayer.Playback; using UntamedMusicPlayer.Playback;
@@ -15,6 +18,8 @@ public sealed partial class LyricPage : Page, IDisposable
{ {
public LyricViewModel ViewModel { get; } public LyricViewModel ViewModel { get; }
private bool isFirstLoad = true;
private readonly Timer _autoScrollDelayTimer; private readonly Timer _autoScrollDelayTimer;
private bool _isManualScrolling; private bool _isManualScrolling;
private bool _isProgrammaticScroll; private bool _isProgrammaticScroll;
@@ -27,13 +32,7 @@ public sealed partial class LyricPage : Page, IDisposable
private DateTimeOffset _contentGridMarginAnimationStart; private DateTimeOffset _contentGridMarginAnimationStart;
private double _contentGridMarginFrom; private double _contentGridMarginFrom;
private double _contentGridMarginTo; private double _contentGridMarginTo;
private CancellationTokenSource? _coverLoadWaitCts;
private DispatcherQueueTimer? _coverSizeAnimationTimer;
private DateTimeOffset _coverSizeAnimationStart;
private double _coverSizeFromWidth;
private double _coverSizeFromHeight;
private double _coverSizeToWidth;
private double _coverSizeToHeight;
public LyricPage() public LyricPage()
{ {
@@ -57,13 +56,89 @@ public sealed partial class LyricPage : Page, IDisposable
{ {
if (ReferenceGrid.ActualWidth > 0 && ReferenceGrid.ActualHeight > 0) if (ReferenceGrid.ActualWidth > 0 && ReferenceGrid.ActualHeight > 0)
{ {
ChangeCoverSize(ReferenceGrid.ActualWidth, ReferenceGrid.ActualHeight); RestartWaitForCoverAndRecalculate();
} }
_isManualScrolling = false; _isManualScrolling = false;
_autoScrollDelayTimer.Change(Timeout.Infinite, Timeout.Infinite); _autoScrollDelayTimer.Change(Timeout.Infinite, Timeout.Infinite);
} }
} }
private void RestartWaitForCoverAndRecalculate()
{
_coverLoadWaitCts?.Cancel();
_coverLoadWaitCts?.Dispose();
_coverLoadWaitCts = new CancellationTokenSource();
_ = RecalculateCoverSizeWhenCoverReadyAsync(_coverLoadWaitCts.Token);
}
private async Task RecalculateCoverSizeWhenCoverReadyAsync(CancellationToken cancellationToken)
{
var cover = Data.PlayState.CurrentSong?.Cover;
if (cover is null)
{
return;
}
var loaded = await WaitCoverLoadedAsync(cover, cancellationToken);
if (!loaded || cancellationToken.IsCancellationRequested)
{
return;
}
if (ReferenceGrid.ActualWidth > 0 && ReferenceGrid.ActualHeight > 0)
{
ChangeCoverSize(ReferenceGrid.ActualWidth, ReferenceGrid.ActualHeight);
}
}
private static async Task<bool> WaitCoverLoadedAsync(
BitmapImage cover,
CancellationToken cancellationToken
)
{
if (cover.PixelWidth > 0 && cover.PixelHeight > 0)
{
return true;
}
var tcs = new TaskCompletionSource<bool>(
TaskCreationOptions.RunContinuationsAsynchronously
);
void OnImageOpened(object sender, RoutedEventArgs args) => tcs.TrySetResult(true);
void OnImageFailed(object sender, ExceptionRoutedEventArgs args) => tcs.TrySetResult(false);
cover.ImageOpened += OnImageOpened;
cover.ImageFailed += OnImageFailed;
using var cancellationRegistration = cancellationToken.Register(() =>
tcs.TrySetCanceled(cancellationToken)
);
try
{
if (cover.PixelWidth > 0 && cover.PixelHeight > 0)
{
return true;
}
var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(1500, cancellationToken));
if (completedTask != tcs.Task)
{
return false;
}
return await tcs.Task;
}
catch (OperationCanceledException)
{
return false;
}
finally
{
cover.ImageOpened -= OnImageOpened;
cover.ImageFailed -= OnImageFailed;
}
}
private void OnRootPlayBarChanged(object? sender, PropertyChangedEventArgs e) private void OnRootPlayBarChanged(object? sender, PropertyChangedEventArgs e)
{ {
if ( if (
@@ -308,6 +383,14 @@ public sealed partial class LyricPage : Page, IDisposable
return; return;
} }
if (isFirstLoad)
{
isFirstLoad = false;
CoverBorder.Width = targetWidth;
CoverBorder.Height = targetHeight;
return;
}
if ( if (
Math.Abs(currentWidth - targetWidth) < 0.5 Math.Abs(currentWidth - targetWidth) < 0.5
&& Math.Abs(currentHeight - targetHeight) < 0.5 && Math.Abs(currentHeight - targetHeight) < 0.5
@@ -318,40 +401,23 @@ public sealed partial class LyricPage : Page, IDisposable
return; return;
} }
_coverSizeAnimationTimer ??= DispatcherQueue.CreateTimer(); CoverBorder.Width = targetWidth;
_coverSizeAnimationTimer.Stop(); CoverBorder.Height = targetHeight;
_coverSizeAnimationTimer.Interval = TimeSpan.FromMilliseconds(16);
_coverSizeAnimationTimer.Tick -= CoverSizeAnimationTick;
_coverSizeAnimationTimer.Tick += CoverSizeAnimationTick;
_coverSizeFromWidth = currentWidth; var visual = ElementCompositionPreview.GetElementVisual(CoverBorder);
_coverSizeFromHeight = currentHeight; visual.StopAnimation("Scale");
_coverSizeToWidth = targetWidth; visual.CenterPoint = new Vector3((float)(targetWidth / 2), (float)(targetHeight / 2), 0f);
_coverSizeToHeight = targetHeight;
_coverSizeAnimationStart = DateTimeOffset.Now;
_coverSizeAnimationTimer.Start(); var initialScaleX = (float)(currentWidth / targetWidth);
} var initialScaleY = (float)(currentHeight / targetHeight);
visual.Scale = new Vector3(initialScaleX, initialScaleY, 1f);
private void CoverSizeAnimationTick(DispatcherQueueTimer sender, object args) var compositor = visual.Compositor;
{ var scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
const double durationMs = 450; scaleAnimation.InsertKeyFrame(1f, Vector3.One);
var elapsedMs = (DateTimeOffset.Now - _coverSizeAnimationStart).TotalMilliseconds; scaleAnimation.Duration = TimeSpan.FromMilliseconds(450);
var progress = Math.Clamp(elapsedMs / durationMs, 0d, 1d);
var easedProgress = 1 - Math.Pow(1 - progress, 3);
CoverBorder.Width = visual.StartAnimation("Scale", scaleAnimation);
_coverSizeFromWidth + ((_coverSizeToWidth - _coverSizeFromWidth) * easedProgress);
CoverBorder.Height =
_coverSizeFromHeight + ((_coverSizeToHeight - _coverSizeFromHeight) * easedProgress);
if (progress >= 1)
{
sender.Stop();
sender.Tick -= CoverSizeAnimationTick;
CoverBorder.Width = _coverSizeToWidth;
CoverBorder.Height = _coverSizeToHeight;
}
} }
private void TextBlock_SizeChanged(object sender, SizeChangedEventArgs e) private void TextBlock_SizeChanged(object sender, SizeChangedEventArgs e)
@@ -431,12 +497,12 @@ public sealed partial class LyricPage : Page, IDisposable
{ {
_autoScrollDelayTimer.Dispose(); _autoScrollDelayTimer.Dispose();
_titleBarHideTimer?.Dispose(); _titleBarHideTimer?.Dispose();
_coverLoadWaitCts?.Cancel();
_coverLoadWaitCts?.Dispose();
_coverLoadWaitCts = null;
_contentGridMarginAnimationTimer?.Stop(); _contentGridMarginAnimationTimer?.Stop();
_contentGridMarginAnimationTimer?.Tick -= ContentGridMarginAnimationTick; _contentGridMarginAnimationTimer?.Tick -= ContentGridMarginAnimationTick;
_contentGridMarginAnimationTimer = null; _contentGridMarginAnimationTimer = null;
_coverSizeAnimationTimer?.Stop();
_coverSizeAnimationTimer?.Tick -= CoverSizeAnimationTick;
_coverSizeAnimationTimer = null;
Data.PlayState.PropertyChanged -= OnStateChanged; Data.PlayState.PropertyChanged -= OnStateChanged;
Data.RootPlayBarViewModel?.PropertyChanged -= OnRootPlayBarChanged; Data.RootPlayBarViewModel?.PropertyChanged -= OnRootPlayBarChanged;
Data.LyricPage = null; Data.LyricPage = null;

View File

@@ -7,7 +7,6 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:model="using:UntamedMusicPlayer.Models" xmlns:model="using:UntamedMusicPlayer.Models"
xmlns:ui="using:CommunityToolkit.WinUI" xmlns:ui="using:CommunityToolkit.WinUI"
Unloaded="RootPlayBarView_Unloaded"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid KeyTipPlacementMode="Top"> <Grid KeyTipPlacementMode="Top">
@@ -215,41 +214,22 @@
</Border> </Border>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Width="6" Visibility="{x:Bind GetNotDetailedVisibility(ViewModel.IsDetail), Mode=OneWay}"/> <TextBlock Width="6" Visibility="{x:Bind GetNotDetailedVisibility(ViewModel.IsDetail), Mode=OneWay}"/>
<Grid x:Name="SongInfoTransitionHost" <StackPanel Margin="0,0,4,0" HorizontalAlignment="Left"
Margin="0,0,4,0" HorizontalAlignment="Left" VerticalAlignment="Center">
VerticalAlignment="Center"> <TextBlock x:Name="SongTitleTextBlock"
<StackPanel x:Name="CurrentSongInfoPanel"> Margin="10,0,0,4"
<StackPanel.RenderTransform> FontSize="20" FontWeight="Medium"
<CompositeTransform x:Name="CurrentSongInfoTransform"/> Text="{x:Bind model:Data.PlayState.CurrentSong.Title, Mode=OneWay}"
</StackPanel.RenderTransform> TextTrimming="CharacterEllipsis"
<TextBlock x:Name="SongTitleTextBlock" TextWrapping="NoWrap"/>
Margin="10,0,0,4" <TextBlock x:Name="ArtistAndAlbumTextBlock"
FontSize="20" FontWeight="Medium" Margin="10,0,0,0"
TextTrimming="CharacterEllipsis" Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="NoWrap"/> Text="{x:Bind model:Data.PlayState.CurrentSong.ArtistAndAlbumStr, Mode=OneWay}"
<TextBlock x:Name="ArtistAndAlbumTextBlock" TextTrimming="CharacterEllipsis"
Margin="10,0,0,0" TextWrapping="NoWrap"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Visibility="{x:Bind GetArtistAndAlbumStrVisibility(model:Data.PlayState.CurrentSong), Mode=OneWay}"/>
TextTrimming="CharacterEllipsis" </StackPanel>
TextWrapping="NoWrap"/>
</StackPanel>
<StackPanel x:Name="IncomingSongInfoPanel"
Opacity="0" Visibility="Collapsed">
<StackPanel.RenderTransform>
<CompositeTransform x:Name="IncomingSongInfoTransform" TranslateY="16"/>
</StackPanel.RenderTransform>
<TextBlock x:Name="IncomingSongTitleTextBlock"
Margin="10,0,0,4"
FontSize="20" FontWeight="Medium"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"/>
<TextBlock x:Name="IncomingArtistAndAlbumTextBlock"
Margin="10,0,0,0"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"/>
</StackPanel>
</Grid>
<TextBlock Width="8" Visibility="{x:Bind ViewModel.IsDetail, Mode=OneWay}"/> <TextBlock Width="8" Visibility="{x:Bind ViewModel.IsDetail, Mode=OneWay}"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

View File

@@ -1,9 +1,6 @@
using System.ComponentModel;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using UntamedMusicPlayer.Contracts.Models; using UntamedMusicPlayer.Contracts.Models;
using UntamedMusicPlayer.Controls; using UntamedMusicPlayer.Controls;
using UntamedMusicPlayer.Helpers; using UntamedMusicPlayer.Helpers;
@@ -18,7 +15,6 @@ namespace UntamedMusicPlayer.Views;
public sealed partial class RootPlayBarView : UserControl public sealed partial class RootPlayBarView : UserControl
{ {
private bool _hasPointerPressed = false; private bool _hasPointerPressed = false;
private Storyboard? _songInfoTransitionStoryboard;
public RootPlayBarViewModel ViewModel { get; } public RootPlayBarViewModel ViewModel { get; }
@@ -28,119 +24,6 @@ public sealed partial class RootPlayBarView : UserControl
InitializeComponent(); InitializeComponent();
ViewModel = App.GetService<RootPlayBarViewModel>(); ViewModel = App.GetService<RootPlayBarViewModel>();
Data.RootPlayBarView = this; Data.RootPlayBarView = this;
Data.PlayState.PropertyChanged += OnStateChanged;
UpdateSongInfoWithoutAnimation();
}
private void OnStateChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(SharedPlaybackState.CurrentSong))
{
AnimateSongInfoToCurrentSong();
}
}
private void UpdateSongInfoWithoutAnimation()
{
var title = Data.PlayState.CurrentSong?.Title ?? "";
var artistAndAlbum = Data.PlayState.CurrentSong?.ArtistAndAlbumStr ?? "";
SongTitleTextBlock.Text = title;
ArtistAndAlbumTextBlock.Text = artistAndAlbum;
ArtistAndAlbumTextBlock.Visibility = string.IsNullOrWhiteSpace(artistAndAlbum)
? Visibility.Collapsed
: Visibility.Visible;
CurrentSongInfoPanel.Opacity = 1;
IncomingSongInfoPanel.Visibility = Visibility.Collapsed;
IncomingSongInfoPanel.Opacity = 0;
}
private void AnimateSongInfoToCurrentSong()
{
var title = Data.PlayState.CurrentSong?.Title ?? "";
var artistAndAlbum = Data.PlayState.CurrentSong?.ArtistAndAlbumStr ?? "";
if (SongTitleTextBlock.Text == title && ArtistAndAlbumTextBlock.Text == artistAndAlbum)
{
return;
}
StopSongInfoTransition();
IncomingSongTitleTextBlock.Text = title;
IncomingArtistAndAlbumTextBlock.Text = artistAndAlbum;
IncomingArtistAndAlbumTextBlock.Visibility = string.IsNullOrWhiteSpace(artistAndAlbum)
? Visibility.Collapsed
: Visibility.Visible;
IncomingSongInfoPanel.Visibility = Visibility.Visible;
var currentFadeOut = new DoubleAnimation
{
From = 1,
To = 0,
Duration = TimeSpan.FromMilliseconds(200),
};
Storyboard.SetTarget(currentFadeOut, CurrentSongInfoPanel);
Storyboard.SetTargetProperty(currentFadeOut, nameof(Opacity));
var currentSlideUp = new DoubleAnimation
{
From = 0,
To = -70,
Duration = TimeSpan.FromMilliseconds(220),
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseIn },
};
Storyboard.SetTarget(currentSlideUp, CurrentSongInfoTransform);
Storyboard.SetTargetProperty(currentSlideUp, nameof(CompositeTransform.TranslateY));
var incomingFadeIn = new DoubleAnimation
{
From = 0,
To = 1,
Duration = TimeSpan.FromMilliseconds(400),
};
Storyboard.SetTarget(incomingFadeIn, IncomingSongInfoPanel);
Storyboard.SetTargetProperty(incomingFadeIn, nameof(Opacity));
var incomingSlideIn = new DoubleAnimation
{
From = 70,
To = 0,
Duration = TimeSpan.FromMilliseconds(450),
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut },
};
Storyboard.SetTarget(incomingSlideIn, IncomingSongInfoTransform);
Storyboard.SetTargetProperty(incomingSlideIn, nameof(CompositeTransform.TranslateY));
_songInfoTransitionStoryboard = new Storyboard();
_songInfoTransitionStoryboard.Children.Add(currentFadeOut);
_songInfoTransitionStoryboard.Children.Add(currentSlideUp);
_songInfoTransitionStoryboard.Children.Add(incomingFadeIn);
_songInfoTransitionStoryboard.Children.Add(incomingSlideIn);
_songInfoTransitionStoryboard.Completed += SongInfoTransitionStoryboard_Completed;
_songInfoTransitionStoryboard.Begin();
}
private void SongInfoTransitionStoryboard_Completed(object? sender, object e)
{
ArtistAndAlbumTextBlock.Visibility = IncomingArtistAndAlbumTextBlock.Visibility;
IncomingSongInfoPanel.Visibility = Visibility.Collapsed;
StopSongInfoTransition();
}
private void StopSongInfoTransition()
{
SongTitleTextBlock.Text = IncomingSongTitleTextBlock.Text;
ArtistAndAlbumTextBlock.Text = IncomingArtistAndAlbumTextBlock.Text;
if (_songInfoTransitionStoryboard is null)
{
return;
}
_songInfoTransitionStoryboard.Completed -= SongInfoTransitionStoryboard_Completed;
_songInfoTransitionStoryboard.Stop();
_songInfoTransitionStoryboard = null;
} }
public string GetCurrent(TimeSpan current) => public string GetCurrent(TimeSpan current) =>
@@ -390,10 +273,4 @@ public sealed partial class RootPlayBarView : UserControl
var dialog = new PropertiesDialog(currentSong!) { XamlRoot = XamlRoot }; var dialog = new PropertiesDialog(currentSong!) { XamlRoot = XamlRoot };
await dialog.ShowAsync(); await dialog.ShowAsync();
} }
private void RootPlayBarView_Unloaded(object sender, RoutedEventArgs e)
{
Data.PlayState.PropertyChanged -= OnStateChanged;
StopSongInfoTransition();
}
} }