一、写在前面

开始之前,老规矩,絮絮叨叨。

本文讲解的是如何自定义一个填空题控件,实现的方式其实有很多,最重要的是了解其中实现的思路和想法,正所谓条条大路通罗马嘛。

在Android系统中,我们最常使用的用于展示文字和编辑文字的控件,就是TextView和EditView,这两个控件基本上已经能够满足我们日常大部分开发需求。

但是,凡事都有个但是。程序猿基本都会遇到一些比较特殊的需求,而作为一个Android开发者,最常见的特殊需求,就是一个特殊的控件,而这个控件刚好是系统没有提供的。

下面就是一个比较特别的控件,一个可填空的控件。要求可以和普通TextView一样展示普通的文字,同时又包含可以编辑的部分,类似EditText。如下:

看到这个,第一反应就是,这不合理啊,又是展示,又是可编辑,又是换行,没办法实现啊!

结果,被人家甩了一句:那啥,(xue xi qiang guo)App里面不就有可以填空答题的嘛!

我去,这下尴尬了。如果实现不了,岂不是显得自己很Low B!不行,无论如何都得做出来!(才能咽得下这口气!)

二、寻寻觅觅,不得所需

哼,系统没有的控件,我找个第三方的轮子还不行吗?我就不信,世界这么大,还有别人没做好的轮子!于是开启了“常规操作模式”(Google/GitHub/百度,搜索,复制,粘贴)。果不其然,有的是轮子(ヾ(´A`)ノ゚)。

比如这两个:

Android 使用代码实现一个填空题

Android 基于TextView实现填空题

他们有一些共同的特点:

1.基于TextView做文字展示
2.基于SpannableString做文字样式变化,文字点击等
3.必须要有一个EditText作为输入

毫无疑问,这是系统提供的,最简单方便的定制一个TextView和EditText结合的方法。但是,他们都存在一些问题,比如

1.非嵌入式的输入,需要在外部提供一个可输入的EditText
2.虽然是嵌入式的输入,但是可编辑文字必须要固定长度,不能根据文字长短动态变化

总而言之,就是体验还是不够好!无奈之下,萌生了自己造一个轮子的想法。

那么,我们就仿造学习强国,定制一个填空题控件呗。

三、拆轮子

既然决定自己造轮子,必然要先分析一下这个轮子,把这个轮子拆开,看看它包含些什么东西。

1.首先,最简单的功能:显示文字
2.其次,实现文字点击,并弹出输入法
3.再次,接收输入法输入
4.最后,光标与文字的输入和删除
1. 如何显示文字?

在定义View中, 显示文字是一件非常简单的函数调用,无非就是

canvas.drawText(text, x, y, paint)

但是,如果你想当然的认为这个是一个简单的事情,那你就大错特错了。

1)文字基线

首先,对于y坐标,指的是文字的基线(baseLine),而非文字的top坐标,这个坐标可以近似认为是文字的bottom坐标,但并没有那么简单。如下图:

关于文字的绘制,这篇下面这篇文章讲得很透彻,建议不熟悉的同学可以看看

自定义控件之绘图篇( 五):drawText()详解

2)文字换行

不可避免的问题,文字过长的时候,我们需要对它进行换行显示,那么我们怎么样才能知道什么时候需要换行呢?

这里就涉及到一个文字宽度计算问题

在Android中如何计算文字的宽度呢?如下:

private fun measureTextLength(text: String): Float {return mNormalPaint.measureText(text)
}

非常简单对不对,measureText这个方法,会根据我们设定的文字画笔中的字体大小,去测量一段文字的宽度,单位是px。

需要注意的是,汉字和数字英文的宽度占位是不一样的。 因此在换行的时候,需要特别关注和处理这两者的关系。

3)区分普通文字和可编辑文字

既然包含特殊的文字部分,那么我们需要将其标记出来,以便做特殊的处理。这里,我使用了一个标签来编辑,举个例子:

原文:大家好,我是<fill>,我来自<fill>。翻译过来就是:大家好,我是【   】,我来自【   】。

这样,经过 String.split("") 后,就可以把这段文字拆分为多个分段。

2.可编辑字段点击

我们知道,每个View都可以接收onTouch事件,并且可以监听到触摸点的x/y坐标。

而在绘制文字的过程中,我们可以将可编辑文字段的坐标信息记录下来,那么在点击的时候,就可以判断有没有触摸碰撞,如果有,那么就可以弹出输入法。

override fun onTouchEvent(event: MotionEvent): Boolean {when (event.action) {MotionEvent.ACTION_DOWN -> {if (touchCollision(event)) {//触摸碰撞检测isFocusableInTouchMode = trueisFocusable = truerequestFocus()try {val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManagerimm.showSoftInput(this, InputMethodManager.RESULT_SHOWN)imm.restartInput(this)} catch (ignore: Exception) {}return true}}}return super.onTouchEvent(event)
}
3.接收输入法输入

通常,需要一个可输入文字的控件时,我们很少自己去定义一个控件,而是直接使用EditText,以至于我们几乎认为只有EditText可以接收输入法输入。

但是,其实Android每个继承View的控件都是可以接收输入的

那么,如何打开这个功能呢?答案就是以下两个方法:

override fun onCheckIsTextEditor(): Boolean {return true
}override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {outAttrs.inputType = InputType.TYPE_CLASS_TEXToutAttrs.imeOptions = EditorInfo.IME_ACTION_DONEreturn 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,输入法输入时,会通过这个方法将文字传输给控件

4.光标
1)绘制

普通的EditText在输入时,都会有一个光标,用于表示输入或删除的位置。绘制光标,只需要一句代码:

canvas.drawLine(startX, startY, stopX, stopY, paint)

没错,就是绘制一条线,通过修改paint的alpha值(0/255),控制线条的显示和隐藏即可。

关键在于,如何确定光标的位置。

2)计算纯汉字输入时的光标位置

还记得上面2点,实现可编辑字段的点击吗?当我们检测到触摸碰撞的时候,我们就可以根据这个时候触摸点的x坐标,以及文字的长度去判断光标的位置。具体如何实现呢?我们从最简单的情况来实现。

假设,输入的文字都是汉字(前面我们就说过,汉字和数字英文占位是不一样的)。

那么,这时,

光标所在汉字的索引 = (触摸点x坐标 - 被触摸的编辑字段起始位置的x坐标)/ 单个汉字宽度

那么,光标所在实际位置的x坐标就是

光标x轴坐标 = (0 至 光标所在汉字的索引)这段文字的长度

转化为代码即:

mNormalPaint.measureText(text.substring(0, index))

如下图:

说明:这里的index,指的是文字在可编辑字段中的位置,也就是光标的位置

光标起始位置的y坐标,就是被触摸的可编辑字段的y坐标。

光标结束位置的x坐标和起始位置相同,y坐标则为其实坐标加上文字高度

3)考虑多类型输入时的光标位置

当输入的文字包含汉字、英文、数字时,由于英文/数字的占位比汉字小,此时,如果按照汉字的单字来计算光标所在文字的索引,那么此时的索引比实际的索引小

这里就需要一个方法来确认:触摸点x坐标到可编辑字段起始位置x坐标的这段长度,可以存放多少个文字。

我采用的方法如下:

我们知道,这段长度,可以放置的最少文字个数,就是汉字的个数。

第一步,我们先取最少的汉字个数,并计算文字长度,如果这时,文字的长度没有超过实际触摸位置。

第二步,取下一个文字,并计算文字总长度,判断长度有没有超过实际触摸位置。

重复第二步,直到超过实际触摸位置。

这时,这是实际的文字索引就是:(取到的最后一个文字的索引 - 1)

至此,我们就得到出实际的光标位置,以及文字索引了。

在此基础上,根据光标的位置和文字索引,就可以对文字进行输入和删除了。

具体计算如下图所示:

四、组装轮子

经过上面的分解,基本上,我们就已经知道实现轮子的各个步骤,剩下的就是将上面的各个步骤拼接起来就行了。

当然,具体的代码我就不贴了。大家可以自己去看一下源码,过程并不复杂。

自定义控件嘛,每个人去实现的时候,都会有不一样的做法,比如上面计算光标实际位置的方法,肯定会有不同的更好的方法。所以,了解实现的思想和可借助工具方法即可,没必要太过较真。

最后还一些边边角角的小功能,比如自定义一些可配置属性:文字颜色,字体大小,可编辑字段格式,光标颜色等等;比如根据文字高度,自适应控件高度;比如输入法的弹出和隐藏…

不再细提,具体可看源码。

五、总结

1.一个复杂的控件往往都可以通过拆解,拆分为一个个简单的功能。

2.从最简单的功能开始实现,你会更有信心。

3.不要放弃,一定有实现的方法。如果没有,说明你还不够了解一些基础属性,Google之。

好了,以上就是给大家介绍的一种定制“填空控件”的思路,当然还有其他的实现方式。仅供大家参考。

源码传送门,喜欢的话,不吝给个star吧?~

一步步教你如何定制一个Android「填空题」控件(仿学习强国填空题控件)相关推荐

  1. Android仿学习强国填空题考试界面

    很久不写博客了,应为一直真的都很忙,没时间写,正好今天有时间写一下O(∩_∩)O哈哈~. 起因:最近工作中遇到一个需求,使用手机进行填空题考试. 分析:因为涉及到判分,需要答案与文字一一对应,刚开始在 ...

  2. 定制一个Android的Launcher(Home)

    如果你要定制一个Android系统,你想用你自己的Launcher(Home)作主界面来替换Android自己的Home,而且不希望用户安装的Launcher来替换掉你的Launcher. 我们可以通 ...

  3. 5分钟搭建一个粗粒度「视频去重」系统

    Jupyter Notebook 教程: How to Build a Video Deduplication System 「视频去重」可以在海量的视频数据中实现侵权片段或者删除掉重复冗余的内容 . ...

  4. 我搭建了一个随机「毒鸡汤」语录网站附源码下载

    小伙伴们注意:公众号的推送机制不再按照时间前后推送了,微信公众号信息流乱序.君哥建议大家把科技毒瘤君公众号置顶(设为星标⭐),以便第一时间看到推送,非常感谢~,方法如下图: 1 演示效果 ★ 遇到喜欢 ...

  5. 一步步教你为网站开发Android客户端---HttpWatch抓包,HttpClient模拟POST请求,Jsoup解析HTML代码,动态更新ListView...

    本文面向Android初级开发者,有一定的Java和Android知识即可. 文章覆盖知识点:HttpWatch抓包,HttpClient模拟POST请求,Jsoup解析HTML代码,动态更新List ...

  6. 一步步教你为网站开发Android客户端

    本文面向Android初级开发者,有一定的Java和Android知识即可. 文章覆盖知识点:HttpWatch抓包,HttpClient模拟POST请求,Jsoup解析HTML代码,动态更新List ...

  7. iphone 快捷指令 python_手把手教你使用 iOS 效率神器 「快捷指令」

    在文章的前半部分,我将给你介绍几个有趣的快捷指令,并且附上共享链接,您可自行安装.在文章的最后,则是一篇教程,用于制作一个比较复杂的快捷指令. 注意:如果需要安装我或者其他朋友共享给你的快捷指令,需要 ...

  8. 一个互联网「打工人」的卑微一天

    大家好,我是「打工人」安酱.午安!打工人! 今天是一年一度的程序员日,但是老天也给我们开了一个玩笑,2020-1024=996.冥冥之中在暗示着什么,但是好像也没啥办法.另外,不知道最近大家有没有被「 ...

  9. 我是一个下「笨功夫」的人

    大家好,我是涩郎,一名「知识挖掘师」兼「知识布道师」. 今天我想跟大家聊一个主题:真正的聪明人才会下「笨功夫」. 不知道大家认同这个观点吗? 我为什么这么讲呢?因为我就是一个喜欢下「笨功夫」的人.我这 ...

最新文章

  1. 在阿里云ECS服务器上面开启tomcat服务并且正常后,无法访问怎么办?原来是没有开通外网访问的端口
  2. Python 索引for循环
  3. UA OPTI501 电磁波 经典电动力学中的Fourier方法基础
  4. 沈南鹏:从五大物理定律看新商业法则
  5. Bezier曲线扫盲
  6. 51单片机c语言程序控制,51单片机C语言编程基础及实例.pdf
  7. python网络通信效率_Python之网络通信
  8. 《Metasploit渗透测试手册》—第3章3.5节在Windows 2003 Server上进行渗透测试
  9. iOS崩溃日志ips文件解析
  10. Mybatis框架中Oracle使用BLOB字段存储图片并展示(详细步骤)
  11. 兼职招募!贪心科技招聘AI课程讲师,薪资超行业标准
  12. NAR:vRhyme - 对宏基因组中的病毒基因组进行分选的生信工具
  13. Worldpress常规利用思路
  14. LeetCode 6036. 构造字符串的总得分和
  15. Word 使用宏根据文件名实现文件版本号自动更新_rev00
  16. 快速了解 Java 线上问题快速诊断神器 Arthas
  17. checkbox jq 监听_「checkbox 选中事件」jquery checkbox 选中、改变状态、change 和 click 事件 - seo实验室...
  18. react 二维码生成并下载qrcode.react
  19. 消防宣传科普|消防安全知识网上答题挑战赛活动方案
  20. R语言学习笔记(十):重抽样与自助法

热门文章

  1. 阿里云5m带宽能支持多少人访问_1M带宽的服务器能同时承载多少访客
  2. 低配电脑适合学计算机网络吗,让低配电脑保持流畅而不出现卡顿的做法,你学会了吗?...
  3. php经纬度换算距离,PHP根据经纬度坐标计算距离
  4. centos卸载harbor_在Linux主机使用命令行批量删除harbor镜像
  5. macbook忘记root用户密码,如何重置密码
  6. ajax的contentType属性理解
  7. Android studio 继承view 画笔画圆、画方形、画三角形、画扇形、画椭圆
  8. 中策大数据:在建工程是指什么?在建工程项目包含哪些类型?
  9. 华为机试真题分类汇总
  10. 【机器学习笔记2.2】用逻辑回归预测马疝病的死亡率