開始以前,老規矩,絮絮不休。git
本文講解的是如何自定義一個填空題控件,實現的方式其實有不少,最重要的是瞭解其中實現的思路和想法,正所謂條條大路通羅馬嘛。github
在Android系統中,咱們最常使用的用於展現文字和編輯文字的控件,就是TextView和EditView,這兩個控件基本上已經可以知足咱們平常大部分開發需求。canvas
可是,凡事都有個可是。程序猿基本都會遇到一些比較特殊的需求,而做爲一個Android開發者,最多見的特殊需求,就是一個特殊的控件,而這個控件恰好是系統沒有提供的。ide
下面就是一個比較特別的控件,一個可填空的控件。要求能夠和普通TextView同樣展現普通的文字,同時又包含能夠編輯的部分,相似EditText。以下:函數
看到這個,第一反應就是,這不合理啊,又是展現,又是可編輯,又是換行,沒辦法實現啊!工具
結果,被人家甩了一句:那啥,學習強國App裏面不就有能夠填空答題的嘛!學習
我去,這下尷尬了。若是實現不了,豈不是顯得本身很Low B!不行,不管如何都得作出來!(才能咽得下這口氣!)字體
哼,系統沒有的控件,我找個第三方的輪子還不行嗎?我就不信,世界這麼大,還有別人沒作好的輪子!因而開啓了「常規操做模式」(Google/GitHub/百度,搜索,複製,粘貼)。果不其然,有的是輪子(ヾ(´A`)ノ゚)。ui
好比這兩個:this
他們有一些共同的特色:
1.基於TextView作文字展現
2.基於SpannableString作文字樣式變化,文字點擊等
3.必需要有一個EditText做爲輸入
複製代碼
毫無疑問,這是系統提供的,最簡單方便的定製一個TextView和EditText結合的方法。可是,他們都存在一些問題,好比
1.非嵌入式的輸入,須要在外部提供一個可輸入的EditText
2.雖然是嵌入式的輸入,可是可編輯文字必需要固定長度,不能根據文字長短動態變化
複製代碼
總而言之,就是體驗仍是不夠好!無奈之下,萌生了本身造一個輪子的想法。
那麼,咱們就仿造學習強國,定製一個填空題控件唄。
既然決定本身造輪子,必然要先分析一下這個輪子,把這個輪子拆開,看看它包含些什麼東西。
1.首先,最簡單的功能:顯示文字
2.其次,實現文字點擊,並彈出輸入法
3.再次,接收輸入法輸入
4.最後,光標與文字的輸入和刪除
複製代碼
在定義View中, 顯示文字是一件很是簡單的函數調用,無非就是
canvas.drawText(text, x, y, paint)
複製代碼
可是,若是你想固然的認爲這個是一個簡單的事情,那你就大錯特錯了。
首先,對於y座標,指的是文字的基線(baseLine),而非文字的top座標,這個座標能夠近似認爲是文字的bottom座標,但並無那麼簡單。以下圖:
關於文字的繪製,這篇下面這篇文章講得很透徹,建議不熟悉的同窗能夠看看
不可避免的問題,文字過長的時候,咱們須要對它進行換行顯示,那麼咱們怎麼樣才能知道何時須要換行呢?
這裏就涉及到一個文字寬度計算問題
在Android中如何計算文字的寬度呢?以下:
private fun measureTextLength(text: String): Float {
return mNormalPaint.measureText(text)
}
複製代碼
很是簡單對不對,measureText這個方法,會根據咱們設定的文字畫筆中的字體大小,去測量一段文字的寬度,單位是px。
須要注意的是,漢字和數字英文的寬度佔位是不同的。 所以在換行的時候,須要特別關注和處理這二者的關係。
既然包含特殊的文字部分,那麼咱們須要將其標記出來,以便作特殊的處理。這裏,我使用了一個標籤來編輯,舉個例子:
原文:
你們好,我是<fill>,我來自<fill>。
翻譯過來就是:
你們好,我是【 】,我來自【 】。
複製代碼
這樣,通過 String.split("") 後,就能夠把這段文字拆分爲多個分段。
咱們知道,每一個View均可以接收onTouch事件,而且能夠監聽到觸摸點的x/y座標。
而在繪製文字的過程當中,咱們能夠將可編輯文字段的座標信息記錄下來,那麼在點擊的時候,就能夠判斷有沒有觸摸碰撞,若是有,那麼就能夠彈出輸入法。
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
if (touchCollision(event)) {//觸摸碰撞檢測
isFocusableInTouchMode = true
isFocusable = true
requestFocus()
try {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(this, InputMethodManager.RESULT_SHOWN)
imm.restartInput(this)
} catch (ignore: Exception) {
}
return true
}
}
}
return super.onTouchEvent(event)
}
複製代碼
一般,須要一個可輸入文字的控件時,咱們不多本身去定義一個控件,而是直接使用EditText,以致於咱們幾乎認爲只有EditText能夠接收輸入法輸入。
可是,其實Android每一個繼承View的控件都是能夠接收輸入的。
那麼,如何打開這個功能呢?答案就是如下兩個方法:
override fun onCheckIsTextEditor(): Boolean {
return true
}
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
outAttrs.inputType = InputType.TYPE_CLASS_TEXT
outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE
return MyInputConnection(this, false, this)
}
複製代碼
其中,第一個方法返回true表示,這是一個可編輯控件,能夠接收輸入法輸入。
第二個方法,則返回一個InputConnection,用於接收輸入。看起來是這樣的:
class MyInputConnection(targetView: View, fullEditor: Boolean, private val mListener: InputListener) : BaseInputConnection(targetView, fullEditor) {
override fun commitText(text: CharSequence, newCursorPosition: Int): Boolean {
mListener.onTextInput(text)
return super.commitText(text, newCursorPosition)
}
override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
return if (beforeLength == 1 && afterLength == 0) {
super.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KEYCODE_DEL)) &&
super.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL))
} else super.deleteSurroundingText(beforeLength, afterLength)
}
}
interface InputListener {
fun onTextInput(text: CharSequence)
}
複製代碼
最主要的方法是commitText,輸入法輸入時,會經過這個方法將文字傳輸給控件
普通的EditText在輸入時,都會有一個光標,用於表示輸入或刪除的位置。繪製光標,只須要一句代碼:
canvas.drawLine(startX, startY, stopX, stopY, paint)
複製代碼
沒錯,就是繪製一條線,經過修改paint的alpha值(0/255),控制線條的顯示和隱藏便可。
關鍵在於,如何肯定光標的位置。
還記得上面2點,實現可編輯字段的點擊嗎?當咱們檢測到觸摸碰撞的時候,咱們就能夠根據這個時候觸摸點的x座標,以及文字的長度去判斷光標的位置。具體如何實現呢?咱們從最簡單的狀況來實現。
假設,輸入的文字都是漢字(前面咱們就說過,漢字和數字英文佔位是不同的)。
那麼,這時,
光標所在漢字的索引 = (觸摸點x座標 - 被觸摸的編輯字段起始位置的x座標)/ 單個漢字寬度
複製代碼
那麼,光標所在實際位置的x座標就是
光標x軸座標 = (0 至 光標所在漢字的索引)這段文字的長度
複製代碼
轉化爲代碼即:
mNormalPaint.measureText(text.substring(0, index))
複製代碼
以下圖:
說明:這裏的index,指的是文字在可編輯字段中的位置,也就是光標的位置
光標起始位置的y座標,就是被觸摸的可編輯字段的y座標。
光標結束位置的x座標和起始位置相同,y座標則爲其實座標加上文字高度
當輸入的文字包含漢字、英文、數字時,因爲英文/數字的佔位比漢字小,此時,若是按照漢字的單字來計算光標所在文字的索引,那麼此時的索引比實際的索引小。
這裏就須要一個方法來確認:觸摸點x座標到可編輯字段起始位置x座標的這段長度,能夠存放多少個文字。
我採用的方法以下:
咱們知道,這段長度,能夠放置的最少文字個數,就是漢字的個數。
第一步,咱們先取最少的漢字個數,並計算文字長度,若是這時,文字的長度沒有超過實際觸摸位置。
第二步,取下一個文字,並計算文字總長度,判斷長度有沒有超過實際觸摸位置。
重複第二步,直到超過實際觸摸位置。
這時,這是實際的文字索引就是:(取到的最後一個文字的索引 - 1)
至此,咱們就獲得出實際的光標位置,以及文字索引了。
在此基礎上,根據光標的位置和文字索引,就能夠對文字進行輸入和刪除了。
具體計算以下圖所示:
通過上面的分解,基本上,咱們就已經知道實現輪子的各個步驟,剩下的就是將上面的各個步驟拼接起來就好了。
固然,具體的代碼我就不貼了。你們能夠本身去看一下源碼,過程並不複雜。
自定義控件嘛,每一個人去實現的時候,都會有不同的作法,好比上面計算光標實際位置的方法,確定會有不一樣的更好的方法。因此,瞭解實現的思想和可藉助工具方法便可,不必太過較真。
最後還一些邊邊角角的小功能,好比自定義一些可配置屬性:文字顏色,字體大小,可編輯字段格式,光標顏色等等;好比根據文字高度,自適應控件高度;好比輸入法的彈出和隱藏......
再也不細提,具體可看源碼。
1.一個複雜的控件每每均可以經過拆解,拆分爲一個個簡單的功能。
2.從最簡單的功能開始實現,你會更有信心。
3.不要放棄,必定有實現的方法。若是沒有,說明你還不夠了解一些基礎屬性,Google之。
好了,以上就是給你們介紹的一種定製「填空控件」的思路,固然還有其餘的實現方式。僅供你們參考。
源碼傳送門,喜歡的話,不吝給個star吧😁~