Android高级开发进阶之路1——自定义控件(Canvas,Paint,贝塞尔曲线)

本文介绍如何在Android中自定义View,包括在Canvas上绘制圆并利用Bitmap保存,通过贝塞尔曲线实现画线动画,以及同时绘制多条路径的技术细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

自定义View(Canvas,Paint,贝塞尔曲线)

目录

自定义View(Canvas,Paint,贝塞尔曲线)

在Canvas上绘制圆,利用Bitmap保存起来

在路径上,依次绘制圆,形成画线动画

同时绘制多条路径


在Android应用层来说,众所周知,自定义一个View,需要经过测量,布局,绘制三个步骤。每个步骤深入将都可以写成长篇大论。今天我们主要了解的是onDraw绘制的部分。通过一个TreeView的例子来引入,下面先看看效果图:

//todo 导入一张效果图

内功口诀:点、线、面。

 

要绘图,需要4个基本组件:

  • Bitmap 保存像素的容器
  • Canvas 执行绘图命令的宿主
  • Rect/Path/text/Bitmap 要绘制的元素
  • Paint 用什么样的方式绘制

 

下面我们分成以下几个步骤:

  1. 在Canvas上绘制圆,利用Bitmap保存起来(这一步很关键,如果不在bitmap上画,则无法保存)

  2. 在路径上绘制圆,保存起来,形成线(形成动画)

  3. 同时绘制多条路径

在Canvas上绘制圆,利用Bitmap保存起来

步骤:创建新Canvas(Bitmap)--->  在新Canvas上绘制--->  在View的Canvas上绘制bitmap

创建新Canvas(Bitmap)

/**
     * 保存所绘制的bitmap
     */
    class BlankBoard {
        var blankBoardCanvas: Canvas
        var blankBoardBitmap: Bitmap

        constructor(bitmap: Bitmap){
            this.blankBoardBitmap = bitmap
            this.blankBoardCanvas = Canvas(this.blankBoardBitmap)
        }
    }
class DrawViewInBitmap : View {
  //...
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        blankBoard = BlankBoard(Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888))
    }
//...
}

在新Canvas上绘制

class DrawViewInBitmap : View {
//...
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        //..
 //1、画一个圆到bitmap
        drawCircle2BlankBoardBitmap(200f, 200f, 50f,blankBoard.blankBoardCanvas, paint)
       
    }

    /**
     * 绘制一个圆到白板上
     * (圆点x坐标,圆点y坐标,圆半径,画笔)
     */
    private fun drawCircle2BlankBoardBitmap(dx: Float, dy: Float
                                            , radius: Float, canvas: Canvas, mPaint: Paint){
        canvas.save()//保存之前的canvas坐标状态
        canvas.translate(dx,dy)
        canvas.drawCircle(0f,0f,radius,mPaint)
        canvas.restore()//恢复之前的canvas坐标状态
    }
//...
}

在View的Canvas上绘制bitmap

class DrawViewInBitmap : View {
  //...
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        //...
        //1、画一个圆到bitmap
        drawCircle2BlankBoardBitmap(200f, 200f, 50f,blankBoard.blankBoardCanvas, paint)
        //2、在自定义view的canvas中绘制blankBoard.bitmap
        canvas?.drawBitmap(blankBoard.blankBoardBitmap,0f,0f,paint)
    }
//...
}

看看效果:

这里只是关键Code,源码我会在后面给到大家。

 

在路径上,依次绘制圆,形成画线动画

效果:

 

通过修改上面的文件,每次绘制完一圆之后,y坐标+10,圆半径大小为原来97%,调用invalidate()重绘布局,即可。

下面是部分关键代码

class DrawViewInBitmap2 : View {
    //当前半径
    private var currentRadius: Float = 50f
    //当前圆点y坐标
    private var currentY: Float = 200f
   //...

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        //...
        //1、画一个圆到bitmap
        drawCircle2BlankBoardBitmap(200f, currentY,         
                 currentRadius,blankBoard.blankBoardCanvas, paint)
        //2、在自定义view的canvas中绘制blankBoard.bitmap
        canvas?.drawBitmap(blankBoard.blankBoardBitmap,0f,0f,paint)

        //步增10
        currentY +=10
        currentRadius*=0.97f
        if (currentY < 700) {
            //重绘布局,重新调用onDraw()
            invalidate()
        }
        /*这里要注意点是:同一个Bitmap可以绘制在不同的Canvas上,
        一开始Bitmap在blankBoard.blankBoardCanvas上绘制,虽然是绘制成功了但是我们要展示给用户看,
        必须要画在View.onDraw方法中得到的canvas上。
        blankBoard.blankBoardBitmap在Canvas*/
    }

//...
}

同时绘制多条路径

class TreeView: View {


    private val paint: Paint = Paint()
    private var hasEnd: Boolean = false
    private var linkedListBranch: LinkedList<Branch> = LinkedList()
    private lateinit var snapShot: SnapShot

    constructor(context: Context?) :this(context, null)
    constructor(context: Context?, attrs: AttributeSet?) :this(context, attrs,0)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr){

        linkedListBranch = LinkedList<Branch>()

        linkedListBranch.add(getBranches())

    }

    private fun getBranches(): Branch {
        val bs = Array<IntArray>(3){ IntArray(10) }

        bs[0] = intArrayOf(0, -1,  340, 617, 210, 435, 324, 301, 30, 100)
        bs[1] = intArrayOf(1,  0,  285, 518, 220, 353, 154, 351, 15, 70)
        bs[2] = intArrayOf(2,  1,  237, 413, 221, 432, 155, 402,  6,  60)


        var temp = arrayOfNulls<Branch>(3)
        for (i in 0..bs.size-1) {
            temp[i] = Branch(bs[i])
            val parentId = temp[i]!!.parentId

            if (parentId > -1) {
                temp[parentId]?.addChild(temp[i])
            }
        }
        return temp[0]!!
    }


    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        snapShot = SnapShot(Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888))

    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        Log.i("this","onDraw")
        drawBranches()
        canvas?.drawBitmap(snapShot.bitmap,0f,0f,paint)

        if (!hasEnd) {
            invalidate()
        }

    }

    private fun drawBranches() {

        if (!linkedListBranch.isEmpty()) {

//            if (linkedListBranch != null) {


            var nextRankBranch: LinkedList<Branch>? = null

            var it = linkedListBranch.iterator()

            snapShot.canvas.save()

            while (it.hasNext()) {
                var itemBranch = it.next()
                if (!itemBranch.drawGrowingBranch(snapShot.canvas, paint, 1)) {
                    it.remove()
                    if (itemBranch.childBranch != null) {
                        if (nextRankBranch == null) {
                            nextRankBranch = itemBranch.childBranch
                        } else {
                            nextRankBranch.addAll(itemBranch.childBranch)
                        }
                    }
                }
            }

            snapShot.canvas.restore()

            //解决无法添加树枝的
            if (nextRankBranch != null) {
                linkedListBranch.addAll(nextRankBranch)
            }

            if (linkedListBranch.isEmpty()) {
                hasEnd = true
            }

        }
    }

    /**
     * 整棵树是由很多点绘制而成,每次绘制需要保存上一次的记录,所以不能放到TreeView的onDraw中
     * 图像绘制类,保存在bitmap中
     */
    class SnapShot {

        constructor(createBitmap: Bitmap?){
            this.bitmap = createBitmap
            this.canvas = Canvas(bitmap)
            /*canvas = Canvas()
            var p = Paint()
            p.color = Color.RED
            canvas.drawBitmap(createBitmap,0f,0f,p)*/
        }

        var bitmap: Bitmap?
        var canvas: Canvas
    }

    /**
     * 树干类
     */
    class Branch {
        private val TAG: String? = "Branch"
        private var currentX: Float = 0.0f
        private var currentY: Float = 0.0f
        private var currentLenght: Int = 0
        public lateinit var childBranch: LinkedList<Branch>
        private var maxLength: Int = 0
        private var radius: Float = 0f
        var parentId: Int = 0
        private var id: Int = 0
        private lateinit var endPoint: Point
        private lateinit var controlPoint: Point
        private lateinit var startPoint: Point

        constructor(data: IntArray) {
            this.id = data[0]
            this.parentId = data[1]
            this.startPoint = Point(data[2], data[3])
            this.controlPoint = Point(data[4], data[5])
            this.endPoint = Point(data[6], data[7])
            this.radius = data[8]*1.0f
            this.maxLength = data[9]


            childBranch = LinkedList<Branch>()
        }

        fun addChild(branch: Branch?) {
            if (childBranch == null) {
                childBranch = LinkedList<Branch>()
            }
            branch?.let { childBranch.add(it) }
        }

        fun drawGrowingBranch(canvas: Canvas, paint: Paint, scaleFactor: Int): Boolean {
            Log.i(TAG,"grow $currentLenght")
            if (currentLenght < maxLength) {
                bezier()
                draw(canvas,paint,scaleFactor*1f)
                radius *= 0.97f
                currentLenght++

                return true
            }else{
                return false
            }

        }

        fun draw(canvas: Canvas, paint: Paint, i: Float) {
            paint.color = Color.RED
            canvas.save()
            canvas.scale(i,i)
            canvas.translate(currentX,currentY)

            Log.i(TAG,"radius is $radius")
            canvas.drawCircle(0f,0f,radius,paint)
            canvas.restore()
        }

        /**
         * 贝塞尔算法计算新的坐标
         */
        private fun bezier() {
            var t = currentLenght*1f / maxLength
            var c0 = (1 - t) * (1 - t)
            var c1 = 2 * t * (1 - t)
            var c2 = t * t
            currentX = c0 * startPoint.x + c1 * controlPoint.x + c2 * endPoint.x
            currentY = c0 * startPoint.y + c1 * controlPoint.y + c2 * endPoint.y

            Log.i(TAG,"x,y -> ($currentX,$currentY)")


        }
    }

}

有了上面的基础,稍作修改就可以画好各种图案了,如下:

//todo 缺一张树图和代码

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值