自定义 View - Canvas - 绘制图片

操作 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);
效果图-g.png

其中右边为原图,左边为绘制的图片。比较后,可以看出,这个方法,将原图 200200 区域的图像,经过变形绘制在 200150 的画布上。

3)第三个方法可以让我们控制绘制图像所在在的画布位置

Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.alu);
canvas.drawBitmap(bitmap,400,400,mPaint);
效果图-g.png

4)第四个方法主要用于图像的扭曲

参数说明:

  • bitmap:指定需要扭曲的源位图;
  • meshWidth:该参数控制在横向上把该源位图划分成多少格;
  • meshHeight:该参数控制在纵向上把该源位图划分成多少格;
  • verts:该参数是一个长度为 (meshWidth+1) * (meshHeight+1) * 2 的数组,它记录了扭曲后的位图各“顶点”位置。虽然它是个数组,实际上它记录的数据是形如 (x0,y0)、(x1,y1)、(x2,y2)....(Nx,Ny) 格式的数据,这些数组元素控制对bitmap位图的扭曲效果;
  • vertOffset:控制verts数组中从第几个数组元素开始才对bitmap进行扭曲。

从方法参数中,可以看到方法会根据参数将图片用网格分割。

这里我们用了一张带有 20 * 20 网格的图片做例子:

target2.png

图片分割为 20 * 20 个方格,这每个方格成为一个拉伸单元。方法中会计算出这个图片中,所有交点的原始坐标组 origins,当你传入了改变的坐标数组 verts 时,它会将 origins 对应坐标围成的单元逐个进行拉伸,变换为计算后的样子。比如,这里我随便点了一下。

效果图-g.png

大致原理是这样,分的网格越多,形变控制的越精细。这里最重要的是交点变化的算法。

贴上代码:

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);

}
效果图-g.png

由于原图为 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 才会有对应的绘制区域。而当绘制区域比实际区域大的时候,图形不会伸缩,只会被裁剪:

效果图-g.png


感谢:

1.Android drawBitmapMesh扭曲图像
2.Picture
3.GcsSloop 自定义 View 系列

以上。

谢谢观赏
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容