ink-app-builder
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseThe Android Ink API () is a modular Jetpack library for building
high-performance, low-latency freehand drawing experiences on Android. It uses a
wet ink and dry ink two-phase model:
androidx.ink- Wet ink : Real-time, ultra-low-latency rendering while the user is drawing, handled by the composable.
InProgressStrokes - Dry ink : Finalized, immutable objects rendered by the app using
Stroke.CanvasStrokeRenderer
Android Ink API () 是一个模块化 Jetpack 库,用于在 Android 上构建
高性能、低延迟的自由手绘体验。它采用 湿墨和干墨 两阶段模型:
androidx.ink- 湿墨:在用户绘制时进行实时、超低延迟渲染,由 composable 处理。
InProgressStrokes - 干墨:已定型且不可变的 对象,由应用使用
Stroke渲染。CanvasStrokeRenderer
Setup and dependencies
设置与依赖项
The Ink API requires , which corresponds to Android 8.0 (API
level 26) and higher. All modules MUST share an identical version
in . Ask user permission before modifying project
build files.
minSdk = 26androidx.inkgradle/libs.versions.tomlInk API 要求 ,即支持 Android 8.0(API 级别 26)及更高版本。所有 模块在 中必须使用完全相同的版本。修改项目构建文件前,请先征得用户许可。
minSdk = 26androidx.inkgradle/libs.versions.tomlVersion selection (1.0.0
stable versus 1.1.0-alpha03
and higher)
1.0.01.1.0-alpha03版本选择(1.0.0
稳定版与 1.1.0-alpha03
及更高版本)
1.0.01.1.0-alpha03- Use (Stable) for production apps that rely on
1.0.0or pre-serializedStockBrushesassets decoded with.brush. InBrushFamily.decodestable,1.0.0,BrushFamily.encode,BrushFamily.decode, andAndroidBrushFamilySerializationrequireBrushFamilyDecodeCallback.@OptIn(ExperimentalInkCustomBrushApi::class) - Use and higher when the app constructs programmatic custom brushes (
1.1.0-alpha03,BrushFamily,BrushCoat,BrushTip,BrushPaint) in Kotlin code. Starting inBrushBehaviorand higher,1.1.0-alpha02serialization requires noBrushFamily(and in@OptInand higher,1.1.0-alpha05isExperimentalInkCustomBrushApi, so NEVER annotate@RestrictTo(LIBRARY_GROUP)and higher code with it).1.1.0-alpha03
- 对于依赖 或使用
StockBrushes解码预序列化BrushFamily.decode资源的生产应用,使用.brush(稳定版)。在1.0.0稳定版中,1.0.0、BrushFamily.encode、BrushFamily.decode和AndroidBrushFamilySerialization需要使用BrushFamilyDecodeCallback。@OptIn(ExperimentalInkCustomBrushApi::class) - 当应用在 Kotlin 代码中以编程方式创建自定义笔刷(、
BrushFamily、BrushCoat、BrushTip、BrushPaint)时,使用BrushBehavior及更高版本。从1.1.0-alpha03及更高版本开始,1.1.0-alpha02序列化不再需要BrushFamily;在@OptIn及更高版本中,1.1.0-alpha05带有ExperimentalInkCustomBrushApi,因此绝不要为@RestrictTo(LIBRARY_GROUP)及更高版本的代码添加此注解。1.1.0-alpha03
Version catalog (gradle/libs.versions.toml
)
gradle/libs.versions.toml版本目录(gradle/libs.versions.toml
)
gradle/libs.versions.toml[versions]
ink = "<latest_version>"
inputMotionPrediction = "<latest_version>"
[libraries]
# Core Ink modules
androidx-ink-authoring = { module = "androidx.ink:ink-authoring", version.ref = "ink" }
androidx-ink-brush = { module = "androidx.ink:ink-brush", version.ref = "ink" }
androidx-ink-geometry = { module = "androidx.ink:ink-geometry", version.ref = "ink" }
androidx-ink-nativeloader = { module = "androidx.ink:ink-nativeloader", version.ref = "ink" }
androidx-ink-rendering = { module = "androidx.ink:ink-rendering", version.ref = "ink" }
androidx-ink-strokes = { module = "androidx.ink:ink-strokes", version.ref = "ink" }
androidx-ink-storage = { module = "androidx.ink:ink-storage", version.ref = "ink" }
# Compose integration modules
androidx-ink-authoring-android = { module = "androidx.ink:ink-authoring-android", version.ref = "ink" }
androidx-ink-authoring-compose = { module = "androidx.ink:ink-authoring-compose", version.ref = "ink" }
androidx-ink-brush-compose = { module = "androidx.ink:ink-brush-compose", version.ref = "ink" }
androidx-ink-geometry-compose = { module = "androidx.ink:ink-geometry-compose", version.ref = "ink" }
# Motion prediction (required for low-latency stylus tracking)
androidx-input-motionprediction = { module = "androidx.input:input-motionprediction", version.ref = "inputMotionPrediction" }
# Persistence (kotlinx.serialization + Room with KSP)
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }[versions]
ink = "<latest_version>"
inputMotionPrediction = "<latest_version>"
[libraries]
# Core Ink modules
androidx-ink-authoring = { module = "androidx.ink:ink-authoring", version.ref = "ink" }
androidx-ink-brush = { module = "androidx.ink:ink-brush", version.ref = "ink" }
androidx-ink-geometry = { module = "androidx.ink:ink-geometry", version.ref = "ink" }
androidx-ink-nativeloader = { module = "androidx.ink:ink-nativeloader", version.ref = "ink" }
androidx-ink-rendering = { module = "androidx.ink:ink-rendering", version.ref = "ink" }
androidx-ink-strokes = { module = "androidx.ink:ink-strokes", version.ref = "ink" }
androidx-ink-storage = { module = "androidx.ink:ink-storage", version.ref = "ink" }
# Compose integration modules
androidx-ink-authoring-android = { module = "androidx.ink:ink-authoring-android", version.ref = "ink" }
androidx-ink-authoring-compose = { module = "androidx.ink:ink-authoring-compose", version.ref = "ink" }
androidx-ink-brush-compose = { module = "androidx.ink:ink-brush-compose", version.ref = "ink" }
androidx-ink-geometry-compose = { module = "androidx.ink:ink-geometry-compose", version.ref = "ink" }
# Motion prediction (required for low-latency stylus tracking)
androidx-input-motionprediction = { module = "androidx.input:input-motionprediction", version.ref = "inputMotionPrediction" }
# Persistence (kotlinx.serialization + Room with KSP)
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }App module configuration (app/build.gradle.kts
)
app/build.gradle.ktsApp 模块配置(app/build.gradle.kts
)
app/build.gradle.ktsplugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp) // Mandatory when using Room in Kotlin
}
android {
defaultConfig {
minSdk = 26
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.ink.authoring)
implementation(libs.androidx.ink.brush)
implementation(libs.androidx.ink.geometry)
implementation(libs.androidx.ink.nativeloader)
implementation(libs.androidx.ink.rendering)
implementation(libs.androidx.ink.strokes)
implementation(libs.androidx.ink.storage)
implementation(libs.androidx.ink.authoring.android)
implementation(libs.androidx.ink.authoring.compose)
implementation(libs.androidx.ink.brush.compose)
implementation(libs.androidx.ink.geometry.compose)
implementation(libs.androidx.input.motionprediction)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler) // NEVER use annotationProcessor in Kotlin
}plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp) // Mandatory when using Room in Kotlin
}
android {
defaultConfig {
minSdk = 26
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.ink.authoring)
implementation(libs.androidx.ink.brush)
implementation(libs.androidx.ink.geometry)
implementation(libs.androidx.ink.nativeloader)
implementation(libs.androidx.ink.rendering)
implementation(libs.androidx.ink.strokes)
implementation(libs.androidx.ink.storage)
implementation(libs.androidx.ink.authoring.android)
implementation(libs.androidx.ink.authoring.compose)
implementation(libs.androidx.ink.brush.compose)
implementation(libs.androidx.ink.geometry.compose)
implementation(libs.androidx.input.motionprediction)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler) // NEVER use annotationProcessor in Kotlin
}Core drawing surface and default brush
核心绘制界面与默认笔刷
Create a standard pressure-sensitive pen with
(from ). The
parameter defines the minimum distance between two input points, in stroke-space
units, to be considered visually distinct ( for general drawing,
for fine precision tools; enforces ):
BrushBrush.createWithComposeColorink-brush-composeepsilon0.1f0.01fBrush0f < epsilon <= sizekotlin
/**
* Creates a default pressure-sensitive pen [Brush].
*
* The `epsilon` parameter sets the geometric tolerance (minimum point distance
* in stroke units) controlling how far a stroke can be zoomed before triangle
* artifacts appear (`0f < epsilon <= size`).
*/
fun createDefaultPenBrush(): Brush {
return Brush.createWithComposeColor(
family = StockBrushes.pressurePen(),
color = Color.Black,
size = 5f,
epsilon = 0.1f,
)
}Stack two layers inside a for the drawing surface: a Compose that
renders finalized dry objects inside using
, and an composable on top for
low-latency wet ink capture. Encapsulate a synchronous Compose
Snapshot State buffer inside so the dry invalidates in
the exact same UI thread run loop when fires, preventing a
1-frame wet-to-dry flicker:
BoxCanvasStrokenativeCanvas.withSaveCanvasStrokeRendererInProgressStrokespendingStrokesDrawingSurfaceCanvasonStrokesFinishedkotlin
@Composable
fun DrawingSurface(
strokes: List<Stroke>,
canvasStrokeRenderer: CanvasStrokeRenderer,
currentBrush: Brush,
onGetNextBrush: () -> Brush,
onStrokesFinished: (List<Stroke>) -> Unit,
isEraserMode: Boolean,
modifier: Modifier = Modifier,
textureStore: AppTextureBitmapStore = LocalTextureStore.current,
onErase: (Float, Float) -> Unit = { _, _ -> },
onEraseStart: () -> Unit = {},
onEraseEnd: () -> Unit = {},
) {
val cacheGen by textureStore.generation.collectAsStateWithLifecycle()
// Holds newly finished strokes synchronously for the single frame before
// asynchronous StateFlow collection delivers the updated strokes list.
var pendingStrokes by remember { mutableStateOf<List<Stroke>>(emptyList()) }
SideEffect {
if (pendingStrokes.isNotEmpty()) {
pendingStrokes = if (strokes.isEmpty()) {
emptyList()
} else {
pendingStrokes.filter { pending -> strokes.none { it === pending } }
}
}
}
val strokesToDraw = remember(strokes, pendingStrokes) {
if (pendingStrokes.isEmpty()) {
strokes
} else {
strokes + pendingStrokes.filter { pending -> strokes.none { it === pending } }
}
}
val identityMatrix = remember { Matrix() }
val currentOnErase by rememberUpdatedState(onErase)
val currentOnEraseStart by rememberUpdatedState(onEraseStart)
val currentOnEraseEnd by rememberUpdatedState(onEraseEnd)
Box(modifier = modifier) {
// Layer 1: Dry ink - completed strokes + same-frame pending handoff strokes
Canvas(modifier = Modifier.fillMaxSize()) {
val nativeCanvas = drawContext.canvas.nativeCanvas
strokesToDraw.fastForEach { stroke ->
nativeCanvas.withSave {
canvasStrokeRenderer.draw(
canvas = this,
stroke = stroke,
strokeToScreenTransform = identityMatrix,
)
}
}
}
// Layer 2: Eraser touch handler OR Wet ink (InProgressStrokes)
if (isEraserMode) {
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { currentOnEraseStart() },
onDragEnd = { currentOnEraseEnd() },
onDragCancel = { currentOnEraseEnd() },
onDrag = { change, _ ->
currentOnErase(change.position.x, change.position.y)
change.consume()
},
)
}
)
} else {
key(cacheGen) {
InProgressStrokes(
defaultBrush = currentBrush,
nextBrush = onGetNextBrush,
onStrokesFinished = { newStrokes ->
pendingStrokes = pendingStrokes + newStrokes
onStrokesFinished(newStrokes)
},
textureBitmapStore = textureStore,
)
}
}
}
}使用 (来自 )创建标准的压感笔 。
参数定义两个输入点之间在笔触空间单位下可被视为视觉上不同的最小距离(一般绘制使用
,精细精度工具使用 ; 要求 ):
Brush.createWithComposeColorink-brush-composeBrushepsilon0.1f0.01fBrush0f < epsilon <= sizekotlin
/**
* Creates a default pressure-sensitive pen [Brush].
*
* The `epsilon` parameter sets the geometric tolerance (minimum point distance
* in stroke units) controlling how far a stroke can be zoomed before triangle
* artifacts appear (`0f < epsilon <= size`).
*/
fun createDefaultPenBrush(): Brush {
return Brush.createWithComposeColor(
family = StockBrushes.pressurePen(),
color = Color.Black,
size = 5f,
epsilon = 0.1f,
)
}在绘制界面的 中堆叠两层:一个 Compose ,在 内使用
渲染已定型的干墨 对象;其上方放置一个
composable,用于以低延迟捕获湿墨。在 内封装一个同步的
Compose Snapshot State 缓冲区,使干墨 能够在 触发时的
同一 UI 线程运行循环中失效重绘,从而避免出现一帧湿墨到干墨的闪烁:
BoxCanvasnativeCanvas.withSaveCanvasStrokeRendererStrokeInProgressStrokesDrawingSurfacependingStrokesCanvasonStrokesFinishedkotlin
@Composable
fun DrawingSurface(
strokes: List<Stroke>,
canvasStrokeRenderer: CanvasStrokeRenderer,
currentBrush: Brush,
onGetNextBrush: () -> Brush,
onStrokesFinished: (List<Stroke>) -> Unit,
isEraserMode: Boolean,
modifier: Modifier = Modifier,
textureStore: AppTextureBitmapStore = LocalTextureStore.current,
onErase: (Float, Float) -> Unit = { _, _ -> },
onEraseStart: () -> Unit = {},
onEraseEnd: () -> Unit = {},
) {
val cacheGen by textureStore.generation.collectAsStateWithLifecycle()
// Holds newly finished strokes synchronously for the single frame before
// asynchronous StateFlow collection delivers the updated strokes list.
var pendingStrokes by remember { mutableStateOf<List<Stroke>>(emptyList()) }
SideEffect {
if (pendingStrokes.isNotEmpty()) {
pendingStrokes = if (strokes.isEmpty()) {
emptyList()
} else {
pendingStrokes.filter { pending -> strokes.none { it === pending } }
}
}
}
val strokesToDraw = remember(strokes, pendingStrokes) {
if (pendingStrokes.isEmpty()) {
strokes
} else {
strokes + pendingStrokes.filter { pending -> strokes.none { it === pending } }
}
}
val identityMatrix = remember { Matrix() }
val currentOnErase by rememberUpdatedState(onErase)
val currentOnEraseStart by rememberUpdatedState(onEraseStart)
val currentOnEraseEnd by rememberUpdatedState(onEraseEnd)
Box(modifier = modifier) {
// Layer 1: Dry ink - completed strokes + same-frame pending handoff strokes
Canvas(modifier = Modifier.fillMaxSize()) {
val nativeCanvas = drawContext.canvas.nativeCanvas
strokesToDraw.fastForEach { stroke ->
nativeCanvas.withSave {
canvasStrokeRenderer.draw(
canvas = this,
stroke = stroke,
strokeToScreenTransform = identityMatrix,
)
}
}
}
// Layer 2: Eraser touch handler OR Wet ink (InProgressStrokes)
if (isEraserMode) {
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { currentOnEraseStart() },
onDragEnd = { currentOnEraseEnd() },
onDragCancel = { currentOnEraseEnd() },
onDrag = { change, _ ->
currentOnErase(change.position.x, change.position.y)
change.consume()
},
)
}
)
} else {
key(cacheGen) {
InProgressStrokes(
defaultBrush = currentBrush,
nextBrush = onGetNextBrush,
onStrokesFinished = { newStrokes ->
pendingStrokes = pendingStrokes + newStrokes
onStrokesFinished(newStrokes)
},
textureBitmapStore = textureStore,
)
}
}
}
}Workflow and task routing
工作流与任务路由
Follow these conditional instructions based on the requested task:
- Step 1 (Dependencies and SDK setup) : When adding Ink API dependencies or configuring , follow the setup blocks in this file and the Ink API setup guide. Verify that
minSdk,ink-nativeloader, andink-authoring-androidare present.androidx.input:input-motionprediction - Step 2 (Drawing surface and stroke rendering) : When creating a Compose drawing screen with wet ink () and dry ink (
InProgressStrokes), implementCanvasStrokeRendererfrom this file and review Draw a stroke. ForDrawingSurfaceparameter details,InProgressStrokeshighlighter layer compositing, andBlendMode.Multiplygeneration invalidation, you MUST follow drawing-and-brushes.md.TextureBitmapStore - Step 3 (Stock brushes and brush switching) : When creating, modifying, or switching stock brushes (,
StockBrushes.pressurePen,marker,highlighter, ordashedLine), review Brush APIs and you MUST follow drawing-and-brushes.md foremojiHighlightergeometry constraints, highlighter alpha preservation, and Android 14 (API level 34)epsilonrules.emojiHighlighter - Step 4 (Eraser, undo and redo, and MVVM architecture) : When implementing a geometry-based stroke eraser, linear undo and redo history stacks, or MVVM and Hilt wiring, review Geometry APIs and you MUST follow editing-and-architecture.md.
ViewModel - Step 5 (Persistence, bitmap export, and gesture coexistence) : When saving and loading strokes with Room (and
StrokeInputBatch.encode), preserving UI state across configuration changes, exporting drawings to adecodeor PNG file, or combining long-press drag gestures with drawing (Bitmap), review State preservation and persistent storage and you MUST follow persistence-and-advanced.md.pointerInputWithSiblingFallthrough - Programmatic custom brushes : For custom creation, custom tip shapes, or behavior node graphs, suggest the
BrushFamilyskill.ink-custom-brush-builder
根据所请求的任务遵循以下条件性说明:
- 步骤 1(依赖项与 SDK 设置):添加 Ink API 依赖项或配置 时,遵循此文件中的设置代码块以及 Ink API 设置指南。确认已包含
minSdk、ink-nativeloader和ink-authoring-android。androidx.input:input-motionprediction - 步骤 2(绘制界面与笔触渲染):创建包含湿墨()和干墨(
InProgressStrokes)的 Compose 绘制界面时,实现此文件中的CanvasStrokeRenderer,并查看绘制笔触。有关DrawingSurface参数详情、InProgressStrokes荧光笔图层合成以及BlendMode.Multiply世代失效机制,必须遵循 drawing-and-brushes.md。TextureBitmapStore - 步骤 3(stock brushes 与笔刷切换):创建、修改或切换 stock brushes(、
StockBrushes.pressurePen、marker、highlighter或dashedLine)时,查看笔刷 API,并且必须遵循 drawing-and-brushes.md 中关于emojiHighlighter几何约束、荧光笔透明度保留以及 Android 14(API 级别 34)epsilon规则的要求。emojiHighlighter - 步骤 4(擦除、撤销与重做及 MVVM 架构):实现基于几何的笔触擦除器、线性撤销和重做历史栈,或 MVVM 与 Hilt 接线时,查看几何 API,并且必须遵循 editing-and-architecture.md。
ViewModel - 步骤 5(持久化、位图导出与手势共存):使用 Room(和
StrokeInputBatch.encode)保存和加载笔触、在配置变更后保留 UI 状态、将绘图导出为decode或 PNG 文件,或将长按拖动手势与绘制结合使用(Bitmap)时,查看状态保留与持久化存储,并且必须遵循 persistence-and-advanced.md。pointerInputWithSiblingFallthrough - 以编程方式创建自定义笔刷:对于自定义 创建、自定义笔尖形状或 behavior node graph,请建议使用
BrushFamilyskill。ink-custom-brush-builder
Antipatterns
反模式
- NEVER use in Compose screens ---that's the View-based API. Use the
InProgressStrokesViewcomposable instead.InProgressStrokes - NEVER omit or
ink-nativeloader---missingink-authoring-androidthrowsink-nativeloader, and missingUnsatisfiedLinkErrorthrowsink-authoring-androidat runtime.ClassNotFoundException - NEVER use or
View.OnTouchListenerdirectly ---theMotionEventcomposable captures and processes touch and stylus input internally.InProgressStrokes - NEVER draw strokes manually with or by iterating stroke points---always call
Canvas.drawPath.CanvasStrokeRenderer.draw(canvas, stroke, transform) - NEVER call ---the 2-argument
CanvasStrokeRenderer.create(forcePathRendering = true)overload isforcePathRendering. Use the public@RestrictTo(LIBRARY_GROUP)factory, which automatically detects softwareCanvasStrokeRenderer.create(textureStore)canvases and falls back to path rendering.Bitmap - NEVER rely solely on asynchronous collection in
StateFlow---onStrokesFinishedremoves completed wet strokes immediately afterInProgressStrokesreturns on the UI thread, whileonStrokesFinishedupdates on a later run loop, causing a 1-frame wet-to-dry flicker. Always mutate Compose Snapshot State synchronously in the same UI thread run loop using acollectAsStateWithLifecyclebuffer as shown inpendingStrokes.DrawingSurface - NEVER store strokes as bitmaps for undo, redo, or database persistence---store objects in memory and serialize
Strokebytes with brush metadata for disk storage.StrokeInputBatch - NEVER use for Room in Kotlin ---always apply the
annotationProcessorplugin and usecom.google.devtools.kspto avoidksp(libs.androidx.room.compiler)crashes at launch.AppDatabase_Impl does not exist
- 绝不要在 Compose 界面中使用 ——它是基于 View 的 API。请改用
InProgressStrokesViewcomposable。InProgressStrokes - 绝不要省略 或
ink-nativeloader——缺少ink-authoring-android会抛出ink-nativeloader,缺少UnsatisfiedLinkError会在运行时抛出ink-authoring-android。ClassNotFoundException - 绝不要直接使用 或
View.OnTouchListener——MotionEventcomposable 会在内部捕获和处理触摸与触控笔输入。InProgressStrokes - 绝不要使用 或遍历笔触点来手动绘制笔触——始终调用
Canvas.drawPath。CanvasStrokeRenderer.draw(canvas, stroke, transform) - 绝不要调用 ——双参数
CanvasStrokeRenderer.create(forcePathRendering = true)重载带有forcePathRendering。请使用公开的@RestrictTo(LIBRARY_GROUP)工厂方法,它会自动检测软件CanvasStrokeRenderer.create(textureStore)Canvas 并回退到路径渲染。Bitmap - 绝不要仅依赖 中异步的
onStrokesFinished收集——StateFlow会在InProgressStrokes于 UI 线程返回后立即移除已完成的湿墨笔触,而onStrokesFinished会在后续运行循环中更新,从而导致一帧湿墨到干墨的闪烁。始终使用collectAsStateWithLifecycle缓冲区,在同一 UI 线程运行循环中同步修改 Compose Snapshot State,如pendingStrokes所示。DrawingSurface - 绝不要将笔触存储为位图以支持撤销、重做或数据库持久化——应在内存中存储 对象,并将
Stroke字节与笔刷元数据一起序列化到磁盘。StrokeInputBatch - 绝不要在 Kotlin 项目中为 Room 使用 ——始终应用
annotationProcessor插件并使用com.google.devtools.ksp,以避免启动时出现ksp(libs.androidx.room.compiler)崩溃。AppDatabase_Impl does not exist