开发应用中图片的使用是必不可少的,Android系统提供了丰富的图片支持功能。我们除了可以使Drawable资源库,还可以使用Bitmap、Picture类去创建图片,也可以使用Canvas、Paint、Path类等去绘制我们满意的图片。在自定义控件时,这些API使用尤为常见。因此,小编觉得有必要简单的做个小总结。
那就先从Bitmap和BitmapFactory开始吧
Bitmap和BitmapFactory
BitmapFactory
Bitmap代表一张位图。BitmapDrawable中封装的图片就是一个Bitmap对象。
可以调用BitmapDrawable的构造器将一个Bitmap对象封装成为一个BitmapDrawable对象,方法如下:
BitmapDrawable drawable = new BitmapDrawable(bitmap);
如果想要获取BitmapDrawable中封装的Bitmap对象,可以采用如下方法:
Bitmap bitmap = drawable.getBitmap();
BitmapFactory中提供了多个方法来解析、创建Bitmap的对象:
decodeByteArray(byte[] data, int offset, int length) :将制定字节数组从offset字节开始length长度的字节解析成Bitmap对象。
decodeFile(String pathName) :将指定路径下的文件解析成Bitmap对象。
decodeFileDescriptor(FileDescriptor fd) :将FileDescriptor对应文件中解析,创建Bitmap对象。
decodeResource(Resources res, int id) :将给定的资源ID解析成Bitmap对象。
decodeStream(InputStream is) :将指定的字节流解析成Bitmap对象。
另外,需要注意的是Android为Bitmap提供了两种方法判断它是否已经回收,以及强制Bitmap回收自己。分别为Boolean isRecycled() 和void recycle()方法
Canvas
Canvas, 我们称之为“画布“,主要适用于绘制View的。 Canvas中提供了大量绘制图形的方法:
绘制扇形:
drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint): 第一个参数RectF对象,指定扇形的区域;二个参数是起始角度;第三个参数是旋转角度,顺时针旋转;第四个参数是是否填充,true为填充,false为不填充,也就是为一条弧线;第五个参数是绘制图形的画笔对象Paint。
RectF:通过RectF(float left, float top, float right, float bottom)构造器创建RectF对象。
Paint:是绘制所有图形所用到的一个画笔,我们在稍后讲解。
drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) :这个是将扇形区域左,上,右,下边的坐标直接输入,而不是通过RectF对象。其他参数同上。
绘制圆形:
drawCircle(float cx, float cy, float radius, Paint paint): 第一、二个参数是指圆形的x, y坐标; 第三个参数是半径; 第四个参数是画笔Paint对象。
绘制直线:
drawLine(float startX, float startY, float stopX, float stopY, Paint paint) :两点确定一条直线,第一、二参数是起始点的坐标;第三、四参数是结束点的坐标;第五个参数画笔Paint对象。
drawLines(float[] pts, Paint paint) :多个点确定一条直线,第一个参数是点的数组;第二个参数是画笔Paint对象。
drawLines(float[] pts, int offset, int count, Paint paint)
绘制椭圆:
drawOval(float left, float top, float right, float bottom, Paint paint):前四个参数是椭圆的左,上,右,下边的坐标,第五个是画笔Paint对象。
drawOval(RectF oval, Paint paint):第一个参数是RectF对象, 第二个参数是画笔Paint对象。
绘制矩形:
drawRect(RectF rect, Paint paint) :第一个参数是RectF对象, 第二个参数是画笔Paint对象。
绘制点:
drawPoint(float x, float y, Paint paint) :第一、二个参数点的坐标,第三个参数为Paint对象。
渲染文本:
drawText(String text, float x, floaty, Paint paint)
drawText(CharSequence text, int start, int end, float x, float y, Paint paint)
drawText(char[] text, int index, int count, float x, float y, Paint paint)
drawText(String text, int start, int end, float x, float y, Paint paint)
Canvas中还给我们提供了很多绘制其他图形的方法,这里我们不在一一列举。我们来看一下Paint”画笔“。
Paint:
Paint是用于绘制的画笔,Canvas就像是我们的画纸,我们需要笔才可以完成一整幅图。Paint中为我们提供了很多设置的方法(我们这里只列举常用的方法):
setARGB(int a, int r, int g, int b) :设置 Paint对象颜色,参数一为alpha透明值
setAlpha(int a) :设置alpha不透明度,范围为0~255
setAntiAlias(boolean aa) :是否抗锯齿,这个一般是都要设置的。
setColor(int color) :设置颜色,这里Android内部定义的有Color类包含了一些常见颜色定义
setTextScaleX(float scaleX) :设置文本缩放倍数,1.0f为原始
setTextSize(float textSize) :设置字体大小
setUnderlineText(booleanunderlineText) :设置下划线
setStrokeCap(Paint.Cap cap) :当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式 Cap.ROUND,或方形样式Cap.SQUARE
setSrokeJoin(Paint.Join join) :设置绘制时各图形的结合方式,如平滑效果等
Path
Path, 轨迹,路径。Path可以沿着多个点绘制一条路径, 在Canvas中可以根据Path绘制不同的图形。
我们在使用Path绘制路径,一般要使用到以下几个方法:
moveTo(float x, float y): 移动到(x, y)坐标点。绘制路径时,路径的第一个点一般我们通过moveTo()来决定,否则默认为(0, 0)点。
lineTo(float x, float y): 从当前点绘制直线到(x, y)点。这个与moveTo()不同,moveTo()是指跳转到(x, y)点,而不绘制连线。
close(): 将路径封闭。举例来说,我们绘制一个三角形,三角形的三个点是(0, 0,),(100, 0),(0, 100)我们使用lineTo将(0, 0,),(100, 0)连接,(100, 0),(0, 100)连接,这样还差(0, 0,),(0, 100)之间的连线,这时我们可以直接调用close()方法,这样就会直接将图形封闭构成三角形。
quadTo(float x1, float y1, float x2, float y2): 绘制贝塞尔曲线,贝塞尔曲线是由三个点控制的:起始点,终止点,控制点。在该方法中,前两个参数是控制点的坐标,后两个参数是终止点坐标。
Path中我们可以通过点来绘制路径也可以通过addXXX()方法来绘制,Path中给我们提供了很多这样的方法来添加不同的路径:
方法 | 用途 |
---|---|
addArc(RectF oval, float startAngle, float sweepAngle) | 添加圆弧轨迹 |
addCircle(float x, float y, float radius, Path.Direction dir) | 添加圆形轨迹 |
addOval(float left, float top, float right, float bottom, Path.Direction dir) | 添加椭圆轨迹 |
addRect(float left, float top, float right, float bottom, Path.Direction dir) | 添加矩形轨迹 |
addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Path.Direction dir) | 添加椭圆角矩形轨迹 |
下面主要通过两个简单的小例子加深对知识点的理解。当然,在文章开始我们已经提到这些Api在自定义控件中比较常见,下面也涉及到一些自定义View方面的知识。(~!~如果时间希望能够系统的学习下自定义View以及ViewGroup方面的知识以及写一篇这方面的总结)
自定义时钟的Demo主要用到了Canvas以及Paint方面的知识,来看看代码吧:
1
2
3
4
5
6
7
8
|
public
class
MainActivity
extends
AppCompatActivity {
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
|
主活动中仅仅加载了一个布局。
1
2
3
4
|
<linearlayout android:layout_height=
"match_parent"
android:layout_width=
"match_parent"
android:paddingbottom=
"@dimen/activity_vertical_margin"
android:paddingleft=
"@dimen/activity_horizontal_margin"
android:paddingright=
"@dimen/activity_horizontal_margin"
android:paddingtop=
"@dimen/activity_vertical_margin"
tools:context=
".MainActivity"
xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:tools=
"http://schemas.android.com/tools"
>
<com.example.zjn.clock.myview android:layout_height=
"match_parent"
android:layout_width=
"match_parent"
>
</com.example.zjn.clock.myview></linearlayout>
|
在布局文件中放置我们自定义View,比较简单不需要太多介绍。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
|
public
class
MyView
extends
View{
private
int
width;
//设置高
private
int
height;
//设置高
private
Paint mPaintLine;
//定义一个绘制直线的画笔
private
Paint mPaintSecondLine;
//定义一个绘制直线的画笔
private
Paint mPaintInterCircle;
//定义一个绘制圆的画笔
private
Paint mPaintOutSideCircle;
//定义一个绘制圆的画笔
private
Paint mPaintText;
//定义一个绘制文字的画笔
private
Calendar mCalendar;
//创建一个时间类
private
static
final
int
NEED_INVALIDATE =
0X6666
;
public
MyView(Context context) {
super
(context);
}
public
MyView(Context context, AttributeSet attrs) {
super
(context, attrs);
//初始化画直线的画笔
mPaintLine =
new
Paint();
mPaintLine.setAntiAlias(
true
);
//消除锯齿
mPaintLine.setColor(Color.GRAY);
//设置画笔颜色
mPaintLine.setStyle(Paint.Style.STROKE);
//设置为空心
mPaintLine.setStrokeWidth(
10
);
//设置宽度// 初始化秒针的画笔
mPaintSecondLine =
new
Paint();
mPaintSecondLine.setAntiAlias(
true
);
//消除锯齿
mPaintSecondLine.setColor(Color.GRAY);
//设置画笔颜色
mPaintSecondLine.setStyle(Paint.Style.STROKE);
//设置为空心
mPaintSecondLine.setStrokeWidth(
7
);
//设置宽度//初始化内圆的画笔
mPaintInterCircle =
new
Paint();
mPaintInterCircle.setAntiAlias(
true
);
//消除锯齿
mPaintInterCircle.setColor(Color.BLACK);
mPaintInterCircle.setStyle(Paint.Style.STROKE);
//设置为空心
mPaintInterCircle.setStrokeWidth(
5
);
//初始化外圆的画笔
mPaintOutSideCircle =
new
Paint();
mPaintOutSideCircle.setAntiAlias(
true
);
//消除锯齿
mPaintOutSideCircle.setColor(Color.BLACK);
mPaintOutSideCircle.setStyle(Paint.Style.STROKE);
//设置为空心
mPaintOutSideCircle.setStrokeWidth(
10
);
//绘制文字的画笔
mPaintText =
new
Paint();
mPaintText.setAntiAlias(
true
);
//消除锯齿
mPaintText.setColor(Color.GRAY);
mPaintText.setStyle(Paint.Style.STROKE);
//设置为空心
mPaintText.setTextAlign(Paint.Align.CENTER);
mPaintText.setTextSize(
40
);
mPaintText.setStrokeWidth(
6
);
//初始化日历
mCalendar = Calendar.getInstance();
//发送一个消息给UI主线程
Handler handler =
new
Handler() {
@Override
public
void
handleMessage(Message msg) {
super
.handleMessage(msg);
switch
(msg.what) {
case
NEED_INVALIDATE:
//跟新时间
mCalendar = Calendar.getInstance();
invalidate();
sendEmptyMessageDelayed(NEED_INVALIDATE,
1000
);
break
;
}
}
};
handler.sendEmptyMessageDelayed(NEED_INVALIDATE,
2000
);
}
@Override
protected
void
onMeasure(
int
widthMeasureSpec,
int
heightMeasureSpec) {
super
.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
setMeasuredDimension(width, height);
//设置宽和高
}
@Override
protected
void
onDraw(Canvas canvas) {
super
.onDraw(canvas);
// 主线程自动调用
canvas.drawCircle(width /
2
, height /
2
,
300
, mPaintInterCircle);
canvas.drawCircle(width /
2
, height /
2
,
320
, mPaintOutSideCircle);
for
(
int
i =
1
; i <=
12
; i++) {
canvas.save();
//保存当前状态
canvas.rotate(
360
/
12
* i, width /
2
, height /
2
);
//根据点width/2,height/2旋转
canvas.drawLine(width /
2
, height /
2
-
300
, width /
2
, height /
2
-
270
, mPaintLine);
canvas.drawText( + i, width /
2
, height /
2
-
240
, mPaintText);
canvas.restore();
//回到save()方法保存的状态
}
//绘制分针
int
minute= mCalendar.get(Calendar.MINUTE);
float
minuteDegree = minute / 60f *
360
;
canvas.save();
canvas.rotate(minuteDegree, width /
2
, height /
2
);
canvas.drawLine(width /
2
, height /
2
-
200
, width /
2
, height /
2
+
40
, mPaintLine);
canvas.restore();
//绘制时针
int
hour= mCalendar.get(Calendar.HOUR);
float
hourDegree = (hour *
60
+ minute);
//(12f*60)*360;
canvas.save();
canvas.rotate(hourDegree, width /
2
, height /
2
);
canvas.drawLine(width /
2
, height /
2
-
170
, width /
2
, height /
2
+
30
, mPaintLine);
canvas.restore();
//绘制秒针
int
second = mCalendar.get(Calendar.SECOND);
float
secondDegree = second *
6
;
//一秒是6度。
canvas.save();
canvas.rotate(secondDegree, width /
2
, height /
2
);
canvas.drawLine(width /
2
, height /
2
-
220
, width /
2
, height /
2
+
50
, mPaintSecondLine);
canvas.restore();
}
}
|
onDraw是UI主线程不断调用重绘界面的,因此我们需要使用到Handler,通过发送一个消息给Handler对象,让Handler对象在每一秒重绘一次MyView控件。这里重绘不能调用onDraw()方法额,而要调用的是invalidate()方法,invalidate()方法中调用了onDraw()方法。
下面马上来看看效果吧:
采用双缓冲实现画图板用到了以上提到的各类知识,主要原理是:当程序需要在指定View上进行绘制时,程序并不直接绘制到该View组建上,而是先绘制到内存中的一个Bitmap图片上,等内存中的Bitmap绘制好之后,再一次性将Bitmap绘制到View上面,还是直接看代码吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public
class
MainActivity
extends
AppCompatActivity {
DrawView drawView;
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
LinearLayout line =
new
LinearLayout(
this
);
DisplayMetrics displayMetrics =
new
DisplayMetrics();
getWindowManager().getDefaultDisplay().getRealMetrics(displayMetrics);
drawView =
new
DrawView(
this
, displayMetrics.widthPixels,displayMetrics.heightPixels);
line.addView(drawView);
setContentView(line);
}
}
|
主活动中主要获取了穿件的宽和高,同是创建DrawView,让DrawView的宽和高保持与该Activity相同。来看看DrawView中的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
public
class
DrawView
extends
View{
float
prex;
float
prey;
private
Path path;
public
Paint paint =
null
;
Bitmap CacheBitmap =
null
;
Canvas CacheCanvas =
null
;
public
DrawView(Context context,
int
widthPixels,
int
heightPixels) {
super
(context);
CacheBitmap = Bitmap.createBitmap(widthPixels,heightPixels,Bitmap.Config.ARGB_8888);
CacheCanvas =
new
Canvas();
path =
new
Path();
CacheCanvas.setBitmap(CacheBitmap);
paint =
new
Paint(Paint.DITHER_FLAG);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(
5
);
paint.setAntiAlias(
true
);
paint.setDither(
true
);
}
@Override
public
boolean
onTouchEvent(MotionEvent event) {
float
x = event.getX();
float
y = event.getY();
switch
(event.getAction())
{
case
MotionEvent.ACTION_DOWN:
path.moveTo(x,y);
prex = x;
prey = y;
break
;
case
MotionEvent.ACTION_MOVE:
path.quadTo(prex, prey, x, y);
prex = x;
prey = y;
break
;
case
MotionEvent.ACTION_UP:
CacheCanvas.drawPath(path,paint);
path.reset();
break
;
}
invalidate();
return
true
;
}
@Override
public
void
onDraw(Canvas canvas) {
Paint bmPaint =
new
Paint();
canvas.drawBitmap(CacheBitmap,
0
,
0
,bmPaint);
canvas.drawPath(path,paint);
}
}
|
为了让view绘制的图形发生改变,需要程序记住一些状态数据:采用变量或者采用事件监听器,在监听器中修改这些数据。不管使用哪种方式,每次VIew组件上的图形状态发生改变时都应该通知View组件重新调用OnDraw()方法重绘该控件,通知可以调用invalidate()。