400 lines
19 KiB
VB.net
400 lines
19 KiB
VB.net
Public Class MyCard
|
||
|
||
'控件
|
||
Inherits Grid
|
||
Private ReadOnly MainGrid As Grid
|
||
Public ReadOnly Property MainChrome As MyDropShadow
|
||
Private ReadOnly MainBorder As Border
|
||
Public Property BorderChild As UIElement
|
||
Get
|
||
Return MainBorder.Child
|
||
End Get
|
||
Set(value As UIElement)
|
||
MainBorder.Child = value
|
||
End Set
|
||
End Property
|
||
Private _MainTextBlock As TextBlock
|
||
Public Property MainTextBlock As TextBlock
|
||
Get
|
||
Init() '当父级触发 Loaded 时,本卡片可能尚未触发 Loaded(该事件从父级向子级调用),因此这会是 null。手动触发以确保控件已加载。
|
||
Return _MainTextBlock
|
||
End Get
|
||
Set(value As TextBlock)
|
||
_MainTextBlock = value
|
||
End Set
|
||
End Property
|
||
Private _MainSwap As Shapes.Path
|
||
Public Property MainSwap As Shapes.Path
|
||
Get
|
||
Init()
|
||
Return _MainSwap
|
||
End Get
|
||
Set(value As Shapes.Path)
|
||
_MainSwap = value
|
||
End Set
|
||
End Property
|
||
|
||
'属性
|
||
Public Uuid As Integer = GetUuid()
|
||
Public ReadOnly Property Inlines As InlineCollection
|
||
Get
|
||
Return MainTextBlock.Inlines
|
||
End Get
|
||
End Property
|
||
Public Property CornerRadius As CornerRadius
|
||
Get
|
||
Return MainChrome.CornerRadius
|
||
End Get
|
||
Set(value As CornerRadius)
|
||
MainChrome.CornerRadius = value
|
||
MainBorder.CornerRadius = value
|
||
End Set
|
||
End Property
|
||
Public Property Title As String
|
||
Get
|
||
Return GetValue(TitleProperty)
|
||
End Get
|
||
Set(value As String)
|
||
SetValue(TitleProperty, value)
|
||
If _MainTextBlock IsNot Nothing Then MainTextBlock.Text = value
|
||
End Set
|
||
End Property
|
||
Public Shared ReadOnly TitleProperty As DependencyProperty = DependencyProperty.Register("Title", GetType(String), GetType(MyCard), New PropertyMetadata(""))
|
||
|
||
'UI 建立
|
||
Public Sub New()
|
||
MainChrome = New MyDropShadow With {
|
||
.Margin = New Thickness(-3, -3, -3, -3 - GetWPFSize(1)), .ShadowRadius = 3, .Opacity = DropShadowIdleOpacity, .CornerRadius = New CornerRadius(5)}
|
||
MainChrome.SetResourceReference(MyDropShadow.ColorProperty, "ColorObject1")
|
||
Children.Insert(0, MainChrome)
|
||
MainBorder = New Border With {.Background = New SolidColorBrush(Color.FromArgb(245, 255, 255, 255)), .CornerRadius = New CornerRadius(5), .IsHitTestVisible = False}
|
||
Children.Insert(1, MainBorder)
|
||
MainGrid = New Grid
|
||
Children.Add(MainGrid)
|
||
End Sub
|
||
Private IsLoad As Boolean = False
|
||
Private Sub Init() Handles Me.Loaded
|
||
If IsLoad Then Return
|
||
IsLoad = True
|
||
'初次加载限定
|
||
If MainTextBlock Is Nothing Then
|
||
MainTextBlock = New TextBlock With {.HorizontalAlignment = HorizontalAlignment.Left, .VerticalAlignment = VerticalAlignment.Top, .Margin = New Thickness(15, 12, 0, 0), .FontWeight = FontWeights.Bold, .FontSize = 13, .IsHitTestVisible = False}
|
||
MainTextBlock.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush1")
|
||
MainTextBlock.SetBinding(TextBlock.TextProperty, New Binding("Title") With {.Source = Me, .Mode = BindingMode.OneWay})
|
||
MainGrid.Children.Add(MainTextBlock)
|
||
End If
|
||
If CanSwap OrElse SwapControl IsNot Nothing Then
|
||
If SwapControl Is Nothing AndAlso Children.Count > 3 Then SwapControl = Children(3)
|
||
MainSwap = New Shapes.Path With {.HorizontalAlignment = HorizontalAlignment.Right, .Stretch = Stretch.Uniform, .Height = 6, .Width = 10, .VerticalAlignment = VerticalAlignment.Top, .Margin = New Thickness(0, 17, 16, 0), .Data = New GeometryConverter().ConvertFromString("M2,4 l-2,2 10,10 10,-10 -2,-2 -8,8 -8,-8 z"), .RenderTransform = New RotateTransform(180), .RenderTransformOrigin = New Point(0.5, 0.5)}
|
||
MainSwap.SetResourceReference(Shapes.Path.FillProperty, "ColorBrush1")
|
||
MainGrid.Children.Add(MainSwap)
|
||
End If
|
||
'改变默认的折叠
|
||
If IsSwapped AndAlso SwapControl IsNot Nothing Then
|
||
MainSwap.RenderTransform = New RotateTransform(If(SwapLogoRight, 270, 0))
|
||
SwapControl.Visibility = Visibility.Collapsed
|
||
'取消由于高度变化被迫触发的高度动画
|
||
Dim RawUseAnimation As Boolean = UseAnimation
|
||
UseAnimation = False
|
||
Height = SwapedHeight
|
||
AniStop("MyCard Height " & Uuid)
|
||
IsHeightAnimating = False
|
||
RunInUi(Sub() UseAnimation = RawUseAnimation, True)
|
||
End If
|
||
End Sub
|
||
Public Sub StackInstall()
|
||
StackInstall(SwapControl, SwapType, Title)
|
||
TriggerForceResize()
|
||
End Sub
|
||
Public Shared Sub StackInstall(ByRef Stack As StackPanel, Type As Integer, Optional CardTitle As String = "")
|
||
'这一部分的代码是好几年前留下的究极屎坑,当时还不知道该咋正确调用这种方法,就写了这么一坨屎
|
||
'但是现在……反正勉强能用……懒得改了就这样吧.jpg
|
||
'别骂了别骂了.jpg
|
||
If Stack.Tag Is Nothing Then Return
|
||
'排序
|
||
Dim HasDuplicates As Boolean = False
|
||
Select Case Type
|
||
Case 3
|
||
Stack.Tag = CType(Stack.Tag, List(Of DlOptiFineListEntry)).SortByComparison(Function(a, b) CompareVersionGE(a.DisplayName, b.DisplayName))
|
||
Case 4, 10
|
||
Stack.Tag = CType(Stack.Tag, List(Of DlLiteLoaderListEntry)).SortByComparison(Function(a, b) CompareVersionGE(a.Inherit, b.Inherit))
|
||
Case 6
|
||
Stack.Tag = CType(Stack.Tag, List(Of DlForgeVersionEntry)).OrderByDescending(Function(a) a).ToList
|
||
Case 8, 9
|
||
Stack.Tag = CType(Stack.Tag, List(Of ResourceVersion)).SortByComparison(Function(a, b) a.ReleaseDate > b.ReleaseDate)
|
||
HasDuplicates =
|
||
CType(Stack.Tag, List(Of ResourceVersion)).DistinctBy(Function(a) a.Display).Count <>
|
||
CType(Stack.Tag, List(Of ResourceVersion)).Count
|
||
End Select
|
||
'控件转换
|
||
Select Case Type
|
||
Case 6
|
||
ForgeDownloadListItemPreload(Stack, Stack.Tag, AddressOf ForgeSave_Click, True)
|
||
Case 8
|
||
PageDownloadResourceDetail.ResourceFilesCardPreload(Stack, Stack.Tag)
|
||
End Select
|
||
'实现控件虚拟化
|
||
For Each Data As Object In Stack.Tag
|
||
Select Case Type
|
||
Case 0
|
||
Stack.Children.Add(CType(Data, McInstance).ToListItem(AddressOf PageSelectRight.McInstanceListContent))
|
||
Case 2
|
||
Stack.Children.Add(McDownloadListItem(Data, AddressOf McDownloadMenuSave, True))
|
||
Case 3
|
||
Stack.Children.Add(OptiFineDownloadListItem(Data, AddressOf OptiFineSave_Click, True))
|
||
Case 6
|
||
Stack.Children.Add(ForgeDownloadListItem(Data, AddressOf ForgeSave_Click, True))
|
||
Case 7
|
||
'不能使用 AddressOf,这导致了 #535,原因完全不明,疑似是编译器 Bug
|
||
Stack.Children.Add(McDownloadListItem(Data, Sub(sender, e) FrmDownloadInstall.MinecraftSelected(sender, e), False))
|
||
Case 8
|
||
'若存在重复的版本名,则显示文件名而非版本名(#1344)
|
||
Stack.Children.Add(CType(Data, ResourceVersion).ToListItem(
|
||
AddressOf FrmDownloadResourceDetail.Save_Click, IsBadDisplay:=HasDuplicates))
|
||
Case 9
|
||
'若存在重复的版本名,则显示文件名而非版本名(#1344)
|
||
Stack.Children.Add(CType(Data, ResourceVersion).ToListItem(
|
||
AddressOf FrmDownloadResourceDetail.Install_Click, AddressOf FrmDownloadResourceDetail.Save_Click, IsBadDisplay:=HasDuplicates))
|
||
Case 10
|
||
Stack.Children.Add(LiteLoaderDownloadListItem(Data, AddressOf LiteLoaderSave_Click, True))
|
||
Case 11
|
||
Stack.Children.Add(CType(Data, HelpEntry).ToListItem)
|
||
Case 12
|
||
Stack.Children.Add(FabricDownloadListItem(CType(Data, JObject), AddressOf FrmDownloadInstall.Fabric_Selected))
|
||
Case 13
|
||
Stack.Children.Add(NeoForgeDownloadListItem(Data, AddressOf NeoForgeSave_Click, True))
|
||
Case Else
|
||
Logger.Error($"未知的虚拟化种类:{Type}")
|
||
End Select
|
||
Next
|
||
Stack.Children.Add(New FrameworkElement With {.Height = 18}) '下边距,同时适应折叠
|
||
Stack.Tag = Nothing
|
||
End Sub
|
||
|
||
'动画
|
||
Private Const DropShadowIdleOpacity As Double = 0.07
|
||
Private Const DropShadowHoverOpacity As Double = 0.4
|
||
Public Property HasMouseAnimation As Boolean = True
|
||
Private Sub MyCard_MouseEnter(sender As Object, e As MouseEventArgs) Handles Me.MouseEnter
|
||
If Not HasMouseAnimation Then Return
|
||
Dim AniList As New List(Of AniData)
|
||
If Not IsNothing(MainTextBlock) Then AniList.Add(AaColor(MainTextBlock, TextBlock.ForegroundProperty, "ColorBrush2", 90))
|
||
If Not IsNothing(MainSwap) Then AniList.Add(AaColor(MainSwap, Shapes.Path.FillProperty, "ColorBrush2", 90))
|
||
AniList.AddRange({
|
||
AaColor(MainChrome, MyDropShadow.ColorProperty, "ColorObject4", 90),
|
||
AaOpacity(MainChrome, DropShadowHoverOpacity - MainChrome.Opacity, 90)
|
||
})
|
||
AniStart(AniList, "MyCard Mouse " & Uuid)
|
||
End Sub
|
||
Private Sub MyCard_MouseLeave(sender As Object, e As MouseEventArgs) Handles Me.MouseLeave
|
||
If Not HasMouseAnimation Then Return
|
||
Dim AniList As New List(Of AniData)
|
||
If Not IsNothing(MainTextBlock) Then AniList.Add(AaColor(MainTextBlock, TextBlock.ForegroundProperty, "ColorBrush1", 90))
|
||
If Not IsNothing(MainSwap) Then AniList.Add(AaColor(MainSwap, Shapes.Path.FillProperty, "ColorBrush1", 90))
|
||
AniList.AddRange({
|
||
AaColor(MainChrome, MyDropShadow.ColorProperty, "ColorObject1", 90),
|
||
AaOpacity(MainChrome, DropShadowIdleOpacity - MainChrome.Opacity, 90)
|
||
})
|
||
AniStart(AniList, "MyCard Mouse " & Uuid)
|
||
End Sub
|
||
|
||
#Region "高度改变动画"
|
||
|
||
''' <summary>
|
||
''' 是否启用高度改变动画。
|
||
''' </summary>
|
||
Public Property UseAnimation As Boolean = True
|
||
Private IsHeightAnimating As Boolean = False
|
||
Private ActualUsedHeight As Double '回滚实际高度(例如 NaN)
|
||
Private Sub MySizeChanged(sender As Object, e As SizeChangedEventArgs) Handles Me.SizeChanged
|
||
If Not UseAnimation Then Return
|
||
Dim DeltaHeight As Double = If(IsSwapped, SwapedHeight, e.NewSize.Height) - e.PreviousSize.Height
|
||
'卡片的进入时动画已被页面通用切换动画替代
|
||
If e.PreviousSize.Height = 0 OrElse IsHeightAnimating OrElse Math.Abs(DeltaHeight) < 1 OrElse ActualHeight = 0 Then Return
|
||
StartHeightAnimation(DeltaHeight, e.PreviousSize.Height, False)
|
||
End Sub
|
||
Private Sub StartHeightAnimation(Delta As Double, PreviousHeight As Double, IsLoadAnimation As Boolean)
|
||
If IsHeightAnimating OrElse FrmMain Is Nothing Then Return '避免 XAML 设计器出错
|
||
|
||
Dim AnimList As New List(Of AniData)
|
||
Dim AbsDelta = Math.Abs(Delta)
|
||
|
||
If AbsDelta <= 800 Then
|
||
'短距离,直接使用 150ms 的缓动动画
|
||
AnimList.Add(AaHeight(Me, Delta, 150,, New AniEaseOutFluent(AniEasePower.ExtraStrong)))
|
||
Else
|
||
Dim EaseLength As Integer, EaseTime As Integer
|
||
Dim InitSpeed As Integer '到达缓动区前的初速度
|
||
If Delta < 0 AndAlso AbsDelta - EaseLength > 5000 * 0.1 Then
|
||
'收回距离过长 (>0.1s),强制以 100ms 完成匀速段,然后让减速段更长
|
||
EaseLength = 200
|
||
EaseTime = 150
|
||
InitSpeed = (AbsDelta - EaseLength) / 0.1
|
||
ElseIf Delta > 0 AndAlso AbsDelta - EaseLength > 5000 * 0.6 Then
|
||
'展开距离过长 (>0.6s),以 5000 速度展示 300ms 匀速段,剩下的距离全部归入减速段
|
||
InitSpeed = 5000
|
||
EaseLength = AbsDelta - InitSpeed * 0.3
|
||
EaseTime = 400
|
||
Else
|
||
'中程,匀速地快速展开(或收回)
|
||
EaseLength = 150
|
||
EaseTime = 200
|
||
InitSpeed = 4000
|
||
End If
|
||
'匀速段
|
||
AnimList.Add(AaHeight(Me, (AbsDelta - EaseLength) * Math.Sign(Delta),
|
||
(AbsDelta - EaseLength) / InitSpeed * 1000))
|
||
'减速段
|
||
AnimList.Add(AaHeight(Me, EaseLength * Math.Sign(Delta),
|
||
EaseTime,, New AniEaseOutFluentWithInitial(InitSpeed, EaseTime / 1000, EaseLength), True))
|
||
End If
|
||
|
||
AnimList.Add(AaCode(
|
||
Sub()
|
||
IsHeightAnimating = False
|
||
Height = ActualUsedHeight
|
||
If IsSwapped AndAlso SwapControl IsNot Nothing Then SwapControl.Visibility = Visibility.Collapsed
|
||
End Sub,, True))
|
||
AniStart(AnimList, "MyCard Height " & Uuid)
|
||
IsHeightAnimating = True
|
||
ActualUsedHeight = If(IsSwapped, SwapedHeight, Height)
|
||
Height = PreviousHeight
|
||
End Sub
|
||
''' <summary>
|
||
''' 通知 MyCard,控件内容已改变,需要中断动画并瞬间更新高度。
|
||
''' </summary>
|
||
Public Sub TriggerForceResize()
|
||
Height = If(IsSwapped, SwapedHeight, Double.NaN)
|
||
AniStop("MyCard Height " & Uuid)
|
||
IsHeightAnimating = False
|
||
End Sub
|
||
|
||
#End Region
|
||
|
||
#Region "折叠"
|
||
|
||
'若设置了 CanSwap,或 SwapControl 不为空,则判定为会进行折叠
|
||
'这是因为不能直接在 XAML 中设置 SwapControl
|
||
Public SwapControl As FrameworkElement
|
||
Public Property CanSwap As Boolean = False
|
||
''' <summary>
|
||
''' 被折叠的种类,用于控件虚拟化。
|
||
''' </summary>
|
||
Public Property SwapType As Integer
|
||
''' <summary>
|
||
''' 是否已被折叠。
|
||
''' </summary>
|
||
Public Property IsSwapped As Boolean
|
||
Get
|
||
Return _IsSwapped
|
||
End Get
|
||
Set(value As Boolean)
|
||
If _IsSwapped = value Then Return
|
||
_IsSwapped = value
|
||
If SwapControl Is Nothing Then Return
|
||
'展开
|
||
If Not IsSwapped AndAlso TypeOf SwapControl Is StackPanel Then StackInstall(SwapControl, SwapType, Title)
|
||
'若尚未加载,会在 Loaded 事件中触发无动画的折叠,不需要在这里进行
|
||
If Not IsLoaded Then Return
|
||
'更新样式
|
||
SwapControl.Visibility = Visibility.Visible
|
||
TriggerForceResize()
|
||
UpdateCursorStyle()
|
||
'改变箭头
|
||
AniStart(AaRotateTransform(MainSwap, If(_IsSwapped, If(SwapLogoRight, 270, 0), 180) - CType(MainSwap.RenderTransform, RotateTransform).Angle, 250,, New AniEaseOutFluent(AniEasePower.ExtraStrong)), "MyCard Swap " & Uuid, True)
|
||
End Set
|
||
End Property
|
||
Private _IsSwapped As Boolean = False
|
||
<Obsolete("IsSwaped 存在拼写错误,请换用 IsSwapped 属性。")>
|
||
Public Property IsSwaped As Boolean
|
||
Get
|
||
Return GetValue(IsSwapedProperty)
|
||
End Get
|
||
Set(value As Boolean)
|
||
SetValue(IsSwapedProperty, value)
|
||
End Set
|
||
End Property
|
||
Public Shared ReadOnly IsSwapedProperty As DependencyProperty = DependencyProperty.Register("IsSwaped", GetType(Boolean), GetType(MyCard), New PropertyMetadata(False,
|
||
Sub(sender, e) If sender IsNot Nothing AndAlso TypeOf sender Is MyCard Then CType(sender, MyCard).IsSwapped = CType(e.NewValue, Boolean)))
|
||
|
||
Public Property SwapLogoRight As Boolean = False
|
||
Private IsSwapMouseDown As Boolean = False '用于触发卡片展开/折叠的 MouseDown
|
||
Private IsCustomMouseDown As Boolean = False '用于触发自定义事件的 MouseDown
|
||
Public Event PreviewSwap(sender As Object, e As RouteEventArgs)
|
||
Public Event Swap(sender As Object, e As RouteEventArgs)
|
||
Public Const SwapedHeight As Integer = 40
|
||
Private Sub MyCard_MouseLeftButtonDown(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonDown
|
||
Dim Pos As Double = Mouse.GetPosition(Me).Y
|
||
If Not IsSwapped AndAlso
|
||
(Pos > If(IsSwapped, SwapedHeight, SwapedHeight - 6) OrElse (Pos = 0 AndAlso Not IsMouseDirectlyOver)) Then Return
|
||
IsCustomMouseDown = True
|
||
If Not IsSwapped AndAlso
|
||
(SwapControl Is Nothing OrElse Pos > If(IsSwapped, SwapedHeight, SwapedHeight - 6) OrElse (Pos = 0 AndAlso Not IsMouseDirectlyOver)) Then Return '检测点击位置;或已经不在可视树上的误判
|
||
IsSwapMouseDown = True
|
||
End Sub
|
||
Private Sub MyCard_MouseLeftButtonUp() Handles Me.MouseLeftButtonUp
|
||
If Not IsCustomMouseDown Then Return
|
||
IsCustomMouseDown = False
|
||
RaiseCustomEvent() '触发自定义事件
|
||
|
||
If Not IsSwapMouseDown Then Return
|
||
IsSwapMouseDown = False
|
||
|
||
Dim Pos As Double = Mouse.GetPosition(Me).Y
|
||
If Not IsSwapped AndAlso
|
||
(SwapControl Is Nothing OrElse Pos > If(IsSwapped, SwapedHeight, SwapedHeight - 6) OrElse (Pos = 0 AndAlso Not IsMouseDirectlyOver)) Then Return '检测点击位置;或已经不在可视树上的误判
|
||
|
||
Dim e = New RouteEventArgs(True)
|
||
RaiseEvent PreviewSwap(Me, e)
|
||
If e.Handled Then
|
||
IsSwapMouseDown = False
|
||
Return
|
||
End If
|
||
|
||
IsSwapped = Not IsSwapped
|
||
Logger.Info($"{If(IsSwapped, "折叠卡片", "展开卡片")}{If(Title Is Nothing, "", ":" & Title)}")
|
||
RaiseEvent Swap(Me, e)
|
||
End Sub
|
||
Private Sub MyCard_MouseLeave_Swap(sender As Object, e As MouseEventArgs) Handles Me.MouseLeave
|
||
IsSwapMouseDown = False
|
||
End Sub
|
||
Private Sub UpdateCursorStyle() Handles Me.MouseMove
|
||
If _MainSwap Is Nothing Then Cursor = Nothing : Return
|
||
Dim Pos As Double = Mouse.GetPosition(Me).Y
|
||
Cursor = If(MainSwap.Visibility = Visibility.Visible AndAlso (IsSwapped OrElse (Pos <= SwapedHeight - 6 AndAlso Not (Pos = 0 AndAlso Not IsMouseDirectlyOver))),
|
||
Cursors.Hand, Nothing)
|
||
End Sub
|
||
|
||
#End Region
|
||
|
||
End Class
|
||
Partial Public Module ModAnimation
|
||
Public Sub AniDispose(Control As MyCard, RemoveFromChildren As Boolean, Optional CallBack As ParameterizedThreadStart = Nothing)
|
||
If Control.IsHitTestVisible Then
|
||
Control.IsHitTestVisible = False
|
||
AniStart({
|
||
AaScaleTransform(Control, -0.08, 200,, New AniEaseInFluent),
|
||
AaOpacity(Control, -1, 200,, New AniEaseOutFluent),
|
||
AaHeight(Control, -Control.ActualHeight, 150, 100, New AniEaseOutFluent),
|
||
AaCode(
|
||
Sub()
|
||
If RemoveFromChildren Then
|
||
If Control.Parent Is Nothing Then Return
|
||
CType(Control.Parent, Object).Children.Remove(Control)
|
||
Else
|
||
Control.Visibility = Visibility.Collapsed
|
||
End If
|
||
If CallBack IsNot Nothing Then CallBack(Control)
|
||
End Sub,, True)
|
||
}, "MyCard Dispose " & Control.Uuid)
|
||
Else
|
||
If RemoveFromChildren Then
|
||
If Control.Parent Is Nothing Then Return
|
||
CType(Control.Parent, Object).Children.Remove(Control)
|
||
Else
|
||
Control.Visibility = Visibility.Collapsed
|
||
End If
|
||
If CallBack IsNot Nothing Then CallBack(Control)
|
||
End If
|
||
End Sub
|
||
End Module |