Paylaşılan öğe geçişleri, aralarında tutarlı içeriğe sahip olan composable'lar arasında sorunsuz bir geçiş yöntemidir. Genellikle gezinme için kullanılırlar. Böylece, kullanıcı ekranlar arasında gezinirken farklı ekranları görsel olarak birbirine bağlayabilirsiniz.
Örneğin, aşağıdaki videoda resmin ve başlığının giriş sayfasından ayrıntılar sayfasına paylaşıldığını görebilirsiniz.
Compose'da paylaşılan öğeler oluşturmanıza yardımcı olan birkaç üst düzey API vardır:
SharedTransitionLayout
: Paylaşılan öğe geçişlerini uygulamak için gereken en dış düzen.SharedTransitionScope
sağlar. Paylaşılan öğe değiştiricileri kullanmak için composable'ların birSharedTransitionScope
içinde olması gerekir.Modifier.sharedElement()
: Başka bir composable ile eşleştirilmesi gereken composable'ıSharedTransitionScope
işaretleyen düzenleyici.Modifier.sharedBounds()
: Bu composable'ın sınırlarının, geçişin gerçekleşmesi gereken yerlerdeki kapsayıcı sınırları olarak kullanılması gerektiğini belirtenSharedTransitionScope
işaretleyici düzenleyici.sharedElement()
uygulamasının aksinesharedBounds()
, görsel olarak farklı içerikler için tasarlanmıştır.
Oluştur'da paylaşılan öğeler oluştururken üzerinde düşünülen önemli bir nokta, bu öğelerin yer paylaşımları ve kırpmayla 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 aşağıdaki geçiş oluşturulur ve küçük "liste" öğesinden daha büyük ve ayrıntılı öğeye geçiş yapılır:
composable'lar arasındaki geçişi sizin için otomatik olarak yönettiğinden Modifier.sharedElement()
kullanmanın en iyi yolu AnimatedContent
, AnimatedVisibility
veya NavHost
ile birlikte kullanılmasıdır.
Başlangıç noktası, paylaşılan öğe eklemeden önce MainContent
ve DetailsContent
composable içeren mevcut bir temel AnimatedContent
öğesidir:
Paylaşılan öğelerin iki düzen arasında animasyonlu olmasını sağlamak için
AnimatedContent
composable içinSharedTransitionLayout
kullanın.SharedTransitionLayout
veAnimatedContent
kapsamındaki kapsamlarMainContent
veDetailsContent
kanallarına iletilir: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()
ekleyin. BirSharedContentState
nesnesi oluşturun ve bunurememberSharedContentState()
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 hatırlanacak öğe içinrememberSharedContentState()
öğesini kullanın.AnimatedContentScope
, animasyonu koordine etmek için kullanılan değiştiriciye aktarılır.@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şmesinin oluşup oluşmadığı hakkında bilgi edinmek için rememberSharedContentState()
kodunu bir değişkene çıkarın ve isMatchFound
sorgusunu sorgulayın.
Bunun sonucunda aşağıdaki otomatik animasyon elde edilir:
Tüm kapsayıcının arka plan rengi ve boyutunun hâlâ varsayılan AnimatedContent
ayarlarını kullandığını fark edebilirsiniz.
Paylaşılan sınırlar ve paylaşılan öğe karşılaştırması
Modifier.sharedBounds()
, Modifier.sharedElement()
ile benzer.
Bununla birlikte, değiştiriciler şu şekillerde farklılık gösterir:
sharedBounds()
görsel olarak farklı olan ancak eyaletler arasında aynı alanı paylaşması gereken içerikler içindir.sharedElement()
ise içeriğin aynı olmasını bekler.sharedBounds()
ile iki durum arasındaki geçiş sırasında ekrana giren ve çıkan içerik görünürkensharedElement()
ile dönüşüm sınırlarında yalnızca hedef içerik oluşturulur.Modifier.sharedBounds()
, içeriğin nasıl geçiş yapacağını belirlemek içinAnimatedContent
'in çalışma şekline benzer şekildeenter
veexit
parametrelerine sahiptir.sharedBounds()
için en yaygın kullanım alanı kapsayıcı dönüşüm kalıbı,sharedElement()
için örnek kullanım alanı ise bir hero geçiştir.Text
composable'ları kullanılırken, italik ile 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
bölümlerine Modifier.sharedBounds()
eklenmesi, ikisinin sınırlarını paylaşmamıza ve geçiş animasyonunu gerçekleştirmemize olanak tanıyarak bunların birbirleri arasında büyümelerini 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()
özelliğini kullanmak için composable'ın SharedTransitionScope
içinde olması gerekir. SharedTransitionLayout
composable, SharedTransitionScope
sağlar. Kullanıcı arayüzü hiyerarşinizde, paylaşmak istediğiniz öğeleri içeren aynı üst düzey noktaya yerleştirdiğinizden emin olun.
Genellikle composable'lar aynı zamanda AnimatedVisibilityScope
içine de yerleştirilmelidir. Görünürlüğü manuel olarak yönetmediğiniz sürece, genellikle composable'lar arasında geçiş yapmak için AnimatedContent
kullanılarak veya doğrudan AnimatedVisibility
kullanılırken ya da NavHost
composable işleviyle sağlanır. Birden fazla kapsam kullanmak için zorunlu kapsamlarınızı bir CompositionLocal öğesine kaydedin, Kotlin'de bağlam alıcıları kullanın veya kapsamları işlevlerinize parametre olarak aktarın.
CompositionLocals
'yi, takip etmeniz gereken birden fazla kapsamınızın veya derinlemesine iç içe yerleştirilmiş bir hiyerarşinin olduğu senaryoda 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, iç içe yerleştirilmiş birden fazla 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 geçirebilirsiniz:
@Composable fun MainContent( animatedVisibilityScope: AnimatedVisibilityScope, sharedTransitionScope: SharedTransitionScope ) { } @Composable fun Details( animatedVisibilityScope: AnimatedVisibilityScope, sharedTransitionScope: SharedTransitionScope ) { }
AnimatedVisibility
ile paylaşılan öğeler
Ö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 kutusuna benzer bir bileşene çekilirmiş gibi görsel bir etkiye 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()
ürünlerinde, Compose'un geri kalanında olduğu gibi düzenleyici zincirinizin sırası önemlidir. Boyut etkileyen değiştiricilerin yanlış yerleştirilmesi, paylaşılan öğe eşleştirmesi sırasında beklenmedik görsel atlamalara neden olabilir.
Örneğin, paylaşılan iki öğede bir dolgu değiştiriciyi farklı bir konuma yerleştirirseniz animasyonda görsel bir fark oluşur.
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 yeniden boyutlandırılması gerektiğinden nasıl göründüğüne dikkat edin |
---|---|
Paylaşılan öğe değiştiricilerinden önce kullanılan değiştiriciler, paylaşılan öğe değiştiricilere kısıtlamalar sağlar. Bu da ilk ve hedef sınırları ve ardından da sınır animasyonunu türetmek için kullanılır.
Paylaşılan öğe değiştiricilerinden 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ştiricileri, alt öğeyi başlangıç boyutundan 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 composable'da Modifier.skipToLookaheadSize()
kullanılmasıdır. Bu durumda Compose, hedef kısıtlamaları kullanarak alt öğeyi düzenler ve düzenin boyutunu değiştirmek yerine animasyonu gerçekleştirmek için ölçek faktörü kullanır.
Benzersiz anahtarlar
Paylaşılan karmaşık öğelerle çalışırken, dizeler eşleşmeye açık olabileceğinden dize olmayan bir anahtar oluşturmak iyi bir uygulamadır. Eşleşmelerin gerçekleşmesi için her anahtar benzersiz olmalıdır. Örneğin, Jetsnack'te aşağıdaki paylaşılan öğelere sahibiz:
Paylaşılan öğe türünü temsil eden bir enum oluşturabilirsiniz. Bu örnekte, atıştırmalık kartının tamamı ana ekranda birden fazla farklı yerden de (örneğin, "Popüler" ve "Önerilen" bölümlerinde) görünebilir. Paylaşılacak paylaşılan öğenin snackId
, origin
("Popüler" / "Önerilen") ve type
öğelerine sahip 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 ) ) // ... }
Veri sınıfları, hashCode()
ve isEquals()
uygulayan anahtarlar için önerilir.
Paylaşılan öğelerin görünürlüğünü manuel olarak yönetin
AnimatedVisibility
veya AnimatedContent
kullanmıyor olabileceğiniz 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 olması gerektiğini 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 birkaç sınırlaması vardır. En önemlisi:
- Görünümler ve Oluşturma arasında birlikte çalışabilirlik özelliği desteklenmez. Bu,
Dialog
gibiAndroidView
öğesini sarmalayan herhangi bir composable'ı içerir. - Aşağıdakiler için otomatik animasyon desteği yoktur:
- Paylaşılan Resim composable'ları:
ContentScale
varsayılan olarak animasyonlu değildir.ContentScale
olarak ayarlanan uca tutturur.
- Ş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 oluşturma).
- Desteklenmeyen durumlar için
sharedElement()
yerineModifier.sharedBounds()
kullanın ve öğelereModifier.animateEnterExit()
ekleyin.
- Paylaşılan Resim composable'ları: