开发应用中图片的使用是必不可少的,Android系统提供了丰富的图片支持功能。我们除了可以使Drawable资源库,还可以使用Bitmap、Picture类去创建图片,也可以使用Canvas、Paint、Path类等去绘制我们满意的图片。在自定义控件时,这些API使用尤为常见。因此,小编觉得有必要简单的做个小总结。
那就先从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, 我们称之为“画布“,主要适用于绘制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是用于绘制的画笔,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可以沿着多个点绘制一条路径, 在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) | 添加椭圆角矩形轨迹 |
自定义时钟的Demo主要用到了Canvas以及Paint方面的知识,来看看代码吧:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }主活动中仅仅加载了一个布局。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <com.example.zjn.clock.MyView android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>在布局文件中放置我们自定义View,比较简单不需要太多介绍。
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上面,还是直接看代码吧:
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中的代码:
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()。
运行结果如下: