声明:博文未经授权一律不允转载
控件源码:https://github.com/halohoop/AndroidDigIn#24带文字的波浪
壹.效果图
贰.知识点
- 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
- 如下图,不管你屏幕中画多少个周期,你给我画多一个周期出来用作移动就好了,每次朝一个方向移动完一个周期就重置。
- 取巧的方法是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,一起为这个社区做出自己的微薄贡献。