Những Điểm Chính
Timeline là một mô hình thời gian hoạt hình xác định khi nào hoạt hình bắt đầu, kéo dài bao lâu và cách tiến trình thời gian được chuyển đổi thành thay đổi của tham số hoạt hình. Không giống như hoạt hình từng khung hình (frame-by-frame), nơi mỗi khung hình được đặt thủ công, hoạt hình dòng thời gian được mô tả bằng các tham số: duration — tổng thời lượng, startDelay — độ trễ trước khi bắt đầu, repeatCount — số lần lặp lại, và timingFunction — đường cong tốc độ.
Dòng thời gian hoạt động trong không gian chuẩn hóa [0, 1] — thời gian cục bộ hoặc tiến trình. Giá trị 0 tương ứng với đầu hoạt hình, 1 với kết thúc. Hàm easing (làm mượt) chuyển đổi thời gian cục bộ tuyến tính thành tiến trình hoạt hình thực tế. Nếu repeatCount > 1, dòng thời gian quay vòng (RESTART) hoặc đảo ngược (REVERSE). Trên iOS, CAMediaTiming thêm speed (tốc độ phát lại, >1 tăng tốc) và timeOffset (dịch chuyển dòng thời gian). Trên Android, Animator sử dụng setCurrentPlayTime() để kiểm soát trực tiếp vị trí dòng thời gian. Theo Material Design, hoạt hình ngắn hơn 100 ms không được cảm nhận là hoạt hình, trong khi dài hơn 500 ms bắt đầu gây khó chịu. Thời lượng tối ưu cho hoạt hình giao diện là 200–350 ms.
Trên iOS, việc quản lý dòng thời gian hoạt hình được thực hiện thông qua giao thức CAMediaTiming, được triển khai bởi CAAnimation và CALayer. Nó cung cấp các thuộc tính: duration (tính bằng giây), beginTime (thời gian bắt đầu so với dòng thời gian cha), repeatCount (số lần lặp lại), repeatDuration (tổng thời lượng lặp lại), autoreverses (tự động đảo ngược), fillMode (hành vi trước/sau hoạt hình), speed (tốc độ phát lại), và timeOffset (dịch chuyển dòng thời gian).
CACurrentMediaTime() là một hàm trả về thời gian tuyệt đối tính bằng giây kể từ lần khởi động lại thiết bị cuối cùng. Nó được sử dụng làm dấu thời gian cơ sở để đồng bộ hóa nhiều hoạt hình: đặt beginTime = CACurrentMediaTime() + offset để bắt đầu một nhóm hoạt hình với độ trễ chính xác. CAMediaTiming cũng hỗ trợ phân cấp dòng thời gian — một lớp cha có thể tăng tốc/giảm tốc tất cả hoạt hình con thông qua thuộc tính speed. Trong UIKit, UIViewPropertyAnimator (iOS 10+) trừu tượng hóa dòng thời gian thông qua các thuộc tính duration, delay và các callback startAnimation/pauseAnimation/stopAnimation. Trong SwiftUI, cấu trúc Animation cung cấp duration như một phần của đặc tả hoạt hình, nhưng việc kiểm soát trực tiếp dòng thời gian bị ẩn — đối với dòng thời gian tùy chỉnh, hãy sử dụng TimelineView (iOS 15+) với bộ lập lịch tùy chỉnh.
Trên Android, quản lý dòng thời gian được tập trung trong lớp Animator và các lớp con của nó (ValueAnimator, ObjectAnimator). Các thuộc tính dòng thời gian chính là: duration (tính bằng mili giây), startDelay (độ trễ trước khi bắt đầu), repeatCount (số lần lặp lại), repeatMode (RESTART hoặc REVERSE). Phương thức setCurrentPlayTime(long) cho phép di chuyển dọc theo dòng thời gian đến bất kỳ vị trí nào.
ValueAnimator thêm một lớp trừu tượng: nó tạo ra các giá trị từ 0 đến 1 (thời gian chuẩn hóa) dựa trên duration và truyền chúng đến TimeInterpolator. AnimatorSet cho phép kết hợp nhiều Animator thành một dòng thời gian duy nhất với thực thi tuần tự (playSequentially) hoặc song song (playTogether). Trong Jetpack Compose, dòng thời gian được quản lý thông qua AnimationSpec: tween (durationMillis, delayMillis, easing), spring (dampingRatio, stiffness) và keyframes (các điểm trên dòng thời gian với easing tùy chỉnh cho mỗi đoạn). Compose AnimatedContent sử dụng dòng thời gian cho transitionSpec — nhà phát triển mô tả cách nội dung xuất hiện/biến mất với độ lệch thời gian. Theo Android Performance, dòng thời gian hoạt hình phải là bội số của 16 ms (chu kỳ vsync) cho 60fps — thời lượng không chính xác có thể gây rớt khung hình do không khớp với tần số làm tươi của màn hình.
Dòng thời gian trở nên đặc biệt quan trọng khi kết hợp nhiều hoạt hình. Ba mẫu cơ bản: tầng (cascade) — hoạt hình bắt đầu tuần tự với độ lệch (mỗi cái tiếp theo bắt đầu sau một độ lệch so với cái trước), song song — tất cả hoạt hình bắt đầu đồng thời với cùng một dòng thời gian, và so le (stagger) — hoạt hình bắt đầu chồng lấn (cái thứ hai bắt đầu trước khi cái thứ nhất kết thúc).
Trên iOS, tầng được triển khai thông qua beginTime của mỗi CAAnimation = beginTime trước + duration trước. Đối với so le, CAAnimationGroup được sử dụng với các giá trị beginTime khác nhau trong các hoạt hình lồng nhau. Trên Android, AnimatorSet hỗ trợ playSequentially và playTogether; so le được tạo bằng cách đặt startDelay cho mỗi Animator. Trong Compose, AnimatedVisibility với enter/exitTransition sử dụng so le trong biến thể staggerChildren để hiển thị tầng của các mục danh sách. Tại WWDC và Google I/O 2024, cả hai nền tảng đều giới thiệu các công cụ trực quan hóa dòng thời gian: Xcode Animations Inspector và Android Studio Animations Timeline — chúng hiển thị dòng thời gian thực tế và các khung hình bị rớt trong thời gian thực.
Một ví dụ về hoạt hình với dòng thời gian tùy chỉnh: độ trễ 0,3 giây, thời lượng 0,6 giây, tự động đảo ngược và lặp lại vô hạn. CAMediaTiming kiểm soát tất cả các tham số thời gian.
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 cung cấp độ trễ 300 ms. autoreverses = true làm cho hoạt hình chạy theo hướng ngược lại sau mỗi chu kỳ. repeatCount = .infinity tạo vòng lặp vô hạn của hiệu ứng “thở”. speed = 1,0 — tốc độ bình thường; tăng tốc hoạt hình 2x với giá trị 2,0.
SwiftUI TimelineView (iOS 15+) cung cấp quyền truy cập vào dòng thời gian hệ thống cho hoạt hình tùy chỉnh. View cập nhật mỗi lần đánh dấu của bộ lập lịch.
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 với bộ lập lịch .periodic tạo ra các đánh dấu mỗi 0,1 giây. Trong closure, context.date là thời gian hiện tại, được chuyển đổi thành pha sóng hình sin. Kích thước hình tròn dao động ở tần số 1 Hz. TimelineView là cách duy nhất để tạo hoạt hình với dòng thời gian riêng trong SwiftUI mà không cần API Animation.
ValueAnimator với kiểm soát dòng thời gian đầy đủ: độ trễ, thời lượng, lặp lại. Phương thức setCurrentPlayTime cho phép nhảy dọc theo dòng thời gian.
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 — độ trễ trước khi bắt đầu. repeatMode = REVERSE đưa hoạt hình trở về giá trị ban đầu. animatedFraction cung cấp tiến trình từ 0 đến 1 có tính đến repeatMode. addUpdateListener kích hoạt mỗi khung hình (thường là 16 ms). Sử dụng pause() / resume() để tạm dừng.
AnimatorSet cho phép tạo các dòng thời gian phức tạp từ nhiều hoạt hình. Ví dụ minh họa thực thi tuần tự với chồng lấn (so le).
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 bắt đầu tất cả hoạt hình song song, nhưng mỗi cái có startDelay riêng — điều này tạo hiệu ứng so le: fadeIn bắt đầu đầu tiên (0 ms), slideUp sau 100 ms, scale sau 200 ms. Dòng thời gian kết quả: 0 — 100 — 200 — 600 ms. Để thực thi tuần tự, hãy sử dụng playSequentially.
Các câu hỏi thường gặp
Animator là một lớp cơ sở trừu tượng cho tất cả các hoạt hình. ValueAnimator là lớp con của nó, hoạt hình các giá trị số mà không gắn với một đối tượng. ObjectAnimator (lớp con của ValueAnimator) hoạt hình một thuộc tính cụ thể của đối tượng (ví dụ: alpha hoặc translationY). Đối với dòng thời gian, hãy sử dụng ValueAnimator như một bộ đếm thời gian phổ quát với callback thông qua addUpdateListener.
Sử dụng đồng hồ hệ thống (CACurrentMediaTime trên iOS, System.nanoTime trên Android) để tính tiến trình, không phải số lượng khung hình. Đối với đồng bộ hóa mạng, hãy sử dụng bộ đếm thời gian dựa trên NTP với hiệu chỉnh độ lệch. iOS hỗ trợ AVAudioSession để đồng bộ hóa dòng thời gian âm thanh-hình ảnh với độ chính xác mili giây. Tránh đồng bộ hóa dựa trên khung hình — tốc độ khung hình thay đổi từ 30 đến 120 fps.
startOffset là độ trễ trước khi bắt đầu hoạt hình tính bằng mili giây. Được sử dụng cho hiệu ứng tầng và so le. repeatCount là số lần lặp lại: 0 = không lặp lại, ValueAnimator.INFINITE = vòng lặp vô hạn. repeatMode xác định hướng: RESTART (đặt lại về đầu) hoặc REVERSE (hướng ngược lại). Trên iOS, startOffset được gọi là beginTime (kết hợp với CACurrentMediaTime), repeatCount là thuộc tính tương tự của CABasicAnimation.
Material Design khuyến nghị 200–350 ms cho hoạt hình giao diện. Hoạt hình ngắn hơn 100 ms không được cảm nhận là hoạt hình, trong khi dài hơn 500 ms gây khó chịu. Đối với vào/ra của các phần tử: 200–300 ms. Đối với chuyển tiếp giữa các màn hình: 300–400 ms. Sử dụng thời lượng ngắn hơn cho các phần tử nhỏ (100–200 ms) và dài hơn cho các phần tử lớn (400–500 ms).
Hiện tượng giật (jank) xảy ra do: các tác vụ chạy dài trên luồng chính(chặn kết xuất), phân cấp View/Layer phức tạp với overdraw, hoặc sử dụng dòng thời gian không đúng (thời lượng không phải bội số của vsync — 16ms cho 60fps, 8ms cho 120fps). Sử dụng Xcode Instruments Animations hoặc Android Studio Profiler để phát hiện các khung hình bị rớt. Trong Compose, hãy sử dụng Modifier.drawWithCache để lưu vào bộ nhớ đệm các bản vẽ phức tạp.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm