ประเด็นสำคัญ
Timeline คือแบบจำลองเวลาแอนิเมชันที่กำหนดว่าแอนิเมชันเริ่มต้นเมื่อใด อยู่นานเท่าใด และความก้าวหน้าของเวลาถูกแปลงเป็นการเปลี่ยนแปลงของพารามิเตอร์ที่ถูกทำให้เคลื่อนไหวอย่างไร แตกต่างจากแอนิเมชันแบบเฟรมต่อเฟรม (frame-by-frame) ซึ่งแต่ละเฟรมถูกตั้งค่าด้วยตนเอง แอนิเมชันแบบเส้นเวลาจะถูกอธิบายด้วยพารามิเตอร์: duration — ระยะเวลาทั้งหมด, startDelay — หน่วงเวลาก่อนเริ่ม, repeatCount — จำนวนการทำซ้ำ และ timingFunction — เส้นโค้งความเร็ว
เส้นเวลาทำงานในพื้นที่ปกติ [0, 1] — เวลาท้องถิ่นหรือความคืบหน้า ค่า 0 สอดคล้องกับจุดเริ่มต้นของแอนิเมชัน ค่า 1 สอดคล้องกับการเสร็จสิ้น ฟังก์ชัน easing (การทำให้เรียบ) แปลงเวลาท้องถิ่นเชิงเส้นเป็นความคืบหน้าแอนิเมชันจริง ถ้า repeatCount > 1 เส้นเวลาจะวนซ้ำ (RESTART) หรือกลับทิศ (REVERSE) ใน iOS CAMediaTiming เพิ่ม speed (อัตราการเล่น, >1 เร่งความเร็ว) และ timeOffset (การเลื่อนเส้นเวลา) ใน Android Animator ใช้ setCurrentPlayTime() สำหรับควบคุมตำแหน่งเส้นเวลาโดยตรง ตาม Material Design แอนิเมชันที่สั้นกว่า 100 ms จะไม่ถูกรับรู้ว่าเป็นแอนิเมชัน ในขณะที่แอนิเมชันที่ยาวกว่า 500 ms จะเริ่มทำให้เกิดความรำคาญ ระยะเวลาที่เหมาะสมที่สุดสำหรับแอนิเมชันอินเทอร์เฟซคือ 200–350 ms
ใน iOS การจัดการเส้นเวลาแอนิเมชันดำเนินการผ่านโพรโทคอล CAMediaTiming ซึ่งถูกนำไปใช้โดย CAAnimation และ CALayer มันให้คุณสมบัติ: duration (เป็นวินาที), beginTime (เวลาเริ่มต้นสัมพันธ์กับเส้นเวลาแม่), repeatCount (จำนวนการทำซ้ำ), repeatDuration (ระยะเวลาการทำซ้ำทั้งหมด), autoreverses (การกลับทิศอัตโนมัติ), fillMode (พฤติกรรมก่อน/หลังแอนิเมชัน), speed (อัตราการเล่น) และ timeOffset (การเลื่อนเส้นเวลา)
CACurrentMediaTime() เป็นฟังก์ชันที่คืนเวลาสัมบูรณ์เป็นวินาทีตั้งแต่การรีบูตอุปกรณ์ครั้งล่าสุด ใช้เป็นประทับเวลาพื้นฐานสำหรับการซิงโครไนซ์แอนิเมชันหลายรายการ: ตั้งค่า beginTime = CACurrentMediaTime() + offset เพื่อเริ่มกลุ่มแอนิเมชันด้วยหน่วงเวลาที่แม่นยำ CAMediaTiming ยังรองรับลำดับชั้นของเส้นเวลา — เลเยอร์แม่สามารถเร่ง/ชะลอแอนิเมชันลูกทั้งหมดผ่านคุณสมบัติ speed ใน UIKit UIViewPropertyAnimator (iOS 10+) ทำให้เส้นเวลาเป็นนามธรรมผ่านคุณสมบัติ duration, delay และ callbacks startAnimation/pauseAnimation/stopAnimation ใน SwiftUI โครงสร้าง Animation ให้ duration เป็นส่วนหนึ่งของข้อกำหนดแอนิเมชัน แต่การควบคุมเส้นเวลาโดยตรงถูกซ่อนไว้ — สำหรับเส้นเวลาแบบกำหนดเอง ให้ใช้ TimelineView (iOS 15+) พร้อมกับ scheduler ที่กำหนดเอง
ใน Android การจัดการเส้นเวลาถูกรวมศูนย์ในคลาส Animator และคลาสย่อย (ValueAnimator, ObjectAnimator) คุณสมบัติเส้นเวลาหลักคือ: duration (เป็นมิลลิวินาที), startDelay (หน่วงเวลาก่อนเริ่ม), repeatCount (จำนวนการทำซ้ำ), repeatMode (RESTART หรือ REVERSE) เมธอด setCurrentPlayTime(long) อนุญาตให้นำทางไปยังตำแหน่งใดๆ บนเส้นเวลา
ValueAnimator เพิ่มชั้นนามธรรม: มันสร้างค่าจาก 0 ถึง 1 (เวลาปกติ) ตาม duration และส่งต่อไปยัง TimeInterpolator AnimatorSet อนุญาตให้รวมหลาย Animator เข้าเป็นเส้นเวลาเดียวด้วยการดำเนินการตามลำดับ (playSequentially) หรือขนาน (playTogether) ใน Jetpack Compose เส้นเวลาถูกจัดการผ่าน AnimationSpec: tween (durationMillis, delayMillis, easing), spring (dampingRatio, stiffness) และ keyframes (จุดบนเส้นเวลาพร้อม easing ที่กำหนดเองสำหรับแต่ละส่วน) Compose AnimatedContent ใช้เส้นเวลาสำหรับ transitionSpec — นักพัฒนาอธิบายว่าเนื้อหาปรากฏ/หายไปอย่างไรพร้อมการเลื่อนเวลา ตาม Android Performance เส้นเวลาแอนิเมชันควรเป็นพหุคูณของ 16 ms (รอบ vsync) สำหรับ 60fps — ระยะเวลาที่ไม่ถูกต้องอาจทำให้เฟรมดรอปเนื่องจากการไม่สอดคล้องกับอัตรารีเฟรชของจอแสดงผล
เส้นเวลามีความสำคัญเป็นพิเศษเมื่อประกอบแอนิเมชันหลายรายการ สามรูปแบบพื้นฐาน: แบบเรียงซ้อน (cascade) — แอนิเมชันเริ่มต้นตามลำดับโดยมีการเลื่อน (แต่ละอันถัดไปเริ่มต้นหลังจากออฟเซ็ตจากอันก่อนหน้า), แบบขนาน — แอนิเมชันทั้งหมดเริ่มต้นพร้อมกันด้วยเส้นเวลาเดียวกัน, และ แบบเหลื่อม (stagger) — แอนิเมชันเริ่มต้นโดยทับซ้อนกัน (อันที่สองเริ่มก่อนที่อันแรกจะเสร็จ)
ใน iOS การเรียงซ้อนถูกนำไปใช้ผ่าน beginTime ของแต่ละ CAAnimation = beginTime ก่อนหน้า + duration ก่อนหน้า สำหรับแบบเหลื่อม CAAnimationGroup ถูกใช้ด้วยค่า beginTime ที่แตกต่างกันในแอนิเมชันที่ซ้อนกัน ใน Android AnimatorSet รองรับ playSequentially และ playTogether; การเหลื่อมถูกสร้างขึ้นโดยการตั้งค่า startDelay สำหรับแต่ละ Animator ใน Compose AnimatedVisibility กับ enter/exitTransition ใช้การเหลื่อมในตัวแปร staggerChildren สำหรับการปรากฏแบบเรียงซ้อนของรายการรายการ ใน WWDC และ Google I/O 2024 ทั้งสองแพลตฟอร์มได้แนะนำเครื่องมือการแสดงภาพเส้นเวลา: Xcode Animations Inspector และ Android Studio Animations Timeline — พวกมันแสดงเส้นเวลาจริงและเฟรมที่ตกในเวลาจริง
ตัวอย่างแอนิเมชันที่มีเส้นเวลาแบบกำหนดเอง: หน่วงเวลา 0.3 วินาที, ระยะเวลา 0.6 วินาที, การกลับทิศอัตโนมัติ และการทำซ้ำไม่สิ้นสุด CAMediaTiming ควบคุมพารามิเตอร์เวลาทั้งหมด
import UIKit
func breathingAnimation(layer: CALayer) {
let animation = CABasicAnimation(keyPath: "opacity")
animation.duration = 0.6
animation.beginTime = CACurrentMediaTime() + 0.3
animation.fromValue = 1.0
animation.toValue = 0.3
animation.autoreverses = true
animation.repeatCount = .infinity
animation.speed = 1.0
animation.fillMode = .both
layer.add(animation, forKey: "breathing")
}
beginTime = CACurrentMediaTime() + 0.3 ให้หน่วงเวลา 300 ms autoreverses = true ทำให้แอนิเมชันทำงานในทิศทางกลับกันหลังจากแต่ละรอบ repeatCount = .infinity สร้างลูปไม่สิ้นสุดของเอฟเฟกต์ “การหายใจ” speed = 1.0 — ความเร็วปกติ; เร่งแอนิเมชัน 2 เท่าด้วยค่า 2.0
SwiftUI TimelineView (iOS 15+) ให้การเข้าถึงเส้นเวลาของระบบสำหรับแอนิเมชันแบบกำหนดเอง View จะอัปเดตทุกครั้งที่ scheduler สัญญาณ
import SwiftUI
struct TimelineViewExample: View {
var body: some View {
TimelineView(.periodic(from: .now, by: 0.1)) { context in
let seconds = context.date.timeIntervalSinceReferenceDate
let phase = sin(seconds * 2 * .pi)
Circle()
.fill(Color.blue)
.frame(width: 60 + 30 * phase)
.animation(.linear(duration: 0), value: phase)
}
}
}
TimelineView กับ scheduler .periodic สร้างสัญญาณทุก 0.1 วินาที ใน closure context.date คือเวลาปัจจุบัน ซึ่งถูกแปลงเป็นเฟสคลื่นไซน์ ขนาดของวงกลมเต้นเป็นจังหวะที่ความถี่ 1 Hz TimelineView เป็นวิธีเดียวในการสร้างแอนิเมชันที่มีเส้นเวลาของตัวเองใน SwiftUI โดยไม่มี Animation API
ValueAnimator พร้อมการควบคุมเส้นเวลาเต็มรูปแบบ: หน่วงเวลา, ระยะเวลา, การทำซ้ำ เมธอด setCurrentPlayTime อนุญาตให้กระโดดไปตามเส้นเวลา
import android.animation.ValueAnimator
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.View
fun View.timelineAnimation() {
ValueAnimator.ofFloat(0f, 1f).apply {
duration = 400
startDelay = 200
repeatCount = 2
repeatMode = ValueAnimator.REVERSE
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener { animator ->
val progress = animator.animatedFraction
alpha = progress
translationX = progress * 100f
}
start()
}
}
startDelay = 200 ms — หน่วงเวลาก่อนเริ่ม repeatMode = REVERSE ทำให้แอนิเมชันกลับไปยังค่าเริ่มต้น animatedFraction ให้ความคืบหน้าจาก 0 ถึง 1 โดยคำนึงถึง repeatMode addUpdateListener ทำงานทุกเฟรม (โดยทั่วไป 16 ms) ใช้ pause() / resume() เพื่อหยุดชั่วคราว
AnimatorSet อนุญาตให้สร้างเส้นเวลาที่ซับซ้อนจากหลายแอนิเมชัน ตัวอย่างแสดงการดำเนินการตามลำดับพร้อมการทับซ้อน (stagger)
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
fun View.staggerAnimation() {
val fadeIn = ObjectAnimator.ofFloat(this, "alpha", 0f, 1f).apply {
duration = 300
}
val slideUp = ObjectAnimator.ofFloat(this, "translationY", 100f, 0f).apply {
duration = 400
startDelay = 100
}
val scale = ObjectAnimator.ofFloat(this, "scaleX", 0.8f, 1f).apply {
duration = 250
startDelay = 200
}
AnimatorSet().apply {
playTogether(fadeIn, slideUp, scale)
start()
}
}
playTogether เริ่มแอนิเมชันทั้งหมดพร้อมกัน แต่ละอันมี startDelay ของตัวเอง — สิ่งนี้สร้างเอฟเฟกต์การเหลื่อม: fadeIn เริ่มก่อน (0 ms), slideUp หลังจาก 100 ms, scale หลังจาก 200 ms เส้นเวลาที่ได้: 0 — 100 — 200 — 600 ms สำหรับการดำเนินการตามลำดับ ให้ใช้ playSequentially
คำถามที่พบบ่อย
Animator คือ คลาสพื้นฐานนามธรรม สำหรับแอนิเมชันทั้งหมด ValueAnimator คือคลาสย่อยที่ทำให้ค่าตัวเลขเคลื่อนไหวโดยไม่ผูกกับวัตถุ ObjectAnimator (คลาสย่อยของ ValueAnimator) ทำให้คุณสมบัติเฉพาะของวัตถุเคลื่อนไหว (เช่น alpha หรือ translationY) สำหรับเส้นเวลา ให้ใช้ ValueAnimator เป็นตัวจับเวลาสากลพร้อม callback ผ่าน addUpdateListener
ใช้ นาฬิการะบบ (CACurrentMediaTime ใน iOS, System.nanoTime ใน Android) เพื่อคำนวณความคืบหน้า ไม่ใช่จำนวนเฟรม สำหรับการซิงโครไนซ์เครือข่าย ให้ใช้ตัวจับเวลาแบบ NTP พร้อมการแก้ไขออฟเซ็ต iOS รองรับ AVAudioSession สำหรับการซิงโครไนซ์เส้นเวลาเสียง-วิดีโอด้วยความแม่นยำระดับมิลลิวินาที หลีกเลี่ยงการซิงโครไนซ์ตามเฟรม — อัตราเฟรมแตกต่างกันตั้งแต่ 30 ถึง 120 fps
startOffset คือหน่วงเวลาก่อนเริ่มแอนิเมชันเป็นมิลลิวินาที ใช้สำหรับเอฟเฟกต์แบบเรียงซ้อนและแบบเหลื่อม repeatCount คือจำนวนการทำซ้ำ: 0 = ไม่ทำซ้ำ, ValueAnimator.INFINITE = ลูปไม่สิ้นสุด repeatMode กำหนดทิศทาง: RESTART (รีเซ็ตเป็นจุดเริ่มต้น) หรือ REVERSE (ทิศทางย้อนกลับ) ใน iOS startOffset เรียกว่า beginTime (ร่วมกับ CACurrentMediaTime), repeatCount เป็นคุณสมบัติเดียวกันของ CABasicAnimation
Material Design แนะนำ 200–350 ms สำหรับแอนิเมชันอินเทอร์เฟซ แอนิเมชันที่สั้นกว่า 100 ms จะไม่ถูกรับรู้ว่าเป็นแอนิเมชัน ในขณะที่ยาวกว่า 500 ms จะทำให้รำคาญ สำหรับ การเข้า/ออก ขององค์ประกอบ: 200–300 ms สำหรับ การเปลี่ยนผ่าน ระหว่างหน้าจอ: 300–400 ms ใช้ระยะเวลาที่สั้นกว่าสำหรับองค์ประกอบขนาดเล็ก (100–200 ms) และยาวกว่าสำหรับองค์ประกอบขนาดใหญ่ (400–500 ms)
การกระตุก (jank) เกิดขึ้นเนื่องจาก: การดำเนินการที่ใช้เวลานานในเธรดหลัก (บล็อกการเรนเดอร์), ลำดับชั้น View/Layer ที่ซับซ้อนพร้อม overdraw, หรือการใช้เส้นเวลาไม่ถูกต้อง (ระยะเวลาไม่เป็นพหุคูณของ vsync — 16ms สำหรับ 60fps, 8ms สำหรับ 120fps) ใช้ Xcode Instruments Animations หรือ Android Studio Profiler เพื่อตรวจจับเฟรมที่ตก ใน Compose ใช้ Modifier.drawWithCache เพื่อแคชการวาดที่ซับซ้อน
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ