InputConnection机制与跨进程文本操作的工程实践
💡 本文精华
- 承接前文:第一篇讲Binder通信,第二篇讲设计缺陷,本篇给出完整的工程解决方案
- 核心需求:既保留数字键盘(inputType="number"),又避免字母输入导致数字丢失
- 最佳方案 :自定义InputConnection在
setComposingText()层拦截非法字符,阻止进入delete-insert流程 - 三种方案对比:InputConnection拦截(推荐⭐⭐⭐⭐⭐)、TextWatcher过滤(⭐⭐⭐)、自定义InputFilter(⭐⭐⭐⭐)
- 价值:提供可直接使用的完整代码,分析方案优劣,指导工程实践
一、需求分析与约束条件
1.1 核心需求
markdown
用户需求:
1. 点击EditText时,弹出数字键盘(不是全键盘)
2. 只允许输入数字字符
3. 切换到字母键盘输入字母时,不应导致已有数字丢失
技术约束:
1. 必须使用 android:inputType="number" (保证数字键盘)
2. 不能修改Android框架代码
3. 需要兼容主流输入法(百度、搜狗、讯飞等)
4. 性能开销要小
1.2 问题回顾
前两篇已分析的根本原因:
css
输入法进程:
ic.setComposingText("qq", 1)
↓ Binder IPC
应用进程:
BaseInputConnection.replaceText("qq", 1, true)
↓
1. delete(5, 6) → 成功(绕过InputFilter)
2. insert("qq") → 失败(被DigitsKeyListener拒绝)
↓
结果: 数据丢失
解决方向:
- 在delete执行之前拦截
- 判断新文本是否合法
- 如果非法,不进入delete-insert流程
二、解决方案一:InputConnection拦截(推荐⭐⭐⭐⭐⭐)
2.1 核心思路
css
原始流程(有Bug):
setComposingText("qq")
→ replaceText()
→ delete(5, 6) ← 删除数字
→ insert("qq") ← 被拒绝
→ Bug!
优化后流程:
setComposingText("qq")
→ NumberInputConnectionWrapper.setComposingText()
→ 检测到字母 'q'
→ 调用 finishComposingText() ← 直接完成组合,不执行delete-insert
→ 数字保持不变 ✓
2.2 完整实现代码
java
public class RecommendedNumberEditText extends AppCompatEditText {
public RecommendedNumberEditText(Context context) {
super(context);
}
public RecommendedNumberEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RecommendedNumberEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
// 获取父类创建的InputConnection
InputConnection base = super.onCreateInputConnection(outAttrs);
if (base == null) {
return null;
}
// 包装成我们的自定义InputConnection
return new NumberInputConnectionWrapper(base, true);
}
/**
* 自定义InputConnection包装类
* 拦截setComposingText,检查是否包含非数字字符
*/
private static class NumberInputConnectionWrapper extends InputConnectionWrapper {
public NumberInputConnectionWrapper(InputConnection target, boolean mutable) {
super(target, mutable);
}
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
// 关键:在执行delete-insert之前检查
if (text != null && containsNonDigit(text)) {
// 包含非数字字符,直接完成组合(不执行delete-insert)
return super.finishComposingText();
}
// 纯数字或null,正常处理
return super.setComposingText(text, newCursorPosition);
}
/**
* 检查CharSequence是否包含非数字字符
*/
private boolean containsNonDigit(CharSequence text) {
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (!Character.isDigit(c)) {
return true; // 发现非数字字符
}
}
return false; // 全部是数字
}
}
}
2.3 使用方法
xml
<!-- activity_main.xml -->
<com.yourpackage.RecommendedNumberEditText
android:id="@+id/edit_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入数字"
android:inputType="number" />
java
// MainActivity.java
RecommendedNumberEditText editText = findViewById(R.id.edit_number);
// 正常使用,无需额外代码
2.4 方案优势
| 优势 | 说明 |
|---|---|
| 拦截时机早 | 在delete之前拦截,从源头解决问题 |
| 性能最优 | 不执行delete-insert,避免无效操作 |
| 对输入法透明 | 返回true,输入法认为成功,不影响正常流程 |
| 不影响正常输入 | 数字输入不受影响,仍走原有逻辑 |
| 代码简洁 | 只需重写一个方法 |
| 数字键盘保留 | inputType="number"保证数字键盘弹出 |
2.5 工作流程图
css
用户操作:
输入数字 "123456"
↓
切换到字母键盘
↓
点击 'q'
输入法进程:
ic.setComposingText("q", 1)
↓ Binder IPC
应用进程:
NumberInputConnectionWrapper.setComposingText("q", 1)
↓
containsNonDigit("q") → true (检测到字母)
↓
调用 finishComposingText()
↓
BaseInputConnection.finishComposingText()
- 移除COMPOSING Span
- 不执行delete
- 不执行insert
↓
返回 true
↓
内容保持 "123456" ✓
继续点击 'q':
同样流程,每次都调用 finishComposingText()
内容始终保持 "123456" ✓
三、解决方案二:TextWatcher过滤(⭐⭐⭐)
3.1 核心思路
原理:
不依赖InputFilter,而是监听文本变化
在文本改变后,过滤掉非数字字符
3.2 完整实现代码
java
public class SimpleNumberEditText extends AppCompatEditText {
private boolean isFiltering = false; // 防止递归
public SimpleNumberEditText(Context context) {
super(context);
init();
}
public SimpleNumberEditText(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SimpleNumberEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
// 不设置inputType为number,避免DigitsKeyListener
// 但会丢失数字键盘,需要手动设置
setRawInputType(InputType.TYPE_CLASS_NUMBER);
addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// 不需要处理
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// 不需要处理
}
@Override
public void afterTextChanged(Editable s) {
if (isFiltering) {
return; // 避免递归
}
// 过滤掉非数字字符
String filtered = s.toString().replaceAll("[^0-9]", "");
if (!s.toString().equals(filtered)) {
isFiltering = true;
// 保存光标位置
int cursorPos = getSelectionStart();
int lengthBefore = s.length();
// 替换为过滤后的内容
setText(filtered);
// 调整光标位置
int lengthAfter = filtered.length();
int diff = lengthBefore - lengthAfter;
int newCursorPos = Math.max(0, cursorPos - diff);
setSelection(Math.min(newCursorPos, filtered.length()));
isFiltering = false;
}
}
});
}
}
3.3 方案优劣
| 维度 | 评价 |
|---|---|
| 优点 | 实现简单,易于理解 |
| 优点 | 不依赖InputConnection,逻辑独立 |
| 缺点 | 需要额外的setText操作,性能略差 |
| 缺点 | 光标位置处理复杂 |
| 缺点 | 会触发额外的TextWatcher回调 |
3.4 性能对比
css
方案一(InputConnection拦截):
setComposingText("qq")
→ 检测到字母
→ finishComposingText()
→ 1次操作
方案二(TextWatcher):
setComposingText("qq")
→ delete(5, 6) → 删除'6'
→ insert("qq") → 被拒绝
→ afterTextChanged触发
→ setText("123456") → 恢复
→ 3次操作
性能差距:方案二约3倍于方案一
四、解决方案三:自定义InputFilter(⭐⭐⭐⭐)
4.1 核心思路
sql
原理:
不仅拦截insert,也拦截delete
通过判断source是否为空来区分操作类型
4.2 完整实现代码
java
public class SafeNumberEditText extends AppCompatEditText {
public SafeNumberEditText(Context context) {
super(context);
init();
}
public SafeNumberEditText(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SafeNumberEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
// 设置自定义Filter,替换系统的DigitsKeyListener
setFilters(new InputFilter[]{new SafeNumberInputFilter()});
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
InputConnection base = super.onCreateInputConnection(outAttrs);
if (base == null) {
return null;
}
// 同时使用InputConnection拦截(双重保护)
return new SafeInputConnectionWrapper(base, true);
}
/**
* 自定义InputFilter
* 拦截非数字字符的插入
*/
private static class SafeNumberInputFilter implements InputFilter {
@Override
public CharSequence filter(CharSequence source, int start, int end,
Spanned dest, int dstart, int dend) {
// 过滤掉非数字字符
StringBuilder filtered = new StringBuilder();
for (int i = start; i < end; i++) {
char c = source.charAt(i);
if (Character.isDigit(c)) {
filtered.append(c);
}
}
// 如果全部被过滤掉,返回空字符串
if (filtered.length() == 0 && end > start) {
return "";
}
// 如果有字符被过滤掉,返回过滤后的内容
if (filtered.length() < (end - start)) {
return filtered.toString();
}
// 全部合法,返回null(接受原始内容)
return null;
}
}
/**
* InputConnection包装类
* 作为第二层防护
*/
private static class SafeInputConnectionWrapper extends InputConnectionWrapper {
public SafeInputConnectionWrapper(InputConnection target, boolean mutable) {
super(target, mutable);
}
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
if (text != null && containsNonDigit(text)) {
return super.finishComposingText();
}
return super.setComposingText(text, newCursorPosition);
}
private boolean containsNonDigit(CharSequence text) {
for (int i = 0; i < text.length(); i++) {
if (!Character.isDigit(text.charAt(i))) {
return true;
}
}
return false;
}
}
}
4.3 方案特点
| 特点 | 说明 |
|---|---|
| 双重保护 | InputFilter + InputConnection两层拦截 |
| 灵活性高 | 可以自定义过滤规则(如允许小数点) |
| 兼容性好 | 适配各种输入场景 |
| 代码复杂 | 需要同时实现Filter和Wrapper |
五、三种方案对比与选型指南
5.1 全面对比
| 维度 | 方案一:InputConnection | 方案二:TextWatcher | 方案三:自定义Filter |
|---|---|---|---|
| 实现复杂度 | ⭐⭐ (简单) | ⭐ (最简单) | ⭐⭐⭐⭐ (复杂) |
| 性能 | ⭐⭐⭐⭐⭐ (最优) | ⭐⭐⭐ (一般) | ⭐⭐⭐⭐ (好) |
| 拦截时机 | 早(delete之前) | 晚(文本已改变) | 早(但有局限) |
| 数字键盘 | ✓ 自动 | ✓ 手动设置 | ✓ 自动 |
| 光标处理 | ✓ 自动 | ✗ 需手动 | ✓ 自动 |
| 代码量 | ~40行 | ~60行 | ~80行 |
| 推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
5.2 选型建议
makefile
场景1: 普通数字输入框
推荐: 方案一(InputConnection拦截)
理由: 性能最优,代码最简洁
场景2: 需要自定义过滤规则(如电话号码:数字 + '-')
推荐: 方案三(自定义Filter + InputConnection)
理由: 灵活性高,可自定义allowed字符集
场景3: 快速原型开发
推荐: 方案二(TextWatcher)
理由: 最简单,快速实现
场景4: 高性能要求(如大量EditText)
推荐: 方案一(InputConnection拦截)
理由: 性能开销最小
场景5: 复杂输入验证(如金额:数字 + 小数点,最多2位小数)
推荐: 方案三(自定义Filter)
理由: 可以在filter()中实现复杂规则
5.3 实际项目中的使用
java
// 示例:金额输入框(允许数字和小数点,最多2位小数)
public class MoneyEditText extends AppCompatEditText {
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
InputConnection base = super.onCreateInputConnection(outAttrs);
if (base == null) {
return null;
}
return new MoneyInputConnectionWrapper(base, true);
}
private static class MoneyInputConnectionWrapper extends InputConnectionWrapper {
public MoneyInputConnectionWrapper(InputConnection target, boolean mutable) {
super(target, mutable);
}
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
// 检查是否符合金额格式
if (text != null && !isValidMoneyInput(text)) {
return super.finishComposingText();
}
return super.setComposingText(text, newCursorPosition);
}
private boolean isValidMoneyInput(CharSequence text) {
// 允许:数字、小数点
String str = text.toString();
// 简单验证:只包含[0-9.]
return str.matches("[0-9.]*");
}
}
// 可以配合InputFilter做更复杂的验证(如最多2位小数)
private static class MoneyInputFilter implements InputFilter {
@Override
public CharSequence filter(CharSequence source, int start, int end,
Spanned dest, int dstart, int dend) {
// 组合后的完整文本
String beforeInsert = dest.subSequence(0, dstart).toString();
String afterInsert = dest.subSequence(dend, dest.length()).toString();
String newText = beforeInsert + source.subSequence(start, end) + afterInsert;
// 验证:最多一个小数点
if (newText.indexOf('.') != newText.lastIndexOf('.')) {
return ""; // 拒绝(多个小数点)
}
// 验证:小数点后最多2位
int dotIndex = newText.indexOf('.');
if (dotIndex != -1 && newText.length() - dotIndex > 3) {
return ""; // 拒绝(超过2位小数)
}
return null; // 接受
}
}
}
六、工程实践的最佳实践
6.1 性能监控
java
public class MonitoredNumberEditText extends AppCompatEditText {
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
InputConnection base = super.onCreateInputConnection(outAttrs);
if (base == null) {
return null;
}
return new MonitoredInputConnectionWrapper(base, true);
}
private static class MonitoredInputConnectionWrapper extends InputConnectionWrapper {
public MonitoredInputConnectionWrapper(InputConnection target, boolean mutable) {
super(target, mutable);
}
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
long startTime = System.nanoTime();
boolean result;
if (text != null && containsNonDigit(text)) {
result = super.finishComposingText();
} else {
result = super.setComposingText(text, newCursorPosition);
}
long duration = System.nanoTime() - startTime;
if (duration > 1_000_000) { // 超过1ms
Log.w("InputMonitor", "setComposingText slow: " + duration / 1_000_000.0 + "ms");
}
return result;
}
private boolean containsNonDigit(CharSequence text) {
for (int i = 0; i < text.length(); i++) {
if (!Character.isDigit(text.charAt(i))) {
return true;
}
}
return false;
}
}
}
6.2 日志调试
java
public class DebugNumberEditText extends AppCompatEditText {
private static final String TAG = "DebugNumberEditText";
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
InputConnection base = super.onCreateInputConnection(outAttrs);
if (base == null) {
return null;
}
return new DebugInputConnectionWrapper(base, true);
}
private static class DebugInputConnectionWrapper extends InputConnectionWrapper {
public DebugInputConnectionWrapper(InputConnection target, boolean mutable) {
super(target, mutable);
}
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
Log.d(TAG, "setComposingText: text='" + text + "', cursor=" + newCursorPosition);
boolean hasNonDigit = false;
if (text != null) {
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (!Character.isDigit(c)) {
hasNonDigit = true;
Log.w(TAG, " Detected non-digit: '" + c + "' at index " + i);
}
}
}
boolean result;
if (hasNonDigit) {
Log.d(TAG, " Action: finishComposingText() (rejecting non-digit input)");
result = super.finishComposingText();
} else {
Log.d(TAG, " Action: setComposingText() (accepting digit input)");
result = super.setComposingText(text, newCursorPosition);
}
Log.d(TAG, " Result: " + result);
return result;
}
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
Log.d(TAG, "commitText: text='" + text + "', cursor=" + newCursorPosition);
return super.commitText(text, newCursorPosition);
}
@Override
public boolean finishComposingText() {
Log.d(TAG, "finishComposingText");
return super.finishComposingText();
}
}
}
6.3 单元测试
java
@RunWith(RobolectricTestRunner.class)
public class NumberEditTextTest {
@Test
public void testDigitInput() {
// 测试正常数字输入
RecommendedNumberEditText editText = new RecommendedNumberEditText(ApplicationProvider.getApplicationContext());
editText.setText("123");
InputConnection ic = editText.onCreateInputConnection(new EditorInfo());
ic.setComposingText("456", 1);
assertEquals("123456", editText.getText().toString());
}
@Test
public void testLetterInput() {
// 测试字母输入(应该被拒绝)
RecommendedNumberEditText editText = new RecommendedNumberEditText(ApplicationProvider.getApplicationContext());
editText.setText("123");
InputConnection ic = editText.onCreateInputConnection(new EditorInfo());
ic.setComposingText("abc", 1);
// 内容不应该变化
assertEquals("123", editText.getText().toString());
}
@Test
public void testMixedInput() {
// 测试混合输入
RecommendedNumberEditText editText = new RecommendedNumberEditText(ApplicationProvider.getApplicationContext());
editText.setText("123");
InputConnection ic = editText.onCreateInputConnection(new EditorInfo());
ic.setComposingText("4a5", 1);
// 包含字母,应该被拒绝
assertEquals("123", editText.getText().toString());
}
}
6.4 兼容性测试
java
/**
* 测试不同输入法的兼容性
*/
public class IMECompatibilityTest {
@Test
public void testBaiduIME() {
// 模拟百度输入法的行为
// ...
}
@Test
public void testSogouIME() {
// 模拟搜狗输入法的行为
// ...
}
@Test
public void testGboard() {
// 模拟Google Gboard的行为
// ...
}
}
七、常见问题与解答
7.1 为什么不直接去掉inputType="number"?
问题:去掉inputType后,不会有Bug,为什么不这样做?
解答:
ini
去掉 inputType="number" 的后果:
1. 弹出全键盘(而不是数字键盘)
2. 用户体验变差(输入数字需要切换键盘)
3. 无法使用系统的数字键盘优化
保留 inputType="number" + InputConnection拦截:
1. 保留数字键盘(用户体验好)
2. 避免数据丢失(技术正确)
3. 最佳实践
7.2 为什么调用finishComposingText()?
问题:为什么不直接return false?
解答:
java
// 错误做法
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
if (containsNonDigit(text)) {
return false; // ✗ 输入法会认为失败,可能重试或报错
}
return super.setComposingText(text, newCursorPosition);
}
// 正确做法
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
if (containsNonDigit(text)) {
return super.finishComposingText(); // ✓ 告诉输入法"组合已完成"
}
return super.setComposingText(text, newCursorPosition);
}
原因:
finishComposingText()是合法的操作,输入法会正常处理- 返回
false表示操作失败,输入法可能出现异常行为 finishComposingText()会清除COMPOSING Span,避免后续问题
7.3 为什么不用setFilters完全替代?
问题:为什么不只用InputFilter,还要用InputConnection?
解答:
markdown
单独使用 InputFilter 的问题:
1. 无法阻止delete操作(delete绕过Filter)
2. 仍会触发Bug
单独使用 InputConnection 的优势:
1. 拦截时机早(delete之前)
2. 从源头解决问题
最佳实践:
InputConnection拦截为主(方案一)
或 InputFilter + InputConnection双重保护(方案三)
7.4 如何支持小数点?
扩展方案:
java
private boolean isValidNumberInput(CharSequence text) {
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
// 允许数字和小数点
if (!Character.isDigit(c) && c != '.') {
return false;
}
}
return true;
}
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
if (text != null && !isValidNumberInput(text)) {
return super.finishComposingText();
}
return super.setComposingText(text, newCursorPosition);
}
// 同时设置inputType
setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
八、总结:从理论到实践的完整链路
8.1 三篇文章的知识链
sql
第一篇:Binder通信机制
├─ 三进程架构
├─ AIDL接口定义
├─ Stub/Proxy实现
├─ 线程切换
└─ 信息不对称
↓ 为什么输入法不知道实际结果
第二篇:文本编辑设计缺陷
├─ InputFilter接口局限
├─ delete绕过Filter
├─ replaceText先delete后insert
├─ Span范围异常扩展
└─ 不对称的控制权
↓ 为什么数字会被删除
第三篇(本文):工程解决方案
├─ InputConnection拦截(推荐)
├─ TextWatcher过滤(简单)
├─ 自定义InputFilter(灵活)
├─ 三种方案对比
├─ 选型指南
├─ 最佳实践
└─ 完整代码
↓ 如何在实际项目中解决
8.2 最佳实践总结
| 场景 | 推荐方案 | 核心代码 |
|---|---|---|
| 普通数字输入 | InputConnection拦截 | finishComposingText() |
| 自定义规则 | 自定义Filter + IC | filter() + finishComposingText() |
| 快速开发 | TextWatcher | afterTextChanged() + setText() |
| 高性能 | InputConnection拦截 | 避免多余操作 |
8.3 核心要点
markdown
关键设计决策:
1. 保留 inputType="number" (数字键盘)
2. 重写 onCreateInputConnection() (拦截入口)
3. 包装 InputConnectionWrapper (扩展功能)
4. 拦截 setComposingText() (关键方法)
5. 调用 finishComposingText() (正确处理)
为什么这样设计:
1. 用户体验:数字键盘更方便
2. 性能优化:拦截早,开销小
3. 兼容性好:对输入法透明
4. 代码简洁:只需40行代码
5. 易于维护:逻辑清晰
8.4 延伸应用
java
// 可以扩展到其他场景:
// 1. 手机号输入(数字 + 空格/横线)
public class PhoneNumberEditText extends AppCompatEditText {
// ...
}
// 2. 邮箱输入(限制特殊字符)
public class EmailEditText extends AppCompatEditText {
// ...
}
// 3. 用户名输入(字母 + 数字 + 下划线)
public class UsernameEditText extends AppCompatEditText {
// ...
}
// 4. 密码输入(增强安全)
public class SecurePasswordEditText extends AppCompatEditText {
// ...
}
九、完整示例项目结构
css
app/src/main/java/com/yourpackage/
├─ widget/
│ ├─ RecommendedNumberEditText.java ← 方案一(推荐)
│ ├─ SimpleNumberEditText.java ← 方案二
│ ├─ SafeNumberEditText.java ← 方案三
│ ├─ MoneyEditText.java ← 扩展:金额输入
│ ├─ PhoneNumberEditText.java ← 扩展:电话输入
│ └─ DebugNumberEditText.java ← 调试版本
│
├─ utils/
│ └─ InputValidationUtils.java ← 通用验证工具
│
└─ MainActivity.java
app/src/main/res/layout/
└─ activity_main.xml
app/src/test/java/com/yourpackage/
└─ NumberEditTextTest.java ← 单元测试
参考资料
- Android源码:
frameworks/base/core/java/android/view/inputmethod/InputConnectionWrapper.java - 官方文档:InputConnectionWrapper
- 官方文档:InputConnection
- 本系列前两篇:
- 第一篇:《Android输入法框架的Binder通信机制剖析》
- 第二篇:《从一个Bug看Android文本编辑的设计缺陷》
附录:完整可运行的示例代码
RecommendedNumberEditText.java(直接复制可用):
java
package com.yourpackage.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputConnectionWrapper;
import androidx.appcompat.widget.AppCompatEditText;
/**
* 推荐的数字输入框实现
* 解决 inputType="number" 时,切换字母键盘导致数字丢失的问题
*
* 使用方法:
* <com.yourpackage.widget.RecommendedNumberEditText
* android:inputType="number"
* ... />
*/
public class RecommendedNumberEditText extends AppCompatEditText {
public RecommendedNumberEditText(Context context) {
super(context);
}
public RecommendedNumberEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RecommendedNumberEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
InputConnection base = super.onCreateInputConnection(outAttrs);
if (base == null) {
return null;
}
return new NumberInputConnectionWrapper(base, true);
}
/**
* 自定义InputConnection包装类
* 拦截setComposingText,检查是否包含非数字字符
*/
private static class NumberInputConnectionWrapper extends InputConnectionWrapper {
public NumberInputConnectionWrapper(InputConnection target, boolean mutable) {
super(target, mutable);
}
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
if (text != null && containsNonDigit(text)) {
// 包含非数字字符,直接完成组合(不执行delete-insert)
return super.finishComposingText();
}
// 纯数字或null,正常处理
return super.setComposingText(text, newCursorPosition);
}
/**
* 检查是否包含非数字字符
*/
private boolean containsNonDigit(CharSequence text) {
for (int i = 0; i < text.length(); i++) {
if (!Character.isDigit(text.charAt(i))) {
return true;
}
}
return false;
}
}
}
activity_main.xml:
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="推荐方案(InputConnection拦截):"
android:textSize="16sp"
android:textStyle="bold" />
<com.yourpackage.widget.RecommendedNumberEditText
android:id="@+id/edit_recommended"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入数字"
android:inputType="number" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="测试说明:"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="1. 输入数字 123456\n2. 切换到字母键盘\n3. 连续点击字母 'q'\n4. 数字不会被删除 ✓"
android:textSize="12sp" />
</LinearLayout>
系列完结。从Binder通信机制,到文本编辑设计缺陷,再到完整的工程解决方案,希望这三篇文章能帮助你深入理解Android输入法框架,并在实际项目中游刃有余地解决类似问题。