Files

499 lines
18 KiB
C#
Raw Permalink Normal View History

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<StackPanel> 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<ModAnimation.AniData>();
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<ModAnimation.AniData>();
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 高度改变动画
/// <summary>
/// 是否启用高度改变动画。
/// </summary>
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);
}
/// <summary>
/// 启动卡片高度变化的动画效果
/// 根据变化距离的大小采用不同的动画策略:短距离使用简单缓动,长距离使用分段动画
/// </summary>
/// <param name="delta">高度变化量</param>
/// <param name="previousHeight">之前的高度</param>
/// <param name="isLoadAnimation">是否为加载动画</param>
private void StartHeightAnimation(double delta, double previousHeight, bool isLoadAnimation)
{
if (isHeightAnimating || ModMain.frmMain is null)
return; // 避免 XAML 设计器出错
var animList = new List<ModAnimation.AniData>();
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;
}
/// <summary>
/// 通知 MyCard,控件内容已改变,需要中断动画并瞬间更新高度。
/// </summary>
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;
/// <summary>
/// 数据转为列表项的转换方法
/// </summary>
/// <returns></returns>
public Action<StackPanel> InstallMethod { get; set; }
/// <summary>
/// 是否已被折叠。
/// </summary>
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);
}
}
/// <summary>
/// 是否已被折叠。(已过时,请使用 IsSwapped)
/// </summary>
[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);
}
}
}