一、前言

添加文本,也是属于 一个比较简单的功能,在第二篇的时候,添加了橡皮擦,在橡皮擦里面通过一个模式的形式进行画笔的判断,当然文本也是如此,添加一个文本模式,在onTouchDown的时候,弹出PopupWindow,输入文本,然后PopupWindow消失的时候,利用staticLayout绘制到画布上即可。当然也有些需要注意的地方

android文本编辑 安卓文本编辑app_画板文字

下面一步步来实现

二、实现

2.1 添加文本模式

例如橡皮擦那样,添加多一个文本模式,然后setModel的时候,需要把画笔的样式修改为FILL,如果是STROKE进行文字绘制会变成空心文字。

companion object {
        const val EDIT_MODE_PEN = 0x1L       //画笔模式
        const val EDIT_MODE_ERASER = 0x2L    //橡皮擦模式
        const val EDIT_MODE_TEXT = 0x3L    //文字模式

    }

    @Retention(AnnotationRetention.SOURCE)
    @IntDef(EDIT_MODE_PEN, EDIT_MODE_ERASER, EDIT_MODE_TEXT)
    annotation class EditMode
    
    /**
     * 设置画笔模式
     */
    fun setModel(@EditMode model: Long) {
        mMode = model
        when (model) {
            EDIT_MODE_PEN -> {
                //画线
                mPaint.xfermode = null
                mPaint.style = Paint.Style.STROKE
            }
            EDIT_MODE_ERASER -> {
                mPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)

            }
            EDIT_MODE_TEXT -> {
                mPaint.style = Paint.Style.FILL
            }
        }
    }

2.2 修改bean类型

StaticLayout 是一个为不可编辑的文本布局的类,这意味着一旦布局完成,文本内容就不可以改变。在单纯地使用TextView来展示静态文本的时候,创建的就是 StaticLayout,在api25,Textview源码6858行可以看到。

StaticLayout.Builder builder = StaticLayout.Builder.obtain(mHint, 0,
                        mHint.length(), mTextPaint, hintWidth)
                        .setAlignment(alignment)
                        .setTextDirection(mTextDir)
                        .setLineSpacing(mSpacingAdd, mSpacingMult)
                        .setIncludePad(mIncludePad)
                        .setBreakStrategy(mBreakStrategy)
                        .setHyphenationFrequency(mHyphenationFrequency);

我们画板的绘制文字也是用到了这个StaticLayout,它有三个构造方法,我们用最少那个即可:

public StaticLayout(CharSequence source,  //字符串
                    TextPaint paint,        //画笔对象
                    int width,              //layout的宽度,字符串超出宽度时自动换行。
                    Layout.Alignment align, //layout的对其方式,有ALIGN_CENTER, ALIGN_NORMAL, ALIGN_OPPOSITE 三种。
                    float spacingmult,   //相对行间距,相对字体大小,1.5f表示行间距为1.5倍的字体高度。
                    float spacingadd,    //在基础行距上添加多少
                    boolean includepad)   //文本顶部和底部是否留白

所以,bean类在之前的基础上,添加了文本、宽度、xy轴的偏移,然后绘制的时候,利用staticLayout进行了绘制。

data class PaintBean(
        var mPaint: Paint,    //保存画笔

        var mPath: Path?,         //保存路径

        var mText: String,       //文本

        var mWidth: Int,

        var mOffX: Float,

        var mOffY: Float,

        private @TPTextView.EditMode var mMode:Long
) {

    constructor(mPaint: Paint, mPath: Path) : this(mPaint,mPath,"",0,0f,0f,TPTextView.EDIT_MODE_PEN)

    /**
     * 撤销和反撤销之后 重新绘制
     * @param canvas 绘制的画布
     */
    fun draw(canvas: Canvas){

        when(mMode){

            TPTextView.EDIT_MODE_TEXT -> {

                if(!TextUtils.isEmpty(mText)){
                    //调节画布起始坐标进行绘制
                    canvas.translate(mOffX,mOffY)
                    //利用staticLayout生成文字,不然不能换行
                    val staticLayout = StaticLayout(mText,mPaint as TextPaint,mWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false)
                    staticLayout.draw(canvas)
                    //Log.e("@@","长度:"+staticLayout.width)
                    //canvas.drawText(mText,mTextOffX,mTextOffY,mPaint)
                    //恢复画布坐标
                    canvas.translate(-mOffX,-mOffY)

                }

            }
            else -> {
                canvas.drawPath(mPath,mPaint)
            }

        }

    }

    fun getMode():Long = mMode

}

2.3 弹窗处理

接下来,设置一个弹框PopupWindow进行文本的输入,弹窗里面的控件就是一个EditText。 在弹窗消失的时候添加到画笔列表,然后进行重绘。 在这里有三点注意点

  • 软键盘自动弹出
  • 编辑框显示在软键盘上面
  • 弹框显示的位置
  • 右边越界
private var mTextPopup: PopupWindow? = null
    private var mTextView: EditText? = null
    
    /**
     * 显示popup文本输入弹窗
     */
    private fun showTextPopup() {

        if (null == mTextPopup) {
            mTextView = EditText(context)
            mTextView?.hint = "文字"

            mTextPopup = PopupWindow(mTextView,
                    WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT,
                    true)
            mTextPopup?.setOnDismissListener {
                if (!TextUtils.isEmpty(mTextView?.text)) {
                    //添加到列表
                    mPaintedList.add(
                            PaintBean(TextPaint(mPaint), null, mTextView?.text.toString(), (width - preX).toInt(),preX,preY - mTextView!!.height / 2, EDIT_MODE_TEXT))

                    invalidate()
                }
            }
            //让popup显示在软键盘上面
            mTextPopup?.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
        }

        mTextView?.requestFocus()

        //自动弹出软键盘,会导致布局变化,重测量、绘制
        val imm = context.getSystemService(Service.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS)


        mTextPopup?.showAtLocation(this, Gravity.TOP and Gravity.LEFT, preX.toInt(), preY.toInt()+mTextView!!.height)

    }

在触摸的时候,进行显示。 移动的时候不用操作,手指起来的时候也不用操作

@SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {

        when (event.action) {
            MotionEvent.ACTION_DOWN -> {  //手指按下的时候
                //记录上次触摸的坐标,注意ACTION_DOWN方法只会执行一次
                preX = event.x
                preY = event.y
                when (mMode) {

                    EDIT_MODE_TEXT -> {
                        //弹出popupWidnwo输入text
                        showTextPopup()
                        //文字在隐藏的时候添加到list
                    }
                    else -> {
                        //将起始点移动到当前坐标
                        mPath.moveTo(event.x, event.y)
                        mPaintedList.add(PaintBean(Paint(mPaint), Path(mPath)))
                    }
                }

            }
            MotionEvent.ACTION_MOVE -> {  //手指移动的时候
                when (mMode) {
                    EDIT_MODE_TEXT -> {

                    }
                    else -> {
                        //绘制圆滑曲线,即贝塞尔曲线,贝塞尔曲线这个知识自行了解
                        mPaintedList.get(mPaintedList.size - 1).mPath?.quadTo(preX, preY, event.x, event.y)
                        preX = event.x
                        preY = event.y
                        //重新绘制,会调用onDraw方法
                        invalidate()
                    }

                }
            }
            MotionEvent.ACTION_UP -> {}
        }
 
        return true
    }

因为绘制在bean类里面,所以view的onDraw方法还是以前那样,不需要变化:

@SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        //超出缓存的就固化到缓存bitmap
        while (mPaintedList.size > PAINT_RECORED_NUM) {
            val paint = mPaintedList.removeAt(0)
            paint.draw(mHoldCanvas!!)
        }

        //绘制固化的内容到缓存Canvas
        mBufferCanvas?.drawBitmap(mHoldBitmap, 0f, 0f, null)

        //绘制记录的画笔
        for (paint in mPaintedList) {
            paint.draw(mBufferCanvas!!)
        }

        //画出缓存bitmap的内容
        canvas.drawBitmap(mBufferBitmap, 0f, 0f, null)

    }