新闻详情

新闻详情

首页 / 资讯中心 / 详情

Android TextView 自定义选中弹出菜单记笔记:TaoToken 统一 Key 接入与 config.toml 骨架

发布时间:2026/11/13 7:39:33来源:尧图网络
Android TextView 自定义选中弹出菜单记笔记:TaoToken 统一 Key 接入与 config.toml 骨架
1. 长按选中弹菜单记笔记为什么原生方案在部分机型上会失效Android 里给 TextView 加一个「记笔记」的选中菜单看起来是个小需求但真正落地时会遇到两个坑。第一个坑是系统自带的ActionMode回调你通过setCustomSelectionActionModeCallback往菜单里塞了一个notes项在原生 Android 或者大部分 AOSP 机型上能正常显示但到了某些深度定制的 ROM 上长按选中后弹出的菜单被系统接管你注入的 item 直接不出现。第二个坑是即使菜单出来了选中文本的起止 offset 在onActionItemClicked里拿到的时机和内容也可能和你预期不一致尤其是 TextView 处于非聚焦状态时getSelectionStart()返回 -1笔记内容就写了个空。这篇要解决的就是这条完整链路TextView 长按选中文本 → 弹出自定义菜单复制 / 记笔记→ 点击记笔记 → 把选中内容写入本地笔记库。同时我会把 TaoToken 的统一 Key 接入和config.toml骨架一起给出来因为很多同学在接大模型做「笔记摘要 / 标签生成」时Key 管理一团乱正好借这个场景把配置规范一次讲清楚。适合谁看正在做阅读类、笔记类、资讯类 App需要在 TextView 上做自定义选中交互并且后续想把选中内容丢给模型做二次处理的 Android 开发者。我试过直接用系统ActionMode的方案在小米、部分华为机型上确实会出现「记笔记」选项消失的情况所以下面会以自绘PopupWindow的SelectableTextHelper为主线把可复制的代码和配置都给全。2. TaoToken 统一 Key 前置准备与 config.toml 骨架在写笔记落库之前先把「选中内容 → 模型处理」这条链路的凭证准备好。TaoToken 的作用是提供一个统一的 API Key让你在 Android 端调用模型对话、代码补全等能力时不用为每个模型单独维护一套鉴权。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。你需要先拿到 Key入口在 API Keys 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。拿到之后不要硬编码进BuildConfig而是走一个config.toml骨架本地开发用local.properties注入CI 用环境变量覆盖。下面是我实测下来比较稳的config.toml骨架放在app/src/main/assets/config.toml运行时读取# app/src/main/assets/config.toml # TaoToken 统一接入配置骨架 [api] # 统一网关地址不要带末尾斜杠 base_url https://taotoken.net/api # 对话补全路径 chat_path /v1/chat/completions # 请求超时秒 timeout_seconds 30 # 重试次数 max_retries 2 [auth] # 运行时从 local.properties / 环境变量注入禁止提交真实 Key api_key_env TAOTOKEN_API_KEY # 请求头字段名 header_name Authorization header_prefix Bearer [model] # 默认模型按需替换 default claude-sonnet # 笔记摘要场景用的模型 note_summary claude-sonnet # 温度 temperature 0.3 max_tokens 1024 [note] # 笔记本地库名 db_name note.db # 单条笔记最大字符数超出截断 max_content_length 4000 # 是否自动生成标签 auto_tag true对应的local.properties里加一行这个文件本来就在.gitignore里TAOTOKEN_API_KEYsk-你的真实key然后在build.gradle里把它读进BuildConfigandroid { defaultConfig { def localProps new Properties() def localFile rootProject.file(local.properties) if (localFile.exists()) { localProps.load(new FileInputStream(localFile)) } buildConfigField String, TAOTOKEN_API_KEY, \${localProps[TAOTOKEN_API_KEY] ?: }\ } }这样 Key 只存在于本地和 CI 的 secret 里代码仓库里永远看不到明文。如果你后面要做长期编码或 Agent 场景可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 它把额度管理和 Key 复用做得更省心。3. 可复制配置SelectableTextHelper 自绘菜单 笔记落库原生ActionMode方案在定制 ROM 上不可靠所以这里用自绘PopupWindow的SelectableTextHelper。核心思路是拦截 TextView 的长按和触摸事件自己计算选中范围自己画光标手柄自己弹菜单。菜单里放「复制」和「记笔记」两个按钮点「记笔记」时把mSelectionInfo.mSelectionContent回调出去。先看菜单布局layout_operate_windows.xml注意用CardView包一层圆角和阴影更自然?xml version1.0 encodingutf-8? RelativeLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthwrap_content android:layout_heightwrap_content androidx.cardview.widget.CardView android:layout_widthwrap_content android:layout_heightwrap_content app:cardBackgroundColorcolor/white app:cardCornerRadius6dp app:cardElevation4dp LinearLayout android:layout_widthwrap_content android:layout_heightwrap_content android:orientationhorizontal TextView android:idid/tv_copy android:layout_widthwrap_content android:layout_heightwrap_content android:padding10dp android:text复制 android:textColorcolor/black / View android:layout_width0.5dp android:layout_height20dp android:layout_gravitycenter android:backgroundcolor/gray_DDDDDD / TextView android:idid/tv_note android:layout_widthwrap_content android:layout_heightwrap_content android:padding10dp android:text记笔记 android:textColorcolor/black / /LinearLayout /androidx.cardview.widget.CardView /RelativeLayoutSelectableTextHelper的完整实现比较长关键点我拆开说。构造函数里把 TextView 的文本转成Spannable注册长按、触摸、点击监听public SelectableTextHelper(Builder builder) { mTextView builder.mTextView; mContext mTextView.getContext(); mSelectedColor builder.mSelectedColor; mCursorHandleColor builder.mCursorHandleColor; mCursorHandleSize TextLayoutUtil.dp2px(mContext, builder.mCursorHandleSizeInDp); init(); } private void init() { mTextView.setText(mTextView.getText(), TextView.BufferType.SPANNABLE); mTextView.setOnLongClickListener(v - { showSelectView(mTouchX, mTouchY); return true; }); mTextView.setOnTouchListener((v, event) - { mTouchX (int) event.getX(); mTouchY (int) event.getY(); return false; }); mTextView.setOnClickListener(v - { resetSelectionInfo(); hideSelectView(); }); mOperateWindow new OperateWindow(mContext); }选中范围的计算靠TextLayoutUtil.getPreciseOffset和getHysteresisOffset这两个方法处理了「行尾字符选不中」的经典问题代码在 excerpt 里已经给全直接抄进TextLayoutUtil.java即可。selectText里用BackgroundColorSpan给选中区域上色同时把内容存进mSelectionInfo.mSelectionContentprivate 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); } } }菜单里「记笔记」按钮的点击回调把内容交给外部监听contentView.findViewById(R.id.tv_note).setOnClickListener(v - { if (mNoteBookClickListener ! null) { mNoteBookClickListener.onTextSelect(mSelectionInfo.mSelectionContent); } SelectableTextHelper.this.resetSelectionInfo(); SelectableTextHelper.this.hideSelectView(); });在 Activity 里这样用mSelectableTextHelper new SelectableTextHelper.Builder(mManusTv) .setSelectedColor(getResources().getColor(R.color.color_tv_theme_transparent15)) .setCursorHandleSizeInDp(20) .setCursorHandleColor(getResources().getColor(R.color.colotBtnTheme)) .build(); mSelectableTextHelper.setOnNotesClickListener(content - { String text content.toString().trim(); if (TextUtils.isEmpty(text)) return; // 写入笔记库 NoteRepository.getInstance().insert(new Note(text, System.currentTimeMillis())); Toast.makeText(this, 已记笔记, Toast.LENGTH_SHORT).show(); });笔记落库用 Room 最省事实体和 DAO 骨架Entity(tableName note) public class Note { PrimaryKey(autoGenerate true) public long id; public String content; public long createdAt; public Note(String content, long createdAt) { this.content content; this.createdAt createdAt; } } Dao public interface NoteDao { Insert long insert(Note note); Query(SELECT * FROM note ORDER BY createdAt DESC) ListNote queryAll(); }到这里选中弹菜单到笔记写入的链路就通了。如果你还想在写入前调模型生成摘要或标签用第 2 节的config.toml读 Key走https://taotoken.net/api的对话接口即可模型对话入口在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。4. 验证请求从选中到笔记落库的完整动作配置写完了怎么确认真的跑通按下面四步走每步都有明确的观察点。第一步启动 App长按 TextView 任意位置。预期现象出现两个圆形光标手柄选中区域被半透明色覆盖上方弹出「复制 / 记笔记」菜单。如果菜单没出现先检查mTextView.setText(mTextView.getText(), TextView.BufferType.SPANNABLE)是否执行Spannable是选中上色的前提。第二步拖动手柄调整选中范围。预期现象菜单跟随手柄位置移动选中内容实时更新。这里依赖CursorHandle.update里的getHysteresisOffset如果拖动时手柄跳动或选不中行尾检查TextLayoutUtil是否完整拷贝。第三步点击「记笔记」。预期现象Toast 提示「已记笔记」菜单和手柄消失。在onTextSelect回调里打一行日志Log.d(NoteDebug, selected text , len text.length());第四步查询数据库确认落库。用 Android Studio 的 App Inspection → Database Inspector打开note.db执行SELECT id, content, createdAt FROM note ORDER BY createdAt DESC LIMIT 5;能看到刚才选中的文本就说明链路通了。如果要做模型处理在insert之前加一段请求用config.toml里的base_url和chat_path拼 URLHeader 用Authorization: Bearer keybody 里带上选中文本。请求成功的返回结构里取choices[0].message.content即可。5. 本篇常见错排查菜单不显示或点了没反应。最常见的原因是PopupWindow的setClippingEnabled(false)没设导致菜单被父容器裁剪。另一个原因是showAtLocation的坐标算错posY小于 0 时菜单跑到屏幕外代码里已经做了posY 16的兜底确认这段没被删。选中内容为空或只有第一个字。检查DEFAULT_SELECTION_LENGTH默认是 1长按后初始只选中一个字符需要拖手柄扩展。如果你希望长按直接选中一个词可以在showSelectView里用getPreciseOffset配合getWordStart/getWordEnd扩展范围。小米等机型上原生 ActionMode 方案失效。这就是本文改用自绘方案的原因。系统setCustomSelectionActionModeCallback在部分 ROM 上被拦截注入的 menu item 不显示。自绘方案完全绕开系统菜单兼容性更好代价是要自己处理光标和滚动隐藏逻辑。滚动时菜单不消失。检查mOnScrollChangedListener是否注册isHideWhenScroll标志位是否在onPreDraw里正确复位。这段逻辑在 excerpt 的init()里确认addOnScrollChangedListener和addOnPreDrawListener都调用了。Key 读取为空导致模型请求 401。确认local.properties里的TAOTOKEN_API_KEY没有多余空格buildConfigField生成后重新 Build 一次。如果走环境变量确认 CI 的 secret 名称和config.toml里的api_key_env一致。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的鉴权和错误码说明。笔记重复插入。onTextSelect回调在某些机型上可能触发两次插入前用内容 时间戳做一次去重或者在回调里加一个isInserting标志位。6. 接入与排障入口如果你在接 TaoToken 的过程中遇到鉴权、路径拼接、超时重试的问题直接去 API Keys 页面确认 Key 状态再对照接入文档检查 Header 和 body 格式。API Keys 入口https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。控制台可以看调用量和额度https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。验证模型是否通用模型对话页面发一条测试消息最快https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。如果你后面要把这套选中笔记的能力接到 Claude Code 或 Agent 工作流里Coding Plan 的额度复用会更合适https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。ClaudeCodeAnthropic 相关配置参考https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_code_anthropicutm_campaignrewrite 。最后留一个我踩过的坑SelectableTextHelper的destroy()一定要在onViewDetachedFromWindow里调用否则ViewTreeObserver的监听器会泄漏页面来回切换几次后内存就上去了。把removeOnScrollChangedListener和removeOnPreDrawListener都加上这个问题就没了。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

【外设】之大彩串口显示屏 2026/11/13 0:43:49

【外设】之大彩串口显示屏

大彩串口屏初步使用 1 .官网下载 STM32 屏幕 GUI 设计资料 http://www.gz-dc.com/category/typeid/4112 找到 STM32 Keil 工程,移植相关代码因项目而异进行移植,由于项目简单,本人只对用到的指令接口进行修改。 比如:注意事项&…

阅读更多 →
无法下载Windows系统iso文件 2026/11/12 23:59:09

无法下载Windows系统iso文件

当我遇到这个问题的时候,我打开了一个网站: 登录 然后我打算下载的时候: 突然那个官方的连接就可以下载了:

阅读更多 →
【清华代码熊】DeepSeek V4.1 Flash 后训练详解 2026/11/12 9:50:02

【清华代码熊】DeepSeek V4.1 Flash 后训练详解

📌 上期解析了 DeepSeek V4.1 Flash 模型架构改进,本期解析 DeepSeek V4.1 Flash 预训练/后训练技术: 🌟 预训练:45T 文本 多模态混合语料、直接训练 sparse attention(取消 DeepSeek V4 的 dense 冷启动&…

阅读更多 →
Shuffle-R1: Efficient RL framework for Multimodal Large Language Models via Data-centric Dynamic ... 2026/11/12 9:25:38

Shuffle-R1: Efficient RL framework for Multimodal Large Language Models via Data-centric Dynamic ...

文章主要内容和创新点 主要内容 本文聚焦于多模态大语言模型(MLLM)强化学习(RL)训练中的效率问题,提出了一个名为Shuffle-R1的框架。研究发现,当前RL训练存在两个关键缺陷: 优势值坍缩(Advantage Collapsing):批次中大多数优势值集中在零附近,导致有效梯度信号被淹…

阅读更多 →
PRvL: Quantifying the Capabilities and Risks of Large Language Models for PII Redaction 2026/11/12 15:57:44

PRvL: Quantifying the Capabilities and Risks of Large Language Models for PII Redaction

一、文章主要内容总结 本文聚焦于利用大型语言模型(LLMs)实现个人身份信息(PII)脱敏的研究,旨在解决传统脱敏方法(如基于规则的系统、领域特定命名实体识别(NER)模型)泛化能力差、跨格式/跨语境适应性弱的问题。 研究通过全面评估多种LLM架构(包括密集型LLM(D-LLM…

阅读更多 →
LLaVA-RE: Binary Image-Text Relevancy Evaluation with Multimodal Large Language Model 2026/11/13 0:02:27

LLaVA-RE: Binary Image-Text Relevancy Evaluation with Multimodal Large Language Model

文章主要内容和创新点 主要内容 本文聚焦于二进制图像-文本相关性评估任务(判断图像与文本“相关”或“不相关”),针对该任务中文本格式多样、相关性定义随场景变化等挑战,提出了基于多模态大语言模型(MLLM)的解决方案LLaVA-RE。 模型设计:LLaVA-RE基于LLaVA 1.5架构,…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉