Android-自定义 View 之绘图基础

释放双眼,带上耳机,听听看~!

1.1 基本图形绘制

1.1.1 概述

画图需要两个工具:纸和笔。在 Android 中,Paint 类就是画笔,而 Canvas 类就是纸,在这里叫作画布。

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

示例:

效果图
public class TestView extends View {

    private Paint mPaint;

    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        // 设置画笔
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(50);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 画圆
        canvas.drawCircle(200, 200, 150, mPaint);
    }
}

直接在主布局中使用自定义控件

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.xxt.xtest.TestView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>

在 onDraw() 函数中不能创建变量。因为当需要重绘时就会调用 onDraw() 函数,这样会导致变量一直被重复创建,会引起频繁的程序 GC (回收内存),进而引起程序卡顿。一般在构造函数中创建变量。

1.1.2 画笔的基本设置

1. setAntiAlias()
void setAntiAlias(boolean aa)

表示是否打开抗锯齿功能。抗锯齿是依赖算法的,一般在绘制不规则的图形时使用,比如圆形、文字等。在绘制棱角分明的图像时,比如一个矩形、一张位图,是不需要打开抗锯齿功能的。

示例:

Anti Alias & No Anti Alias
public class TestView extends View {

    private Paint mPaint1, mPaint2;

    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        mPaint1 = new Paint();
        mPaint1.setColor(Color.RED);
        mPaint1.setStyle(Paint.Style.FILL);
        mPaint1.setAntiAlias(true);
        mPaint1.setStrokeWidth(50);

        mPaint2 = new Paint();
        mPaint2.setColor(Color.RED);
        mPaint2.setStyle(Paint.Style.FILL);
        mPaint2.setStrokeWidth(50);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawCircle(200, 200, 150, mPaint1);
        canvas.drawCircle(540, 200, 150, mPaint2);
    }
}
2. setColor()
void setColor(int color)

设置画笔颜色。一个颜色值是由红、绿、蓝三色合成出来的,所以,参数 color 只能取 8 位的 0xAARRGGBB 样式颜色值。其中:

  • A 代表透明度(Alpha),取值范围是 0~255(对应十六进制的 0x00~0xFF),取值越小,透明度越高,图像也就越透明。当取 0 时,图像完全不可见。
  • R 代表红色值(Red),取值范围是 0~255(对应十六进制的 0x00~0xFF),取值越小,红色越少。当取 0 时,表示红色完全不可见;当取 255 时,红色完全显示。
  • G 代表绿色值(Green),取值范围是 0~255(对应十六进制的 0x00~0xFF),取值越小,绿色越少。当取 0 时,表示绿色完全不可见;当取 255 时,绿色完全显示。
  • B 代表蓝色值(Blue),取值范围是 0~255(对应十六进制的 0x00~0xFF),取值越小,蓝色越少。当取 0 时,表示蓝色完全不可见;当取 255 时,蓝色完全显示。

示例:

效果图
public class TestView extends View {

    private Paint mPaint1, mPaint2;

    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        mPaint1 = generatePaint(Color.RED, Paint.Style.FILL, 50);
        mPaint2 = generatePaint(0x7EFFFF00, Paint.Style.FILL, 50);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawCircle(200, 200, 150, mPaint1);
        canvas.drawCircle(200, 200, 100, mPaint2);
    }

    private Paint generatePaint(int color, Paint.Style style, int strokeWidth) {
        Paint paint = new Paint();
        paint.setColor(color);
        paint.setStyle(style);
        paint.setStrokeWidth(strokeWidth);
        return paint;
    }
}
3. setStyle()
void setStyle(Style style)

设置填充样式,对于文字和几何图形都有效。style 的取值如下:

  • Paint.Style.FILL:仅填充内部。
  • Paint.Style.FILL_AND_STROKE:填充内部和描边。
  • Paint.Style.STROKE:仅描边。

示例:

依次是 FILL、STROKE、FILL_AND_STROKE
paintF = generatePaint(Color.RED, Paint.Style.FILL, 50);
paintS = generatePaint(Color.RED, Paint.Style.STROKE, 50);
paintFS = generatePaint(Color.RED, Paint.Style.FILL_AND_STROKE, 50);
paintLine = generatePaint(Color.BLACK, Paint.Style.STROKE, 1);

canvas.drawCircle(225, 300, 150, paintF);
canvas.drawCircle(600, 300, 150, paintS);
canvas.drawCircle(1000, 300, 150, paintFS);
canvas.drawLine(225, 150, 1000, 150, paintLine);
4. setStrokeWidth()
void setStrokeWidth(float width)

设置描边宽度值,单位是 px。当画笔的 Style 样式是 STROKE、FILL_AND_STROKE 时有效。当 Style 不起作用时,用于设置画笔宽度。

1.1.3 Canvas使用基础

1. 画布背景设置

有三种方法可以实现画布背景设置:

void drawColor(int color)
void drawARGB(int a, int r, int g, int b)
void drawRGB(int r, int g, int b)

drawColor() 函数中参数 color 的取值必须是 8 位的 0xAARRGGBB 样式颜色值。
drawARGB() 函数允许分别传入 A、R、G、B 分量,每个颜色值的取值范围都是 0~255(对应十六进制的 0x00~0xFF),内部会通过这些颜色分量构造出对应的颜色值。 drawRGB() 函数只允许传入 R、G、B 分量,透明度 Alpha 的值取 255。

示例:

将画布默认填充为紫色
canvas.drawColor(0xFFFF00FF);
canvas.drawARGB(0xFF, 0xFF, 0, 0xFF);
canvas.drawRGB(255, 0, 255);
2. 画直线
void drawLine(float startX, float startY, 
              float stopX, float stopY, Paint paint)

参数:

  • startX:起始点 X 坐标。
  • startY:起始点 Y 坐标。
  • stopX:终点 X 坐标。
  • stopY:终点 Y 坐标。

示例:

分别设置 Style:FILL、STROKE、FILL_AND_STROKE
mPaintF = generatePaint(Color.RED, Paint.Style.FILL, 50);
mPaintS = generatePaint(Color.RED, Paint.Style.STROKE, 50);
mPaintFS = generatePaint(Color.RED, Paint.Style.FILL_AND_STROKE, 50);

从效果图中可以明显看出,直线的粗细与画笔 Style 是没有关系的。当设置不同的 StrokeWidth 时,效果如下图所示。

width 分别是 :5、25、50
mPaintF = generatePaint(Color.RED, Paint.Style.FILL, 5);
mPaintS = generatePaint(Color.RED, Paint.Style.STROKE, 25);
mPaintFS = generatePaint(Color.RED, Paint.Style.FILL_AND_STROKE, 50);

可见,直线的粗细是与 paint.setStrokeWidth 有直接关系的。所以,一般而言,paint.setStrokeWidth 在 Style 起作用时,用于设置描边宽度;在 Style 不起作用时,用于设置画笔宽度。

3. 多条直线
void drawLines(float[] pts, Paint paint)

参数:
pts:点的集合。从下面的代码中可以看到,这里不是形成连接线,而是每两个点形成一条直线,pts 的组织方式 {x1,y1,x2,y2,x3,y3,…}。

示例:

效果图
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(5);
float []pts = {10,10,100,100,200,200,400,400};

// onDraw()
canvas.drawLines(pts, mPaint);

上面有 4 个点,分别是(10,10)、(100,100)、(200,200)和(400,400),两两连成一条直线。

另一个构造函数:

void drawLines(float[] pts, int offset, int count, Paint paint)

相比上面的构造函数,这里多了两个参数。

  • int offset:集合中跳过的数值个数。注意不是点的个数!一个点有两个数值。
  • int count:参与绘制的数值个数,指 pts 数组中数值的个数,而不是点的个数,因为一个点有两个数值。
float[] pts = {10,10,100,100,200,200,400,400};
canvas.drawLines(pts, 2, 4, paint);

表示从 pts 数组中索引为 2 的数字开始绘图,有 4 个数值参与绘图,也就是点(100,100) 和(200,200),所以效果图就是这两个点的连线。

4. 点
void drawPoint(float x, float y, Paint paint)

示例:

效果图
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(15);

canvas.drawPoint(100, 100, mPaint);

在(100,100)位置画一个点。同样,点的大小只与 paint.setStrokeWidth(width) 有关,而与 paint.setStyle 无关。

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

这几个参数的含义与多条直线中的参数含义相同。

  • float[] pts:点的合集,与上面的直线一致,样式为{x1,y1,x2,y2,x3,y3,…}。
  • int offset:集合中跳过的数值个数。注意不是点的个数!一个点有两个数值。
  • int count:参与绘制的数值个数,指 pts 数组中数值的个数,而不是点的个数。

示例:

效果图
private Paint mPaint;
float[] pts = {10,10, 100,100, 200,200, 400,400};

mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(15);

canvas.drawPoints(pts, 2, 4, mPaint);

同样是上面的 4 个点:(10,10)、(100,100)、(200,200)和(400,400),在 drawPoints()函数里跳过前两个数值,即第一个点的横、纵坐标,画出后面 4 个数值代表的点,即第二、三个点,第四个点没画。

6. 矩形工具类 RectF、Rect 概述

这两个类都是矩形工具类,根据 4 个点构造出一个矩形结构。RectF 与 Rect 中的方法、 成员变量完全一样,唯一不同的是:RectF 是用来保存 float 类型数值的矩形结构的;而 Rect 是用来保存 int 类型数值的矩形结构的。

// RectF 的构造函数有如下 4 个,但最常用的还是第二个
RectF()
RectF(float left, float top, float right, float bottom)
RectF(RectF r)
RectF(Rect r)

// Rect 的构造函数有如下 3 个
Rect()
Rect(int left, int top, int right, int bottom)
Rect(Rect r)

一般而言,要构造一个矩形结构,可以通过以下两种方法来实现。

// 方法一:直接构造
Rect rect = new Rect(10,10,100,100); 
// 方法二:间接构造
Rect rect = new Rect(); 
rect.set(10,10,100,100);
7. 矩形
void drawRect(float left, float top, float right, float bottom, Paint paint) 
void drawRect(RectF rect, Paint paint)
void drawRect(Rect r, Paint paint)

第一个函数是直接传入矩形的 4 个点来绘制矩形的;第二、三个函数是根据传入 RectF 或者 Rect 的矩形变量来指定所绘制的矩形的。

示例:

效果图
mPaintS = generatePaint(Color.RED, Paint.Style.STROKE, 15);
mPaintF = generatePaint(Color.RED, Paint.Style.FILL, 15);
mRect = new RectF(210f, 10f, 300f, 100f);

// 直接构造
canvas.drawRect(10, 10, 100, 100, mPaintS);
// 使用 RectF 构造
canvas.drawRect(mRect, mPaintF);
8. 圆角矩形
void drawRoundRect(RectF rect, float rx, float ry, Paint paint)

参数:

  • RectF rect:要绘制的矩形。
  • float rx:生成圆角的椭圆的 X 轴半径。
  • float ry:生成圆角的椭圆的 Y 轴半径。

示例:

圆角矩形效果图
mPaintF = generatePaint(Color.RED, Paint.Style.FILL, 15);
mRect = new RectF(100, 110, 300, 200);

canvas.drawRoundRect(mRect, 20, 10, mPaintF);
9. 圆形
void drawCircle(float cx, float cy, float radius, Paint paint)

参数:

  • float cx:圆心点的 X 轴坐标。
  • float cy:圆心点的 Y 轴坐标。
  • float radius:圆的半径。
10. 椭圆
void drawOval(RectF oval, Paint paint)

参数:
RectF oval:用来生成椭圆的矩形。

椭圆是根据矩形生成的,以矩形的长为椭圆的 X 轴,以矩形的宽为椭圆的 Y 轴。

示例:

使用矩形画椭圆
mPaintO = generatePaint(Color.RED, Paint.Style.STROKE, 5);
mPaintR = generatePaint(Color.BLUE, Paint.Style.STROKE, 5);
mRect = new RectF(100, 110, 300, 200);

canvas.drawRect(mRect, mPaintR);    // 画矩形
canvas.drawOval(mRect, mPaintO);    // 画椭圆
11. 弧
void drawArc(RectF oval, float startAngle, float sweepAngle, 
             boolean useCenter, Paint paint)

参数:

  • RectF oval:生成椭圆的矩形。
  • float startAngle:弧开始的角度,以 X 轴正方向为 0°。
  • float sweepAngle:弧持续的角度。
  • boolean useCenter:是否有弧的两边。为 true 时,表示带有两边;为 false 时,只有一条弧。

示例:

弧 效果图
mPaint = generatePaint(Color.RED, Paint.Style.STROKE, 5);
mRect1 = new RectF(100, 100, 200, 200);
mRect2 = new RectF(220, 100, 320, 200);

// 带弧的两边
canvas.drawArc(mRect1, 0, 90, true, mPaint);
// 不带弧的两边
canvas.drawArc(mRect2, 0, 90, false, mPaint);

上述代码中,仅将 paint 的样式设置为 FILL 。效果图如下:

填充样式的弧

当画笔设为填充模式时,填充区域只限于圆弧的起始点和终点所形成的区域。当带有两边时,会将两边及圆弧内部全部填充;如果没有两边,则只填充圆弧部分。

1.1.4 Rect与RectF

1.是否包含点、矩形

1)判断是否包含某个点

boolean contains(int x, int y)

该函数用于判断某个点是否在当前矩形中。如果在,则返回 true;如果不在,则返回 false。 参数(x,y)就是当前要判断的点的坐标。

示例:绘制一个灰色矩形,当手指在这个矩形区域内时,矩形变为红色。

效果图
public class TestView extends View {

    private float mX, mY;
    private Paint mPaint;
    private RectF mRect;

    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL);
        mRect = new RectF(100, 100, 500, 350);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mRect.contains(mX, mY)) {
            mPaint.setColor(Color.RED);
        } else {
            mPaint.setColor(Color.GRAY);
        }
        canvas.drawRect(mRect, mPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mX = event.getX();
        mY = event.getY();
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            invalidate();
            return true;
        } else if (event.getAction() == MotionEvent.ACTION_UP) {
            mX = -1;
            mY = -1;
        }
        invalidate();
        return super.onTouchEvent(event);
    }
}

上述代码注意两点:

  • 在 MotionEvent.ACTION_DOWN 中返回 true,因为当 MotionEvent. ACTION_DOWN 消息到来时,系统会判断返回值,当返回 true 时,表示当前控件已经在拦截 (消费)这个消息了,所以后续的 ACTION_MOVE、ACTION_UP 消息仍然继续传过来。如果返回 false(系统默认返回 false),就表示当前控件不需要这个消息,那么后续的 ACTION_MOVE、ACTION_UP 消息就不会再传到这个控件。
  • postInvalidate()和 invalidate()函数都是用来重绘控件的,区别是 invalidate()函数一定要在主线程中执行,否则就会报错;而 postInvalidate()函数则没有那么多讲究,它可以在任何线程中执行,而不必一定是主线程。因为在 postInvalidate()函数中就是利用 handler 给主线程发送刷新界面的消息来实现的,所以它可以在任何线程中执行而不会出错。而正因为它是通过发送消息来实现的,所以它的界面刷新速度可能没有直接调用 invalidate()函数那么快。确定当前线程是主线程的情况下,以 invalidate()函数为主。否则调用调用 postInvalidate()函数为好。因为 onTouchEvent()函数本来就是在主线程中的,所以使用 invalidate()函数更合适。

2)判断是否包含某个矩形

// 根据矩形的 4 个点或者一个 Rect 矩形对象来判断这个矩形是否在当前的矩形区域内。
boolean contains(float left, float top, float right, float bottom)
boolean contains(RectF r)
2.判断两个矩形是否相交

1)静态方法判断是否相交

static boolean intersects(Rect a, Rect b)

这是 Rect 类的一个静态方法,用来判断参数中所传入的两个 Rect 矩形是否相交,如果相交则返回 true,否则返回 false。
2)成员方法判断是否相交
判断当前 Rect 对象与其他矩形是否相交。

boolean intersects(int left, int top, int right, int bottom)

使用方法:

Rect rect_1 = new Rect(10,10,200,200);
boolean interset1_2 = rect_1.intersects(190, 10, 250, 200);

3)判断相交并返回结果

boolean intersect(int left, int top, int right, int bottom)
boolean intersect(Rect r)

这两个成员方法与 intersects()方法的区别是,不仅会返回是否相交的结果,而且会把相交部分的矩形赋给当前 Rect 对象。如果两个矩形不相交,则当前 Rect 对象的值不变。

3.合并

1)合并两个矩形
合并两个矩形的意思就是将两个矩形合并成一个矩形,即无论这两个矩形是否相交,取两个矩形最小左上角点作为结果矩形的左上角点,取两个矩形最大右下角点作为结果矩形的右下角点。如果要合并的两个矩形有一方为空,则将有值的一方作为最终结果。

public void union(int left, int top, int right, int bottom)
public void union(Rect r)

示例:

绿色、红色矩形合并成蓝色矩形
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mRect1 = new RectF(200, 100, 500, 300);
mRect2 = new RectF(100, 200, 300, 400);

// 画出右上的红色矩形
mPaint.setColor(Color.RED);
canvas.drawRect(mRect1, mPaint);
// 画出左下的绿色矩形
mPaint.setColor(Color.GREEN);
canvas.drawRect(mRect2, mPaint);
// 矩形合并
mRect1.union(mRect2);
// 画出合并后的矩形(蓝色部分)
mPaint.setColor(Color.BLUE);
canvas.drawRect(mRect1, mPaint);

2)合并矩形与某个点

public void union(int x, int y)

先判断当前矩形与目标合并点的关系。如果不相交,则根据目标点(x,y)的位置,将目标点设置为当前矩形的左上角点或者右下角点。如果当前矩形是一个空矩形,则最后的结果矩形为([0,0],[x,y]),即结果矩形的左上角点为[0,0],右下角点为[x,y]。

1.1.5 Color

Color 是 Android 中与颜色处理有关的类。

1. 常量颜色
@ColorInt public static final int BLACK       = 0xFF000000;
@ColorInt public static final int DKGRAY      = 0xFF444444;
@ColorInt public static final int GRAY        = 0xFF888888;
@ColorInt public static final int LTGRAY      = 0xFFCCCCCC;
@ColorInt public static final int WHITE       = 0xFFFFFFFF;
@ColorInt public static final int RED         = 0xFFFF0000;
@ColorInt public static final int GREEN       = 0xFF00FF00;
@ColorInt public static final int BLUE        = 0xFF0000FF;
@ColorInt public static final int YELLOW      = 0xFFFFFF00;
@ColorInt public static final int CYAN        = 0xFF00FFFF;
@ColorInt public static final int MAGENTA     = 0xFFFF00FF;
@ColorInt public static final int TRANSPARENT = 0;

Color.XXX 来直接使用这些颜色,比如红色:Color.RED。

2. 构造颜色
// 带有透明度的颜色
static int argb(int alpha, int red, int green, int blue)
// 不带透明度的颜色:alpha 值取 255
static int rgb(int red, int green, int blue)

argb() 函数的源码:

public static int argb(int alpha, int red, int green, int blue) {
    // 位运算,值得借鉴
    return (alpha << 24) | (red << 16) | (green << 8) | blue;
}
3. 提取颜色分量
static int alpha(int color)
static int red(int color)
static int green(int color)
static int blue(int color)

通过上面的 4 个函数提取出对应的 A、R、G、B 颜色分量。

// 得到的结果 green 的值就是 0x0F
int green = Color.green(0xFF000F00);

 

人已赞赏
Android文章

Android 屏幕适配:最全面的解决方案

2019-10-13 14:48:18

Android文章

Android本地分享

2019-10-13 15:18:19

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
有新消息 消息中心
搜索