一步步教你如何定制一個(gè)Android「填空題」控件(仿學(xué)習(xí)強(qiáng)國(guó)填空題控件)

一旺遮、寫(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)單扔涧。如下圖:

文字基線(來(lái)源:自定義控件之繪圖篇( 五):drawText()詳解,侵刪)

關(guān)于文字的繪制届谈,這篇下面這篇文章講得很透徹枯夜,建議不熟悉的同學(xué)可以看看

自定義控件之繪圖篇( 五):drawText()詳解

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))

如下圖:

純漢字光標(biāo)計(jì)算.png

說(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ì)算如下圖所示:

多類(lèi)型文字光標(biāo)計(jì)算.png

四馍乙、組裝輪子

經(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吧??~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市冲簿,隨后出現(xiàn)的幾起案子粟判,更是在濱河造成了極大的恐慌,老刑警劉巖峦剔,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件档礁,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡吝沫,警方通過(guò)查閱死者的電腦和手機(jī)呻澜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)惨险,“玉大人羹幸,你說(shuō)我怎么就攤上這事”栌洌” “怎么了栅受?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)恭朗。 經(jīng)常有香客問(wèn)我屏镊,道長(zhǎng),這世上最難降的妖魔是什么冀墨? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任闸衫,我火速辦了婚禮,結(jié)果婚禮上诽嘉,老公的妹妹穿的比我還像新娘蔚出。我一直安慰自己,他們只是感情好虫腋,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布骄酗。 她就那樣靜靜地躺著,像睡著了一般悦冀。 火紅的嫁衣襯著肌膚如雪趋翻。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,046評(píng)論 1 285
  • 那天盒蟆,我揣著相機(jī)與錄音踏烙,去河邊找鬼师骗。 笑死,一個(gè)胖子當(dāng)著我的面吹牛讨惩,可吹牛的內(nèi)容都是我干的辟癌。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼荐捻,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼黍少!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起处面,我...
    開(kāi)封第一講書(shū)人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤厂置,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后魂角,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體昵济,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年或颊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了砸紊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片传于。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡囱挑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出沼溜,到底是詐尸還是另有隱情平挑,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布系草,位于F島的核電站通熄,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏找都。R本人自食惡果不足惜唇辨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望能耻。 院中可真熱鬧赏枚,春花似錦、人聲如沸晓猛。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)戒职。三九已至栗恩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間洪燥,已是汗流浹背磕秤。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工乳乌, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人市咆。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓钦扭,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親床绪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子客情,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345