434 lines
14 KiB
C#
Raw Normal View History

2026-03-20 17:42:24 +08:00
using System;
2025-09-22 17:02:19 +08:00
using System.Collections.ObjectModel;
2025-09-22 15:22:53 +08:00
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
2025-09-23 10:15:06 +08:00
using System.Windows.Media.Animation;
2025-09-26 16:46:22 +08:00
using System.Windows.Threading;
2025-09-22 15:22:53 +08:00
namespace Wpf_AiSportsMicrospace.MyUserControl
{
2025-09-22 16:49:49 +08:00
/// <summary>
/// CoverFlowControl.xaml 的交互逻辑
/// </summary>
2025-09-22 15:22:53 +08:00
public partial class CoverFlowControl : UserControl
2025-09-28 09:17:44 +08:00
{// 新增事件:进度条完成
2026-03-20 17:42:24 +08:00
public event Action<CoverFlowItem>? ProgressCompleted;
2025-09-28 09:17:44 +08:00
public ObservableCollection<CoverFlowItem> Images { get; set; } = new ObservableCollection<CoverFlowItem>();
2025-09-23 10:15:06 +08:00
private int _selectedIndex = 0;
2026-03-20 17:42:24 +08:00
private bool _isAnimating;
private int _pendingSlideSteps;
2026-03-27 17:42:25 +08:00
private const double CardWidth = 368;
2026-03-20 17:42:24 +08:00
private const double CardHeight = 534;
private const double CardGap = 30; // 卡片之间水平间距
private const double SlideDurationMs = 380; // 动画时长
2025-09-28 09:17:44 +08:00
2025-09-23 10:15:06 +08:00
public int SelectedIndex
2025-09-22 15:22:53 +08:00
{
2025-09-23 10:15:06 +08:00
get => _selectedIndex;
set
{
2025-09-23 10:18:28 +08:00
if (Images.Count == 0) return;
2025-09-26 16:46:22 +08:00
if (value < 0) _selectedIndex = Images.Count - 1;
else if (value >= Images.Count) _selectedIndex = 0;
else _selectedIndex = value;
2026-03-20 17:42:24 +08:00
UpdateSelectionFlags();
RefreshCards(instant: true);
2025-09-26 16:46:22 +08:00
// 启动进度条动画
//var current = Images[_selectedIndex];
//StartProgress(current);
}
2025-09-28 09:17:44 +08:00
2025-09-26 16:46:22 +08:00
}
2025-09-28 09:17:44 +08:00
public CoverFlowControl()
{
InitializeComponent();
DataContext = this;
2026-03-20 17:42:24 +08:00
Loaded += CoverFlowControl_Loaded;
2025-09-28 09:17:44 +08:00
//var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(5) };
//timer.Tick += (s, e) =>
//{
// if (Images.Count == 0) return;
// var current = Images[_selectedIndex];
// if (current.Progress < 1)
// current.Progress += 0.01; // 调整速度
//};
//timer.Start();
}
2026-03-20 17:42:24 +08:00
private EventHandler? _renderHandler;
2025-09-26 16:46:22 +08:00
2026-03-20 17:42:24 +08:00
private CoverFlowItem? _currentItem;
private void CoverFlowControl_Loaded(object sender, RoutedEventArgs e)
{
if (Images.Count > 0)
{
UpdateSelectionFlags();
RefreshCards(instant: true);
}
CarouselCanvas.Focus();
Keyboard.Focus(CarouselCanvas);
}
2025-09-26 16:46:22 +08:00
2025-09-28 09:17:44 +08:00
/// <summary>
/// 针对当前选中项启动进度条动画
/// </summary>
public void StartSelectedProgress()
{
if (SelectedIndex >= 0 && SelectedIndex < Images.Count)
{
var current = Images[SelectedIndex];
if (current.Progress >= 1) return; // 已完成,直接返回,不再启动
StartProgress(current);
}
}
public void CancelSelectedProgress()
{
if (SelectedIndex >= 0 && SelectedIndex < Images.Count)
{
var current = Images[SelectedIndex];
// 停止动画/计时
StopProgress();
// 清零当前的进度值
current.Progress = 0;
}
}
public void StartProgress(CoverFlowItem item)
2025-09-26 16:46:22 +08:00
{
StopProgress();
_currentItem = item;
2025-09-28 09:17:44 +08:00
item.Progress = 0;
2025-09-26 16:46:22 +08:00
_renderHandler = (s, e) =>
{
if (_currentItem.Progress < 1)
{
2026-03-30 15:41:09 +08:00
_currentItem.Progress += 0.007; // 控制速度
2025-09-26 16:46:22 +08:00
}
else
{
2025-09-28 09:17:44 +08:00
ProgressCompleted?.Invoke(_currentItem);
// 停止动画
2025-09-26 16:46:22 +08:00
StopProgress();
}
};
CompositionTarget.Rendering += _renderHandler;
}
private void StopProgress()
2025-09-28 14:08:36 +08:00
2025-09-26 16:46:22 +08:00
{
if (_renderHandler != null)
{
CompositionTarget.Rendering -= _renderHandler;
_renderHandler = null;
}
2025-09-28 09:17:44 +08:00
if (_currentItem != null)
2025-09-26 16:46:22 +08:00
{
2025-09-28 09:17:44 +08:00
_currentItem.Progress = 0;
_currentItem = null;
2025-09-23 10:15:06 +08:00
}
2025-09-22 17:02:19 +08:00
}
2026-03-20 17:42:24 +08:00
private void UpdateSelectionFlags()
{
for (int i = 0; i < Images.Count; i++)
Images[i].IsSelected = i == _selectedIndex;
}
2025-09-22 17:02:19 +08:00
2026-03-20 17:42:24 +08:00
private int WrapIndex(int index)
2025-09-22 17:02:19 +08:00
{
2026-03-20 17:42:24 +08:00
if (Images.Count == 0) return 0;
var m = index % Images.Count;
if (m < 0) m += Images.Count;
return m;
2025-09-22 15:22:53 +08:00
}
2025-09-22 17:02:19 +08:00
2026-03-20 17:42:24 +08:00
private double SlotDistance => CardWidth + CardGap;
private double XFarLeft => 0;
private double XLeft => SlotDistance;
private double XCenter => SlotDistance * 2;
private double XRight => SlotDistance * 3;
private double XOffLeft => -SlotDistance;
2026-03-27 17:42:25 +08:00
private double XOffRight => SlotDistance * 4;
2026-03-20 17:42:24 +08:00
private void SetCard(ContentPresenter card, CoverFlowItem? item, double left, double opacity = 1)
2025-09-22 17:02:19 +08:00
{
2026-03-20 17:42:24 +08:00
card.Content = item;
Canvas.SetLeft(card, left);
Canvas.SetTop(card, 0);
card.Opacity = opacity;
}
private void RefreshCards(bool instant)
{
if (Images.Count == 0) return;
var farLeft = Images[WrapIndex(_selectedIndex - 2)];
var left = Images[WrapIndex(_selectedIndex - 1)];
var center = Images[WrapIndex(_selectedIndex)];
var right = Images[WrapIndex(_selectedIndex + 1)];
SetCard(CardFarLeft, farLeft, XFarLeft, 1);
SetCard(CardLeft, left, XLeft, 1);
SetCard(CardCenter, center, XCenter, 1);
SetCard(CardRight, right, XRight, 1);
// spare stays hidden offscreen
SetCard(CardSpare, null, XOffRight, 0);
if (!instant)
return;
}
private void Carousel_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (e.Delta < 0) SlideLeft();
else if (e.Delta > 0) SlideRight();
e.Handled = true;
}
private void Carousel_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Right || e.Key == Key.Down)
2025-09-22 17:02:19 +08:00
{
2026-03-20 17:42:24 +08:00
SlideLeft();
e.Handled = true;
2025-09-22 17:02:19 +08:00
}
2026-03-20 17:42:24 +08:00
else if (e.Key == Key.Left || e.Key == Key.Up)
{
SlideRight();
e.Handled = true;
}
}
private void CardLeft_Click(object sender, MouseButtonEventArgs e)
{
RequestSlide(-1);
}
private void CardRight_Click(object sender, MouseButtonEventArgs e)
{
RequestSlide(+1);
}
private void CardFarLeft_Click(object sender, MouseButtonEventArgs e)
{
RequestSlide(-2);
}
private void CardCenter_Click(object sender, MouseButtonEventArgs e)
{
// center click: treat as selecting current (already selected)
}
private void RequestSlide(int steps)
{
if (Images.Count == 0) return;
if (steps == 0) return;
_pendingSlideSteps += steps;
TryRunNextSlide();
2025-09-22 17:02:19 +08:00
}
2026-03-20 17:42:24 +08:00
private void TryRunNextSlide()
2025-09-22 17:02:19 +08:00
{
2026-03-20 17:42:24 +08:00
if (_isAnimating) return;
if (_pendingSlideSteps == 0) return;
2025-09-23 10:15:06 +08:00
2026-03-20 17:42:24 +08:00
if (_pendingSlideSteps > 0)
2025-09-22 17:02:19 +08:00
{
2026-03-20 17:42:24 +08:00
_pendingSlideSteps--;
AnimateLeftOne();
}
else
{
_pendingSlideSteps++;
AnimateRightOne();
}
}
2025-09-23 10:15:06 +08:00
2026-03-20 17:42:24 +08:00
private void AnimateLeftOne()
{
if (_isAnimating) return;
if (Images.Count == 0) return;
2025-09-23 10:15:06 +08:00
2026-03-20 17:42:24 +08:00
_isAnimating = true;
StopProgress();
2025-09-23 10:15:06 +08:00
2026-03-20 17:42:24 +08:00
var newSelected = WrapIndex(_selectedIndex + 1);
2026-03-27 17:42:25 +08:00
var incomingRight = Images[WrapIndex(newSelected + 1)];
2025-09-23 10:15:06 +08:00
2026-03-27 17:42:25 +08:00
// Prepare spare entering from the right (to right slot)
SetCard(CardSpare, incomingRight, XOffRight, 1);
2026-03-20 17:42:24 +08:00
var sb = new Storyboard { FillBehavior = FillBehavior.Stop };
var ease = new CubicEase { EasingMode = EasingMode.EaseOut };
var dur = TimeSpan.FromMilliseconds(SlideDurationMs);
2025-09-22 17:02:19 +08:00
2026-03-20 17:42:24 +08:00
void AddMove(FrameworkElement fe, double toX)
{
var anim = new DoubleAnimation
2025-09-23 10:15:06 +08:00
{
2026-03-20 17:42:24 +08:00
To = toX,
Duration = new Duration(dur),
EasingFunction = ease
};
Storyboard.SetTarget(anim, fe);
Storyboard.SetTargetProperty(anim, new PropertyPath("(Canvas.Left)"));
sb.Children.Add(anim);
}
void AddFade(FrameworkElement fe, double toOpacity)
{
var anim = new DoubleAnimation
2025-09-23 10:15:06 +08:00
{
2026-03-20 17:42:24 +08:00
To = toOpacity,
Duration = new Duration(dur),
EasingFunction = ease
};
Storyboard.SetTarget(anim, fe);
Storyboard.SetTargetProperty(anim, new PropertyPath("Opacity"));
sb.Children.Add(anim);
2025-09-23 10:15:06 +08:00
}
2026-03-20 17:42:24 +08:00
// Far-left card slides out to left and fades
AddMove(CardFarLeft, XOffLeft);
AddFade(CardFarLeft, 0);
2026-03-27 17:42:25 +08:00
// Shift others left by one slot, spare enters to right
2026-03-20 17:42:24 +08:00
AddMove(CardLeft, XFarLeft);
AddMove(CardCenter, XLeft);
AddMove(CardRight, XCenter);
2026-03-27 17:42:25 +08:00
AddMove(CardSpare, XRight);
2026-03-20 17:42:24 +08:00
sb.Completed += (_, _) =>
{
_selectedIndex = newSelected;
UpdateSelectionFlags();
var oldFarLeftContent = (CoverFlowItem?)CardFarLeft.Content;
var oldLeftContent = (CoverFlowItem?)CardLeft.Content;
var oldCenterContent = (CoverFlowItem?)CardCenter.Content;
var oldRightContent = (CoverFlowItem?)CardRight.Content;
var oldSpareContent = (CoverFlowItem?)CardSpare.Content;
SetCard(CardFarLeft, oldLeftContent, XFarLeft, 1);
SetCard(CardLeft, oldCenterContent, XLeft, 1);
SetCard(CardCenter, oldRightContent, XCenter, 1);
2026-03-27 17:42:25 +08:00
SetCard(CardRight, oldSpareContent, XRight, 1);
2026-03-20 17:42:24 +08:00
SetCard(CardSpare, null, XOffRight, 0);
_isAnimating = false;
TryRunNextSlide();
};
sb.Begin(this, true);
2025-09-22 17:02:19 +08:00
}
2026-03-20 17:42:24 +08:00
private void AnimateRightOne()
2025-09-22 17:02:19 +08:00
{
2026-03-20 17:42:24 +08:00
if (_isAnimating) return;
if (Images.Count == 0) return;
_isAnimating = true;
StopProgress();
var newSelected = WrapIndex(_selectedIndex - 1);
var incomingFarLeft = Images[WrapIndex(newSelected - 2)];
// Prepare spare entering from the left (to far-left slot)
SetCard(CardSpare, incomingFarLeft, XOffLeft, 1);
var sb = new Storyboard { FillBehavior = FillBehavior.Stop };
var ease = new CubicEase { EasingMode = EasingMode.EaseOut };
var dur = TimeSpan.FromMilliseconds(SlideDurationMs);
void AddMove(FrameworkElement fe, double toX)
2025-09-23 10:15:06 +08:00
{
2026-03-20 17:42:24 +08:00
var anim = new DoubleAnimation
{
To = toX,
Duration = new Duration(dur),
EasingFunction = ease
};
Storyboard.SetTarget(anim, fe);
Storyboard.SetTargetProperty(anim, new PropertyPath("(Canvas.Left)"));
sb.Children.Add(anim);
2025-09-23 10:15:06 +08:00
}
2026-03-20 17:42:24 +08:00
void AddFade(FrameworkElement fe, double toOpacity)
{
var anim = new DoubleAnimation
{
To = toOpacity,
Duration = new Duration(dur),
EasingFunction = ease
};
Storyboard.SetTarget(anim, fe);
Storyboard.SetTargetProperty(anim, new PropertyPath("Opacity"));
sb.Children.Add(anim);
}
2026-03-27 17:42:25 +08:00
// Right card slides out to right and fades
AddMove(CardRight, XOffRight);
AddFade(CardRight, 0);
2026-03-20 17:42:24 +08:00
// Shift others right by one slot, spare enters to far-left
AddMove(CardCenter, XRight);
AddMove(CardLeft, XCenter);
AddMove(CardFarLeft, XLeft);
AddMove(CardSpare, XFarLeft);
sb.Completed += (_, _) =>
{
_selectedIndex = newSelected;
UpdateSelectionFlags();
var oldFarLeftContent = (CoverFlowItem?)CardFarLeft.Content;
var oldLeftContent = (CoverFlowItem?)CardLeft.Content;
var oldCenterContent = (CoverFlowItem?)CardCenter.Content;
var oldRightContent = (CoverFlowItem?)CardRight.Content;
var oldSpareContent = (CoverFlowItem?)CardSpare.Content;
SetCard(CardRight, oldCenterContent, XRight, 1);
SetCard(CardCenter, oldLeftContent, XCenter, 1);
SetCard(CardLeft, oldFarLeftContent, XLeft, 1);
SetCard(CardFarLeft, oldSpareContent, XFarLeft, 1);
SetCard(CardSpare, null, XOffRight, 0);
_isAnimating = false;
TryRunNextSlide();
};
sb.Begin(this, true);
2025-09-22 17:02:19 +08:00
}
2025-09-28 09:17:44 +08:00
2025-09-23 16:04:47 +08:00
public void SlideLeft()
{
2026-03-20 17:42:24 +08:00
RequestSlide(+1);
2025-09-23 16:04:47 +08:00
}
public void SlideRight()
{
2026-03-20 17:42:24 +08:00
RequestSlide(-1);
2025-09-23 16:04:47 +08:00
}
2026-03-27 17:42:25 +08:00
2025-09-22 15:22:53 +08:00
}
2025-09-28 09:17:44 +08:00
}