跑马灯 (marquee) — 是一种视觉特效,文本在受限区域内自动水平滚动,允许在紧凑容器中显示长行文本。历史上,该术语源自 HTML 标签 <marquee>,在 1990 年代的 Web 中很流行。在现代移动开发中,滚动文本用于播放器、新闻滚动条和状态栏。根据 Apple Human Interface Guidelines 和 Google Material Design,跑马灯仅在有限场景中允许使用,即静态截断加省略号会损害用户体验的情况。
要点
跑马灯 — 是一种 UI 效果,文本在固定区域内水平移动,循环滚动,以便用户可以阅读容器中容纳不下的所有内容。与隐藏部分文本的 ellipsize 不同,marquee 显示全部文本,但在运动中。
Marquee 一词(英语意为“遮阳篷”、“广告牌”)因 HTML 标签 <marquee> 而固定下来,该标签被添加到 Netscape 和 Internet Explorer 浏览器的早期版本中。该标签允许在没有 JavaScript 和 CSS 的情况下创建滚动文本,但从未成为 HTML 标准的一部分。现代浏览器为了向后兼容而继续支持它,但不推荐使用。
在移动开发中,跑马灯解决了紧凑容器中长标签的问题:播放器中的歌曲名称、汇率滚动条、电视屏幕上的新闻行。然而,由于动画增加了可访问性负担并可能惹恼用户,因此仅限特定场景使用。
根据 Nielsen Norman Group(2023)的研究,移动文本 使阅读速度降低 15-20%,并增加认知负荷。仅在静态截断(ellipsize)确实不可接受时才使用跑马灯。
在 Android 上,跑马灯通过 TextView 的内置机制实现,使用属性 android:ellipsize="marquee"。这是唯一不截断文本而是动画化其滚动的 ellipsize 模式。
激活 marquee 需要满足三个条件:singleLine="true"(或 maxLines="1")、ellipsize="marquee" 和在代码中调用 setSelected(true)。不调用 setSelected() 动画将不会启动。
<!-- XML layout -->
<TextView
android:id="@+id/marquee_text"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever" />
// Kotlin:激活 marquee
textView.setSelected(true)
textView.isSingleLine = true
marqueeRepeatLimit 属性确定动画的重复次数:marquee_forever(无限)或特定数字。默认值为 marquee_forever。如果文本短于容器宽度,动画不会启动 — TextView 显示静态文本。
重要特性:setSelected(true) 仅在 TextView 具有焦点或处于 selected 模式时有效。在 RecyclerView 中这可能导致问题 — 动画仅对一个元素启动。解决方案 — 使用自定义 MarqueeTextView,重写 isFocused() 并始终返回 true。
从 Android 9(API 28)开始,Google 改进了 marquee 的行为 — 动画变得更流畅,并且在滚动列表时不会卡住。但在旧设备上,marquee 可能会抖动,尤其是在高像素密度下。
iOS 没有 android:ellipsize="marquee" 的内置等效项。跑马灯通过 Core Animation 或使用 SwiftUI 动画手动实现。最常见的方法 — layer.position 动画与裁剪容器结合。
在 UIKit 中,跑马灯基于三个元素构建:带有 clipsToBounds = true 的 UILabel 容器、文本本身和 CABasicAnimation,用于在 X 轴上移动文本,无限重复且 autoreverses = false,使条带向一个方向滚动并跳跃返回。
func startMarquee(for label: UILabel) {
label.clipsToBounds = true
label.numberOfLines = 1
let textWidth = (label.text as? NSString)?
.size(withAttributes: [.font: label.font as any]).width ?? 0
guard textWidth > label.bounds.width else { return }
let animation = CABasicAnimation(keyPath: "position.x")
animation.fromValue = label.layer.position.x + label.bounds.width / 2
animation.toValue = label.layer.position.x - textWidth - label.bounds.width / 2
animation.duration = CFTimeInterval(textWidth / 60)
animation.repeatCount = .infinity
animation.autoreverses = false
label.layer.add(animation, forKey: "marquee")
}
duration = textWidth / 60 的计算给出大约每秒 60 像素的速度 — 舒适的阅读节奏。如果速度超过 120 px/s,文本将变得不可读。如果低于 30 px/s — 动画看起来缓慢。根据具体场景调整 speed。
在 SwiftUI 中,跑马灯的实现更简单 — 通过 offset() 修饰符和可重复动画 .animation(.linear(duration:).repeatForever(autoreverses: false))。SwiftUI 自动管理动画的生命周期,但需要 GeometryReader 来确定文本宽度。
在 Web 上,过时的 <marquee> 标签不推荐使用,尽管浏览器继续支持它。跑马灯的现代实现 — 使用关键帧和 transform: translateX 的 CSS 动画。
CSS 方法的优点:完全控制速度、暂停和动画曲线,支持 prefers-reduced-motion 以实现可访问性,没有语义不正确的标签。
.marquee {
overflow: hidden;
white-space: nowrap;
width: 300px;
}
.marquee-content {
display: inline-block;
animation: marquee 10s linear infinite;
}
@keyframes marquee {
0% { transform: translateX(0); }
100% { transform: translateX(-100%); }
}
/* Pause on hover */
.marquee-content:hover {
animation-play-state: paused;
}
关键技巧:内容上的 display: inline-block 确保元素的宽度等于文本的宽度,translateX(-100%) 将其精确向左移动该宽度。如果内容具有 display: block,translateX(-100%) 将等于容器的宽度而不是文本的宽度,动画将会失效。
为了可访问性,添加 @media (prefers-reduced-motion: reduce) { .marquee-content { animation: none; } } — 患有前庭障碍的用户将看到带有滚动功能的静态文本。
跑马灯在用户无法自行扩展容器且完整文本至关重要的情况下是合理的:锁定屏幕上播放器中的当前曲目、电视面板上的新闻条、出租车应用中的跑马灯(订单状态)。
跑马灯不应使用在表格、列表、表单和对话框中 — 用户积极与界面交互的任何地方。动画会分散注意力,增加元素搜索时间并引起不适。根据 WebAIM(2024),每 3 名前庭障碍用户中就有 1 名报告在观看动画文本时感到不适。
经验法则:如果文本可以缩短或放在下一行 — 就这样做。跑马灯是最后的手段,而不是长标签的通用解决方案。
跑马灯是从可访问性角度来看最有问题的 UI 动画之一。移动文本可能引起前庭障碍患者的头晕、恶心和定向障碍。根据 WCAG 2.2,任何持续超过 5 秒的动画都必须具有暂停或关闭机制。
在 iOS 上,使用 UIAccessibility.isReduceMotionEnabled 属性检查系统设置。如果启用了 reduceMotion — 不要启动动画,显示带 ellipsize 的静态文本。
在 Android 上,通过 Settings.Global.ANIMATOR_DURATION_SCALE 检查系统动画。如果全局动画已关闭(duration scale = 0),则不应启用跑马灯。
在 Web 上,使用媒体功能 prefers-reduced-motion:@media (prefers-reduced-motion: reduce) { .marquee-content { animation: none; } }。这是所有现代浏览器都支持的标准 CSS 功能。在 DevTools 中检查:启用 prefers-reduced-motion 模拟并确保动画关闭。
常见问题
Ellipsize 截断文本并放置省略号,隐藏部分内容。Marquee 滚动全部文本,但在运动中。选择取决于优先级:显示所有内容(marquee)或保持静态(ellipsize)。
内置的 android:ellipsize="marquee" 不允许更改速度 — 由系统 ScrollSpeed 决定。对于自定义速度,手动使用 Animation 或 ValueAnimator。
由于视图的重复使用,setSelected(true) 被重置。解决方案:创建重写 isFocused() 并返回 true 的自定义 MarqueeTextView,或在 Adapter.onViewAttachedToWindow() 中使用自定义动画。
可以,通过 translateY 替代 translateX。垂直 marquee 用于致谢(credits)或垂直条带中的新闻行。原理相同,但动画轴发生变化。
在 CSS 中::hover { animation-play-state: paused; }。在 Android 上:通过 OnFocusChangeListener 跟踪焦点并重置 setSelected(false)。在 iOS 上:通过 layer.removeAnimation(forKey:) 移除动画。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。