一旺遮、寫(xiě)在前面
開(kāi)始之前,老規(guī)矩盈咳,絮絮叨叨耿眉。
本文講解的是如何自定義一個(gè)填空題控件,實(shí)現(xiàn)的方式其實(shí)有很多鱼响,最重要的是了解其中實(shí)現(xiàn)的思路和想法鸣剪,正所謂條條大路通羅馬嘛。
在Android系統(tǒng)中,我們最常使用的用于展示文字和編輯文字的控件筐骇,就是TextView和EditView债鸡,這兩個(gè)控件基本上已經(jīng)能夠滿(mǎn)足我們?nèi)粘4蟛糠珠_(kāi)發(fā)需求。
但是铛纬,凡事都有個(gè)但是娘锁。程序猿基本都會(huì)遇到一些比較特殊的需求,而作為一個(gè)Android開(kāi)發(fā)者饺鹃,最常見(jiàn)的特殊需求莫秆,就是一個(gè)特殊的控件,而這個(gè)控件剛好是系統(tǒng)沒(méi)有提供的悔详。
下面就是一個(gè)比較特別的控件镊屎,一個(gè)可填空的控件。要求可以和普通TextView一樣展示普通的文字茄螃,同時(shí)又包含可以編輯的部分缝驳,類(lèi)似EditText。如下:
看到這個(gè)归苍,第一反應(yīng)就是用狱,這不合理啊,又是展示拼弃,又是可編輯夏伊,又是換行,沒(méi)辦法實(shí)現(xiàn)拔茄酢溺忧!
結(jié)果,被人家甩了一句:那啥盯孙,學(xué)習(xí)強(qiáng)國(guó)App里面不就有可以填空答題的嘛鲁森!
我去,這下尷尬了振惰。如果實(shí)現(xiàn)不了歌溉,豈不是顯得自己很Low B!不行骑晶,無(wú)論如何都得做出來(lái)M炊狻(才能咽得下這口氣!)
二、尋尋覓覓透罢,不得所需
哼榜晦,系統(tǒng)沒(méi)有的控件冠蒋,我找個(gè)第三方的輪子還不行嗎羽圃?我就不信,世界這么大,還有別人沒(méi)做好的輪子朽寞!于是開(kāi)啟了“常規(guī)操作模式”(Google/GitHub/百度识窿,搜索,復(fù)制脑融,粘貼)喻频。果不其然,有的是輪子(ヾ(′A`)ノ?)肘迎。
比如這兩個(gè):
Android 使用代碼實(shí)現(xiàn)一個(gè)填空題
Android 基于TextView實(shí)現(xiàn)填空題
他們有一些共同的特點(diǎn):
1.基于TextView做文字展示
2.基于SpannableString做文字樣式變化甥温,文字點(diǎn)擊等
3.必須要有一個(gè)EditText作為輸入
毫無(wú)疑問(wèn),這是系統(tǒng)提供的妓布,最簡(jiǎn)單方便的定制一個(gè)TextView和EditText結(jié)合的方法姻蚓。但是,他們都存在一些問(wèn)題匣沼,比如
1.非嵌入式的輸入狰挡,需要在外部提供一個(gè)可輸入的EditText
2.雖然是嵌入式的輸入,但是可編輯文字必須要固定長(zhǎng)度释涛,不能根據(jù)文字長(zhǎng)短動(dòng)態(tài)變化
總而言之加叁,就是體驗(yàn)還是不夠好!無(wú)奈之下唇撬,萌生了自己造一個(gè)輪子的想法它匕。
那么,我們就仿造學(xué)習(xí)強(qiáng)國(guó)窖认,定制一個(gè)填空題控件唄超凳。
三、拆輪子
既然決定自己造輪子耀态,必然要先分析一下這個(gè)輪子轮傍,把這個(gè)輪子拆開(kāi),看看它包含些什么東西首装。
1.首先创夜,最簡(jiǎn)單的功能:顯示文字
2.其次,實(shí)現(xiàn)文字點(diǎn)擊仙逻,并彈出輸入法
3.再次驰吓,接收輸入法輸入
4.最后,光標(biāo)與文字的輸入和刪除
1. 如何顯示文字系奉?
在定義View中檬贰, 顯示文字是一件非常簡(jiǎn)單的函數(shù)調(diào)用,無(wú)非就是
canvas.drawText(text, x, y, paint)
但是缺亮,如果你想當(dāng)然的認(rèn)為這個(gè)是一個(gè)簡(jiǎn)單的事情翁涤,那你就大錯(cuò)特錯(cuò)了。
1)文字基線
首先,對(duì)于y坐標(biāo)葵礼,指的是文字的基線(baseLine)号阿,而非文字的top坐標(biāo),這個(gè)坐標(biāo)可以近似認(rèn)為是文字的bottom坐標(biāo)鸳粉,但并沒(méi)有那么簡(jiǎn)單扔涧。如下圖:
關(guān)于文字的繪制届谈,這篇下面這篇文章講得很透徹枯夜,建議不熟悉的同學(xué)可以看看
2)文字換行
不可避免的問(wèn)題,文字過(guò)長(zhǎng)的時(shí)候艰山,我們需要對(duì)它進(jìn)行換行顯示卤档,那么我們?cè)趺礃硬拍苤朗裁磿r(shí)候需要換行呢?
這里就涉及到一個(gè)文字寬度計(jì)算問(wèn)題
在Android中如何計(jì)算文字的寬度呢程剥?如下:
private fun measureTextLength(text: String): Float {
return mNormalPaint.measureText(text)
}
非常簡(jiǎn)單對(duì)不對(duì)劝枣,measureText這個(gè)方法,會(huì)根據(jù)我們?cè)O(shè)定的文字畫(huà)筆中的字體大小织鲸,去測(cè)量一段文字的寬度舔腾,單位是px。
需要注意的是搂擦,漢字和數(shù)字英文的寬度占位是不一樣的稳诚。 因此在換行的時(shí)候,需要特別關(guān)注和處理這兩者的關(guān)系瀑踢。
3)區(qū)分普通文字和可編輯文字
既然包含特殊的文字部分扳还,那么我們需要將其標(biāo)記出來(lái),以便做特殊的處理橱夭。這里氨距,我使用了一個(gè)標(biāo)簽<fill>來(lái)編輯,舉個(gè)例子:
原文:
大家好棘劣,我是<fill>俏让,我來(lái)自<fill>。
翻譯過(guò)來(lái)就是:
大家好茬暇,我是【 】首昔,我來(lái)自【 】。
這樣糙俗,經(jīng)過(guò) String.split("<fill>") 后勒奇,就可以把這段文字拆分為多個(gè)分段。
2.可編輯字段點(diǎn)擊
我們知道巧骚,每個(gè)View都可以接收onTouch事件赊颠,并且可以監(jiān)聽(tīng)到觸摸點(diǎn)的x/y坐標(biāo)格二。
而在繪制文字的過(guò)程中,我們可以將可編輯文字段的坐標(biāo)信息記錄下來(lái)巨税,那么在點(diǎn)擊的時(shí)候,就可以判斷有沒(méi)有觸摸碰撞粉臊,如果有草添,那么就可以彈出輸入法。
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
if (touchCollision(event)) {//觸摸碰撞檢測(cè)
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)
}
3.接收輸入法輸入
通常扼仲,需要一個(gè)可輸入文字的控件時(shí)远寸,我們很少自己去定義一個(gè)控件,而是直接使用EditText屠凶,以至于我們幾乎認(rèn)為只有EditText可以接收輸入法輸入驰后。
但是,其實(shí)Android每個(gè)繼承View的控件都是可以接收輸入的矗愧。
那么灶芝,如何打開(kāi)這個(gè)功能呢?答案就是以下兩個(gè)方法:
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)
}
其中唉韭,第一個(gè)方法返回true表示夜涕,這是一個(gè)可編輯控件,可以接收輸入法輸入属愤。
第二個(gè)方法女器,則返回一個(gè)InputConnection,用于接收輸入住诸〖莸ǎ看起來(lái)是這樣的:
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,輸入法輸入時(shí)贱呐,會(huì)通過(guò)這個(gè)方法將文字傳輸給控件
4.光標(biāo)
1)繪制
普通的EditText在輸入時(shí)丧诺,都會(huì)有一個(gè)光標(biāo),用于表示輸入或刪除的位置奄薇。繪制光標(biāo)锅必,只需要一句代碼:
canvas.drawLine(startX, startY, stopX, stopY, paint)
沒(méi)錯(cuò),就是繪制一條線惕艳,通過(guò)修改paint的alpha值(0/255)搞隐,控制線條的顯示和隱藏即可。
關(guān)鍵在于远搪,如何確定光標(biāo)的位置劣纲。
2)計(jì)算純漢字輸入時(shí)的光標(biāo)位置
還記得上面2點(diǎn),實(shí)現(xiàn)可編輯字段的點(diǎn)擊嗎谁鳍?當(dāng)我們檢測(cè)到觸摸碰撞的時(shí)候癞季,我們就可以根據(jù)這個(gè)時(shí)候觸摸點(diǎn)的x坐標(biāo)劫瞳,以及文字的長(zhǎng)度去判斷光標(biāo)的位置。具體如何實(shí)現(xiàn)呢绷柒?我們從最簡(jiǎn)單的情況來(lái)實(shí)現(xiàn)志于。
假設(shè),輸入的文字都是漢字(前面我們就說(shuō)過(guò)废睦,漢字和數(shù)字英文占位是不一樣的)伺绽。
那么,這時(shí)嗜湃,
光標(biāo)所在漢字的索引 = (觸摸點(diǎn)x坐標(biāo) - 被觸摸的編輯字段起始位置的x坐標(biāo))/ 單個(gè)漢字寬度
那么奈应,光標(biāo)所在實(shí)際位置的x坐標(biāo)就是
光標(biāo)x軸坐標(biāo) = (0 至 光標(biāo)所在漢字的索引)這段文字的長(zhǎng)度
轉(zhuǎn)化為代碼即:
mNormalPaint.measureText(text.substring(0, index))
如下圖:
說(shuō)明:這里的index,指的是文字在可編輯字段中的位置购披,也就是光標(biāo)的位置
光標(biāo)起始位置的y坐標(biāo)杖挣,就是被觸摸的可編輯字段的y坐標(biāo)。
光標(biāo)結(jié)束位置的x坐標(biāo)和起始位置相同刚陡,y坐標(biāo)則為其實(shí)坐標(biāo)加上文字高度
3)考慮多類(lèi)型輸入時(shí)的光標(biāo)位置
當(dāng)輸入的文字包含漢字惩妇、英文、數(shù)字時(shí)筐乳,由于英文/數(shù)字的占位比漢字小屿附,此時(shí),如果按照漢字的單字來(lái)計(jì)算光標(biāo)所在文字的索引哥童,那么此時(shí)的索引比實(shí)際的索引小挺份。
這里就需要一個(gè)方法來(lái)確認(rèn):觸摸點(diǎn)x坐標(biāo)到可編輯字段起始位置x坐標(biāo)的這段長(zhǎng)度,可以存放多少個(gè)文字贮懈。
我采用的方法如下:
我們知道匀泊,這段長(zhǎng)度,可以放置的最少文字個(gè)數(shù)朵你,就是漢字的個(gè)數(shù)各聘。
第一步,我們先取最少的漢字個(gè)數(shù)抡医,并計(jì)算文字長(zhǎng)度躲因,如果這時(shí),文字的長(zhǎng)度沒(méi)有超過(guò)實(shí)際觸摸位置忌傻。
第二步大脉,取下一個(gè)文字,并計(jì)算文字總長(zhǎng)度水孩,判斷長(zhǎng)度有沒(méi)有超過(guò)實(shí)際觸摸位置镰矿。
重復(fù)第二步,直到超過(guò)實(shí)際觸摸位置俘种。
這時(shí),這是實(shí)際的文字索引就是:(取到的最后一個(gè)文字的索引 - 1)
至此,我們就得到出實(shí)際的光標(biāo)位置睬罗,以及文字索引了。
在此基礎(chǔ)上牢酵,根據(jù)光標(biāo)的位置和文字索引,就可以對(duì)文字進(jìn)行輸入和刪除了衙猪。
具體計(jì)算如下圖所示:
四馍乙、組裝輪子
經(jīng)過(guò)上面的分解,基本上屈嗤,我們就已經(jīng)知道實(shí)現(xiàn)輪子的各個(gè)步驟潘拨,剩下的就是將上面的各個(gè)步驟拼接起來(lái)就行了吊输。
當(dāng)然饶号,具體的代碼我就不貼了。大家可以自己去看一下源碼季蚂,過(guò)程并不復(fù)雜茫船。
自定義控件嘛,每個(gè)人去實(shí)現(xiàn)的時(shí)候扭屁,都會(huì)有不一樣的做法算谈,比如上面計(jì)算光標(biāo)實(shí)際位置的方法,肯定會(huì)有不同的更好的方法料滥。所以然眼,了解實(shí)現(xiàn)的思想和可借助工具方法即可,沒(méi)必要太過(guò)較真葵腹。
最后還一些邊邊角角的小功能高每,比如自定義一些可配置屬性:文字顏色,字體大小践宴,可編輯字段格式鲸匿,光標(biāo)顏色等等;比如根據(jù)文字高度,自適應(yīng)控件高度;比如輸入法的彈出和隱藏......
不再細(xì)提阻肩,具體可看源碼带欢。
五、總結(jié)
1.一個(gè)復(fù)雜的控件往往都可以通過(guò)拆解烤惊,拆分為一個(gè)個(gè)簡(jiǎn)單的功能乔煞。
2.從最簡(jiǎn)單的功能開(kāi)始實(shí)現(xiàn),你會(huì)更有信心柒室。
3.不要放棄瘤缩,一定有實(shí)現(xiàn)的方法。如果沒(méi)有伦泥,說(shuō)明你還不夠了解一些基礎(chǔ)屬性剥啤,Google之锦溪。
好了,以上就是給大家介紹的一種定制“填空控件”的思路府怯,當(dāng)然還有其他的實(shí)現(xiàn)方式刻诊。僅供大家參考。
源碼傳送門(mén)牺丙,喜歡的話则涯,不吝給個(gè)star吧??~