軟體工程師
開發人員關係工程師
準備好將 Android UI 從「堪用」提升到「藝術品」等級了嗎?
歡迎參加導覽,瞭解 Jetpack Compose 的最新圖像功能。我們將說明如何運用 Android 強大的新圖形功能,打造全新的視覺效果,包括精美的網格漸層和漸進式模糊效果。我們也會說明如何提升圖像功能的效能,同時不影響電池續航力或影格率。
PolygonShape 和 MorphPolygonShape API,可用於自訂幾何圖形。MorphPolygonShape 會在輪廓解析階段讀取進度,逐格更新形狀,不會觸發重組。RenderNode.setBackdropRenderEffect,用於取樣背景內容。開發人員可以鏈結背景效果,建立毛玻璃或放大效果。graphicsLayer 中。轉錄稿
大家好,歡迎參加「運用 Compose 和 Android 的最新圖像功能提升應用程式品質」課程。
我是 Rebecca Franks,今天與我一同上台的是
我是 Aayush Chaudhary。
大家好,我是 Rahul Ray。
我們發現,越來越多開發人員運用 AI 突破 Android 和 Compose 的極限,將絕妙構想化為現實。
我們也樂見應用程式提供優質、原生且令人愉悅的體驗。
我們知道,如果應用程式能讓使用者感到滿意,並持續獲得良好評分,通常會利用最新的系統層級 API 變更、在共用元素和模糊處理的介面之間保持連續性、在各種板型規格中都能提供良好的體驗、效能良好、不會讓使用者猜測,並突破裝置的可能性。
在今天的演講中,我們將介紹一些最新功能,協助您充分運用 Android,包括使用網格漸層、啟用廣色域支援、使用多邊形形狀進行令人愉悅的轉場、模糊處理的新功能,最後,我們將介紹在應用程式中新增圖形功能時,如何考量效能。
不過,在今天的講座中,我們會介紹堆疊中不同層級的一些新 API。
雖然舊版 View UI 工具包目前處於維護模式,但我們仍會持續投資 Jetpack Compose 採用的基礎 Android 圖形硬體轉譯引擎。
Android 的圖形可分為三個層級:第一層是 Compose API,所有高階的新 UI 圖形 API 都在這裡,例如網格漸層繪圖器、多邊形和漸進式模糊處理 API。
接著是中層,Compose 的 DrawScope、繪圖修飾符節點和 AGSL 可讓您建構自訂效果。
最後,我們有 Android 平台,Compose 隨時可以存取原生 Canvas,並在 Compose 修飾符中使用全新的 OS 轉譯原始物件。
今天我們會展示一些範例。
現在就開始吧。
首先,我們來看看 Compose 1.12 推出的新 API:網格漸層。
先前我們有線性、掃描和放射狀漸層,這些漸層非常適合用於簡單的轉場效果,但如果您想要豐富的多色混合效果,例如環境背景或泛光照明效果,則需要 2D 網格系統,讓位置和顏色在空間中混合。
如要建立這類網格漸層,請先指定色塊的列數和欄數。
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
}
}接著在結尾的 lambda 中呼叫 setVertex,並指派介於 0F 到 1F 的正規化偏移值,以及每個格線交點的顏色。
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)
)定義繪圖器後,只要透過繪圖修飾符,即可使用任何可組合項呼叫。
現在,我們在 JetChat 應用程式中新增了一項功能,只要輕觸兩下訊息,就能對訊息按讚。
如要建構這個效果,我們會設定 2x3 網格,並在元件中放置色標。
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
}請注意,我們在這裡將 hasBicubicColor 設為 true。
根據預設,顏色會以線性方式在三角形之間插補,有時可能會留下明顯的色塊。
開啟這項設定後,著色器會切換為平滑雙三次插補,讓顏色完美融合。
現在,如果想在輸入框後方建立像這樣的動畫環境光暈,該怎麼做?
如要使用網格漸層達到類似效果,需要兩個要素。
首先,內部頂點的流暢有機動作,以及柔化外緣的方法,這樣就不會在卡片周圍形成硬矩形。
因此,我們首先設定 rememberInfiniteTransition,其中兩個相位角的執行時間略有不同,讓動作感覺自然,而非機械化。
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",
)在這裡,我們為內部頂點加上動畫,並固定外部環,也就是 Alpha 值為 0 的邊緣頂點外部環。
由於外邊界是靜止且透明的,因此光暈會自然地向外擴散。
最後,我們將這些動畫偏移量提供給 MeshGradientPainter。
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])
}
}
}您剛才看到的網格漸層,是在 Android 14 以上版本中使用 drawMesh API 繪製而成。
接下來,我們將從頭開始建構特效,進一步瞭解這個 API。
在 JetChat 歡迎畫面中,我們要繪製一條看起來像緞帶的路徑。
請注意,這裡有兩個重點:筆劃寬度不固定,且顏色會沿著路徑的每個筆劃延伸。
我們來拆解一下,瞭解實際需要哪些元素才能打造這個效果。
對於每個像素,我們需要決定是否要繪製,以及要使用哪種顏色。
為此,我們需要兩個距離。
第一個值是像素與筆劃中心的距離。
第二個距離是像素從起點到終點的路徑長度。
這會告訴我們從漸層中挑選哪種顏色,並混合一點第一個值,使顏色曲線橫跨寬度。
那麼,我們能否只使用路徑和繪圖,沿著路徑繪製漸層?
所以,是的,我們可以。
這需要將路徑扁平化為一組短直線,同時提供沿著路徑的距離。
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,
)
//...
}接著,我們繪製每條線,並套用各自的漸層。
雖然這個方法可行,但每條線都需要個別的繪製呼叫和繪製物件,而且整個路徑仍只有單一寬度。
另一個選項是執行階段著色器,但只能取得容器邊界內的像素位置。
但並未說明繪製這條緞帶所需的資訊。
我們也無法輕易從像素位置計算。
Draw mesh 則提供頂點著色器。
什麼是網格?
網格由三角形組成,每個角都有資料,每個角稱為頂點。
每個頂點都包含一小筆資料記錄。
通常該記錄會保留一個位置,但並非必要。
頂點著色器會讀取該記錄,並據此計算位置。
以下是三角形的路徑:頂點著色器會針對每個角執行,計算角的位置,以及我們要為每個像素傳遞的任何值。
接著,光柵化器會找出該三角形涵蓋的所有像素,並根據這三個頂點的值,為每個像素插補值。
最後,片段著色器會針對這些像素執行,使用這些內插值並計算像素的顏色。
接下來,我們透過一個更簡單的例子,看看如何編寫頂點和片段著色器:一條直線,上面有脈衝沿著直線移動。
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;
}頂點著色器會將屬性做為輸入內容,並傳回變化。
屬性是單一角落的資料。
這裡只有兩個數字:沿著線的進度,以及線的哪一側。
接著,系統會內插變化值,並傳遞至片段著色器。
我們會計算與起點的像素距離,並根據時間設定筆劃寬度,在半徑上建立脈衝效果。
然後將半徑乘以角邊,即可得出筆劃的符號距離。
當 GPU 在這些正負號之間進行內插時,中心線會變成零。
最後,我們會計算角落的位置,並將所有這些值儲存到筆劃變化中,然後傳遞至片段著色器。
與執行階段著色器不同,網格片段著色器會透過參數輸出顏色,並傳回用於取樣繪製的位置 (如有設定)。
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;
}取橫向座標的絕對值,即可得出與中心線的像素距離。
接著,我們將該距離與半徑進行比較,以取得涵蓋範圍。至於顏色,我們會沿著線條的路徑混合,並將漸層效果稍微向邊緣彎曲。
將該涵蓋率乘以涵蓋率,就是最終顏色。
在 Compose 端,我們使用 MeshSpecification 物件將著色器和資料版面配置連結在一起。
val spec = remember {
MeshSpecification.make(
arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
vertexStride = 8,
arrayOf(Varying(TYPE_FLOAT3, "stroke")),
LineShaders.VERTEX, LineShaders.FRAGMENT)
}首先,我們將角屬性宣告為兩個浮點數,從位元組 0 開始。
由於兩個浮點數會占用 8 個位元組,因此緩衝區中的每個角跨距為 8。
接著,我們將筆劃變化宣告為三個浮點數,並傳入剛才編寫的頂點和片段著色器。
我們建構了頂點緩衝區。
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
}在線條的每個步驟中,我們會完成進度,計算從 0 到 1 的進度,並將兩個角寫入緩衝區。
最後,我們將規格和緩衝區合併為網格物件。
val mesh = Mesh(
spec,
mode = Mesh.TRIANGLE_STRIP,
vertexBuffer = corners,
vertexCount = (segments + 1) * 2,
bounds = RectF(0f, 0f, size.width, size.height)
)Compose 畫布尚未公開 drawMesh,因此我們會降至 nativeCanvas 並繪製網格。
Box(Modifier.fillMaxSize().drawWithCache {
val mesh = Mesh(...)
onDrawBehind {
mesh.setFloatUniform("uTime", time.value)
drawIntoCanvas {
it.nativeCanvas.drawMesh(mesh,android.graphics.BlendMode.DST, paint)
}
}
})由於您可以控制幾何圖形和像素,因此可以使用繪製網格繪製任何幾何圖形驅動的視覺效果。
如要進一步瞭解這個 API,請參閱說明文件。
Compose 1.12 開始支援廣色域。
這代表什麼意思?
你知道嗎?螢幕可以重現與傳統 sRGB 色域不同的色彩範圍。
舉例來說,在這張圖片中,白色三角形代表大多數螢幕可顯示的標準 sRGB 色彩範圍。
不過,有些新螢幕支援更廣的色域 (如黃色三角形所示),色彩更飽和鮮豔。
但當我們表示顏色時,實際上是用該三角形每個頂點的描述來表示。
查看這項漸層比較時,請務必區分顯示色域和漸層內插。
在標準 sRGB 螢幕或這台投影機上,螢幕可能無法實際呈現 sRGB 色域以外的色彩。
因此,現在要在台上示範這項功能,其實非常困難。
因此,請相信我,Android 支援廣色域後,色彩會更鮮豔。
在這個範例中,您實際看到的是在更廣的色域中插補色標的效果。
在廣色域或線性色域中混合色彩,會改變這個漸層的實際數學路徑,避免在不使用廣色域的情況下,出現這種中灰色和刺眼的條紋。
Compose 1.12 可讓您在任何螢幕上更流暢地混合漸層,並在支援的硬體上執行時,支援真正的廣色域。
如要在裝置上啟用正確的支援功能,請務必將活動或視窗設為啟用廣色域模式。
這項操作可以在 AndroidManifest 中完成。
<!-- 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
}
}如未設定 colorMode,視窗會維持標準 8 位元 sRGB,且 GPU 算繪的任何廣色域顏色都會遭到裁剪。
因此,如果螢幕不支援廣色域,請務必避免使用廣色域色彩。
請只在實際顯示媒體、相片或豐富圖片的活動或程式碼路徑上啟用廣色域,以節省電池用量並提升效能。
也就是說,您可以在 Compose 中建立不同色域的顏色,且系統現在會採用這些顏色。
// 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
)您可以傳入要建立顏色的 colorSpace 類型,如果未傳入任何 colorSpace,系統會使用標準 sRGB。
如果是點陣圖,您可以使用 getColorSpace 檢查點陣圖的來源,熱門的圖片載入程式庫會自動為您處理正確的點陣圖色域。
請使用 Bitmap.Config.HARDWARE 保留顏色資訊,因為 565 等設定實際上會失去精確度,並向使用者顯示可見的條帶。
如要進一步瞭解顏色,您可以回顧 2017 年 Romain Guy 談論 Compose 中的顏色,或 Android 中的顏色。
Compose 1.13 開始原生支援多邊形。
這能讓您更輕鬆地建構自訂形狀和自然外觀的 UI 元素。
以往如要繪製這類複雜幾何圖形,必須使用 RoundedPolygon 描述會輸出路徑的幾何圖形,然後將該路徑對應至 Compose 形狀 API。
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)
)
}舉例來說,首先,您可以使用 RoundedPolygon 定義比例,然後我們設定在這個程式碼片段中,我們將內點設定為與外點相比,距離中心 84%。
您也可以在這裡套用圓角,但這只會描述數學曲線。
如要在 Compose 中實際使用這個多邊形進行裁剪或背景,您需要編寫自訂 Shape 包裝函式,如下所示。
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)
}
}在這個 createOutline 程式碼片段中,您可以看到必須建立路徑,並手動縮放及轉譯。
現在有了新的 PolygonShape API,您就不再需要自訂 Shape 包裝函式。
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)由於這是專用的 Compose Shape 建構工具,因此可直接接收密度和大小。
這表示您可以動態計算頂點,並直接以 dp 或百分比指定圓角。
定義完成後,只要將其傳遞至任何採用 Shape 的 API 即可。
我們也推出了 MorphPolygonShape API,可讓您在不同多邊形形狀之間順暢轉換。
val recordShape = remember {
MorphPolygonShape(
start = PolygonShape.circle(),
end = PolygonShape.star(
numPoints = 9,
innerRadiusRatio = 0.8f,
outerRounding = CornerRounding(percent = 50),
),
) { shapeProgress.value }
}在這個範例中,我們有一個錄製按鈕,當您與其互動時,它會從圓形變成星形。
您只需提供起始形狀、結束形狀和進度值。
MorphPolygonShape API 會在輪廓解析階段讀取進度值,也就是說,不是在重組期間,也不是在組成期間。
這表示當形狀在影格之間變形時,只會解析成新的輪廓,不會觸發重組。
接著來談談模糊處理。
在過去幾個版本中,我們致力讓模糊效果的實作更簡單、更具表現力。
只要談到模糊處理,我們通常會看到這個修飾符。
Modifier
.blur(
radiusX = 10.dp,
radiusY = 10.dp,
edgeTreatment = BlurredEdgeTreatment(
shape = RoundedCornerShape(8.dp),
),
)Compose 自 Android 12 起就提供這個修飾符。
如果想在整個可組合項中套用完全相同的模糊半徑,達到一致的模糊效果,這個方法非常實用。
不過,現代 UI 設計通常會要求從清晰區域平滑轉換到模糊區域,而不是硬性切換。
這就帶我們來到漸進式模糊處理,從 Compose 1.13 開始,這項功能已成為官方 API。
現在,您可以在整個空間中調整模糊強度,不必在整個可組合項中使用單一常數半徑。
以下是 JetChat 應用程式使用者個人資料畫面的範例。
新的模糊修飾符會採用模糊半徑規格。
Modifier.blur {
blurRadiusSpec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 0.dp),
BlurStop(1.0f, 32.dp),
)
)
edgeTreatment = BlurredEdgeTreatment.Unbounded
}這裡我們使用垂直漸層,並設定兩個模糊停靠點,讓相片上半部在 0 dp 時完全清晰,並在底部邊緣逐漸增加到 32 dp 的模糊效果。
請注意,我們也將 edgeTreatment 設為無限制,這基本上會指示模糊效果,對可組合項邊界外的透明像素進行取樣。
除了垂直淡出效果,這項模糊半徑規格還提供多種內建方式,可產生漸進式模糊效果。
舉例來說,左側顯示的是水平漸層,起始半徑為 0 dp,結束半徑則由滑桿值決定。
BlurRadiusSpec.horizontalGradient(
startRadius = 0.dp,
endRadius = sliderValue.dp,
)右側則是放射狀漸層,中心點的模糊半徑為 0 dp,隨著向 fallOffRadius 移動 (此處為 300 dp),模糊效果會平滑增加至 24 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
},我們在這裡編寫一個小型片段著色器,用於計算動畫對角正弦波,並將該強度輸出至 Alpha 色板。
接著,著色器規格會將 0 到 1 的遮罩直接對應至 24 dp 遮罩 maxRadius。
在討論背景模糊處理之前,我們先來看看兩者的差異。
左側是套用圖層模糊效果的頂端應用程式列。
也就是說,它會模糊可組合項本身。
如您所見,標題和動作圖示會模糊,但後方的即時通訊元件完全不會模糊。
右側是背景模糊效果。
這裡只會對頂端列後方捲動的即時通訊訊息進行取樣和模糊處理。
不過,文字和圖示仍清晰可辨。
從 Android 37.2 開始,您可以使用名為 RenderNode.setBackdropRenderEffect 的新 API 模糊處理背景。
不過,在深入探討模糊效果的改善方式之前,請先瞭解 Android 的運作方式,確保模糊效果能發揮良好效能。
Android 實際上會使用 RenderNode,也就是硬體加速的顯示清單容器,這表示可以透過開始錄製和結束錄製,壓縮及儲存多個不同的繪圖指令。
RenderNode 包含 3D 轉譯、旋轉、Alpha、縮放和位置邊界等資訊。
Compose 實際上會在背景使用 RenderNode。
因此,如要在 Compose 中使用背景幕算繪效果,我們可以建立自訂 Modifier.Node,以便重複使用這個修飾符。
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 的參照,並在另一個函式中設定 RenderNode 的效果。
舉例來說,我們在這裡傳入模糊效果。
接著,我們呼叫 drawIntoCanvas,在原生畫布上呼叫 drawRenderNode。
最後,我們呼叫 drawContent,在頂端繪製實際未模糊處理的內容。
如要使用剛才建立的修飾符,請呼叫 Modifier.backdropBlur,並傳入模糊半徑規格。
JetchatAppBar(
modifier = modifier
.backdropBlur(
tint = MaterialTheme.colorScheme
.surface.copy(alpha = 0.5f),
shape = RectangleShape,
spec = BlurRadiusSpec.uniform(5.dp)
)
)請注意,使用這種方法時,我們不需要在底層放置任何內容。
RenderNode.setBackdropRenderEffect 會自動對 JetChat 應用程式列介面下方的內容進行取樣。
最終呈現出令人驚豔的背景模糊效果。
我們也可以設定漸進式模糊半徑規格,而不只是漸進式模糊,並在內容上取得垂直模糊。
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),
),
)
)
)垂直漸層會顯示下方的內容,並在容器的不同部分以不同半徑模糊處理。
我們正在開發 Compose 內建的背景模糊修飾符。
這只是 Android 採用 Compose 的基礎技術範例。
我展示的這個範例也只能在 API 級別 37.2 運作,但我們也正在為 Compose 進行向後移植,敬請期待。
RenderNode 背景幕效果的另一個有趣範例是,我們也可以將這些效果鏈結在一起,形成非常美觀的使用者介面,並在上面執行多種不同的效果。
因此,為了鏈結效果,我們建立了這兩種效果。
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)在這個範例中,我們建立模糊效果,並將飽和度設為 30%。
我們呼叫 createChainEffect,先套用模糊效果,再套用飽和度。
以下是 JetChat 的範例。
val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")
val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
renderNode?.setBackdropRenderEffect(finalEffect)跳轉按鈕:跳轉至底部按鈕,我們可以結合幾種不同的效果,讓這個可組合項的邊緣像放大鏡一樣圓潤,看起來就像一個小放大鏡。
除了放大效果,您也可以將任何類型的著色器鏈結在一起,套用至背景。
以下是我們實作的一些有趣範例,展示了結合這些功能後可以達成的效果。
現在可以實現傳統的磨砂玻璃效果。
但我們也可以做其他有趣的效果,例如萬花筒。
或者,如果想將這個 Android 吉祥物變成一種變形 UI 元素,用來轉換下方的內容,我們也可以套用這些背景幕算繪效果,讓它看起來像是透過這個小機器人看到的。
我們也可以使用背景幕轉譯效果。
因此,這個新 API 不僅能解鎖背景模糊和毛玻璃效果,還能讓您盡情發揮想像力。
我們在平台中推出的另一個新 API 是 SurfaceView.setBlurRegions。
目前,您可以使用 Modifier.blur 和 RenderNode 背景效果,模糊處理紋理檢視畫面和任何可組合項,但模糊處理 SurfaceView 的部分內容相當棘手,而且這不是應用程式檢視畫面 RenderNode 樹狀結構的一部分,因為它實際上會在視窗中穿孔。
因此,Compose 實際上無法對 SurfaceView 的備份緩衝區進行取樣,這在 API 37.2 之前是不可能做到的。
因此,SurfaceView 的 setBlurRegions 可讓應用程式直接宣告模糊區域。
舉例來說,在 JetChat 的影片播放器中,我們在控制項顯示和隱藏時,加入了 SurfaceView 模糊效果。
如要使用這個新 API,您可以在 SurfaceView 方法上存取 setBlurRegions。
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這會採用邊界區域、模糊半徑、Alpha 值和圓角半徑清單。
傳入這些邊界時,必須與畫面上的 SurfaceView 相關。
因此,您需要追蹤 SurfaceView 的座標和要模糊處理的區域。
Android 16 以上版本的另一項新功能是 RuntimeColorFilter。
過去,您只能為圖片設定靜態矩陣色彩濾鏡,並套用飽和度等效果。
不過,在 Android 16 以上版本中,您可以傳入著色器,該著色器會接收輸入顏色 (該特定內容的輸入像素),並產生新顏色。
舉例來說,如果想為相片套用賽博龐克濾鏡,可以定義一個小型著色器,做為 RuntimeColorFilter 的輸入內容。
@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);
}
"""這個著色器會產生電馭叛客主題,但你也可以製作任何其他效果。
因此,如要使用著色器本身,我們會套用 RuntimeColorFilter,並將其轉換為 Compose 顏色濾鏡。
AsyncImage(
model = imageUri,
contentDescription = filter.displayName,
colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
.asComposeColorFilter(),
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)如這個範例所示,我們現在可以建立比以往更動態的效果,不必深入研究 OpenGL、SurfaceView 設定或任何類似項目。
這項功能也適用於任何可組合項,不限於圖片。
感謝您!
我們已經建構了一些非常棒的效果,但請確保應用程式仍能快速流暢地運作。
捲動對話時,所有效果都會同時執行。
首先,我們來評估這個捲動事件。
@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%'因此有六個延遲影格。
我們來開啟其中一個延遲影格。
因此,在這一幀之前,Compose 會預先擷取下一則訊息,然後再向上捲動至檢視畫面。
預先擷取作業應在影格之間的短暫間隔內完成,但準備這則訊息時,也會在主要執行緒上解碼圖片,導致執行時間過長,且不會讓出資源。
由於主執行緒卡在解碼這張圖片,因此無法啟動新的影格。
接著,當該訊息實際捲動到檢視畫面時,RenderThread 必須分配記憶體,並將所有像素上傳至 GPU,因此同一張圖片會延遲第二個影格。
現在我們知道時間花在哪裡,但真的需要做這麼多工作才能繪製這個影格嗎?
如何判斷?
追蹤記錄會告訴我們。
每當大型點陣圖繪製到螢幕上較小的區域時,Android 都會在追蹤記錄中記錄標記。
系統會以像素為單位顯示兩個數字:完整點陣圖尺寸,以及在螢幕上實際涵蓋的區域。
除以螢幕密度,即可得到 DP 值。
接著,我們使用該 DP 值搜尋程式碼,找出該圖片。
這就是我們發現的結果。
圖片是以 painterResource 載入,但不知道容器的大小,因此會以完整解析度解碼整張圖片。
Image(
painter = painterResource(it),
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)因此,我們將其替換為 Coil 的 AsyncImage。
AsyncImage(
model = it,
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)它會先測量容器,因此只會解碼所需的像素,並在不同的執行緒上執行這項工作。
再次執行相同的基準測試,主執行緒上的圖片解碼作業降至 0,延遲影格則降至 2。
我們不會在這裡對這兩個影格進行偵錯,但重點很簡單:盡量不要計算不會到達影格的像素。
因此,圖片解碼作業會導致影格延遲,但並非所有成本都會顯示為影格遺漏,而且在編寫程式碼時,仍可避免一些較小的成本。
我們來看看其中兩種。
每次使用 RuntimeShader 時,都應該建立一次,因為雖然它只會編譯並快取,但每次建立時仍會剖析其來源字串。
每當效果為每個影格建立動畫時,我們都可以將其包裝在 graphicsLayer 中,這樣系統只會重新記錄動畫元素,而不會重新繪製周圍的 UI。
「跳至底部」按鈕則會在捲動時顯示。
這個範例使用兩個 RuntimeShader 呈現效果,每次按鈕出現時,都會從來源字串重新建立這兩個著色器。
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) }這項變更讓建構效果的速度提升近 40 倍,按鈕顯示時的成本也降低 20 倍。
在先前看到的網格漸層光暈中,光暈會在 drawBehind 內為每個影格加上動畫效果。
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)
}因此,在繪圖階段讀取狀態會保留組合結構和版面配置,但顯示清單無法就地更新單一繪圖指令。
因此,Compose 會向上遍歷樹狀結構,尋找具有圖形層的最接近節點,在這裡是紅色外框,也就是整個畫面,並在每個影格重新記錄整個圖層。
只要在方塊上加上 graphicsLayer,移動就會在綠色外框處停止,因此只有發光方塊會重新記錄,主執行緒繪製切片也會減少 0.2 毫秒。
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 時,可能難以分析追蹤記錄中的錯誤,因此您可以使用追蹤記錄分析技巧,協助收集一些深入分析資訊。
如需更多協助,瞭解如何編寫高效能的 Compose 程式碼,請務必安裝 Android CLI,並使用 Compose 效能技能。
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