《Qt5 Cadaques》学习笔记(五):流体元素

到目前为止,我们主要研究的是简单的图形元素以及如何排列和操作它们。 本章是关于如何控制这些变化,让一个属性的值不只是瞬间改变,更重要的是值如何随时间变化:一个动画。 该技术是现代流畅用户界面的关键基础之一,并且可以通过系统进行扩展,以使用状态和转换来描述您的用户界面。 每个状态都定义了一组属性更改,并且可以与状态更改的动画相结合,称为转换。

5.1 动画

动画应用于属性变化。 动画定义了属性值更改时的插值曲线,以创建从一个值到另一个值的平滑过渡。 动画由一系列要设置动画的目标属性、插值曲线的缓动曲线以及在大多数情况下定义属性更改时间的持续时间来定义。 Qt Quick 中的所有动画都由同一个计时器控制,因此是同步的。 这提高了动画的性能和视觉质量。

注意:动画控制属性如何变化,即值插值。 这是一个基本概念。 QML 基于元素、属性和脚本。 每个元素都提供了数十个属性,每个属性都在等待您为您设置动画。 在书中,你会看到这是一个壮观的运动场。 您会看到一些动画并欣赏它们的美丽,当然还有您的创意天才。 请记住:动画控制属性的变化,每个元素都有数十个属性可供您使用。

// animation.qml
import QtQuick 2.5

Image {
    id: root
    source: "assets/background.png"
    property int padding: 40
    property int duration: 4000
    property bool running: false
    Image {
        id: box
        x: root.padding;
        y: (root.height-height)/2
        source: "assets/box_green.png"
        NumberAnimation on x {
            to: root.width - box.width - root.padding
            duration: root.duration
            running: root.running
        }
        RotationAnimation on rotation {
            to: 360
            duration: root.duration
            running: root.running
        }
    }

    MouseArea {
        anchors.fill: parent
        onClicked: root.running = true
    }
}

上面的示例显示了一个应用于 x 和 rotation 属性的简单动画。 每个动画的持续时间为 4000 毫秒 (msec) 并且永远循环。 x 上的动画将 x 坐标从对象逐渐移动到 240px。 旋转动画从当前角度运行到 360 度。 两个动画并行运行,并在加载 UI 后立即启动。

现在您可以通过更改 to 和 duration 属性来玩转动画,或者您可以添加另一个动画,例如在不透明度甚至比例上。 结合这些,看起来物体正在消失在深空。 试试看!

5.1.1 动画元素

有几种类型的动画元素,每种都针对特定的用例进行了优化。 以下是最突出的动画列表:

  • PropertyAnimation - 动画属性值的变化
  • NumberAnimation - 动画 qreal 类型值的变化
  • ColorAnimation - 动画颜色值的变化
  • RotationAnimation - 动画旋转值的变化

除了这些基本且广泛使用的动画元素之外,Qt Quick 还为特定用例提供了更专业的动画:

  • PauseAnimation - 为动画提供暂停
  • SequentialAnimation - 允许动画按顺序运行
  • ParallelAnimation - 允许动画并行运行
  • AnchorAnimation - 动画锚值的变化
  • ParentAnimation - 动画父值的变化
  • SmoothedAnimation - 允许属性平滑地跟踪一个值
  • SpringAnimation - 允许属性以类似弹簧的动作跟踪值
  • PathAnimation - 沿路径为项目设置动画
  • Vector3dAnimation - 动画 QVector3d 值的变化

稍后我们将学习如何创建一系列动画。 在处理更复杂的动画时,需要在正在进行的动画期间更改属性或运行脚本。 为此,Qt Quick 提供了动作元素,可以在可以使用其他动画元素的任何地方使用:

  • PropertyAction - 指定动画期间的即时属性更改
  • ScriptAction - 定义动画期间要运行的脚本

本章将使用小的重点示例来讨论主要的动画类型。

5.1.2 应用动画

动画可以通过多种方式应用:

  • 属性动画 - 元素完全加载后自动运行
  • 属性行为 - 属性值更改时自动运行
  • 独立动画 - 在使用 start() 显式启动动画或将 running 设置为 true 时运行(例如,通过属性绑定)

 稍后我们还会看到如何在状态转换中使用动画。

扩展 ClickableImage 版本 2
为了演示动画的用法,我们重用了前面章节中的 ClickableImage 组件,并使用文本元素对其进行了扩展。
为了组织图像下方的元素,我们使用了 Column 定位器,并根据列的 childrenRect 属性计算了宽度和高度。 我们公开了两个属性:文本和图像源以及点击信号。 我们还希望文本与图像一样宽并且应该换行。 我们通过使用 Text 元素的 wrapMode 属性来实现后者。

// ClickableImageV2.qml
// Simple image which can be clicked
import QtQuick 2.5

Item {
    id: root
    width: container.childrenRect.width
    height: container.childrenRect.height
    property alias text: label.text
    property alias source: image.source
    signal clicked
    Column {
        id: container
        Image {
            id: image
        }
        Text {
            id: label
            width: image.width
            horizontalAlignment: Text.AlignHCenter
            wrapMode: Text.WordWrap
            color: "#ececec"
        }
    }

    MouseArea {
        anchors.fill: parent
        onClicked: root.clicked()
    }
}

注意:由于几何依赖的反转(父几何依赖于子几何),我们不能在 ClickableImageV2 上设置宽度/高度,因为这会破坏我们的宽度/高度绑定。 这是对我们内部设计的限制,作为组件设计师,您应该意识到这一点。 通常你应该更喜欢孩子的几何图形依赖于父母的几何图形。

物体上升

这三个对象都在相同的 y 位置(y=200)。 他们需要全部移动到 y=40。 他们每个人都使用具有不同副作用和功能的不同方法。 

ClickableImageV2 {
    id: greenBox
    x: 40
    y: root.height-height
    source: "assets/box_green.png"
    text: "animation on property"
    NumberAnimation on y {
        to: 40; duration: 4000
    }
}

第一个对象

第一个对象使用 <property> 策略上的动画移动。 动画立即开始。单击对象时,它们的 y 位置将重置为开始位置,这适用于所有对象。 在第一个对象上,只要动画正在运行,重置就不会产生任何影响。 这甚至令人不安,因为在动画开始之前将 y 位置设置为几分之一秒的新值。 应该避免这种相互竞争的财产变化。

ClickableImageV2 {
    id: blueBox
    x: (root.width-width)/2
    y: root.height-height
    source: "assets/box_blue.png"
    text: "behavior on property"
    Behavior on y {
        NumberAnimation { duration: 4000 }
    }

    onClicked: y = 40
    // random y on each click
    // onClicked: y = 40+Math.random()*(205-40)
}

第二个对象

第二个对象使用动画行为移动。 这个行为告诉属性,每次属性值改变时,都会通过这个动画来改变。 可以通过在 Behavior 元素上启用 : false 来禁用该行为。 当您单击对象时,该对象将开始移动(然后将 y 位置设置为 40)。 由于位置已设置,因此再次单击没有影响。 您可以尝试使用随机值(例如 40+(Math.random()*(205-40)) 作为 y 位置。您将看到对象将始终动画到新位置并调整其速度以匹配 到动画持续时间定义的目的地的 4 秒。

ClickableImageV2 {
    id: redBox
    x: root.width-width-40
    y: root.height-height
    source: "assets/box_red.png"

    onClicked: anim.start()
    // onClicked: anim.restart()
    text: "standalone animation"

    NumberAnimation {
        id: anim
        target: redBox
        properties: "y"
        to: 40
        duration: 4000
    }
}

第三个对象

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

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

注意:启动/停止动画的另一种方法是将属性绑定到动画的运行属性。这在用户输入控制属性时特别有用:

NumberAnimation {
    ...
    // animation runs when mouse is pressed
    running: area.pressed
}

MouseArea {
    id: area
}

5.1.3 缓动曲线

属性的值变化可以通过动画来控制。 缓动属性允许影响属性更改的插值曲线。 我们现在定义的所有动画都使用线性插值,因为动画的初始缓动类型是 Easing.Linear。 最好用一个小图来可视化,其中 y 轴是要设置动画的属性,x 轴是时间(持续时间)。 线性插值会从动画开始时的 from 值到动画结束时的 to 值绘制一条直线。 所以缓动类型定义了变化曲线。 缓动类型经过精心选择,以支持移动对象的自然贴合,例如当页面滑出时。 最初,页面应该慢慢滑出,然后加快速度,最后以高速滑出,类似于翻书的页面。

注意:动画不应过度使用。 作为 UI 设计的其他方面,动画也应该仔细设计并支持 UI 流而不是支配它。 眼睛对移动的物体非常敏感,动画很容易分散用户的注意力。

在下一个示例中,我们将尝试一些缓动曲线。 每条缓动曲线都由一个可点击的图像显示,当点击时,将在方形动画上设置一个新的缓动类型,然后触发 restart() 以使用新曲线运行动画。

 这个例子的代码稍微复杂了一点。 我们首先创建一个 EasingTypes 网格和一个由缓动类型控制的 Box。 缓动类型只显示盒子将用于其动画的曲线。 当用户点击缓动曲线时,框会按照缓动曲线的方向移动。 动画本身是一个独立动画,目标设置为框,并配置为 x 属性动画,持续时间为 2 秒。

注意:EasingType 的内部是实时渲染曲线的,有兴趣的读者可以在 EasingCurves 示例中查找。

// EasingCurves.qml
import QtQuick 2.5
import QtQuick.Layouts 1.2

Rectangle {
    id: root
    width: childrenRect.width
    height: childrenRect.height
    color: '#4a4a4a'
    gradient: Gradient {
        GradientStop { position: 0.0; color: root.color }
        GradientStop { position: 1.0; color: Qt.lighter(root.color, 1.2) }
    }

    ColumnLayout {
        Grid {
            spacing: 8
            columns: 5
            EasingType {
                easingType: Easing.Linear
                title: 'Linear'
                onClicked: {
                    animation.easing.type = easingType
                    box.toggle = !box.toggle
                }
            }
            EasingType {
                easingType: Easing.InExpo
                title: "InExpo"
                onClicked: {
                    animation.easing.type = easingType
                    box.toggle = !box.toggle
                }
            }
            EasingType {
                easingType: Easing.OutExpo
                title: "OutExpo"
                onClicked: {
                    animation.easing.type = easingType
                    box.toggle = !box.toggle
                }
            }
            EasingType {
                easingType: Easing.InOutExpo
                title: "InOutExpo"
                onClicked: {
                    animation.easing.type = easingType
                    box.toggle = !box.toggle
                }
            }
            EasingType {
                easingType: Easing.InOutCubic
                title: "InOutCubic"
                onClicked: {
                    animation.easing.type = easingType
                    box.toggle = !box.toggle
                }
            }
            EasingType {
                easingType: Easing.SineCurve
                title: "SineCurve"
                onClicked: {
                    animation.easing.type = easingType
                    box.toggle = !box.toggle
                }
            }
            EasingType {
                easingType: Easing.InOutCirc
                title: "InOutCirc"
                onClicked: {
                    animation.easing.type = easingType
                    box.toggle = !box.toggle
                }
            }
            EasingType {
                easingType: Easing.InOutElastic
                title: "InOutElastic"
                onClicked: {
                    animation.easing.type = easingType
                    box.toggle = !box.toggle
                }
            }
            EasingType {
                easingType: Easing.InOutBack
                title: "InOutBack"
                onClicked: {
                    animation.easing.type = easingType
                    box.toggle = !box.toggle
                }
            }
            EasingType {
                easingType: Easing.InOutBounce
                title: "InOutBounce"
                onClicked: {
                    animation.easing.type = easingType
                    box.toggle = !box.toggle
                }
            }
        }
        Item {
            height: 80
            Layout.fillWidth: true
            Box {
                id: box
                property bool toggle
                x: toggle?20:root.width-width-20
                anchors.verticalCenter: parent.verticalCenter
                gradient: Gradient {
                    GradientStop { position: 0.0; color: "#2ed5fa" }
                    GradientStop { position: 1.0; color: "#2467ec" }
                }
                Behavior on x {
                    NumberAnimation {
                        id: animation
                        duration: 500
                    }
                }
            }
        }
    }
}

请开始玩,请观察动画过程中的速度变化。 有些动画让对象感觉更自然,而有些则令人恼火。
除了 duration 和 easing.type 之外,您还可以微调动画。 例如,大多数动画继承自的通用 PropertyAnimation 还支持 easing.amplitude、easing.overshoot 和 easing.period 属性,允许您微调特定缓动曲线的行为。 并非所有缓动曲线都支持这些参数。 请查阅 PropertyAnimation 文档中的缓动表以检查缓动参数是否对缓动曲线有影响。

注意:为用户界面上下文中的元素选择正确的动画对于结果至关重要。
记住动画应该支持 UI 流; 不会激怒用户。

5.1.4 分组动画

通常动画会比仅仅动画一个属性更复杂。 您可能希望同时或一个接一个地运行多个动画,甚至在两个动画之间执行一个脚本。 为此,分组动画为您提供了一种可能性。 顾名思义,可以对动画进行分组。 分组可以通过两种方式完成:并行或顺序。 您可以使用 SequentialAnimation 或 ParallelAnimation 元素,它们充当其他动画元素的动画容器。 这些分组动画本身就是动画,可以完全按原样使用。

 并行动画的所有直接子动画将在启动时并行运行。 这允许您同时为不同的属性设置动画。

// parallelanimation.qml
import QtQuick 2.5

BrightSquare {
    id: root
    width: 600
    height: 400
    property int duration: 3000
    property Item ufo: ufo
    Image {
        anchors.fill: parent
        source: "assets/ufo_background.png"
    }
    ClickableImageV3 {
        id: ufo
        x: 20; y: root.height-height
        text: 'ufo'
        source: "assets/ufo.png"
        onClicked: anim.restart()
    }
    ParallelAnimation {
        id: anim
        NumberAnimation {
            target: ufo
            properties: "y"
            to: 20
            duration: root.duration
        }
        NumberAnimation {
            target: ufo
            properties: "x"
            to: 160
            duration: root.duration
        }
    }
}

顺序动画将首先运行第一个子动画,然后从那里继续。 

// SequentialAnimationExample.qml
import QtQuick 2.5

BrightSquare {
    id: root
    width: 600
    height: 400
    property int duration: 3000
    property Item ufo: ufo
    Image {
        anchors.fill: parent
        source: "assets/ufo_background.png"
    }
    ClickableImageV3 {
        id: ufo
        x: 20; y: root.height-height
        text: 'rocket'
        source: "assets/ufo.png"
        onClicked: anim.restart()
    }
    SequentialAnimation {
        id: anim
        NumberAnimation {
            target: ufo
            properties: "y"
            to: 20
            // 60% of time to travel up
            duration: root.duration*0.6
        }
        NumberAnimation {
            target: ufo
            properties: "x"
            to: 400
            // 40% of time to travel sideways
            duration: root.duration*0.4
        }
    }
}

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

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

  • 从左到右的 x 平移 (X1)
  • 从下到上 (Y1) 的 y 平移,然后是从上到下 (Y2) 的平移,带有一些弹跳
  • 在整个动画持续时间内进行 360 度旋转 (ROT1)

动画的整个持续时间应该需要三秒钟。
我们从一个空项作为根元素开始,宽度为 480,高度为 300。

import QtQuick 2.5

Item {
    id: root
    width: 480
    height: 300
    property int duration: 3000
    ...
}

我们已将总动画持续时间定义为参考,以更好地同步动画部分。
下一步是添加背景,在我们的例子中是 2 个带有绿色和蓝色渐变的矩形。

Rectangle {
    id: sky
    width: parent.width
    height: 200
    gradient: Gradient {
        GradientStop { position: 0.0; color: "#0080FF" }
        GradientStop { position: 1.0; color: "#66CCFF" }
    }
}

Rectangle {
    id: ground
    anchors.top: sky.bottom
    anchors.bottom: root.bottom
    width: parent.width
    gradient: Gradient {
        GradientStop { position: 0.0; color: "#00FF00" }
        GradientStop { position: 1.0; color: "#00803F" }
    }
}

上面的蓝色矩形占用 200 像素的高度,下面的蓝色矩形锚定在天空的顶部和根元素的底部。
让我们把足球带到果岭上。 球是一个图像,存储在“assets/soccer_ball.png”下。 一开始,我们想把它放在左下角,靠近边缘。

Image {
    id: ball
    x: 0
    y: root.height-height
    source: "assets/soccer_ball.png"
    MouseArea {
        anchors.fill: parent
        onClicked: {
            ball.x = 0;
            ball.y = root.height-ball.height;
            ball.rotation = 0;
            anim.restart()
        }
    }
}

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

SequentialAnimation {
    id: anim
    NumberAnimation {
        target: ball
        properties: "y"
        to: 20
        duration: root.duration * 0.4
    }
    NumberAnimation {
        target: ball
        properties: "y"
        to: 240
        duration: root.duration * 0.6
    }
}

 

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

ParallelAnimation {
    id: anim
    SequentialAnimation {
        // ... our Y1, Y2 animation
    }
    NumberAnimation { // X1 animation
        target: ball
        properties: "x"
        to: 400
        duration: root.duration
    }
}

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

ParallelAnimation {
    id: anim
    SequentialAnimation {
        // ... our Y1, Y2 animation
    }
    NumberAnimation { // X1 animation
        // X1 animation
    }
    RotationAnimation {
        target: ball
        properties: "rotation"
        to: 720
        duration: root.duration
    }
}

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

ParallelAnimation {
    id: anim
    SequentialAnimation {
        NumberAnimation {
            target: ball
            properties: "y"
            to: 20
            duration: root.duration * 0.4
            easing.type: Easing.OutCirc
        }
        NumberAnimation {
            target: ball
            properties: "y"
            to: root.height-ball.height
            duration: root.duration * 0.6
            easing.type: Easing.OutBounce
        }
    }
    NumberAnimation {
        target: ball
        properties: "x"
        to: root.width-ball.width
        duration: root.duration
    }
    RotationAnimation {
        target: ball
        properties: "rotation"
        to: 720
        duration: root.duration
    }
}

5.2 状态和转换

用户界面的某些部分通常可以用状态来描述。 状态定义了一组属性更改,并且可以由特定条件触发。 另外,这些状态开关可以附加一个转换,该转换定义了应该如何动画化这些更改或应用任何其他操作。 进入状态时也可以应用动作。

5.2.1 状态

Item {
    id: root
    states: [
        State {
            name: "go"
            PropertyChanges { ... }
        },
        State {
            name: "stop"
            PropertyChanges { ... }
        }
    ]
}

通过将新的状态名称分配给已定义状态的元素的 state 属性来更改状态。

注意:切换状态的另一种方法是使用 State 元素的 when 属性。 当应用状态时,when 属性可以设置为计算结果为 true 的表达式。

Item {
    id: root
    states: [
        ...
    ]
    Button {
        id: goButton
        ...
        onClicked: root.state = "go"
    }
}

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

当系统打开时,它会自动进入停止模式作为默认状态。 停止状态将 light1 变为红色,将 light2 变为黑色(关闭)。 外部事件现在可以触发状态切换到“开始”状态。 在 go 状态下,我们将颜色属性从 light1 更改为 black(关闭),将 light2 更改为 green,以指示路人现在可以行走。
为了实现这个场景,我们开始为 2 盏灯绘制我们的用户界面。 为简单起见,我们使用 2 个矩形,半径设置为宽度的一半(宽度与高度相同,即为正方形)。

Rectangle {
    id: light1
    x: 25
    y: 15
    width: 100
    height: width
    radius: width/2
    color: root.black
    border.color: Qt.lighter(color, 1.1)
}
Rectangle {
    id: light2
    x: 25
    y: 135
    width: 100
    height: width
    radius: width/2
    color: root.black
    border.color: Qt.lighter(color, 1.1)
}

正如状态图中所定义的,我们希望有两种状态,一种是“go”状态,另一种是“stop”状态,其中每个状态都将交通灯分别更改为红色或绿色。 我们将 state 属性设置为 stop 以确保我们的红绿灯的初始状态是停止状态。

注意:通过将 light1 的颜色设置为红色,将 light2 的颜色设置为黑色,我们可以只使用“go”状态而没有明确的“stop”状态来实现相同的效果。 由初始属性值定义的初始状态“”将作为“停止”状态。

state: "stop"

states: [
    State {
        name: "stop"
        PropertyChanges { target: light1; color: root.red }
        PropertyChanges { target: light2; color: root.black }
    },
    State {
        name: "go"
        PropertyChanges { target: light1; color: root.black }
        PropertyChanges { target: light2; color: root.green }
    }
]

使用 PropertyChanges { 目标:light2; color: "black" } 在这个例子中并不是真正需要的,因为 light2 的初始颜色已经是黑色。 在一个状态中,只需要描述属性应该如何从它们的默认状态(而不是从之前的状态)改变。
使用覆盖整个交通灯的鼠标区域触发状态更改,并在单击时在 goand stop 状态之间切换。

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

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

注意:可以使用脚本而不是 QML 状态来创建类似的逻辑。 开发人员很容易陷入编写更多 JavaScript 程序而不是 QML 程序的陷阱。

5.2.2 过渡

可以为每个项目添加一系列转换。通过状态更改执行转换。您可以使用 from: 和 to: 属性定义可以在哪些状态更改上应用特定转换。这两个属性就像一个过滤器,当过滤器为真时,将应用过渡。您还可以使用通配符“*”,表示“任何状态”。例如来自:“*”; to:"*" 表示从任何状态到任何其他状态,是 from 和 to 的默认值,表示转换应用于每个状态切换。

对于这个例子,我们希望在将状态从“go”切换到“stop”时动画颜色变化。对于其他相反的状态变化(“停止”到“开始”),我们希望保持立即的颜色变化并且不应用过渡。我们使用 from 和 to 属性限制转换,只过滤从“go”到“stop”的状态变化。在过渡中,我们为每个灯光添加了两个颜色动画,这将动画状态描述中定义的属性变化。

transitions: [
    Transition {
        from: "stop"; to: "go"
        // from: "*"; to: "*"
        ColorAnimation { target: light1; properties: "color"; duration: 2000 }
        ColorAnimation { target: light2; properties: "color"; duration: 2000 }
    }
]

您可以通过单击 UI 来更改状态。 状态会立即应用,并且还会在转换运行时更改状态。 因此,请尝试在状态从“停止”到“开始”的过渡时单击 UI。 你会看到改变会立即发生。

例如,您可以通过缩小非活动光以突出显示活动光来使用此 UI。为此,您需要添加另一个属性更改以缩放到状态,并处理缩放属性中的动画 过渡。 另一种选择是添加一个“注意”状态,其中灯闪烁黄色。 为此,您需要在过渡中添加一个连续动画,一秒钟变为黄色(动画的“to”属性,一秒钟变为“黑色”)。 也许您还想更改缓动曲线以使其更具视觉吸引力。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值