Paylaşılan öğe geçişleri, aralarında tutarlı içerik bulunan composable'lar arasında sorunsuz bir geçiş imkanı sunar. Genellikle gezinme için kullanılır ve kullanıcı aralarında gezinirken farklı ekranları görsel olarak bağlamanızı sağlar.
Örneğin aşağıdaki videoda, atıştırmalık resminin ve başlığının giriş sayfasından ayrıntılar sayfasına paylaşıldığını görebilirsiniz.
Oluşturma'da, paylaşılan öğeler oluşturmanıza yardımcı olacak birkaç üst düzey API vardır:
SharedTransitionLayout
: Paylaşılan öğe geçişlerini uygulamak için gereken en dıştaki düzen.SharedTransitionScope
sağlar. Ortak öğe değiştiricileri kullanmak için derlenebilir öğelerinSharedTransitionScope
içinde olması gerekir.Modifier.sharedElement()
: Başka bir derlenebilir öğeyle eşleştirilmesi gereken derlenebilir öğeyiSharedTransitionScope
işaretleyen değiştirici.Modifier.sharedBounds()
:SharedTransitionScope
için bu derlenebilir öğenin sınırlarının, geçişin gerçekleşeceği kapsayıcı sınırları olarak kullanılması gerektiğini belirten değiştirici.sharedElement()
uygulamasından farklı olaraksharedBounds()
, görsel olarak farklı içerikler için tasarlanmıştır.
Oluşturma'da paylaşılan öğeler oluştururken dikkat etmeniz gereken önemli bir kavram, bu öğelerin yer paylaşımları ve kırpma ile nasıl çalıştığıdır. Bu önemli konu hakkında daha fazla bilgi edinmek için kırpma ve yer paylaşımları bölümüne göz atın.
Temel Kullanım
Bu bölümde, daha küçük "liste" öğesinden daha kapsamlı olan ayrıntılı öğeye geçilerek aşağıdaki geçiş oluşturulacak:
Modifier.sharedElement()
'ü kullanmanın en iyi yolu, AnimatedContent
, AnimatedVisibility
veya NavHost
ile birlikte kullanmaktır. Bu yöntem, bileşenler arasındaki geçişi sizin için otomatik olarak yönetir.
Başlangıç noktası, ortak öğeler eklenmeden önce MainContent
ve DetailsContent
bileşeni içeren mevcut bir temel AnimatedContent
'tür:
Paylaşılan öğelerin iki düzen arasında animasyon yapması için
AnimatedContent
composable'ıSharedTransitionLayout
ile doldurun.SharedTransitionLayout
veAnimatedContent
'ten alınan kapsamlarMainContent
veDetailsContent
özelliklerine aktarılır:var showDetails by remember { mutableStateOf(false) } SharedTransitionLayout { AnimatedContent( showDetails, label = "basic_transition" ) { targetState -> if (!targetState) { MainContent( onShowDetails = { showDetails = true }, animatedVisibilityScope = this@AnimatedContent, sharedTransitionScope = this@SharedTransitionLayout ) } else { DetailsContent( onBack = { showDetails = false }, animatedVisibilityScope = this@AnimatedContent, sharedTransitionScope = this@SharedTransitionLayout ) } } }
Eşleşen iki composable'da composable değiştirici zincirinize
Modifier.sharedElement()
kodunu ekleyin. BirSharedContentState
nesnesi oluşturun verememberSharedContentState()
ile hatırlayın.SharedContentState
nesnesi, paylaşılan öğeleri belirleyen benzersiz anahtarı depolar. İçeriği tanımlamak için benzersiz bir anahtar sağlayın ve öğenin hatırlanması içinrememberSharedContentState()
kullanın.AnimatedContentScope
, animasyonu koordine etmek için kullanılan değiştiriciye geçirilir.@Composable private fun MainContent( onShowDetails: () -> Unit, modifier: Modifier = Modifier, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope ) { Row( // ... ) { with(sharedTransitionScope) { Image( painter = painterResource(id = R.drawable.cupcake), contentDescription = "Cupcake", modifier = Modifier .sharedElement( rememberSharedContentState(key = "image"), animatedVisibilityScope = animatedVisibilityScope ) .size(100.dp) .clip(CircleShape), contentScale = ContentScale.Crop ) // ... } } } @Composable private fun DetailsContent( modifier: Modifier = Modifier, onBack: () -> Unit, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope ) { Column( // ... ) { with(sharedTransitionScope) { Image( painter = painterResource(id = R.drawable.cupcake), contentDescription = "Cupcake", modifier = Modifier .sharedElement( rememberSharedContentState(key = "image"), animatedVisibilityScope = animatedVisibilityScope ) .size(200.dp) .clip(CircleShape), contentScale = ContentScale.Crop ) // ... } } }
Paylaşılan bir öğe eşleşmesi olup olmadığı hakkında bilgi almak için rememberSharedContentState()
öğesini bir değişkene ayıklayın ve isMatchFound
sorgusunu sorgulayın.
Bu, aşağıdaki otomatik animasyonla sonuçlanır:
Arka plan rengi ve tüm kapsayıcının boyutunun hâlâ varsayılan AnimatedContent
ayarlarını kullandığını fark edebilirsiniz.
Paylaşılan sınırlar ve paylaşılan öğe
Modifier.sharedBounds()
, Modifier.sharedElement()
ile benzer.
Bununla birlikte, değiştiriciler aşağıdaki şekillerde farklıdır:
sharedBounds()
, görsel olarak farklı ancak eyaletler arasında aynı alanı paylaşması gereken içerikler içindir.sharedElement()
ise içeriğin aynı olmasını bekler.sharedBounds()
ile ekrana giren ve ekrandan çıkan içerik iki durum arasındaki geçiş sırasında görünürkensharedElement()
ile dönüştürme sınırlarında yalnızca hedef içerik oluşturulur.Modifier.sharedBounds()
, içeriğin nasıl geçiş yapacağını belirtmek içinAnimatedContent
'ın işleyişine benzer şekildeenter
veexit
parametrelerine sahiptir.sharedBounds()
için en yaygın kullanım alanı kapsayıcı dönüştürme kalıbıdır.sharedElement()
için örnek kullanım alanı hero geçiştir.Text
bileşenleri kullanılırken italik ve kalın arasında geçiş yapma veya renk değişiklikleri gibi yazı tipi değişikliklerini desteklemek içinsharedBounds()
tercih edilir.
Önceki örnekten, iki farklı senaryoda Row
ve Column
öğesine Modifier.sharedBounds()
eklemek, bu iki senaryonun sınırlarını paylaşmamıza ve geçiş animasyonunu gerçekleştirmemize olanak tanıyarak bunların birbiri arasında büyümesini sağlar:
@Composable private fun MainContent( onShowDetails: () -> Unit, modifier: Modifier = Modifier, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope ) { with(sharedTransitionScope) { Row( modifier = Modifier .padding(8.dp) .sharedBounds( rememberSharedContentState(key = "bounds"), animatedVisibilityScope = animatedVisibilityScope, enter = fadeIn(), exit = fadeOut(), resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds() ) // ... ) { // ... } } } @Composable private fun DetailsContent( modifier: Modifier = Modifier, onBack: () -> Unit, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope ) { with(sharedTransitionScope) { Column( modifier = Modifier .padding(top = 200.dp, start = 16.dp, end = 16.dp) .sharedBounds( rememberSharedContentState(key = "bounds"), animatedVisibilityScope = animatedVisibilityScope, enter = fadeIn(), exit = fadeOut(), resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds() ) // ... ) { // ... } } }
Kapsamları Anlama
Modifier.sharedElement()
'ü kullanmak için derlenebilir öğenin bir SharedTransitionScope
içinde olması gerekir. SharedTransitionLayout
composable, SharedTransitionScope
sağlar. Paylaşmak istediğiniz öğeleri içeren kullanıcı arayüzü hiyerarşinizdeki aynı üst düzey noktaya yerleştirdiğinizden emin olun.
Genel olarak, derlenebilirler de bir AnimatedVisibilityScope
içine yerleştirilmelidir. Bu, genellikle composable'lar arasında geçiş yapmak için AnimatedContent
kullanılarak veya doğrudan AnimatedVisibility
kullanıldığında ya da görünürlüğü manuel olarak yönetmediğiniz NavHost
composable işlevi ile sağlanır. Birden fazla kapsam kullanmak için gerekli kapsamlarınızı bir CompositionLocal içine kaydedin, Kotlin'de bağlam alıcıları kullanın veya kapsamları işlevlerinize parametre olarak iletin.
Takip edilecek birden fazla kapsamın veya derinlemesine iç içe geçmiş bir hiyerarşinin olduğu senaryoda CompositionLocals
kullanın. CompositionLocal
, kaydedilip kullanılacak tam kapsamları seçmenize olanak tanır. Öte yandan, bağlam alıcıları kullandığınızda hiyerarşinizdeki diğer düzenler, sağlanan kapsamları yanlışlıkla geçersiz kılabilir.
Örneğin, birden fazla iç içe yerleştirilmiş AnimatedContent
öğeniz varsa kapsamlar geçersiz kılınabilir.
val LocalNavAnimatedVisibilityScope = compositionLocalOf<AnimatedVisibilityScope?> { null } val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { null } @Composable private fun SharedElementScope_CompositionLocal() { // An example of how to use composition locals to pass around the shared transition scope, far down your UI tree. // ... SharedTransitionLayout { CompositionLocalProvider( LocalSharedTransitionScope provides this ) { // This could also be your top-level NavHost as this provides an AnimatedContentScope AnimatedContent(state, label = "Top level AnimatedContent") { targetState -> CompositionLocalProvider(LocalNavAnimatedVisibilityScope provides this) { // Now we can access the scopes in any nested composables as follows: val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalStateException("No SharedElementScope found") val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current ?: throw IllegalStateException("No AnimatedVisibility found") } // ... } } } }
Alternatif olarak, hiyerarşiniz derinlemesine iç içe yerleştirilmemişse kapsamları parametre olarak aşağı aktarabilirsiniz:
@Composable fun MainContent( animatedVisibilityScope: AnimatedVisibilityScope, sharedTransitionScope: SharedTransitionScope ) { } @Composable fun Details( animatedVisibilityScope: AnimatedVisibilityScope, sharedTransitionScope: SharedTransitionScope ) { }
Öğeler AnimatedVisibility
ile paylaşıldı
Önceki örneklerde, paylaşılan öğelerin AnimatedContent
ile nasıl kullanılacağı gösterilmiştir ancak paylaşılan öğeler AnimatedVisibility
ile de çalışır.
Örneğin, bu geç ızgara örneğinde, her öğe AnimatedVisibility
içine sarmalanmıştır. Öğe tıklandığında içerik, kullanıcı arayüzünden iletişim kutusu benzeri bir bileşene çekilmiş gibi görsel bir efekte sahiptir.
var selectedSnack by remember { mutableStateOf<Snack?>(null) } SharedTransitionLayout(modifier = Modifier.fillMaxSize()) { LazyColumn( // ... ) { items(listSnacks) { snack -> AnimatedVisibility( visible = snack != selectedSnack, enter = fadeIn() + scaleIn(), exit = fadeOut() + scaleOut(), modifier = Modifier.animateItem() ) { Box( modifier = Modifier .sharedBounds( sharedContentState = rememberSharedContentState(key = "${snack.name}-bounds"), // Using the scope provided by AnimatedVisibility animatedVisibilityScope = this, clipInOverlayDuringTransition = OverlayClip(shapeForSharedElement) ) .background(Color.White, shapeForSharedElement) .clip(shapeForSharedElement) ) { SnackContents( snack = snack, modifier = Modifier.sharedElement( state = rememberSharedContentState(key = snack.name), animatedVisibilityScope = this@AnimatedVisibility ), onClick = { selectedSnack = snack } ) } } } } // Contains matching AnimatedContent with sharedBounds modifiers. SnackEditDetails( snack = selectedSnack, onConfirmClick = { selectedSnack = null } ) }
Değiştirici sıralaması
Modifier.sharedElement()
ve Modifier.sharedBounds()
ile birlikte, Compose'un geri kalanında olduğu gibi değiştirici zincirinizin sırası önemlidir. Boyutu etkileyen değiştiricilerin yanlış yerleştirilmesi, ortak öğe eşleştirme sırasında beklenmedik görsel sıçramalara neden olabilir.
Örneğin, bir dolgu değiştiriciyi iki paylaşılan öğede farklı bir konuma yerleştirirseniz animasyonda görsel bir fark olur.
var selectFirst by remember { mutableStateOf(true) } val key = remember { Any() } SharedTransitionLayout( Modifier .fillMaxSize() .padding(10.dp) .clickable { selectFirst = !selectFirst } ) { AnimatedContent(targetState = selectFirst, label = "AnimatedContent") { targetState -> if (targetState) { Box( Modifier .padding(12.dp) .sharedBounds( rememberSharedContentState(key = key), animatedVisibilityScope = this@AnimatedContent ) .border(2.dp, Color.Red) ) { Text( "Hello", fontSize = 20.sp ) } } else { Box( Modifier .offset(180.dp, 180.dp) .sharedBounds( rememberSharedContentState( key = key, ), animatedVisibilityScope = this@AnimatedContent ) .border(2.dp, Color.Red) // This padding is placed after sharedBounds, but it doesn't match the // other shared elements modifier order, resulting in visual jumps .padding(12.dp) ) { Text( "Hello", fontSize = 36.sp ) } } } }
Eşleşen sınırlar |
Eşleşmeyen sınırlar: Paylaşılan öğe animasyonunun, yanlış sınırlara göre yeniden boyutlandırması gerektiğinden nasıl göründüğüne dikkat edin. |
---|---|
Paylaşılan öğe değiştiricileri önce kullanılan değiştiriciler, paylaşılan öğe değiştiricilerine kısıtlamalar sağlar. Bu kısıtlamalar, ilk ve hedef sınırları ve ardından sınır animasyonu oluşturmak için kullanılır.
Paylaşılan öğe değiştiricilerden sonra kullanılan değiştiriciler, alt öğenin hedef boyutunu ölçmek ve hesaplamak için önceki kısıtlamaları kullanır. Paylaşılan öğe değiştiriciler, alt öğeyi ilk boyuttan hedef boyuta kademeli olarak dönüştürmek için bir dizi animasyonlu kısıtlama oluşturur.
Bunun istisnası, animasyon için resizeMode = ScaleToBounds()
veya bir kompozisyonda Modifier.skipToLookaheadSize()
kullanmanızdır. Bu örnekte Compose, hedef kısıtlamaları kullanarak alt öğeyi yerleştirir ve animasyon oluşturmak için düzenin boyutunu değiştirmek yerine ölçek faktörünü kullanır.
Benzersiz anahtarlar
Karmaşık paylaşılan öğelerle çalışırken, dize eşleştirme hataya açık olabileceğinden dize olmayan bir anahtar oluşturmak iyi bir uygulamadır. Eşleşmelerin gerçekleşmesi için her anahtarın benzersiz olması gerekir. Örneğin, Jetsnack'ta aşağıdaki paylaşılan öğeler vardır:
Paylaşılan öğe türünü temsil eden bir sıralama oluşturabilirsiniz. Bu örnekte, atıştırmalık kartının tamamı ana ekrandaki birden fazla farklı yerden de görünebilir (ör. "Popüler" ve "Önerilen" bölümleri). snackId
, origin
("Popüler" / "Önerilen") ve paylaşılacak paylaşılan öğenin type
özelliklerini içeren bir anahtar oluşturabilirsiniz:
data class SnackSharedElementKey( val snackId: Long, val origin: String, val type: SnackSharedElementType ) enum class SnackSharedElementType { Bounds, Image, Title, Tagline, Background } @Composable fun SharedElementUniqueKey() { // ... Box( modifier = Modifier .sharedElement( rememberSharedContentState( key = SnackSharedElementKey( snackId = 1, origin = "latest", type = SnackSharedElementType.Image ) ), animatedVisibilityScope = this@AnimatedVisibility ) ) // ... }
hashCode()
ve isEquals()
uyguladıkları için anahtarlar için veri sınıfları önerilir.
Paylaşılan öğelerin görünürlüğünü manuel olarak yönetme
AnimatedVisibility
veya AnimatedContent
kullanmadığınız durumlarda paylaşılan öğe görünürlüğünü kendiniz yönetebilirsiniz. Modifier.sharedElementWithCallerManagedVisibility()
özelliğini kullanın ve bir öğenin ne zaman görünür olup olmayacağını belirleyen kendi koşulunuzu sağlayın:
var selectFirst by remember { mutableStateOf(true) } val key = remember { Any() } SharedTransitionLayout( Modifier .fillMaxSize() .padding(10.dp) .clickable { selectFirst = !selectFirst } ) { Box( Modifier .sharedElementWithCallerManagedVisibility( rememberSharedContentState(key = key), !selectFirst ) .background(Color.Red) .size(100.dp) ) { Text(if (!selectFirst) "false" else "true", color = Color.White) } Box( Modifier .offset(180.dp, 180.dp) .sharedElementWithCallerManagedVisibility( rememberSharedContentState( key = key, ), selectFirst ) .alpha(0.5f) .background(Color.Blue) .size(180.dp) ) { Text(if (selectFirst) "false" else "true", color = Color.White) } }
Mevcut sınırlamalar
Bu API'lerin bazı sınırlamaları vardır. En önemlisi:
- Görünümler ve Oluşturma arasında birlikte çalışabilirlik desteklenmez. Buna
AndroidView
'ü saran tüm derlenebilirler (ör.Dialog
) dahildir. - Aşağıdakiler için otomatik animasyon desteği yoktur:
- Paylaşılan resim bileşimleri:
ContentScale
varsayılan olarak animasyonlu değildir. Ayarlanan uca sabitlenirContentScale
.
- Şekil kırpma: Şekiller arasında otomatik animasyon için yerleşik bir destek yoktur. Örneğin, öğe geçişi sırasında bir kareden daireye animasyon uygulama.
- Desteklenmeyen durumlarda
sharedElement()
yerineModifier.sharedBounds()
kullanın ve öğelereModifier.animateEnterExit()
ekleyin.
- Paylaşılan resim bileşimleri: