Loading...
Loading...
Adds inking and freehand drawing capabilities to an Android app using Jetpack Ink (androidx.ink) and Compose. Use this skill when implementing stylus or touch drawing surfaces, rendering wet and dry ink strokes, configuring stock brushes (pressure pen, marker, highlighter), implementing geometry-based erasing, linear undo and redo history, or stroke persistence with Room. Delivers working Compose and ViewModel implementations. Don't use for View-based (XML) inking or programmatic custom BrushFamily creation. For custom BrushFamily creation, custom tip shapes, or behavior node graphs, suggest the ink-custom-brush-builder skill.
npx skill4agent add android/skills ink-app-builderandroidx.inkInProgressStrokesStrokeCanvasStrokeRendererminSdk = 26androidx.inkgradle/libs.versions.toml1.0.01.1.0-alpha031.0.0StockBrushes.brushBrushFamily.decode1.0.0BrushFamily.encodeBrushFamily.decodeAndroidBrushFamilySerializationBrushFamilyDecodeCallback@OptIn(ExperimentalInkCustomBrushApi::class)1.1.0-alpha03BrushFamilyBrushCoatBrushTipBrushPaintBrushBehavior1.1.0-alpha02BrushFamily@OptIn1.1.0-alpha05ExperimentalInkCustomBrushApi@RestrictTo(LIBRARY_GROUP)1.1.0-alpha03gradle/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" }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
}BrushBrush.createWithComposeColorink-brush-composeepsilon0.1f0.01fBrush0f < epsilon <= size/**
* 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,
)
}BoxCanvasStrokenativeCanvas.withSaveCanvasStrokeRendererInProgressStrokespendingStrokesDrawingSurfaceCanvasonStrokesFinished@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,
)
}
}
}
}minSdkink-nativeloaderink-authoring-androidandroidx.input:input-motionpredictionInProgressStrokesCanvasStrokeRendererDrawingSurfaceInProgressStrokesBlendMode.MultiplyTextureBitmapStoreStockBrushes.pressurePenmarkerhighlighterdashedLineemojiHighlighterepsilonemojiHighlighterViewModelStrokeInputBatch.encodedecodeBitmappointerInputWithSiblingFallthroughBrushFamilyink-custom-brush-builderInProgressStrokesViewInProgressStrokesink-nativeloaderink-authoring-androidink-nativeloaderUnsatisfiedLinkErrorink-authoring-androidClassNotFoundExceptionView.OnTouchListenerMotionEventInProgressStrokesCanvas.drawPathCanvasStrokeRenderer.draw(canvas, stroke, transform)CanvasStrokeRenderer.create(forcePathRendering = true)forcePathRendering@RestrictTo(LIBRARY_GROUP)CanvasStrokeRenderer.create(textureStore)BitmapStateFlowonStrokesFinishedInProgressStrokesonStrokesFinishedcollectAsStateWithLifecyclependingStrokesDrawingSurfaceStrokeStrokeInputBatchannotationProcessorcom.google.devtools.kspksp(libs.androidx.room.compiler)AppDatabase_Impl does not exist