ink-app-builder

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
The Android Ink API (
androidx.ink
) 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:
  • Wet ink : Real-time, ultra-low-latency rendering while the user is drawing, handled by the
    InProgressStrokes
    composable.
  • Dry ink : Finalized, immutable
    Stroke
    objects rendered by the app using
    CanvasStrokeRenderer
    .
Android Ink API (
androidx.ink
) 是一个模块化 Jetpack 库,用于在 Android 上构建 高性能、低延迟的自由手绘体验。它采用 湿墨和干墨 两阶段模型:
  • 湿墨:在用户绘制时进行实时、超低延迟渲染,由
    InProgressStrokes
    composable 处理。
  • 干墨:已定型且不可变的
    Stroke
    对象,由应用使用
    CanvasStrokeRenderer
    渲染。

Setup and dependencies

设置与依赖项

The Ink API requires
minSdk = 26
, which corresponds to Android 8.0 (API level 26) and higher. All
androidx.ink
modules MUST share an identical version in
gradle/libs.versions.toml
. Ask user permission before modifying project build files.
Ink API 要求
minSdk = 26
,即支持 Android 8.0(API 级别 26)及更高版本。所有
androidx.ink
模块在
gradle/libs.versions.toml
中必须使用完全相同的版本。修改项目构建文件前,请先征得用户许可。

Version selection (
1.0.0
stable versus
1.1.0-alpha03
and higher)

版本选择(
1.0.0
稳定版与
1.1.0-alpha03
及更高版本)

  • Use
    1.0.0
    (Stable)
    for production apps that rely on
    StockBrushes
    or pre-serialized
    .brush
    assets decoded with
    BrushFamily.decode
    . In
    1.0.0
    stable,
    BrushFamily.encode
    ,
    BrushFamily.decode
    ,
    AndroidBrushFamilySerialization
    , and
    BrushFamilyDecodeCallback
    require
    @OptIn(ExperimentalInkCustomBrushApi::class)
    .
  • Use
    1.1.0-alpha03
    and higher
    when the app constructs programmatic custom brushes (
    BrushFamily
    ,
    BrushCoat
    ,
    BrushTip
    ,
    BrushPaint
    ,
    BrushBehavior
    ) in Kotlin code. Starting in
    1.1.0-alpha02
    and higher,
    BrushFamily
    serialization requires no
    @OptIn
    (and in
    1.1.0-alpha05
    and higher,
    ExperimentalInkCustomBrushApi
    is
    @RestrictTo(LIBRARY_GROUP)
    , so NEVER annotate
    1.1.0-alpha03
    and higher code with it).
  • 对于依赖
    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
)

[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 模块配置(
app/build.gradle.kts
)

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
}
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
Brush
with
Brush.createWithComposeColor
(from
ink-brush-compose
). The
epsilon
parameter defines the minimum distance between two input points, in stroke-space units, to be considered visually distinct (
0.1f
for general drawing,
0.01f
for fine precision tools;
Brush
enforces
0f < epsilon <= size
):
kotlin
/**
 * 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,
    )
}
<br />
Stack two layers inside a
Box
for the drawing surface: a Compose
Canvas
that renders finalized dry
Stroke
objects inside
nativeCanvas.withSave
using
CanvasStrokeRenderer
, and an
InProgressStrokes
composable on top for low-latency wet ink capture. Encapsulate a synchronous
pendingStrokes
Compose Snapshot State buffer inside
DrawingSurface
so the dry
Canvas
invalidates in the exact same UI thread run loop when
onStrokesFinished
fires, preventing a 1-frame wet-to-dry flicker:
kotlin
@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,
                )
            }
        }
    }
}
<br />
使用
Brush.createWithComposeColor
(来自
ink-brush-compose
)创建标准的压感笔
Brush
。
epsilon
参数定义两个输入点之间在笔触空间单位下可被视为视觉上不同的最小距离(一般绘制使用
0.1f
,精细精度工具使用
0.01f
;
Brush
要求
0f < epsilon <= size
):
kotlin
/**
 * 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,
    )
}
<br />
在绘制界面的
Box
中堆叠两层:一个 Compose
Canvas
,在
nativeCanvas.withSave
内使用
CanvasStrokeRenderer
渲染已定型的干墨
Stroke
对象;其上方放置一个
InProgressStrokes
composable,用于以低延迟捕获湿墨。在
DrawingSurface
内封装一个同步的
pendingStrokes
Compose Snapshot State 缓冲区,使干墨
Canvas
能够在
onStrokesFinished
触发时的 同一 UI 线程运行循环中失效重绘,从而避免出现一帧湿墨到干墨的闪烁:
kotlin
@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,
                )
            }
        }
    }
}
<br />

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
    minSdk
    , follow the setup blocks in this file and the Ink API setup guide. Verify that
    ink-nativeloader
    ,
    ink-authoring-android
    , and
    androidx.input:input-motionprediction
    are present.
  • Step 2 (Drawing surface and stroke rendering) : When creating a Compose drawing screen with wet ink (
    InProgressStrokes
    ) and dry ink (
    CanvasStrokeRenderer
    ), implement
    DrawingSurface
    from this file and review Draw a stroke. For
    InProgressStrokes
    parameter details,
    BlendMode.Multiply
    highlighter layer compositing, and
    TextureBitmapStore
    generation invalidation, you MUST follow drawing-and-brushes.md.
  • Step 3 (Stock brushes and brush switching) : When creating, modifying, or switching stock brushes (
    StockBrushes.pressurePen
    ,
    marker
    ,
    highlighter
    ,
    dashedLine
    , or
    emojiHighlighter
    ), review Brush APIs and you MUST follow drawing-and-brushes.md for
    epsilon
    geometry constraints, highlighter alpha preservation, and Android 14 (API level 34)
    emojiHighlighter
    rules.
  • Step 4 (Eraser, undo and redo, and MVVM architecture) : When implementing a geometry-based stroke eraser, linear undo and redo history stacks, or MVVM
    ViewModel
    and Hilt wiring, review Geometry APIs and you MUST follow editing-and-architecture.md.
  • Step 5 (Persistence, bitmap export, and gesture coexistence) : When saving and loading strokes with Room (
    StrokeInputBatch.encode
    and
    decode
    ), preserving UI state across configuration changes, exporting drawings to a
    Bitmap
    or PNG file, or combining long-press drag gestures with drawing (
    pointerInputWithSiblingFallthrough
    ), review State preservation and persistent storage and you MUST follow persistence-and-advanced.md.
  • Programmatic custom brushes : For custom
    BrushFamily
    creation, custom tip shapes, or behavior node graphs, suggest the
    ink-custom-brush-builder
    skill.
根据所请求的任务遵循以下条件性说明:
  • 步骤 1(依赖项与 SDK 设置):添加 Ink API 依赖项或配置
    minSdk
    时,遵循此文件中的设置代码块以及 Ink API 设置指南。确认已包含
    ink-nativeloader
    、
    ink-authoring-android
    和
    androidx.input:input-motionprediction
    。
  • 步骤 2(绘制界面与笔触渲染):创建包含湿墨(
    InProgressStrokes
    )和干墨(
    CanvasStrokeRenderer
    )的 Compose 绘制界面时,实现此文件中的
    DrawingSurface
    ,并查看绘制笔触。有关
    InProgressStrokes
    参数详情、
    BlendMode.Multiply
    荧光笔图层合成以及
    TextureBitmapStore
    世代失效机制,必须遵循 drawing-and-brushes.md。
  • 步骤 3(stock brushes 与笔刷切换):创建、修改或切换 stock brushes(
    StockBrushes.pressurePen
    、
    marker
    、
    highlighter
    、
    dashedLine
    或
    emojiHighlighter
    )时,查看笔刷 API,并且必须遵循 drawing-and-brushes.md 中关于
    epsilon
    几何约束、荧光笔透明度保留以及 Android 14(API 级别 34)
    emojiHighlighter
    规则的要求。
  • 步骤 4(擦除、撤销与重做及 MVVM 架构):实现基于几何的笔触擦除器、线性撤销和重做历史栈,或 MVVM
    ViewModel
    与 Hilt 接线时,查看几何 API,并且必须遵循 editing-and-architecture.md。
  • 步骤 5(持久化、位图导出与手势共存):使用 Room(
    StrokeInputBatch.encode
    和
    decode
    )保存和加载笔触、在配置变更后保留 UI 状态、将绘图导出为
    Bitmap
    或 PNG 文件,或将长按拖动手势与绘制结合使用(
    pointerInputWithSiblingFallthrough
    )时,查看状态保留与持久化存储,并且必须遵循 persistence-and-advanced.md。
  • 以编程方式创建自定义笔刷:对于自定义
    BrushFamily
    创建、自定义笔尖形状或 behavior node graph,请建议使用
    ink-custom-brush-builder
    skill。

Antipatterns

反模式

  • NEVER use
    InProgressStrokesView
    in Compose screens
    ---that's the View-based API. Use the
    InProgressStrokes
    composable instead.
  • NEVER omit
    ink-nativeloader
    or
    ink-authoring-android
    ---missing
    ink-nativeloader
    throws
    UnsatisfiedLinkError
    , and missing
    ink-authoring-android
    throws
    ClassNotFoundException
    at runtime.
  • NEVER use
    View.OnTouchListener
    or
    MotionEvent
    directly
    ---the
    InProgressStrokes
    composable captures and processes touch and stylus input internally.
  • NEVER draw strokes manually with
    Canvas.drawPath
    or by iterating stroke points---always call
    CanvasStrokeRenderer.draw(canvas, stroke, transform)
    .
  • NEVER call
    CanvasStrokeRenderer.create(forcePathRendering = true)
    ---the 2-argument
    forcePathRendering
    overload is
    @RestrictTo(LIBRARY_GROUP)
    . Use the public
    CanvasStrokeRenderer.create(textureStore)
    factory, which automatically detects software
    Bitmap
    canvases and falls back to path rendering.
  • NEVER rely solely on asynchronous
    StateFlow
    collection in
    onStrokesFinished
    ---
    InProgressStrokes
    removes completed wet strokes immediately after
    onStrokesFinished
    returns on the UI thread, while
    collectAsStateWithLifecycle
    updates 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 a
    pendingStrokes
    buffer as shown in
    DrawingSurface
    .
  • NEVER store strokes as bitmaps for undo, redo, or database persistence---store
    Stroke
    objects in memory and serialize
    StrokeInputBatch
    bytes with brush metadata for disk storage.
  • NEVER use
    annotationProcessor
    for Room in Kotlin
    ---always apply the
    com.google.devtools.ksp
    plugin and use
    ksp(libs.androidx.room.compiler)
    to avoid
    AppDatabase_Impl does not exist
    crashes at launch.
  • 绝不要在 Compose 界面中使用
    InProgressStrokesView
    ——它是基于 View 的 API。请改用
    InProgressStrokes
    composable。
  • 绝不要省略
    ink-nativeloader
    或
    ink-authoring-android
    ——缺少
    ink-nativeloader
    会抛出
    UnsatisfiedLinkError
    ,缺少
    ink-authoring-android
    会在运行时抛出
    ClassNotFoundException
    。
  • 绝不要直接使用
    View.OnTouchListener
    或
    MotionEvent
    ——
    InProgressStrokes
    composable 会在内部捕获和处理触摸与触控笔输入。
  • 绝不要使用
    Canvas.drawPath
    或遍历笔触点来手动绘制笔触
    ——始终调用
    CanvasStrokeRenderer.draw(canvas, stroke, transform)
    。
  • 绝不要调用
    CanvasStrokeRenderer.create(forcePathRendering = true)
    ——双参数
    forcePathRendering
    重载带有
    @RestrictTo(LIBRARY_GROUP)
    。请使用公开的
    CanvasStrokeRenderer.create(textureStore)
    工厂方法,它会自动检测软件
    Bitmap
    Canvas 并回退到路径渲染。
  • 绝不要仅依赖
    onStrokesFinished
    中异步的
    StateFlow
    收集
    ——
    InProgressStrokes
    会在
    onStrokesFinished
    于 UI 线程返回后立即移除已完成的湿墨笔触,而
    collectAsStateWithLifecycle
    会在后续运行循环中更新,从而导致一帧湿墨到干墨的闪烁。始终使用
    pendingStrokes
    缓冲区,在同一 UI 线程运行循环中同步修改 Compose Snapshot State,如
    DrawingSurface
    所示。
  • 绝不要将笔触存储为位图以支持撤销、重做或数据库持久化——应在内存中存储
    Stroke
    对象,并将
    StrokeInputBatch
    字节与笔刷元数据一起序列化到磁盘。
  • 绝不要在 Kotlin 项目中为 Room 使用
    annotationProcessor
    ——始终应用
    com.google.devtools.ksp
    插件并使用
    ksp(libs.androidx.room.compiler)
    ,以避免启动时出现
    AppDatabase_Impl does not exist
    崩溃。