一、写在前面
本文讲解的是如何自定义一个填空题控件,实现的方式其实有很多,最重要的是了解其中实现的思路和想法,正所谓条条大路通罗马嘛。
在Android系统中,我们最常使用的用于展示文字和编辑文字的控件,就是TextView和EditView,这两个控件基本上已经能够满足我们日常大部分开发需求。
但是,凡事都有个但是。程序猿基本都会遇到一些比较特殊的需求,而作为一个Android开发者,最常见的特殊需求,就是一个特殊的控件,而这个控件刚好是系统没有提供的。
下面就是一个比较特别的控件,一个可填空的控件。要求可以和普通TextView一样展示普通的文字,同时又包含可以编辑的部分,类似EditText。如下:
看到这个,第一反应就是,这不合理啊,又是展示,又是可编辑,又是换行,没办法实现啊!
结果,被人家甩了一句:那啥,学习强国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坐标,但并没有那么简单。如下图:
关于文字的绘制,这篇下面这篇文章讲得很透彻,建议不熟悉的同学可以看看
2)文字换行
不可避免的问题,文字过长的时候,我们需要对它进行换行显示,那么我们怎么样才能知道什么时候需要换行呢?
这里就涉及到一个文字宽度计算问题
在Android中如何计算文字的宽度呢?如下:
private fun measureTextLength(text: String): Float {
return mNormalPaint.measureText(text)
}
非常简单对不对,measureText这个方法,会根据我们设定的文字画笔中的字体大小,去测量一段文字的宽度,单位是px。
需要注意的是,汉字和数字英文的宽度占位是不一样的。 因此在换行的时候,需要特别关注和处理这两者的关系。
3)区分普通文字和可编辑文字
既然包含特殊的文字部分,那么我们需要将其标记出来,以便做特殊的处理。这里,我使用了一个标签<fill>来编辑,举个例子:
这样,经过 String.split("<fill>") 后,就可以把这段文字拆分为多个分段。
2.可编辑字段点击
我们知道,每个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)
}
3.接收输入法输入
通常,需要一个可输入文字的控件时,我们很少自己去定义一个控件,而是直接使用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,输入法输入时,会通过这个方法将文字传输给控件
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之。
好了,以上就是给大家介绍的一种定制“填空控件”的思路,当然还有其他的实现方式。仅供大家参考。
好了,文章到这里就结束了,如果你觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。
最后这里是关于我自己的Android 学习,面试文档,视频收集大整理,有兴趣的伙伴们可以看看~
如果你觉得还算有用的话,不妨把它们推荐给你的朋友。
相关推荐
这个“Android高仿IOS日历滚轮选择控件”就是一个很好的例子,它旨在复刻iOS日历中常用的日期和时间选择方式。这个控件通常用于让用户方便快捷地通过滚动选择日期、小时或分钟,提升应用的交互性和用户友好性。 ...
在本案例中,我们讨论的是一个名为"Android-可实现三级联动的选择器高仿iOS的滚轮控件字体大小自适应"的项目,它旨在提供一种高度定制的滚轮视图,同时能够自动调整字体大小以适应不同的屏幕尺寸和内容。 首先,这...
本文将详细介绍如何在Android中创建一个高仿iOS风格的滚轮选择控件,主要基于名为Android-PickerView的开源项目。 首先,让我们理解`WheelView`的概念。在iOS中,滚轮选择器是一种用户界面元素,允许用户通过滚动一...
本教程将详细介绍如何在Android中创建一个高仿iOS的日期控件。 首先,我们需要了解iOS日期控件的基本特征。iOS的日期选择器(UIDatePicker)通常包含年、月、日、小时和分钟的滚动条,用户可以通过滑动来选择所需的...
效果图 功能 自定义卡片的堆叠效果 自定义卡片移除动画 支持加载更多 使用方式 gradle dependency // 1. Add it in your root build.gradle at the end of ...// 2.... android:clipChildren="false"> ... android:
标题中的“Android应用源码之高仿iOS ActionSheet控件”揭示了这个压缩包内容的核心:它是一个Android项目,目标是实现对iOS平台上的ActionSheet控件的模仿。ActionSheet在iOS中通常用于展示一系列可选操作,用户...
VB高仿控件实例集合是一个宝贵的资源,对于学习和提升VB开发能力,尤其是UI设计部分,具有很大的帮助。 标题中提到的"VB高仿控件实例,有十几个",意味着这个压缩包内包含了一系列VB高仿控件的代码示例,涵盖不同的...
"Android 自定义控件实现ViewPagerIndicator 高仿MIUI"这个项目旨在教你如何创建一个类似MIUI风格的ViewPager指示器,用于更好地引导用户在多个页面间切换。ViewPagerIndicator是一个常见的组件,它可以帮助用户了解...
在Android应用开发中,创建一个高仿微信支付密码输入控件是常见的需求,它能提供安全、直观的用户交互体验。下面将详细讲解如何实现这样一个控件。 首先,我们需要动态生成用于输入密码的文本框。在给定的代码中,...
在Android中,自定义View通常需要扩展一个基础View类,例如`ImageView`或`TextView`,然后重写`onDraw()`方法来绘制自定义图形。这涉及到Canvas、Paint、Shape等绘图对象的使用,以及对Android绘图API的深入了解。 ...
本项目"Android自定义View之高仿QQ健康"旨在教你如何模仿流行的QQ健康应用,利用谷歌的Material Design风格来设计自定义界面,提供一个既美观又实用的样式。 首先,我们来了解一下Material Design。它是谷歌推出的...
本教程将深入探讨如何在Android平台上创建一个高仿iOS的ActionSheet控件,并分析提供的源码。 首先,ActionSheet在Android中的实现通常涉及到自定义对话框(AlertDialog)或底部弹出框(BottomSheet)。在iOS中,...
"Android高仿IOS动态高斯模糊背景"是一个技术主题,它旨在实现iOS系统中常见的动态高斯模糊效果,并将其应用于Android应用的背景上。这种效果通常用于界面过渡或者某些组件的背景,可以提供一种轻量化且美观的视觉...
"高仿IOS开关控件",也被称为"SwitchButton",就是这样一个组件,它模仿了iOS中的滑动开关样式,允许用户在两个状态之间切换,通常用于开启或关闭某个功能。 一、自定义控件的基础知识 在Android中,自定义控件主要...
通过研究这个开源项目,开发者不仅可以学到如何创建一个模仿iOS ActionSheet的控件,还能提升自己在Android自定义组件、动画效果和用户体验设计方面的技能。这对于那些希望在Android应用中提供跨平台一致体验的...
本项目将展示如何从零开始创建一个高仿iOS的ActionSheet控件。 首先,我们需要理解ActionSheet的基本结构。它通常包含一个标题、取消按钮以及一组可选操作按钮。这些元素需要根据用户选择动态显示和隐藏。在Android...
在Android应用开发中,"高仿"掘金Android App是一个典型的示例,它展示了如何将现代编程技术如Data Binding、Kotlin以及RxJava融合在一起,实现高效、优雅的代码实践。这个项目的目标是模仿掘金App的功能,同时利用...