ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

QML文本滚动实战:Flickable、ListView与自动滚动机制全解析

QML文本滚动实战:Flickable、ListView与自动滚动机制全解析 有人问我 QML 里文本滚动怎么写我第一反应总是先反问一句你要的是哪种滚动是公告栏那种自己会跑的横向跑马灯还是长篇新闻页面那种手指一划就带惯性的纵向滚动又或者是消息列表那种几千条记录也不能卡的滚动列表这三种需求代码完全是三套东西拿同一套方案去套结果不是能跑但体验别扭就是直接卡到没法用。这篇我会把 QML 文本滚动这件事从原理到实战拆开讲覆盖长文本阅读、自动滚动、列表虚拟化、常见编译错误和运行期陷阱给正在做 Qt 界面、尤其是刚接触 QML 的开发者一份能直接参照落地的方案。开头先建立一个基本认知QML 里文本和滚动本来就不是同一个组件的职责。看完这一篇你再遇到文本滚动需求时会先问自己几个关键问题而不是急着复制代码。1. 先搞懂 QML 文本滚动的底层分工谁负责画谁负责滚1.1 Text 只是个画文字的画家别指望它有滚动属性先泼一盆冷水QML 自带的 Text 没有滚动属性。你翻文档翻到头也找不到 text.scrollY 这种东西因为 Text 只负责把字符串渲染成一个长宽可度量的图形元素它的输出是一块画布没有交互逻辑也没有裁剪窗口的概念。这个设计不是偷懒而是职责分离的体现。QML 坐标系里一个元素能显示多大区域由 width、height 和 clip 决定元素能不能被手势带动由 Flickable 之类的容器处理元素内部内容有没有超出可视区则由 contentWidth 和 contentHeight 告诉外部。Text 只需要老老实实汇报自己有多少内容滚动的事交给别人做。理解这一点以后很多疑问都能自己解答。比如你发现 Text 里的文字超出边框、被截断成一半那不是滚动的锅是没加 clip 或者没包 Flickable你发现文本没有换行、所有字挤在一行那是 wrapMode 没设你发现滚动条不出来往往不是控件坏了而是 contentHeight 和实际内容高度不一致。先把画和滚分开看待后面所有方案都是顺着这个思路展开的。1.2 四个滚动容器的分工表与场景选型QML 里能承载滚动的容器我实际用下来主要是四个Flickable、ScrollView、ListView、TableView。它们不是并列关系而是层级不同。Flickable 是最底层的交互基座负责手势、惯性和内容偏移ScrollView 是官方在 Flickable 之上做的一套便捷封装ListView 是在 Flickable 基础上实现了数据驱动和虚拟化TableView 又进一步在行与列两个维度上做内容复用。理解这个层级关系你就能明白为什么同一个滚动想法可以用不同组件实现也能在遇到问题时判断该改哪一层。我用一张表总结自己平时的选型逻辑场景内容特征首选方案备注单个长文本阅读一段文字可能几千行Flickable Text最可控高度自算弹窗、设置页等快捷滚动表单、混合控件ScrollView直接放 Column 就能滚重复结构的消息/列表几十到几十万条ListView虚拟化不卡交叉表格行和列都需要锁定的数据TableView官方表格组件这里最关键的分叉点是你面对的是一段文本还是一批文本。一段文本用滚动容器包住 Text一批文本走 ListView 的 delegate 复用机制。我见过很多刚开始学 QML 的人拿着 ListView 去显示一篇长文章又拿着 Text 去硬塞一百条消息都属于选错了工具后面越修越乱。做这个判断并不难用户看到的是单个滚动的文本区域还是多个格式一致、像流水一样滚动的行前者进 Flickable后者进 ListView这是 QML 文本滚动第一步要做的事比任何代码都重要。2. 手拖长文本Flickable 与 Text 的黄金组合2.1 为什么我不用 ScrollView 而手动搭 Flickable新版本 Qt Quick Controls 提供了 ScrollView看起来更省事拖一个控件进去内容超了就能滚。但我自己搭长文本区域仍然习惯用 Flickable Text 的组合原因有三个。第一ScrollView 在内容宽度上偶尔会出现不知道到底应该多宽的尴尬。文本内容受 width 约束也受内容本身影响放在 ScrollView 里有时候会有横向误滚动的现象手动搭 Flickable 时contentWidth 我明确指定为视图宽度不会有歧义。第二ScrollView 内部对内容做了一层隐式布局排查问题多一层间接性直接 Flickable 时每个属性都是自己设置的出问题一眼就能看到是哪一行。第三Flickable 可以直接配 ScrollBar.vertical 或者 ScrollIndicator也能和 Behavior 动画无缝配合后面做自动滚动时更顺手。当然ScrollView 不是不能用。当你滚动区域内是一整块 Column里面混合了各种输入框、按钮、图片懒得手动管理 layout 时ScrollView 确实方便。但纯粹做一段文本的滚动展示少一层封装就少一份不确定性。而且我后续要接 Behavior、要接自动滚动、要控制滚动条样式这些在裸 Flickable 上都直接现成放进 ScrollView 之后反而需要绕一圈。这个选择有个人偏好成分但我建议你至少在做一个纯文本滚动组件时试试 Flickable 方案体验过就知道可控性有多重要。2.2 最小可运行代码与 implicitHeight 的作用直接给一个能跑的最小示例。新建一个 QML 文件窗口大小自己改核心是看整体结构import QtQuick 2.15 import QtQuick.Controls 2.15 ApplicationWindow { visible: true width: 360 height: 480 title: TextScroll Demo Flickable { id: flick anchors.fill: parent contentWidth: width contentHeight: bodyText.implicitHeight clip: true Text { id: bodyText width: flick.width text: 这里是很长很长的一段文本……请替换成你的真实内容。\n\n这一段要足够长长到超过窗口高度滚动才能体现出来。 wrapMode: Text.WordWrap font.pixelSize: 16 color: #333333 lineHeight: 1.4 } ScrollBar.vertical: ScrollBar { } } }这段代码有三个要点。第一contentHeight 绑的是 bodyText.implicitHeight不是 bodyText.height。Text 的 implicitHeight 含义是给我这些文字、这个宽度、这个字体我自然渲染出来的高度。当设置了 wrapMode 之后文本会根据宽度折行implicitHeight 就等于折行后的总高度这正是 Flickable 需要的滚动范围。如果你写成 contentHeight: bodyText.height而 Text 的高度没有被显式设置它可能还是 0 或者一个自适应值滚动区域就会消失或比实际短一大截。第二wrapMode: Text.WordWrap 一定要设。不设的话Text 只会在硬换行符处折行长句子会横着延伸Flickable 的 contentWidth 就会超出容器出现横向滚动体验很差。如果文本里有很长的无空格单词可以改成 Text.WrapAnywhere 防止单个单词溢出。第三clip: true 这一行通常不能省。Flickable 自身不会自动裁剪它的下属内容到可视区域不加 clip 时超出窗口的部分会直接绘制出来文本层叠得没法看。我在实际项目里看到过很多次没加 clip 导致滚动时文字穿透容器的问题这个开关是滚动视觉不出错的根基。2.3 滚动条、触摸节奏和边界的细节ScrollBar.vertical: ScrollBar {} 这种写法是 Qt Quick Controls 2 的附件式滚动条。好处是它会自动跟随 Flickable 的移动状态出现和隐藏policy 默认是 AsNeeded也就是内容没超时不显示超出时才显示。如果你的目标平台是触摸屏我更推荐用 ScrollIndicator它更薄、不带交互手柄视觉上更干净ScrollIndicator.vertical: ScrollIndicator { }两个小细节值得留意。一个是 ScrollBar 的 minimum 和 maximum 不需要你手动维护Flickable 会根据 contentHeight 和 height 自动计算。另一个是 Flickable 自带惯性滚动和边缘回弹你不需要写任何 MouseArea 逻辑。我之前看到有人拿 MouseArea 拖 Text 的 y 来实现滚动又得算 startY又得处理边界代码写得痛苦效果还生硬。Flickable 的物理引擎是被无数项目验证过的不要自己去复制一个劣化版。边界问题也在这里说清楚。Flickable 的滚动范围自动限制在 contentY ∈ [0, contentHeight - height] 区间内用户拖过头会有回弹到边界会自动停下。这一点在长文本阅读场景里基本够用但如果你做的是翻页器而不是滚动区那 Flickable 就不太合适了翻页应该用 ListView 的 SnapPosition 或者自己维护 currentIndex。选型别错位后面少返工。3. 主动滚动的三种形态跑马灯、歌词跟随与行情播报如果说手拖滚动是人找内容那自动滚动就是内容找人。这一章基本都是我在做广告屏和车载 HMI 时攒下的经验也是文本滚动设计里最容易出彩的部分。自动滚动和手动滚动的代码看起来差不多但对动画节奏、状态管理的敏感度完全不在一个级别。3.1 单行跑马灯的裁剪与动画时长设计单行跑马灯是 QML 里最常见的自动滚动形态比如公告栏、股票行情、歌词单行预览。它的核心就三件事裁剪、位移动画、时长随文本长度自适应。裁剪直接用 clip容器宽度固定超出部分不绘制。位移动画可以是一个 NumberAnimation 作用于 Text 的 xRectangle { id: marqueeBox width: 320 height: 36 clip: true color: #f5f5f5 Text { id: marqueeText property bool running: false text: 这是一条会横向滚动的公告文本用来测试跑马灯效果 width: implicitWidth y: (marqueeBox.height - height) / 2 function startMarquee() { marqueeAnim.from marqueeBox.width marqueeAnim.to -width marqueeAnim.duration Math.max(3000, width * 25) marqueeAnim.start() } NumberAnimation on x { id: marqueeAnim loops: Animation.Infinite running: marqueeText.running } } Timer { interval: 500 running: true onTriggered: { marqueeText.startMarquee() marqueeText.running false } } }很多新手会把 duration 写成固定 2000 毫秒结果文本长一点就像飞一样短一点又半天挪不动。正确做法是让 duration 和文本宽度成正比位移量除以速度等于时间文本越宽跑的时间就得越长视觉速度才稳定。上面代码里我用了Math.max(3000, width * 25)这个 25 是经验系数你可以根据视觉偏好调整大屏可以放到 30 以上手机端可以收到 15 左右。另一个坑是首次显示。组件初始化时文本可能还没设置好如果立刻 start动画的 from 和 to 都是基于错误宽度算出来的跑起来位置会跳。所以我用一个 Timer 延迟 500 毫秒启动让文本先完成布局计算。更严谨的写法是等width 0再启动或者放在文本 setter 之后延迟一帧调用本质都是等文本宽度量完再动。3.2 歌词/公告纵向自动滚动Timer 驱动还是动画驱动纵向自动滚动的需求是另一类常见于歌词跟随显示、滚动新闻、客服聊天自动播报。这里有两个技术路线我分别说清楚适用场景。路线一是 Timer 驱动每隔固定时间把 contentY 加上一个步进值。代码最简单适合匀速、无状态变化的滚动字幕场景播放感强但每 50 毫秒跳一次视觉上会有一点顿号感Timer { interval: 50 repeat: true running: autoScrollEnabled onTriggered: { listView.contentY Math.min(listView.contentY 2, listView.contentHeight - listView.height) } }路线二是动画驱动用 NumberAnimation 或 Behavior 对 contentY 做插值。它比 Timer 直接赋值平滑因为 QML 动画系统会按渲染帧插值而不是固定间隔跳变。我在歌词同步这种对流畅度要求高的场景里都用动画具体实现是给 Flickable 的属性加动画NumberAnimation { target: lyricFlick property: contentY duration: 600 easing.type: Easing.OutCubic }这里有一个常见误区有人觉得动画必须显式写 target 和 property实际上你想让 contentY 每次变化都自动平滑可以直接在 Flickable 里写Behavior on contentY { NumberAnimation { duration: 300 } }这样不管代码里哪个位置给 contentY 赋值滚动过程都会自动润滑省掉一坨手动动画管理。代价是用户手指拖动时也会被强行加一个 300 毫秒的动画拖起来感觉黏糊糊的。所以更严谨的做法是只在自动滚动模式下开 Behavior用户手势接管时关闭下面这一节就讲这个接管逻辑。3.3 自动滚动与用户手势的接管机制自动滚动最忌讳的事情就是用户想停下来它还在跑。在广告屏上这还好但在车载、医疗、收银台上用户一旦触摸文本区域通常意味着要暂停自动滚动否则操作会被动画干扰。我的做法是用一个布尔状态 autoScroll加上 Flickable 的交互信号来做抢占。Flickable 内置了 dragging 和 moving 属性当用户手指按下并拖动时它们都会变成 true。实际代码这样组织Flickable { id: contentFlick property bool autoScroll: true onDraggingChanged: { if (dragging) { autoScroll false } } onMovingChanged: { if (!moving !dragging) { resumeTimer.restart() } } } Timer { id: resumeTimer interval: 3000 onTriggered: { contentFlick.autoScroll true } }恢复自动滚动时最好让 contentY 接着当前的位置继续滚而不是跳回顶部。做法是把自动滚动的起点设为当前 contentY动画的 to 属性设为新的目标值。我做过一个土办法每次 resume 时记录当前 contentY 作为 fromto 设为 contentHeight - heightduration 按剩余距离折算。这样不管用户划到哪都能无缝衔接自动播报不会出现突然跳回开头的突兀感。这套接管逻辑说白了是一个状态机激活自动滚动、用户抢占、延迟恢复。QML 没有现成 SDK 给这个但我用这段逻辑在多个触摸平台上跑过稳定性足够。唯一要注意的是 Timer 重启时记得先 stop 再 start否则恢复时间会被无限重置。4. 文本列表滚动ListView 的虚拟化与性能控制长文本列表是文本滚动里最考验性能的一类。前面说的 Flickable 方案一次只渲染一个 Text多长都不会太卡但换成几百条消息的列表如果方向错了界面会肉眼可见地掉帧。这一章说说我常用的 ListView 调优路径。4.1 ListView 为什么比 Repeater 更适合长文本列表先说结论QML 里做长文本列表默认应该上 ListView而不是 Repeater。Repeater 非常实诚模型里有多少条数据它就老老实实创建多少个 delegate 实例。假如你有 500 条消息、每条消息里有 3 行文本那就是 1500 个 Text 元素同时活着QML 对每一个元素都要维护属性绑定、布局计算、渲染纹理拖起来不卡才怪。ListView 则采用虚拟化和元素回收它只创建当前可视区域附近的 delegate大约是屏幕高度再加一个缓冲垫通常是十几到二十几个。滚动时离开可视区的 delegate 会被释放或者重新绑定给新的数据因此无论滚到底部是 500 条还是 5 万条场景中同时存在的 Text 数量都差不多。我做过一次对比测试400 条带三行描述的模型Repeater 启动时能明显感觉到卡一下而 ListView 首屏秒开滚动时帧率稳定在 60 附近。所以如果你看到qml repeater相关的求助很多答案都是用 Repeater 塞大列表导致卡死换成 ListView 就好了。Repeater 适合静态的、数量确定且数量少的界面元素比如画一组 Tab 按钮列表滚动场景就不要逞强了。4.2 cacheBuffer 与 delegate 文本设计的平衡ListView 虽然虚拟化但也不是完全没有配置项。最常用的是 cacheBuffer它告诉 ListView 在可视区域之外多预创建多高像素的 delegate。默认值可能只覆盖一两屏如果你的列表项很重创建时有一堆复杂 Text 和绑定快速滚动时容易看到空白占位。实践里我会按列表项高度来设 cacheBuffer。假设 delegate 高度是 60 像素我预创建一屏半外再多 200 像素就写cacheBuffer: 200如果 delegate 很高比如 300 像素的大卡片再按需往上调。设得太大有副作用ListView 会一次性创建更多 delegate滚得快时频繁实例化和销毁也消耗性能。我的习惯是先从可接受的最低值开始肉眼观察快速滚动时有没有白屏有就往上加 50直到稳定为止。delegate 内部的 Text 也要节制。能用 elide 单行截断的不要让它自由换行消息摘要在列表里只需要两行就明确计算好高度字体、颜色这些不变的东西尽量写成固定值减少不必要的绑定。QML 的绑定引擎会在依赖变化时重新求值文本列表滚动时如果每个 delegate 都在刷宽度、高度、间距帧时间会显著拉长。最后一点经验是如果 delegate 里有图片把图片的缓存策略设好比滚动代码本身更影响流畅度。4.3 TableView 的滚动场景和边界热搜词里有 qml tableview所以这里也顺带说一句。TableView 适合真正意义上的交叉表格有行有列、可能需要多列表头固定、单元格内容随模型刷新。它的滚动机制底层同样是虚拟化但它是按 cell 粒度复用文本内容的生命周期比 ListView 的 delegate 更碎。如果你的需求只是一列全是文本的清单也就是 ListView 的本职活别为了一句更高级把单列数据硬塞进 TableView性能不一定更好代码还不直观。如果确实遇到多列表格要注意 TableView 里 delegate 的高度通常需要你固定。尤其是文本在某列折行时QML 表格无法像网页 table 那样自动撑高行我一般给每个字段设一个最大行数上限超出直接 elide 加 tooltip避免行高在滚动过程中来回跳动。横纵双向滚动时用户体验的关键是表头能否固定。Qt 的 TableView 可以把 header 单独分一层滚动这个可以深入研究官方示例这里不展开讲但记住表格滚动选型前先明确是否需要行列交叉这条就够了。5. 滚动路上的实测坑从编译错误到内容跳变这一章全部来自我实际调试过的案例。QML 文本滚动表面上代码量不大真正难的是那些运行期才暴露的细节。5.1 编译错误90% 出在 import 和属性拼写先说 QML 编译错误。我刚用 ScrollBar 时被卡过一次ScrollBar.vertical: ScrollBar {}写完QML 报ScrollBar is not a type。原因很简单——ScrollBar 在 QtQuick.Controls 2 里不是 QtQuick 2 的一部分。文件顶部只写了import QtQuick 2.15自然找不到类型。补齐import QtQuick.Controls 2.15即可。还有一类高频错误是clip拼成clep、contentHeight拼成contentheight。QML 对属性名大小写敏感拼写错了不会立刻崩溃而是提示Cannot assign to non-existent property看起来像编译错误实际上只是打错字。我的建议是把 QML 引擎的控制台错误输出都打开盯着报错行号改比瞎猜快得多。另一个容易翻车的是 import 版本。比如import QtQuick 2.15写成了import QtQuick 2.0而 Text 的 lineHeight、renderType 这些属性在老版本里不存在编译直接报Property does not exist。我的习惯是新建文件默认用当前 Qt 版本对应的 import别为了兼容旧环境刻意降低版本除非你有明确的跨版本发布需求。QML 的报错机制有时不会定位到具体行这时候把整个文件单独用 qmlscene 加载报错信息会清晰很多。5.2 contentHeight 测量不准引发的滚动范围漂移滚动范围漂移是最难排查的坑之一。现象是文本明明有 1000 像素长Flickable 滚到 700 像素就到底了或者反过来滚不到底。大多数时候问题出在 contentHeight 的赋值来源。我遇到过一个经典案例Text 放在 Flickable 里我给 Text 设置了 anchors.top 和 anchors.left但没设 width结果 Text 的 implicitWidth 远大于容器wrapMode 失效行数变少contentHeight 自然不对。解决方式很简单给 Text 显式设置宽度等于 Flickable 的宽度让 wrapMode 按这个宽度折行。另一个案例是字体尚未加载完成时implicitHeight 按缺省字体计算等 FontLoader 真正加载完自定义字体行高变高原来的 contentHeight 就小了。解决方式有两种用 FontLoader 的 statusChanged 信号等字体到位后再重新给 contentHeight 赋值或者在 QML 引擎启动前把字体通过系统 fontconfig 注册好让首帧就正确。如果你用的是 ListView滚动漂移还可能来自 delegate 的高度估算。ListView 在未加载时不知道每个 delegate 实际多高如果 delegate 高度不固定、内容又动态变化滚动范围每次都会跳动。最简单的稳定办法是 delegate 中显式设置 height不要让它被内容撑高。5.3 自动滚到边界的锯齿感与 EmptyArea自动滚动到底部时文本经常会有轻微的抖动或残留半截像素看起来毛糙。这里有两层原因。一个是物理因素contentY 的取值范围 [0, contentHeight - height] 里有浮点。如果 contentHeight 和 height 中一个带小数滚动到极限位置时就会出现 0.5 像素的错位文本边缘自然不齐。我通常在 Text 上开renderType: Text.NativeRendering在 Windows 和 Android 上锯齿感会明显减少矢量字体模式下可以配合verticalAlignment: Text.AlignTop消除额外行高带来的底部留白。另一个诱因是 Flickable 的 boundsBehavior。默认到边界有回弹动画自动滚动时这个回弹会被触发视觉上就像滚过头又弹回来。做法是把boundsBehavior: Flickable.StopAtBounds加上或者只在 autoScroll 模式下切换。如果你希望保留弹跳感至少要把自动滚动动画的 to 控制在精确边界上不要留尾巴。这个尾巴问题我在歌词同步组件里改过很多次最后结论是自动滚动场景一律 StopAtBounds手动滚动场景才考虑保留回弹。5.4 设计器里预览滚动效果的几个注意事项QML 设计器只能静态预览布局手势滚动、惯性这些运行时行为基本看不出来。这一点别指望设计器真正调滚动手感必须跑真机或桌面模拟。我见过有人在设计器里看到 Flickable 内容被裁剪就误以为代码有问题其实运行时滚一下就正常了。设计器还有一个坑自动滚动的 Timer 默认 running 为 true 时设计器打开文件就会开始跑动画编辑体验很抓狂。我的组件里自动滚动开关默认值是 false并且加一个Qt.application.state Qt.ApplicationActive的判断只在应用处于前台时让动画运行。这样既不耽误运行效果也不干扰设计器预览。如果你在设计器里看到文本滚动区域一直在动或一直闪先检查是不是 Timer 和动画没做前台判断。5.5 字体渲染与 DPI 带来的行高差异最后一条经验是 DPI。QML 的 Text 高度在 Windows 的 125% 或 150% 缩放下和 Linux 默认 96 DPI 下是有细微差别的用 pixelSize 时尤其明显。如果滚动区域的高度恰好卡在一个视觉上刚刚好的临界值换一台机器可能就会多出一行或少一行原本刚好一屏的内容突然多出半屏空白。我的做法是给文本滚动容器留出 10 到 20 像素的缓冲不要把内容高度卡得太死同时避免到处写死width: 320这种魔鬼数字用Math.round(320 * textScaleFactor)或者走 Layout 的相对布局。字体渲染差异还会影响滚到底时最后一行是否完整有些字体把 descent 部分算进行高有些则不算结果同一个文本在不同平台上的 implicitHeight 可能差几个像素。遇到这种情况别急着改 contentHeight 的公式先在目标设备上截图看看是哪一侧的空白再决定是调整 Text 的 bottomPadding 还是容器的 height。6. 直接能用的封装滚动文本组件与自检清单前面讲了原理和坑最后给一份可以直接拿去改的封装。我自己项目里基本就是两个组件一个长文本滚动区一个单行跑马灯。遇到滚动需求先判断是哪种形态然后二选一。6.1 一个通用长文本滚动组件的接口设计TextScrollArea.qml 的接口我这样设计对外暴露 textValue、fontSize、textColor、scrollBarVisible 四个属性内部用 Flickable Text ScrollBar。自动滚动不是这个组件的默认功能因为长文本通常由用户手动阅读自动滚反而干扰阅读需要自动滚动的场景用后面的 MarqueeText或者单独接 Timer。import QtQuick 2.15 import QtQuick.Controls 2.15 Item { id: root property string textValue: property int fontSize: 14 property color textColor: #333333 property bool scrollBarVisible: true Flickable { id: flick anchors.fill: parent clip: true contentWidth: width contentHeight: textView.implicitHeight boundsBehavior: Flickable.StopAtBounds Text { id: textView width: flick.width text: root.textValue color: root.textColor wrapMode: Text.WordWrap font.pixelSize: root.fontSize lineHeight: 1.4 } ScrollBar.vertical: ScrollBar { policy: root.scrollBarVisible ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff } } }使用示例TextScrollArea { anchors.fill: parent textValue: 这里放你的长文本内容…… fontSize: 16 }接口越简单越不容易被用坏。我见过有人把组件做成七八个信号、四五个 mode结果每个使用处都得写一堆配置最后没人愿意用。滚动组件真正需要暴露的只有文本内容和几个视觉参数剩下的都应该是内部实现细节。如果你需要给组件加回到顶部跳转到底部等方法优先用函数而不是属性保持 API 语义清晰。6.2 跑马灯组件 MarqueeText 的完整代码单行跑马灯的封装思路类似。对外暴露 textValue、speed像素/秒、loop 三个属性内部按宽度比例计算动画时长。这里我特意按速度而不是时长来设计接口因为调用方的心智模型更简单传一个30 像素每秒读代码的人立刻能感知滚动快慢。import QtQuick 2.15 Rectangle { id: marqueeRoot property string textValue: property real speed: 30 property bool loop: true property color bgColor: transparent property color textColor: #333333 height: 32 clip: true color: bgColor Text { id: marqueeText width: implicitWidth anchors.verticalCenter: parent.verticalCenter text: marqueeRoot.textValue color: marqueeRoot.textColor font.pixelSize: 14 } NumberAnimation on x { id: marqueeAnim loops: marqueeRoot.loop ? Animation.Infinite : 1 running: false onFinished: x marqueeRoot.width } onWidthChanged: restartMarquee() function restartMarquee() { marqueeText.x marqueeRoot.width marqueeAnim.stop() marqueeAnim.from marqueeRoot.width marqueeAnim.to -marqueeText.width marqueeAnim.duration Math.max(100, (marqueeRoot.width marqueeText.width) / marqueeRoot.speed * 1000) marqueeAnim.start() } }注意两个细节。一个是速度为零时要防御否则除法会得到无限大。可以在 restartMarquee 里加一句if (marqueeRoot.speed 0) return;。另一个是组件宽度的变化会改变起点和位移量所以我用onWidthChanged: restartMarquee()把动画重启一遍避免文本从陈旧坐标开始跑。这个逻辑是我在屏幕旋转、窗口 resize 场景下反复调出来的不然跑马灯会从错误的位置起跑看起来像跳帧。6.3 性能自检清单与最后的私有技巧最后给一份自检清单每次写完滚动相关代码我基本按这个过一遍delegate 里有没有多个 Text 叠加能合并成一个富文本节点的就别拆成三个 Text。模型变化频繁吗如果数据每秒都在变考虑用 ListModel 的批量更新而不是逐条 insert。滚动动画里有动态创建对象吗滚动过程中不应该 new 任何组件组件应在滚动前预创建完成。自动滚动时有没有和用户手势冲突必须有接管和停止机制。文本行高固定吗固定高度下 ListView 的布局计算最快滚动范围也最稳定。高 DPI 设备上有没有做像素对齐必要时设置 renderType避免半个像素的坐标导致锯齿。私有技巧方面还能分享一个给自动滚动的文本加一层首尾渐隐遮罩视觉上会干净很多。方法是在滚动容器上叠加两个渐变矩形一个从顶部透明到底部实色一个反过来让文本滚入滚出时像是淡入淡出。这个效果在广告屏和车载系统中远比硬切边框高级代码也不复杂。核心是用一个带 opacity 渐变的 Rectangle 覆盖在 Flickable 上层记得把它的interactive关掉以免挡住手势。做多了滚动功能后你会发现真正难的从来不是让文本动起来而是在合适的场景以合适的节奏动起来。上面这些封装和自检都只是脚手架具体什么速度适合阅读、什么时机该暂停还是得拿到真实设备上让用户试。这个点没有文档能替你回答只能靠一次次真机调试来判断。
返回列表