Покретна трака (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: активирај 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 у комбинацији са clipping контејнером.
У 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 — анимација изгледа споро. Мењајте speed према конкретном сценарију.
У SwiftUI-ју покретна трака се имплементира једноставније — кроз модификатор offset() са анимацијом која се понавља и .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 ручно.
Због поновне употребе view-ова, 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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође