告别手写延迟!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 都是你的最佳选择!

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui