ข้อความวิ่ง (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 เบราว์เซอร์สมัยใหม่ยังคงรองรับเพื่อความเข้ากันได้ย้อนหลัง แต่ไม่แนะนำให้ใช้งาน
ในการพัฒนาแอปพลิเคชัน marquee แก้ปัญหาป้ายข้อความยาวในคอนเทนเนอร์ขนาดเล็ก: ชื่อเพลงในเครื่องเล่น ราคาสกุลเงิน ข่าววิ่งบนหน้าจอทีวี อย่างไรก็ตาม เนื่องจากแอนิเมชัน มันสร้างภาระต่อการเข้าถึงและอาจรบกวนผู้ใช้ จึงใช้อย่าง selective
จากการวิจัยของ Nielsen Norman Group (2023) ข้อความที่เคลื่อนที่ ลดความเร็วในการอ่านลง 15-20% และเพิ่มภาระทางปัญญา ใช้ marquee เฉพาะเมื่อการตัดแบบคงที่ (ellipsize) ไม่สามารถยอมรับได้จริง ๆ
บน Android marquee ถูกใช้งานผ่านกลไกในตัวของ 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" marquee ถูกใช้งานด้วยตนเองผ่าน Core Animation หรือแอนิเมชัน SwiftUI วิธีที่พบบ่อยที่สุดคือการทำแอนิเมชัน layer.position ร่วมกับคอนเทนเนอร์ตัดขอบ
ใน UIKit marquee สร้างจากสามองค์ประกอบ: คอนเทนเนอร์ 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 marquee ใช้งานได้ง่ายกว่า — ผ่านตัวปรับแต่ง offset() ด้วยแอนิเมชันที่ทำซ้ำได้และ .animation(.linear(duration:).repeatForever(autoreverses: false)) SwiftUI จัดการวงจรชีวิตแอนิเมชันโดยอัตโนมัติแต่ต้องใช้ GeometryReader เพื่อกำหนดความกว้างของข้อความ
บนเว็บ แท็ก <marquee> ที่เลิกใช้แล้ว ไม่แนะนำ ให้ใช้ แม้ว่าเบราว์เซอร์จะยังคงรองรับ การใช้งาน marquee สมัยใหม่คือ CSS แอนิเมชันด้วย keyframes และ transform: translateX
ข้อดีของวิธี 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; } } — ผู้ใช้ที่มีความผิดปกติของ vestibular จะเห็นข้อความคงที่พร้อมความสามารถในการเลื่อน
marquee สมเหตุสมผล ในสถานการณ์ที่ผู้ใช้ไม่สามารถขยายคอนเทนเนอร์ได้ด้วยตนเองและข้อความเต็มมีความสำคัญมาก: เพลงปัจจุบันในเครื่องเล่นบนหน้าจอล็อก ข่าววิ่งบนแผงทีวี หรือ marquee ในแอปเรียกแท็กซี่ (สถานะคำสั่งซื้อ)
marquee ไม่ควรใช้ ในตาราง รายการ ฟอร์ม และไดอะล็อก — ทุกที่ที่ผู้ใช้โต้ตอบกับอินเทอร์เฟซอย่างแข็งขัน แอนิเมชันเบี่ยงเบนความสนใจจากเป้าหมาย เพิ่มเวลาในการค้นหาองค์ประกอบ และสร้างความไม่สบาย ตาม WebAIM (2024) 1 ใน 3 ผู้ใช้ที่มีความผิดปกติของ vestibular รายงานความไม่สบายเมื่อดูข้อความเคลื่อนไหว
กฎทั่วไป: หากสามารถย่อข้อความหรือตัดไปบรรทัดถัดไปได้ — ทำเลย marquee เป็นทางเลือกสุดท้าย ไม่ใช่ทางออกสากลสำหรับป้ายข้อความยาว
marquee เป็นหนึ่งในแอนิเมชัน UI ที่ มีปัญหามากที่สุด จากมุมมองการเข้าถึง ข้อความเคลื่อนที่อาจทำให้เกิดอาการวิงเวียน คลื่นไส้ และสับสนในผู้ที่มีความผิดปกติของ vestibular ตาม WCAG 2.2 แอนิเมชันใด ๆ ที่นานกว่า 5 วินาทีต้องมีกลไกหยุดชั่วคราวหรือปิด
บน iOS ใช้คุณสมบัติ UIAccessibility.isReduceMotionEnabled เพื่อตรวจสอบการตั้งค่าระบบ หากเปิด reduceMotion ไว้ — อย่าเริ่มแอนิเมชัน แสดงข้อความคงที่ด้วย ellipsize
บน Android ตรวจสอบแอนิเมชันของระบบผ่าน Settings.Global.ANIMATOR_DURATION_SCALE หากแอนิเมชันทั่วโลกถูกปิด (duration scale = 0) ไม่ควรเปิดใช้ marquee
บนเว็บ ใช้มีเดียฟีเจอร์ 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 แนวตั้งใช้สำหรับเครดิตหรือข่าววิ่งในฟีดแนวตั้ง หลักการเหมือนกันแต่แกนแอนิเมชันเปลี่ยนไป
ใน CSS: :hover { animation-play-state: paused; } บน Android: ติดตามโฟกัสผ่าน OnFocusChangeListener และรีเซ็ต setSelected(false) บน iOS: ลบแอนิเมชันผ่าน layer.removeAnimation(forKey:)
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ