StackView 的页面默认是"一次只显示一张",切页时旧的退场、新的进场。
但它的转场机制其实暴露了每个页面在栈里的位置信息,利用这一点可以做出一类很特别的效果:所有层级的页面同时可见,像一叠错开摆放的卡片,最上层是当前页,下面的每张卡露出一条边。
这篇拆解这个"卡片堆叠"demo:卡片为什么能叠着露出来、push/pop 时卡片怎么归位、以及三个让效果成立的细节。本示例借鉴了Qt官方的示例库。
堆叠效果

每推入一张新卡,它比下面那张往右下方错开 10 像素,并带淡入动画效果;最上层卡片中央有 Push/Pop 两个操作按钮,Push 叠一张带随机颜色的新卡(最多叠加 6 层),Pop 把最上面那张弹掉,下层卡片平滑归位。因为每张卡都一直可见,堆叠的层次感始终在画面上。
演示代码
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
FadeInAnimation {
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
StackView {
id: stack
Layout.preferredWidth: 250
Layout.preferredHeight: 200
property real offset: 10
initialItem: Component {
id: page
Rectangle {
property real pos: StackView.index * stack.offset
property real hue: Math.random()
color: Qt.hsla(hue, 0.5, 0.8, 0.6)
border.color: Qt.hsla(hue, 0.5, 0.5, 0.8)
StackView.visible: true
RowLayout {
anchors.centerIn: parent
spacing: 10
RoundButton {
text: "Push"
Layout.preferredWidth: 60
Layout.preferredHeight: 30
enabled: stack.depth < 6
onClicked: stack.push(page)
}
RoundButton {
text: "Pop"
Layout.preferredWidth: 60
Layout.preferredHeight: 30
enabled: stack.depth > 1
onClicked: stack.pop()
}
}
}
}
pushEnter: Transition {
id: pushEnter
ParallelAnimation {
PropertyAction { property: "x"; value: pushEnter.ViewTransition.item.pos }
NumberAnimation { properties: "y"; from: pushEnter.ViewTransition.item.pos + stack.offset; to: pushEnter.ViewTransition.item.pos; duration: 400; easing.type: Easing.OutCubic }
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 400; easing.type: Easing.OutCubic }
}
}
popExit: Transition {
id: popExit
ParallelAnimation {
PropertyAction { property: "x"; value: popExit.ViewTransition.item.pos }
NumberAnimation { properties: "y"; from: popExit.ViewTransition.item.pos; to: popExit.ViewTransition.item.pos + stack.offset; duration: 400; easing.type: Easing.OutCubic }
NumberAnimation { property: "opacity"; from: 1; to: 0; duration: 400; easing.type: Easing.OutCubic }
}
}
pushExit: Transition {
id: pushExit
PropertyAction { property: "x"; value: pushExit.ViewTransition.item.pos }
PropertyAction { property: "y"; value: pushExit.ViewTransition.item.pos }
}
popEnter: Transition {
id: popEnter
PropertyAction { property: "x"; value: popEnter.ViewTransition.item.pos }
PropertyAction { property: "y"; value: popEnter.ViewTransition.item.pos }
}
}
Item { Layout.fillHeight: true }
}
}
关键逻辑解析
每一层卡片都有自己的 "层号"
把 StackView 想成一叠扑克牌:最底下是第 0 层,往上依次是第 1、2、3 层。系统自动给每张卡标好层号,卡片被推进来或弹出去时,层号会自动更新,不用手动管。
demo 的错位量就是按层号算的:层号 × 10。于是第 0 层不动,第 1 层往右下挪 10 像素,第 2 层挪 20...... 一层层错开,堆叠效果就出来了。
"所有卡片都显示" 的开关
StackView 默认只显示最顶上那一张,其余层是不画的 ------ 这就是 "一次只看到一页" 的原因。demo 特意告诉每张卡:"不管你是不是最上层,都给我显示出来",下层卡的边缘才会露出来,形成错落堆叠。
⚠️ 做普通页面导航时千万别加这一句,否则切换页面时所有页面会叠在一起。
转场动画:一组 "在动",一组 "被带着走"
卡片进出场时,系统会告诉动画 "现在正在动的是哪张卡"。demo 把四种转场分成两组:
- 负责 "动的卡" :新卡进来时,从下方 10 像素处缓缓滑到自己的位置,同时从透明变到不透明 ------ 看起来就像从下面那张卡的边缘滑出来、叠上去;弹走时反向滑出并淡出。
- 负责 "被连带的卡" :新卡盖上来时下面的旧卡位置变了,旧卡弹走时露出来的下一张位置也变了。这些卡不需要动画,直接瞬间换到新位置 "站好" 就行 ------ 观众的注意力都在上面那张卡上。
(小提示:这两组动画写法里有个属性名是复数、有个是单数,都是对的,只是别写混。)
按钮固定在卡片正中
每张卡中央都放了一组 Push / Pop 按钮,让它始终停在卡片正中。如果换成 "铺满整卡" 的定位方式,按钮就会堆到左上角。虽然每层卡都有按钮,但只有最上面那张露脸,实际能点到的也只有它。
颜色随机、层数封顶
每张卡生成时随机取一个颜色(半透明粉彩,边框同色),每叠一层颜色都不同,谁上谁下一眼就能分清。另外还限制了层数:少于 6 层才允许继续叠,多于 1 层才允许弹 ------ 防止无限叠下去,或者把最底层弹掉。
为什么不能加裁剪(clip)
滑动类组件通常要裁剪边界,防止内容画出范围;但这里恰恰相反 ------ 堆叠效果靠的就是下层卡片露出的边缘。每张卡都比前一张往右下挪 10 像素,越靠上的卡越接近边界,一旦开了裁剪,露出的边缘被切掉,层次感就没了。
已验证环境:
- Qt 版本:Qt 6.8.2 / Qt 6.11.1
- 操作系统:Windows 11
- GitHub:QML-Minimal-Demos/qml_stackview