QML SpinBox 动画效果:缩放反馈与数字滚动

前两篇讲了 SpinBox 的基础用法和自定义样式,这篇加点手感:按钮按下去有缩放反馈,数值变化时数字弹一下;或者干脆把输入区换成列表,数字像滚轮一样滚过去。改动不大,交互质感提升不少。

  • 缩放动画 --- 加减号按下缩放,数值变化时弹出再回弹,附带阴影
  • 滚动列表 --- 用 ListView 替换默认输入区,数值滚动切换

Demo 1:缩放动画

在 Basic 自定义的基础上加了三层效果:按钮按下缩放、数值变化时文本弹出、整体阴影。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects

FadeInAnimation {
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        // ... 省略标题组件 TitleSeparator ...

        RowLayout {
            Layout.alignment: Qt.AlignHCenter
            Layout.topMargin: 20
            spacing: 10

            SpinBox {
                id: control
                editable: true
                from: 0
                to: 100

                background: Rectangle {
                    implicitWidth: 140
                    implicitHeight: 40
                    radius: height / 2
                    color: control.enabled ? (control.up.pressed || control.down.pressed ? "#eee" : "#fff") : "#f0f0f0"
                    border.color: control.focus ? "#1296ff" : "#ccc"
                    border.width: control.focus ? 2 : 1

                    // 阴影效果
                    layer.enabled: true
                    layer.effect: DropShadow {
                        transparentBorder: true
                        color: "#20000000"
                        radius: 4
                        samples: 8
                    }
                }

                up.indicator: Rectangle {
                    x: parent.width - width
                    implicitWidth: 40
                    implicitHeight: 40
                    radius: height / 2
                    color: control.up.pressed ? "#eee" : "transparent"

                    // 使用 Rectangle 实现加号
                    Rectangle {
                        x: (parent.width - width) / 2
                        y: (parent.height - height) / 2
                        width: parent.width / 3
                        height: 2
                        color: enabled ? "#666" : "#999"
                    }
                    Rectangle {
                        x: (parent.width - width) / 2
                        y: (parent.height - height) / 2
                        width: 2
                        height: parent.width / 3
                        color: enabled ? "#666" : "#999"
                    }

                    // 点击时的缩放动画
                    scale: control.up.pressed ? 0.9 : 1.0
                    Behavior on scale { NumberAnimation { duration: 100 } }
                }

                down.indicator: Rectangle {
                    x: 0
                    implicitWidth: 40
                    implicitHeight: 40
                    radius: height / 2
                    color: control.down.pressed ? "#eee" : "transparent"

                    // 使用 Rectangle 实现减号
                    Rectangle {
                        x: (parent.width - width) / 2
                        y: (parent.height - height) / 2
                        width: parent.width / 3
                        height: 2
                        color: enabled ? "#666" : "#999"
                    }

                    // 点击时的缩放动画
                    scale: control.down.pressed ? 0.9 : 1.0
                    Behavior on scale { NumberAnimation { duration: 100 } }
                }

                contentItem: TextInput {
                    id: textInput
                    text: control.textFromValue(control.value, control.locale)
                    font.pointSize: 11
                    color: "#333"
                    selectionColor: "#1296ff"
                    selectedTextColor: "#fff"
                    horizontalAlignment: Qt.AlignHCenter
                    verticalAlignment: Qt.AlignVCenter
                    readOnly: !control.editable
                    validator: control.validator
                    inputMethodHints: Qt.ImhFormattedNumbersOnly

                    // 数值改变时的动画:弹出并改变透明度
                    Behavior on text {
                        SequentialAnimation {
                            NumberAnimation { target: textInput; property: "scale"; from: 1.0; to: 1.2; duration: 50; easing.type: Easing.OutQuad }
                            NumberAnimation { target: textInput; property: "scale"; from: 1.2; to: 1.0; duration: 150; easing.type: Easing.OutBack }
                        }
                    }
                }
            }
        }

        Text {
            Layout.fillWidth: true
            color: "#999"
            font.pointSize: 10
            text: "提示:加减号按下时缩放至 0.9 倍,数值变化时弹出放大再回弹"
        }

        Item { Layout.fillHeight: true }
    }
}

关键逻辑解析

这个 demo 里其实藏着三层效果:

1. 按钮按下缩放

qml 复制代码
scale: control.up.pressed ? 0.9 : 1.0
Behavior on scale { NumberAnimation { duration: 100 } }

up.pressed 在按钮按下时为 true,松开恢复 false,scale 跟着在 0.9 和 1.0 之间切换。关键是 Behavior on scale:它给 scale 加了一层过渡,数值一变就播放一小段动画,而不是瞬间跳变。没有它,按钮是"啪"地一下变小;有了它,才是"弹"地一下缩回去。

2. 数值变化弹出

qml 复制代码
Behavior on text {
    SequentialAnimation {
        NumberAnimation { target: textInput; property: "scale"; from: 1.0; to: 1.2; duration: 50; easing.type: Easing.OutQuad }
        NumberAnimation { target: textInput; property: "scale"; from: 1.2; to: 1.0; duration: 150; easing.type: Easing.OutBack }
    }
}

Behavior on text 在输入框文字变化时触发,也就是数值一变就动。SequentialAnimation 把两个动画串起来:先快速放大,再回弹到原样。回弹用的是 OutBack 缓动,它会先冲过头一点再落回来,就是这一下过冲做出了弹性感。放大多大、弹多快,都按自己的手感调。

3. 阴影

DropShadowQt5Compat.GraphicalEffects 里来,给背景加一层淡淡的投影,SpinBox 就像浮在界面上。transparentBorder: true 别忘了,不然阴影会被边缘裁掉。

Demo 2:滚动列表

数值跳变太生硬?这个 demo 把 SpinBox 的输入区换成了 ListView,点按钮时数字不是直接换,而是像滚轮一样滑到下一个。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects

FadeInAnimation {
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        // ... 省略标题组件 TitleSeparator ...

        RowLayout {
            Layout.alignment: Qt.AlignHCenter
            Layout.topMargin: 20
            spacing: 10

            SpinBox {
                id: control
                editable: false
                from: 0
                to: 100

                background: Rectangle {
                    implicitWidth: 140
                    implicitHeight: 40
                    radius: height / 2
                    color: control.enabled ? "#fff" : "#f0f0f0"
                    border.color: control.focus ? "#1296ff" : "#bdbdbd"
                    border.width: control.focus ? 2 : 1

                    // 阴影效果
                    layer.enabled: true
                    layer.effect: DropShadow {
                        transparentBorder: true
                        color: "#20000000"
                        radius: 4
                        samples: 8
                    }
                }

                up.indicator: Rectangle {
                    x: parent.width - width
                    height: parent.height
                    implicitWidth: 40
                    implicitHeight: 40
                    radius: height / 2
                    color: control.up.pressed ? "#eee" : "transparent"

                    // 使用 Rectangle 实现加号
                    Rectangle {
                        x: (parent.width - width) / 2
                        y: (parent.height - height) / 2
                        width: parent.width / 3
                        height: 2
                        color: enabled ? "#666" : "#999"
                    }
                    Rectangle {
                        x: (parent.width - width) / 2
                        y: (parent.height - height) / 2
                        width: 2
                        height: parent.width / 3
                        color: enabled ? "#666" : "#999"
                    }
                }

                down.indicator: Rectangle {
                    x: 0
                    height: parent.height
                    implicitWidth: 40
                    implicitHeight: 40
                    radius: height / 2
                    color: control.down.pressed ? "#eee" : "transparent"

                    // 使用 Rectangle 实现减号
                    Rectangle {
                        x: (parent.width - width) / 2
                        y: (parent.height - height) / 2
                        width: parent.width / 3
                        height: 2
                        color: enabled ? "#666" : "#999"
                    }
                }

                // 核心滚动区域
                contentItem: ListView {
                    id: rollingList
                    clip: true
                    model: control.to - control.from + 1
                    currentIndex: control.value - control.from

                    // 禁止手动拖拽,只通过按钮控制
                    interactive: false

                    // 设置滚动动画速度和曲线
                    highlightMoveDuration: 300
                    highlightMoveVelocity: -1 // 强制使用 duration
                    preferredHighlightBegin: 0
                    preferredHighlightEnd: height
                    highlightRangeMode: ListView.ApplyRange

                    delegate: Item {
                        width: rollingList.width
                        height: rollingList.height

                        Text {
                            text: index + control.from
                            anchors.centerIn: parent
                            color: "#333"
                            font.pointSize: 11
                        }
                    }
                }
            }
        }

        Text {
            Layout.fillWidth: true
            color: "#999"
            font.pointSize: 10
            text: "提示:仅可通过按钮控制滚动,禁用手动拖拽,高亮动画时长 300ms"
        }

        Item { Layout.fillHeight: true }
    }
}

关键逻辑解析

核心思路一句话:把数值变化和列表滚动绑在一起。

qml 复制代码
contentItem: ListView {
    model: control.to - control.from + 1
    currentIndex: control.value - control.from
    interactive: false
    highlightMoveDuration: 300
}
  • model 生成 fromto 的全部数字(0~100 共 101 个),一个数字一个列表项;
  • currentIndex 绑定 value - from,值一变 index 跟着变,列表自动滚到对应项;
  • interactive: false 禁掉手动拖拽------列表只负责展示,操作权全在按钮;
  • highlightMoveDuration 是滚动动画的时长,想快想慢随意调。

有个细节容易踩坑:highlightMoveVelocity: -1ListView 默认按速度算动画时长,条目越多,同样速度下看起来滚得越快。设成 -1 是强制改按时长算,保证每次滚动节奏一致。

滚动范围用 highlightRangeMode: ListView.ApplyRange 加上 preferredHighlightBegin/End 控制,让当前项始终停在可视区中央,不会滚到一半卡在边缘。

这个 demo 设了 editable: false------ListView 本身不支持编辑。想同时支持滚动和直接输入,得在两种模式之间做切换,复杂度会高不少。

运行验证

  1. 用 Qt Creator 打开 qml_spinbox/CMakeLists.txt
  2. Ctrl+R 运行;
  3. 左侧导航切换到缩放动画和滚动列表 demo,点加减按钮看效果。

扩展复用方向

  • 把缩放的 scale 换成 rotation,按钮按下就转一下,效果也很俏皮。
  • 想更接近 iOS 的滚轮选择器,把 ListView 换成 PathView,做成弧形滚动。
  • 给滚动列表上下加渐变遮罩,让数字"沉"进画面里,滚动感更自然。

已验证环境

相关推荐
Quz1 小时前
QML Tumbler 基础用法:多滚轮选择与自定义外观
qt
小灰灰搞电子15 小时前
Qt 实现魔法导航菜单源码分享
开发语言·qt
Quz17 小时前
QML SpinBox 基础与步长控制:可编辑输入与自定义步长
qt
隐积18 小时前
Qt魔法之旅 · 全系列课程导航
qt
海清河晏11121 小时前
Qt 实战:信号与槽+事件系统
开发语言·c++·qt
郝学胜-神的一滴1 天前
Qt 高级编程 037:QSS按钮样式封神指南
开发语言·c++·qt·软件工程·用户界面
only-lucky1 天前
QML对话框控件
qt
郝学胜_神的一滴1 天前
Qt 高级编程 038:打造高颜值下拉菜单按钮
c++·qt
小灰灰搞电子1 天前
Qt 队列容器 QQueue 的详解与示例
开发语言·qt·qqueue