自定义控件绘图(Canvas,Paint,Region等)篇一

参考:

  1. https://blog.csdn.net/harvic880925/article/details/38875149
  2. https://www.jianshu.com/p/f2b14c994f0f
  3. https://blog.csdn.net/harvic880925/article/details/38926877

关于绘图相关的 Paint、Canvas多少都接触过一些,但没有系统的学习过,每次都是边查边用,这里都是参考大神的博客而成的学习记录,(采用Kotlin语言,来编写,kotlin确实有些坑,但不妨碍学习),同时将心得与组内开发人员一起学习,分享;

1. Paint与Canvas

Paint是画笔,Canvas是布,程序中为画布;

  1. 跟要要画的东西的设置相关的,比如大小,粗细,画笔颜色,透明度,字体的样式等等,都是在Paint里设置;
  2. 凡是要画出成品的东西,比如圆形,矩形,文字等相关的都是在Canvas里生成;

Paint 基本设置函数:

  • paint.setAntiAlias(true);//抗锯齿功能
  • paint.setColor(Color.RED); //设置画笔颜色
  • paint.setStyle(Style.FILL);//设置填充样式,Style.STOKE 为空心
  • paint.setStrokeWidth(30);//设置画笔宽度
  • paint.setShadowLayer(10, 15, 15, Color.GREEN);//设置阴影

以上几个方法除setShadowLayer都好理解,不做过多解释;setShadowLayer签名为:

/**
 * radius:阴影的倾斜度
 * dx:水平位移
 * dy:垂直位移
 */
public void setShadowLayer(float radius, float dx, float dy, int shadowColor)

Canvas的基本设置:

  • canvas.drawColor(Color.BLUE);
  • canvas.drawRGB(255, 255, 0); //这两个功能一样,都是用来设置背景颜色

2. 基本几何图形绘制

单条直线与多条直线

我们知道,确定一条直线,需要2个点,所以此方法API与此定义一致如:

// 方法签名
public void drawLine(float startX, float startY, float stopX, float stopY,
            @NonNull Paint paint) {...}
// 使用 
canvas.drawLine(0f,0f, 100f, 10f, paint)  

多条直线

public void drawLines(@Size(multiple = 4) @NonNull float[] pts, @NonNull Paint paint) 
public void drawLines(@Size(multiple = 4) @NonNull float[] pts, int offset, int count,
            @NonNull Paint paint) 

参数:

  • pts为一系列点的集合,至少要有4个点,他们不是连接线,而是每2个点形成一条直线;
// multi line
val pts = floatArrayOf(200f, 350f, 230f, 400f, 250f, 450f, 300f, 500f)
canvas.drawLines(pts, paint)

单个点与多个点

// 单个点
public void drawPoint(float x, float y, @NonNull Paint paint) 

// 多个点
public void drawPoints(@Size(multiple = 2) @NonNull float[] pts, @NonNull Paint paint) 
public void drawPoints(@Size(multiple = 2) float[] pts, int offset, int count,
            @NonNull Paint paint) 

参数:

  • offset:集合中跳过的数值个数,注意不是点的个数!一个点是两个数值;
  • count:参与绘制的数值的个数,指pts[]里人数值个数,而不是点的个数,因为一个点是两个数值;
val paint2 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    color = Color.GREEN
    style = Paint.Style.FILL
    strokeWidth = 20f
}
canvas.drawPoint(500f, 500f, paint2)
val points = floatArrayOf(550f, 550f, 550f, 600f, 500f, 650f, 500f, 700f)
// 跳过2个数值,然后用后面6个点,来画poiner,也就是最后3个点,
// 如果count传入4,即最后一个不画,传大于6的数,奔溃(ArrayIndexOutOfBoundsException),
// 因为8个数,除去2个数外,只有6个数了;
canvas.drawPoints(points, 2, 6, paint2)

矩形工具类RectF与Rect

根据四个点构建一个矩形结构;在画图时,利用这个矩形结构可以画出对应的矩形或者与其它图形Region相交、相加等等;
Region部分,后面细说;

圆角矩形

public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) 

参数:

  • rx,ry 表示生成圆角的椭圆的X|Y轴半径

画圆

很好理解,确定圆心与半径;

椭圆

椭圆是根据矩形生成的,以矩形的长为椭圆的X轴,矩形的宽为椭圆的Y轴,建立的椭圆图形;

val paint = Paint().apply {
    color = Color.RED
    style = Paint.Style.STROKE//填充样式改为描边
    strokeWidth = 5f//设置画笔宽度
}

val rect = RectF(100f, 10f, 600f, 300f)
canvas.drawRect(rect, paint)//画矩形
paint.color = Color.GREEN//更改画笔颜色
canvas.drawOval(rect, paint)//同一个矩形画椭圆
oval

弧是椭圆的一部分,而椭圆是根据矩形来生成的,所以弧也是根据矩形来生成的;

public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, 
      boolean useCenter, @NonNull Paint paint) 

参数:

  • RectF oval:生成椭圆的矩形
  • float startAngle:弧开始的角度,以X轴正方向为0度
  • float sweepAngle:弧持续的角度
  • boolean useCenter:是否有弧的两边,True,还两边,False,只有一条弧
val paint = Paint().apply {
    color = Color.RED
    style = Paint.Style.STROKE//填充样式改为描边
    strokeWidth = 2f//设置画笔宽度
}

val rect = RectF(20f, 10f, 600f, 300f)
canvas.drawRect(rect, paint)
paint.color = Color.GREEN
paint.strokeWidth = 5f
canvas.drawArc(rect, 0f, 90f, true, paint)
canvas.drawArc(rect, 180f, 90f, false, paint)

画笔描边效果:


image.png

画笔填充效果


FILL

路径 path

使用 canvas.drawPath(path, paint)

直线路径

  1. void moveTo (float x1, float y1): 表示直线的开始点;
  2. void lineTo (float x2, float y2):直线的结束点,又是下一次绘制直线路径的开始点;lineTo()可以一直用;
  3. void close ():将路径首尾点连接起来,形成闭环;
// 画一个三角形
val path = Path().apply {
      moveTo(10f, 10f)
      lineTo(10f, 100f)//第一条直线的终点,也是第二条直线的起点
      lineTo(300f, 100f)//画第二条直线
      close() //形成闭环
}
canvas.drawPath(path, paint)

画一个气泡图:
https://www.jianshu.com/p/40abd770d05c

矩形路径

void addRect (float left, float top, float right, float bottom, Path.Direction dir)
void addRect (RectF rect, Path.Direction dir)

参数其中Path.Direction有两个值:
Path.Direction.CCW:是counter-clockwise缩写,指创建逆时针方向的矩形路径;
Path.Direction.CW:是clockwise的缩写,指创建顺时针方向的矩形路径;

圆角矩形路径

void addRoundRect (RectF rect, float[] radii, Path.Direction dir)
void addRoundRect (RectF rect, float rx, float ry, Path.Direction dir)

两个画圆角函数,部分参数说明如下:

  • float[] radii:必须传入8个数值,分四组,分别对应每个角所使用的椭圆的横轴半径和纵轴半径,如{x1,y1,x2,y2,x3,y3,x4,y4},其中,x1,y1对应第一个角的(左上角)用来产生圆角的椭圆的横轴半径和纵轴半径,其它类推……
  • 第二个构造函数:只能构建统一圆角大小
    • rx/ry:所产生圆角的椭圆的横/纵轴半径;

圆形、椭圆、弧等等

参考源博客

文字

涉及画文字相关的API比较多;

画笔设置

val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
            // 1.基本设置
            style = Paint.Style.FILL;//绘图样式,对于设文字和几何图形都有效
            textAlign = Paint.Align.CENTER;//设置文字对齐方式,取值:align.CENTER、align.LEFT或align.RIGHT
            textSize = 12f;//设置文字大小

            // 2.样式设置
            isFakeBoldText = true      // 粗体 bold
            isUnderlineText = true     // 下划线
            textSkewX = 0.25f       // 设置字体水平倾斜度
            isStrikeThruText = true // 删除线

            // 3.其他
            textScaleX = 2f     // 水平方向拉伸
}

画文字

 public void drawText(String text, float x, float y, Paint paint) {

参数说明:

  • x : LEFT 默认,字符串的左边在屏幕的位置,CENTER 字符串的中心,RIGHT,右边
  • y:是指定这个字符baseline在屏幕上的位置,y不是这个字符中心在屏幕上的位置,而是baseline在屏幕上的位置

例子1:绘图样式

paint.textSize = 38f
paint.color = Color.RED
paint.strokeWidth = 2f              //设置画笔宽度,必须要设置,否则设置style无效
paint.textAlign = Paint.Align.LEFT
paint.style = Paint.Style.FILL
canvas.drawText("填充(FILL) ==》Kotlin 重新学Android", 2f, 100f, paint)

paint.style = Paint.Style.STROKE
canvas.drawText("描边(STROKE) ==》Kotlin 重新学Android", 2f, 200f, paint)

paint.style = Paint.Style.FILL_AND_STROKE
canvas.drawText("填充&描边(FILL_AND_STROKE) ==》Kotlin 重新学Android", 2f, 300f, paint)
绘图样式

例子2:样式与倾斜度与水平拉伸

val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    textSize = 38f
    color = Color.RED
    strokeWidth = 2f
    textAlign = Paint.Align.LEFT
    style = Paint.Style.FILL

    // 样式设置
    isFakeBoldText = true
    isUnderlineText = true
    isStrikeThruText = true
}

// ======= 水平倾斜度为:0.25,右斜
paint.textSkewX = -0.25f
canvas.drawText("textSkewX -0.25f ==> Kotlin 重新学Android", 10f, 100f, paint)
paint.textSkewX = 0.25f
canvas.drawText("textSkewX 0.25f ==> Kotlin 重新学Android", 10f, 200f, paint)

canvas.drawLine(0f, 235f, width.toFloat(), 235f, paint)

// ======= 水平拉伸
val paint2 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    textSize = 38f
    color = Color.RED
    strokeWidth = 2f
    style = Paint.Style.FILL
}

// 正常绘制
canvas.drawText("Kotlin 重新学Android", 10f, 300f, paint2)
paint2.textScaleX = 2f  // 水平拉伸2倍
canvas.drawText("Kotlin 重新学Android", 10f, 400f, paint2)

// 对比
paint2.textScaleX = 1f  //
canvas.drawText("Android开发艺术探索", 10f, 500f, paint2)
paint2.textScaleX = 2f
paint2.color = Color.GREEN
canvas.drawText("Android开发艺术探索", 10f, 500f, paint2)
image.png

Canvas绘图方式

普通水平方向绘制

void drawText (String text, float x, float y, Paint paint)
void drawText (CharSequence text, int start, int end, float x, float y, Paint paint)
void drawText (String text, int start, int end, float x, float y, Paint paint)
void drawText (char[] text, int index, int count, float x, float y, Paint paint)

参考原博客

沿路径绘制

public void drawTextOnPath(String text, Path path, float hOffset,
            float vOffset, Paint paint) {  }

参数:

  • float hOffset : 与路径起始点的水平偏移距离
  • float vOffset : 与路径中心的垂直偏移量
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    textSize = 38f
    color = Color.RED
    strokeWidth = 2f
    style = Paint.Style.STROKE
}

// ==== 沿路径绘制
val text = "风萧萧兮易水寒,壮士一去兮不复返"
val circlePath1 = Path().apply {
    addCircle(width / 4.toFloat(), 220f, 180f, Path.Direction.CCW)  // 逆時針
}
val circlePath2 = Path().apply {
    addCircle(width / 4 * 3.toFloat(), 220f, 180f, Path.Direction.CCW)
}
canvas.drawPath(circlePath1, paint)
canvas.drawPath(circlePath2, paint)

paint.color = Color.GREEN
// 设置0,0 ,注意这里是逆时针画的圆
canvas.drawTextOnPath(text, circlePath1, 0f, 0f, paint)
// 设置了偏移,水平100,垂直 30f
canvas.drawTextOnPath(text, circlePath2, 100f, 30f, paint)
图片转自原博客

字体样式与设置自定义字体

参考源博客

3.区域 Region

表示的是canvas图层上的某一块封闭的区域;

构造Region

// 基本构造
public Region()  //创建一个空的区域  
public Region(Region region) //拷贝一个region的范围  
public Region(Rect r)  //创建一个矩形的区域  
public Region(int left, int top, int right, int bottom) //创建一个矩形的区域  

// 间接构造
public void setEmpty()  //置空
public boolean set(Region region)    // 替换
public boolean set(Rect r)   
public boolean set(int left, int top, int right, int bottom) 
// 根据路径的区域与某区域的交集,构造出新区域  
public boolean setPath(Path path, Region clip)

通过Region来画一个矩形

private val region = Region(10, 10, 100, 100)

val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    strokeWidth = 2f
    style = Paint.Style.FILL;//绘图样式,对于设文字和几何图形都有效
    color = Color.RED
}
drawRegion(canvas, region, paint)

private fun drawRegion(canvas: Canvas, rgn: Region, paint: Paint) {
    val iter = RegionIterator(rgn)
    val r = Rect()
    while (iter.next(r)) {
        canvas.drawRect(r, paint)  // 画出region
    }
}

使用setPath()构造不规则区域

public boolean setPath(Path path, Region clip) 

参数:

  • path : 路径
  • clip:与path所构成的路径取交集,并将两交集设置为最终的区域
// 椭圆路径
val ovalPath = Path()
val rect = RectF(50f, 50f, 200f, 500f)
canvas.drawRect(rect, paint)
ovalPath.addOval(rect, Path.Direction.CCW)
// setPath时,传入一个比椭圆区域小的矩形区域,让其取【交集】
val region = Region()
region.setPath(ovalPath, Region(50, 50, 200, 300))
paint.color = Color.RED
paint.style = Paint.Style.FILL

drawRegion(canvas, region, paint)
交集效果

矩形集枚举区域——RegionIterator

一定数量的矩形所合成的形状,也可以代表区域的形状。RegionIterator类,实现了获取组成区域的矩形集的功能,RegionIterator两个函数,一个构造函数和一个获取下一个矩形的函数(next);

在Canvas中没有直接绘制Region的函数,要绘制一个区域,就只能通过利用RegionIterator构造矩形集来逼近的显示区域,就是上面的 drawRegion()方法;

根据区域构建一个矩形集,然后利用next(Rect r)来逐个获取所有矩形,绘制出来,最终得到的就是整个区域;

将上面的画笔Style从FILL改为STROKE,的效果,可以看到很多个rect绘制出来了;

STROKE region 效果

区域的合并、交叉等操作

无论是区域还是矩形,都会涉及到与另一个区域的一些操作,比如取交集、取并集等,涉及到的函数有:

// 并集
public final boolean union(Rect r)    
public boolean op(Rect r, Op op) {  
public boolean op(int left, int top, int right, int bottom, Op op)   
public boolean op(Region region, Op op)   
public boolean op(Rect rect, Region region, Op op)   

// Op为
public enum Op {    
        DIFFERENCE(0), //最终区域为region1 与 region2不同的区域    
        INTERSECT(1), // 最终区域为region1 与 region2相交的区域    
        UNION(2),      //最终区域为region1 与 region2组合一起的区域    
        XOR(3),        //最终区域为region1 与 region2相交之外的区域    
        REVERSE_DIFFERENCE(4), //最终区域为region2 与 region1不同的区域    
        REPLACE(5); //最终区域为为region2的区域    
 }   

实例代码:

val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    strokeWidth = 2f
    color = Color.RED
    style = Paint.Style.STROKE
}

val rect1 = Rect(200, 200, 400, 260)
val rect2 = Rect(270, 130, 330, 330)

canvas.let {
    it.drawRect(rect1, paint)
    it.drawRect(rect2, paint)
}

// 集合操作
val paint2 = Paint().apply {
    style = Paint.Style.FILL
    color = Color.GREEN
}
val region1 = Region(rect1)
val region2 = Region(rect2)
region1.op(region2, op)     // Region.Op.INTERSECT
drawRegion(canvas, region1, paint2)

// 文字说明
paint.textAlign = Paint.Align.CENTER
paint.textSize = 28f
canvas.drawText("横为region1", 300f, 450f, paint)
canvas.drawText("竖为region2", 300f, 480f, paint)
canvas.drawText("操作为:region1.op(region2, op)", 300f, 510f, paint)
region1.op(region2,op)

图片copy自源博客

补集有点不好理解

Region其他方法

查看源博客
https://blog.csdn.net/harvic880925/article/details/39056701

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容