MyPageRight.vb 22 KB

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