Yazılım Mühendisi
Geliştirici İlişkileri Mühendisi
Android kullanıcı arayüzünüzü "çalışıyor"dan "bir sanat eseri"ne dönüştürmeye hazır mısınız?
Jetpack Compose'daki en yeni grafik özelliklerini inceleyeceğimiz tura katılın. Android'deki güçlü yeni grafik özelliklerini kullanarak, keyifli ağ geçişlerinden aşamalı bulanıklıklara kadar yeni görsel efektleri nasıl oluşturacağınızı göstereceğiz. Ayrıca, pil ömründen veya kare hızlarından ödün vermeden grafik özelliklerinizin performansını nasıl artıracağınızı da göstereceğiz.
PolygonShape ve MorphPolygonShape API'lerini kullanıma sunuyor. MorphPolygonShape, ana hat çözünürlüğü aşamasında ilerleme durumunu okur ve yeniden oluşturmayı tetiklemeden şekilleri kare kare günceller.RenderNode.setBackdropRenderEffect özelliği sunuluyor. Geliştiriciler, buzlu cam veya büyütme efektleri oluşturmak için arka plan efektlerini birbirine bağlayabilir.graphicsLayer içine alın.Transkript
Herkese merhaba. "Compose ve Android'deki En Yeni Grafik Özellikleriyle Uygulamalarınızı Geliştirme" oturumumuza hoş geldiniz.
Adım Rebecca Franks. Bugün sahnede bana eşlik eden--
Ben Aayush Chaudhary.
Merhaba, ben Rahul Ray.
Android ve Compose'da mümkün olanın sınırlarını zorlamak için yapay zekayı kullanarak inanılmaz fikirlerinizi hayata geçirme konusunda yeni bir trend görüyoruz.
Premium, yerel ve keyifli bir deneyim sunan uygulamalar görmekten mutluluk duyuyoruz.
Kullanıcıların beğenisini kazanan ve sürekli olarak yüksek puan alan uygulamaların genellikle en son sistem düzeyindeki API değişikliklerinden yararlandığını, paylaşılan öğeler ve bulanıklaştırma ile yüzeyler arasında süreklilik sağladığını, farklı form faktörlerine mükemmel şekilde uyum sağladığını, yüksek performanslı olduğunu, kullanıcıları tahmin yürütmek zorunda bırakmadığını ve cihazınızla mümkün olanın sınırlarını zorladığını biliyoruz.
Bugünkü konuşmamızda, Android'den yararlanmanıza yardımcı olacak en yeni özelliklerden bazılarını göstereceğiz. Bunlar arasında, kullanıcıları ağ geçişleriyle memnun etme, geniş renk gamı desteğini etkinleştirme, çokgen şekillerle keyifli geçişler, bulanıklaştırmadaki yenilikler ve son olarak, uygulamalarınıza yeni grafik özellikleri eklerken performansı nasıl değerlendirebileceğiniz yer alıyor.
Ancak bugünkü konuşmamızda, yığının farklı seviyelerindeki yeni API'lerden bazılarını ele alacağız.
Eski görünüm kullanıcı arayüzü araç seti şu anda bakım modunda olsa da Jetpack Compose'u destekleyen temel Android grafik donanımı oluşturma motorlarına yatırım yapmaya devam ediyoruz.
Android'deki grafikleri üç farklı katman olarak düşünebilirsiniz: İlk katmanda Compose API'leri bulunur. Bu katmanda, ağ geçişli boyama araçları, çokgen şekiller ve aşamalı bulanıklaştırma API'leri gibi tüm üst düzey, yeni kullanıcı arayüzü grafik API'leri yer alır.
Ardından, Compose'un DrawScope, çizim değiştirici düğümleri ve AGSL ile özel efektler oluşturmanıza olanak tanıyan orta katman gelir.
Son olarak, Android platformu var. Compose, her zaman yerel bir Canvas'a erişebilir ve Compose değiştiricilerinizde yepyeni OS oluşturma temel öğelerini kullanabilir.
Bugün size bununla ilgili bazı örnekler göstereceğiz.
Gelin şimdi bu değerleri inceleyelim.
İlk olarak, Compose 1.12'de kullanıma sunulan yeni bir API olan mesh gradyanlara bakalım.
Daha önce doğrusal, açısal ve dairesel renk geçişleri kullanıyorduk. Bunlar basit geçişler için idealdir ancak ortam arka planları veya projektör efektleri gibi zengin çok renkli karışımlar istiyorsanız konumların ve renklerin alan boyunca karışabileceği 2D ızgara sistemine ihtiyacınız vardır.
Bunun gibi bir ağ gradyanı oluşturmak için önce kaç satır ve sütun yama istediğinizi belirtin.
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
}
}Ardından, sondaki lambda içinde setVertex işlevini çağırın ve her bir kılavuz kesişimi için bir renk ile birlikte 0F ile 1F arasında normalleştirilmiş bir ofset atayın.
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
}
}Ardından bu köşeleri canlandırmak için sekiz köşeye animasyonlu ofsetler ekleyin.
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
// ....
setVertex(1, 1, Offset(0.2f, 0.4f) + Offset(animatedOffset, animatedOffset), coral)
// ....
setVertex(3, 0, Offset(0.0f, 1.0f), sunshine)
// ....
}
}
Box(
modifier = modifier.padding(32.dp)
.safeContentPadding()
.aspectRatio(16 / 9f)
.fillMaxWidth()
.paint(gradientPainter)
)Painter'ınızı tanımladıktan sonra, bir boyama değiştirici aracılığıyla herhangi bir composable ile çağırabilirsiniz.
JetChat uygulamasında, bir mesaja iki kez dokunduğunuzda mesajı beğenme özelliği ekledik.
Bunu oluşturmak için 2'ye 3'lük bir ağ oluşturup renk duraklarımızı bileşene yerleştiririz.
MeshGradientPainter(
rows = 2,
columns = 3,
hasBicubicColor = true,
) {
// Row 0 (top edge, y = 0.0f)
setVertex(0, 0, Offset(0.0000f, 0.0000f), Color(0xFFFF6B6B))
setVertex(0, 1, Offset(0.3300f, 0.0000f), Color(0xFFFFD6D6))
setVertex(0, 2, Offset(0.6700f, 0.0000f), Color(0xFFE8C3FF))
setVertex(0, 3, Offset(1.0000f, 0.0000f), Color(0xFFE396FF))
// ... remaining vertices for Rows 0, 1, and 2
}Burada hasBicubicColor değerini "true" olarak ayarladığımıza dikkat edin.
Varsayılan olarak renkler, üçgenler arasında doğrusal olarak interpolasyon yapar. Bu durum bazen görünür yamalar bırakabilir.
Bu ayar etkinleştirildiğinde gölgelendirici, yumuşak bikübik enterpolasyona geçer. Böylece renkler sorunsuz bir şekilde birbirine karışabilir.
Peki, giriş kutusunun arkasında bunun gibi animasyonlu bir ortam parıltısı oluşturmak isterseniz ne yapmanız gerekir?
Ağ geçişiyle böyle bir efekt elde etmek için iki şeye ihtiyacınız var.
İlk olarak, iç köşeler için yumuşak ve organik bir hareket ve kartın etrafında sert bir dikdörtgen oluşmaması için dış kenarları yumuşatmanın bir yolu.
Bu nedenle, hareketin mekanik değil doğal hissettirmesi için biraz farklı sürelerde çalışan iki faz açısına sahip bir rememberInfiniteTransition oluşturarak başlıyoruz.
val transition = rememberInfiniteTransition(label = "userInputGlowMesh")
val phase1 by transition.animateFloat(
initialValue = 0f,
targetValue = (2 * PI).toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 2800, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "glowPhase1",
)
val phase2 by transition.animateFloat(
initialValue = 0f,
targetValue = (2 * PI).toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 4200, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "glowPhase2",
)Burada, iç köşeleri canlandırıyoruz ve dış halkaları, yani 0 alfa değerine sahip kenarları olan köşelerin dış halkasını sabitliyoruz.
Dış sınır sabit ve şeffaf olduğundan parlama doğal bir şekilde yumuşar.
Son olarak, bu animasyonlu ofsetleri MeshGradientPainter modelimize besliyoruz.
MeshGradientPainter(
rows = 6,
columns = 5,
hasBicubicColor = true,
) {
for (r in 0..6) {
for (c in 0..5) {
val offset = if (r in 1..5 && c in 1..4) {
val ampX = when (r) {
1, 2 -> 0.048f
3 -> 0.042f
4 -> 0.028f
else -> 0.036f
}
val ampY = when (r) {
1, 2 -> 0.028f
3 -> 0.020f
4 -> 0.022f
else -> 0.016f
}
val d = orbit(c, r, ampX, ampY)
Offset(
x = (baseU + d.x).coerceIn(0.04f, 0.96f),
y = (baseV + d.y).coerceIn(0.05f, 0.95f),
)
} else {
Offset(baseU, baseV)
}
setVertex(r, c, offset, gridColors[r][c])
}
}
}Az önce gördüğünüz ağ geçişi, Android 14 ve sonraki sürümlerde drawMesh API'si kullanılarak çizilir.
Şimdi sıfırdan bir efekt oluşturarak bu API'yi daha yakından inceleyelim.
JetChat karşılama ekranı için kurdeleye benzeyen bir yol çizmek istiyoruz.
Burada iki şeye dikkat edin: Kontur genişliği sabit değildir ve renk, yolun her konturu boyunca uzanır.
Şimdi bu efekti oluşturmak için nelere ihtiyacımız olduğunu inceleyelim.
Her piksel için çizilip çizilmeyeceğine ve hangi rengin kullanılacağına karar vermemiz gerekir.
Bunu hesaplamak için iki mesafeyi bilmemiz gerekir.
İlk değer, bir pikselin konturun merkezinden ne kadar uzakta olduğunu gösterir.
İkinci mesafe, bir pikselin yol boyunca başlangıçtan sona kadar ne kadar uzakta olduğudur.
Bu, gradyandan hangi rengi seçeceğimizi belirtir. İlk değerin bir kısmını karıştırmak, rengin genişlik boyunca eğri oluşturmasını sağlar.
Peki, yalnızca yol ve boya kullanarak bir yol boyunca gradyan çizebilir miyiz?
Evet, yapabiliriz.
Bunun için bir yolun kısa düz çizgilerden oluşan bir listeye düzleştirilmesi gerekir. Bu işlem, yol boyunca mesafeyi de verir.
val lines = remember {
path.asAndroidPath().flatten(0.5f)
}
//... inside draw scope
lines.forEach { line ->
//...
drawLine(
brush = Brush.linearGradient(...),
start = Offset(line.start.x,
line.start.y),
end = Offset(line.end.x, line.end.y),
strokeWidth = 30f,
)
//...
}Ardından her çizgiyi kendi gradyanıyla çizeriz.
Bu yöntem işe yarar ancak her çizgi için ayrı bir çekim çağrısı ve boyama nesnesi gerekir. Ayrıca, tüm yolun genişliği yine tek bir değerle sınırlıdır.
Diğer seçenek ise çalışma zamanı gölgelendiricisidir ancak bu seçenek yalnızca pikselin kapsayıcı sınırı içindeki konumunu alır.
Bu şeridi çizmek için neye ihtiyacımız olduğunu söylemiyor.
Pikselin konumundan da kolayca hesaplayamayız.
Draw mesh, bunun için bize vertex gölgelendiriciyi verir.
Peki, mesh nedir?
Ağ, her köşesinde veri bulunan üçgenlerden oluşur ve her köşeye köşe noktası denir.
Her köşe, küçük bir veri kaydı taşır.
Genellikle bu kayıt bir pozisyonu tutar, ancak bu zorunlu değildir.
Vertex gölgelendirici bu kaydı okur ve konumu buna göre hesaplar.
Burada bir üçgenin izlediği yol gösteriliyor: Tepe gölgelendirici her köşe için çalışır, köşelerin konumunu ve her piksel için iletmek istediğimiz değerleri hesaplar.
Ardından rasterleştirici, bu üçgenin kapsadığı her pikseli bulur ve her piksel için bu üç köşeden değerleri enterpole eder.
Son olarak, parça gölgelendirici bu pikseller için çalışır, bu enterpolasyonlu değerleri kullanır ve hesaplanan piksellerin rengini hesaplar.
Şimdi, daha basit bir örnekle tepe ve parça gölgelendiriciyi nasıl yazabileceğimizi görelim. Bu örnekte, üzerinde bir dalganın hareket ettiği düz bir çizgi kullanacağız.
LineShaders.VERTEX
uniform float2 uStart;
uniform float2 uEnd;
uniform float uHalfWidth;
uniform float uTime;
Varyings main(const Attributes a) {
Varyings v;
float2 dir = uEnd - uStart;
float2 normal = normalize(float2(-dir.y, dir.x));
float along = a.corner.x * length(dir);
float pulse = (a.corner.x - uTime) / 0.18;
float radius = uHalfWidth * (1.0 + 0.6 * exp(-pulse * pulse));
float across = a.corner.y * (radius + 1.0);
v.position = uStart + dir * a.corner.x + normal * across;
v.stroke = float3(along, across, radius);
return v;
}Tepe gölgelendirici, giriş olarak bir özellik alır ve çıkış olarak değişkenler döndürür.
Özellikler, tek bir köşenin verileridir.
Burada sadece iki sayı var: çizgi boyunca ilerlemesi ve çizginin hangi tarafında olduğu.
Ardından, değişkenler enterpolasyona tabi tutulur ve parça gölgelendiriciye iletilir.
Başlangıçtan itibaren piksel mesafesini hesaplarız ve kontur genişliği için yarıçapta bir nabız atışı oluşturmak üzere zamanı kullanırız.
Ardından bu yarıçapı köşe kenarıyla çarparak kontur boyunca işaret mesafesini elde ederiz.
GPU bu işaretler arasında enterpolasyon yaparken merkez çizgileri sıfır olur.
Son olarak, köşelerin konumunu hesaplar, tüm bu değerleri bir fırça darbesi varyasyonunda depolar ve bunları parça gölgelendiriciye iletiriz.
Çalışma zamanı gölgelendiricisinin aksine, ağ parçası gölgelendiricisi bir parametre aracılığıyla renk çıkışı verir ve ayarlanmışsa boyayı örneklemek için bir konum döndürür.
LineShaders.FRAGMENT
uniform float uLength;
layout(color) uniform float4 uStartColor;
layout(color) uniform float4 uEndColor;
float2 main(const Varyings v, out float4 color) {
float distance = abs(v.stroke.y);
float coverage = 1.0 - smoothstep(
v.stroke.z - 0.5, v.stroke.z + 0.5, distance);
float side = distance / v.stroke.z;
float4 ink = mix(uStartColor, uEndColor,
clamp(v.stroke.x / uLength + 0.4 * side * side, 0.0, 1.0));
color = ink * coverage;
return v.position;
}Enine koordinatın mutlak değerini aldığımızda, merkez çizgisinden piksel piksel uzaklığı elde ederiz.
Kapsamı elde etmek için bu mesafeyi yarıçapla karşılaştırırız. Renk için ise çizginin yolu boyunca karıştırma yapar ve gradyanı kenara doğru hafifçe bükeriz.
Bu kapsamı bir kapsamla çarptığımızda nihai rengimizi elde ederiz.
Compose tarafında, gölgelendiricilerimizi ve veri düzenimizi MeshSpecification nesnesiyle birbirine bağlarız.
val spec = remember {
MeshSpecification.make(
arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
vertexStride = 8,
arrayOf(Varying(TYPE_FLOAT3, "stroke")),
LineShaders.VERTEX, LineShaders.FRAGMENT)
}Öncelikle köşe özelliğimizi 0 baytından başlayan iki kayan nokta olarak tanımlarız.
İki kayan nokta 8 bayt yer kapladığından, arabellekteki her köşe adımı 8'dir.
Ardından, kontur varyasyonlarımızı üç kayan nokta olarak tanımlar ve az önce yazdığımız köşe ve parça gölgelendiriciyi iletiriz.
Vertex arabelleğini oluşturuyoruz.
fun lineCorners(segments: Int): ByteBuffer {
val buffer = ByteBuffer.allocateDirect((segments + 1) * 2 * 8)
.order(ByteOrder.nativeOrder())
for (i in 0..segments) {
val t = i / segments.toFloat()
buffer.putFloat(t).putFloat(-1f) // one side
buffer.putFloat(t).putFloat(1f) // the other side
}
return buffer.rewind() as ByteBuffer
}Çizgi boyunca her adımda ilerleme durumunu tamamlar, ilerleme durumunu 0 ile 1 arasında hesaplar ve arabelleğe iki köşe yazarız.
Son olarak, spesifikasyonu ve tamponu bir mesh nesnesinde birleştiririz.
val mesh = Mesh(
spec,
mode = Mesh.TRIANGLE_STRIP,
vertexBuffer = corners,
vertexCount = (segments + 1) * 2,
bounds = RectF(0f, 0f, size.width, size.height)
)Compose tuvali henüz drawMesh özelliğini göstermediğinden nativeCanvas özelliğine geçip ağı çiziyoruz.
Box(Modifier.fillMaxSize().drawWithCache {
val mesh = Mesh(...)
onDrawBehind {
mesh.setFloatUniform("uTime", time.value)
drawIntoCanvas {
it.nativeCanvas.drawMesh(mesh,android.graphics.BlendMode.DST, paint)
}
}
})Geometriyi ve pikseli kontrol ettiğiniz için, ağ çizimini kullanarak geometriye dayalı herhangi bir görsel efekt çizebilirsiniz.
Bu API hakkında daha fazla bilgiyi dokümanlardan edinebilirsiniz.
Compose 1.12'de geniş renk gamı desteğini kullanıma sunduk.
Peki bu tam olarak ne anlama geliyor?
Ekranların, geleneksel sRGB renk alanından farklı bir renk aralığı üretebildiğini biliyor muydunuz?
Örneğin, bu resme bakarsanız beyaz üçgenin, çoğu ekranın gösterebileceği standart sRGB renk aralığı olduğunu görürsünüz.
Ancak yeni ekranların bazıları, sarı üçgenle gösterilen daha geniş bir renk yelpazesini destekler. Bu renklerin bazıları daha doygun ve canlıdır.
Ancak bir rengi temsil ederken aslında o üçgenin her bir köşesinin açıklamasıyla temsil ederiz.
Bu gradyan karşılaştırmasına baktığınızda, ekran gamı ile gradyan interpolasyonu arasındaki farkı ayırt etmek önemlidir.
Standart bir sRGB ekranda veya muhtemelen bu projektörde, ekran bu sRGB gamının dışındaki renkleri fiziksel olarak oluşturamaz.
Bu nedenle, şu anda sahnede bunu size göstermek gerçekten çok zor.
Bu nedenle, geniş renk gamı desteği sayesinde renklerin Android'de daha iyi göründüğü konusunda bana güvenmeniz gerekecek.
Bu örnekte, renk duraklarının daha geniş bir renk uzayında enterpolasyonunun etkisini görüyorsunuz.
Renkleri geniş veya doğrusal bir renk alanında karıştırmak, bu gradyanın gerçek matematiksel yolunu değiştirir. Böylece, geniş renk gamı olmadan yaptığınızda oluşabilecek bu orta grileşme ve sert bantlanma önlenir.
Compose 1.12 ile her ekranda daha yumuşak gradyan karışımından ve desteklenen donanımlarda gerçek geniş renk gamı desteğinden yararlanabilirsiniz.
Cihazda doğru desteği etkinleştirmek için etkinliğinizin veya pencerenizin geniş renk gamı modunu etkinleştirecek şekilde ayarlandığından emin olmanız gerekir.
Bunu AndroidManifest'inizde yapabilirsiniz.
<!-- AndroidManifest.xml -->
<activity
android:name=".NavActivity"
android:colorMode="wideColorGamut"
android:exported="true">
</activity>Alternatif olarak, programatik olarak composable'ınızda da kullanabilirsiniz.
// Dynamic toggle in code
DisposableEffect(window) {
window?.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
onDispose {
window?.colorMode = ActivityInfo.COLOR_MODE_DEFAULT
}
}colorMode ayarlanmazsa pencere standart 8 bit sRGB'de kalır ve GPU tarafından oluşturulan tüm geniş renk gamı renkleri kırpılır.
Bu nedenle, ekranın desteklemediği geniş renk gamı renklerini kullanmaktan kaçınmak önemlidir.
Pil ve performans tasarrufu sağlamak için geniş renk gamutlarını yalnızca medya, fotoğraf veya zengin grafikler gösteren etkinliklerde ya da kod yollarında etkinleştirmeniz gerekir.
Bu, Compose'da farklı renk uzaylarında renkler oluşturabileceğiniz ve bunların artık dikkate alınacağı anlamına gelir.
// 1. Display P3 Color (Wider gamut: greens & reds are more saturated) colorMode=wideColorGamut
val VividP3Red = Color(
red = 1.0f,
green = 0.05f,
blue = 0.05f,
alpha = 1.0f,
colorSpace = ColorSpaces.DisplayP3
)
// 2. Extended sRGB (Enables HDR values > 1.0f) use colorMode=hdr
val HdrNeonCyan = Color(
red = 0.0f,
green = 1.8f, // Peak brightness > 1.0 in extended gamut
blue = 2.0f,
alpha = 1.0f,
colorSpace = ColorSpaces.ExtendedSrgb
)
// 3. BT.2020 HLG (Broadcast HDR standard, supported API 34+) - use colorMode=hdr
val HdrSunsetOrange = Color(
red = 1.0f,
green = 0.45f,
blue = 0.1f,
alpha = 1.0f,
colorSpace = ColorSpaces.Bt2020Hlg
)Renginizin oluşturulmasını istediğiniz colorSpace türünü iletebilirsiniz. Herhangi bir colorSpace iletmezseniz standart sRGB kullanılır.
Özellikle bit eşlemler için getColorSpace kullanarak bit eşlemin kaynağını kontrol edebilirsiniz. Popüler resim yükleme kitaplıkları, doğru bit eşlem renk alanlarını sizin için otomatik olarak işler.
Renk bilgilerini korumak için Bitmap.Config.HARDWARE kullanmanız gerekir. 565 gibi bir yapılandırma, hassasiyeti azaltır ve kullanıcıların görebileceği bantlanmaya neden olur.
Renkler hakkında daha fazla bilgi edinmek için 2017'ye gidip Romain Guy'ın Compose'da renk veya Android'de renk hakkındaki konuşmasını izleyebilirsiniz.
Compose 1.13 ile poligon şekli için yerel destek sunuyoruz.
Bu sayede özel şekilli ve organik görünümlü kullanıcı arayüzü öğeleri oluşturmak çok daha kolay olacak.
Daha önce, bu tür karmaşık bir geometri çizmek istiyorsanız bir yol oluşturacak geometriyi tanımlamak için RoundedPolygon kullanmanız ve ardından bu yolu bir Compose şekil API'siyle eşlemeniz gerekiyordu.
fun cookiePolygon(): RoundedPolygon {
val vertices = FloatArray(32)
for (i in 0 until 16) {
val radius = if (i % 2 == 0) 1f else 0.84f
// …
}
return RoundedPolygon(
vertices,
rounding = CornerRounding(0.12f)
)
}Örneğin, önce RoundedPolygon kullanarak oranları tanımlarsınız. Biz de bu snippet'te iç noktalarımızı, dış noktalarımıza kıyasla merkezden %84 uzaklıkta olacak şekilde ayarlarız.
Köşe yuvarlama özelliğini buradan da uygulayabilirsiniz ancak bu yalnızca matematiksel eğrileri tanımlar.
Bu poligonu Compose'da kırpma veya arka planlar için kullanmak istiyorsanız aşağıdaki gibi özel bir Shape sarmalayıcı yazmanız gerekir.
class RoundedPolygonShape(
polygon: RoundedPolygonShape
) : Shape {
// …
override fun createOutline(size: Size, layoutDirection: LayoutDirection,
density: Density,
): Outline {
path = polygon.toPath().asComposePath()
// …
matrix.scale(size.width / maxDimension, size.height / maxDimension)
matrix.translate(-bounds.left, -bounds.top)
// …
return Outline.Generic(path)
}
}Bu createOutline snippet'inde, yolu oluşturmanız ve bunları manuel olarak ölçeklendirip çevirmeniz gerektiğini görebilirsiniz.
Artık yeni PolygonShape API sayesinde bu özel Shape sarmalayıcısına ihtiyacınız yok.
private val AvatarShape = PolygonShape {
val vertices = List(16) { i ->
val radius = size.minDimension / 2f * if (i % 2 == 0) 1f else 0.86f
val angle = i * PI.toFloat() / 8f
size.center + Offset(cos(angle), sin(angle)) * radius
}
polygon(vertices, rounding = CornerRounding(radius = 8.dp))
}
// …
Modifier.clip(AvatarShape)Bu, özel Compose Shape oluşturucu olduğundan yoğunluğu ve boyutu doğrudan kullanıma hazır olarak alır.
Bu sayede, köşelerinizi dinamik olarak hesaplayabilir ve köşe yuvarlama değerinizi doğrudan dp cinsinden, hatta yüzde olarak belirtebilirsiniz.
Tanımlandıktan sonra, Shape alan herhangi bir API'ye iletebilirsiniz.
Ayrıca, farklı poligon şekilleri arasında sorunsuz bir şekilde geçiş yapmanıza olanak tanıyan MorphPolygonShape API'yi de kullanıma sunuyoruz.
val recordShape = remember {
MorphPolygonShape(
start = PolygonShape.circle(),
end = PolygonShape.star(
numPoints = 9,
innerRadiusRatio = 0.8f,
outerRounding = CornerRounding(percent = 50),
),
) { shapeProgress.value }
}Bu örnekte, etkileşimde bulunduğunuzda daireden yıldıza dönüşen bir kayıt düğmesi var.
Başlangıç şeklini, bitiş şeklini ve ilerleme değerini belirtmeniz yeterlidir.
MorphPolygonShape API'si, ilerleme değerini ana hat çözünürlüğü aşamasında okur. Yani, yeniden oluşturma sırasında değil, oluşturma sırasında okur.
Bu, şekliniz kare kare değişirken yeniden oluşturma işlemini tetiklemeden yeni bir ana hat oluşturacağı anlamına gelir.
Şimdi de bulanıklaştırmadan bahsedelim.
Son birkaç sürümde, bulanıklaştırma efektlerini uygulamayı çok daha kolay ve etkileyici hale getirmeye çalışıyoruz.
Bulanıklaştırmadan bahsettiğimizde, genellikle bu niteleyicinin kullanıldığını görürüz.
Modifier
.blur(
radiusX = 10.dp,
radiusY = 10.dp,
edgeTreatment = BlurredEdgeTreatment(
shape = RoundedCornerShape(8.dp),
),
)Compose, Android 12'den beri bu değiştiriciye sahiptir.
Birleştirilebilir öğenin tamamına aynı bulanıklık yarıçapının uygulandığı tek tip bir bulanıklık istediğinizde idealdir.
Ancak modern kullanıcı arayüzü tasarımlarında genellikle keskin bir kesim yerine keskin bölgelerden bulanık bölgelere yumuşak bir geçiş tercih edilir.
Bu da bizi, Compose 1.13'ten itibaren resmi bir API olan Progressive Blur'a getiriyor.
Artık tüm composable'da tek bir sabit yarıçap yerine, alan genelinde bulanıklık yoğunluğunu değiştirebilirsiniz.
Kullanıcı profili ekranındaki JetChat uygulamamızdan bir örneği aşağıda bulabilirsiniz.
Yeni bulanıklaştırma değiştirici, bulanıklaştırma yarıçapı spesifikasyonu alır.
Modifier.blur {
blurRadiusSpec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 0.dp),
BlurStop(1.0f, 32.dp),
)
)
edgeTreatment = BlurredEdgeTreatment.Unbounded
}Burada, iki bulanıklık noktası içeren dikey gradyan kullanıyoruz. Fotoğrafın üst yarısı 0 dp'de tamamen keskin kalırken alt kenarda 32 dp bulanıklığa ulaşıyor.
edgeTreatment değerini sınırsız olarak ayarladığımızı da fark etmişsinizdir. Bu, bulanıklaştırma efektine, birleştirilebilir sınırların dışındaki şeffaf pikseli de örneklemesini söyler.
Bu bulanıklık yarıçapı spesifikasyonu, dikey karartmaların yanı sıra aşamalı bulanıklık oluşturmak için çeşitli yerleşik yöntemler sunar.
Örneğin, solda başlangıç yarıçapı 0 dp olan ve bitiş yarıçapı kaydırma çubuğu değeriyle belirlenen yatay gradyanı görebilirsiniz.
BlurRadiusSpec.horizontalGradient(
startRadius = 0.dp,
endRadius = sliderValue.dp,
)Sağda ise dairesel renk geçişi var. Merkezde bulanıklık yarıçapı 0 dp. fallOffRadius'e (bu örnekte 300 dp) doğru ilerledikçe bulanıklık sorunsuz bir şekilde 24 dp'ye kadar artıyor.
BlurRadiusSpec.radialGradient(
center = center,
fallOffRadius = 300.dp,
startRadius = 0.dp,
endRadius = 24.dp,
)Tüm bu dikey, dairesel ve yatay gradyanlar sizin için yeterli değilse özel gölgelendiriciler kullanarak aşamalı bulanıklığınızı her zaman yönlendirebilirsiniz.
val customShader = remember {
RuntimeShader(
"""
uniform float2 size;
uniform float time;
half4 main(float2 coords) {
// Diagonal wave mask
float normalizedX = coords.x / size.x;
float normalizedY = coords.y / size.y;
float wave = sin((normalizedX + normalizedY) * 6.0 + time) * 0.5 + 0.5;
return half4(0.0, 0.0, 0.0, wave);
}
""".trimIndent()
)
}
val customBlurRadius = remember(customShader) {
BlurRadiusSpec.shader(maxRadius = 24.dp) { customShader }
}
Modifier.blur {
val w = size.width.toPx()
val h = size.height.toPx()
customShader.setFloatUniform("size", w, h)
customShader.setFloatUniform("time", animatedTime)
blurRadiusSpec = customBlurRadius
},Burada, animasyonlu bir çapraz sinüs dalgası hesaplayan ve bu yoğunluğu bir alfa kanalına çıkaran küçük bir parça gölgelendirici yazıyoruz.
Ardından gölgelendirici spesifikasyonu, bu 0 ila 1 maskesini doğrudan 24 dp'lik bir maskeyle eşler maxRadius.
Arka plan bulanıklığından bahsetmeden önce ikisi arasındaki farka bakalım.
Sol tarafta, katman bulanıklığı uyguladığımız bir üst uygulama çubuğu var.
Yani, composable'ın kendisini bulanıklaştırır.
Gördüğünüz gibi başlık ve işlem simgeleri bulanık ancak arkadaki sohbet bileşeni hiç bulanık değil.
Sağ tarafta ise arka plan bulanıklaştırma var.
Burada yalnızca üst çubuğun arkasında kayan sohbet mesajları örneklenir ve bulanıklaştırılır.
Ancak metin ve simgeler tamamen net ve okunabilir.
Android 37.2'den itibaren, RenderNode.setBackdropRenderEffect adlı yeni bir API'yi kullanarak arka planı bulanıklaştırabilirsiniz.
Ancak bulanıklaştırmanın nasıl iyileştirildiğine geçmeden önce, bulanıklaştırmanın performanslı olmasını sağlamak için Android'in arka planda nasıl çalıştığını anlayalım.
Android, arka planda donanım hızlandırmalı görüntüleme listesi kapsayıcıları olan RenderNode'ları kullanır. Bu sayede, kaydı başlatma ve kaydı sonlandırma aracılığıyla bir dizi farklı çizim komutunu sıkıştırıp depolayabilir.
RenderNode; 3D çevirileriniz, rotasyonlarınız, alfa, ölçekleriniz ve konum sınırlarınız gibi bilgileri içerir.
Compose, arka planda RenderNode kullanır.
Bu nedenle, Compose'da arka plan oluşturma efektlerini kullanmak için bu değiştiriciyi yeniden kullanabileceğimiz özel bir Modifier.Node oluşturabiliriz.
fun Modifier.backdropBlur(spec: BlurRadiusSpec, shape: Shape, tint: Color): Modifier =
this then BackdropBlurElement(spec, shape, tint)
private class BackdropBlurNode(...) : Modifier.Node(), DrawModifierNode {
private var renderNode: RenderNode? = null
override fun ContentDrawScope.draw() {
val node = getOrCreateNode()
if (isDirty || sizeChanged) {
node.setPosition(0, 0, widthPx, heightPx)
node.setBackdropRenderEffect(spec.createRenderEffect(this))
}
drawIntoCanvas { it.nativeCanvas.drawRenderNode(node) }
drawContent()
}
}RenderNode için bir referans tutarız ve RenderNode, başka bir işlevde istediğimiz efektlerle yapılandırılır.
Örneğin, burada bulanıklaştırma efekti uyguluyoruz.
Ardından, yerel tuvalimizde drawRenderNode yöntemini çağıran drawIntoCanvas yöntemini çağırırız.
Son olarak, bulanıklaştırılmamış asıl içeriği de üstte çizmek için drawContent yöntemini çağırırız.
Oluşturduğumuz bu değiştiriciyi kullanmak için Modifier.backdropBlur öğesini çağırıp bir bulanıklık yarıçapı spesifikasyonu iletiriz.
JetchatAppBar(
modifier = modifier
.backdropBlur(
tint = MaterialTheme.colorScheme
.surface.copy(alpha = 0.5f),
shape = RectangleShape,
spec = BlurRadiusSpec.uniform(5.dp)
)
)Bu yaklaşımda, alttaki katmana herhangi bir şey yerleştirmemiz gerekmediğini unutmayın.
RenderNode.setBackdropRenderEffect, JetChat uygulama çubuğunun altındaki içeriği otomatik olarak örnekliyor.
Sonuç olarak arka plan bu şekilde bulanıklaştırılıyor.
Ayrıca, yalnızca kademeli bir bulanıklık yerine kademeli bir bulanıklık yarıçapı spesifikasyonu da ayarlayabilir ve içerikte dikey bir bulanıklık da elde edebiliriz.
JetchatAppBar(
modifier = modifier
.backdropBlur(
tint = MaterialTheme.colorScheme
.surface.copy(alpha = 0.5f),
shape = RectangleShape,
spec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 32.dp),
BlurStop(1f, 0.dp),
),
)
)
)Dikey gradyanla, altındaki içeriği kapsayıcının farklı bölümlerinde farklı yarıçaplarla bulanıklaştırılmış olarak gösterir.
Compose'da yerleşik bir arka plan bulanıklaştırma değiştiricisi üzerinde çalışıyoruz.
Bu, Android'de Compose ile kullanılan temel teknolojinin yalnızca bir örneğidir.
Gösterdiğim bu örnek yalnızca API düzeyi 37.2'de çalışır ancak Compose için de bir geriye dönük uyarlama üzerinde çalışıyoruz. Gelişmeler için bizi takip etmeye devam edin.
RenderNode arka plan efektleriyle ilgili bir başka eğlenceli örnek de bunları birbirine bağlayarak çok güzel görünen kullanıcı arayüzleri oluşturabilmemiz ve bunun üzerine birden fazla farklı efekt uygulayabilmemizdir.
Efektleri zincirlemek için bu iki efekti oluştururuz.
val blur = RenderEffect.createBlurEffect(24f, 24f, Shader.TileMode.CLAMP)
// Boost saturation by 30% and apply blur together
val matrix = ColorMatrix().apply { setSaturation(1.3f) }
val vibrancy = RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(matrix))
val vibrantBlur = RenderEffect.createChainEffect(vibrancy, blur)Bu örnekte, bulanıklaştırma efekti ve %30 doygunluk oluşturuyoruz.
Önce bulanıklaştırma efektini, ardından doygunluk efektini uygulayarak createChainEffect işlevini çağırırız.
Bu, JetChat'ten bir örnektir.
val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")
val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
renderNode?.setBackdropRenderEffect(finalEffect)En alta atla düğmesi. Bu düğmeyi, birkaç farklı efekti birleştirerek küçük bir büyüteç gibi gösterebiliriz. Böylece bu composable'ın kenarları büyüteç gibi yuvarlak olur.
Ancak yalnızca büyütme efektleri uygulamakla kalmaz, aynı zamanda her türlü gölgelendiriciyi birbirine zincirleyerek arka plana uygulayabilirsiniz.
Bunları birleştirerek neler yapabileceğinizi gösteren, uyguladığımız bazı eğlenceli örnekleri burada bulabilirsiniz.
Geleneksel buzlu cam efekti artık kullanılabilir.
Ancak kaleydoskop gibi başka eğlenceli efektler de yapabiliriz.
Bu Android maskotunu, altındaki içeriği dönüştüren bir tür şekil değiştiren kullanıcı arayüzü öğesine dönüştürmek istersek bu küçük droidin içinden bakıyormuşuz gibi görünmesi için bu arka plan oluşturma efektlerini de uygulayabiliriz.
Bunu, arka plan oluşturma efektleriyle de yapabiliriz.
Bu yeni API ile yalnızca arka plan bulanıklaştırma ve buzlu cam efektleri kullanılabilir hale gelmedi, aynı zamanda hayal gücünüzün sınırlarını zorlayacak yeni özellikler de eklendi.
Platforma eklediğimiz bir diğer yeni API ise SurfaceView.setBlurRegions.
Şu anda Modifier.blur ve RenderNode Backdrop Effects'i kullanarak doku görünümlerini ve herhangi bir composable'ı bulanıklaştırabilirsiniz. Ancak SurfaceView'un bölümlerini bulanıklaştırmak zordur ve pencerede bir delik açtığı için uygulamanın görünüm RenderNode ağacının bir parçası değildir.
Bu nedenle, Compose aslında SurfaceView'un yedekleme arabelleğini örnekleyemez. Bu durum, API 37.2'ye kadar mümkün değildi.
Bu nedenle, SurfaceView'un setBlurRegions özelliği, uygulamanın bulanık bölgeleri doğrudan tanımlamasına olanak tanır.
Örneğin, JetChat'teki bu video oynatıcıda, kontroller gösterildiğinde ve gizlendiğinde SurfaceView bulanıklığı ekledik.
Bu yeni API'yi kullanmak için SurfaceView yönteminde setBlurRegions özelliğine erişebilirsiniz.
val region = RoundedRectBlurRegion().apply {
bounds = RectF(left, top, right, bottom)
setCornerRadii(spec.cornerRadiusPx.coerceAtLeast(0f))
alpha = spec.alpha.coerceIn(0f, 1f)
blurRadius = spec.blurRadiusPx.coerceAtLeast(0f)
}
surfaceView.setBlurRegions(listOf(region))
surfaceView.setBlurRegions(emptyList()) // ClearBu, sınır alanlarının listesini, bulanıklık yarıçapını, alfayı ve üzerindeki köşe yarıçaplarının listesini alır.
Bu sınırlar iletilirken ekrandaki SurfaceView'a göre olmalıdır.
Bu nedenle, SurfaceView'un koordinatlarını ve bulanıklaştırmak istediğiniz alanı takip etmeniz gerekir.
Android 16 ve sonraki sürümlerdeki bir diğer yeni özellik de RuntimeColorFilter.
Önceden, resimlerde yalnızca statik bir matris renk filtresi ayarlayabiliyor ve doygunluk gibi efektler uygulayabiliyordunuz.
Ancak Android 16 ve sonraki sürümlerde, giriş rengini (belirli bir içeriğin giriş pikseli) alan ve yeni bir renk üreten bir gölgelendirici iletebilirsiniz.
Örneğin, bir fotoğrafa siberpunk filtresi uygulamak istiyorsak RuntimeColorFilter için giriş olarak kullanılan küçük bir gölgelendirici tanımlayabiliriz.
@Language("AGSL")
private const val CYBERPUNK_SHADER = """
vec4 main(half4 in_color) {
float lum = dot(in_color.rgb, vec3(0.299, 0.587, 0.114));
vec3 shadow = vec3(0.06, 0.04, 0.25);
vec3 mid = vec3(0.92, 0.12, 0.58);
vec3 high = vec3(0.18, 0.94, 0.98);
vec3 col = lum < 0.5 ? mix(shadow, mid, lum * 2.0) : mix(mid, high, (lum - 0.5) * 2.0);
return vec4(mix(in_color.rgb, col, 0.7), in_color.a);
}
"""Bu gölgelendirici, siberpunk teması oluşturur ancak istediğiniz başka bir efekti de uygulayabilirsiniz.
Gölgeleyiciyi kullanmak için RuntimeColorFilter uygulayıp bunu bir Compose renk filtresine dönüştürüyoruz.
AsyncImage(
model = imageUri,
contentDescription = filter.displayName,
colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
.asComposeColorFilter(),
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)Bu örnekte de görebileceğiniz gibi, artık OpenGL, SurfaceView kurulumları veya benzeri konulara daha derinlemesine girmeden öncekinden daha dinamik efektler oluşturabiliyoruz.
Bu, yalnızca resimlere değil, tüm composable'lara da uygulanabilir.
Teşekkür ederiz.
Çok güzel bir efekt oluşturduk ancak uygulamamızın hâlâ hızlı ve sorunsuz çalıştığından emin olalım.
Görüşmede gezinirken tüm efektlerimiz birlikte çalışır.
Bu kaydırma işlemini ölçerek başlayalım.
@Test
fun scrollFling() = measureScroll(swipeSteps = 5)
private fun measureScroll(swipeSteps: Int) = rule.measureRepeated(
packageName = TARGET_PACKAGE,
metrics = defaultComposeScrollingMetrics(),
compilationMode = CompilationMode.Full(),
iterations = 5,
setupBlock = {
killProcess()
}
//...Uygulamayı yeni bir işlemde başlatan ve görüşme boyunca kaydırma işlemini çalıştıran bir makro karşılaştırma testi kullanıyoruz.
Ardından, son tarihleri kaçıran kareleri saymak için izi sorgularız.
select count(*) as late_frames
from actual_frame_timeline_slice
where upid = (select upid from process where name = 'com.example.compose.jetchat.perf')
and jank_type like '%App Deadline Missed%'Bu da altı gecikmeli kare olduğu anlamına gelir.
Son karelerden birini açalım.
Bu kareden hemen önce Compose, bir sonraki mesajı görünür hale gelmeden önce önceden getirir.
Önceden getirme işleminin kareler arasındaki kısa boşluğa sığması gerekir ancak bu mesajın hazırlanması, görüntüsünün de doğrudan ana iş parçacığında çözülmesine neden olur ve bu işlem, sonuç vermeden uzun süre çalışır.
Ana iş parçacığı bu görüntünün kodunu çözmeye çalıştığı için yeni kareler başlatılamaz.
Ardından, bu mesaj ekrana geldiğinde RenderThread'in bellek ayırması ve tüm bu pikselleri GPU'ya yüklemesi gerekir. Bu nedenle, aynı resim ikinci bir kareyi geciktirir.
Şimdi, zamanımızın nereye gittiğini görüyoruz. Ancak bu kareyi çizmek için tüm bu çalışmaya ihtiyacımız var mı?
Bunu nasıl anlayabiliriz?
İzleme bunu bize söyler.
Büyük bir bitmap, ekranda çok daha küçük bir alana çizildiğinde Android, doğrudan izin içine bir işaretçi kaydeder.
Piksel cinsinden iki sayı gösterir: tam bitmap boyutu ve ekranda gerçekte kapladığı alan.
Bu değeri ekran yoğunluğuna böldüğünüzde DP cinsinden değeri elde edersiniz.
Ardından, bu resmi bulmak için kodumuzda bu DP değeriyle arama yaparız.
Şimdi de bu konuyu inceleyelim.
Resim, kapsayıcının ne kadar büyük olduğunu bilmeyen painterResource ile yüklenir. Bu nedenle, resmin tamamının kodu tam çözünürlükte çözülür.
Image(
painter = painterResource(it),
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)Bu nedenle, bobinden AsyncImage ile değiştiriyoruz.
AsyncImage(
model = it,
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)Önce kapsayıcıyı ölçer, böylece yalnızca ihtiyaç duyduğu pikselin kodunu çözer ve bu işlemi farklı bir iş parçacığında yapar.
Aynı karşılaştırmayı tekrar çalıştırdığınızda ana iş parçacığındaki resim kod çözme işlemi 0'a, geç kare ise 2'ye düştü.
Bu iki karede hata ayıklama yapmayacağız ancak çıkarılacak sonuç basit: Kareye hiçbir zaman ulaşmayacak pikselleri hesaplamamaya çalışın.
Yani resim kod çözme işlemi, karelerin gecikmesine neden olacak kadar büyüktü ancak her maliyet, kare düşmesi olarak görünmez ve kodu yazarken önleyebileceğimiz daha küçük maliyetler de vardır.
Şimdi bunlardan ikisine bakalım.
RuntimeShader kullandığımızda bunu bir kez oluşturmamız gerekir. Çünkü yalnızca derlenip önbelleğe alınsa da her oluşturduğumuzda kaynak dizesini ayrıştırır.
Bir efekt her karede animasyon oluşturduğunda bunu bir graphicsLayer içine sarmalayabiliriz. Böylece, etrafındaki kullanıcı arayüzünü yeniden çizmek yerine yalnızca animasyonlu öğe yeniden kaydedilir.
"En alta git" düğmesi, sayfayı kaydırdığınızda görünür.
Bu efekt için iki RuntimeShader kullanılır ve düğme her göründüğünde her iki gölgelendiriciyi de kaynak dizesinden yeniden oluştururuz.
val lensShader = RuntimeShader(MAGNIFIER_LENS_SHADER).apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("zoom", zoom)
// …
val lensEffect = RenderEffect.createRuntimeShaderEffect(lensShader, "content")
// …
val glassShader = RuntimeShader(GLASS_FINISH_SHADER).apply {Bu iki gölgelendiriciyi ayrıştırmak ve efekti oluşturmak, düğmenin her gösterilişinde ana iş parçacığında yüzlerce mikrosaniye sürer.
Bunun yerine, gölgelendiriciyi tüm işlem için bir kez oluştururuz veya hatırlayabiliriz ve ardından üniformasını güncelleriz.
val lensShader = MagnifierShaders.lens.apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("zoom", zoom)
// …
private object MagnifierShaders {
val lens: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(MAGNIFIER_LENS_SHADER) }
val glassFinish: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(GLASS_FINISH_SHADER) }Bu değişiklikle birlikte, bu efekti oluşturmak yaklaşık 40 kat daha hızlı hale geldi ve düğme göründüğünde maliyet 20 kat azaldı.
Daha önce gördüğümüz ağ geçişli parıltıda, parıltı drawBehind içinde her karede animasyonlu olarak gösterilir.
Box(
modifier = Modifier
.matchParentSize()
.drawBehind {
// Behind the card, scaled past its bounds.
translate(top = -58.dp.toPx()) {
scale(scaleX = 1.38f, scaleY = 2.85f) {
with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}Bu nedenle, çizim aşamasında durumu okumak, oluşturma ve düzeni korur ancak görüntüleme listesi, tek bir çizim komutunu yerinde güncelleyemez.
Bu nedenle Compose, grafik katmanına sahip en yakın düğümü bulmak için ağaçta yukarı doğru ilerler. Bu örnekte, tüm ekranı kaplayan kırmızı çerçeve bu düğümdür. Compose, her karede bu katmanın tamamını yeniden kaydeder.
Kutunun üzerine graphicsLayer yerleştirildiğinde, gezinme yeşil çerçevede durur. Böylece yalnızca parlayan kutu yeniden kaydedilir ve ana iş parçacığı çizim dilimi saniyenin onda ikisi kadar azalır.
Box(
modifier = Modifier
.matchParentSize()
.graphicsLayer()
.drawBehind {
// Behind the card, scaled past its bounds.
translate(top = -58.dp.toPx()) {
scale(scaleX = 1.38f, scaleY = 2.85f) {
with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}Perfetto ile izlemede neyin yanlış gittiğini analiz etmek zor olabilir. Bu nedenle, bazı analizler toplamanıza yardımcı olması için izleme analizi becerisini kullanabilirsiniz.
Performanslı Compose kodu yazma konusunda daha fazla yardıma ihtiyacınız varsa Android KSA'yı yükleyip Compose performans becerisini kullanmaya başlayabilirsiniz.
android skills add perfetto-trace-analysis
android skills add jetpack-compose-performanceval gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
}
}val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
}
}val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
// ....
setVertex(1, 1, Offset(0.2f, 0.4f) + Offset(animatedOffset, animatedOffset), coral)
// ....
setVertex(3, 0, Offset(0.0f, 1.0f), sunshine)
// ....
}
}
Box(
modifier = modifier.padding(32.dp)
.safeContentPadding()
.aspectRatio(16 / 9f)
.fillMaxWidth()
.paint(gradientPainter)
)MeshGradientPainter(
rows = 2,
columns = 3,
hasBicubicColor = true,
) {
// Row 0 (top edge, y = 0.0f)
setVertex(0, 0, Offset(0.0000f, 0.0000f), Color(0xFFFF6B6B))
setVertex(0, 1, Offset(0.3300f, 0.0000f), Color(0xFFFFD6D6))
setVertex(0, 2, Offset(0.6700f, 0.0000f), Color(0xFFE8C3FF))
setVertex(0, 3, Offset(1.0000f, 0.0000f), Color(0xFFE396FF))
// ... remaining vertices for Rows 0, 1, and 2
}val transition = rememberInfiniteTransition(label = "userInputGlowMesh")
val phase1 by transition.animateFloat(
initialValue = 0f,
targetValue = (2 * PI).toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 2800, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "glowPhase1",
)
val phase2 by transition.animateFloat(
initialValue = 0f,
targetValue = (2 * PI).toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 4200, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "glowPhase2",
)MeshGradientPainter(
rows = 6,
columns = 5,
hasBicubicColor = true,
) {
for (r in 0..6) {
for (c in 0..5) {
val offset = if (r in 1..5 && c in 1..4) {
val ampX = when (r) {
1, 2 -> 0.048f
3 -> 0.042f
4 -> 0.028f
else -> 0.036f
}
val ampY = when (r) {
1, 2 -> 0.028f
3 -> 0.020f
4 -> 0.022f
else -> 0.016f
}
val d = orbit(c, r, ampX, ampY)
Offset(
x = (baseU + d.x).coerceIn(0.04f, 0.96f),
y = (baseV + d.y).coerceIn(0.05f, 0.95f),
)
} else {
Offset(baseU, baseV)
}
setVertex(r, c, offset, gridColors[r][c])
}
}
}val lines = remember {
path.asAndroidPath().flatten(0.5f)
}
//... inside draw scope
lines.forEach { line ->
//...
drawLine(
brush = Brush.linearGradient(...),
start = Offset(line.start.x,
line.start.y),
end = Offset(line.end.x, line.end.y),
strokeWidth = 30f,
)
//...
}LineShaders.VERTEX
uniform float2 uStart;
uniform float2 uEnd;
uniform float uHalfWidth;
uniform float uTime;
Varyings main(const Attributes a) {
Varyings v;
float2 dir = uEnd - uStart;
float2 normal = normalize(float2(-dir.y, dir.x));
float along = a.corner.x * length(dir);
float pulse = (a.corner.x - uTime) / 0.18;
float radius = uHalfWidth * (1.0 + 0.6 * exp(-pulse * pulse));
float across = a.corner.y * (radius + 1.0);
v.position = uStart + dir * a.corner.x + normal * across;
v.stroke = float3(along, across, radius);
return v;
}LineShaders.FRAGMENT
uniform float uLength;
layout(color) uniform float4 uStartColor;
layout(color) uniform float4 uEndColor;
float2 main(const Varyings v, out float4 color) {
float distance = abs(v.stroke.y);
float coverage = 1.0 - smoothstep(
v.stroke.z - 0.5, v.stroke.z + 0.5, distance);
float side = distance / v.stroke.z;
float4 ink = mix(uStartColor, uEndColor,
clamp(v.stroke.x / uLength + 0.4 * side * side, 0.0, 1.0));
color = ink * coverage;
return v.position;
}val spec = remember {
MeshSpecification.make(
arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
vertexStride = 8,
arrayOf(Varying(TYPE_FLOAT3, "stroke")),
LineShaders.VERTEX, LineShaders.FRAGMENT)
}fun lineCorners(segments: Int): ByteBuffer {
val buffer = ByteBuffer.allocateDirect((segments + 1) * 2 * 8)
.order(ByteOrder.nativeOrder())
for (i in 0..segments) {
val t = i / segments.toFloat()
buffer.putFloat(t).putFloat(-1f) // one side
buffer.putFloat(t).putFloat(1f) // the other side
}
return buffer.rewind() as ByteBuffer
}val mesh = Mesh(
spec,
mode = Mesh.TRIANGLE_STRIP,
vertexBuffer = corners,
vertexCount = (segments + 1) * 2,
bounds = RectF(0f, 0f, size.width, size.height)
)Box(Modifier.fillMaxSize().drawWithCache {
val mesh = Mesh(...)
onDrawBehind {
mesh.setFloatUniform("uTime", time.value)
drawIntoCanvas {
it.nativeCanvas.drawMesh(mesh,android.graphics.BlendMode.DST, paint)
}
}
})<!-- AndroidManifest.xml -->
<activity
android:name=".NavActivity"
android:colorMode="wideColorGamut"
android:exported="true">
</activity>// Dynamic toggle in code
DisposableEffect(window) {
window?.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
onDispose {
window?.colorMode = ActivityInfo.COLOR_MODE_DEFAULT
}
}// 1. Display P3 Color (Wider gamut: greens & reds are more saturated) colorMode=wideColorGamut
val VividP3Red = Color(
red = 1.0f,
green = 0.05f,
blue = 0.05f,
alpha = 1.0f,
colorSpace = ColorSpaces.DisplayP3
)
// 2. Extended sRGB (Enables HDR values > 1.0f) use colorMode=hdr
val HdrNeonCyan = Color(
red = 0.0f,
green = 1.8f, // Peak brightness > 1.0 in extended gamut
blue = 2.0f,
alpha = 1.0f,
colorSpace = ColorSpaces.ExtendedSrgb
)
// 3. BT.2020 HLG (Broadcast HDR standard, supported API 34+) - use colorMode=hdr
val HdrSunsetOrange = Color(
red = 1.0f,
green = 0.45f,
blue = 0.1f,
alpha = 1.0f,
colorSpace = ColorSpaces.Bt2020Hlg
)fun cookiePolygon(): RoundedPolygon {
val vertices = FloatArray(32)
for (i in 0 until 16) {
val radius = if (i % 2 == 0) 1f else 0.84f
// …
}
return RoundedPolygon(
vertices,
rounding = CornerRounding(0.12f)
)
}class RoundedPolygonShape(
polygon: RoundedPolygonShape
) : Shape {
// …
override fun createOutline(size: Size, layoutDirection: LayoutDirection,
density: Density,
): Outline {
path = polygon.toPath().asComposePath()
// …
matrix.scale(size.width / maxDimension, size.height / maxDimension)
matrix.translate(-bounds.left, -bounds.top)
// …
return Outline.Generic(path)
}
}private val AvatarShape = PolygonShape {
val vertices = List(16) { i ->
val radius = size.minDimension / 2f * if (i % 2 == 0) 1f else 0.86f
val angle = i * PI.toFloat() / 8f
size.center + Offset(cos(angle), sin(angle)) * radius
}
polygon(vertices, rounding = CornerRounding(radius = 8.dp))
}
// …
Modifier.clip(AvatarShape)val recordShape = remember {
MorphPolygonShape(
start = PolygonShape.circle(),
end = PolygonShape.star(
numPoints = 9,
innerRadiusRatio = 0.8f,
outerRounding = CornerRounding(percent = 50),
),
) { shapeProgress.value }
}Modifier
.blur(
radiusX = 10.dp,
radiusY = 10.dp,
edgeTreatment = BlurredEdgeTreatment(
shape = RoundedCornerShape(8.dp),
),
)Modifier.blur {
blurRadiusSpec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 0.dp),
BlurStop(1.0f, 32.dp),
)
)
edgeTreatment = BlurredEdgeTreatment.Unbounded
}BlurRadiusSpec.horizontalGradient(
startRadius = 0.dp,
endRadius = sliderValue.dp,
)BlurRadiusSpec.radialGradient(
center = center,
fallOffRadius = 300.dp,
startRadius = 0.dp,
endRadius = 24.dp,
)val customShader = remember {
RuntimeShader(
"""
uniform float2 size;
uniform float time;
half4 main(float2 coords) {
// Diagonal wave mask
float normalizedX = coords.x / size.x;
float normalizedY = coords.y / size.y;
float wave = sin((normalizedX + normalizedY) * 6.0 + time) * 0.5 + 0.5;
return half4(0.0, 0.0, 0.0, wave);
}
""".trimIndent()
)
}
val customBlurRadius = remember(customShader) {
BlurRadiusSpec.shader(maxRadius = 24.dp) { customShader }
}
Modifier.blur {
val w = size.width.toPx()
val h = size.height.toPx()
customShader.setFloatUniform("size", w, h)
customShader.setFloatUniform("time", animatedTime)
blurRadiusSpec = customBlurRadius
},fun Modifier.backdropBlur(spec: BlurRadiusSpec, shape: Shape, tint: Color): Modifier =
this then BackdropBlurElement(spec, shape, tint)
private class BackdropBlurNode(...) : Modifier.Node(), DrawModifierNode {
private var renderNode: RenderNode? = null
override fun ContentDrawScope.draw() {
val node = getOrCreateNode()
if (isDirty || sizeChanged) {
node.setPosition(0, 0, widthPx, heightPx)
node.setBackdropRenderEffect(spec.createRenderEffect(this))
}
drawIntoCanvas { it.nativeCanvas.drawRenderNode(node) }
drawContent()
}
}JetchatAppBar(
modifier = modifier
.backdropBlur(
tint = MaterialTheme.colorScheme
.surface.copy(alpha = 0.5f),
shape = RectangleShape,
spec = BlurRadiusSpec.uniform(5.dp)
)
)JetchatAppBar(
modifier = modifier
.backdropBlur(
tint = MaterialTheme.colorScheme
.surface.copy(alpha = 0.5f),
shape = RectangleShape,
spec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 32.dp),
BlurStop(1f, 0.dp),
),
)
)
)val blur = RenderEffect.createBlurEffect(24f, 24f, Shader.TileMode.CLAMP)
// Boost saturation by 30% and apply blur together
val matrix = ColorMatrix().apply { setSaturation(1.3f) }
val vibrancy = RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(matrix))
val vibrantBlur = RenderEffect.createChainEffect(vibrancy, blur)val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")
val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
renderNode?.setBackdropRenderEffect(finalEffect)val region = RoundedRectBlurRegion().apply {
bounds = RectF(left, top, right, bottom)
setCornerRadii(spec.cornerRadiusPx.coerceAtLeast(0f))
alpha = spec.alpha.coerceIn(0f, 1f)
blurRadius = spec.blurRadiusPx.coerceAtLeast(0f)
}
surfaceView.setBlurRegions(listOf(region))
surfaceView.setBlurRegions(emptyList()) // Clear@Language("AGSL")
private const val CYBERPUNK_SHADER = """
vec4 main(half4 in_color) {
float lum = dot(in_color.rgb, vec3(0.299, 0.587, 0.114));
vec3 shadow = vec3(0.06, 0.04, 0.25);
vec3 mid = vec3(0.92, 0.12, 0.58);
vec3 high = vec3(0.18, 0.94, 0.98);
vec3 col = lum < 0.5 ? mix(shadow, mid, lum * 2.0) : mix(mid, high, (lum - 0.5) * 2.0);
return vec4(mix(in_color.rgb, col, 0.7), in_color.a);
}
"""AsyncImage(
model = imageUri,
contentDescription = filter.displayName,
colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
.asComposeColorFilter(),
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)@Test
fun scrollFling() = measureScroll(swipeSteps = 5)
private fun measureScroll(swipeSteps: Int) = rule.measureRepeated(
packageName = TARGET_PACKAGE,
metrics = defaultComposeScrollingMetrics(),
compilationMode = CompilationMode.Full(),
iterations = 5,
setupBlock = {
killProcess()
}
//...select count(*) as late_frames
from actual_frame_timeline_slice
where upid = (select upid from process where name = 'com.example.compose.jetchat.perf')
and jank_type like '%App Deadline Missed%'Image(
painter = painterResource(it),
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)AsyncImage(
model = it,
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)val lensShader = RuntimeShader(MAGNIFIER_LENS_SHADER).apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("zoom", zoom)
// …
val lensEffect = RenderEffect.createRuntimeShaderEffect(lensShader, "content")
// …
val glassShader = RuntimeShader(GLASS_FINISH_SHADER).apply {val lensShader = MagnifierShaders.lens.apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("zoom", zoom)
// …
private object MagnifierShaders {
val lens: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(MAGNIFIER_LENS_SHADER) }
val glassFinish: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(GLASS_FINISH_SHADER) }Box(
modifier = Modifier
.matchParentSize()
.drawBehind {
// Behind the card, scaled past its bounds.
translate(top = -58.dp.toPx()) {
scale(scaleX = 1.38f, scaleY = 2.85f) {
with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}Box(
modifier = Modifier
.matchParentSize()
.graphicsLayer()
.drawBehind {
// Behind the card, scaled past its bounds.
translate(top = -58.dp.toPx()) {
scale(scaleX = 1.38f, scaleY = 2.85f) {
with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}android skills add perfetto-trace-analysis
android skills add jetpack-compose-performance