Shared Element Transition — ένα κινούμενο σχέδιο κατά το οποίο ένα κοινό στοιχείο (εικόνα, κείμενο, κάρτα) μετακινείται και μεταμορφώνεται ομαλά μεταξύ δύο οθονών κατά την πλοήγηση. Αντί για απότομη μετάβαση, ο χρήστης βλέπει συνεχή κίνηση: το στοιχείο από την οθόνη Α «ρέει» στην οθόνη Β, διατηρώντας το περιβάλλον και την οπτική σύνδεση. Σύμφωνα με το Material Design Guidelines (2026), το Shared Element Transition αυξάνει την αντιλαμβανόμενη απόδοση κατά 35% και βελτιώνει την κατανόηση της πλοήγησης κατά 28%. Διαβάστε περισσότερα για άλλους τύπους κινούμενων σχεδίων στον γενικό οδηγό κινούμενων σχεδίων.
Κύρια σημεία
Shared Element Transition — είναι ένα κινούμενο σχέδιο αλλαγής οθόνης όπου ένα ή περισσότερα οπτικά στοιχεία «μετακομίζουν» από την προηγούμενη οθόνη στην επόμενη. Τη στιγμή της πλοήγησης, το σύστημα τραβά ένα snapshot του στοιχείου στην πηγαία οθόνη, κινεί τη μεταμόρφωσή του (θέση, μέγεθος, σχήμα, χρώμα) προς την κατάσταση προορισμού στη νέα οθόνη. Ως αποτέλεσμα, ο χρήστης αντιλαμβάνεται τις δύο οθόνες ως μέρη ενός ενιαίου χώρου, όχι ως ξεχωριστές σελίδες.
Κύρια σενάρια χρήσης: κάρτα προϊόντος → οθόνη λεπτομερειών (η εικόνα μεγαλώνει και πηγαίνει πάνω), συλλογή εικόνων → προβολή (η εικόνα ανοίγει ομαλά σε πλήρη οθόνη), λίστα επαφών → προφίλ (το avatar και το όνομα μετακινούνται). Για σωστή λειτουργία και οι δύο οθόνες πρέπει να έχουν View με το ίδιο transitionName (Android) ή αναγνωριστικό snapshot (iOS).
Σύμφωνα με το Google I/O 2025, το Shared Element Transition είναι ένα από τα τρία πιο αποτελεσματικά μοτίβα UX για εφαρμογές κινητών: οι χρήστες επιστρέφουν 22% λιγότερο συχνά αμέσως μετά τη μετάβαση αν είδαν το κινούμενο σχέδιο του κοινού στοιχείου. Ωστόσο, η υπερβολική χρήση (περισσότερα από 3 κοινά στοιχεία ανά μετάβαση) μειώνει το αποτέλεσμα και μπορεί να προκαλέσει πτώση FPS κάτω από 40 σε συσκευές μεσαίας κατηγορίας.
Στο Android, το Shared Element Transition υλοποιείται μέσω FragmentTransaction.addSharedElement(view, transitionName) ή ActivityOptions.makeSceneTransitionAnimation. Σε κάθε View αποδίδεται ένα μοναδικό android:transitionName — ίδιο και στις δύο οθόνες. Το σύστημα δημιουργεί αυτόματα κινούμενο σχέδιο μετακίνησης (ChangeBounds), αλλαγής μεγέθους (ChangeTransform) και κλιμάκωσης εικόνας (ChangeImageTransform).
<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" />
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 — κινεί αυτόματα την αλλαγή του τρόπου εμφάνισης. Για View με στρογγυλεμένες γωνίες (CardView) προσθέστε ChangeBounds, που κινεί την ακτίνα στρογγυλοποίησης μέσω outline. Για κινούμενο σχέδιο κειμένου (αλλαγή μεγέθους γραμματοσειράς) χρησιμοποιήστε ChangeTextTransform. Αν το κοινό στοιχείο εξαφανίζεται ή εμφανίζεται σε μία από τις οθόνες — το σύστημα εφαρμόζει αυτόματα Fade.
Στο iOS δεν υπάρχει ενσωματωμένο αντίστοιχο του sharedElementEnterTransition του Android — τα shared-κινούμενα σχέδια υλοποιούνται χειροκίνητα μέσω UIViewControllerAnimatedTransitioning χρησιμοποιώντας UIViewPropertyAnimator ή CASpringAnimation. Κύρια τεχνική: στο animateTransition(using:) πάρτε το View της πηγαίας οθόνης, τραβήξτε ένα snapshot, προσθέστε το στο containerView, κρύψτε το πρωτότυπο και κινήστε το snapshot προς τη θέση προορισμού. Στο τέλος εμφανίστε το View προορισμού και αφαιρέστε το snapshot.
import UIKit
"> Προσαρμοσμένος animator 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
}
// Snapshot κοινού στοιχείου
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 — βιβλιοθήκη για απλοποίηση shared transitions
// Το heroID ταιριάζει και στις δύο οθόνες, ανάλογα με το transitionName στο Android
import Hero
imageView.hero.id = "itemImage"
imageView.hero.modifiers = [.spring(stiffness: 300, damping: 20)]
titleLabel.hero.id = "itemTitle"
UIViewPropertyAnimator — η προτιμώμενη μέθοδος για shared-κινούμενα σχέδια στο iOS 10+. Σε αντίθεση με το UIView.animate, το PropertyAnimator υποστηρίζει διαδραστικό έλεγχο (fractionComplete), επιτρέποντας την ενσωμάτωση του shared transition με τη χειρονομία swipe-back. Για τη βιβλιοθήκη Hero (τρίτου μέρους), το shared transition ορίζεται μέσω hero.id — αυτό είναι το αντίστοιχο του transitionName από το Android, που κινεί αυτόματα θέση, μέγεθος, cornerRadius, zPosition και backgroundColor μεταξύ οθονών.
Στο Jetpack Compose, το Shared Element Transition υλοποιείται μέσω του τροποποιητή graphicsLayer με κινούμενο σχέδιο SharedTransitionScope (Compose 1.7+). Κάθε κοινό στοιχείο επισημαίνεται μέσω sharedElement() με μοναδικό κλειδί. Το σύστημα κινεί αυτόματα την αλλαγή θέσης, μεγέθους, σχήματος, χρώματος και μάσκας αποκοπής κατά την πλοήγηση μεταξύ συναρτήσεων composable. Σύμφωνα με το Android Developers Blog (2026), το Compose Shared Element Transition υποστηρίζει AnimatedContent, NavHost και προσαρμοσμένα δοχεία.
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() }
// Οθόνη Α: λίστα
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)
}
}
}
// Οθόνη Β: λεπτομέρειες (εμφανίζεται κατά την επιλογή στοιχείου)
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 διαθέσιμο σε πειραματική λειτουργία. Αντικαθιστά τη χειροκίνητη διαχείριση shared-κινούμενων σχεδίων στο NavHost. Βασικές δυνατότητες: boundsTransform για προσαρμοσμένη καμπύλη κινούμενου σχεδίου, clipToBounds για στρογγυλεμένες γωνίες, zIndex για σειρά επικάλυψης. Για κινούμενο σχέδιο εικόνων με διαφορετικό contentScale χρησιμοποιήστε Modifier.sharedElement με περιτύλιγμα SharedContentKey. Για υποστήριξη παλαιότερων εκδόσεων Compose (< 1.7) χρησιμοποιήστε τη βιβλιοθήκη SharedTransitionScope της Google.
Shared Element Transition μπορεί να προκαλέσει προβλήματα απόδοσης αν δεν ληφθούν υπόψη οι περιορισμοί πλατφόρμας. Στο Android το κύριο πρόβλημα είναι το ChangeImageTransform με μεγάλες εικόνες (πάνω από 2048 px). Στο iOS — snapshot με βαριά επίπεδα (CAGradientLayer, CAShapeLayer με πολλούς κόμβους). Λύση: χρησιμοποιήστε συμπιεσμένες εικόνες για shared-κινούμενο σχέδιο και τραβήξτε snapshot με afterScreenUpdates: false.
| Πρόβλημα | Αιτία | Λύση |
|---|---|---|
| Τραχύ κινούμενο σχέδιο | Διαφορετική διάταξη πριν/μετά τη μετάβαση / βαρύ snapshot | Χρησιμοποιήστε Placeholder: εμφανίστε μικρή προεπισκόπηση κατά τη φόρτωση |
| Το κοινό στοιχείο δεν κινείται | Το transitionName δεν ταιριάζει και στις δύο οθόνες | Ελέγξτε την ακριβή αντιστοιχία transitionName σε XML/κώδικα |
| FPS < 40 στο Android | Το κοινό στοιχείο περιέχει σύνθετη ιεραρχία View | Κάντε shared μόνο το ImageView, όχι ολόκληρο το CardView |
| iOS: τεχνουργήματα snapshot | renderInContext σε επίπεδο με μάσκες/κινούμενα σχέδια | Χρησιμοποιήστε drawHierarchy(in:afterScreenUpdates: true) |
| Compose: το στοιχείο δεν εμφανίζεται | Το SharedTransitionLayout δεν περιβάλλει και τις δύο οθόνες | Το SharedTransitionLayout πρέπει να είναι γονέας και για τις δύο |
// Βελτιστοποίηση: Placeholder για εικόνα σε shared transition
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide
"> Φόρτωση preview πριν την πλοήγηση
Glide.with(context)
.load(imageUrl)
.override(300, 300) // Περιορισμός μεγέθους για shared transition
.placeholder(R.drawable.placeholder)
.into(imageView)
> Καθυστερημένη φόρτωση full-resolution μετά τη μετάβαση
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
Glide.with(context).load(imageUrl).into(detailImage)
}
// iOS: βελτιστοποίηση snapshot με drawHierarchy
// snapshotView(afterScreenUpdates: false) — πιο γρήγορο, αγνοεί εκκρεμείς ενημερώσεις
// Για σύνθετα View: drawHierarchy(in: view.bounds, afterScreenUpdates: true)
Βέλτιστες πρακτικές: μην προσθέτετε περισσότερα από 3 κοινά στοιχεία ανά μετάβαση — κάθε επιπλέον στοιχείο αυξάνει το φορτίο απόδοσης. Για λίστες (RecyclerView/UICollectionView) κάντε το κοινό στοιχείο στο πατημένο στοιχείο, όχι σε ολόκληρη τη λίστα. Για κάρτες με στρογγυλεμένες γωνίες χρησιμοποιήστε clipToOutline στο Android ή cornerRadius στο snapshot στο iOS. Αν το κοινό στοιχείο στη δεύτερη οθόνη βρίσκεται σε ScrollView, περιμένετε την ολοκλήρωση της διάταξης (view.doOnPreDraw) πριν ξεκινήσετε το κινούμενο σχέδιο.
Συχνές Ερωτήσεις
Συνιστάται όχι περισσότερα από 3 κοινά στοιχεία ανά μετάβαση. Κάθε επιπλέον στοιχείο αυξάνει το φορτίο απόδοσης και μπορεί να προκαλέσει πτώση FPS. Βέλτιστο: 1 εικόνα + 1 κείμενο (τίτλος). Για Android κάθε στοιχείο προστίθεται μέσω addSharedElement(view, transitionName). Για iOS κάθε snapshot προστίθεται στο containerView ξεχωριστά.
Βεβαιωθείτε ότι το transitionName ορίζεται δυναμικά στο onBindViewHolder: holder.imageView.transitionName = «image_${position}». Για FragmentTransaction.addSharedElement μεταδώστε το συγκεκριμένο View από το holder. Για ActivityOptions χρησιμοποιήστε Pair.create(holder.imageView, holder.imageView.transitionName). Καθυστερήστε το shared transition μέχρι την ολοκλήρωση της διάταξης μέσω postponeEnterTransition() και startPostponedEnterTransition().
Στο Android: προσθέστε ChangeBackground στο TransitionSet ή χρησιμοποιήστε προσαρμοσμένο Transition που παρακάμπτει τα captureStartValues και captureEndValues. Στο iOS: κινήστε το backgroundColor του snapshot ή UIViewPropertyAnimator με αλλαγή χρώματος. Στο Jetpack Compose: το Modifier.sharedElement κινεί αυτόματα το background μέσω Color transition. Για κάρτες, κινήστε επίσης το elevation — από 2dp στη λίστα σε 8dp στην οθόνη λεπτομερειών.
Ναι, με το 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+).
Στο Android ενεργοποιήστε το Transition Debugging: adb shell setprop debug.transition 1 — στο logcat θα εμφανιστούν μηνύματα σχετικά με τη λήψη και κίνηση κοινών στοιχείων. Ενεργοποιήστε το «Show layout bounds» στο Developer Options για έλεγχο ορίων πριν/μετά. Στο iOS χρησιμοποιήστε Debug View Hierarchy για έλεγχο του snapshot στο containerView. Για παρακολούθηση FPS χρησιμοποιήστε Profile GPU Rendering (Android) ή Core Animation instrument (iOS).
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης