学习kotlin把以前的一些知识重新理了一遍
首先对于需要大量绘制的操作,不能直接绘制,使用 SurfaceView,直接把绘制工作放到子线程中去操作,否则绘制工作加大一下,会卡顿,不过SurfaceView是独立的一层View,不能平移,缩放,旋转或者设置透明度等,如果需要这些操作可以考虑使用TextureView,这两者都是可以通过回调在子线程中去更新UI
思路:
使用二级贝塞尔曲线来完成,简单的把屏幕对半,然后各用贝塞尔去绘制一个扇形,在到屏幕外复制一份,改变控制点,达到可循环移动的效果
对比以前:
简化计算逻辑,直接循环计算控制点,重复利用线程暂停恢复操作,利用插值器替代直接计算移动的坐标(对于不同的场景可以更换不同的插值器),抽离绘制逻辑
流程:
1.从繁化简,首先不考虑其它,一级贝塞尔是一条直线,两个控制点,二级贝塞尔多了一个控制点,通过控制点利用公式算法绘制出形状,下面是直接一个贝塞尔梭哈
假设起点(0,0)屏幕的四分之一就是第二个点,屏幕的二分之一就是第三个点,然后加个二级贝塞尔,扇形就成了
path.reset()
path.moveTo(starPoint[0], starPoint[1])
path.quadTo(quadPoint[0], quadPoint[1], quadPoint[2], quadPoint[3])
canvas.drawPath(path, paint)
2.绘制第二个扇形,同样的扇形,但是方向反一下(改变控制点),path会把第一个连接点就是第一个扇形的最后一个控制点当作第二个扇形的起点,第二个坐标就是屏幕的四分之三,第三个坐标就是屏幕的宽度,然后连起来就是两个相连接的扇形,看起来不太像,这里只有线条
path.reset() path.moveTo(starPoint[0], starPoint[1]) path.quadTo(quad1Point[0], quad1Point[1], quad1Point[2], quad1Point[3]) path.quadTo(quad2Point[0], quad2Point[1], quad2Point[2], quad2Point[3]) canvas.drawPath(path, paint)
3.把扇形闭合一下,画笔设置为填充,这样看起来有点样子了,如果能让这个波浪动起来,基本的效果也就有了,可以改变x坐标移动它们的位置,但是就两个波浪,移动后很显然还达不到想要的效果,这时候需要在绘制两条一模一样的扇形,然后
循环切换
paint.style = Paint.Style.FILL //闭合区域 path.lineTo(mWidth, mHeight) path.lineTo(0f, mHeight) path.close()
4.绘制移动的波浪,假设左边还有一个屏幕,也分为四等分,以此类推,这样波浪就绘制完成了
5.改变所有波浪的x的坐标,让它从左往右移动,到临界点在置为0,重复操作,这里可以交给插值器去做,直接从0开始移动,临界点是屏幕的宽度,刚好两个波浪的距离
不用插值器也行,直接自己计算一下,可以看到,动起来了
我这边为了更好的看到效果,我给每个x坐标控制点都绘制了一个小圆点,这样看起来就更直观一点,也有利于调试一些bug,也可以把坐标都打印出来,方便计算,然后逻辑简化了一下,上面是直接把每个点坐标都一条一条的去绘制,但是由于未了达到无线波浪的效果,所以其实就是两个屏幕,一样的波浪,复制了一份,所以可以利用一个循环,从左到右,动态的改变它的坐标去绘制出两个屏幕一样的扇形(i*width)
然后可以看到答应出来的坐标信息
y坐标基本可以不看,y坐标主要控制扇形的弧度,因为是相反的两个扇形,所以一个高一个低,然后x坐标根据屏幕的宽度,分了四等份,在用贝塞尔曲线连接起来,这样一看就清晰多了
因为绘制了圆点,所以看起来视觉上比较生硬,如果把圆点去掉就丝滑多了
这里初步的思路就完成了,如果想丰富一些还能增加一些操作,比如动态改变控制点的高度,让波浪移动的同时还能有个上下起伏的效果,或者多绘制几条波浪等
class DrawWaterUtils(private val holder: SurfaceHolder) : Thread() { private var mWidth: Float = 0f private var mHeight: Float = 0f //波浪的高度 private var mWaterHeight: Float = 0f //起伏的高度 private var mWaterUp: Float = 0f /** * off = 偏移值 */ private var offx: Float = 0f private var path: Path = Path() private var paint: Paint = Paint() private var isRun: Boolean = false private var circlePaint: Paint = Paint() private lateinit var mValueAnimator: ValueAnimator init { // 去除画笔锯齿 paint.isAntiAlias = true // 设置风格为实线 paint.style = Paint.Style.FILL paint.strokeWidth = 2f circlePaint.style = Paint.Style.FILL } fun init(width: Int, height: Int) { mWidth = width.toFloat() mHeight = height.toFloat() //线性差值器 mValueAnimator = ValueAnimator.ofFloat(0f, mWidth) mValueAnimator.duration = 1700 mValueAnimator.interpolator = LinearInterpolator() mValueAnimator.repeatCount = ValueAnimator.INFINITE mValueAnimator.addUpdateListener { animation -> offx = animation.animatedValue as Float } //波浪的高度 mWaterHeight = mHeight / 2 //起伏的高度 mWaterUp = mWaterHeight / 2 } /** 启动线程 */ fun runDraw() { isRun = true start() mValueAnimator.start() } /** 恢复线程 */ fun resumeThread() { synchronized(this) { isRun = true notify() mValueAnimator.start() } } /** 暂停线程 */ fun stopDraw() { isRun = false mValueAnimator.cancel() } override fun run() { synchronized(this) { while (true) { if (!isRun) { wait() } val canvas = holder.lockCanvas() if (canvas != null) { //清除画布 canvas.drawColor( KtxProvider.mContext.getColor(R.color.text_color_titleBar_title), android.graphics.PorterDuff.Mode.CLEAR ) paint.color = KtxProvider.mContext.getColor(R.color.bg_color_3159c7_83) circlePaint.color = KtxProvider.mContext.getColor(R.color.black) water(canvas) //循环起伏 // offx += 3 // if (offx >= mWidth) { // offx = 0f // } // 解除锁定,并提交修改内容 holder.unlockCanvasAndPost(canvas) } } } } /** 绘制波浪 */ private fun water(canvas: Canvas) { path.reset() //起点 path.moveTo(-mWidth + offx, mWaterHeight) //波浪的数量 for (i in 0 until 2) { path.quadTo( -mWidth * 3 / 4 + i * mWidth + offx, mWaterHeight - mWaterUp, -mWidth / 2 + i * mWidth + offx, mWaterHeight ) //canvas.drawCircle(-mWidth * 3 / 4 + i * mWidth + offx, mWaterHeight - mWaterUp,5f,circlePaint) //canvas.drawCircle(-mWidth / 2 + i * mWidth + offx, mWaterHeight,5f,circlePaint) path.quadTo( -mWidth / 4 + i * mWidth + offx, mWaterHeight + mWaterUp, i * mWidth + offx, mWaterHeight ) //canvas.drawCircle(-mWidth / 4 + i * mWidth + offx, mWaterHeight + mWaterUp,5f,circlePaint) //canvas.drawCircle(i * mWidth + offx, mWaterHeight,5f,circlePaint) Log.e( "===\n", "i = $i\n" + "x1 ${-mWidth * 3 / 4 + i * mWidth} y1 ${mWaterHeight - mWaterUp} x2 ${-mWidth / 2 + i * mWidth} y2 $mWaterHeight \n" + "x1 ${-mWidth / 4 + i * mWidth} y1 ${mWaterHeight + mWaterUp} x2 ${i * mWidth} y2 $mWaterHeight" ) } //闭合操作 path.lineTo(mWidth, mHeight) path.lineTo(0f, mHeight) path.close() canvas.drawPath(path, paint) } }
/** * created by YooJin. * date: 2021/2/4 14:44 * desc:波浪动画 */ class WaterBgSurfaceView(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : SurfaceView(context, attrs, defStyleAttr), SurfaceHolder.Callback { init { init() } constructor(context: Context, attrs: AttributeSet?):this(context, attrs, 0) constructor(context: Context):this(context, null) private lateinit var drawWater: DrawWaterUtils private fun init() { holder.addCallback(this) //透明处理 holder.setFormat(PixelFormat.TRANSPARENT) setZOrderOnTop(true) drawWater = DrawWaterUtils(holder) } @SuppressLint("DrawAllocation") override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) if (changed){ drawWater.init(width,height) } } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) } override fun surfaceCreated(p0: SurfaceHolder) { //Log.e("===","surfaceCreated drawWater.isAlive ${drawWater.isAlive}") if (!drawWater.isAlive) { //Log.e("===","run") drawWater.runDraw() }else{ //Log.e("===","notify") drawWater.resumeThread() } } override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) { } override fun surfaceDestroyed(p0: SurfaceHolder) { //Log.e("===","surfaceDestroyed") drawWater.stopDraw() } }
这里虽然很丝滑,不过surfaceview也有缺点,就是不能像一般的view一样去设置偏移旋转等动画透明度啥的,如果你的绘制不复杂,直接draw就行了,或者可以使用 TextureView 去实现,项目里也有一套TextureView的绘制类,没什么大的区别,下面是项目地址