第2周:`EditText` 不只是输入框,它是 Android 输入体验的第一道门

第 1 周做完 TextView 后,页面终于能把文字稳定地展示出来了。到了第 2 周,事情开始往前走一步:用户不只是看,还要开始输入。

很多人第一次接触 EditText,会觉得它就是"能打字的 TextView"。这个理解不能说完全错,但太浅了。真正写业务以后你会发现,输入框一旦出问题,用户的感受会特别明显:键盘弹不出来、密码切换后光标乱跳、验证码输入不了、搜索每打一个字就卡一下、退出页面后监听器还没清理,这些都不是"好不好看"的问题,而是页面有没有专业度的问题。

所以第 2 周的目标很明确:EditText 从"能输入"做到"输入得舒服、稳定、可控"。

这周到底要解决什么

第 2 周主题是:EditText 全功能 + 输入体验优化

这一周要把下面这些点真正跑通:

  • 输入类型限制:inputType

  • 自定义过滤规则:InputFilter

  • 密码显隐:TYPE_TEXT_VARIATION_PASSWORD / TYPE_TEXT_VARIATION_VISIBLE_PASSWORD

  • 字数校验:LengthFilter + TextWatcher

  • 软键盘控制:InputMethodManager

  • 光标操作:setSelection

  • 文本监听:TextWatcher

  • 输入防抖:DebouncedTextWatcher

  • 生命周期清理:removeTextChangedListenerHandler.removeCallbacks

看起来很多,但它们其实都围绕同一件事:输入过程必须可治理。

先从输入边界开始:用户能输入什么,不应该最后才知道

真实项目里,很多输入框不是"什么都能输入"。用户名可能只能允许字母、数字和下划线;验证码只能是数字;昵称有最大长度;简介不能无限输入。

这时候第一层能力就是 inputTypeInputFilter

bash 复制代码
binding.etUsername.apply {
    inputType=InputType.TYPE_CLASS_TEXTorInputType.TYPE_TEXT_VARIATION_NORMAL
    filters=arrayOf(
        InputFilter.LengthFilter(12),
        EditTextExperienceOptimizer.usernameFilter()
    )
}

这段代码做了两件事:

  • inputType:告诉系统这是一个普通文本输入场景,系统会据此决定键盘类型和输入行为

  • InputFilter.LengthFilter(12):限制最多输入 12 个字符

  • usernameFilter():自定义过滤规则,只允许特定字符进入输入框

如果没有这些限制,很多错误会被拖到"点击提交"那一刻才暴露。对用户来说,这种体验很糟:他已经输入了一长串,最后你才告诉他"不合法"。更好的做法是,在输入入口就把边界立住。

自定义过滤规则可以这样写:

bash 复制代码
funusernameFilter(): InputFilter {
    returnInputFilter { source, start, end, _, _, _->
        valfiltered=buildString {
            for (indexinstartuntilend) {
                valchar=source[index]
                if (char.isLetterOrDigit() ||char=='_') {
                    append(char)
                }
            }
        }
        if (filtered.length==end-start) nullelsefiltered
    }
}

这里有一个初学者很容易漏掉的点:InputFilter 返回 null 表示"原样接受输入",返回一个字符串表示"用这个过滤后的内容替换原输入"。所以这段代码不是提交时校验,而是字符进入 EditText 前就先筛一遍。

这就是输入治理的第一步:不合法的东西,最好不要让它进来。

密码显隐:别只会切换,还要保住光标

密码显隐看起来很简单:点一下显示密码,再点一下隐藏密码。很多人第一次写的时候也确实能跑起来,但会遇到一个很烦的小问题:切换后光标位置乱跳。

这不是小事。用户正在输入密码,你一切换显示状态,光标跳到开头或者末尾,他马上会觉得这个输入框"不顺手"。

bash 复制代码
privatefuntogglePasswordVisible() {
    valselection=binding.etPassword.selectionEnd.coerceAtLeast(0)
    passwordVisible=!passwordVisible
    binding.etPassword.inputType=InputType.TYPE_CLASS_TEXTorif (passwordVisible) {
        InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
    } else {
        InputType.TYPE_TEXT_VARIATION_PASSWORD
    }
    binding.etPassword.setSelection(selection.coerceAtMost(binding.etPassword.text.length))
    binding.btnTogglePassword.text=if (passwordVisible) "隐藏密码"else"显示密码"
}

这段代码的关键不是 passwordVisible 这个布尔值,而是这三步:

  1. 切换前先保存 selectionEnd

  2. 修改 inputType

  3. setSelection() 把光标恢复回去

如果少了第三步,密码显隐看起来也许能用,但体验会很粗糙。写输入框的时候,这种"看起来没大问题"的细节,往往就是专业和不专业的分界线。

字数校验:LengthFilter 管上限,TextWatcher 管反馈

很多输入框都要做字数限制,比如昵称 12 个字、简介 80 个字、评论 500 个字。这里至少要分清两件事:

  • InputFilter.LengthFilter:负责硬性限制输入上限

  • TextWatcher:负责在输入过程中给用户实时反馈

bash 复制代码
binding.etBio.apply {
    inputType=InputType.TYPE_CLASS_TEXTorInputType.TYPE_TEXT_FLAG_MULTI_LINE
    filters=arrayOf(InputFilter.LengthFilter(80))
}

attachSimpleWatcher(binding.etBio) {
    binding.tvCounterState.text="简介字数:${binding.etBio.text.length}/80"
}

这里 LengthFilter(80) 保证用户最多只能输入 80 个字符,TextWatcher 则让用户随时知道自己已经输入了多少。

这比"提交时才告诉你超字数"要友好得多。输入体验的很多优化,不是靠炫技,而是靠这种及时反馈一点点堆出来的。

TextWatcher 的基础写法是这样的:

bash 复制代码
privatefunattachSimpleWatcher(editText: EditText, afterChanged: () ->Unit) {
    valwatcher=object : TextWatcher {
        overridefunbeforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) =Unit
        overridefunonTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) =Unit
        overridefunafterTextChanged(s: Editable?) =afterChanged()
    }
    editText.addTextChangedListener(watcher)
    normalWatchers+=editTexttowatcher
}

TextWatcher 有三个回调:

  • beforeTextChanged:文本变化前

  • onTextChanged:文本变化过程中

  • afterTextChanged:文本变化后

真实业务里最常用的是 afterTextChanged,因为这时候拿到的内容已经是最终结果,适合做计数、按钮状态更新、简单校验。

软键盘控制:先有焦点,再谈弹键盘

EditText 里一个很常见的误区是:我调用了显示键盘的方法,为什么键盘没弹?

原因经常是:输入框还没有焦点。

bash 复制代码
privatefunshowKeyboard(editText: EditText) {
    editText.requestFocus()
    valimm=getSystemService(Context.INPUT_METHOD_SERVICE) asInputMethodManager
    imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
}

privatefunhideKeyboard() {
    valtarget=currentFocus?: binding.root
    valimm=getSystemService(Context.INPUT_METHOD_SERVICE) asInputMethodManager
    imm.hideSoftInputFromWindow(target.windowToken, 0)
}

这段代码里有两个专业名词一定要记住:

  • requestFocus():让某个输入框成为当前焦点目标

  • InputMethodManager:Android 里负责和输入法交互的系统服务

显示键盘时,先 requestFocus(),再 showSoftInput()。隐藏键盘时,需要通过当前 View 的 windowToken 告诉系统"我要隐藏这个窗口上的输入法"。

这类代码看起来不像业务逻辑,但它直接决定输入体验。搜索页、登录页、评论弹窗、聊天输入框,全都会遇到它。

光标操作:setSelection 是小 API,但很常用

输入框里还有一个细节经常被忽略:光标位置。

比如用户点"编辑简介",你希望光标默认在末尾;点"补充前缀",你希望光标移动到开头;密码显隐切换后,你希望光标还停在原来的地方。这些都离不开 setSelection()

bash 复制代码
privatefunmoveBioCursor(toEnd: Boolean) {
    binding.etBio.requestFocus()
    valtarget=if (toEnd) binding.etBio.text.lengthelse0
    binding.etBio.setSelection(target)
    showKeyboard(binding.etBio)
}

这里一定要注意:setSelection() 的目标位置不能越界。比如文本长度是 10,你不能把光标设置到 20,否则就可能直接崩。

这也是为什么密码显隐那段代码里要写:

bash 复制代码
binding
.
etPassword
.
setSelection
(
selection
.
coerceAtMost
(
binding
.
etPassword
.
text
.
length
))

coerceAtMost() 的作用就是把光标位置控制在合法范围内。

输入防抖:不要每输入一个字就做重任务

第 2 周最重要的优化意识,就是输入防抖。

想象一个搜索框:用户输入"android",如果你每打一个字就请求一次网络,那么 aanandandrandroandroid 都会触发请求。这不仅浪费资源,还可能造成结果乱序、页面卡顿和状态混乱。

更合理的方式是:用户停下来一小段时间后,再执行真正的逻辑。

bash 复制代码
classDebouncedTextWatcher(
    privatevaldelayMillis: Long=300L,
    privatevalonTextDebounced: (String) ->Unit
) : TextWatcher {

    privatevalhandler=Handler(Looper.getMainLooper())
    privatevarpendingTask: Runnable?=null

    overridefunbeforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) =Unit

    overridefunonTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        pendingTask?.let(handler:: )
        valsnapshot=s?.toString().orEmpty()
        pendingTask=Runnable { onTextDebounced(snapshot) }
        handler.postDelayed(pendingTask!!, delayMillis)
    }

    overridefunafterTextChanged(s: Editable?) =Unit

    funclear() {
        pendingTask?.let(handler::removeCallbacks)
        pendingTask=null
    }
}

这段代码做了一个很经典的防抖动作:

  1. 每次输入变化,先取消上一次还没执行的 Runnable

  2. 保存这次输入的文本快照

  3. 延迟一段时间再执行真正逻辑

  4. 如果这段时间内用户继续输入,就继续取消并重新计时

防抖不是为了"显得高级",它解决的是高频输入带来的性能和状态问题。搜索联想、用户名可用性校验、远程表单校验,都很需要它。

生命周期清理:TextWatcher 用完要拆

这一点非常重要,但很多初学者会跳过去。

TextWatcher 是挂在 EditText 上的监听器;防抖里还有 Handler 和延迟任务。如果页面退出后这些东西还没清理,轻则逻辑继续执行,重则可能让 Activity 被间接持有,增加内存泄漏风险。

bash 复制代码
overridefunonDestroy() {
    normalWatchers.forEach { (editText, watcher) ->
        editText.removeTextChangedListener(watcher)
    }
    debounceWatchers.forEach { (editText, watcher) ->
        editText.removeTextChangedListener(watcher)
        watcher.clear()
    }
    normalWatchers.clear()
    debounceWatchers.clear()
    super.onDestroy()
}

这段代码做的是"收尾工作":

  • removeTextChangedListener():把监听器从 EditText 上拆下来

  • watcher.clear():取消还没执行的延迟任务

  • clear():清空集合引用

说实话,这类代码写起来不刺激,也不酷,但它很专业。因为真正稳定的页面,不只是进入时能跑,还要退出时干净。

为什么第 2 周也要做成可交互 Demo

这周的 Demo 不是只摆几个输入框,而是做成了一个完整的输入实验区:

  • 用户名输入:看 inputTypeInputFilter

  • 密码输入:看显隐切换和强度提示

  • 简介输入:看字数统计和防抖预览

  • 验证码输入:看数字输入和实时监听

  • 控制按钮:看软键盘、焦点和光标操作

  • 功能卡片:把每个输入能力拆开讲清楚

  • 验收页:把本周交付结果做成可复盘材料

这样做有一个很大的好处:你不是只学"某个 API 怎么写",而是在训练自己把输入框当成一个完整系统来处理。

这一周最容易踩的坑

1. 只用 inputType,不做过滤

inputType 可以影响键盘和输入场景,但它不等于业务规则。比如你设置了文本键盘,不代表用户就不能粘贴非法字符。所以真正重要的输入边界,还是要配合 InputFilter 或提交前校验。

2. 密码显隐后忘记恢复光标

这个问题很细,但用户非常容易感觉到。只要光标一乱跳,输入就会变得别扭。

3. 在 TextWatcher 里直接做重任务

每输入一个字符都会触发监听。如果你在里面直接查数据库、发网络请求、做复杂计算,很容易把输入体验拖慢。防抖就是为了解决这类问题。

4. 页面退出时不清理监听器

Demo 里可能看不出问题,但真实项目里页面越来越多、输入框越来越复杂,不清理监听器会让问题越来越难查。

这一周真正学到的是什么

第 2 周表面上学的是 EditText,但真正学的是输入体验的完整链路:

  • 输入前:用 inputTypeInputFilter 控制边界

  • 输入中:用 TextWatcher 和计数提示给用户反馈

  • 输入后:用校验、防抖和状态快照控制业务逻辑

  • 页面交互:用焦点、软键盘、光标让输入过程更顺

  • 页面销毁:移除监听和延迟任务,保证收尾干净

到这里你会发现,EditText 真的不是"能打字"那么简单。它是用户和 App 对话的入口。入口体验做得粗糙,后面页面再漂亮也很难救回来。

下一步怎么走

第 3 周会进入基础按钮组件:ButtonCheckBoxRadioButtonSwitch 等。

如果说第 2 周解决的是"用户怎么输入",那第 3 周要解决的就是"用户怎么选择、确认和触发动作"。到时候会开始接触按钮状态、选中逻辑、水波纹、重复点击拦截和状态切换优化。

在进入第 3 周之前,建议先确认这几个问题你能自己说清楚:

  • inputTypeInputFilter 分别解决什么问题?

  • 密码显隐为什么要保存并恢复光标?

  • TextWatcher 三个回调分别什么时候触发?

  • 为什么输入搜索、远程校验这类场景要做防抖?

  • 为什么 Activity 销毁时要移除 TextWatcher 和延迟任务?

如果这些问题你能讲出来,第 2 周就不是"写了几个输入框",而是真的开始理解 Android 输入体验了。

相关推荐
我命由我123451 小时前
Kotlin 开发 - lateinit 关键字
android·java·开发语言·kotlin·android studio·android-studio·android runtime
一起搞IT吧2 小时前
Android性能系列专题理论之十:systrace/perfetto相关指标知识点细节含义总结
android·嵌入式硬件·智能手机·性能优化
小书房6 小时前
Kotlin的by
android·开发语言·kotlin·委托·by
jinanwuhuaguo7 小时前
(第二十八篇)OpenClaw成本与感知的奇点——从“Token封建制”到“全民养虾”的本体论地基
android·人工智能·kotlin·拓扑学·openclaw
xxjj998a7 小时前
Laravel4.x核心特性全解析
android·mysql·laravel
JoshRen8 小时前
2026教程:在Android Termux中集成Gemini 3镜像站实现移动端文档自动处理与摘要生成(附国内免费方案)
android
诸神黄昏EX9 小时前
Android Google KEY
android
一起搞IT吧9 小时前
Android性能系列专题理论之十一:block IO问题分析思路
android·嵌入式硬件·智能手机·性能优化
小妖66610 小时前
怎么用 tauri 创建编译 android 应用程序
android·tauri