EditText 限制輸入字元個數的三種方式
最近有個需求是限制使用者輸入的字元個數,其中中文算2個,非中文字元算1個,比如“1個人”就算5個,當用戶輸入超過字數限制的時候可以擷取並用toast提示使用者,這是個非常簡單的需求,實現也有很多方法。
首先我們實現檢測中文的方法,網上有很多方式,主要是檢測字元的unicode值範圍:
fun isChinese(c: Char): Boolean { return c.toInt() in 0x4E00..0x9FA5 }
如果是中文就算2個,否則算1個,函式實現如下:
fun getCharTextCount(c: Char) = if (Utils.isChinese(c)) 2 else 1
根據上面的規則,檢測一個字串的字數的函式實現如下:
fun calcTextLength(charSequence: CharSequence?): Int { if (charSequence.isNullOrBlank()) { return 0; } var sum = 0 for (c in charSequence) { sum += Utils.getCharTextCount(c) } return sum }
這部分程式碼在 Utils.java
中,作為專案的函式工具類。下面列舉各種實現方式並做對比。
1、使用InputFilter 限制字數
實現InputFilter過濾器, 需要覆蓋一個叫filter的方法。
public abstract CharSequence filter ( CharSequence source,//輸入的文字 int start,//輸入的文字 開始位置 int end,//輸入的文字 結束位置 Spanned dest, //當前顯示的內容 int dstart,//當前顯示的內容開始位置 int dend //當前顯示的內容結束位置 );
一開始看這個filter函式,引數比較多,意思也比較相近,可能容易搞混,但是當你注意每個引數的含義後,會很好理解,其實就是”將dest中範圍為dstart到dend的用source的start到end範圍的替換”。接下來實現這個函式:
class TextLengthFilter(private val maxLength: Int = Utils.MAX_LENGTH, val listener: TextLengthListener? = null) : InputFilter { override fun filter( source: CharSequence?, start: Int, end: Int, dest: Spanned?, dstart: Int, dend: Int ): CharSequence { if (source.isNullOrEmpty()) { return "" } val source: CharSequence = source.subSequence(start, end) var sum = Utils.calcTextLength(dest as CharSequence, dstart, dend) + Utils.calcTextLength(source) - maxLength if (sum > 0) { // 輸入字元超過了限制,擷取 val delete = Utils.getDeleteIndex(source, 0, source.length, sum) if (delete >= 0) { listener?.onTextLengthOutOfLimit() return source.subSequence(0, delete) } } // 沒有超過限制,直接返回source return source } }
我們用 Utils.calcTextLength(source: CharSequence, dstart: Int, dend: Int)
來計算字串除了[dstart,dend]外的字元數,因為通過上面的分析可知 [dstart,dend]
範圍內的字元是會被替換的,所以不需要計算總字數內。在程式碼中新增InputFilter監聽即可實現功能 :
edit_inputfilter.filters = arrayOf(TextLengthFilter(listener = MainActivity@ this))
2、使用TextWatcher 限制字數
使用TextWather監聽EditText的字元變化,我們需要實現三個抽象方法:
- beforeTextChanged(CharSequence s, int start, int count, int after)
s: 修改之前的文字。
start: 字串中即將發生修改的位置。
count: 字串中即將被修改的文字的長度。如果是新增的話則為0。
after: 被修改的文字修改之後的長度。如果是刪除的話則為0。 - onTextChanged(CharSequence s, int start, int before, int count)
s: 改變後的字串
start: 有變動的字串的序號
before: 被改變的字串長度,如果是新增則為0
count: 新增的字串長度,如果是刪除則為0。 - afterTextChanged(Editable s)
s: 修改後的文字
上面的註釋已經寫得很明白了,比如在beforeTextChanged回撥中,我們可以知道插入字元的位置 start
,還有插入的個數 after
,被替換的個數 count
,這與InputFilter中的各個引數含義相近。實現這幾個函式:
class TextLengthWatcher(private val maxLength: Int = Utils.MAX_LENGTH, val listener: TextLengthListener? = null) : TextWatcher { private var destCount: Int = 0 private var dStart: Int = 0 private var dEnd: Int = 0 override fun afterTextChanged(s: Editable) { // count是輸入後的字元長度 val count = Utils.calcTextLength(s) if (count > maxLength) { // 超過了sum個字元,需要擷取 var sum = count - maxLength // 輸入字元超過了限制,擷取 val delete = Utils.getDeleteIndex(s, dStart, dEnd, sum) if (delete >= 0) { listener?.onTextLengthOutOfLimit() s.delete(delete, dEnd) } } } override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { destCount = Utils.calcTextLength(s) // 獲取輸入字元的起始位置 dStart = start // 獲取輸入字元的個數 dEnd = start + after } override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { } }
需要注意的是,在TextWatcher中修改文字(Editable.delete、EditText.setText等)要小心不要陷入死迴圈。即:文字改變->watcher接收到通知->setText->文字改變->watcher接受到通知->…。所以我們在修改文字前加了一個結束條件 count > maxLength
。在程式碼中新增TextWatcher監聽即可實現功能 :
edit_textwatcher.addTextChangedListener(TextLengthWatcher(listener = MainActivity@ this))
3、使用InputConnection 限制字數
InputConnection 是輸入法和應用內View(通常是EditText)互動的通道,輸入法的文字輸入和刪改事件,包括key event事件都是通過InputConnection傳送給EditText。示意圖如下:

InputConnection有幾個關鍵方法,通過重寫這幾個方法,我們基本可以攔截軟鍵盤的所有輸入和點選事件:
//當輸入法輸入了字元,包括表情,字母、文字、數字和符號等內容,會回撥該方法 public boolean commitText(CharSequence text, int newCursorPosition) //當有按鍵輸入時,該方法會被回撥。比如點選退格鍵時,搜狗輸入法應該就是通過呼叫該方法, //傳送keyEvent的,但谷歌輸入法卻不會呼叫該方法,而是呼叫下面的deleteSurroundingText()方法。 public boolean sendKeyEvent(KeyEvent event); //當有文字刪除操作時(剪下,點選退格鍵),會觸發該方法 public boolean deleteSurroundingText(int beforeLength, int afterLength) //結束組合文字輸入的時候,回撥該方法 public boolean finishComposingText();
從中可以發現,我們可以利用 commitText
來攔截使用者的輸入。設定InputConnection的方法在EditText類裡面,所以我們繼承EditText自定義一個 TextLengthEditText
。完全重寫InputConnection的成本是很高的,我們可以繼承 InputConnectionWrapper
類 :
inner class TextLengthInputConnecttion( val target: InputConnection, private val maxLength: Int = Utils.MAX_LENGTH, val listener: TextLengthListener? = null ) : InputConnectionWrapper(target, false) { override fun commitText(source: CharSequence, newCursorPosition: Int): Boolean { val count = Utils.calcTextLength(source) val destCount = Utils.calcTextLength(text as CharSequence, selectionStart, selectionEnd) if (count + destCount > maxLength) { // 超過了sum個字元,需要擷取 var sum = count + destCount - maxLength // 輸入字元超過了限制,擷取 val delete = Utils.getDeleteIndex(source, 0, source.length, sum) if (delete >= 0) { listener?.onTextLengthOutOfLimit() return super.commitText(source.subSequence(0, delete), newCursorPosition) } } return super.commitText(source, newCursorPosition) } }
我們還需要通過重寫EditText的 onCreateInputConnection
方法來設定InputConnection :
override fun onCreateInputConnection(outAttrs: EditorInfo?): InputConnection { return TextLengthInputConnecttion(super.onCreateInputConnection(outAttrs), listener = TextLengthEditText@ this) }
直接把自定義的 TextLengthEditText
新增在layout xml檔案中即可實現功能。
總結
本文介紹了三種限制字元個數的方法,各個方法各有優缺點,畢竟我們也要考慮到以後的擴充套件,不能哪個方便用哪個,不然以後需求變更的話就要修改很多程式碼了。最後各方法的總結對比如下:
\ | 優點 | 缺點 |
---|---|---|
InputFilter | 可以檢測文字輸入、刪除 | 不能檢測按鍵輸入 |
TextWatcher | 可以檢測文字輸入、刪除 | 不能檢測按鍵輸入,只能在輸入變更後檢測,導致回撥方法可能被多次執行 |
InputConnection | 可以檢測文字輸入、刪除,可以攔截按鍵輸入,比InputFilter、TextWatcher先執行 | 實現時必須自定義EditText,比較麻煩 |
程式碼已經上傳 ofollow,noindex">Github 地址 ,歡迎star。
參考
- android 使用 InputConnection 監聽並攔截軟鍵盤的退格鍵
- Android TextWatcher三個回撥詳解,監聽EditText的輸入