MyPageRight.vb 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. Public Class MyPageRight
  2. Inherits AdornerDecorator
  3. Public PageUuid As Integer = GetUuid()
  4. '当前状态
  5. Public Enum PageStates
  6. Empty '默认状态,页面全空
  7. LoaderWait '加载环初始等待
  8. LoaderEnter '加载环进入动画
  9. LoaderStayForce '加载环正常显示(强制等待)
  10. LoaderStay '加载环正常显示
  11. LoaderExit '加载环退出动画
  12. ContentEnter '内容进入动画
  13. ContentStay '内容正常显示
  14. ContentExit '刷新导致的全部退出动画,或页面内容退出(子页面更改)导致的全部退出动画
  15. PageExit '切换页面导致的全部退出动画
  16. End Enum
  17. Private _PageState As PageStates = PageStates.Empty
  18. Public Property PageState As PageStates
  19. Get
  20. Return _PageState
  21. End Get
  22. Set(value As PageStates)
  23. If _PageState = value Then Exit Property
  24. _PageState = value
  25. If ModeDebug Then Log("[UI] 页面状态切换为 " & GetStringFromEnum(value))
  26. End Set
  27. End Property
  28. #Region "加载器"
  29. Private PageLoader As LoaderBase
  30. Private PageLoaderInputInvoke
  31. Private PageLoaderUi As MyLoading
  32. Private PanLoader As FrameworkElement
  33. Private PanContent As FrameworkElement
  34. Private PanAlways As FrameworkElement
  35. Private PageLoaderAutoRun As Boolean
  36. '初始化
  37. ''' <summary>
  38. ''' 表明页面存在需要在后台执行的加载器。
  39. ''' </summary>
  40. ''' <param name="LoaderUi">MyLoading 控件。</param>
  41. ''' <param name="PanLoader">MyLoading 控件对应的卡片。</param>
  42. ''' <param name="PanContent">加载结束后出现的内容容器。</param>
  43. ''' <param name="PanAlways">无论是否在加载总是要显示的容器。可以为 Nothing。</param>
  44. ''' <param name="RealLoader">在工作线程执行的加载器。</param>
  45. ''' <param name="FinishedInvoke">当加载器执行完成,在 UI 线程触发的 UI 初始化事件。</param>
  46. Public Sub PageLoaderInit(LoaderUi As MyLoading, PanLoader As FrameworkElement, PanContent As FrameworkElement, PanAlways As FrameworkElement,
  47. RealLoader As LoaderBase,
  48. Optional FinishedInvoke As Action(Of LoaderBase) = Nothing, Optional InputInvoke As Func(Of Object) = Nothing,
  49. Optional AutoRun As Boolean = True)
  50. '初始化参数
  51. Me.PanLoader = PanLoader
  52. Me.PanContent = PanContent
  53. Me.PanAlways = PanAlways
  54. Me.PageLoader = RealLoader
  55. Me.PageLoaderUi = LoaderUi
  56. Me.PageLoaderInputInvoke = InputInvoke
  57. Me.PageLoaderAutoRun = AutoRun
  58. '添加结束 Invoke
  59. If FinishedInvoke IsNot Nothing Then
  60. AddHandler RealLoader.PreviewFinish,
  61. Sub()
  62. Do While PageState = MyPageRight.PageStates.PageExit OrElse PageState = MyPageRight.PageStates.ContentExit
  63. Thread.Sleep(10) '不在退出动画时执行 UI 线程操作,避免退出动画被重置
  64. Loop
  65. RunInUiWait(Sub() FinishedInvoke(RealLoader))
  66. Thread.Sleep(20) '由于大量初始化控件会导致掉帧,延迟触发 State 改变事件
  67. End Sub
  68. End If
  69. AddHandler RealLoader.OnStateChangedUi, Sub(Loader As LoaderBase, NewState As LoadState, OldState As LoadState) RunInUi(Sub() PageLoaderState(Loader, NewState, OldState))
  70. '隐藏 UI
  71. PanLoader.Visibility = Visibility.Collapsed
  72. PanContent.Visibility = Visibility.Collapsed
  73. If PanAlways IsNot Nothing Then PanAlways.Visibility = Visibility.Collapsed
  74. '初次运行加载器
  75. If PageLoaderAutoRun Then
  76. If PageLoader.GetType.Name.StartsWith("LoaderTask") Then
  77. PageLoader.Start(CType(PageLoader, Object).StartGetInput(Nothing, PageLoaderInputInvoke))
  78. Else
  79. Dim Input = Nothing
  80. If PageLoaderInputInvoke IsNot Nothing Then Input = PageLoaderInputInvoke()
  81. PageLoader.Start(Input)
  82. End If
  83. End If
  84. If PageLoader.State = LoadState.Finished AndAlso FinishedInvoke IsNot Nothing Then
  85. RunInUiWait(Sub() FinishedInvoke(RealLoader)) '加载器已提前完成,直接触发事件
  86. End If
  87. '设置加载环
  88. PageLoaderUi.State = RealLoader
  89. AddHandler PageLoaderUi.Click, Sub() If RealLoader.State = LoadState.Failed Then PageLoaderRestart() '点击重试事件
  90. End Sub
  91. '重试
  92. Public Sub PageLoaderRestart(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = True) '由外部调用的重试
  93. If Not PageLoaderAutoRun Then Exit Sub
  94. If PageLoader.GetType.Name.StartsWith("LoaderTask") Then
  95. PageLoader.Start(CType(PageLoader, Object).StartGetInput(Input, PageLoaderInputInvoke), IsForceRestart:=IsForceRestart)
  96. Else
  97. If Input Is Nothing AndAlso PageLoaderInputInvoke IsNot Nothing Then Input = PageLoaderInputInvoke()
  98. PageLoader.Start(Input, IsForceRestart:=IsForceRestart)
  99. End If
  100. End Sub
  101. #End Region
  102. #Region "事件"
  103. '外部触发的事件
  104. ''' <summary>
  105. ''' 需要切换到当前页面,并且原本的 Loaded 事件已执行完成。
  106. ''' 需要根据加载器状态,从 Empty 切换到 ContentEnter、LoaderWait、LoaderEnter。
  107. ''' </summary>
  108. Public Sub PageOnEnter()
  109. If ModeDebug Then Log("[UI] 已触发 PageOnEnter")
  110. RaiseEvent PageEnter()
  111. Select Case PageState
  112. Case PageStates.Empty
  113. If PageLoader Is Nothing OrElse PageLoader.State = LoadState.Finished OrElse PageLoader.State = LoadState.Waiting OrElse PageLoader.State = LoadState.Aborted Then
  114. '如果加载器在进入页面时不启动(例如 HiPer 联机),那么在此时就会有 State = Waiting
  115. PageState = PageStates.ContentEnter
  116. TriggerEnterAnimation(PanAlways, If(PanContent, Child))
  117. ElseIf PageLoader.State = LoadState.Loading Then
  118. PageState = PageStates.LoaderWait
  119. AniStart(AaCode(AddressOf PageOnLoaderWaitFinished, 200), "PageRight PageChange " & PageUuid)
  120. Else 'PageLoader.State = LoadState.Failed
  121. PageState = PageStates.LoaderEnter
  122. TriggerEnterAnimation(PanAlways, PanLoader)
  123. End If
  124. Case PageStates.ContentExit
  125. '和上面的一样,但是不管 PanAlways
  126. If PageLoader Is Nothing OrElse PageLoader.State = LoadState.Finished OrElse PageLoader.State = LoadState.Waiting OrElse PageLoader.State = LoadState.Aborted Then
  127. PageState = PageStates.ContentEnter
  128. TriggerEnterAnimation(If(PanContent, Child))
  129. ElseIf PageLoader.State = LoadState.Loading Then
  130. PageState = PageStates.LoaderWait
  131. AniStart(AaCode(AddressOf PageOnLoaderWaitFinished, 200), "PageRight PageChange " & PageUuid)
  132. Else 'PageLoader.State = LoadState.Failed
  133. PageState = PageStates.LoaderEnter
  134. TriggerEnterAnimation(PanLoader)
  135. End If
  136. Case PageStates.ContentEnter '重复调用 PageOnEnter,直接忽略
  137. Case Else
  138. Throw New Exception("在状态为 " & GetStringFromEnum(PageState) & " 时触发了 PageOnEnter 事件。")
  139. End Select
  140. End Sub
  141. Public Event PageEnter()
  142. ''' <summary>
  143. ''' 需要切换到其他页面。
  144. ''' 需要立即切换至 PageExit 或 Empty。
  145. ''' </summary>
  146. Public Sub PageOnExit()
  147. If ModeDebug Then Log("[UI] 已触发 PageOnExit")
  148. RaiseEvent PageExit()
  149. Select Case PageState
  150. Case PageStates.ContentEnter, PageStates.ContentStay
  151. PageState = PageStates.PageExit
  152. TriggerExitAnimation(PanAlways, If(PanContent, Child))
  153. Case PageStates.LoaderEnter, PageStates.LoaderStayForce, PageStates.LoaderStay
  154. PageState = PageStates.PageExit
  155. TriggerExitAnimation(PanAlways, PanLoader)
  156. Case PageStates.LoaderWait
  157. PageState = PageStates.PageExit
  158. TriggerExitAnimation(PanAlways)
  159. Case PageStates.LoaderExit, PageStates.ContentExit
  160. PageState = PageStates.PageExit
  161. If PanAlways IsNot Nothing Then TriggerExitAnimation(PanAlways, If(PanContent, Child))
  162. Case PageStates.PageExit, PageStates.Empty
  163. End Select
  164. End Sub
  165. Public Event PageExit()
  166. ''' <summary>
  167. ''' 即将切换到其他页面,需要强制完成页面状态清理。
  168. ''' 需要立即切换至 Empty。
  169. ''' </summary>
  170. Public Sub PageOnForceExit()
  171. If PageState = PageStates.Empty Then Exit Sub
  172. If ModeDebug Then Log("[UI] 已触发 PageOnForceExit")
  173. PageState = PageStates.Empty
  174. AniStop("PageRight PageChange " & PageUuid)
  175. '由于动画会被强制中止,所以需要手动进行隐藏
  176. If PageLoader Is Nothing Then
  177. Child.Visibility = Visibility.Collapsed
  178. Else
  179. PanContent.Visibility = Visibility.Collapsed
  180. PanLoader.Visibility = Visibility.Collapsed
  181. If PanAlways IsNot Nothing Then PanAlways.Visibility = Visibility.Collapsed
  182. End If
  183. End Sub
  184. ''' <summary>
  185. ''' PanContent 中的子页面改变,需要让当前内容退出,再显示新的内容。
  186. ''' 需要在 PageEnter 事件确认要显示的子页面有哪些。
  187. ''' </summary>
  188. Public Sub PageOnContentExit()
  189. If ModeDebug Then Log("[UI] 已触发 PageOnContentExit")
  190. If PageLoader IsNot Nothing AndAlso PageLoader.State = LoadState.Loading Then
  191. Throw New Exception("在调用 PageOnContentExit 时,加载器不能为 Loading 状态")
  192. 'Loading 的加载器可能触发进一步变化,难以预测会触发子页面的动画还是加载器完成的动画
  193. End If
  194. Select Case PageState
  195. Case PageStates.ContentEnter, PageStates.ContentStay
  196. PageState = PageStates.ContentExit
  197. TriggerExitAnimation(PanContent)
  198. Case PageStates.LoaderExit
  199. PageState = PageStates.ContentExit
  200. Case PageStates.LoaderEnter, PageStates.LoaderStayForce, PageStates.LoaderStay
  201. PageState = PageStates.ContentExit
  202. TriggerExitAnimation(PanLoader)
  203. Case PageStates.LoaderWait, PageStates.Empty
  204. PageOnEnter()
  205. End Select
  206. End Sub
  207. '内部触发的事件
  208. ''' <summary>
  209. ''' 逐个进入动画已执行完成。
  210. ''' 需要根据目前状态,从 ContentEnter 切换到 ContentStay,或从 LoaderEnter 切换到 LoaderStayForce。
  211. ''' </summary>
  212. Private Sub PageOnEnterAnimationFinished()
  213. If ModeDebug Then Log("[UI] 已触发 PageOnEnterAnimationFinished")
  214. Select Case PageState
  215. Case PageStates.ContentEnter
  216. PageState = PageStates.ContentStay
  217. Case PageStates.LoaderEnter
  218. PageState = PageStates.LoaderStayForce
  219. AniStart(AaCode(AddressOf PageOnLoaderStayFinished, 400), "PageRight PageChange " & PageUuid)
  220. Case Else
  221. Throw New Exception("在状态为 " & GetStringFromEnum(PageState) & " 时触发了 PageOnEnterAnimationFinished 事件。")
  222. End Select
  223. End Sub
  224. ''' <summary>
  225. ''' 逐个退出动画已执行完成。
  226. ''' 需要根据目前状态,从 AllExit 切换到 Empty,或从 LoaderExit 切换到 ContentEnter,或从 ContentExit 重新触发 PageOnEnter。
  227. ''' </summary>
  228. Private Sub PageOnExitAnimationFinished()
  229. If ModeDebug Then Log("[UI] 已触发 PageOnExitAnimationFinished")
  230. Select Case PageState
  231. Case PageStates.PageExit
  232. PageState = PageStates.Empty
  233. Case PageStates.ContentExit
  234. PageOnEnter()
  235. Case PageStates.LoaderExit
  236. PageState = PageStates.ContentEnter
  237. TriggerEnterAnimation(PanContent)
  238. Case Else
  239. Throw New Exception("在状态为 " & GetStringFromEnum(PageState) & " 时触发了 PageOnExitAnimationFinished 事件。")
  240. End Select
  241. End Sub
  242. ''' <summary>
  243. ''' 加载环进入等待已结束。
  244. ''' 需要从 LoaderWait 切换到 LoaderEnter。
  245. ''' </summary>
  246. Private Sub PageOnLoaderWaitFinished()
  247. If ModeDebug Then Log("[UI] 已触发 PageOnLoaderWaitFinished")
  248. Select Case PageState
  249. Case PageStates.LoaderWait
  250. PageState = PageStates.LoaderEnter
  251. If PanAlways IsNot Nothing AndAlso PanAlways.Visibility = Visibility.Collapsed Then
  252. TriggerEnterAnimation(PanAlways, PanLoader)
  253. Else
  254. TriggerEnterAnimation(PanLoader)
  255. End If
  256. Case Else
  257. Throw New Exception("在状态为 " & GetStringFromEnum(PageState) & " 时触发了 PageOnLoaderWaitFinished 事件。")
  258. End Select
  259. End Sub
  260. ''' <summary>
  261. ''' 加载环展示等待已结束。
  262. ''' 需要从 LoaderStayForce 切换到 LoaderStay 或 LoaderExit。
  263. ''' </summary>
  264. Private Sub PageOnLoaderStayFinished()
  265. If ModeDebug Then Log("[UI] 已触发 PageOnLoaderStayFinished")
  266. Select Case PageState
  267. Case PageStates.LoaderStayForce
  268. If PageLoader.State = LoadState.Finished Then
  269. PageState = PageStates.LoaderExit
  270. TriggerExitAnimation(PanLoader)
  271. Else
  272. PageState = PageStates.LoaderStay
  273. End If
  274. Case Else
  275. Throw New Exception("在状态为 " & GetStringFromEnum(PageState) & " 时触发了 PageOnLoaderWaitFinished 事件。")
  276. End Select
  277. End Sub
  278. ''' <summary>
  279. ''' 全局加载状态已改变。
  280. ''' </summary>
  281. Private Sub PageLoaderState(sender As Object, NewState As LoadState, OldState As LoadState)
  282. Select Case NewState
  283. Case LoadState.Failed, LoadState.Loading
  284. If OldState = LoadState.Failed OrElse OldState = LoadState.Loading Then Exit Sub
  285. If ModeDebug Then Log("[UI] 已触发 PageLoaderState (Start/Refresh)")
  286. '(重新)开始运行
  287. '需要从部分状态切换到 ReloadExit
  288. Select Case PageState
  289. Case PageStates.ContentEnter, PageStates.ContentStay
  290. PageState = PageStates.ContentExit
  291. TriggerExitAnimation(PanContent)
  292. Case PageStates.LoaderExit
  293. PageState = PageStates.ContentExit
  294. End Select
  295. Case LoadState.Finished, LoadState.Aborted, LoadState.Waiting
  296. If Not (OldState = LoadState.Failed OrElse OldState = LoadState.Loading) Then Exit Sub
  297. If ModeDebug Then Log("[UI] 已触发 PageLoaderState (Stop/Abort)")
  298. '运行结束
  299. '需要从 LoaderWait 切换到 ContentEnter,或从 LoaderStay 切换到 LoaderExit
  300. Select Case PageState
  301. Case PageStates.LoaderWait
  302. PageState = PageStates.ContentEnter
  303. If PanAlways IsNot Nothing AndAlso PanAlways.Visibility = Visibility.Collapsed Then
  304. TriggerEnterAnimation(PanAlways, PanContent)
  305. Else
  306. TriggerEnterAnimation(PanContent)
  307. End If
  308. Case PageStates.LoaderStay
  309. PageState = PageStates.LoaderExit
  310. TriggerExitAnimation(PanLoader)
  311. End Select
  312. End Select
  313. End Sub
  314. #End Region
  315. #Region "动画"
  316. '逐个进入动画
  317. Public Sub TriggerEnterAnimation(ParamArray Elements As FrameworkElement())
  318. Dim RealElements = Elements.Where(Function(e) e IsNot Nothing)
  319. For Each Element In RealElements
  320. Element.Visibility = Visibility.Visible '页面均处于默认的隐藏状态
  321. Next
  322. Dim AniList As New List(Of AniData)
  323. Dim Delay As Integer = 0
  324. '基础动画
  325. For Each Element In RealElements
  326. For Each Control As FrameworkElement In GetAllAnimControls(Element, True)
  327. '还原被隐藏的卡片的消失动画
  328. Control.IsHitTestVisible = True
  329. If Control.RenderTransform IsNot Nothing AndAlso TypeOf Control.RenderTransform Is TranslateTransform Then Control.RenderTransform = Nothing
  330. Next
  331. For Each Control As FrameworkElement In GetAllAnimControls(Element)
  332. Control.Opacity = 0
  333. Control.RenderTransform = New TranslateTransform(0, -16)
  334. AniList.Add(AaOpacity(Control, 1, 150, Delay, New AniEaseOutFluent(AniEasePower.Weak)))
  335. AniList.Add(AaTranslateY(Control, 5, 250, Delay, New AniEaseOutFluent))
  336. AniList.Add(AaTranslateY(Control, 11, 350, Delay, New AniEaseOutBack))
  337. Delay += 40
  338. Next
  339. Next
  340. '滚动条动画
  341. Dim Scroll As MyScrollBar = GetFirstScrollViewer(RealElements)
  342. If Scroll IsNot Nothing Then
  343. If TypeOf Scroll.RenderTransform IsNot TranslateTransform Then Scroll.RenderTransform = New TranslateTransform(10, 0)
  344. AniList.Add(AaTranslateX(Scroll, -CType(Scroll.RenderTransform, TranslateTransform).X, 350, 0, New AniEaseOutFluent))
  345. End If
  346. '结束
  347. AniList.Add(AaCode(Sub() PageOnEnterAnimationFinished(),, True))
  348. AniStart(AniList, "PageRight PageChange " & PageUuid)
  349. End Sub
  350. '逐个退出动画
  351. Public Sub TriggerExitAnimation(ParamArray Elements As FrameworkElement())
  352. Dim RealElements = Elements.Where(Function(e) e IsNot Nothing)
  353. Dim AniList As New List(Of AniData)
  354. Dim Delay As Integer = 0
  355. For Each Element In RealElements
  356. For Each Control As FrameworkElement In GetAllAnimControls(Element)
  357. Control.IsHitTestVisible = False
  358. AniList.Add(AaOpacity(Control, -1, 90, Delay))
  359. AniList.Add(AaTranslateY(Control, -6, 90, Delay))
  360. Delay += 20
  361. Next
  362. Next
  363. '滚动条动画
  364. Dim Scroll As MyScrollBar = GetFirstScrollViewer(RealElements)
  365. If Scroll IsNot Nothing Then
  366. If TypeOf Scroll.RenderTransform IsNot TranslateTransform Then Scroll.RenderTransform = New TranslateTransform
  367. AniList.Add(AaTranslateX(Scroll, 10 - CType(Scroll.RenderTransform, TranslateTransform).X, 90, 0, New AniEaseInFluent))
  368. End If
  369. '结束
  370. AniList.Add(AaCode(Sub()
  371. For Each Element In RealElements
  372. Element.Visibility = Visibility.Collapsed
  373. Next
  374. PageOnExitAnimationFinished()
  375. End Sub,, True))
  376. AniStart(AniList, "PageRight PageChange " & PageUuid)
  377. End Sub
  378. '遍历获取所有需要生成动画的控件
  379. Friend Function GetAllAnimControls(Element As FrameworkElement, Optional IgnoreInvisibility As Boolean = False) As List(Of FrameworkElement)
  380. Dim AllControls As New List(Of FrameworkElement)
  381. GetAllAnimControls(Element, AllControls, IgnoreInvisibility)
  382. Return AllControls
  383. End Function
  384. Private Sub GetAllAnimControls(Element As FrameworkElement, ByRef AllControls As List(Of FrameworkElement), IgnoreInvisibility As Boolean)
  385. If Not IgnoreInvisibility AndAlso Element.Visibility = Visibility.Collapsed Then Exit Sub
  386. If TypeOf Element Is MyCard OrElse TypeOf Element Is MyHint OrElse TypeOf Element Is TextBlock OrElse TypeOf Element Is MyTextButton Then
  387. AllControls.Add(Element)
  388. ElseIf TypeOf Element Is ContentControl Then
  389. Dim Content = CType(Element, ContentControl).Content
  390. If Content IsNot Nothing AndAlso TypeOf Content Is FrameworkElement Then GetAllAnimControls(Content, AllControls, IgnoreInvisibility)
  391. ElseIf TypeOf Element Is Panel Then
  392. For Each Element2 In CType(Element, Panel).Children
  393. If TypeOf Element2 Is FrameworkElement Then GetAllAnimControls(Element2, AllControls, IgnoreInvisibility)
  394. Next
  395. End If
  396. End Sub
  397. '查找列表中的第一个滚动条
  398. Private Function GetFirstScrollViewer(Elements As IEnumerable(Of FrameworkElement)) As MyScrollBar
  399. Dim Viewer As MyScrollViewer = Nothing
  400. For Each Element In Elements
  401. If TypeOf Element Is MyScrollViewer Then
  402. Viewer = Element
  403. GoTo FindViewer
  404. End If
  405. For Each Control In LogicalTreeHelper.GetChildren(Element)
  406. If TypeOf Control Is MyScrollViewer Then
  407. Viewer = Control
  408. GoTo FindViewer
  409. End If
  410. Next
  411. Next
  412. Return Nothing
  413. FindViewer:
  414. If Viewer.ComputedVerticalScrollBarVisibility <> Visibility.Visible Then Return Nothing
  415. Return Viewer.ScrollBar
  416. End Function
  417. #End Region
  418. End Class