Рухомий рядок (marquee) — це візуальний ефект, за якого текст автоматично прокручується по горизонталі в межах обмеженої області, дозволяючи відображати довгі рядки в компактному контейнері. Історично термін походить від HTML-тега <marquee>, популярного у вебі 1990-х років. У сучасній мобільній розробці scrolling text використовується в плеєрах, новинних рядках і панелях статусу. Згідно з Apple Human Interface Guidelines і Google Material Design, рухомий рядок допустимий лише в обмежених сценаріях, де статичне обрізання з трьома крапками погіршує UX.
Головне
Рухомий рядок — це UI-ефект, за якого текст рухається по горизонталі всередині фіксованої області, циклічно прокручуючись, щоб користувач міг прочитати весь вміст, який не вміщується в контейнер. На відміну від ellipsize, який приховує частину тексту, marquee показує весь текст, але в русі.
Термін marquee (англ. — «шатро», «рекламний щит») закріпився завдяки HTML-тегу <marquee>, який був доданий у ранніх версіях браузерів Netscape та Internet Explorer. Тег дозволяв створювати прокручуваний текст без JavaScript і CSS, але ніколи не був частиною стандарту HTML. Сучасні браузери продовжують підтримувати його для зворотної сумісності, але використовувати його не рекомендується.
У мобільній розробці рухомий рядок вирішує проблему довгих підписів у компактних контейнерах: назви треків у плеєрі, курси валют, новинні рядки на телеекранах. Однак через анімацію вона створює навантаження на accessibility і може дратувати користувачів — тому застосовується вибірково.
За даними досліджень 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: activate 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 рухомий рядок будується на трьох елементах: контейнер UILabel з clipsToBounds = true, сам текст і 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 — анімація виглядає млявою. Змінюйте швидкість під конкретний сценарій.
У SwiftUI рухомий рядок реалізується простіше — через модифікатор offset() із repeatable анімацією та .animation(.linear(duration:).repeatForever(autoreverses: false)). SwiftUI автоматично керує життєвим циклом анімації, але вимагає GeometryReader для визначення ширини тексту.
У вебі застарілий тег <marquee> не рекомендується до використання, хоча браузери продовжують його підтримувати. Сучасна реалізація рухомого рядка — CSS-анімація з ключовими кадрами та transform: translateX.
Переваги CSS-підходу: повний контроль над швидкістю, паузою та кривою анімації, підтримка prefers-reduced-motion для accessibility, відсутність семантично некоректного тега.
.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%) дорівнюватиме ширині контейнера, а не тексту, і анімація зламається.
Для accessibility додайте @media (prefers-reduced-motion: reduce) { .marquee-content { animation: none; } } — користувачі з вестибулярними розладами побачать статичний текст із можливістю скролу.
Рухомий рядок виправданий у сценаріях, де користувач не може самостійно розширити контейнер, а повний текст критично важливий: поточний трек у плеєрі на заблокованому екрані, новинний рядок на телевізійній панелі, рухомий рядок у додатку для таксі (статус замовлення).
Рухомий рядок не варто використовувати у таблицях, списках, формах і діалогах — усюди, де користувач активно взаємодіє з інтерфейсом. Анімація відволікає від мети, збільшує час пошуку елемента та створює дискомфорт. За даними WebAIM (2024), 1 із 3 користувачів із вестибулярними розладами повідомляє про дискомфорт під час перегляду анімованого тексту.
Правило великого пальця: якщо текст можна скоротити або помістити на наступний рядок — зробіть це. Рухомий рядок — крайній захід, а не універсальне рішення для довгих підписів.
Рухомий рядок — одна з найбільш проблемних UI-анімацій з точки зору accessibility. Рухомий текст може викликати запаморочення, нудоту та дезорієнтацію в людей із вестибулярними розладами. Згідно з WCAG 2.2, будь-яка анімація, яка триває більше 5 секунд, повинна мати механізм паузи або вимкнення.
На iOS використовуйте властивість UIAccessibility.isReduceMotionEnabled для перевірки системного налаштування. Якщо reduceMotion увімкнено — не запускайте анімацію, покажіть статичний текст із ellipsize.
На Android перевіряйте системну анімацію через Settings.Global.ANIMATOR_DURATION_SCALE. Якщо глобальна анімація вимкнена (duration scale = 0), рухомий рядок вмикати не потрібно.
У вебі використовуйте медіа-функцію 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) скидається. Рішення: створіть кастомний MarqueeTextView, який перевизначає isFocused() і повертає true, або використовуйте кастомну анімацію в Adapter.onViewAttachedToWindow().
Так, через translateY замість translateX. Вертикальний marquee використовується для титрів (credits) або новинних рядків у вертикальних стрічках. Принцип той самий, але змінюється вісь анімації.
У CSS: :hover { animation-play-state: paused; }. На Android: відстежуйте фокус через OnFocusChangeListener і скидайте setSelected(false). На iOS: видаліть анімацію через layer.removeAnimation(forKey:).
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також