Android的自定义控件是非常有意思的一样东西,同时也是很需要花时间钻研的,往往可以看到别人制作出的控件高大上,而自己着手却经常感慨理想与现实的差距...
1. 基础概念
如果不使用代码,仅仅是在现实世界中,如果你想要进行绘制,会使用什么工具?
其实无非纸和笔,或者类似这二者的东西,像是手指和哈了气的窗玻璃,木棍和沙滩等等
画笔用来勾勒线条、涂色、展现创作者的意图,而纸则是用作载体,呈现内容
了解到这里,对于Android绘画所需要的工具也就有了基本的认识,这里完全可以与现实进行对应
Canvas类正是Android里面扮演“纸”这个角色的类,作用也是呈现内容
Paint类便是Android里的“画笔”,通过Paint可以让Canvas上呈现各种形状和图案
2. 准备工作
自定义控件总是离不开View,因此如果要进行显示,还需要做一些简单工作
创建一个继承自View的子类,满足这个条件,这个类便是自定义View
继承了View的4个构造方法并覆写onDraw()
这4个构造方法对应不同的创建View的场景,onDraw()方法会提供Canvas对象供我们用来作画,之后绘制的操作会放到onDraw()方法当中
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
public class FirstView extends View {
public FirstView(Context context) {
super(context);
}
public FirstView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public FirstView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public FirstView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas); // 提供了Canvas,借助Canvas进行绘制
}
}
接下来,像添加其他控件一样将这个新创建的View放到xml布局中,比如直接放在启动默认的Activity的布局当中,比较方便显示,宽高先配置为match_parent,撑满父布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!--将自定义控件添加到布局中-->
<com.minos.customviewdemo.customview.FirstView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
此时由于还没有进行绘制的操作,因而即使运行,页面也没有任何东西进行呈现,画布是一片“空白”
3. 画笔——Paint
就像现实生活中进行绘画一样,需要有什么作为画笔,能够进行线条的勾勒,颜色的填充
在Android当中,充当“画笔”这个角色的是Paint类
The Paint class holds the style and color information about how to draw geometries, text and bitmaps.
关于Paint类,官方文档对此简单地一句话带过,其中最主要的就是Paint类包含了样式和颜色信息,对比日常绘画,其实也会发现二者很相似
画笔的笔头有粗细之分,还有些是圆的,有些是方的,这些都可以对应于样式
要想创作出丰富多彩的画面,需要有各种颜色的画笔,这便是颜色
接下来具体看下代码层面的方法(常用的):
3.1. 颜色
setColor()方法就是用来设置画笔的颜色的,这个代表颜色的int类型参数是包含透明度的,颜色是采用ARGB的形式进行表示的,A代表透明度,R代表红色分量,G代表绿色分量,B代表蓝色分量,由于int是32位,因此每个分量占用8位,8位放到二进制表示的便是0~255的范围
透明度0~255,数字越大,不透明度越高
而RGB部分,前端基本功,可以使用AS的这个小工具帮帮忙
public class FirstView extends View {
private Paint mPaint = new Paint(); // 创建对象
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(0xFFFF0000); // 红色画笔
canvas.drawCircle(0, 0, 300, mPaint); // 画个圆,圆心在屏幕左上角
}
}
由于坐标原点在屏幕左上角,因此只显示了1/4的圆,这是调用了Canvas的draw系列方法中的drawCircle()方法画了一个圆(先不深究),主要目的还是看一下Paint的setColor()实际使用的效果,就像是用红色的彩笔去填充这样一个形状
3.2. 样式
虽然setStyle()直接翻译就是“设置样式”,但是感觉并不具体,样式还是太宽泛了
其实该方法主要影响的还是对于图形的填充
该方法接收的类型为Paint.Style,这是一个枚举类,主要有这3个变体,对应的注释中其实已经描述得比较详细了
FILL
FILL变体会填充对应的几何图形或文字,与线条相关的设置将会被忽略
public class FirstView extends View {
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(0xFFFF0000);
mPaint.setStyle(Paint.Style.FILL); // 填充模式
canvas.drawCircle(100, 100, 100, mPaint);
}
}
其实默认情况下也会是FILL(除画位图)
STROKE
STROKE则是对应描边,因此绘制出来的仅仅是轮廓、线框
public class FirstView extends View {
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(0xFFFF0000);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(100, 100, 100, mPaint);
}
}
FILL_AND_STROKE
最后一种看名称就知道是前面二者的结合,同时拥有线框和填充,就是在第二种的基础上顺带涂上中心的颜色,看上去和第一种差不多,仅仅多出了描边的宽度
3.3.线宽
既然在样式当中提及了STROKE,那么很自然地,会有用于描边粗细的设置,这就像是不同粗细的笔一样,在绘制画面的时候,有着明确的分工
需要注意的是,setStroke()在FILL下不生效,因为该模式没有线,因此没有意义,单位是像素
public class FirstView extends View {
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(0xFFFF0000);
mPaint.setStyle(Paint.Style.STROKE); // 画线模式,确保设置线宽能够生效
mPaint.setStrokeWidth(10); // 设置线宽,10px
canvas.drawCircle(100, 100, 100, mPaint);
}
}
4. 画布——Canvas
Canvas就如其名称一般,扮演着“画布”的角色,作为绘画内容的载体
The Canvas class holds the "draw" calls. To draw something, you need 4 basic components: A Bitmap to hold the pixels, a Canvas to host the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect, Path, text, Bitmap), and a paint (to describe the colors and styles for the drawing).
其实之前也有稍稍提到,Canvas持有一系列draw开头的方法,就像一个工具集,负责各种执行各种绘制工作,而我们负责去调用,让它亲自执笔
4.1. 面——drawColor
主要使用drawColor()方法对当前的区域涂上统一的背景色
public class FirstView extends View {
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(0xFF000000); // 黑色画布
}
}
4.2. 线——drawLine
drawLine()根据给定的起止点坐标绘制一条线段,并且需要画笔Paint对象控制其样式
public class FirstView extends View {
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(0xFF880066); // 颜色
mPaint.setStrokeWidth(10); // 线宽
canvas.drawLine(0, 0, 300, 400, mPaint); // 左上角(0,0)到(300,400)的线段
}
}
由于线本身就是线框的样式,因此修改Paint的Style毫无作用(FILL和STROKE都一样),只能修改StrokeWidth更改线的宽度
4.3. 点——drawPoint
在指定的坐标位置画一个点
public class FirstView extends View {
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(0xFF880066);
mPaint.setStrokeWidth(50);
mPaint.setStrokeCap(Paint.Cap.ROUND); // 圆形笔头
canvas.drawPoint(100, 100, mPaint); // (100, 100)处画个点
}
}
在使用drawPoint()时,也是和drawLine()类似,Style也会被忽略,使用StrokeWidth可以修改宽度,另外StrokeCap表示笔头,圆形笔头点一下是圆,方形笔头就是方
5. 基本技法
首先,在Android当中,进行绘制使用的参考系都是以左上角位置作为坐标原点,向右为x轴正方向,向下为y轴正方向
5.1. 平移——translate
public class FirstView extends View {
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(0xFF880066);
mPaint.setStrokeWidth(50);
mPaint.setStrokeCap(Paint.Cap.ROUND);
canvas.drawPoint(100, 100, mPaint);
canvas.translate(200, 200); // 向右下平移,坐标轴跟随移动
mPaint.setColor(0xFF000066);
canvas.drawPoint(100, 100, mPaint);
}
}
坐标系与画布是绑定的,一旦画布移动坐标轴会跟随移动,这就像是平时绘画时,一边画一边移动纸一样,而屏幕是桌子,桌子是不动的,但我们可以移动纸张,方便我们作画
5.2. 保存与恢复——save & restore
save()和restore()是成对出现的,利用栈保存内容,然后进行恢复,防止对于画布的变换操作影响原来的画面元素
一种常用的场景就是绘画的时候,发现某个元素计算距离比较麻烦,那么可以将坐标系,进行平移,放到中心,画完后再恢复原来的位置
比方说现在有一个在(600, 200),半径为400的圆,现在我想画它的同心圆
一种方法是直接找到圆心(600,200),再画一个
另一种是save(),把圆心放到原点,画圆,再restore()
public class FirstView extends View {
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(0xFF00FFFF);
canvas.drawCircle(600, 200, 400, mPaint);
}
}
public class FirstView extends View {
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(0xFF00FFFF);
canvas.drawCircle(600, 200, 400, mPaint);
canvas.save();
canvas.translate(600, 200); // 坐标原点右移,到了圆心
mPaint.setColor(Color.MAGENTA);
canvas.drawCircle(0, 0, 200, mPaint);
canvas.restore();
}
}
似乎从代码量上更多了呢,但是其实思路会很明确,也符合平时画画的习惯,就像PS里的抓手工具,我们会把要画的东西放到中央
当视图复杂后,会涉及各种计算,而使用平移、保存、恢复这三板斧,可以免去很多距离的计算,每次都从原点作画,能够减少出错的概率