自定义 View - Canvas - 绘制图片

作者: Arnold_J | 来源:发表于2017-12-11 08:52 被阅读345次
    操作 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 系列

    以上。

    谢谢观赏

    相关文章

      网友评论

        本文标题:自定义 View - Canvas - 绘制图片

        本文链接:https://www.haomeiwen.com/subject/rdlxbxtx.html