Shared Element Transition — nó là gì, các phần tử dùng chung trong chuyển tiếp

Tác giả: IT Sectr Đã đăng: 2026-03-02 Thời gian đọc: 10 phút

Shared Element Transition là một hoạt ảnh trong đó một phần tử dùng chung (hình ảnh, văn bản, thẻ) di chuyển và biến đổi mượt mà giữa hai màn hình trong quá trình điều hướng. Thay vì chuyển tiếp đột ngột, người dùng thấy chuyển động liên tục: phần tử từ màn hình A chuyển sang màn hình B, giữ nguyên ngữ cảnh và kết nối trực quan. Theo Material Design Guidelines (2026), Shared Element Transition tăng hiệu suất cảm nhận lên 35% và cải thiện khả năng hiểu điều hướng lên 28%. Tìm hiểu thêm về các loại hoạt ảnh khác trong hướng dẫn hoạt ảnh chung.

Những điểm chính

  • Shared Element Transition — hoạt ảnh của một phần tử dùng chung giữa hai màn hình trong quá trình điều hướng.
  • sharedElementEnterTransition — thuộc tính XML Android để cấu hình đầu vào của phần tử dùng chung.
  • UIViewPropertyAnimator — nền tảng cho các hoạt ảnh dùng chung tùy chỉnh giữa các UIViewController trong iOS.
  • Phần tử dùng chung được xác định qua transitionName trong Android và tag hoặc snapshot trong iOS.
  • Hoạt ảnh hoạt động với ImageView, TextView, CardView, Button và bất kỳ View nào có định danh duy nhất.

Shared Element Transition là gì?

Shared Element Transition là một hoạt ảnh chuyển tiếp màn hình trong đó một hoặc nhiều phần tử trực quan di chuyển từ màn hình trước sang màn hình tiếp theo. Tại thời điểm điều hướng, hệ thống chụp ảnh nhanh của phần tử trên màn hình nguồn, tạo hoạt ảnh biến đổi (vị trí, kích thước, hình dạng, màu sắc) đến trạng thái đích trên màn hình mới. Kết quả là người dùng cảm nhận hai màn hình như các phần của một không gian duy nhất thay vì các trang riêng biệt.

Các trường hợp sử dụng chính: thẻ sản phẩm → màn hình chi tiết (hình ảnh phóng to và di chuyển lên trên), thư viện ảnh → trình xem (hình ảnh mở rộng mượt mà ra toàn màn hình), danh sách liên hệ → hồ sơ (hình đại diện và tên di chuyển). Để hoạt động chính xác, cả hai màn hình phải có Views với cùng transitionName (Android) hoặc định danh snapshot (iOS).

Theo Google I/O 2025, Shared Element Transition nằm trong ba mẫu UX hiệu quả nhất cho ứng dụng di động: người dùng có khả năng quay lại ngay sau khi chuyển tiếp thấp hơn 22% nếu họ thấy hoạt ảnh phần tử dùng chung. Tuy nhiên, sử dụng quá mức (hơn 3 phần tử dùng chung mỗi lần chuyển tiếp) làm giảm hiệu quả và có thể khiến FPS giảm xuống dưới 40 trên các thiết bị tầm trung.

Shared Element Transition trong Android (Kotlin)

Trong Android, Shared Element Transition được triển khai qua FragmentTransaction.addSharedElement(view, transitionName) hoặc ActivityOptions.makeSceneTransitionAnimation. Mỗi View được gán một android:transitionName duy nhất — giống nhau trên cả hai màn hình. Hệ thống tự động tạo hoạt ảnh di chuyển (ChangeBounds), hoạt ảnh thay đổi kích thước (ChangeTransform) và hoạt ảnh chia tỷ lệ hình ảnh (ChangeImageTransform).

xml

<androidx.cardview.widget.CardView
    android:id="@+id/itemCard"
    android:transitionName="shared_element_@{item.id}"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <ImageView
        android:id="@+id/itemImage"
        android:transitionName="shared_image_@{item.id}" />
</androidx.cardview.widget.CardView>


<ImageView
    android:id="@+id/detailImage"
    android:transitionName="shared_image_@{item.id}"
    android:layout_width="match_parent"
    android:layout_height="300dp" />
kotlin
import androidx.core.app.ActivityOptionsCompat
import androidx.core.util.Pair
import androidx.fragment.app.FragmentTransaction

// Activity: Shared Element qua ActivityOptions
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
    this,
    Pair(imageView, getString(R.string.transition_name_image)),
    Pair(titleView, getString(R.string.transition_name_title))
)
startActivity(intent, options.toBundle())

// Fragment: Shared Element qua FragmentTransaction
val detailFragment = DetailFragment().apply {
    sharedElementEnterTransition = TransitionInflater.from(context)
        .inflateTransition(android.R.transition.move)
    sharedElementReturnTransition = TransitionInflater.from(context)
        .inflateTransition(android.R.transition.move)
}

supportFragmentManager.beginTransaction()
    .addSharedElement(imageView, imageView.transitionName)
    .addSharedElement(titleView, titleView.transitionName)
    .replace(R.id.container, detailFragment)
    .addToBackStack(null)
    .commit()

// Tùy chỉnh chuyển tiếp: ChangeBounds + ChangeImageTransform
detailFragment.sharedElementEnterTransition = TransitionSet().apply {
    ordering = TransitionSet.ORDERING_TOGETHER
    addTransition(ChangeBounds())
    addTransition(ChangeTransform())
    addTransition(ChangeImageTransform())
    duration = 400L
    interpolator = FastOutSlowInInterpolator()
}

Chi tiết quan trọng về Android: để tạo hoạt ảnh chính xác cho ImageView với scaleType khác nhau (centerCrop → fitCenter), hãy sử dụng ChangeImageTransform — nó tự động tạo hoạt ảnh thay đổi chế độ hiển thị. Đối với Views có góc bo tròn (CardView), hãy thêm ChangeBounds, nó tạo hoạt ảnh bán kính góc qua outline. Đối với hoạt ảnh văn bản (thay đổi kích thước phông chữ), hãy sử dụng ChangeTextTransform. Nếu một phần tử dùng chung biến mất hoặc xuất hiện trên một trong các màn hình, hệ thống sẽ tự động áp dụng Fade.

Shared Element Transition trong iOS (Swift)

iOS không có sẵn chức năng tương đương với sharedElementEnterTransition của Android — các hoạt ảnh dùng chung được triển khai thủ công qua UIViewControllerAnimatedTransitioning sử dụng UIViewPropertyAnimator hoặc CASpringAnimation. Kỹ thuật chính: trong animateTransition(using:), lấy View của màn hình nguồn, chụp ảnh nhanh, thêm nó vào containerView, ẩn bản gốc và tạo hoạt ảnh cho ảnh nhanh đến vị trí đích. Cuối cùng, hiển thị View đích và xóa ảnh nhanh.

swift
import UIKit

"> Trình hoạt ảnh Shared Element tùy chỉnh
class SharedElementAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    let fromView: UIView
    let toView: UIView
    let fromFrame: CGRect
    let toFrame: CGRect
    let isPresenting: Bool

    func transitionDuration(using ctx: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.4
    }

    func animateTransition(using ctx: UIViewControllerContextTransitioning) {
        guard let toVC = ctx.viewController(forKey: .to),
              let fromVC = ctx.viewController(forKey: .from)
        else { return }

        let container = ctx.containerView

        if isPresenting {
            container.addSubview(toVC.view)
            toVC.view.layoutIfNeeded()
            toVC.view.alpha = 0
        }

        // Ảnh nhanh phần tử dùng chung
        let snapshot = fromView.snapshotView(afterScreenUpdates: false)!
        snapshot.frame = isPresenting ? fromFrame : toFrame
        snapshot.layer.cornerRadius = isPresenting ? 12 : 0
        container.addSubview(snapshot)

        fromView.isHidden = true
        toView.isHidden = true

        UIView.animate(withDuration: transitionDuration(using: ctx),
                       delay: 0,
                       usingSpringWithDamping: 0.7,
                       initialSpringVelocity: 0) {
            snapshot.frame = self.isPresenting ? self.toFrame : self.fromFrame
            snapshot.layer.cornerRadius = self.isPresenting ? 0 : 12
            toVC.view.alpha = 1
        } completion: { _ in
            fromView.isHidden = false
            toView.isHidden = false
            snapshot.removeFromSuperview()
            ctx.completeTransition(!ctx.transitionWasCancelled)
        }
    }
}

// Hero — thư viện để đơn giản hóa chuyển tiếp dùng chung
// heroID khớp trên cả hai màn hình, tương tự như transitionName trong Android
import Hero

imageView.hero.id = "itemImage"
imageView.hero.modifiers = [.spring(stiffness: 300, damping: 20)]
titleLabel.hero.id = "itemTitle"

UIViewPropertyAnimator là cách ưa thích cho các hoạt ảnh dùng chung trên iOS 10+. Không giống như UIView.animate, PropertyAnimator hỗ trợ điều khiển tương tác (fractionComplete), cho phép tích hợp chuyển tiếp dùng chung với cử chỉ vuốt về. Đối với thư viện Hero (bên thứ ba), chuyển tiếp dùng chung được đặt qua hero.id — tương tự như transitionName trong Android, tự động tạo hoạt ảnh cho vị trí, kích thước, cornerRadius, zPosition và backgroundColor giữa các màn hình.

Shared Element trong Jetpack Compose

Trong Jetpack Compose, Shared Element Transition được triển khai qua bộ sửa đổi graphicsLayer với hoạt ảnh SharedTransitionScope (Compose 1.7+). Mỗi phần tử dùng chung được đánh dấu bằng sharedElement() với một khóa duy nhất. Hệ thống tự động tạo hoạt ảnh cho các thay đổi vị trí, kích thước, hình dạng, màu sắc và mặt nạ cắt khi điều hướng giữa các hàm composable. Theo Android Developers Blog (2026), Compose Shared Element Transition hỗ trợ AnimatedContent, NavHost và các vùng chứa tùy chỉnh.

kotlin
import androidx.compose.animation.core.spring
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut

// SharedTransitionLayout — vùng chứa cho các phần tử dùng chung
SharedTransitionLayout {
    val sharedTransitionState = remember { SharedTransitionState() }

    // Màn hình A: danh sách
    LazyColumn {
        items(items) { item ->
            Card(
                modifier = Modifier.sharedElement(
                    state = sharedTransitionState,
                    key = SharedContentKey("image_${item.id}"),
                    boundsTransform = { _, _ ->
                        spring(dampingRatio = Spring.DampingRatioMediumBouncy)
                    }
                )
            ) {
                AsyncImage(model = item.imageUrl, contentDescription = null)
            }
        }
    }

    // Màn hình B: chi tiết (hiển thị khi chọn một phần tử)
    AnimatedContent(
        targetState = selectedItem,
        transitionSpec = {
            EnterTransition.None using SizeTransform(clip = false)
        }
    ) { item ->
        item?.let {
            Image(
                modifier = Modifier.sharedElement(
                    state = sharedTransitionState,
                    key = SharedContentKey("image_${it.id}")
                ),
                painter = rememberAsyncImagePainter(it.imageUrl),
                contentDescription = null,
                contentScale = ContentScale.FillWidth
            )
        }
    }
}

Compose 1.7+ SharedTransitionScope — một API mới có sẵn trong chế độ thử nghiệm. Nó thay thế việc quản lý hoạt ảnh dùng chung thủ công trong NavHost. Các tính năng chính: boundsTransform cho đường cong hoạt ảnh tùy chỉnh, clipToBounds cho góc bo tròn, zIndex cho thứ tự xếp chồng. Để tạo hoạt ảnh cho hình ảnh với contentScale khác nhau, hãy sử dụng Modifier.sharedElement với trình bao bọc SharedContentKey. Đối với các phiên bản Compose cũ hơn (< 1.7), hãy sử dụng thư viện SharedTransitionScope của Google.

Tối ưu hóa và các vấn đề thường gặp

Shared Element Transition có thể gây ra vấn đề hiệu suất nếu không xem xét các giới hạn của nền tảng. Trong Android, vấn đề chính là ChangeImageTransform với hình ảnh lớn (trên 2048 px). Trong iOS — ảnh nhanh với các lớp nặng (CAGradientLayer, CAShapeLayer với nhiều nút). Giải pháp: sử dụng hình ảnh nén cho hoạt ảnh dùng chung và chụp ảnh nhanh với afterScreenUpdates: false.

Vấn đềNguyên nhânGiải pháp
Hoạt ảnh giậtBố cục khác nhau trước/sau khi chuyển tiếp / ảnh nhanh nặngSử dụng Placeholder: hiển thị bản xem trước nhỏ trong khi tải
Phần tử dùng chung không tạo hoạt ảnhtransitionName không khớp trên cả hai màn hìnhKiểm tra sự khớp chính xác của transitionName trong XML/mã
FPS < 40 trên AndroidPhần tử dùng chung chứa hệ thống phân cấp View phức tạpChỉ chia sẻ ImageView, không phải toàn bộ CardView
iOS: tạo tác ảnh nhanhrenderInContext trên lớp có mặt nạ/hoạt ảnhSử dụng drawHierarchy(in:afterScreenUpdates: true)
Compose: phần tử không xuất hiệnSharedTransitionLayout không bao bọc cả hai màn hìnhSharedTransitionLayout phải là cha của cả hai
kotlin
// Tối ưu hóa: Placeholder cho hình ảnh trong chuyển tiếp dùng chung
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide

"> Tải preview trước khi điều hướng
Glide.with(context)
    .load(imageUrl)
    .override(300, 300) // Giới hạn kích thước cho chuyển tiếp dùng chung
    .placeholder(R.drawable.placeholder)
    .into(imageView)

> Tải full-resolution trễ sau khi chuyển
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
    Glide.with(context).load(imageUrl).into(detailImage)
}

// iOS: tối ưu hóa ảnh nhanh với drawHierarchy
// snapshotView(afterScreenUpdates: false) — nhanh hơn, bỏ qua các cập nhật đang chờ
// Đối với Views phức tạp: drawHierarchy(in: view.bounds, afterScreenUpdates: true)

Các phương pháp tốt nhất: không thêm quá 3 phần tử dùng chung mỗi lần chuyển tiếp — mỗi phần tử bổ sung làm tăng tải kết xuất. Đối với danh sách (RecyclerView/UICollectionView), hãy tạo phần tử dùng chung trên mục được nhấp, không phải toàn bộ danh sách. Đối với thẻ có góc bo tròn, sử dụng clipToOutline trên Android hoặc cornerRadius trong ảnh nhanh trên iOS. Nếu phần tử dùng chung trên màn hình thứ hai nằm trong ScrollView, hãy đợi hoàn tất bố cục (view.doOnPreDraw) trước khi bắt đầu hoạt ảnh.

Các câu hỏi thường gặp

Có thể tạo hoạt ảnh cho bao nhiêu phần tử dùng chung trong một lần chuyển tiếp?

Khuyến nghị không sử dụng quá 3 phần tử dùng chung cho mỗi lần chuyển tiếp. Mỗi phần tử bổ sung làm tăng tải kết xuất và có thể gây giảm FPS. Tối ưu: 1 hình ảnh + 1 văn bản (tiêu đề). Đối với Android, mỗi phần tử được thêm qua addSharedElement(view, transitionName). Đối với iOS, mỗi ảnh nhanh được thêm vào containerView riêng biệt.

Shared Element Transition không hoạt động với RecyclerView — phải làm gì?

Đảm bảo transitionName được đặt động trong onBindViewHolder: holder.imageView.transitionName = "image_${position}". Đối với FragmentTransaction.addSharedElement, hãy chuyển View cụ thể từ holder. Đối với ActivityOptions, hãy sử dụng Pair.create(holder.imageView, holder.imageView.transitionName). Trì hoãn chuyển tiếp dùng chung cho đến khi hoàn tất bố cục qua postponeEnterTransition() và startPostponedEnterTransition().

Làm thế nào để thực hiện Shared Element Transition với thay đổi màu nền?

Trên Android: thêm ChangeBackground vào TransitionSet hoặc sử dụng Transition tùy chỉnh ghi đè captureStartValues và captureEndValues. Trên iOS: tạo hoạt ảnh cho backgroundColor của ảnh nhanh hoặc sử dụng UIViewPropertyAnimator với thay đổi màu sắc. Trong Jetpack Compose: Modifier.sharedElement tự động tạo hoạt ảnh cho nền qua chuyển tiếp Color. Đối với thẻ, cũng tạo hoạt ảnh cho elevation — từ 2dp trong danh sách đến 8dp trên màn hình chi tiết.

Shared Element có hoạt động trên Android với Jetpack Navigation không?

Có, với Navigation Component, Shared Element được hỗ trợ qua NavOptions: val navOptions = NavOptions.Builder().setEnterAnim(R.transition.move).applyNavOptions(fragment). Sử dụng FragmentNavigatorExtras để truyền các phần tử dùng chung: FragmentNavigatorExtras(view to transitionName). Đối với Navigation Compose, hãy sử dụng Modifier.sharedElement từ SharedTransitionScope (Compose 1.7+).

Làm thế nào để gỡ lỗi Shared Element Transition?

Trên Android, bật Transition Debugging: adb shell setprop debug.transition 1 — trong logcat sẽ xuất hiện thông báo về việc chụp và tạo hoạt ảnh phần tử dùng chung. Bật "Show layout bounds" trong Developer Options để kiểm tra ranh giới trước/sau. Trên iOS, sử dụng Debug View Hierarchy để kiểm tra ảnh nhanh trong containerView. Đối với FPS, sử dụng Profile GPU Rendering (Android) hoặc công cụ Core Animation (iOS).

Tổng kết

  • Shared Element Transition — hoạt ảnh của phần tử dùng chung giữa các màn hình trong khi vẫn giữ ngữ cảnh trực quan.
  • Android: FragmentTransaction.addSharedElement hoặc ActivityOptions.makeSceneTransitionAnimation.
  • iOS: triển khai tùy chỉnh qua UIViewControllerAnimatedTransitioning + ảnh nhanh + PropertyAnimator.
  • Jetpack Compose: Modifier.sharedElement từ SharedTransitionScope (Compose 1.7+).
  • Tối đa 3 phần tử dùng chung mỗi lần chuyển tiếp để tránh giảm FPS.
  • Đối với ImageView với scaleType khác nhau, sử dụng ChangeImageTransform (Android) hoặc ảnh nhanh (iOS).
  • Thư viện Hero (iOS) — giải pháp bên thứ ba với hero.id, tương tự transitionName trong Android.

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