首页 > 技术文章 > 第六章:Fluid元素

sammy621 2022-02-28 15:07 原文

第六章:Fluid元素

这个Fluid元素,实在没有想好对应的中文,暂且直接用英文。目前为止,我们已经看过大多数的可视图形元素了,也了解了它们如何排列及操作。
本章将会通过动画应用使这些元素的变化更有趣。
要实现应用的华丽用户界面,动画至关重要,它通过状态、过渡动画来应用到用户交互上。每个状态代表了一系列的属性变化,状态变化可以与动画关联起来。这些变化被称为从一种状态到另一种状态过渡
动画除了应用在过渡,还能以单独的元素来被某些脚本调用。
动画实现的原理及涉及的概念说起来较难理解,需要你有一点点数学知识(非必须),但看完例子再回看这些定义,就会豁然开朗。

动画

动画是应用于属性变更。动画定义了从一个值到另一个值之间的一系列插值。利用这些插值,动画实现了初始值与目标值的平滑变换。
一个动画是由一系列目标属性、插值曲线的缓和曲线和持续时间来定义的。Qt Quick中的所有动画都由同一个定时器控制,因此是同步的。这提高了动画的性能和视觉质量。

动画使用 数值插值 来控制属性如何变化
这是一个基础概念。QML基于元素、属性、脚本。每个元素都有一系列属性,每个属性都可以成为动画。本书中,你会发现这是很有趣的领域。
你会发现自己在看一些动画时,不仅看会到它们的美,还有其天才的创意。请记住:动画控制属性的变化,每个元素都有几十个属性供你使用。
快来解锁这神秘的力量吧

 


 

  1. // animation.qml 
  2.  
  3. import QtQuick 
  4.  
  5. Image { 
  6. id: root 
  7. source: "assets/background.png" 
  8.  
  9. property int padding: 40 
  10. property int duration: 4000 
  11. property bool running: false 
  12.  
  13. Image { 
  14. id: box 
  15. x: root.padding; 
  16. y: (root.height-height)/2 
  17. source: "assets/box_green.png" 
  18.  
  19. NumberAnimation on x { 
  20. to: root.width - box.width - root.padding 
  21. duration: root.duration 
  22. running: root.running 
  23. } 
  24. RotationAnimation on rotation { 
  25. to: 360 
  26. duration: root.duration 
  27. running: root.running 
  28. } 
  29. } 
  30.  
  31. MouseArea { 
  32. anchors.fill: parent 
  33. onClicked: root.running = true 
  34. } 
  35. } 

上例演示了一个应用到属性xrotation的简单动画。每个动画持续约4000毫秒,且不断循环。属性x上的动画在横坐标上从初始位置逐渐移动到240px处,旋转动画从初始角度转至360度。两个动画是在UI加载后开始同步运行的。
可以改变toduration属性来观察动画的改变,也可以添加另外的动画(如,透明度opacity甚至缩放scale)。
将这些动画组合起来,效果会象是跌入深空中,来试下吧

动画元素

有一些针对特定场景优化的动画元素类型,下列是比较常用的几种动画类型:

  • PropertyAnimation - 属性改变的动画
  • NumberAnimation - 数值类型改变的动画
  • ColorAnimation - 改变颜色的动画
  • RotationAnimation - 旋转动画
    除了这些基础和常用的动画元素,Qt Quick也为一些特殊应用场景提供了特别的动画。
  • PauseAnimation - 为动画提供暂停
  • SequentialAnimation - 允许动画按顺序运行
  • ParallelAnimation - 允许动画并行运行
  • AnchorAnimation - 锚点变化动画
  • ParentAnimation - 变动父元素的动画
  • SmoothedAnimation - 允许属性平滑地跟踪值变化
  • SpringAnimation - 允许属性在类似弹簧的运动中跟踪值变化
  • PathAnimation - 有移动路径的动画
  • Vector3dAnimation - 3维动画

后续会学到如何创建一系列动画,同时会涉及更复杂的动画,有时需要在动画执行时改变属性或执行一段脚本。对此,Qt Quick提供了动作元素,任何其它使用动画的地方,它都可以在使用。

  • PropertyAction - 指定动画期间立即改变的属性
  • ScriptAction - 定义动画运行期间运行的脚本
    本章将引入短小精焊的例子来探讨几个主要的动画类型。

应用动画

动画可以以几种方式使用:

  • Animation on property - 在元素全部加载后自动运行
  • Behavior on property - 属性值变化时自动运行
  • Standalone Animation - 当动画元素的start()running被显式设为ture时运行(如通过属性绑定)
    后续会看到动画如何应用在状态变换里。

升级版可点图象

为了演示如何使用动画,我们扩展并重用早先章节的可点击图象组件。

  1. // ClickableImageV2.qml 
  2. // Simple image which can be clicked 
  3.  
  4. import QtQuick 
  5.  
  6. Item { 
  7. id: root 
  8. width: container.childrenRect.width 
  9. height: container.childrenRect.height 
  10. property alias text: label.text 
  11. property alias source: image.source 
  12. signal clicked 
  13.  
  14. Column { 
  15. id: container 
  16. Image { 
  17. id: image 
  18. } 
  19. Text { 
  20. id: label 
  21. width: image.width 
  22. horizontalAlignment: Text.AlignHCenter 
  23. wrapMode: Text.WordWrap 
  24. color: "#ececec" 
  25. } 
  26. } 
  27.  
  28. MouseArea { 
  29. anchors.fill: parent 
  30. onClicked: root.clicked() 
  31. } 
  32. } 

我们使用列定位器并通过计算列属性childrenRect的宽度和高度,来组织图象下的元素。将文本与图象元素的source属性和点击信号暴露出来(给根级元素)。我们希望文本与图象一样宽,必要时可包围图象。需要使用文本元素的wrapMode属性。

父子坐标系依赖
由于坐标系依赖反转(父坐标依赖于子坐标),我们不能在ClickableImageV2元素上设置width/height属性,因为这将破坏width/height绑定。如果元素相对于其它元素象容器一样,那子元素依赖/适配父元素坐标似乎更受欢迎(然而本例不是这样)。

物体上升动画

 



这三个对象有相同的y坐标(y=200)。他们将用不同的方法都移动到 y=40,每种方法都有不同的副作用和效果。

 

  1. ClickableImageV2 { 
  2. id: greenBox 
  3. x: 40; y: root.height-height 
  4. source: "assets/box_green.png" 
  5. text: "animation on property" 
  6. NumberAnimation on y { 
  7. to: 40; duration: 4000 
  8. } 
  9. } 

第一个物体

第一个物体运动轨迹使用Animation on <property>方式。动画立即启动。
当被点击时,其纵坐标位置被重置,三个小广场都是这样。第一个物体,当动画正在运行时,重置不起作用。
这可能会在视觉上造成干扰,因为在动画开始前的几分之一秒内,y-position被设置为一个新的值,所以这种属性变化方式应该避免。

  1. ClickableImageV2 { 
  2. id: blueBox 
  3. x: (root.width-width)/2; y: root.height-height 
  4. source: "assets/box_blue.png" 
  5. text: "behavior on property" 
  6. Behavior on y { 
  7. NumberAnimation { duration: 4000 } 
  8. } 
  9.  
  10. onClicked: y = 40 
  11. // random y on each click 
  12. // onClicked: y = 40 + Math.random() * (205-40) 
  13. } 

第二个物体

第二个物体按照Behavior on方式运动。这种方式告诉属性的每个值变化时的行为动作。可以通过在 Behavior 元素上设置 enabled: false 来禁用行为。
当单击对象时,该对象将开始移动(然后将其 y 位置设置为 40)。再次单击没有影响,因为位置已经设置。
您可以尝试对 y 位置使用随机值(例如 40+(Math.random())*(205-40))。你将看到,不管目标位置在哪里,物体都将在4秒内完成动画。

  1. ClickableImageV2 { 
  2. id: redBox 
  3. x: root.width-width-40; y: root.height-height 
  4. source: "assets/box_red.png" 
  5. onClicked: anim.start() 
  6. // onClicked: anim.restart() 
  7.  
  8. text: "standalone animation" 
  9.  
  10. NumberAnimation { 
  11. id: anim 
  12. target: redBox 
  13. properties: "y" 
  14. to: 40 
  15. duration: 4000 
  16. } 
  17. } 

第三个物体

第三个对象使用独立动画。动画被定义为它自己的元素,几乎可以在文档中的任何位置。
单击将使用动画的 start() 函数启动动画。每个动画都有 start()、stop()、resume()restart() 函数。动画本身包含的信息比之前的其他动画类型多得多。
我们需要定义target,它是元素要到达的目标位置,以及我们想要命名的动画的属性名称。我们还需要定义一个to值,在这种情况下,定义一个from值,它允许重新启动动画。

单击背景会将所有对象重置为其初始位置。除非重新启动程序才会触发所有元素重新加载,否则无法重新启动第一个对象。

控制动画的其它方式
启动/停止动画的另一种方法是将属性绑定到动画的running属性。当属性可能过用户输入控制时特别有用:

  1. NumberAnimation { 
  2. ... 
  3. // animation runs when mouse is pressed 
  4. running: area.pressed 
  5. } 
  6. MouseArea { 
  7. id: area 
  8. } 

平滑曲线

动画可以控制属性值的变化。平滑属性可以影响属性变化的插值。
目前所接触到的动画使用线性插值。因为动画默认的平滑属性就是Easing.Liner。最好是用一个小的图形来表示,其中y轴是要绑定到动画的属性值,x轴是时间(持续时间)。线性插值将从属性的from值开始到to值画一条直线。平滑类型就是这样定义变化曲线的。
平滑类型应当谨慎选择以为恰当搭配动画物体。比如,当页面滑出时,页面应该先缓慢滑出,然后获得动力,最后以高速滑出,类似于翻页。

动画不应被过度使用
根据UI设计的其它原则,动画应该谨慎设计以给予UI支持,而非主导UI。视觉注意力很容易被动画干扰。

下面的例子里,我们将尝试使用平滑曲线。每个平滑曲线都由一个可点击的图像显示,当点击时,将在正方形动画上设置一个新的缓和类型,然后触发restart()来运行带有新曲线的动画。

本例代码略复杂。先创建一个由EasingTypes元素和由平滑类型控制的Box组成的列表。平滑类型仅展示方块动画所体现的值变化曲线。当用户在平滑曲线上点击时,小方块会沿着曲线的方向移动。动画本身是一个独立的动画,目标设置为方框,并配置为x-property动画,持续时间为2秒。

注意
EasingType实时渲染曲线的原理,感兴趣的读者可以在EasingCurves的例子中查找。

  1. import QtQuick 
  2. import QtQuick.Layouts 
  3.  
  4. Rectangle { 
  5. id: root 
  6. width: childrenRect.width 
  7. height: childrenRect.height 
  8.  
  9. color: '#4a4a4a' 
  10. gradient: Gradient { 
  11. GradientStop { position: 0.0; color: root.color } 
  12. GradientStop { position: 1.0; color: Qt.lighter(root.color, 1.2) } 
  13. } 
  14.  
  15. ColumnLayout { 
  16. Grid { 
  17. spacing: 8 
  18. columns: 5 
  19. EasingType { 
  20. easingType: Easing.Linear 
  21. title: 'Linear' 
  22. onClicked: { 
  23. animation.easing.type = easingType 
  24. box.toggle = !box.toggle 
  25. } 
  26. } 
  27. EasingType { 
  28. easingType: Easing.InExpo 
  29. title: "InExpo" 
  30. onClicked: { 
  31. animation.easing.type = easingType 
  32. box.toggle = !box.toggle 
  33. } 
  34. } 
  35. EasingType { 
  36. easingType: Easing.OutExpo 
  37. title: "OutExpo" 
  38. onClicked: { 
  39. animation.easing.type = easingType 
  40. box.toggle = !box.toggle 
  41. } 
  42. } 
  43. EasingType { 
  44. easingType: Easing.InOutExpo 
  45. title: "InOutExpo" 
  46. onClicked: { 
  47. animation.easing.type = easingType 
  48. box.toggle = !box.toggle 
  49. } 
  50. } 
  51. EasingType { 
  52. easingType: Easing.InOutCubic 
  53. title: "InOutCubic" 
  54. onClicked: { 
  55. animation.easing.type = easingType 
  56. box.toggle = !box.toggle 
  57. } 
  58. } 
  59. EasingType { 
  60. easingType: Easing.SineCurve 
  61. title: "SineCurve" 
  62. onClicked: { 
  63. animation.easing.type = easingType 
  64. box.toggle = !box.toggle 
  65. } 
  66. } 
  67. EasingType { 
  68. easingType: Easing.InOutCirc 
  69. title: "InOutCirc" 
  70. onClicked: { 
  71. animation.easing.type = easingType 
  72. box.toggle = !box.toggle 
  73. } 
  74. } 
  75. EasingType { 
  76. easingType: Easing.InOutElastic 
  77. title: "InOutElastic" 
  78. onClicked: { 
  79. animation.easing.type = easingType 
  80. box.toggle = !box.toggle 
  81. } 
  82. } 
  83. EasingType { 
  84. easingType: Easing.InOutBack 
  85. title: "InOutBack" 
  86. onClicked: { 
  87. animation.easing.type = easingType 
  88. box.toggle = !box.toggle 
  89. } 
  90. } 
  91. EasingType { 
  92. easingType: Easing.InOutBounce 
  93. title: "InOutBounce" 
  94. onClicked: { 
  95. animation.easing.type = easingType 
  96. box.toggle = !box.toggle 
  97. } 
  98. } 
  99. } 
  100. Item { 
  101. height: 80 
  102. Layout.fillWidth: true 
  103. Box { 
  104. id: box 
  105. property bool toggle 
  106. x: toggle?20:root.width-width-20 
  107. anchors.verticalCenter: parent.verticalCenter 
  108. gradient: Gradient { 
  109. GradientStop { position: 0.0; color: "#2ed5fa" } 
  110. GradientStop { position: 1.0; color: "#2467ec" } 
  111. } 
  112. Behavior on x { 
  113. NumberAnimation { 
  114. id: animation 
  115. duration: 500 
  116. } 
  117. } 
  118. } 
  119. } 
  120. } 
  121. } 

运行本例,观察在动画过程中速度的变化。有些动画会让物体感觉更自然,有些则会让人感觉不舒服。
除了持续时间duration和平滑类型easing.type,还可以微调动画。例如,通用的PropertyAnimation类型(大多数动画都继承自此类型)还支持easing.amplitude,easing.overshoot,easing.period属性,它允许您微调特定平滑曲线的行为。
并不是所有的平滑曲线都支持这些参数。请参考PropertyAnimation文档中的平滑列表,检查平滑参数是否对平滑曲线有影响。

正确选择动画
在用户界面上下文中为元素选择正确的动画至关重要。记住,动画应该改善UI交互,而非打扰用户。

组合动画

常见的动画不是由一个属性变化引起的。您可能希望同时运行多个动画,或者一个接一个地运行,或者甚至在两个动画之间执行一个脚本。
为此,可以使用分组动画。顾名思义,可以对动画进行分组。分组有两种方式:并行或顺序。您可以使用SequentialAnimationParallelAnimation元素,它们充当其他动画元素的动画容器。这些分组动画本身就是动画,可以完全按照动画的方式使用。

一个并行动画内的所有直接子动画在启动时并行运行。这允许你在同一时间以不同的属性生成动画。

  1. // parallelanimation.qml 
  2. import QtQuick 
  3.  
  4. BrightSquare { 
  5. id: root 
  6. width: 600 
  7. height: 400 
  8. property int duration: 3000 
  9. property Item ufo: ufo 
  10.  
  11. Image { 
  12. anchors.fill: parent 
  13. source: "assets/ufo_background.png" 
  14. } 
  15.  
  16.  
  17. ClickableImageV3 { 
  18. id: ufo 
  19. x: 20; y: root.height-height 
  20. text: 'ufo' 
  21. source: "assets/ufo.png" 
  22. onClicked: anim.restart() 
  23. } 
  24.  
  25. ParallelAnimation { 
  26. id: anim 
  27. NumberAnimation { 
  28. target: ufo 
  29. properties: "y" 
  30. to: 20 
  31. duration: root.duration 
  32. } 
  33. NumberAnimation { 
  34. target: ufo 
  35. properties: "x" 
  36. to: 160 
  37. duration: root.duration 
  38. } 
  39. } 
  40. } 

 


 

有序动画按照声明的顺序(从上至下)逐个运行每个子动画。

  1. // SequentialAnimationExample.qml 
  2. import QtQuick 
  3.  
  4. BrightSquare { 
  5. id: root 
  6. width: 600 
  7. height: 400 
  8. property int duration: 3000 
  9.  
  10. property Item ufo: ufo 
  11.  
  12. Image { 
  13. anchors.fill: parent 
  14. source: "assets/ufo_background.png" 
  15. } 
  16.  
  17. ClickableImageV3 { 
  18. id: ufo 
  19. x: 20; y: root.height-height 
  20. text: 'rocket' 
  21. source: "assets/ufo.png" 
  22. onClicked: anim.restart() 
  23. } 
  24.  
  25. SequentialAnimation { 
  26. id: anim 
  27. NumberAnimation { 
  28. target: ufo 
  29. properties: "y" 
  30. to: 20 
  31. // 60% of time to travel up 
  32. duration: root.duration*0.6 
  33. } 
  34. NumberAnimation { 
  35. target: ufo 
  36. properties: "x" 
  37. to: 400 
  38. // 40% of time to travel sideways 
  39. duration: root.duration*0.4 
  40. } 
  41. } 
  42. } 

 



分组动画也可以嵌套。例如,一个顺序动画可以有两个并行动画作为子动画,依此类推。我们可以用一个足球例子来形象化这一点。这个想法是从左到右扔一个球并为其行为设置动画。

要理解动画,我们需要将其分解为对象的整体变换。我们需要记住,动画元素会为属性更改生成动画。以下是不同的转换:

 

  • 从左到右的 x 平移 (X1)
  • 从底部到顶部 (Y1) 的 y 平移,然后是从上到下 (Y2) 的平移,并带有一些弹跳
  • 在整个动画期间旋转 360 度 (ROT1)
    动画的整个持续时间应该需要三秒钟。

    先建一个根元素,它是宽度480,高度300的空元素。
  1. import QtQuick 
  2.  
  3. Item { 
  4. id: root 
  5. width: 480 
  6. height: 300 
  7. property int duration: 3000 
  8.  
  9. ... 
  10. } 

我们定义了动画总时长作为参考,以更好地同步动画部分。
下一步是添加背景,在我们的例子中,背景是两个带有绿色和蓝色梯度的矩形。

  1. Rectangle { 
  2. id: sky 
  3. width: parent.width 
  4. height: 200 
  5. gradient: Gradient { 
  6. GradientStop { position: 0.0; color: "#0080FF" } 
  7. GradientStop { position: 1.0; color: "#66CCFF" } 
  8. } 
  9. } 
  10. Rectangle { 
  11. id: ground 
  12. anchors.top: sky.bottom 
  13. anchors.bottom: root.bottom 
  14. width: parent.width 
  15. gradient: Gradient { 
  16. GradientStop { position: 0.0; color: "#00FF00" } 
  17. GradientStop { position: 1.0; color: "#00803F" } 
  18. } 
  19. } 

 


 

上面的蓝色矩形占据了200像素的高度,下面的矩形其顶部被锚定在天空的底部而其底部被锚定在根元素的底部。
让我们把足球带到绿色元素上。这个球是一个图像,存储在 "assets/soccer_ball.png "下。开始时,把它放在左下角,靠近边缘的位置。

  1. Image { 
  2. id: ball 
  3. x: 0; y: root.height-height 
  4. source: "assets/soccer_ball.png" 
  5.  
  6. MouseArea { 
  7. anchors.fill: parent 
  8. onClicked: { 
  9. ball.x = 0; 
  10. ball.y = root.height-ball.height; 
  11. ball.rotation = 0; 
  12. anim.restart() 
  13. } 
  14. } 
  15. } 

 


 

该图像附加了一个鼠标区域。如果球被点击,球的位置会重置,动画会重新开始。
让我们先从两个 y 平移的顺序动画开始。

  1. SequentialAnimation { 
  2. id: anim 
  3. NumberAnimation { 
  4. target: ball 
  5. properties: "y" 
  6. to: 20 
  7. duration: root.duration * 0.4 
  8. } 
  9. NumberAnimation { 
  10. target: ball 
  11. properties: "y" 
  12. to: 240 
  13. duration: root.duration * 0.6 
  14. } 
  15. } 

 


 

这里指定总动画持续时间的 40% 是向上动画,60% 是向下动画,每个动画依次运行。变换在线性路径上进行动画处理,但目前没有弯曲。稍后将使用缓动曲线添加曲线,目前我们正专注于使状态变换形成动画。
接下来,我们需要添加 x 平移。 x-translation 应该与 y-translation 并行运行,因此我们需要将 y-translation 序列与 x-translation 一起封装成一个并行动画。

  1. ParallelAnimation { 
  2. id: anim 
  3. SequentialAnimation { 
  4. // ... our Y1, Y2 animation 
  5. } 
  6. NumberAnimation { // X1 animation 
  7. target: ball 
  8. properties: "x" 
  9. to: 400 
  10. duration: root.duration 
  11. } 
  12. } 

 



最后,我们希望球能够旋转。为此,我们需要在并行动画中添加另一个动画。我们选择 RotationAnimation动画,因为它专门用于旋转。

 

  1. ParallelAnimation { 
  2. id: anim 
  3. SequentialAnimation { 
  4. // ... our Y1, Y2 animation 
  5. } 
  6. NumberAnimation { // X1 animation 
  7. // X1 animation 
  8. } 
  9. RotationAnimation { 
  10. target: ball 
  11. properties: "rotation" 
  12. to: 720 
  13. duration: root.duration 
  14. } 
  15. } 

这就是整个动画序列。剩下的一件事是为球的运动提供正确的平滑曲线。对于 Y1 动画,我们使用 Easing.OutCirc 曲线,因为这看起来更像是一个圆周运动。使用 Easing.OutBounce 增强 Y2 以使球弹起,并且弹跳应该发生在最后(尝试使用 Easing.InBounce,会看到弹跳立即开始)。
X1和ROT1的动画保持原样,采用线性曲线。
下面是最终的动画代码以供参考:

  1. ParallelAnimation { 
  2. id: anim 
  3. SequentialAnimation { 
  4. NumberAnimation { 
  5. target: ball 
  6. properties: "y" 
  7. to: 20 
  8. duration: root.duration * 0.4 
  9. easing.type: Easing.OutCirc 
  10. } 
  11. NumberAnimation { 
  12. target: ball 
  13. properties: "y" 
  14. to: root.height-ball.height 
  15. duration: root.duration * 0.6 
  16. easing.type: Easing.OutBounce 
  17. } 
  18. } 
  19. NumberAnimation { 
  20. target: ball 
  21. properties: "x" 
  22. to: root.width-ball.width 
  23. duration: root.duration 
  24. } 
  25. RotationAnimation { 
  26. target: ball 
  27. properties: "rotation" 
  28. to: 720 
  29. duration: root.duration 
  30. } 
  31. } 

提示
本节所涉及内容较抽象,一定要运行一下文中所涉及的例子,有助于理解动画的原理。文中的代码是例子的核心代码,较零碎,你可以自已动手将这些零散代码组成可运行的Qt Quick UI Prototype项目,这也是建议的方式。如果想快速浏览效果,可以从GitHub上下载完整工程源码来运行。

状态与过渡

用户界面的常见部分可以用状态描述。状态定义了一系列可在特定条件下触发的属性变化。
另外,这些状态变化绑定了形成动画的属性转换及附加脚本。当状态满足时,也会触发动作。

状态

在QML中以state来定义状态,并要将状态明细列表绑定到states
状态由状态名来唯一识别,状态元素最简单的形式,由一系列的元素属性变化组成。默认状态由元素的初始属性定义,并命名为" "字符串)。

  1. Item { 
  2. id: root 
  3. states: [ 
  4. State { 
  5. name: "go" 
  6. PropertyChanges { ... } 
  7. }, 
  8. State { 
  9. name: "stop" 
  10. PropertyChanges { ... } 
  11. } 
  12. ] 
  13. } 

假设元素中定义了状态列表,则为元素的state属性赋予一个新的状态名,就改变了元素的状态。

使用when来控制状态
另一种控制状态的方法是使用State元素的when属性。when属性可被赋予表达式,当表达式为真时,状态被应用。

  1. Item { 
  2. id: root 
  3. states: [ 
  4. ... 
  5. ] 
  6.  
  7. Button { 
  8. id: goButton 
  9. ... 
  10. onClicked: root.state = "go" 
  11. } 
  12. } 

 



例如,一个交通灯可能有两个信号灯。上面的红色为停止信号,下面的绿色为放行信号。在本例中,两盏灯不应同时亮起。让我们看一下状态图。

程序运行后,默认进入stop状态。该状态将light1变为红,light2变为黑(熄灭)。
外部事件可以将状态变为go状态。在go状态下,将light1变黑(熄灭),light2点亮为绿色,指示行人可以穿越马路了。
为了实现这个场景,我们开始为 2 盏灯绘制我们的用户界面。为简单起见,我们使用 2 个矩形,半径设置为宽度的一半(宽度与高度相同,即为正方形)。

 

  1. Rectangle { 
  2. id: light1 
  3. x: 25; y: 15 
  4. width: 100; height: width 
  5. radius: width/2 
  6. color: root.black 
  7. border.color: Qt.lighter(color, 1.1) 
  8. } 
  9.  
  10. Rectangle { 
  11. id: light2 
  12. x: 25; y: 135 
  13. width: 100; height: width 
  14. radius: width/2 
  15. color: root.black 
  16. border.color: Qt.lighter(color, 1.1) 
  17. } 

正如状态图中所定义的那样,我们希望有两种状态:一种是"go"状态,另一种是"stop"状态,它们中的每一个都将交通信号灯的相应颜色更改为红色或绿色。我们将 state 属性设置为 stop 以确保我们的红绿灯的初始状态是stop状态。

初始状态
我们可以通过把light1的颜色设置为红色,把light2的颜色设置为黑色来达到同样的效果,只有一个"go"的状态,而没有明确的 "stop "的状态。然后,由初始属性值定义的初始状态 " " 将充当 "stop"状态。

  1. state: "stop" 
  2.  
  3. states: [ 
  4. State { 
  5. name: "stop" 
  6. PropertyChanges { target: light1; color: root.red } 
  7. PropertyChanges { target: light2; color: root.black } 
  8. }, 
  9. State { 
  10. name: "go" 
  11. PropertyChanges { target: light1; color: root.black } 
  12. PropertyChanges { target: light2; color: root.green } 
  13. } 
  14. ] 

本例中,使用PropertyChanges { target: light2; color: "black" }并非必要,因为light2的初始颜色已经是黑色了。在一个状态中,只需要描述属性如何从它们的默认状态改变(而不是从以前的状态)。
状态的改变是通过一个鼠标区域来触发的,这个区域覆盖了整个交通灯,当点击的时候会在gostop状态之间进行切换。

  1. MouseArea { 
  2. anchors.fill: parent 
  3. onClicked: parent.state = (parent.state == "stop"? "go" : "stop") 
  4. } 

 



我们现在能够成功地改变交通灯的状态。为了使 UI 更吸引人、更自然,我们应该添加一些带有动画效果的过渡。状态变化可以触发转换。

 

脚本应用
可以使用脚本而非QML实现类似的逻辑。虽然QML相比JavaScript更适合描述用户界面。情况允许,尽可能写声明性代码而非命令性代码。

过渡

每个元素都可添加一系列过渡。状态改变会引发过渡。当状态改变是,你可以定义一个使用from:to:的特定过渡。这两个属性就象过滤器:当条件为真时应用过渡。你也可以使用通配符 " * " ,意味着 "任何状态" 。
比如:from "*" to "*" 意味着从任一状态到另一任意的状态 ,这也是fromto的默认值。这意味着过渡将应用于何意状态。
在这个例子中,我们想让颜色在从“stop”到“go”切换状态时有动画变化。对于另一个反向的状态改变(“go”到“stop”),我们颜色立即改变,而不应用过渡(这一句,原文中好象说反了)。
我们用fromto属性限制转换,只过滤从“go”到“stop”的状态变化。在过渡中,我们为每盏灯添加了两个颜色动画,这将使状态描述中定义的属性变化产生动画效果。

  1. transitions: [ 
  2. Transition { 
  3. from: "stop"; to: "go" 
  4. // from: "*"; to: "*" 
  5. ColorAnimation { target: light1; properties: "color"; duration: 2000 } 
  6. ColorAnimation { target: light2; properties: "color"; duration: 2000 } 
  7. } 
  8. ] 

您可以通过单击UI更改状态。状态会立即应用,并且在过渡运行时也会改变状态。所以,当状态从" stop "过渡到" go "时,试着点击UI,你会看到变化马上就会发生。

你可以反复操作这个UI,例如,逐渐熄灯和逐渐点亮灯。
为此,你需要为状态的缩放添加另一个属性更改,并在转换中处理缩放属性的动画。
另一种选择是添加一个“注意”状态,即黄色闪烁灯。为此,你需要在过渡中添加一个连续的动画,一秒钟变成黄色(动画的“to”属性,一秒钟变成“黑色”)。
也许你还想改变缓和曲线,让它在视觉上更吸引人。

提示
本节所涉及内容较抽象,一定要运行一下文中所涉及的例子,有助于理解动画的原理,这一点非常关键。文中的代码是例子的核心代码,较零碎,你可以自已动手将这些零散代码组成可运行的Qt Quick UI Prototype项目,这也是建议的方式。如果想快速浏览效果,可以从GitHub上下载完整工程源码来运行。

推荐阅读