您当前的位置: 首页 >  android

Kevin-Dev

暂无认证

  • 3浏览

    0关注

    544博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

【Android 自定义 View】-->Android「填空题」控件

Kevin-Dev 发布时间:2019-12-06 09:23:18 ,浏览量:3

不断学习,做更好的自己!💪

视频号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 使用代码实现一个填空题

关注
打赏
1658837700
查看更多评论
立即登录/注册

微信扫码登录

0.0414s