1. 长按复制这件事为什么原生 TextView 总差点意思Android 里 TextView 默认的textIsSelectabletrue确实能选中文字但真放到业务里用问题一堆选中后系统菜单样式改不动、拖拽手柄在 RecyclerView 里一滚动就错位、想加个「复制全部」按钮还得跟系统菜单抢位置。更麻烦的是很多 App 的文案是动态拼接的系统选择器对 Spannable 的支持时好时坏产品那边一句「要像微信那样自由选、自由复制」你就得自己撸一套。这篇就干两件事。第一把 TextView 自由选择复制的核心实现拆开讲清楚包括手柄拖拽、选区高亮、复制弹窗的定位逻辑代码可以直接抄。第二把 AI 辅助编码工具比如 Claude Code 这类的接入配置一起收进来用 TaoToken 的统一 Key 走 API 通道配好settings.json骨架让代码生成、补全、排障都能在一个通道里跑通。适合正在做 Android 原生文本交互、又想把 AI 工具链接进日常开发流的同学。我试过把选择复制和 AI 配置分开折腾结果两边都卡复制逻辑调半天API Key 又散落在各个工具里。后来统一到一套配置省事很多。2. TaoToken 前置统一 Key 与 API 通道怎么理解TaoToken 在这里的角色是给 AI 编码工具提供一个统一的接入入口。你不用在每个工具里分别填不同的 Key而是拿一个统一 Key通过 API 通道去调模型。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 这个不加 UTM。对 Android 开发来说它的价值在于你写 TextView 选择复制这种偏底层的代码时可以让 AI 帮你补全TextLayoutUtil里的偏移计算、帮你检查PopupWindow的坐标越界而不用来回切浏览器。统一 Key 的好处是配置一次多个工具复用。需要先拿到 Key去控制台创建https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 然后在 API Keys 页面生成https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。生成后复制那串 Key后面配置里要用。注意Key 只显示一次生成后立刻存到本地安全位置别提交到 Git。如果你只是想先验证模型通不通可以直接用模型对话页面试https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。长期做编码和 Agent 任务再考虑 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。3. 可复制配置settings.json 骨架与 TextView 核心代码3.1 settings.json 配置骨架AI 编码工具以支持settings.json的客户端为例的配置骨架如下。把YOUR_TAOTOKEN_KEY换成你刚才生成的 Key{ apiProvider: openai-compatible, apiKey: YOUR_TAOTOKEN_KEY, baseUrl: https://taotoken.net/api, model: claude-sonnet-4-20250514, maxTokens: 8192, temperature: 0.2, timeout: 60000, retry: { enabled: true, maxAttempts: 3, backoffMs: 1000 }, context: { includeProjectFiles: true, maxFileSizeKb: 512, excludePatterns: [ **/build/**, **/.gradle/**, **/*.apk ] } }几个参数说明baseUrl指向 TaoToken 的 API 入口不要带 UTM 参数temperature设低一点代码生成更稳excludePatterns把 Android 的build目录排除掉避免把编译产物塞进上下文。3.2 TextView 选择复制的核心结构原实现里类比较多我按职责归一下方便你对照抄类/文件职责OnSelectListener选区变化回调把选中内容抛给业务SelectionInfo保存 start/end 偏移和选中文本TextLayoutUtil坐标与字符偏移互转含边界吸附SelectableTextHelper总控手柄、弹窗、高亮 SpanCursorHandle左右拖拽手柄处理方向切换OperateWindow复制/全选弹窗及定位核心的偏移计算getPreciseOffset负责把触摸点转成字符位置getHysteresisOffset负责拖拽时的行间吸附避免手柄在换行处抖动。这两个方法是整个自由选择的手感关键别省。选区高亮用BackgroundColorSpan在selectText里动态 setSpanprivate void selectText(int startPos, int endPos) { if (startPos ! -1) mSelectionInfo.mStart startPos; if (endPos ! -1) mSelectionInfo.mEnd endPos; if (mSelectionInfo.mStart mSelectionInfo.mEnd) { int temp mSelectionInfo.mStart; mSelectionInfo.mStart mSelectionInfo.mEnd; mSelectionInfo.mEnd temp; } if (mSpannable ! null) { if (mSpan null) { mSpan new BackgroundColorSpan(mSelectedColor); } mSelectionInfo.mSelectionContent mSpannable.subSequence(mSelectionInfo.mStart, mSelectionInfo.mEnd).toString(); mSpannable.setSpan(mSpan, mSelectionInfo.mStart, mSelectionInfo.mEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); if (mSelectListener ! null) { mSelectListener.onTextSelected(mSelectionInfo.mSelectionContent); } } }弹窗定位在OperateWindow.show()里做了三处越界保护posX 0归到 16posY 0归到 16右边超出屏幕宽度就左移。这三行不加弹窗在窄屏或靠边文字上会跑出屏幕。3.3 调用方式private var mSelectableTextHelper: SelectableTextHelper? null mSelectableTextHelper SelectableTextHelper.Builder(textView) .setSelectedColor(Color.parseColor(#afe1f4)) .setCursorHandleSizeInDp(20f) .setCursorHandleColor(Color.parseColor(#0d7aff)) .build() mSelectableTextHelper?.setSelectListener { content - // content 就是当前选中的文本 copyToClipboard(content) }textView记得在布局里不要设textIsSelectabletrue否则系统选择器和自定义手柄会打架。4. 验证请求复制功能与 API 通道连通性4.1 验证复制功能跑起来后长按 TextView应该出现左右两个圆形手柄和上方弹窗。拖动手柄选区高亮实时更新松手弹窗回到选区上方。点「复制」内容进剪贴板可以粘贴到任意输入框确认。验证全选点弹窗里的「全选」选区覆盖整段文本手柄分别落到首尾。再滚动外层 RecyclerView手柄和弹窗应该自动隐藏停止滚动后重新出现——这是OnScrollChangedListener在起作用。4.2 验证 API 通道配置写好后用一条最小请求确认通道通。命令行里可以这样测curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer YOUR_TAOTOKEN_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [ {role: user, content: 用一句话说明 Android TextView 如何实现自由选择复制} ], max_tokens: 256 }返回里能看到choices[0].message.content就说明通道正常。如果返回 401检查 Key 有没有多余空格返回 404检查baseUrl是不是写成了带路径的完整地址。在编码工具里验证直接让它读一个TextLayoutUtil.java问「这个 getHysteresisOffset 在什么情况下会返回 -1」能正常回答就说明上下文和通道都通了。5. 本篇常见错排查手柄拖拽时选区乱跳。多半是getHysteresisOffset里的previousOffset没传对。拖拽开始时要把mBeforeDragStart/mBeforeDragEnd存好移动过程中用旧偏移做参考否则每次都用新偏移行间吸附就失效了。弹窗位置偏上或偏左。检查mTempCoors有没有在show()前重新getLocationInWindow。TextView 如果嵌在可滚动容器里窗口坐标会变不刷新就会偏。复制出来是空字符串。看mSpannable是不是 null。TextView 的文本如果不是BufferType.SPANNABLEgetText()拿到的不是 SpannablesetSpan 会失败。初始化时那句setText(getText(), BufferType.SPANNABLE)不能漏。API 请求超时。timeout设 60000 还超时先确认网络能到taotoken.net再看是不是maxTokens设太大导致响应慢。代码生成场景 8192 够用别一上来就拉满。Key 泄露风险。settings.json如果放在项目目录里记得加进.gitignore。更稳的做法是用环境变量注入配置文件里只留占位符。6. 把两件事收进同一条工作流TextView 自由选择复制这套代码难点不在写在于边界情况多换行吸附、滚动隐藏、弹窗越界、Spannable 类型。这些点我上面都标了位置你抄的时候重点看TextLayoutUtil和OperateWindow.show()。AI 工具链这边统一 Key 配一次settings.json骨架直接复用。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite Claude Code 相关配置看 https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecode-anthropicutm_campaignrewrite 。排障和接入问题优先查 API Keys 和文档验证模型走模型对话长期编码任务再上 Coding Plan。最后留个实用习惯每次改完SelectableTextHelper先在一段超长文本比如 2000 字上测拖拽短文本测不出行间吸附的问题。这个坑我踩过短文本一切正常长文本一拖就飘。
