告别手写延迟!Android Ink API 1.0 正式版重磅发布,4ms 极致体验触手可及

2025年12月17日,Google 正式发布 Android Ink API 1.0 稳定版,这是一个让触控笔应用开发变得前所未有简单的 Jetpack 库。Google Docs、Google Photos、Chrome PDF、Circle to Search 等明星应用都在使用它!

Ink API 是什么?

背景:为什么需要 Ink API?

用户期望触控笔体验能像在纸上书写一样流畅自然。虽然 Android 早已通过框架级 API 将书写延迟降至惊人的 4 毫秒(几乎无法感知),但开发者仍然面临诸多挑战:

  • 🎨 笔触生成算法复杂
  • 🖌️ 渲染性能优化困难
  • 📐 几何计算(擦除、选择)繁琐
  • ⚡ 低延迟实现门槛高

Ink API 应运而生! 它将这些复杂的底层逻辑封装成简洁的 API,让开发者能够专注于打造独特的应用功能。

核心特性

特性 说明
🚀 超低延迟 基于 Jetpack Graphics Library,提供 4ms 级响应
🧩 模块化设计 按需引入,灵活组合
🎯 Compose 优先 完美适配 Jetpack Compose
📱 广泛兼容 支持 Android 5.0 (API 21) 及以上

谁在用?

Ink API 已经在多款 Google 核心应用中大规模使用:

  • Google Docs - 文档标注
  • Google Photos - 照片编辑
  • Chrome PDF - PDF 批注
  • Circle to Search - 圈选搜索
  • Pixel Studio - 创意绘画
  • YouTube Effect Maker - 特效制作

Circle to Search 团队反馈:"集成过程非常顺利,一周内就构建了可工作的原型!"


模块架构详解

Ink API 采用 五大核心模块 的架构设计:

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                        Ink API                               │
├──────────────┬──────────────┬──────────────┬────────────────┤
│   Strokes    │    Brush     │   Geometry   │   Rendering    │
│   笔触模块    │   画笔模块    │   几何模块    │    渲染模块     │
├──────────────┴──────────────┴──────────────┴────────────────┤
│                    Authoring (实时创作模块)                   │
└─────────────────────────────────────────────────────────────┘
模块 Gradle 依赖 功能说明
ink-strokes ink-strokes 墨迹输入的数据结构与表示
ink-brush ink-brush-compose 声明式定义画笔风格(颜色、粗细、类型)
ink-geometry ink-geometry-compose 几何操作:擦除、选择、碰撞检测
ink-rendering ink-rendering 高效渲染笔触到屏幕
ink-authoring ink-authoring-compose 处理实时输入,生成平滑笔触
ink-storage ink-storage 笔触序列化与持久化存储

快速上手

添加依赖

build.gradle.kts 中添加:

kotlin 复制代码
dependencies {
    val ink_version = "1.0.0"

    // 核心依赖
    implementation("androidx.ink:ink-nativeloader:$ink_version")
    implementation("androidx.ink:ink-strokes:$ink_version")
    implementation("androidx.ink:ink-rendering:$ink_version")

    // Compose 集成
    implementation("androidx.ink:ink-authoring-compose:$ink_version")
    implementation("androidx.ink:ink-brush-compose:$ink_version")
    implementation("androidx.ink:ink-geometry-compose:$ink_version")

    // 可选:存储支持
    implementation("androidx.ink:ink-storage:$ink_version")
}

创建画笔

kotlin 复制代码
import androidx.ink.brush.Brush
import androidx.ink.brush.StockBrushes
import androidx.compose.ui.graphics.Color

// 创建一个黑色压感画笔
val blackPen = Brush.createWithComposeColor(
    family = StockBrushes.pressurePen(),  // 压感笔
    colorIntArgb = Color.Black.toArgb(),
    size = 5f,                             // 笔触粗细
    epsilon = 0.1f                         // 精度参数
)

// 创建荧光笔效果
val highlighter = Brush.createWithComposeColor(
    family = StockBrushes.highlighter(),
    colorIntArgb = Color.Yellow.toArgb(),
    size = 20f,
    epsilon = 0.1f
)

// 基于现有画笔修改颜色
val redPen = blackPen.copyWithComposeColor(
    color = Color.Red.toArgb()
)

StockBrushes 内置画笔类型:

  • pressurePen() - 压感钢笔
  • highlighter() - 荧光笔
  • marker() - 马克笔
  • pressure() - 通用压感笔

实现绘图画布

kotlin 复制代码
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Matrix
import androidx.ink.authoring.InProgressStrokes
import androidx.ink.rendering.android.canvas.CanvasStrokeRenderer
import androidx.ink.strokes.Stroke

@Composable
fun InkCanvas(
    modifier: Modifier = Modifier
) {
    // 已完成的笔触列表
    var finishedStrokes by remember { mutableStateOf(listOf<Stroke>()) }

    // 当前使用的画笔
    var currentBrush by remember { mutableStateOf(blackPen) }

    // 笔触渲染器
    val strokeRenderer = remember { CanvasStrokeRenderer.create() }

    Box(modifier = modifier.fillMaxSize()) {

        // 底层 Canvas:渲染已完成的"干"笔触
        Canvas(modifier = Modifier.fillMaxSize()) {
            finishedStrokes.forEach { stroke ->
                strokeRenderer.draw(
                    stroke = stroke,
                    canvas = drawContext.canvas.nativeCanvas,
                    strokeToScreenTransform = Matrix()
                )
            }
        }

        // 顶层:实时渲染正在绘制的"湿"笔触
        InProgressStrokes(
            modifier = Modifier.fillMaxSize(),
            defaultBrush = currentBrush,
            onStrokesFinished = { newStrokes ->
                // 将完成的笔触添加到列表
                finishedStrokes = finishedStrokes + newStrokes
            }
        )
    }
}

关键概念:湿墨与干墨

scss 复制代码
用户绘制中 ──► 湿墨 (InProgressStrokes)
                 │
                 ▼ 手指/笔抬起
            干墨 (Canvas 渲染)
  • 湿墨 (Wet Ink) :正在绘制的笔触,由 InProgressStrokes 实时渲染
  • 干墨 (Dry Ink) :已完成的笔触,通过 CanvasStrokeRenderer 渲染到 Canvas

实现橡皮擦功能

kotlin 复制代码
import androidx.ink.geometry.MutableVec
import androidx.ink.geometry.MutableSegment
import androidx.ink.geometry.MutableParallelogram
import androidx.ink.geometry.AffineTransform

class Eraser(
    private val eraserSize: Float = 20f
) {
    private var previousPoint: MutableVec? = null

    /**
     * 擦除与橡皮擦路径相交的笔触
     */
    fun eraseAt(
        currentX: Float,
        currentY: Float,
        strokes: MutableList<Stroke>
    ) {
        val prev = previousPoint
        previousPoint = MutableVec(currentX, currentY)

        // 第一个点,无法形成线段
        if (prev == null) return

        // 构建擦除路径线段
        val segment = MutableSegment(
            prev,
            MutableVec(currentX, currentY)
        )

        // 创建带有宽度的擦除区域(平行四边形)
        val eraseArea = MutableParallelogram()
            .populateFromSegmentAndPadding(segment, eraserSize / 2)

        // 移除与擦除区域相交的笔触
        strokes.removeAll { stroke ->
            stroke.shape.intersects(
                eraseArea,
                AffineTransform.IDENTITY
            )
        }
    }

    fun reset() {
        previousPoint = null
    }
}

完整示例:带工具栏的绘图应用

kotlin 复制代码
@Composable
fun DrawingApp() {
    var strokes by remember { mutableStateOf(listOf<Stroke>()) }
    var currentTool by remember { mutableStateOf(Tool.PEN) }
    var brushColor by remember { mutableStateOf(Color.Black) }
    var brushSize by remember { mutableStateOf(5f) }

    val currentBrush by remember(brushColor, brushSize) {
        derivedStateOf {
            Brush.createWithComposeColor(
                family = StockBrushes.pressurePen(),
                colorIntArgb = brushColor.toArgb(),
                size = brushSize,
                epsilon = 0.1f
            )
        }
    }

    Column(modifier = Modifier.fillMaxSize()) {
        // 工具栏
        ToolBar(
            currentTool = currentTool,
            onToolChange = { currentTool = it },
            currentColor = brushColor,
            onColorChange = { brushColor = it },
            currentSize = brushSize,
            onSizeChange = { brushSize = it },
            onClear = { strokes = emptyList() }
        )

        // 画布
        Box(modifier = Modifier.weight(1f)) {
            when (currentTool) {
                Tool.PEN -> {
                    InkCanvas(
                        brush = currentBrush,
                        finishedStrokes = strokes,
                        onStrokesFinished = { strokes = strokes + it }
                    )
                }
                Tool.ERASER -> {
                    EraserCanvas(
                        strokes = strokes.toMutableList(),
                        onStrokesChanged = { strokes = it }
                    )
                }
            }
        }
    }
}

enum class Tool { PEN, ERASER }

进阶功能

笔触序列化与存储

kotlin 复制代码
import androidx.ink.storage.StrokeEncoder
import androidx.ink.storage.StrokeDecoder

// 序列化笔触
fun saveStrokes(strokes: List<Stroke>): ByteArray {
    return StrokeEncoder.encode(strokes)
}

// 反序列化笔触
fun loadStrokes(data: ByteArray): List<Stroke> {
    return StrokeDecoder.decode(data)
}

自定义画笔家族

kotlin 复制代码
@OptIn(ExperimentalInkCustomBrushApi::class)
fun loadCustomBrush(context: Context): BrushFamily {
    // 从 raw 资源加载 gzipped protocol buffer 格式的画笔定义
    return context.resources.openRawResource(R.raw.calligraphy).use {
        BrushFamily.decode(it)
    }
}

坐标系统转换

kotlin 复制代码
// 支持画布缩放和平移
val transform = Matrix().apply {
    scale(zoomLevel, zoomLevel)
    translate(offsetX, offsetY)
}

strokeRenderer.draw(
    stroke = stroke,
    canvas = canvas,
    strokeToScreenTransform = transform
)

最佳实践

✅ 推荐做法

  1. 分离湿墨和干墨层 - 使用 InProgressStrokes 处理实时绘制,Canvas 处理已完成笔触
  2. 及时提交笔触 - 在 onStrokesFinished 回调中立即处理完成的笔触
  3. 复用渲染器 - 使用 remember 缓存 CanvasStrokeRenderer 实例
  4. 合理设置 epsilon - 一般 0.1f 即可满足大多数场景

❌ 避免做法

  1. 不要在主线程执行大量笔触的几何运算
  2. 不要频繁创建新的 Brush 对象
  3. 不要忽略坐标系统转换

版本历程

版本 日期 重要更新
1.0.0 2025.12.17 🎉 正式版发布
1.0.0-beta02 2025.11.19 自定义低延迟形状 API
1.0.0-alpha07 2025.10.08 荧光笔自重叠参数、平滑算法优化
1.0.0-alpha05 2025.06.18 Compose 互操作模块
1.0.0-alpha01 2024.10.10 首个 Alpha 版本

总结

Android Ink API 1.0 的正式发布,标志着触控笔应用开发进入了一个新时代:

  • 📉 开发门槛大幅降低 - 无需深入理解底层渲染和几何算法
  • 性能开箱即用 - 4ms 超低延迟,流畅如纸
  • 🧩 模块化按需引入 - 从简单绘图到复杂编辑器都能满足
  • 🏢 生产环境验证 - Google 核心应用背书

无论你是想开发笔记应用、创意绘画工具,还是为现有应用添加批注功能,Ink API 都是你的最佳选择!

相关推荐
CheungChunChiu2 小时前
# Xorg 配置与 modesetting 驱动详解:从设备节点到显示旋转
android·linux·ubuntu·显示·xserver
tangweiguo030519872 小时前
动态库探秘:如何快速查看.so文件中的JNI方法
android
jackletter2 小时前
DBUtil设计:c#中的DateTime和DateTimeOffset转sql时应该输出时区信息吗?
android·sql·c#
摘星编程2 小时前
React Native for OpenHarmony 实战:ToastAndroid 安卓提示详解
android·react native·react.js
peachSoda72 小时前
使用HBuilderX 自带hbuilderx-cli 自动化打包uniapp的移动端app(Android,iOS)
android·uni-app·自动化
我命由我123452 小时前
Android 开发 - 关于 startActivity 后立刻 finish、requestWindowFeature 方法注意事项
android·java·开发语言·java-ee·kotlin·android studio·android-studio
PyHaVolask3 小时前
安全编码实战示例
android·安全·web安全代码
氦客11 小时前
Android Compose : 传统View在Compose组件中的等价物
android·compose·jetpack·对比·传统view·等价物·compose组件