using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; using PCL.Core.UI.Controls; namespace PCL; public class MyCard : AnimatedBackgroundGrid { // 动画 private const double dropShadowIdleOpacity = 0.07d; private const double dropShadowHoverOpacity = 0.4d; public static readonly DependencyProperty TitleProperty = DependencyProperty.Register("Title", typeof(string), typeof(MyCard), new PropertyMetadata("")); private readonly BlurBorder mainBorder; // 控件 private readonly Grid mainGrid; private TextBlock _MainTextBlock; private bool isLoad; // UI 建立 public MyCard() : base(BlurBorder.BackgroundProperty) { MainChrome = new MyDropShadow { Margin = new Thickness(-3, -3, -3, -3 - ModBase.GetWPFSize(1d)), ShadowRadius = 3d, Opacity = dropShadowIdleOpacity, CornerRadius = new CornerRadius(5d) }; MainChrome.SetResourceReference(MyDropShadow.ColorProperty, "ColorObject1"); Children.Insert(0, MainChrome); mainBorder = new BlurBorder { CornerRadius = new CornerRadius(5d), IsHitTestVisible = false }; Children.Insert(1, mainBorder); mainGrid = new Grid(); Children.Add(mainGrid); // 设置背景色 SetResourceReference(BackgroundBrushProperty, "ColorBrushTransparentBackground"); Loaded += (_, _) => Init(); MouseEnter += MyCard_MouseEnter; MouseLeave += MyCard_MouseLeave; SizeChanged += MySizeChanged; MouseLeftButtonDown += MyCard_MouseLeftButtonDown; MouseLeftButtonUp += MyCard_MouseLeftButtonUp; MouseLeave += MyCard_MouseLeave_Swap; } public MyDropShadow MainChrome { get; } public UIElement BorderChild { get => mainBorder.Child; set => mainBorder.Child = value; } public TextBlock MainTextBlock { get { Init(); // 当父级触发 Loaded 时,本卡片可能尚未触发 Loaded(该事件从父级向子级调用),因此这会是 null。手动触发以确保控件已加载。 return _MainTextBlock; } set => _MainTextBlock = value; } public Path MainSwap { get { Init(); return field; } set => field = value; } // 属性 public InlineCollection Inlines => MainTextBlock.Inlines; public CornerRadius CornerRadius { get => MainChrome.CornerRadius; set { MainChrome.CornerRadius = value; mainBorder.CornerRadius = value; } } public string Title { get => (string)GetValue(TitleProperty); set { SetValue(TitleProperty, value); if (_MainTextBlock is not null) MainTextBlock.Text = value; } } protected override SolidColorBrush AnimatableBrush { get => (SolidColorBrush)mainBorder.Background; set => mainBorder.Background = value; } protected override FrameworkElement AnimatableElement => mainBorder; public bool HasMouseAnimation { get; set; } = true; private void Init() { if (isLoad) return; isLoad = true; // AddHandler ThemeChanged, AddressOf _BackgroundBrushChanged '已在依赖属性中实现 // 初次加载限定 if (MainTextBlock is null) { MainTextBlock = new TextBlock { HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(15d, 12d, 0d, 0d), FontWeight = FontWeights.Bold, FontSize = 13d, IsHitTestVisible = false }; MainTextBlock.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush1"); MainTextBlock.SetBinding(TextBlock.TextProperty, new Binding("Title") { Source = this, Mode = BindingMode.OneWay }); mainGrid.Children.Add(MainTextBlock); } if (CanSwap || SwapControl is not null) { if (SwapControl is null && Children.Count > 3) SwapControl = Children[3]; MainSwap = new Path { HorizontalAlignment = HorizontalAlignment.Right, Stretch = Stretch.Uniform, Height = 6d, Width = 10d, VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(0d, 17d, 16d, 0d), Data = (Geometry)new GeometryConverter().ConvertFromString("M2,4 l-2,2 10,10 10,-10 -2,-2 -8,8 -8,-8 z"), RenderTransform = new RotateTransform(180d), RenderTransformOrigin = new Point(0.5d, 0.5d) }; MainSwap.SetResourceReference(Shape.FillProperty, "ColorBrush1"); mainGrid.Children.Add(MainSwap); } // 改变默认的折叠 if (IsSwapped && SwapControl is not null) { MainSwap.RenderTransform = new RotateTransform(SwapLogoRight ? 270 : 0); SwapControl.Visibility = Visibility.Collapsed; // 取消由于高度变化被迫触发的高度动画 var rawUseAnimation = UseAnimation; UseAnimation = false; Height = SwapedHeight; ModAnimation.AniStop("MyCard Height " + uuid); isHeightAnimating = false; ModBase.RunInUi(() => UseAnimation = rawUseAnimation, true); } } public void StackInstall() { var argstack = (StackPanel)SwapControl; StackInstall(ref argstack, InstallMethod); SwapControl = argstack; TriggerForceResize(); } public static void StackInstall(ref StackPanel stack, Action installMethod) { if (stack.Tag is null) return; try { installMethod(stack); } catch (Exception ex) { ModBase.Log(ex, "[MyCard] InstallMethod 调用失败"); } stack.Children.Add(new FrameworkElement { Height = 18d }); // 下边距,同时适应折叠 stack.Tag = null; } private void MyCard_MouseEnter(object sender, MouseEventArgs e) { if (!HasMouseAnimation) return; var aniList = new List(); if (MainTextBlock is not null) aniList.Add(ModAnimation.AaColor(MainTextBlock, TextBlock.ForegroundProperty, "ColorBrush2", 90)); if (MainSwap is not null) aniList.Add(ModAnimation.AaColor(MainSwap, Shape.FillProperty, "ColorBrush2", 90)); aniList.AddRange(new[] { ModAnimation.AaColor(MainChrome, MyDropShadow.ColorProperty, "ColorObject4", 90), ModAnimation.AaOpacity(MainChrome, dropShadowHoverOpacity - MainChrome.Opacity, 90) }); if (!IsAnimating) ModAnimation.AniStart(aniList, "MyCard Mouse " + uuid); } private void MyCard_MouseLeave(object sender, MouseEventArgs e) { if (!HasMouseAnimation) return; var aniList = new List(); if (MainTextBlock is not null) aniList.Add(ModAnimation.AaColor(MainTextBlock, TextBlock.ForegroundProperty, "ColorBrush1", 90)); if (MainSwap is not null) aniList.Add(ModAnimation.AaColor(MainSwap, Shape.FillProperty, "ColorBrush1", 90)); aniList.AddRange(new[] { ModAnimation.AaColor(MainChrome, MyDropShadow.ColorProperty, "ColorObject1", 90), ModAnimation.AaOpacity(MainChrome, dropShadowIdleOpacity - MainChrome.Opacity, 90) }); if (!IsAnimating) ModAnimation.AniStart(aniList, "MyCard Mouse " + uuid); } #region 高度改变动画 /// /// 是否启用高度改变动画。 /// public bool UseAnimation { get; set; } = true; private bool isHeightAnimating; private double actualUsedHeight; // 回滚实际高度(例如 NaN) private void MySizeChanged(object sender, SizeChangedEventArgs e) { if (!UseAnimation) return; var deltaHeight = (IsSwapped ? SwapedHeight : e.NewSize.Height) - e.PreviousSize.Height; // 卡片的进入时动画已被页面通用切换动画替代 if (e.PreviousSize.Height == 0d || isHeightAnimating || Math.Abs(deltaHeight) < 1d || ActualHeight == 0d) return; StartHeightAnimation(deltaHeight, e.PreviousSize.Height, false); } /// /// 启动卡片高度变化的动画效果 /// 根据变化距离的大小采用不同的动画策略:短距离使用简单缓动,长距离使用分段动画 /// /// 高度变化量 /// 之前的高度 /// 是否为加载动画 private void StartHeightAnimation(double delta, double previousHeight, bool isLoadAnimation) { if (isHeightAnimating || ModMain.frmMain is null) return; // 避免 XAML 设计器出错 var animList = new List(); var absDelta = Math.Abs(delta); if (absDelta <= 800d) { // 短距离,直接使用 150ms 的缓动动画 animList.Add(ModAnimation.AaHeight(this, delta, 150, ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong))); } else { var easeLength = default(int); int easeTime; int initSpeed; // 到达缓动区前的初速度 if (delta < 0d && absDelta - easeLength > 5000d * 0.1d) { // 收回距离过长 (>0.1s),强制以 100ms 完成匀速段,然后让减速段更长 easeLength = 200; easeTime = 150; initSpeed = (int)Math.Round((absDelta - easeLength) / 0.1d); } else if (delta > 0d && absDelta - easeLength > 5000d * 0.6d) { // 展开距离过长 (>0.6s),以 5000 速度展示 300ms 匀速段,剩下的距离全部归入减速段 initSpeed = 5000; easeLength = (int)Math.Round(absDelta - initSpeed * 0.3d); easeTime = 400; } else { // 中程,匀速地快速展开(或收回) easeLength = 150; easeTime = 200; initSpeed = 4000; } // 匀速段 animList.Add(ModAnimation.AaHeight(this, (absDelta - easeLength) * Math.Sign(delta), (int)Math.Round((absDelta - easeLength) / initSpeed * 1000d))); // 减速段 animList.Add(ModAnimation.AaHeight(this, easeLength * Math.Sign(delta), easeTime, ease: new ModAnimation.AniEaseOutFluentWithInitial(initSpeed, easeTime / 1000d, easeLength), after: true)); } animList.Add(ModAnimation.AaCode(() => { isHeightAnimating = false; Height = actualUsedHeight; if (IsSwapped && SwapControl is not null) SwapControl.Visibility = Visibility.Collapsed; }, after: true)); ModAnimation.AniStart(animList, "MyCard Height " + uuid); isHeightAnimating = true; actualUsedHeight = IsSwapped ? SwapedHeight : Height; Height = previousHeight; } /// /// 通知 MyCard,控件内容已改变,需要中断动画并瞬间更新高度。 /// public void TriggerForceResize() { Height = IsSwapped ? SwapedHeight : double.NaN; ModAnimation.AniStop("MyCard Height " + uuid); isHeightAnimating = false; } #endregion #region 折叠 // 若设置了 CanSwap,或 SwapControl 不为空,则判定为会进行折叠 // 这是因为不能直接在 XAML 中设置 SwapControl public UIElement SwapControl; public bool CanSwap { get; set; } = false; /// /// 数据转为列表项的转换方法 /// /// public Action InstallMethod { get; set; } /// /// 是否已被折叠。 /// public bool IsSwapped { get => field; set { if (field == value) return; field = value; if (SwapControl is null) return; // 当卡片展开时,如果SwapControl是StackPanel类型,则执行安装方法 // 这通常用于动态添加内容到折叠卡片中 if (!IsSwapped && SwapControl is StackPanel) { var argstack = (StackPanel)SwapControl; StackInstall(ref argstack, InstallMethod); SwapControl = argstack; } // 若尚未加载,会在 Loaded 事件中触发无动画的折叠,不需要在这里进行 if (!IsLoaded) return; // 更新控件的可见性和高度 SwapControl.Visibility = Visibility.Visible; TriggerForceResize(); // 根据折叠状态旋转箭头图标 // 折叠时箭头指向右侧或向上(根据SwapLogoRight设置),展开时指向下方 ModAnimation.AniStart( ModAnimation.AaRotateTransform(MainSwap, (field ? SwapLogoRight ? 270 : 0 : 180) - ((RotateTransform)MainSwap.RenderTransform).Angle, 250, ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)), "MyCard Swap " + uuid, true); } } /// /// 是否已被折叠。(已过时,请使用 IsSwapped) /// [Obsolete("请使用 IsSwapped 属性,IsSwaped 存在拼写错误")] public bool IsSwaped { get => IsSwapped; set => IsSwapped = value; } public bool SwapLogoRight { get; set; } = false; private bool isSwapMouseDown = false; //用于触发卡片展开/折叠的 MouseDown private bool isCustomMouseDown = false; //用于触发自定义事件的 MouseDown public event PreviewSwapEventHandler? PreviewSwap; public delegate void PreviewSwapEventHandler(object sender, ModBase.RouteEventArgs e); public event SwapEventHandler? Swap; public delegate void SwapEventHandler(object sender, ModBase.RouteEventArgs e); public const int SwapedHeight = 40; private void MyCard_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { double pos = Mouse.GetPosition(this).Y; if (!IsSwapped && (pos > (IsSwapped ? SwapedHeight : SwapedHeight - 6) || (pos == 0 && !IsMouseDirectlyOver))) return; isCustomMouseDown = true; if (!IsSwapped && (SwapControl is null || pos > (IsSwapped ? SwapedHeight : SwapedHeight - 6) || (pos == 0 && !IsMouseDirectlyOver))) return; isSwapMouseDown = true; } private void MyCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (!isCustomMouseDown) return; isCustomMouseDown = false; ModMain.RaiseCustomEvent(this); if (!isSwapMouseDown) return; isSwapMouseDown = false; double pos = Mouse.GetPosition(this).Y; if (!IsSwapped && (SwapControl is null || pos > (IsSwapped ? SwapedHeight : SwapedHeight - 6) || (pos == 0 && !IsMouseDirectlyOver))) return; // 检测点击位置;或已经不在可视树上的误判 var e2 = new ModBase.RouteEventArgs(true); PreviewSwap?.Invoke(this, e2); if (e2.handled) { isSwapMouseDown = false; return; } IsSwapped = !IsSwapped; ModBase.Log("[Control] " + (IsSwapped ? "折叠卡片" : "展开卡片") + (Title is null ? "" : ":" + Title)); Swap?.Invoke(this, e2); } private void MyCard_MouseLeave_Swap(object sender, MouseEventArgs e) { isSwapMouseDown = false; } #endregion } public static partial class ModAnimation { public static void AniDispose(MyCard control, bool removeFromChildren, ParameterizedThreadStart callBack = null) { if (control.IsHitTestVisible) { control.IsHitTestVisible = false; AniStart(new[] { AaScaleTransform(control, -0.08d, 200, ease: new AniEaseInFluent()), AaOpacity(control, -1, 200, ease: new AniEaseOutFluent()), AaHeight(control, -control.ActualHeight, 150, 100, new AniEaseOutFluent()), AaCode(() => { if (removeFromChildren) { if (control.Parent is null) return; ((Panel)control.Parent).Children.Remove(control); } else { control.Visibility = Visibility.Collapsed; } if (callBack is not null) callBack(control); }, after: true) }, "MyCard Dispose " + control.uuid); } else { if (removeFromChildren) { if (control.Parent is null) return; ((Panel)control.Parent).Children.Remove(control); } else { control.Visibility = Visibility.Collapsed; } if (callBack is not null) callBack(control); } } }