自定义View(Canvas,Paint,贝塞尔曲线)
目录
在Android应用层来说,众所周知,自定义一个View,需要经过测量,布局,绘制三个步骤。每个步骤深入将都可以写成长篇大论。今天我们主要了解的是onDraw绘制的部分。通过一个TreeView的例子来引入,下面先看看效果图:
//todo 导入一张效果图
内功口诀:点、线、面。
要绘图,需要4个基本组件:
- Bitmap 保存像素的容器
- Canvas 执行绘图命令的宿主
- Rect/Path/text/Bitmap 要绘制的元素
- Paint 用什么样的方式绘制
下面我们分成以下几个步骤:
-
在Canvas上绘制圆,利用Bitmap保存起来(这一步很关键,如果不在bitmap上画,则无法保存)
-
在路径上绘制圆,保存起来,形成线(形成动画)
-
同时绘制多条路径
在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 缺一张树图和代码