
在 Jetpack Compose 中打造惊艳、交互流畅的 UI,核心在于把合适的工具用到位,GraphicsLayer 修饰符就是这样一个关键工具。
本文将深挖 GraphicsLayer 的潜力,演示如何用它打造独特、动态的用户体验。
你可以把 GraphicsLayer 直观理解为组件的画布:它会将子组件的渲染指令独立隔离,让你能单独对这一层做变换、特效与渲染优化,而不影响其他部分。
为了展示 GraphicsLayer 的通用性,我们会实现一套支持实时变换 (缩放、旋转、位移)的图片 UI,同时探索如何用颜色滤镜、混合模式做出高级视觉效果,最后还会演示:如何用极简代码,通过这个修饰符生成可导出、可分享的 Bitmap。
视图变换
我创建了一个 TransformSection 页面,专门演示 GraphicsLayer 在视觉变换上的强大能力。

页面通过滑块动态控制缩放、旋转、位移、透明度等属性,你可以实时修改 GraphicsLayer 内部内容的外观,只需要把滑块值赋给 GraphicsLayer 对应的属性即可。
示例代码如下:
Kotlin
private fun TransformSection() {
// ...
Image(
painter = painterResource(id = R.drawable.plant),
contentDescription = null,
modifier = Modifier
.size(180.dp)
.graphicsLayer {
this.scaleX = scaleX
this.scaleY = scaleY
this.translationX = (100 * translateX).dp.toPx()
this.translationY = (100 * translateY).dp.toPx()
this.transformOrigin = TransformOrigin(transformOrigin, transformOrigin)
this.rotationX = rotationX
this.rotationY = rotationY
this.rotationZ = rotationZ
this.alpha = alpha
this.clip = clip
this.shape = CircleShape
},
contentScale = ContentScale.Crop,
)
// ...
}
这套写法充分体现了 GraphicsLayer 高度灵活、可交互的组件操控能力。
渲染特效
除了几何变换,GraphicsLayer 另一大核心能力是支持颜色滤镜 与混合模式,可以高度定制视觉效果,让 UI 质感直接拉满。

Android Developers 官方 YouTube 频道发布过一段视频,Rebecca Franks 在视频里介绍了两个非常实用的修饰符,可以直接给视图应用这类特效。下面我们就基于 GraphicsLayer 封装这两个自定义修饰符。
1. 混合模式
blendMode 修饰符用于定义重叠内容在视觉上如何混合。
kotlin
private fun Modifier.applyBlendMode(blendMode: BlendMode): Modifier {
return this.drawWithCache {
val graphicsLayer = obtainGraphicsLayer()
graphicsLayer.apply {
record {
drawContent()
}
this.blendMode = blendMode
}
onDrawWithContent {
drawLayer(graphicsLayer)
}
}
}
2. 颜色滤镜
colorFilter 修饰符会对内容执行颜色变换,可实现灰度、复古滤镜、自定义着色等效果。
Kotlin
private fun Modifier.applyColorFilter(colorFilter: ColorFilter): Modifier {
return this.drawWithCache {
val graphicsLayer = obtainGraphicsLayer()
graphicsLayer.apply {
record {
drawContent()
}
this.colorFilter = colorFilter
}
onDrawWithContent {
drawLayer(graphicsLayer)
}
}
}
这两个修饰符和 GraphicsLayer 无缝配合,提供了极强的渲染控制力。
无论你想尝试混合模式,还是做复杂的颜色变换,这套方案都能轻松实现。
截图
GraphicsLayer 还有一个高频实用能力:轻松把 Compose 转换成 Bitmap,即通常我们所说的截图功能。

想象一个场景:你在 App 里让用户自定义海报、创意素材,然后一键分享。
借助 GraphicsLayer,这个功能不仅能完全实现,而且代码非常简洁。
示例如下:
kotlin
val graphicsLayer = rememberGraphicsLayer()
val coroutineScope = rememberCoroutineScope()
Box(
modifier = Modifier
.fillMaxWidth()
.border(2.dp, Color.Black)
.drawWithContent {
graphicsLayer.record {
this@drawWithContent.drawContent()
}
drawLayer(graphicsLayer)
}
.clickable {
coroutineScope.launch {
val bitmap = graphicsLayer
.toImageBitmap()
.asAndroidBitmap()
shareBitmap(bitmap, context)
}
}
) {
// 你的自定义内容
}
这段代码里,我们创建 GraphicsLayer 实例捕获内容,再通过 toImageBitmap() 转成 Bitmap,后续可进一步处理成可分享文件。
整个接入成本极低,也体现了 GraphicsLayer 的通用性。
What can i say,man,在 Jetpack Compose 里实现这类功能,真的非常简单。
总结
GraphicsLayer 提供的视图变换、动画控制、渲染特效能力,能让开发者突破常规 UI 设计边界,做出独特、动态的体验。
从缩放、旋转、位移 ,到混合模式、颜色滤镜 ,再到视图导出位图 ,GraphicsLayer 用极低的成本,打开了极大的创意空间。
本文我们依次探索了它的三大能力:
- 实时视图变换
- 高级渲染特效
- 位图导出与分享
无论你在做视觉炫酷的界面、加精致动效,还是做创意类功能,这个修饰符都非常值得用起来。希望该文章能给你带来启发,在项目里大胆尝试,把它的潜力完全释放出来。
祝你各位编码愉快!
完整代码
拷贝直接运行!
Kotlin
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Card
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.rememberGraphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.xetom.mlkitchen.R
import kotlinx.coroutines.launch
@Composable
fun GraphicLayerPage(
onNavigate: (Any) -> Unit,
onBack: () -> Unit,
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
item { TransformSection() }
item { HorizontalDivider() }
item { RenderEffectSection() }
item { HorizontalDivider() }
item { BitmapExportSection() }
}
}
// Section 1 - View Transformations
@Composable
private fun TransformSection() {
var scaleX by remember { mutableFloatStateOf(1f) }
var scaleY by remember { mutableFloatStateOf(1f) }
var rotationX by remember { mutableFloatStateOf(0f) }
var rotationY by remember { mutableFloatStateOf(0f) }
var rotationZ by remember { mutableFloatStateOf(0f) }
var translateX by remember { mutableFloatStateOf(0f) }
var translateY by remember { mutableFloatStateOf(0f) }
var alpha by remember { mutableFloatStateOf(1f) }
var transformOrigin by remember { mutableFloatStateOf(0.5f) }
var clip by remember { mutableStateOf(false) }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SectionTitle("1. View Transformations")
Image(
painter = painterResource(id = R.drawable.plant),
contentDescription = null,
modifier = Modifier
.size(180.dp)
.graphicsLayer {
this.scaleX = scaleX
this.scaleY = scaleY
this.translationX = (100 * translateX).dp.toPx()
this.translationY = (100 * translateY).dp.toPx()
this.transformOrigin = TransformOrigin(transformOrigin, transformOrigin)
this.rotationX = rotationX
this.rotationY = rotationY
this.rotationZ = rotationZ
this.alpha = alpha
this.clip = clip
this.shape = CircleShape
},
contentScale = ContentScale.Crop,
)
LabeledSlider("ScaleX", scaleX, 0.1f..3f) { scaleX = it }
LabeledSlider("ScaleY", scaleY, 0.1f..3f) { scaleY = it }
LabeledSlider("RotationX", rotationX, -180f..180f) { rotationX = it }
LabeledSlider("RotationY", rotationY, -180f..180f) { rotationY = it }
LabeledSlider("RotationZ", rotationZ, -180f..180f) { rotationZ = it }
LabeledSlider("TranslateX", translateX, -1f..1f) { translateX = it }
LabeledSlider("TranslateY", translateY, -1f..1f) { translateY = it }
LabeledSlider("Alpha", alpha, 0f..1f) { alpha = it }
LabeledSlider("Origin", transformOrigin, 0f..1f) { transformOrigin = it }
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text("Clip (CircleShape)", style = MaterialTheme.typography.bodyMedium)
Switch(checked = clip, onCheckedChange = { clip = it })
}
}
}
@Composable
private fun LabeledSlider(
label: String,
value: Float,
range: ClosedFloatingPointRange<Float>,
onValueChange: (Float) -> Unit,
) {
Column {
Text(
"$label: ${"%.2f".format(value)}",
style = MaterialTheme.typography.bodySmall,
fontSize = 10.sp,
)
Slider(
value = value,
onValueChange = onValueChange,
valueRange = range,
modifier = Modifier.padding(top = 4.dp).height(4.dp)
)
}
}
// Section 2 - Render Effects
private fun Modifier.applyBlendMode(blendMode: BlendMode): Modifier {
return this.drawWithCache {
val graphicsLayer = obtainGraphicsLayer()
graphicsLayer.apply {
record {
drawContent()
}
this.blendMode = blendMode
}
onDrawWithContent {
drawLayer(graphicsLayer)
}
}
}
private fun Modifier.applyColorFilter(colorFilter: ColorFilter): Modifier {
return this.drawWithCache {
val graphicsLayer = obtainGraphicsLayer()
graphicsLayer.apply {
record {
drawContent()
}
this.colorFilter = colorFilter
}
onDrawWithContent {
drawLayer(graphicsLayer)
}
}
}
private data class EffectItem(val name: String, val modifier: Modifier)
@Composable
private fun RenderEffectSection() {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SectionTitle("2. Render Effects")
Text("ColorFilter", style = MaterialTheme.typography.titleSmall)
val grayscaleMatrix = remember {
ColorMatrix().apply { setToSaturation(0f) }
}
val sepiaMatrix = remember {
ColorMatrix(
floatArrayOf(
0.393f, 0.769f, 0.189f, 0f, 0f,
0.349f, 0.686f, 0.168f, 0f, 0f,
0.272f, 0.534f, 0.131f, 0f, 0f,
0f, 0f, 0f, 1f, 0f,
)
)
}
val invertMatrix = remember {
ColorMatrix(
floatArrayOf(
-1f, 0f, 0f, 0f, 255f,
0f, -1f, 0f, 0f, 255f,
0f, 0f, -1f, 0f, 255f,
0f, 0f, 0f, 1f, 0f,
)
)
}
val colorFilterEffects = remember {
listOf(
EffectItem("Original", Modifier),
EffectItem("Grayscale", Modifier.applyColorFilter(ColorFilter.colorMatrix(grayscaleMatrix))),
EffectItem("Sepia", Modifier.applyColorFilter(ColorFilter.colorMatrix(sepiaMatrix))),
EffectItem("Invert", Modifier.applyColorFilter(ColorFilter.colorMatrix(invertMatrix))),
EffectItem("Tint Red", Modifier.applyColorFilter(ColorFilter.tint(Color.Red.copy(alpha = 0.3f)))),
)
}
LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
items(colorFilterEffects, key = { it.name }) { effect ->
EffectCard(effect)
}
}
Spacer(modifier = Modifier.height(8.dp))
Text("BlendMode", style = MaterialTheme.typography.titleSmall)
val blendModeEffects = remember {
listOf(
EffectItem("Multiply", Modifier.applyBlendMode(BlendMode.Multiply)),
EffectItem("Screen", Modifier.applyBlendMode(BlendMode.Screen)),
EffectItem("Overlay", Modifier.applyBlendMode(BlendMode.Overlay)),
EffectItem("Darken", Modifier.applyBlendMode(BlendMode.Darken)),
EffectItem("Lighten", Modifier.applyBlendMode(BlendMode.Lighten)),
EffectItem("ColorDodge", Modifier.applyBlendMode(BlendMode.ColorDodge)),
)
}
LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
items(blendModeEffects, key = { it.name }) { effect ->
EffectCard(effect)
}
}
}
}
@Composable
private fun EffectCard(effect: EffectItem) {
Card {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(8.dp),
) {
Image(
painter = painterResource(id = R.drawable.plant),
contentDescription = null,
modifier = Modifier
.size(120.dp)
.then(effect.modifier),
contentScale = ContentScale.Crop,
)
Spacer(modifier = Modifier.height(4.dp))
Text(effect.name, style = MaterialTheme.typography.labelSmall)
}
}
}
// Section 3 - Bitmap Export
@Composable
private fun BitmapExportSection() {
val graphicsLayer = rememberGraphicsLayer()
val coroutineScope = rememberCoroutineScope()
var capturedBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SectionTitle("3. Bitmap Export")
Text(
"Tap the content below to capture it as a bitmap.",
style = MaterialTheme.typography.bodyMedium,
)
Box(
modifier = Modifier
.fillMaxWidth()
.border(2.dp, Color.Black)
.drawWithContent {
graphicsLayer.record {
this@drawWithContent.drawContent()
}
drawLayer(graphicsLayer)
}
.clickable {
coroutineScope.launch {
capturedBitmap = graphicsLayer.toImageBitmap()
}
},
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(id = R.drawable.plant),
contentDescription = null,
modifier = Modifier.size(120.dp),
contentScale = ContentScale.Crop,
)
Column {
Text(
"GraphicsLayer Demo",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
"Tap to capture as bitmap. Result shown below.",
style = MaterialTheme.typography.bodySmall,
)
}
}
}
capturedBitmap?.let { bitmap ->
Text("Captured bitmap:", style = MaterialTheme.typography.titleSmall)
Image(
bitmap = bitmap,
contentDescription = "Captured bitmap",
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
contentScale = ContentScale.Fit,
)
}
}
}
// region Common
@Composable
private fun SectionTitle(title: String) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
)
}