[点心]自定义控件之这文字有点浪-Kotlin实现

声明:博文未经授权一律不允转载

控件源码:https://github.com/halohoop/AndroidDigIn#24带文字的波浪

壹.效果图

demo0

贰.知识点

  • 1.SurfaceView子线程高效绘制;
  • 2.贝塞尔曲线画波浪;
  • 3.文字测量;
  • 4.Kotlin语法;
  • 5.Region+Path的使用;

叁.背景&介绍

  • 给自己的需求是酱的:想要实现一个随着波浪浮动的文字。
  • 可能是因为使用kotlin语法简洁的缘故,代码只有不到400行。
  • 用工的孩子时间都不多,心照啦,所以全文会尽量不说废话或少说废话,先来一句废话,么么哒。

肆.使用方式

  • 改变速度,绘制的时候就是根据速度的值来决定波浪移动的距离的,代码见下文的绘制方法

progress.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
    override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
        val value = progress.toFloat()
        view.mSpeed = if (value<=0) 1F else value
    }

    ...
})

  • 修改文字,可以看到Kotlin的set我们只需要直接给变量赋值即可。

view实例.text = "中abcdefghijkl文"

//具体的set、get方法
var text: String = "Halohoop"
    set(value) {
        stopLoopDraw()//先暂停
        lock(lockObj,{
            field = value
            val textPosMidX = mMidX;
            updateTextsConfigs(value, textPosMidX)
        })
        startLoopDraw()//重新开始
    }
    get

伍.拆解轮子

1.绘制波浪。

  • 准备波浪Path
    • 如下图,不管你屏幕中画多少个周期,你给我画多一个周期出来用作移动就好了,每次朝一个方向移动完一个周期就重置。
    • demo1
    • 取巧的方法是Path.rQuadTo方法,而不是直接用Path.quadTo,因为可以相对于上一个Path移动到的位置作为起点。
    • 看码

private fun drawWave(canvas: Canvas?) {
    paint.color = WAVE_COLOR
    val quaterWaveWidth = mHalfWaveWidth / 2f;//四分之一个周期

    path.rewind()//重置path,清空路径

    val dx = mDx;//移动的距离
    path.moveTo(0f - mHalfWaveWidth * 2f + dx, mMidY)//移动到开始点

    for (i in 0..mWaveCount) {
        path.rQuadTo(quaterWaveWidth, mWaveHeight, mHalfWaveWidth, 0f)
        path.rQuadTo(quaterWaveWidth, -mWaveHeight, mHalfWaveWidth, 0f)
    }

    path.lineTo(measuredWidth.toFloat(), measuredHeight.toFloat())//和右下角连接起来
    path.lineTo(0f, measuredHeight.toFloat())//和左下角连接起来
    path.close()//封闭起来

    canvas?.drawPath(path, paint)
}

2.拆解文字,得到文字绘制的区域Rect。

  • 看码,你应该注意到方法中又定义方法,这是Kotlin特性,就是这么任性,抄抄JavaScript有时还抄抄C++抄抄Groovy…
  • 看码中注释即可,不废话

//用于计算文字位置的Region集合
private var mTextPositionHelperRegions: ArrayList<Region>? = null
//每个文字的宽度数组
private lateinit var mEveryLetterWidths: FloatArray
...
@MainThread
private fun updateTextsConfigs(newText: String, textPosMidX: Float) {

    //获取每个文字的宽度
    fun getEveryLetterWidth(text: String) : FloatArray{
        mEveryLetterWidths = FloatArray(text.length)
        textPaint.getTextWidths(text, 0, text.length, mEveryLetterWidths)
        return mEveryLetterWidths
    }

    //得到所有文字的总宽
    fun getHowWidthOfTexts(everyLetterWidths: FloatArray): Float {
        mHowWidthOfTexts = 0f;
        everyLetterWidths.forEach { mHowWidthOfTexts += it }
        return mHowWidthOfTexts
    }

    //得到所有的文字的绘制区域,放入集合中
    fun updateTextPositionHelperRegions(textPosMidX: Float = 0f, howWidthOfTexts: Float,
                                                everyLetterWidths: FloatArray) {
        val startX = textPosMidX - howWidthOfTexts / 2
        val tmpEveryLetterWidths = everyLetterWidths
        var tmpHowWidthOfTexts = howWidthOfTexts
        val lettersCount = tmpEveryLetterWidths.size

        if (mTextPositionHelperRegions != null) {
            mTextPositionHelperRegions!!.clear()
            mTextPositionHelperRegions = null
        }
        mTextPositionHelperRegions = ArrayList<Region>()

        for (i in lettersCount - 1 downTo 0) {//倒序遍历每个字
            if (tmpHowWidthOfTexts < 0) tmpHowWidthOfTexts = 0f
            tmpHowWidthOfTexts -= tmpEveryLetterWidths[i]
            val region = Region((startX - 1 + tmpHowWidthOfTexts).toInt(), 0,
                    (startX + tmpHowWidthOfTexts).toInt(), measuredHeight)
            mTextPositionHelperRegions!!.add(0, region)//往最前面插
        }
    }

    val everyLetterWidths = getEveryLetterWidth(newText)
    //get how width of texts
    val howWidthOfTexts = getHowWidthOfTexts(everyLetterWidths)
    //initialize TextPositionHelperRegions
    updateTextPositionHelperRegions(textPosMidX, howWidthOfTexts, everyLetterWidths)
}

3.通过region得到波浪Path上的x,y坐标,将文字画出来。


private fun drawTexts(canvas: Canvas?) {
    textPaint.color = TEXT_COLOR
    var i = 0
    val toCharArray = text!!.toCharArray()
    mTextPositionHelperRegions?.forEach {
        //以下这句是得到波浪上点的关键
        //会往it,也就是mTextPositionHelperRegions的一个元素Region里面塞入裁剪之后的Rect
        mRegion.setPath(path, it)//里面记录了Path和Region相交的四个方向的最值。
        if (DEBUG) {
            //debug
            canvas?.drawRect(it.bounds, textPaint)
        }

        //使用左和上值绘制文字
        canvas?.drawText(toCharArray[i] + "", it.bounds.left.toFloat(), mRegion.bounds.top.toFloat(), textPaint)
        if (DEBUG && i == 0) {
            canvas?.drawText(text, it.bounds.left.toFloat(), mRegion.bounds.top.toFloat() - 100, textPaint)
        }
        i++
    }
}

4.设置动画动起来和停下,当你非常确定一个变量不为空的时候可以加入“!!”拒绝kotlin帮你做好的空指针危险限制。


fun startFlow() {
    stopFlow()
    flowAnimator = ValueAnimator.ofFloat(0f, mHalfWaveWidth * 2)
            .setDuration(1000)
    flowAnimator!!.setRepeatMode(ValueAnimator.RESTART)
    flowAnimator!!.setRepeatCount(ValueAnimator.INFINITE)
    flowAnimator!!.interpolator = LinearInterpolator()
    flowAnimator!!.addUpdateListener {
        mDx = it.animatedValue as Float
        this@WaveWithTextView.invalidate(0, (mMidY - mWaveHeight).toInt(), measuredHeight, (mMidY + mWaveHeight).toInt())
    }

    flowAnimator!!.start()
}

fun stopFlow() {
    if (flowAnimator != null && flowAnimator!!.isRunning && flowAnimator!!.isStarted) {
        flowAnimator!!.cancel()
        flowAnimator = null
    }
}

5.使用SurfaceView子线程绘制实现。

  • 5.1.这里贴出关键绘制线程run方法代码:
    • 其中是抽象方法onThreadRenderDraw,负责使用给的Canvas绘制工作,执行在子线程中.
    • 更多详情请参阅BaseSurfaceWaveWithTextsViewKotlin原码。

override fun run() {
    val startTime = System.currentTimeMillis()
    while (true) {
        if (!mIsRunning) {//控制是否开始波浪起伏
            Log.i("halohoop", "Halohoop--" + "stop")
            break
        }
        val canvas = mSurfaceHolder.lockCanvas()
        if (canvas != null) {
            lock(lockObj, {
                if (mIsRunning) {
                    onThreadRenderDraw(canvas, System.currentTimeMillis() - startTime)
                }
            })
            //绘制完成
            mSurfaceHolder.unlockCanvasAndPost(canvas)
        }
        try {
            Thread.sleep(SLEEP_TIME)//用于保证和垂直刷新信号同步的时间差
        } catch (e: InterruptedException) {
            e.printStackTrace()
        }
    }
}
...
companion object {
    private val SLEEP_TIME: Long = 16//毫秒
}
...
//其中onThreadRenderDraw是基类的抽象方法,
//具体需要话什么需要子类自己去实现
//并且这个方法是在子线程中进行绘制的。
@WorkerThread
protected abstract fun onThreadRenderDraw(canvas: Canvas, l: Long)

  • 5.2.多线程加锁工具方法,由于涉及线程因此一些关键状态的修改需要加锁

fun <T> lock(lock: Lock, body: () -> T) {//为了加锁方便,定义了一个加锁工具方法
    lock.lock()
    try {
        body()
    }
    finally {
        lock.unlock()
    }
}
...
//使用示例
override fun surfaceDestroyed(holder: SurfaceHolder) {
    lock(lockObj,{
        stopLoopDraw()
    })
}

  • 5.3.具体的绘制过程

override fun onThreadRenderDraw(canvas: Canvas, l: Long) {
    canvas.drawColor(Color.WHITE)

    //当你单位时间水平方向的移动距离越大时候,速度就越快
    mDx += mSpeed
    if (mDx >= mHalfWaveWidth * 2) {
        mDx = 0f
    }

    if (DEBUG) {
        canvas?.drawLine((measuredWidth shr 1).toFloat(), 0f,
                (measuredWidth shr 1).toFloat(), measuredHeight.toFloat(), paint)
    }

    drawTexts(canvas)//画文字
    drawWave(canvas)//画波浪

}

//没什么可说的,一目了然的命名
private fun drawWave(canvas: Canvas?) {
    paint.color = WAVE_COLOR
    val quaterWaveWidth = mHalfWaveWidth / 2f;

    path.rewind()//清空旧path

    val dx = mDx;
    path.moveTo(0f - mHalfWaveWidth * 2f + dx, mMidY)

    for (i in 0..mWaveCount) {
        path.rQuadTo(quaterWaveWidth, mWaveHeight, mHalfWaveWidth, 0f)
        path.rQuadTo(quaterWaveWidth, -mWaveHeight, mHalfWaveWidth, 0f)
    }

    path.lineTo(measuredWidth.toFloat(), measuredHeight.toFloat())
    path.lineTo(0f, measuredHeight.toFloat())
    path.close()

    canvas?.drawPath(path, paint)
}

private fun drawTexts(canvas: Canvas?) {
    textPaint.color = TEXT_COLOR
    var i = 0
    val toCharArray = text!!.toCharArray()
    mTextPositionHelperRegions?.forEach {
        //kotlin的forEach遍历可以使用it来接收每一轮遍历的结果
        mRegion.setPath(path, it)
        //当一个Region和path相交的时候
        //这个path在这个region中被用一个Rect来描述,也就是下面的bounds
        //因此就能够得到这个path被region裁剪之后四边之最,左上右下
        if (DEBUG) {
            //debug
            canvas?.drawRect(it.bounds, textPaint)
        }

        canvas?.drawText(toCharArray[i] + "", it.bounds.left.toFloat(), mRegion.bounds.top.toFloat(), textPaint)
        if (DEBUG && i == 0) {
            canvas?.drawText(text, it.bounds.left.toFloat(), mRegion.bounds.top.toFloat() - 100, textPaint)
        }
        i++
    }
}

陆.新需求迭代展望:

  • 1.支持使用图片替换文字。
  • 2.添加TextureView的版本,因为TextureView可以做view的动画。
  • 3.加入角度的旋转,打造文字随着波浪的切线摆动,会更加真实,欢迎继续关注。

柒.最后

声明:此控件没有经过完整测试,纯练手控件,不要随便在项目中使用,请自行完善。
思想为主,知识为辅,Coding随后。
如果读者还有不明朗的地方,欢迎查看源码,并且给我提bug,一起为这个社区做出自己的微薄贡献。

控件源码:https://github.com/halohoop/AndroidDigIn#24带文字的波浪

文章作者: Halohoop
文章链接: http://halohoop.com/2017/08/08/snacks-wave_float_text_view/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 卖牙膏的芖口钉