Shared Element Transition — คืออะไร องค์ประกอบที่ใช้ร่วมกันในการเปลี่ยนหน้า

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-03-02 เวลาอ่าน: 10 นาที

Shared Element Transition คือแอนิเมชันที่องค์ประกอบที่ใช้ร่วมกัน (รูปภาพ ข้อความ การ์ด) เคลื่อนที่และเปลี่ยนรูปร่างอย่างราบรื่นระหว่างสองหน้าจอระหว่างการนำทาง แทนที่จะเป็นการเปลี่ยนหน้าอย่างฉับพลัน ผู้ใช้จะเห็นการเคลื่อนไหวต่อเนื่อง: องค์ประกอบจากหน้าจอ A ไหลไปยังหน้าจอ B โดยคงบริบทและการเชื่อมต่อทางภาพไว้ ตาม Material Design Guidelines (2026) Shared Element Transition ช่วยเพิ่มประสิทธิภาพการรับรู้ได้ 35% และปรับปรุงความเข้าใจในการนำทางขึ้น 28% เรียนรู้เพิ่มเติมเกี่ยวกับแอนิเมชันประเภทอื่น ๆ ใน คู่มือแอนิเมชันทั่วไป

ประเด็นสำคัญ

  • Shared Element Transition — แอนิเมชันขององค์ประกอบที่ใช้ร่วมกันระหว่างสองหน้าจอระหว่างการนำทาง
  • sharedElementEnterTransition — แอตทริบิวต์ XML ของ Android สำหรับกำหนดค่าการเข้าขององค์ประกอบที่ใช้ร่วมกัน
  • UIViewPropertyAnimator — พื้นฐานสำหรับแอนิเมชันที่ใช้ร่วมกันแบบกำหนดเองระหว่าง UIViewController ใน iOS
  • องค์ประกอบที่ใช้ร่วมกันถูกกำหนดผ่าน transitionName ใน Android และ tag หรือ snapshot ใน iOS
  • แอนิเมชันทำงานกับ ImageView, TextView, CardView, Button และ View ใด ๆ ที่มีตัวระบุที่ไม่ซ้ำกัน

Shared Element Transition คืออะไร?

Shared Element Transition คือแอนิเมชันการเปลี่ยนหน้าจอที่องค์ประกอบทางภาพหนึ่งรายการขึ้นไปย้ายจากหน้าจอก่อนหน้าไปยังหน้าจอถัดไป ในขณะที่นำทาง ระบบจะถ่ายภาพนิ่งขององค์ประกอบบนหน้าจอต้นทาง ทำให้การเปลี่ยนแปลง (ตำแหน่ง ขนาด รูปร่าง สี) เป็นแอนิเมชันไปยังสถานะเป้าหมายบนหน้าจอใหม่ ผลลัพธ์คือผู้ใช้รับรู้สองหน้าจอเป็นส่วนหนึ่งของพื้นที่เดียวกันแทนที่จะเป็นหน้าแยกกัน

กรณีการใช้งานหลัก: การ์ดสินค้า → หน้าจอรายละเอียด (รูปภาพขยายใหญ่และเลื่อนขึ้น), แกลเลอรีรูปภาพ → ตัวแสดง (รูปภาพขยายเต็มหน้าจออย่างราบรื่น), รายชื่อผู้ติดต่อ → โปรไฟล์ (รูปประจำตัวและชื่อเคลื่อนที่) เพื่อให้ทำงานได้อย่างถูกต้อง ทั้งสองหน้าจอต้องมี Views ที่มี transitionName (Android) หรือตัวระบุ snapshot (iOS) เดียวกัน

ตาม Google I/O 2025 Shared Element Transition อยู่ในสามรูปแบบ UX ที่มีประสิทธิภาพสูงสุดสำหรับแอปพลิเคชันมือถือ: ผู้ใช้มีโอกาสกลับทันทีหลังจากเปลี่ยนหน้าน้อยลง 22% หากเห็นแอนิเมชันองค์ประกอบที่ใช้ร่วมกัน อย่างไรก็ตาม การใช้งานมากเกินไป (มากกว่า 3 องค์ประกอบที่ใช้ร่วมกันต่อการเปลี่ยนหน้า) จะลดประสิทธิภาพและอาจทำให้ FPS ต่ำกว่า 40 บนอุปกรณ์ระดับกลาง

Shared Element Transition ใน Android (Kotlin)

ใน Android Shared Element Transition ถูกนำไปใช้ผ่าน FragmentTransaction.addSharedElement(view, transitionName) หรือ ActivityOptions.makeSceneTransitionAnimation แต่ละ View จะได้รับ android:transitionName ที่ไม่ซ้ำกัน — เหมือนกันทั้งสองหน้าจอ ระบบจะสร้างแอนิเมชันการเคลื่อนไหว (ChangeBounds) แอนิเมชันการเปลี่ยนขนาด (ChangeTransform) และแอนิเมชันการปรับขนาดรูปภาพ (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 ผ่าน 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 ผ่าน 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()

// การปรับแต่งการเปลี่ยนหน้า: ChangeBounds + ChangeImageTransform
detailFragment.sharedElementEnterTransition = TransitionSet().apply {
    ordering = TransitionSet.ORDERING_TOGETHER
    addTransition(ChangeBounds())
    addTransition(ChangeTransform())
    addTransition(ChangeImageTransform())
    duration = 400L
    interpolator = FastOutSlowInInterpolator()
}

รายละเอียดสำคัญของ Android: สำหรับแอนิเมชันที่ถูกต้องของ ImageView ที่มี scaleType ต่างกัน (centerCrop → fitCenter) ให้ใช้ ChangeImageTransform — มันจะทำให้การเปลี่ยนโหมดการแสดงผลเป็นแอนิเมชันโดยอัตโนมัติ สำหรับ Views ที่มีมุมมน (CardView) ให้เพิ่ม ChangeBounds ซึ่งทำให้รัศมีมุมเป็นแอนิเมชันผ่าน outline สำหรับแอนิเมชันข้อความ (การเปลี่ยนขนาดตัวอักษร) ให้ใช้ ChangeTextTransform หากองค์ประกอบที่ใช้ร่วมกันหายไปหรือปรากฏบนหน้าจอใดหน้าจอหนึ่ง ระบบจะใช้ Fade โดยอัตโนมัติ

Shared Element Transition ใน iOS (Swift)

iOS ไม่มีคุณสมบัติในตัวที่เทียบเท่ากับ sharedElementEnterTransition ของ Android — แอนิเมชันที่ใช้ร่วมกันถูกนำไปใช้ด้วยตนเองผ่าน UIViewControllerAnimatedTransitioning โดยใช้ UIViewPropertyAnimator หรือ CASpringAnimation เทคนิคหลัก: ใน animateTransition(using:) ให้รับ View ของหน้าจอต้นทาง ถ่ายภาพนิ่ง เพิ่มไปยัง containerView ซ่อนต้นฉบับ และทำให้ภาพนิ่งเป็นแอนิเมชันไปยังตำแหน่งเป้าหมาย ในตอนท้าย แสดง View เป้าหมายและลบภาพนิ่ง

swift
import UIKit

"> แอนิเมเตอร์ Shared Element ที่กำหนดเอง
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
        }

        // ภาพนิ่งขององค์ประกอบที่ใช้ร่วมกัน
        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 — ไลบรารีสำหรับทำให้การเปลี่ยนหน้าที่ใช้ร่วมกันง่ายขึ้น
// heroID ตรงกันบนทั้งสองหน้าจอ คล้ายกับ transitionName ใน Android
import Hero

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

UIViewPropertyAnimator เป็นวิธีที่แนะนำสำหรับแอนิเมชันที่ใช้ร่วมกันบน iOS 10+ ซึ่งแตกต่างจาก UIView.animate PropertyAnimator รองรับการควบคุมแบบโต้ตอบ (fractionComplete) ทำให้สามารถรวมการเปลี่ยนหน้าที่ใช้ร่วมกันกับท่าปัดกลับได้ สำหรับไลบรารี Hero (ของบุคคลที่สาม) การเปลี่ยนหน้าที่ใช้ร่วมกันถูกตั้งค่าผ่าน hero.id — ซึ่งคล้ายกับ transitionName ใน Android ที่ทำให้ตำแหน่ง ขนาด cornerRadius zPosition และ backgroundColor ระหว่างหน้าจอเป็นแอนิเมชันโดยอัตโนมัติ

Shared Element ใน Jetpack Compose

ใน Jetpack Compose Shared Element Transition ถูกนำไปใช้ผ่านตัวปรับแต่ง graphicsLayer ด้วยแอนิเมชัน SharedTransitionScope (Compose 1.7+) องค์ประกอบที่ใช้ร่วมกันแต่ละรายการถูกทำเครื่องหมายด้วย sharedElement() โดยใช้คีย์ที่ไม่ซ้ำกัน ระบบจะทำให้การเปลี่ยนแปลงตำแหน่ง ขนาด รูปร่าง สี และมาสก์ตัดเป็นแอนิเมชันโดยอัตโนมัติเมื่อนำทางระหว่างฟังก์ชัน composable ตาม Android Developers Blog (2026) Compose Shared Element Transition รองรับ AnimatedContent, NavHost และคอนเทนเนอร์ที่กำหนดเอง

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 — คอนเทนเนอร์สำหรับองค์ประกอบที่ใช้ร่วมกัน
SharedTransitionLayout {
    val sharedTransitionState = remember { SharedTransitionState() }

    // หน้าจอ A: รายการ
    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)
            }
        }
    }

    // หน้าจอ B: รายละเอียด (แสดงเมื่อเลือกองค์ประกอบ)
    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 — API ใหม่ที่พร้อมใช้งานในโหมดทดลอง มันแทนที่การจัดการแอนิเมชันที่ใช้ร่วมกันด้วยตนเองใน NavHost คุณสมบัติหลัก: boundsTransform สำหรับเส้นโค้งแอนิเมชันที่กำหนดเอง clipToBounds สำหรับมุมมน zIndex สำหรับลำดับการซ้อนทับ สำหรับการทำให้รูปภาพเป็นแอนิเมชันด้วย contentScale ที่ต่างกัน ให้ใช้ Modifier.sharedElement กับ wrapper SharedContentKey สำหรับ Compose เวอร์ชันเก่า (< 1.7) ให้ใช้ไลบรารี SharedTransitionScope ของ Google

การปรับแต่งและปัญหาที่พบบ่อย

Shared Element Transition อาจทำให้เกิดปัญหาด้านประสิทธิภาพหากไม่พิจารณาข้อจำกัดของแพลตฟอร์ม ใน Android ปัญหาหลักคือ ChangeImageTransform กับรูปภาพขนาดใหญ่ (สูงกว่า 2048 px) ใน iOS — ภาพนิ่งกับเลเยอร์ที่หนัก (CAGradientLayer, CAShapeLayer ที่มีโหนดจำนวนมาก) วิธีแก้ไข: ใช้รูปภาพที่บีบอัดสำหรับแอนิเมชันที่ใช้ร่วมกันและถ่ายภาพนิ่งด้วย afterScreenUpdates: false

ปัญหาสาเหตุวิธีแก้ไข
แอนิเมชันกระตุกเลย์เอาต์ต่างกันก่อน/หลังเปลี่ยนหน้า / ภาพนิ่งหนักใช้ Placeholder: แสดงตัวอย่างเล็กขณะโหลด
องค์ประกอบที่ใช้ร่วมกันไม่เป็นแอนิเมชันtransitionName ไม่ตรงกันบนทั้งสองหน้าจอตรวจสอบความตรงกันที่แน่นอนของ transitionName ใน XML/โค้ด
FPS < 40 บน Androidองค์ประกอบที่ใช้ร่วมกันมีลำดับชั้น View ที่ซับซ้อนแชร์เฉพาะ ImageView ไม่ใช่ CardView ทั้งหมด
iOS: สิ่งแปลกปลอมของภาพนิ่งrenderInContext บนเลเยอร์ที่มีมาสก์/แอนิเมชันใช้ drawHierarchy(in:afterScreenUpdates: true)
Compose: องค์ประกอบไม่ปรากฏSharedTransitionLayout ไม่ครอบคลุมทั้งสองหน้าจอSharedTransitionLayout ต้องเป็น parent ของทั้งสอง
kotlin
// การปรับแต่ง: Placeholder สำหรับรูปภาพในการเปลี่ยนหน้าที่ใช้ร่วมกัน
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide

"> โหลดตัวอย่างก่อนการนำทาง
Glide.with(context)
    .load(imageUrl)
    .override(300, 300) // ข้อจำกัดขนาดสำหรับการเปลี่ยนหน้าที่ใช้ร่วมกัน
    .placeholder(R.drawable.placeholder)
    .into(imageView)

> โหลดความละเอียดเต็มแบบเลื่อนเวลา หลังการเปลี่ยนผ่าน
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
    Glide.with(context).load(imageUrl).into(detailImage)
}

// iOS: การปรับแต่งภาพนิ่งด้วย drawHierarchy
// snapshotView(afterScreenUpdates: false) — เร็วกว่า ไม่สนใจการอัปเดตที่ค้างอยู่
// สำหรับ Views ที่ซับซ้อน: drawHierarchy(in: view.bounds, afterScreenUpdates: true)

แนวทางปฏิบัติที่ดีที่สุด: อย่าเพิ่มองค์ประกอบที่ใช้ร่วมกันมากกว่า 3 รายการต่อการเปลี่ยนหน้า — แต่ละองค์ประกอบที่เพิ่มขึ้นจะเพิ่มภาระการเรนเดอร์ สำหรับรายการ (RecyclerView/UICollectionView) ให้สร้างองค์ประกอบที่ใช้ร่วมกันบนรายการที่คลิก ไม่ใช่ทั้งรายการ สำหรับการ์ดที่มีมุมมน ให้ใช้ clipToOutline บน Android หรือ cornerRadius ในภาพนิ่งบน iOS หากองค์ประกอบที่ใช้ร่วมกันบนหน้าจอที่สองอยู่ใน ScrollView ให้รอให้เลย์เอาต์เสร็จสมบูรณ์ (view.doOnPreDraw) ก่อนเริ่มแอนิเมชัน

คำถามที่พบบ่อย

สามารถทำให้องค์ประกอบที่ใช้ร่วมกันเป็นแอนิเมชันได้กี่รายการในการเปลี่ยนหน้าหนึ่งครั้ง?

แนะนำให้ใช้ไม่เกิน 3 องค์ประกอบที่ใช้ร่วมกัน ต่อการเปลี่ยนหน้า องค์ประกอบที่เพิ่มขึ้นแต่ละรายการจะเพิ่มภาระการเรนเดอร์และอาจทำให้ FPS ลดลง เหมาะสมที่สุด: 1 รูปภาพ + 1 ข้อความ (หัวเรื่อง) สำหรับ Android แต่ละองค์ประกอบจะถูกเพิ่มผ่าน addSharedElement(view, transitionName) สำหรับ iOS ภาพนิ่งแต่ละภาพจะถูกเพิ่มลงใน containerView แยกกัน

Shared Element Transition ไม่ทำงานกับ RecyclerView — ต้องทำอย่างไร?

ตรวจสอบให้แน่ใจว่า transitionName ถูกตั้งค่าแบบไดนามิกใน onBindViewHolder: holder.imageView.transitionName = "image_${position}" สำหรับ FragmentTransaction.addSharedElement ให้ส่ง View เฉพาะจาก holder สำหรับ ActivityOptions ให้ใช้ Pair.create(holder.imageView, holder.imageView.transitionName) หน่วงเวลาการเปลี่ยนหน้าที่ใช้ร่วมกันจนกว่าเลย์เอาต์จะเสร็จสมบูรณ์ผ่าน postponeEnterTransition() และ startPostponedEnterTransition()

วิธีทำ Shared Element Transition พร้อมการเปลี่ยนสีพื้นหลัง?

บน Android: เพิ่ม ChangeBackground ใน TransitionSet หรือใช้ Transition ที่กำหนดเองที่แทนที่ captureStartValues และ captureEndValues บน iOS: ทำให้ backgroundColor ของภาพนิ่งเป็นแอนิเมชันหรือใช้ UIViewPropertyAnimator พร้อมการเปลี่ยนสี ใน Jetpack Compose: Modifier.sharedElement จะทำให้พื้นหลังเป็นแอนิเมชันโดยอัตโนมัติผ่าน Color transition สำหรับการ์ด ให้ทำให้ elevation เป็นแอนิเมชันด้วย — จาก 2dp บนรายการเป็น 8dp บนหน้าจอรายละเอียด

Shared Element ทำงานบน Android กับ Jetpack Navigation หรือไม่?

ใช่ กับ Navigation Component Shared Element รองรับผ่าน NavOptions: val navOptions = NavOptions.Builder().setEnterAnim(R.transition.move).applyNavOptions(fragment) ใช้ FragmentNavigatorExtras เพื่อส่งองค์ประกอบที่ใช้ร่วมกัน: FragmentNavigatorExtras(view to transitionName) สำหรับ Navigation Compose ให้ใช้ Modifier.sharedElement จาก SharedTransitionScope (Compose 1.7+)

วิธีดีบัก Shared Element Transition?

บน Android เปิด Transition Debugging: adb shell setprop debug.transition 1 — ใน logcat จะปรากฏข้อความเกี่ยวกับการจับภาพและแอนิเมชันขององค์ประกอบที่ใช้ร่วมกัน เปิด "Show layout bounds" ใน Developer Options เพื่อตรวจสอบขอบเขตก่อน/หลัง บน iOS ใช้ Debug View Hierarchy เพื่อตรวจสอบภาพนิ่งใน containerView สำหรับ FPS ให้ใช้ Profile GPU Rendering (Android) หรือเครื่องมือ Core Animation (iOS)

สรุป

  • Shared Element Transition — แอนิเมชันขององค์ประกอบที่ใช้ร่วมกันระหว่างหน้าจอในขณะที่คงบริบททางภาพไว้
  • Android: FragmentTransaction.addSharedElement หรือ ActivityOptions.makeSceneTransitionAnimation
  • iOS: การนำไปใช้แบบกำหนดเองผ่าน UIViewControllerAnimatedTransitioning + ภาพนิ่ง + PropertyAnimator
  • Jetpack Compose: Modifier.sharedElement จาก SharedTransitionScope (Compose 1.7+)
  • สูงสุด 3 องค์ประกอบที่ใช้ร่วมกัน ต่อการเปลี่ยนหน้าเพื่อหลีกเลี่ยง FPS ตก
  • สำหรับ ImageView ที่มี scaleType ต่างกัน ให้ใช้ ChangeImageTransform (Android) หรือภาพนิ่ง (iOS)
  • ไลบรารี Hero (iOS) — โซลูชันของบุคคลที่สามด้วย hero.id คล้ายกับ transitionName ใน Android

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม