QML SwipeView:标签导航 与 Loader 懒加载

文章来源声明: 原文作者:Quz; 来源站点:掘金; 原文链接:https://juejin.cn/post/7684203867400618022; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

TabBar 联动与 Loader 懒加载两条路线讲得很透,active 三条件与日志验证是可直接复用的写法,适合 QML 中大型页面导航选型参考。

上一篇主要介绍了 SwipeView 的滑动翻页和指示器样式调整。这篇解决"页面多了怎么管理"的问题。

一是把 SwipeView 和 TabBar 绑定在一起,将页签点击与页面滑动操作连接起来,做成 App 常见的"顶部标签 + 内容页"结构;二是给每个页面套上 Loader,用日志区实时观察页面创建与销毁,确认懒加载真的只建了该建的页面。

  • SwipeView 与 TabBar 联动 — 页签点击和手势滑动双向同步
  • SwipeView 与 Loader — 按需创建页面,日志区验证懒加载过程

SwipeView 与 TabBar

3.gif

数据源用一个 ListModel 统一管理,TabBarSwipeView 各自用 Repeater 遍历同一份数据生成页签和页面。点页签切页面,滑动页面页签也跟着走,两边永远一致。

演示代码

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

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

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

        ListModel {
            id: listModel
            ListElement { demoText: "Page1"; demoDes: "This is Page1" }
            ListElement { demoText: "Page2"; demoDes: "This is Page2" }
            ListElement { demoText: "Page3"; demoDes: "This is Page3" }
        }

        TabBar {
            id: tabBar
            Layout.fillWidth: true
            currentIndex: swipeView.currentIndex
            Repeater {
                model: listModel
                delegate: TabButton {
                    text: demoText
                    onClicked: swipeView.currentIndex = index
                }
            }
        }

        SwipeView {
            id: swipeView
            Layout.fillWidth: true
            Layout.fillHeight: true
            clip: true

            Repeater {
                model: listModel
                delegate: CustomRect {
                    description: demoDes
                }
            }
        }
    }
}

关键逻辑解析

同步的关键是 currentIndex,方向是"手势滑动驱动页签"tabBar.currentIndex 直接绑到 swipeView.currentIndex:滑动翻页时 SwipeView 先改自己的 currentIndex,TabBar 收到变化自动高亮对应页签——这是 TabBar 作为"跟随方"的绑定。

点页签时反向写回 swipeView.currentIndexTabButton.onClicked 里执行 swipeView.currentIndex = index,SwipeView 翻到对应页,页签高亮又由上一行绑定同步回来。两个方向凑齐,双向联动成立,而且没有双向绑定造成的循环隐患:只有一处绑定(TabBar ← SwipeView),点击是命令式赋值。

一个 ListModel 喂两个 Repeater,保证页签和页面数量天然一致TabBar 的 delegate 取 demoText 当页签文字,SwipeView 的 delegate 取 demoDes 当页面内容。增删数据只需改 listModel,页签和页面同步增减,不会出现"页签 3 个、页面 2 个"的错位。

注意这个 demo 没有用 interactive 指示器,页签本身就是导航。TabBar 自带选中态高亮和均分宽度,比 PageIndicator 更适合"多、需要文字说明"的页面;指示器适合"少、只有圆点"的场景。

SwipeView 与 Loader

4.gif

5 个页面由一个 Repeatermodel: 5 生成,但每个页面都包在 Loader 里,只有"当前页和左右相邻页"才真正实例化。底部日志区实时打印每页的 created/destroyed,滑动时能看到页面按需创建、滑远后销毁,而不是一次建满 5 个。

演示代码

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

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

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

        SwipeView {
            id: swipeView
            Layout.fillWidth: true
            Layout.fillHeight: true
            clip: true

            currentIndex: indicator.currentIndex
            Repeater {
                model: 5
                Loader {
                    active: SwipeView.isCurrentItem || SwipeView.isNextItem || SwipeView.isPreviousItem
                    sourceComponent: CustomRect {
                        description: "This is CustomRect" + (index+1)
                        Component.onCompleted: logArea.message.append("created: CustomRect" + index)
                        Component.onDestruction: logArea.message.append("destroyed: CustomRect" + index)
                    }
                }
            }
        }

        Rectangle {
            Layout.fillWidth: true
            Layout.minimumHeight: 100
            Layout.maximumHeight: 100
            Layout.margins: 10
            border.width: 1
            border.color: "#ccc"
            radius: 6

            LogArea {
                id: logArea
                anchors.fill: parent
            }
        }

        CustomPageIndicator {
            id: indicator
            Layout.alignment: Qt.AlignHCenter

            count: swipeView.count
            currentIndex: swipeView.currentIndex
            visible: count > 1
        }
    }
}

页面组件的日志往 logArea 上追加(LogArea 是基于 TextArea 的自制滚动日志区),指示器复用项目里的 CustomPageIndicator——一个封装好 delegate、开了 interactive 的 PageIndicator。

关键逻辑解析

active 三选一是懒加载的开关SwipeView.isCurrentItemisNextItemisPreviousItem 是 SwipeView 给每个页面子项提供的附加属性,分别表示"我是当前页 / 我是下一页 / 我是上一页"。三个条件用 || 连起来:只要页面处在当前、左邻或右邻位置,Loader.active 为 true,组件才被创建;滑到更远的位置条件变 false,页面被销毁。这是上一篇提到懒加载写法的完整落地。

为什么是"当前 ± 1"而不是只建当前页?滑动动画过程中,手指拖到一半屏幕同时露着两页——如果只预建当前页,拖出下一页的瞬间才去创建,会出现白屏闪烁。保留左右邻页,动画随时有内容可画;而隔了两三页的远端页面滑过去还需要时间,没必要提前占内存。

日志区把"创建/销毁"变成可观察的过程。页面在 Component.onCompletedComponent.onDestruction 里各打一条日志,滑到第 3 页时控制台会看到前两页陆续 destroyed、后两页陆续 created——懒加载到底省了多少实例,一眼可见。

currentIndex: indicator.currentIndex 让指示器成为唯一输入源。这里没让 SwipeView 自己管页码,而是指示器点哪页、SwipeView 就翻到哪页;用户手势滑动时 currentIndex 变化又回写给指示器高亮,和上一篇"自定义指示器"的双向同步是同一套路。

TabBar 方案和 Loader 方案怎么选

TabBar 联动Loader 懒加载
导航形式文字页签圆点指示器
页面管理常驻,全部创建只建当前页 ± 相邻页
适合场景页签少、内容轻、要导航可见页多、单页重、省启动开销
内容来源`ListModel` 驱动,可增删`Repeater` + `Loader`

两者不冲突:想让"重页面"也有文字页签导航,就把 TabBar 联动和 Loader 懒加载拼在一起——页签驱动 currentIndex,页面用 active 条件按需创建。

运行验证

  1. Qt Creator 打开 qml_swipeview/CMakeLists.txt,按 Ctrl+R 运行;
  2. 点「SwipeView 与 TabBar」,点 Page1/2/3 页签或直接滑动内容区,观察两者始终同步;
  3. 点「SwipeView 与 Loader」,连续滑动页面,看日志区滚动输出 created/destroyed,再点底部圆点跳页,观察跳远页时中间页的创建销毁顺序。

扩展复用方向

  • 想让 TabBar 方案也省内存,把 SwipeView 里的 delegate 换成 Loader + active 三条件即可,TabBar 联动逻辑不用动;
  • 页面是外部 .qml 文件时,把 sourceComponent 换成 source,就能做成"按文件名懒加载页面"的通用宿主;
  • 日志区的做法可以推广到任何想看组件生命周期的调试场景——Component.onCompleted/onDestruction 打点即可。

已验证环境