不要让别人的无知断送了你的梦想,永远坚信你所坚信的。
我喜欢写博客之前用一句励志名言激励自己!
一个漂亮的UI交互,会给APP增色不少,而学习特效之前,有关graphics绘图的基础知识是必不可少的,下面就分几篇对涉及到的基础知识进行梳理。
Paint与Canvas
我们平时画图需要两个工具:纸和笔。在 Android 中,Paint 类就是画笔,而 Canvas 类就是纸,在这里叫作画布。所以,凡是跟画笔设置相关的,比如画笔大小、粗细、画笔颜色、透明度、字体的样式等,都在 Paint 类里设置;同样,凡是要画出成品的东西,比如圆形、矩形、文字等,都调用Canvas 类里的函数生成。
下面通过一个自定义控件的例子来看一下如何生成自定义控件,以及 Paint 和 Canvas 类的用法。我们Paint 就像魔法棒,Canvas 就想梦想成真的那一刻。我们用Canvas绘制一个不同世界!
(1)我们新建一个工程,然后写一个类继承自 View。
package com.wwj.lightview.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
public class BasisView extends View {
public BasisView(Context context) {
this(context, null);
}
public BasisView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BasisView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//木工粉墙用的油漆<==>画笔 <==> 水彩笔 <==> 钢笔 <==> 万能的水彩笔
Paint paint = new Paint();
paint.setColor(Color.GREEN); //设置绿色画笔颜色
paint.setStyle(Paint.Style.STROKE); //设置填充样式为描边
paint.setStrokeWidth(4); //设置画笔宽度,好大的一个画笔
////粗帆布<==>油画布<==>帆布鞋<==>画布 <==> 水晶瓶 <==>万能的显示屏
canvas.drawCircle(190, 200, 150, paint);
}
}
代码很简单,首先,写一个类派生(继承)自 View。派生自 View 表示当前是一个自定义的控件,类似 Button、TextView 这些控件都是派生自 View 的。
其次,重写 onDraw(Canvas canvas)函数。可以看到,在该函数中,参数是一个 Canvas对象,也就是当前控件的画布,所以我们只要调用 Canvas 的绘图函数,效果就可以直接显示在控件上了。画圆所用的画笔就是我们在这里指定的。
(2)使用自定义控件。
我们可以直接在主布局中使用自定义控件(activity_main.xml)。
可以看到,在 XML 中使用自定义控件时,需要使用完整的包名加类包的方式来引入。注意:这里的布局方式使用的全屏方式。后面会讲到如何给自定义控件使用 wrap_content 属性,目前我们全屏显示控件即可。
效果如下图所示。
从这里可以看到,只需要先创建一个派生自 View 的类,再重新在 onDraw()函数中设置Paint 并调用 Canvas 的一些绘图函数,就可以画出我们想要的图形。由此看来,自定义控件并不复杂。下面我们分别来看如何设置画笔,以及 Canvas 中常用的一些绘图函数。
画笔的基本设置
该函数的具体声明如下:
void setAntiAlias(boolean aa)
表示是否打开抗锯齿功能。抗锯齿是依赖算法的,一般在绘制不规则的图形时使用,比如圆形、文字等。在绘制棱角分明的图像时,比如一个矩形、一张位图,是不需要打开抗锯齿功能的。在打开抗锯齿功能的情况下,所绘图像可以产生平滑的边缘。
paint.setAntiAlias(true); //打开抗锯齿功能
该函数的作用是设置画笔颜色,完整的函数声明如下:
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 时,蓝色完全显示。
比如 0xFFFF0000 就表示大红色。因为透明度是 255,表示完全不透明,红色取全量值 255,其他色值全取 0,表示颜色中只有红色;当然,如果我们不需要那么红,则可以适当减少红色值,比如 0xFF0F0000 就会显示弱红色。当表示黄色时,由于黄色是由红色和绿色合成的,所以 0xFFFFFF00 就表示纯黄色。当然,如果我们需要让黄色带有一部分透明度,以便显示出所画图像底层图像,则可以适当减少透明度值,比如 0xABFFFF00;当透明度减少到 0 时,任何颜色都是不可见的,也就是图像变成了全透明,比如 0x00FFFFFF,虽然有颜色值,但由于透明度是 0,所以整个颜色是不可见的。
其实,除手动组合颜色的方法以外,系统还提供了一个专门用来解析颜色的类——Color
(有关 Color 类的使用,我们稍后将在本章中提及)。
//木工粉墙用的油漆<==>画笔 <==> 水彩笔 <==> 钢笔 <==> 万能的水彩笔
Paint paint = new Paint();
paint.setColor(Color.GREEN); //设置绿色画笔颜色
paint.setStyle(Paint.Style.STROKE); //设置填充样式为描边
paint.setStrokeWidth(4); //设置画笔宽度,好大的一个画笔
////粗帆布<==>油画布<==>帆布鞋<==>画布 <==> 水晶瓶 <==>万能的显示屏
canvas.drawCircle(190, 200, 150, paint);
paint.setColor(Color.BLUE); //设置绿色画笔颜色
canvas.drawRect(40, 50, 340, 350, paint); // 四个顶点绘制一个矩形,距离左边40个像素,顶部50个像素,右边距离340个像素,底部距离350像素
我们在圆圈的外边绘制一个矩形
下面绘制一大一小两个圆,并且将这两个圆叠加起来,上方的圆半透明,代码如下:
Paint paint=new Paint();
paint.setColor(0xFFFF0000); //不透明的红色
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(50);
canvas.drawCircle(190, 200, 150, paint);
paint.setColor(0x7EFFFF00); //半透明的黄色
canvas.drawCircle(190, 200, 100, paint);
这里绘制了两个圆,第一个圆的颜色值是 0xFFFF0000,即不透明的红色,半径取 150px;第二个圆的颜色值是 0x7EFFFF00,即半透明的黄色,半径取 100px。效果如下图所示。
void setStyle(Style style)
该函数用于设置填充样式,对于文字和几何图形都有效。style 的取值如下。
- Paint.Style.FILL:仅填充内部,不包括描边
- Paint.Style.FILL_AND_STROKE:填充内部和描边。
- Paint.Style.STROKE:仅描边。
设置填充内部及描边的样式代码如下:
Paint paint=new Paint();
paint.setColor(0xFFFF0000);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setStrokeWidth(50);
canvas.drawCircle(190, 200, 150, paint);
明显可见,FILL_AND_STROKE是FILL和STROKE叠加在一起显示的结果,FILL_AND_STROKE 比 FILL 多了一个描边的宽度。
void setStrokeWidth(float width)
用于设置描边宽度值,单位是 px。当水彩笔的 Style 样式是 STROKE 和 FILL_AND_STROKE时有效。
Canvas使用基础
有三种方法可以实现画布背景设置
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 位4字节的 0xAARRGGBB 样式颜色值。drawARGB()函数允许分别传入 A、R、G、B 分量,每个颜色值的取值范围都是 0~255(对应十六进制的 0x00~0xFF),内部会通过这些颜色分量构造出对应的颜色值。drawRGB()函数只允许传入 R、G、B 分量,透明度 Alpha 的值取 255。比如,将画布默认填充为紫色。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRGB(255,0,255);
}
这里使用的是十进制的数值。当然,使用十六进制的数值会更直观。
drawRGB(0xFF,0x00,0xFF);
这个颜色值对应的另外两个函数的写法如下:
canvas.drawColor(0xFFFF00FF);
canvas.drawARGB(0xFF,0xFF,0,0xFF);
运行结果
画直线(两个点确定一条直线)
void drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
参数:
- startX:起始点 X 坐标。
- startY:起始点 Y 坐标。
- stopX:终点 X 坐标。
- stopY:终点 Y 坐标。
canvas.drawColor(0xFFEA4335); //橙红色
Paint paint=new Paint();
paint.setColor(Color.GREEN);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setStrokeWidth(50); //水彩笔好大呀
canvas.drawLine(100, 100, 200, 200, paint);
paint.setStrokeWidth(5); //水彩笔弄小点
哎呀,妈呀一根小细线蹦出来了
可见,直线的粗细是与 paint.setStrokeWidth 有直接关系的。
一般而言,paint.setStrokeWidth
在 Style
起作用时,用于设置描边宽度;在 Style
不起作用时,用于设置画笔宽度。
void drawLines(float[] pts, Paint paint)
pts:点的集合。从下面的代码中可以看到,这里不是形成连接线,而是每两个点形成一条直线,pts 的组织方式为{x1,y1,x2,y2,x3,y3,…}很明显是...
Paint paint=new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(5);
float []pts={10,10,100,100,200,200,400,400};
canvas.drawLines(pts, paint);
上面有 4 个点,分别是(10,10)、(100,100)、(200,200)和(400,400),两两连成一条直线。
效果如下图所示。
void drawLines(float[] pts, int offset, int count, Paint paint)
相比上面的构造函数,这里多了两个参数。
- int offset:集合中跳过的数值个数。注意不是点的个数!一个点有两个数值。
- int count:参与绘制的数值个数,指 pts 数组中数值的个数,而不是点的个数,因为一个点有两个数值。
Paint paint=new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(5);
float []pts={10,10,100,100,200,200,400,400};
canvas.drawLines(pts,2,4,paint);
表示从 pts 数组中索引为 2 的数字开始绘图,有 4 个数值参与绘图,也就是点(100,100)和(200,200),所以效果图就是这两个点的连线。
瞧一瞧点
Paint paint=new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(15);
canvas.drawPoint(100, 100, paint);
代码很简单,就是在(100,100)位置画一个点。同样,点的大小只与 paint.setStrokeWidth(width)有关,而与 paint.setStyle 无关。
多个点
void drawPoints(float[] pts, Paint paint)
void drawPoints(float[] pts, int offset, int count, Paint paint)
这几个参数的含义与多条直线中的参数含义相同。
- float[] pts:点的合集,与上面的直线一致,样式为{x1,y1,x2,y2,x3,y3,…}。
- int offset:集合中跳过的数值个数。注意不是点的个数!一个点有两个数值。
- int count:参与绘制的数值个数,指 pts 数组中数值的个数,而不是点的个数。
Paint paint=new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(25);
float []pts={10,10,100,100,200,200,400,400};
canvas.drawPoints(pts, 2, 4, paint);
同样是上面的 4 个点:(10,10)、(100,100)、(200,200)和(400,400),在 drawPoints()函数里跳过前两个数值,即第一个点的横、纵坐标,画出后面 4 个数值代表的点,即第二、三个点,第四个点没画。效果如下图所示
矩形工具类 RectF、Rect 概述
这两个类都是矩形工具类,根据 4 个点构造出一个矩形结构。RectF 与 Rect 中的方法、成员变量完全一样,唯一不同的是:RectF 是用来保存 float 类型数值的矩形结构的;而 Rect是用来保存 int 类型数值的矩形结构的。我们先对比一下它们的构造函数。RectF 的构造函数有如下 4 个,但最常用的还是第二个,即根据 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)
可以看出,RectF 与 Rect 的构造函数基本相同,不同的只是 RectF 所保存的数值类型是float 类型,而 Rect 所保存的数值类型是 int 类型。
一般而言,要构造一个矩形结构,可以通过以下两种方法来实现。
//方法一:直接构造
Rect rect = new Rect(10,10,100,100);
//方法二:间接构造
Rect rect = new Rect();
rect.set(10,10,100,100);
在看完矩形的存储结构 RectF、Rect 以后,再来看看矩形的绘制方法。
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 的矩形变量来指定所绘制的矩形的。
Paint paint=new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(15);
//直接构造
canvas.drawRect(10, 10, 100, 100, paint);
//使用 RectF 构造
paint.setStyle(Paint.Style.FILL);
RectF rect = new RectF(210f, 10f, 300f, 100f);
canvas.drawRect(rect, paint);
这里绘制了两个同样大小的矩形。第一个直接使用 4 个点来绘制矩形,并且填充为描边类型;第二个通过 RectF 来绘制矩形,并且仅填充内容。
圆角矩形
void drawRoundRect(RectF rect, float rx, float ry, Paint paint)
- RectF rect:要绘制的矩形。
- float rx:生成圆角的椭圆的 X 轴半径。
-
float ry:生成圆角的椭圆的 Y 轴半径。
使用过标签的读者应该知道,Android 在生成矩形的圆角时,其实利用的是椭圆。shape标签 4 个角都可以设置生成圆角的椭圆,它生成圆角矩形的原理如下图所示。
可见,圆角矩形的圆角其实是由椭圆的一角形成的。与 shape 标签不同的是,drawRoundRect()函数不能针对每个角设置对应的椭圆,而只能统一设置 4 个角对应的椭圆。
Paint paint=new Paint();
paint.setColor(Color.RED);
paint.setStyle(Style.FILL);
paint.setStrokeWidth(15);
RectF rect = new RectF(100, 10, 300, 100);
canvas.drawRoundRect(rect, 20, 20, paint);
画圆
void drawCircle(float cx, float cy, float radius, Paint paint)
- float cx:圆心点的 X 轴坐标。
- float cy:圆心点的 Y 轴坐标。
-
float radius:圆的半径。
椭圆是根据矩形生成的,以矩形的长为椭圆的 X 轴,以矩形的宽为椭圆的 Y 轴。
Paint paint=new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
RectF rect = new RectF(100, 10, 300, 100);
canvas.drawRect(rect, paint);
paint.setColor(Color.GREEN);//更改画笔颜色
canvas.drawOval(rect, paint);//根据同一个矩形画椭圆
针对同一个矩形,先把它的矩形区域画出来,然后再把根据这个矩形生成的椭圆画出来,就可以很好地理解根据矩形所生成的椭圆与矩形的关系了,效果如下图所示。
弧是椭圆的一部分,而椭圆是根据矩形来生成的,所以弧也是根据矩形来生成的。
void drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter,Paint paint)
- RectF oval:生成椭圆的矩形
- float startAngle:弧开始的角度,以 X 轴正方向为 0°
- float sweepAngle:弧持续的角度
- boolean useCenter:是否有弧的两边。为 true 时,表示带有两边;为 false 时,只有一条弧。
将画笔设为描边
Paint paint=new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
//带两边
RectF rect1 = new RectF(10, 10, 100, 100);
canvas.drawArc(rect1, 0, 90, true, paint);
//不带两边
RectF rect2 = new RectF(110, 10, 200, 100);
canvas.drawArc(rect2, 0, 90, false, paint);
将画笔设为填充
从效果图中可以看出,当画笔设为填充模式时,填充区域只限于圆弧的起始点和终点所形成的区域。当带有两边时,会将两边及圆弧内部全部填充;如果没有两边,则只填充圆弧部分。
Rect与RectF
在讲完 Paint、Canvas 的基本使用方法以后,再回来看看上面所涉及的 Rect、RectF 的常用函数。由于 Rect、RectF 所具有的函数是相同的,只是保存的数值类型不同,所以下面就以Rect 为例来进行讲解。
判断是否包含某个点
boolean contains(int x, int y)
该函数用于判断某个点是否在当前矩形中。如果在,则返回 true;如果不在,则返回 false。参数(x,y)就是当前要判断的点的坐标。
利用这个函数,可以定义一个很简单的控件:绘制一个矩形,当手指在矩形区域内点击的时候,矩形边框是红色的;当手指在矩形区域外点击的时候,矩形边框是绿色的。
下面讲解一下具体的实现方法
package com.wwj.lightview.view;
import android.content.Context;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
public class extends View {
private int mX, mY;
private Paint mPaint;
private Rect mRect;
public RectPointView(Context context) {
this(context, null);
}
public RectPointView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RectPointView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mRect = new Rect(100, 10, 300, 100);
}
}
代码很简单,就是初始化 Paint,并指定一个矩形区域。
@Override
public boolean onTouchEvent(MotionEvent event) {
mX = (int) event.getX();
mY = (int) event.getY();
if (event.getAction() == MotionEvent.ACTION_DOWN) {
invalidate();
return true;
} else if (event.getAction() == MotionEvent.ACTION_UP) {
mX = -1;
mY = -1;
}
postInvalidate();
return super.onTouchEvent(event);
}
需要注意的是,因为我们需要判断当前手指是否在矩形区域内,以改变矩形的颜色,所以,我们必须首先获取到手指所在的位置,通过 event.getX()和 event.getY()函数就可以获取到当前手指在控件中的坐标。然后,在手指下按时,我们需要让屏幕重绘,如果当前用户点击位置在矩形区域内,则需要将矩形变成红色。
值得注意的是,在 MotionEvent.ACTION_DOWN 中返回 true,因为当 MotionEvent.ACTION_DOWN 消息到来时,系统会判断返回值,当返回 true 时,表示当前控件已经在拦截(消费)这个消息了,所以后续的ACTION_MOVE、ACTION_UP 消息仍然继续传过来。如果返回 false(系统默认返回 false),就表示当前控件不需要这个消息,那么后续的ACTION_MOVE、ACTION_UP 消息就不会再传到这个控件。
当用户手指弹起时,我们需要还原矩形的颜色(绿色),所以将 mX,mY 全部设置为负值。由于我们构造的矩形不包含坐标为负的点,所以也就还原了矩形的颜色。
最后,调用 postInvalidate()函数刷新控件屏幕,让控件重绘。
细心的读者会发现,在 ACTION_DOWN 消息到来时, invalidate()函数一定要在主线程中执行,否则就会报错;而 postInvalidate()函数则没有那么多讲究,它可以在任何线程中执行。因为在 postInvalidate()函数中就是利用 handler 给主线程发送刷新界面的消息来实现的,所以它可以在任何线程中执行而不会出错。而正因为它是通过发送消息来实现的,所以它的界面刷新速度可能没有直接调用 invalidate()函数那么快。
绘图
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mRect.contains(mX,mY)){
mPaint.setColor(Color.RED);
}else {
mPaint.setColor(Color.GREEN);
}
canvas.drawRect(mRect,mPaint);
}
如果当前手指触点在矩形区域内,则将矩形画为红色;否则,画为绿色。
在控件写好以后,就可以使用了,同样在布局中引入。
到这里,这个自定义控件就结束了,我们很容易地实现了一个与手指交互的自定义控件。可见,自定义控件并没有想象中那么难。
判断是否包含某个矩形
Boolean contains(int left, int top, int right, int bottom)
boolean contains(Rect r)
根据矩形的 4 个点或者一个 Rect 矩形对象来判断这个矩形是否在当前的矩形区域内。
静态方法判断是否相交
static boolean intersects(Rect a, Rect b)
这是 Rect 类的一个静态方法,用来判断参数中所传入的两个 Rect 矩形是否相交,如果相交则返回 true,否则返回 false。
下面用三种颜色画出三个矩形,然后判断矩形的相交情况。
Paint paint = new Paint();
paint.setStrokeWidth(4);
paint.setStyle(Paint.Style.STROKE);
Rect rect_1 = new Rect(10, 10, 200, 200);
Rect rect_2 = new Rect(190, 10, 250, 200);
Rect rect_3 = new Rect(10, 210, 200, 300);
//分别画出三个矩形
paint.setColor(Color.RED);
canvas.drawRect(rect_1, paint);
paint.setColor(Color.GREEN);
canvas.drawRect(rect_2, paint);
paint.setColor(Color.BLUE);
canvas.drawRect(rect_3, paint);
//判断是否相交
Boolean interset1_2 = Rect.intersects(rect_1, rect_2);
Boolean interset1_3 = Rect.intersects(rect_1, rect_3);
Log.d("tag", "rect_1&rect_2:" + interset1_2 + " rect_1&rect_3:" + interset1_3);
很明显,rect_1 与 rect_2 是相交的,rect_1 与 rect_3 是不相交的。
rect_1&rect_2:true rect_1&rect_3:false
还可以使用 Rect 类中自带的方法来判断当前 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);
判断相交并返回结果
boolean intersect(int left, int top, int right, int bottom)
boolean intersect(Rect r)
这两个成员方法与 intersects()方法的区别是,不仅会返回是否相交的结果,而且会把相交部分的矩形赋给当前 Rect 对象。如果两个矩形不相交,则当前 Rect 对象的值不变。
Rect rect_1 = new Rect(10, 10, 200, 200);
Boolean result_1 = rect_1.intersects(190, 10, 250, 200);
printResult(result_1,rect_1);
Boolean result_2 = rect_1.intersect(210, 10, 250, 200);
printResult(result_2, rect_1);
Boolean result_3 = rect_1.intersect(190, 10, 250, 200);
printResult(result_3,rect_1);
private void printResult(Boolean result, Rect rect) {
Log.d("qijian", rect.toShortString() + " result:" + result);
}
在上面的示例中,分别使用 intersects()和 intersect()函数来判断是否与指定矩形相交,并将判断相交的对象 rect_1 的边角打印出来。
日志如下:
2020-05-26 15:24:29.831 13374-13374/com.wwj.lightview D/qijian: [10,10][200,200] result:true
2020-05-26 15:24:29.831 13374-13374/com.wwj.lightview D/qijian: [10,10][200,200] result:false
2020-05-26 15:24:29.831 13374-13374/com.wwj.lightview D/qijian: [190,10][200,200] result:true
很明显,intersects()函数只是判断是否相交,并不会改变原矩形 rect_1 的值。当 intersect()函数判断的两个矩形不相交时,也不会改变 rect_1 的值;只有当两个矩形相交时,intersect()函数才会把结果赋给 rect_1。
合并两个矩形
合并两个矩形的意思就是将两个矩形合并成一个矩形,即无论这两个矩形是否相交,取两个矩形最小左上角点作为结果矩形的左上角点,取两个矩形最大右下角点作为结果矩形的右下角点。如果要合并的两个矩形有一方为空,则将有值的一方作为最终结果。
public void union(int left, int top, int right, int bottom)
public void union(Rect r)
同样根据参数是矩形的 4 个点还是一个 Rect 对象分为两个构造函数。合并的结果将会被赋给当前的 rect 变量。
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(4);
Rect rect_1 = new Rect(10, 10, 30, 30);
Rect rect_2 = new Rect(100, 100, 130, 130);
//分别画出源矩形 rect_1、rect_2
paint.setColor(Color.RED);
canvas.drawRect(rect_1, paint);
paint.setColor(Color.GREEN);
canvas.drawRect(rect_2, paint);
//画出合并之后的结果 rect_1
paint.setColor(Color.BLUE);
rect_1.union(rect_2);
canvas.drawRect(rect_1,paint);
从结果图中可以看出,两个小矩形在合并以后,取两个矩形的最小左上角点作为结果矩形的左上角点,取两个矩形的最大右下角点作为结果矩形的右下角点。
public void union(int x, int y)
先判断当前矩形与目标合并点的关系,如果不相交,则根据目标点(x,y)的位置,将目标点设置为当前矩形的左上角点或者右下角点。如果当前矩形是一个空矩形,则最后的结果矩形为([0,0],[x,y]),即结果矩形的左上角点为[0,0],右下角点为[x,y]。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Rect rect_1 = new Rect(10, 10, 20, 20);
rect_1.union(100,100);
printResult(rect_1);
rect_1 = new Rect();
rect_1.union(100,100);
printResult(rect_1);
}
private void printResult( Rect rect) {
Log.d("test", rect.toShortString());
}
在上述代码中,先将与指定矩形不相交的点与矩形合并,然后将矩形置空,再与同一个点相交,日志如下:
2020-05-26 15:38:55.980 13809-13809/? D/test: [10,10][100,100]
2020-05-26 15:38:55.980 13809-13809/? D/test: [0,0][100,100]
结果很容易理解,当与指定矩形合并时,根据当前点的位置,将该点设为矩形的右下角点;当点与空矩形相交时,结果为([0,0],[x,y])。
前面提到,除手动组合颜色的方法以外,系统还提供了一个专门用来解析颜色的类:Color。Color 是 Android 中与颜色处理有关的类。
首先,它定义了很多常量的颜色值,我们可以直接使用。
int BLACK
int BLUE
int CYAN
int DKGRAY
int GRAY
int GREEN
int LTGRAY
int MAGENTA
int RED
int TRANSPARENT
int WHITE
int YELLOW
可以通过 Color.XXX 来直接使用这些颜色,比如红色,在代码中可以直接使用 Color.RED。
带有透明度的颜色
static int argb(int alpha, int red, int green, int blue)
这个函数允许我们分别传入 A、R、G、B 4 个色彩分量,然后合并成一个色彩。其中,alpha、red、green、blue 4 个色彩分量的取值范围都是 0~255。
我们来看一下 argb()函数的具体实现源码,如下:
public static int argb(int alpha, int red, int green, int blue) {
return (alpha << 24) | (red << 16) | (green << 8) | blue;
}
其中,<<是向左位移符号,表示将指定的二进制数字向左位移多少位。比如,二进制的 110向左移一位之后的结果为 1100。
比如,alpha << 24 表示向左位移 24 位。我们知道,一个色彩值对应的取值范围是 0~255,所以每个色彩值对应二进制的 8 位,比如 alpha 的取值为 255,它的二进制表示就是 11111111(8 个 1)。所以,alpha << 24 的结果为 11111111 00000000 00000000 00000000。同样,如果我们构造的是一个白色,那么 A、R、G、B 各个分量的值都是 255,当它们对应位移之后的结果如下。
alpha << 24:11111111 00000000 00000000 00000000
red << 16:00000000 11111111 00000000 00000000
green << 8:00000000 00000000 11111111 00000000
blue:00000000 00000000 00000000 11111111
利用“|”(二进制的或运算)将各个二进制的值合并之后的结果就是 11111111 11111111 11111111 11111111,分别对应 A、R、G、B 分量。这就是各个分量最终合成对应颜色值的过程。在读代码时,有时会看到直接利用(alpha << 24) | (red << 16) | (green << 8) | blue 来合成对应颜色值的情况,其实跟我们使用 Color.argb()函数来合成的结果是一样的。
不带透明度的颜色
static int rgb(int red, int green, int blue)
其实跟上面的构造函数是一样的,只是不允许指定 alpha 值,alpha 值取 255(完全不透明)。
提取颜色分量
我们不仅能通过 Color 类来合并颜色分量,而且能从一个颜色中提取出指定的颜色分量。
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 颜色分量。
int green = Color.green(0xFF000F00);
得到的结果 green 的值就是 0x0F,很简单,就不再赘述了
本节主要讲解了有关 Paint 和 Canvas 的基本使用,并讲述了相关的 Rect 和 Color 类的用法。但我们在示例中创建 Paint 对象和其他对象时都是在 onDraw()函数中实现的,其实这在现实代码中是不被允许的。因为当需要重绘时就会调用 onDraw()函数,所以在 onDraw()函数中创建的变量会一直被重复创建,这样会引起频繁的程序 GC(回收内存),进而引起程序卡顿。这里之所以这样做,是因为可以提高代码的可读性。大家一定要记住,在 onDraw()函数中不能创建变量!一般在自定义控件的构造函数中创建变量,即在初始化时一次性创建。
下一篇文章