当前位置:   article > 正文

Qt6 QML 第六章第一节:动画_qml behavior on

qml behavior on

动画

动画应用于属性变化。当一个属性值改变时,动画定义了从一个值到另一个值的插值曲线。这些动画曲线创建了从一个值到另一个值的平滑过渡。

动画由一系列要动画的目标属性、插值曲线的缓和曲线和持续时间定义。Qt Quick中的所有动画都由相同的计时器控制,因此是同步的。这提高了动画的性能和视觉质量。

动画使用值插值控制属性的变化。

这是一个基本概念。QML基于元素、属性和脚本。每个元素都提供了几十个属性,每个属性都等待着你来制作动画。在书中,你会看到这是一个壮观的竞技场。

你会发现自己看着一些动画,只是欣赏他们的美丽,以及你的创作天才。请记住:动画控制属性变化,每个元素都有几十个属性供你使用。

释放这种力量吧!

在这里插入图片描述

// AnimationExample.qml

import QtQuick

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
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

上面的例子展示了一个应用于x和旋转属性的简单动画。每个动画的持续时间为4000毫秒(毫秒)。x上的动画将物体的x坐标逐渐移动到240px。旋转时的动画从当前角度运行到360度。两个动画并行运行,并在单击MouseArea时启动。

您可以通过更改toduration属性来处理动画,或者您可以添加另一个动画(例如,opacity甚至scale)。综合这些,看起来好像这个物体正在消失在太空深处。试试吧!

动画元素

有几种类型的动画元素,每种都针对特定的用例进行了优化。以下是一些最重要的动画:
PropertyAnimation -根据属性值的变化产生动画

NumberAnimation - Animates changes in qreal-type values

ColorAnimation - Animates changes in color values

RotationAnimation - Animates changes in rotation values

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

PauseAnimation - Provides a pause for an animation

SequentialAnimation - Allows animations to be run sequentially

ParallelAnimation - Allows animations to be run in parallel

AnchorAnimation - Animates changes in anchor values

ParentAnimation - Animates changes in parent values

SmoothedAnimation - Allows a property to smoothly track a value

SpringAnimation - Allows a property to track a value in a spring-like motion

PathAnimation - Animates an item alongside a path

Vector3dAnimation - Animates changes in QVector3d values

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

PropertyAction - 指定在动画期间立即更改属性

ScriptAction - 定义在动画期间运行的脚本

主要的动画类型将在本章用小的,集中的例子来讨论。

应用动画

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

Animation on property - 在元素完全加载后自动运行

Behavior on property - r当属性值更改时自动运行

Standalone Animation - 当使用start()显式启动动画或将running设置为true(例如通过属性绑定)时运行。

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

#Clickable Image V2

为了演示动画的用法,我们重用了前一章中的ClickableImage组件,并将其扩展为一个text元素。

// ClickableImageV2.qml
// Simple image which can be clicked

import QtQuick

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()
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

为了组织图像下面的元素,我们使用了Column定位器,并根据列的childrenRect属性计算了宽度和高度。我们公开了文本和图像源属性,以及点击信号。我们还希望文本和图像一样宽,并且可以换行。我们通过使用Text元素的wrapMode属性来实现后者。

父/子几何依赖

由于几何依赖的反转(父几何依赖于子几何),我们不能在ClickableImageV2上设置宽度/高度,因为这会破坏我们的宽度/高度绑定。

您应该希望子几何依赖于父几何,项目更像是其他项目的容器,应该适应父的几何形状。

上升的对象

在这里插入图片描述

这三个物体都在相同的y位置(y=200)。它们都需要到达y=40,每个都使用不同的方法,具有不同的副作用和特征。

第一个对象

第一个对象使用Animation on 策略移动。动画立即开始

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

当一个对象被单击时,它的y轴位置被重置为起始位置,这适用于所有对象。在第一个对象上,只要动画在运行,重置就没有任何效果。

这可能在视觉上令人不安,因为在动画开始前的几分之一秒,y位置被设置为一个新值。应该避免这种竞争性的属性变动。

第二个对象

第二个对象使用Behavior on 实现动画。这个行为告诉属性它应该动画化每一个值的变化。可以通过在Behavior元素上设置 enabled: false来禁用该行为。

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

    onClicked: y = 40
    // random y on each click
    // onClicked: y = 40 + Math.random() * (205-40)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

当你点击这个物体时,它将开始移动(它的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: qsTr("standalone animation")

    NumberAnimation {
        id: anim
        target: redBox
        properties: "y"
        to: 40
        duration: 4000
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

点击将使用动画的start()函数开始动画。每个动画都有start()、stop()、resume()restart()函数。动画本身比之前的其他动画类型包含更多的信息。

我们需要定义目标,即要动画化的元素,以及要动画化的属性的名称。我们还需要定义一个to 值,在本例中,还需要定义一个from 值,它允许重新启动动画。
在这里插入图片描述

点击背景将重置所有对象到初始位置。第一个对象不能重新启动,除非重新启动触发元素重新加载的程序。

控制动画的其他方法。

启动/停止动画的另一种方法是将一个属性绑定到动画的运行属性。当用户输入控制属性时,这尤其有用:

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

缓和曲线

属性值的改变可以通过动画来控制。缓和属性允许影响属性更改的插值曲线。

我们现在定义的所有动画都使用线性插值,因为动画的初始缓动类型是Easing.Linear。最好使用一个小图来可视化,其中y轴是要动画的属性,x轴是时间(持续时间)。线性插值将绘制一条直线,从动画开始时的from值到动画结束时的to值。缓变类型定义了变化曲线。

应该仔细选择缓和类型,以支持对移动对象的自然匹配。例如,当一页纸滑出时,这页纸最初应该缓慢地滑出,然后获得动量,最终以高速滑出,类似于翻页一本书。

动画不应该被过度使用。

与UI设计的其他方面一样,动画应该精心设计以支持UI流,而不是主导它。眼睛对移动的物体非常敏感,动画很容易分散用户的注意力。

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

在这里插入图片描述

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

TIP
EasingType的内部实时渲染曲线,感兴趣的读者可以在EasingCurves示例中查找。

// EasingCurves.qml

import QtQuick
import QtQuick.Layouts

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
                    }
                }
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123

请使用示例并观察动画期间速度的变化。有些动画对物体来说感觉更自然,有些则令人恼火。

除了durationeasing.type,您可以微调动画。例如,一般的PropertyAnimation类型(大多数动画都是从它继承的)还支持 easing.amplitude, easing.overshooteasing.period 属性,允许您微调特定缓和曲线的行为。

并非所有的缓动曲线都支持这些参数。请参考PropertyAnimation文档中的缓动表(打开新窗口)来检查缓动参数是否对缓动曲线有影响。

选择合适的动画

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

分组动画

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

在这里插入图片描述

并行动画

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

// ParallelAnimationExample.qml
import QtQuick

BrightSquare {
    id: root

    property int duration: 3000
    property Item ufo: ufo

    width: 600
    height: 400

    Image {
        anchors.fill: parent
        source: "assets/ufo_background.png"
    }

    ClickableImageV3 {
        id: ufo
        x: 20; y: root.height-height
        text: qsTr('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
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

在这里插入图片描述

串行动画

串行动画按照声明的顺序运行每个子动画:从上到下。

// SequentialAnimationExample.qml
import QtQuick

BrightSquare {
    id: root

    property int duration: 3000
    property Item ufo: ufo

    width: 600
    height: 400

    Image {
        anchors.fill: parent
        source: "assets/ufo_background.png"
    }

    ClickableImageV3 {
        id: ufo
        x: 20; y: root.height-height
        text: qsTr('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
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

在这里插入图片描述

嵌套动画

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

在这里插入图片描述

为了理解动画,我们需要将其分解为对象的积分变换。我们需要记住,动画是对属性变化进行动画处理的。以下是不同的转换:

从左到右的x平移(X1)

一个从下到上的y平移(Y1)接着一个从上到下的平移(Y2)并有一些弹跳。

在整个动画过程中360度旋转(ROT1)

整个动画的持续时间应该是3秒。

在这里插入图片描述

我们从一个空条目开始,作为根元素,宽度为480,高度为300。

import QtQuick

Item {
    id: root

    property int duration: 3000

    width: 480
    height: 300

    // [...]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

我们已经定义了总动画持续时间作为参考,以便更好地同步动画部分。

下一步是添加背景,在我们的例子中是两个矩形,绿色和蓝色渐变。

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" }
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在这里插入图片描述
上面的蓝色矩形的高度为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()
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在这里插入图片描述

该图像附带了一个鼠标区域。如果球被点击,球的位置将被重置,动画将重新开始。

让我们先从两个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
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在这里插入图片描述

这指定了总动画持续时间的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
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在这里插入图片描述

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

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

这就是整个动画序列。剩下的一件事就是为球的运动提供正确的缓和曲线。对于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
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

Previous
Contents

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/喵喵爱编程/article/detail/843927
推荐阅读
相关标签
  

闽ICP备14008679号