操作 | API 类 | 备注 |
---|---|---|
绘制图片 | drawBitmap | -- |
录制绘制过程 | Picture | -- |
一、绘制图片
API | 备注 |
---|---|
drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) | 根据特定的 Matrix 进行绘制 |
drawBitmap(int[] colors, int offset, int stride, float x, float y, int width, int height, boolean hasAlpha, Paint paint) | API 21废弃 |
drawBitmap(int[] colors, int offset, int stride, int x, int y, int width, int height, boolean hasAlpha, Paint paint) | API 21废弃 |
drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) | 将指定区域的内容利用移动或者缩放的方式填充指定绘制区域 |
drawBitmap(Bitmap bitmap, RectF src, Rect dst, Paint paint) | 将指定区域的内容利用移动或者缩放的方式填充指定绘制区域 |
drawBitmap(Bitmap bitmap, float left, float top, Paint paint) | 以 (left, top) 为左上角,绘制图片 |
drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint) | 图片扭曲形变 |
在自定义 View 时,难免会遇到难以绘制的图标和背景,这个时候,我们就需要用到绘制图片。在 android 中,当我们使用到图片的时候,通常会使用到两个类:Drawable 和 Bitmap。
这两个类在开发中用的不少,想必都已经很熟悉了。由于绘制方法是用的 Bitmap ,这里只讲获取 Bitmap 的方法。
就通常而言,获取 Bitmap 对象有两种方法
- 1.利用 Bitmap 构造器获取,这种方式获取只能复制位图或者新建位图
- 2.利用 BitmapFactory 获取,这种方式可以根据传入的参数返回指定的位图
由于图片资源的位置不同,获取相应位图的方法也会不同,但是基本只要使用下面的两个方法,就可以应对大部分的情况:
BitmapFactory.decodeResource() //获取 drawable 文件夹下资源文件
BitmapFactory.decodeStream() //将指定路径的文件转化为 IO 流后,获取指定位图
抛开废弃方法不看,我们发现实际上,绘制 bitmap 的方法有四个:
drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)
drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)
drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint)
1)其中第一个方法中,根据 Matrix 来绘制图片,这里涉及到 Matrix 的使用,有兴趣的可以自己了解一下。这里不展开讲了。
2)第二个方法中间两个参数:
- src 源视图的显示部分
- dst 画布上允许的绘制区域
演示代码:
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.alu);
Rect src = new Rect(0, 0, 200, 200);
Rect dst = new Rect(0, 0, 200, 150);
canvas.drawBitmap(bitmap,src,dst,mPaint);
其中右边为原图,左边为绘制的图片。比较后,可以看出,这个方法,将原图 200200 区域的图像,经过变形绘制在 200150 的画布上。
3)第三个方法可以让我们控制绘制图像所在在的画布位置
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.alu);
canvas.drawBitmap(bitmap,400,400,mPaint);
4)第四个方法主要用于图像的扭曲
参数说明:
- bitmap:指定需要扭曲的源位图;
- meshWidth:该参数控制在横向上把该源位图划分成多少格;
- meshHeight:该参数控制在纵向上把该源位图划分成多少格;
- verts:该参数是一个长度为 (meshWidth+1) * (meshHeight+1) * 2 的数组,它记录了扭曲后的位图各“顶点”位置。虽然它是个数组,实际上它记录的数据是形如 (x0,y0)、(x1,y1)、(x2,y2)....(Nx,Ny) 格式的数据,这些数组元素控制对bitmap位图的扭曲效果;
- vertOffset:控制verts数组中从第几个数组元素开始才对bitmap进行扭曲。
从方法参数中,可以看到方法会根据参数将图片用网格分割。
这里我们用了一张带有 20 * 20 网格的图片做例子:
图片分割为 20 * 20 个方格,这每个方格成为一个拉伸单元。方法中会计算出这个图片中,所有交点的原始坐标组 origins,当你传入了改变的坐标数组 verts 时,它会将 origins 对应坐标围成的单元逐个进行拉伸,变换为计算后的样子。比如,这里我随便点了一下。
大致原理是这样,分的网格越多,形变控制的越精细。这里最重要的是交点变化的算法。
贴上代码:
public class MeshView extends View {
private Bitmap bitmap;
//定义两个常量,这两个常量指定该图片横向、纵向上都被划分为20格。
private final int WIDTH = 20;
private final int HEIGHT = 20;
//记录该图片上包含441个顶点
private final int COUNT = (WIDTH + 1) * (HEIGHT + 1);
//定义一个数组,保存Bitmap上的21 * 21个点的座标
private final float[] verts = new float[COUNT * 2];
//定义一个数组,记录Bitmap上的21 * 21个点经过扭曲后的座标
//对图片进行扭曲的关键就是修改该数组里元素的值。
private final float[] orig = new float[COUNT * 2];
private Paint mPaint;
public MeshView(Context context, int drawableId) {
super(context);
setFocusable(true);
//根据指定资源加载图片
bitmap = BitmapFactory.decodeResource(context.getResources(),
drawableId);
//获取图片宽度、高度
float bitmapWidth = bitmap.getWidth();
float bitmapHeight = bitmap.getHeight();
int index = 0;
for (int y = 0; y <= HEIGHT; y++) {
float fy = bitmapHeight * y / HEIGHT;
for (int x = 0; x <= WIDTH; x++) {
float fx = bitmapWidth * x / WIDTH;
/*
* 初始化orig、verts数组。
* 初始化后,orig、verts两个数组均匀地保存了21 * 21个点的x,y座标
*/
orig[index * 2 + 0] = verts[index * 2 + 0] = fx;
orig[index * 2 + 1] = verts[index * 2 + 1] = fy;
index += 1;
}
}
//设置背景色
setBackgroundColor(Color.WHITE);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(1);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
/* 对bitmap按verts数组进行扭曲
* 从第一个点(由第5个参数0控制)开始扭曲
*/
canvas.drawBitmapMesh(bitmap, WIDTH, HEIGHT, verts
, 0, null, 0, null);
}
//工具方法,用于根据触摸事件的位置计算verts数组里各元素的值
private void warp(float cx, float cy) {
for (int i = 0; i < COUNT * 2; i += 2) {
float dx = cx - orig[i + 0];
float dy = cy - orig[i + 1];
float dd = dx * dx + dy * dy;
//计算每个座标点与当前点(cx、cy)之间的距离
float d = (float) Math.sqrt(dd);
//计算扭曲度,距离当前点(cx、cy)越远,扭曲度越小
float pull = 80000 / ((float) (dd * d));
//对verts数组(保存bitmap上21 * 21个点经过扭曲后的座标)重新赋值
if (pull >= 1) {
verts[i + 0] = cx;
verts[i + 1] = cy;
} else {
//控制各顶点向触摸事件发生点偏移
verts[i + 0] = orig[i + 0] + dx * pull;
verts[i + 1] = orig[i + 1] + dy * pull;
}
}
//通知View组件重绘
invalidate();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//调用warp方法根据触摸屏事件的座标点来扭曲verts数组
warp(event.getX(), event.getY());
return true;
}
}
二、Picture
抄一段官方翻译:
A Picture records drawing calls (via the canvas returned by beginRecording) and can then play them back into Canvas (via draw(Canvas) or drawPicture(Picture)).For most content (e.g. text, lines, rectangles), drawing a sequence from a picture can be faster than the equivalent API calls, since the picture performs its playback without incurring any method-call overhead.
简而言之,就是录制一个绘制过程,然后在需要的时候,可以把这个过程重现。
API | 备注 |
---|---|
beginRecording(int width, int height) | -- |
endRecording() | -- |
draw(Canvas canvas) | -- |
getHeight() | -- |
getWidth() | -- |
createFromStream(InputStream stream) | deprecated in API level 18 |
writeToStream(OutputStream stream) | deprecated in API level 18 |
Picture 的 api 方法比较简单,基本就是方法名所代表的意思,下面主要演示用法和需要注意的地方。
录制一段绘制操作
private void initPicture() {
if (mPicture == null) {
mPicture = new Picture();
Canvas canvas = mPicture.beginRecording(200, 200);
canvas.translate(150, 150);
canvas.drawCircle(0, 0, 100, mPaint);
mPicture.endRecording();
}
}
上面的代码就已经录制好了一段绘画操作,值得注意的是,在这之后,即便你改变了 mPaint 的属性,或者移动旋转了 onDraw 方法中的画布,录制中的图像并不会有所改变,再次绘制的时候,只会和第一次录制时一样。单就这一点而言,和录像机还真是相像。
绘制录像中绘制的图片
下面我们来看,如何把这个 picture 绘制到画布上去。想要把已经录制好的图像绘制到画布上,一共有三种方法:
Picture#draw(Canvas canvas)
Canvas#drawPicture(Picture picture)
PictureDrawable#draw(Canvas)
1)Picture#draw(Canvas canvas)
我们知道,在调用录制方法的时候,返回了 canvas 对象,而我们的绘制操作就是对这个画布进行的操作。这里将 onDraw 中的画布传入 picture 进行绘制,需要注意的是在某些低版本的机型上,绘制结束后,所有在录像过程中进行的操作都会被实际作用在你传入的画布上,因此这个方法是不推荐使用的。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPicture.draw(canvas);
}
2)Canvas#drawPicture(Picture picture)
我们可以在 onDraw 方法中直接调用 drawPicture 方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPicture(mPicture);
}
当然,如果你已经开始使用了,会发现,它还可以再添加一个参数,像这样:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(0, 0, 200, 100);
canvas.drawPicture(mPicture,rectF);
}
由于原图为 200 * 200 的圆形,要将其放入 200 * 100 的矩形区域内,图形发生的拉伸。上图中,右边为原图,左边为实际绘制的图形。
3)PictureDrawable#draw(Canvas)
这个方法让我挺郁闷的,因为我像这样调用,是没有任何效果的:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
PictureDrawable drawable = new PictureDrawable(mPicture);
// 设置绘制区域 -- 注意此处所绘制的实际内容不会缩放
//drawable.setBounds(0,0,mPicture.getWidth(),mPicture.getHeight());
// 绘制
drawable.draw(canvas);
}
在没有调用 drawable.setBounds 时,不会有任何图像被绘制,因为在 PictureDrawable 源码中,onDraw 方法是这样写的:
@Override
public void draw(Canvas canvas) {
if (mPicture != null) {
Rect bounds = getBounds();
canvas.save();
canvas.clipRect(bounds);
canvas.translate(bounds.left, bounds.top);
canvas.drawPicture(mPicture);
canvas.restore();
}
}
@NonNull
public final Rect getBounds() {
if (mBounds == ZERO_BOUNDS_RECT) {
mBounds = new Rect();
}
return mBounds;
}
也就是说,只有调用 drawable.setBounds 才会有对应的绘制区域。而当绘制区域比实际区域大的时候,图形不会伸缩,只会被裁剪:
感谢:
1.Android drawBitmapMesh扭曲图像
2.Picture
3.GcsSloop 自定义 View 系列
以上。