Timeline — kiến thức cơ bản, quản lý thời gian hoạt hình trong ứng dụng

Tác giả: IT Sectr Đã đăng: 2026-03-02 Thời gian đọc: 8 phút
Timeline (dòng thời gian) là một khái niệm hoạt hình cơ bản xác định thời gian hoạt hình kéo dài, thứ tự thực hiện các giai đoạn và cách thời gian được ánh xạ thành tiến trình. Trong phát triển ứng dụng di động, dòng thời gian được quản lý thông qua duration, delay, repeat và dấu thời gian. Trên iOS, dòng thời gian hoạt hình được triển khai qua CACurrentMediaTime và CAMediaTiming, trong khi trên Android sử dụng ValueAnimator và Animator với các tham số duration, startDelay và repeatCount. Theo Apple Documentation, việc kiểm soát dòng thời gian chính xác là cực kỳ quan trọng cho đồng bộ hóa hoạt hình: sự mất đồng bộ chỉ 16 ms (một khung hình ở 60fps) được người dùng cảm nhận như hiện tượng giật. Tại IT Sectr, chúng tôi đã chuẩn hóa việc sử dụng dòng thời gian thông qua ValueAnimator trên Android và CABasicAnimation với CAMediaTiming trên iOS — điều này đảm bảo quản lý thời gian đồng nhất trong tất cả các dự án.

Những Điểm Chính

  • Timeline — khái niệm quản lý thời gian hoạt hình: duration, delay, repeat, ánh xạ thời gian thành tiến trình.
  • CAMediaTiming — giao thức iOS Core Animation để quản lý dòng thời gian: duration, beginTime, repeatCount, speed, timeOffset.
  • ValueAnimator — lớp trung tâm của Android để làm việc với dòng thời gian hoạt hình thông qua duration, startDelay, repeatCount.
  • CACurrentMediaTime — hàm iOS để lấy thời gian tuyệt đối tính bằng giây, được sử dụng để đồng bộ hóa.
  • TimeInterval — kiểu dữ liệu trong iOS (Double) biểu thị khoảng thời gian cho tính toán hoạt hình.

Timeline là gì?

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.

Timeline trên iOS: CAMediaTiming và CACurrentMediaTime

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.

Timeline trên Android: Animator và duration

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.

Kết hợp hoạt hình trong thời gian

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.

Ví dụ mã

iOS: CABasicAnimation với CAMediaTiming

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.

swift
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.

iOS: TimelineView cho Dòng thời gian Tùy chỉnh

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.

swift
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.

Android: ValueAnimator với Dòng thời gian Tùy chỉnh

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.

kotlin
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.

Android: AnimatorSet để Kết hợp Dòng thời gian

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).

kotlin
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 khác ValueAnimator trong Android như thế nào?

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.

Làm thế nào để đồng bộ hóa hoạt hình trên các thiết bị khác nhau?

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 và repeatCount trong Animator là gì?

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.

Thời lượng hoạt hình tối ưu là bao nhiêu?

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).

Tại sao hoạt hình bị giật trong khi phát lại?

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

  • Timeline là một mô hình thời gian hoạt hình với các tham số duration, startDelay, repeatCount, repeatMode xác định luồng hoạt hình.
  • Giao thức iOS CAMediaTiming quản lý dòng thời gian thông qua duration, beginTime, speed, timeOffset, repeatCount, fillMode.
  • Android Animator và ValueAnimator cung cấp duration, startDelay, repeatCount, repeatMode và setCurrentPlayTime để kiểm soát trực tiếp.
  • AnimatorSet trong Android và CAAnimationGroup trong iOS cho phép kết hợp các hoạt hình thành một dòng thời gian duy nhất.
  • TimelineView trong SwiftUI (iOS 15+) cung cấp quyền truy cập vào bộ lập lịch hệ thống cho các dòng thời gian tùy chỉnh mà không cần API Animation.
  • CACurrentMediaTime() trên iOS và System.nanoTime trên Android là các dấu thời gian cơ sở để đồng bộ hóa hoạt hình.
  • Thời lượng tối ưu cho hoạt hình giao diện là 200-350 ms theo khuyến nghị của Material Design và HIG.

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.

Thảo luận dự án

Đọc thêm