不断学习,做更好的自己!💪
视频号CSDN简书欢迎打开微信,关注我的视频号:KevinDev点我点我 前言本文讲解的是如何自定义一个填空题控件,实现的方式其实有很多,最重要的是了解其中实现的思路和想法,正所谓条条大路通罗马嘛。
在 Android 系统中,我们最常使用的用于展示文字和编辑文字的控件,就是 TextView 和 EditView ,这两个控件基本上已经能够满足我们日常大部分开发需求。
效果图 那么,我们就仿造学习强国,定制一个填空题控件呗。
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)
}
注意:汉字和数字英文的宽度占位是不一样的。 因此在换行的时候,需要特别关注和处理这两者的关系。
3)区分普通文字和可编辑文字
原文:
大家好,我是,我来自。
翻译过来就是:
大家好,我是【 】,我来自【 】。
这样,经过 String.split(“”) 后,就可以把这段文字拆分为多个分段。
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)
}
代码
1. attr.xml
2. activity_main.xml
">>,我来自<fill>。我就是来填个空而已<fill>"/>
3. MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun clickBtn(view: View) {
var t = ""
for (text in fillText.getFillTexts()) {
t += text
t +=","
}
tv_fills.text = t.subSequence(0, t.length - 1)
}
}
4. FillTextView.kt
/**
* Created on 2022/5/17 15:52
*
* @author Gong Youqiang
*/
class FillTextView : View, MyInputConnection.InputListener, View.OnKeyListener {
//编辑字段标记
private var EDIT_TAG = ""
//编辑字段替换
private var EDIT_REPLACEMENT = "【 】"
//可编辑空白
private val BLANKS = " "
//可编辑开始符
private var mEditStartTag = "【"
//可编辑结束符
private var mEditEndTag = "】"
//文本
private var mText = StringBuffer()
//存放文字段的列表,根据分割为多个字段
private var mTextList = arrayListOf()
//正在输入的字段
private var mEditingText: AText? = null
//当前正在编辑的文本行数
private var mEditTextRow = 1
//光标[0]:x坐标,[1]:文字的基准线
private var mCursor = arrayOf(-1f, -1f)
//光标所在文字索引
private var mCursorIndex = 0
//光标闪烁标志
private var mHideCursor = true
//控件宽度
private var mWidth = 0
//文字画笔
private val mNormalPaint = Paint()
//普通文字颜色
private var mNormalColor = Color.BLACK
//文字画笔
private val mFillPaint = Paint()
//填写文字颜色
private var mFillColor = Color.BLACK
//光标画笔
private val mCursorPain = Paint()
//光标宽度1dp
private var mCursorWidth = 1f
//一个汉字的宽度
private var mOneWordWidth = 0f
//一行最大的文字数
private var mMaxSizeOneLine = 0
//字体大小
private var mTextSize = sp2px(16f).toFloat()
//当前绘制到第几行
private var mCurDrawRow = 1
//获取文字的起始位置
private var mStartIndex = 0
//获取文字的结束位置
private var mEndIndex = 0
//存放每行的文字,用于计算文字长度
private var mOneRowText = StringBuffer()
//一行字包含的字段:普通字段,可编辑字段
private var mOneRowTexts = arrayListOf()
//默认行距2dp,也是最小行距(用户设置的行距在此基础上叠加,即:2 + cst)
private var mRowSpace = dp2px(2f).toFloat()
//是否显示下划线
private var mUnderlineVisible = false
//下划线画笔
private val mUnderlinePain = Paint().apply {
strokeWidth = dp2px(1f).toFloat()
color = Color.BLACK
isAntiAlias = true
}
constructor(context: Context): super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet): super(context, attrs) {
getAttrs(attrs)
init()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int): super(context, attrs, defStyleAttr) {
getAttrs(attrs)
init()
}
private fun getAttrs(attrs: AttributeSet) {
val ta = context.obtainStyledAttributes(attrs, R.styleable.filled_text)
mTextSize = ta.getDimension(R.styleable.filled_text_fillTextSize, mTextSize)
mText = mText.append(ta.getText(R.styleable.filled_text_filledText)?: "")
mNormalColor = ta.getColor(R.styleable.filled_text_normalColor, Color.BLACK)
mFillColor = ta.getColor(R.styleable.filled_text_fillColor, Color.BLACK)
mRowSpace += ta.getDimension(R.styleable.filled_text_rowSpace, 0f)
ta.recycle()
}
private fun init() {
isFocusable = true
initCursorPaint()
initTextPaint()
initFillPaint()
splitTexts()
initHandler()
setOnKeyListener(this)
}
/**
* 初始化光标画笔
*/
private fun initCursorPaint() {
mCursorWidth = dp2px(mCursorWidth).toFloat()
mCursorPain.strokeWidth = mCursorWidth
mCursorPain.color = mFillColor
mCursorPain.isAntiAlias = true
}
/**
* 初始化文字画笔
*/
private fun initTextPaint() {
// mTextSize = sp2px(mTextSize).toFloat()
// mRowSpace = dp2px(mRowSpace).toFloat()
mNormalPaint.color = mNormalColor
mNormalPaint.textSize = mTextSize
mNormalPaint.isAntiAlias = true
mOneWordWidth = measureTextLength("测")
}
private fun initFillPaint() {
mFillPaint.color = mFillColor
mFillPaint.textSize = mTextSize
mFillPaint.isAntiAlias = true
}
private fun dp2px(dp: Float): Int {
val density = resources.displayMetrics.density
return (dp * density + 0.5).toInt()
}
private fun sp2px(sp: Float): Int {
val density = resources.displayMetrics.scaledDensity
return (sp * density + 0.5).toInt()
}
/**
* 拆分文字,普通文字和可编辑文字
*/
private fun splitTexts() {
mTextList.clear()
val texts = mText.split(EDIT_TAG)
for (i in 0 until texts.size - 1) {
var text = texts[i]
if (i > 0) {
text = mEditEndTag + text
}
text += mEditStartTag
mTextList.add(AText(text))
mTextList.add(AText(BLANKS, true))
}
mTextList.add(AText(mEditEndTag + texts[texts.size - 1]))
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
var width = widthSize
var height = heightSize
val realText = StringBuffer()
for (aText in mTextList) {
realText.append(aText.text)
}
when(widthMode) {
MeasureSpec.EXACTLY -> {
width = widthSize
//用户指定宽高
mWidth = width
mMaxSizeOneLine = (width / mOneWordWidth).toInt()
}
MeasureSpec.UNSPECIFIED, MeasureSpec.AT_MOST -> {
//绘制宽高为文字最大长度,如果长度超过,则使用父布局可用的最大长度
width = if (mText.isEmpty()) 0
else Math.min(widthSize, measureTextLength(realText.toString()).toInt())
//设配最大宽高
mWidth = widthSize
mMaxSizeOneLine = (widthSize / mOneWordWidth).toInt()
}
}
when(heightMode) {
MeasureSpec.EXACTLY -> height = heightSize
MeasureSpec.UNSPECIFIED, MeasureSpec.AT_MOST ->
height = if (realText.isEmpty()) 0
else //其中mRowSpace + mNormalPaint.fontMetrics.descent是最后一行距离底部的间距
(getRowHeight() * (mCurDrawRow - 1) + mRowSpace + mNormalPaint.fontMetrics.descent).toInt()
}
setMeasuredDimension(width, height)
}
override fun draw(canvas: Canvas) {
clear()
canvas.save()
mStartIndex = 0
mEndIndex = mMaxSizeOneLine
for (i in 0 until mTextList.size) {
val aText = mTextList[i]
val text = aText.text
while (true) {
if (mEndIndex > text.length) {
mEndIndex = text.length
}
addEditStartPos(aText) //记录编辑初始位置
val cs = text.subSequence(mStartIndex, mEndIndex)
mOneRowTexts.add(AText(cs.toString(), aText.isFill))
mOneRowText.append(cs)
val textWidth = measureTextLength(mOneRowText.toString())
if (textWidth posInfo.rect.left && event.x posInfo.rect.top && event.y = 0) { //可能存在换行
mEditTextRow = firstRow
}
}
mEditingText = aText
calculateCursorPos(event, aText.posInfo[mEditTextRow]!!, aText.text)
return true
}
}
}
}
return false
}
/**
* 计算光标位置
*/
private fun calculateCursorPos(event: MotionEvent, posInfo: EditPosInfo, text: String) {
val eX = event.x
var innerWidth = eX - posInfo.rect.left
var nWord = (innerWidth / mOneWordWidth).toInt()
var wordsWidth = 0
if (nWord row) {
firstRow = row
}
}
return firstRow
}
}
data class EditPosInfo(var index: Int, var rect: Rect)
推荐文章
Android 使用代码实现一个填空题