Android自定义控件探索之旅一4(笔记)

前言:这是自定义控件探索之旅的第四篇,上一篇文章主要介绍的是Canvas画布的基本的位置操作,本篇文章主要介绍的是Canvas如何进行绘制的操作,既然是涉及到绘制的具体操作,总的来说就有两大内容,分别是:绘制图片和绘制文字,本篇文章主要介绍的是绘制图片。

Android自定义控件探索之旅一7
Android自定义控件探索之旅一6
Android自定义控件探索之旅一5
Android自定义控件探索之旅一4
Android自定义控件探索之旅一3
Android自定义控件探索之旅一2
Android自定义控件探索之旅一1

绘制图片

Canvas绘制图片具体有两种方法,第一种:drawPicture(矢量图); 和第二种:drawBitmap(位图),由于考虑篇幅的问题,因此将Canvas绘制图片的内容分为两篇文章,本篇文章主要介绍的是:Canvas.drawPicture(Picture picture),关于drawBitmap,会在下一篇文章详细说明解释。

绘制图片:Canvas.drawPicture(Picture picture)

绘制图片的第一种方法是Canvas使用了drawPicture()这个方法,这个方法里面会要求传一个Picture对象,drawPicture()是一个方法重载,允许传入指定的参数。但是必须要传Picture,因此对于Picture的掌握就至关重要了。关于Picture,首先看一下系统源码:

/**
 * A Picture records drawing calls (via the canvas returned by beginRecording)
 * and can then play them back into Canvas (via {@link Picture#draw(Canvas)} or
 * {@link Canvas#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.
 *
 * 

Note: Prior to API level 23 a picture cannot * be replayed on a hardware accelerated canvas.

*/ public class Picture { private PictureCanvas mRecordingCanvas; private long mNativePicture; private boolean mRequiresHwAcceleration; private static final int WORKING_STREAM_STORAGE = 16 * 1024; /** * Creates an empty picture that is ready to record. */ public Picture() { this(nativeConstructor(0)); } /** * Create a picture by making a copy of what has already been recorded in * src. The contents of src are unchanged, and if src changes later, those * changes will not be reflected in this picture. */ public Picture(Picture src) { this(nativeConstructor(src != null ? src.mNativePicture : 0)); } private Picture(long nativePicture) { if (nativePicture == 0) { throw new RuntimeException(); } mNativePicture = nativePicture; } @Override protected void finalize() throws Throwable { try { nativeDestructor(mNativePicture); mNativePicture = 0; } finally { super.finalize(); } } /** * To record a picture, call beginRecording() and then draw into the Canvas * that is returned. Nothing we appear on screen, but all of the draw * commands (e.g. {@link Canvas#drawRect(Rect, Paint)}) will be recorded. * To stop recording, call endRecording(). After endRecording() the Canvas * that was returned must no longer be used, and nothing should be drawn * into it. */ public Canvas beginRecording(int width, int height) { if (mRecordingCanvas != null) { throw new IllegalStateException("Picture already recording, must call #endRecording()"); } long ni = nativeBeginRecording(mNativePicture, width, height); mRecordingCanvas = new PictureCanvas(this, ni); mRequiresHwAcceleration = false; return mRecordingCanvas; } /** * Call endRecording when the picture is built. After this call, the picture * may be drawn, but the canvas that was returned by beginRecording must not * be used anymore. This is automatically called if {@link Picture#draw} * or {@link Canvas#drawPicture(Picture)} is called. */ public void endRecording() { if (mRecordingCanvas != null) { mRequiresHwAcceleration = mRecordingCanvas.mHoldsHwBitmap; mRecordingCanvas = null; nativeEndRecording(mNativePicture); } } /** * Get the width of the picture as passed to beginRecording. This * does not reflect (per se) the content of the picture. */ public int getWidth() { return nativeGetWidth(mNativePicture); } /** * Get the height of the picture as passed to beginRecording. This * does not reflect (per se) the content of the picture. */ public int getHeight() { return nativeGetHeight(mNativePicture); } /** * Indicates whether or not this Picture contains recorded commands that only work when * drawn to a hardware-accelerated canvas. If this returns true then this Picture can only * be drawn to another Picture or to a Canvas where canvas.isHardwareAccelerated() is true. * * Note this value is only updated after recording has finished by a call to * {@link #endRecording()}. Prior to that it will be the default value of false. * * @return true if the Picture can only be drawn to a hardware-accelerated canvas, * false otherwise. */ public boolean requiresHardwareAcceleration() { return mRequiresHwAcceleration; } /** * Draw this picture on the canvas. *

* Prior to {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this call could * have the side effect of changing the matrix and clip of the canvas * if this picture had imbalanced saves/restores. * *

* Note: This forces the picture to internally call * {@link Picture#endRecording()} in order to prepare for playback. * * @param canvas The picture is drawn to this canvas */ public void draw(Canvas canvas) { if (mRecordingCanvas != null) { endRecording(); } if (mRequiresHwAcceleration && !canvas.isHardwareAccelerated()) { canvas.onHwBitmapInSwMode(); } nativeDraw(canvas.getNativeCanvasWrapper(), mNativePicture); } /** * Create a new picture (already recorded) from the data in the stream. This * data was generated by a previous call to writeToStream(). Pictures that * have been persisted across device restarts are not guaranteed to decode * properly and are highly discouraged. * * @see #writeToStream(java.io.OutputStream) * @deprecated The recommended alternative is to not use writeToStream and * instead draw the picture into a Bitmap from which you can persist it as * raw or compressed pixels. */ @Deprecated public static Picture createFromStream(InputStream stream) { return new Picture(nativeCreateFromStream(stream, new byte[WORKING_STREAM_STORAGE])); } /** * Write the picture contents to a stream. The data can be used to recreate * the picture in this or another process by calling createFromStream(...) * The resulting stream is NOT to be persisted across device restarts as * there is no guarantee that the Picture can be successfully reconstructed. * * @see #createFromStream(java.io.InputStream) * @deprecated The recommended alternative is to draw the picture into a * Bitmap from which you can persist it as raw or compressed pixels. */ @Deprecated public void writeToStream(OutputStream stream) { // do explicit check before calling the native method if (stream == null) { throw new NullPointerException(); } if (!nativeWriteToStream(mNativePicture, stream, new byte[WORKING_STREAM_STORAGE])) { throw new RuntimeException(); } } // return empty picture if src is 0, or a copy of the native src private static native long nativeConstructor(long nativeSrcOr0); private static native long nativeCreateFromStream(InputStream stream, byte[] storage); private static native int nativeGetWidth(long nativePicture); private static native int nativeGetHeight(long nativePicture); private static native long nativeBeginRecording(long nativeCanvas, int w, int h); private static native void nativeEndRecording(long nativeCanvas); private static native void nativeDraw(long nativeCanvas, long nativePicture); private static native boolean nativeWriteToStream(long nativePicture, OutputStream stream, byte[] storage); private static native void nativeDestructor(long nativePicture); private static class PictureCanvas extends Canvas { private final Picture mPicture; boolean mHoldsHwBitmap; public PictureCanvas(Picture pict, long nativeCanvas) { super(nativeCanvas); mPicture = pict; // Disable bitmap density scaling. This matches DisplayListCanvas. mDensity = 0; } @Override public void setBitmap(Bitmap bitmap) { throw new RuntimeException("Cannot call setBitmap on a picture canvas"); } @Override public void drawPicture(Picture picture) { if (mPicture == picture) { throw new RuntimeException("Cannot draw a picture into its recording canvas"); } super.drawPicture(picture); } @Override protected void onHwBitmapInSwMode() { mHoldsHwBitmap = true; } } }

关于Picture这个类的注释说明,翻译过来就是:
一个图片记录绘画调用(通过beginrecord()返回的画布),然后可以将它们回放到画布(通过Picture内置的draw(Canvas)方法或结合Canvas的 drawPicture( Picture) 方法)。对于大多数内容(例如文本、线、矩形),从图片中绘制序列可能比等效API调用快,因为图片执行回放时不会产生任何方法调用开销。在API级别23之前,图片不能在硬件加速画布上重播。

通过以上翻译可以知道以下内容:

  • A:Picture这个类这个主要的功能是用来记录,记录的内容就是Canvas中绘制的内容;Picture记录内容完毕之后,开发者需要的时候直接拿来就能用。
  • B:另外,使用Picture和再次调用绘图API进行对比,Picture的内存开销是比较小的,也就是说对于重复的操作Picture可以更加省时省力。当然,这个根据是系统源码说到的。
  • C:在API23之前,图片不能在硬件加速画布上重播。因此要合理的时机去关闭硬件加速。之所以会说到硬件加速,是因为一些第三方库会要求开启硬件加深,比如腾讯的X5WebView等,因此对硬件的加速在具体的场景要具体问题具体分析

简单说完了这个类的作用,下面就来看看这个Picture里面具体的API(可参考上面的系统源码 ):

相关方法 简介
int getWidth ( ) 获取宽度
int getHeight () 获取高度
Canvas beginRecording (int width, int height) 开始录制 (返回一个Canvas,在Canvas中所有的绘制都会存储在Picture中)
void endRecording () 结束录制
void draw (Canvas canvas) 将Picture中内容绘制到Canvas中
static Picture createFromStream (InputStream stream) (已废弃)通过输入流创建一个Picture
void writeToStream (OutputStream stream) (已废弃)将Picture中内容写出到输出流中

这里面最重要的就是 beginRecording() 和 endRecording(),另外beginRecording 和 endRecording 是成对使用的,一个是开始录制,一个是结束录制,两者之间的操作将会存储在Picture中。下面是关于Picture基本的代码:

 void stuPicture(){

        //创建一个Picture对象
        Picture mPicture = new Picture();
        //根据Picture、设置宽高、开始记录,返回一个Canvas
        Canvas mCanvas = mPicture.beginRecording(500, 500);
        //创建画笔
        Paint mPaint = new Paint();
        //设置颜色
        mPaint.setColor(Color.GRAY);
        //设置样式、填充
        mPaint.setStyle(Paint.Style.FILL);
        // 在Canvas中具体操作
        // 位移200 \ 200
        mCanvas.translate(200,200);
        // 绘制圆、半径100
        mCanvas.drawCircle(0,0,100, mPaint);
        //结束录制
        mPicture.endRecording();

    }

由于录制的内容不会直接显示(上面的代码也只是结束了录制),类似于存储的视频不点击播放就不会自动播放一样。因此,想要将Picture中的内容显示出来就需要手动调用播放(绘制)的API。那么如何将Picture中的内容绘制出来?将Picture中的内容绘制出来可以有以下3种方法:

序号 简介
1 使用Picture提供的draw方法绘制。
2 使用Canvas提供的drawPicture方法绘制。
3 将Picture包装成为PictureDrawable,使用PictureDrawable的draw方法绘制。

这3种方法的主要区别:

主要区别 分类 简介
是否对Canvas有影响 1有影响;2,3不影响 此处指绘制完成后是否会影响Canvas的状态(Matrix clip等)
可操作性强弱 1可操作性较弱;2,3可操作性较强 此处的可操作性可以简单理解为对绘制结果可控程度。

由于方法一对Canvas有影响,因此这里对方法一就不做过多研究,主要针对方法二、方法三来进行研究。

drawPicture()

首先看一下Canvas中,drawPicture()三个方法重载的系统源码:

  /**
     * Save the canvas state, draw the picture, and restore the canvas state.
     * This differs from picture.draw(canvas), which does not perform any
     * save/restore.
     *
     * 

* Note: This forces the picture to internally call * {@link Picture#endRecording} in order to prepare for playback. * * @param picture The picture to be drawn */ public void drawPicture(@NonNull Picture picture) { picture.endRecording(); int restoreCount = save(); picture.draw(this); restoreToCount(restoreCount); } /** * Draw the picture, stretched to fit into the dst rectangle. */ public void drawPicture(@NonNull Picture picture, @NonNull RectF dst) { save(); translate(dst.left, dst.top); if (picture.getWidth() > 0 && picture.getHeight() > 0) { scale(dst.width() / picture.getWidth(), dst.height() / picture.getHeight()); } drawPicture(picture); restore(); } /** * Draw the picture, stretched to fit into the dst rectangle. */ public void drawPicture(@NonNull Picture picture, @NonNull Rect dst) { save(); translate(dst.left, dst.top); if (picture.getWidth() > 0 && picture.getHeight() > 0) { scale((float) dst.width() / picture.getWidth(), (float) dst.height() / picture.getHeight()); } drawPicture(picture); restore(); }

可以看到第二个方法和第三个方法虽然多了一个参数RectF或者Rect,但是内部是做了一个translate的功能,最终还是调用了第一个drawPicture(picture),因此这个方法的使用就是:

       //绘制:
        mCanvas.drawPicture(mPicture);

        mCanvas.drawPicture(mPicture,new Rect());

        mCanvas.drawPicture(mPicture,new Rect(100,100,100,100));
        
        mCanvas.drawPicture(mPicture,new RectF(0,0,mPicture.getWidth(),100));

关于drawPicture( )这个方法就介绍到这里。如有纰漏、欢迎补充。

PictureDrawable()

这个方法的本质是将Picture包装成为PictureDrawable(PictureDrawable是Drawable的子类)、接着使用PictureDrawable内部的draw方法来进行绘制
首先看一下PictureDrawable的系统源码:

public class PictureDrawable extends Drawable {

    private Picture mPicture;

    /**
     * Construct a new drawable referencing the specified picture. The picture
     * may be null.
     *
     * @param picture The picture to associate with the drawable. May be null.
     */
    public PictureDrawable(Picture picture) {
        mPicture = picture;
    }

    /**
     * Return the picture associated with the drawable. May be null.
     *
     * @return the picture associated with the drawable, or null.
     */
    public Picture getPicture() {
        return mPicture;
    }

    /**
     * Associate a picture with this drawable. The picture may be null.
     *
     * @param picture The picture to associate with the drawable. May be null.
     */
    public void setPicture(Picture picture) {
        mPicture = picture;
    }

    @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();
        }
    }

    @Override
    public int getIntrinsicWidth() {
        return mPicture != null ? mPicture.getWidth() : -1;
    }

    @Override
    public int getIntrinsicHeight() {
        return mPicture != null ? mPicture.getHeight() : -1;
    }

    @Override
    public int getOpacity() {
        // not sure, so be safe
        return PixelFormat.TRANSLUCENT;
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {}

    @Override
    public void setAlpha(int alpha) {}
}

通过源码可以得知,PictureDrawable内部的draw方法,内部逻辑首先是判断非空,接着调用了getBounds( ),值得注意的是这里的getBounds( )是PictureDrawable父类Drawable的内置方法,跟进Drawable的getBounds( )方法看下源码:

   /**
     * Return the drawable's bounds Rect. Note: for efficiency, the returned
     * object may be the same object stored in the drawable (though this is not
     * guaranteed), so if a persistent copy of the bounds is needed, call
     * copyBounds(rect) instead.
     * You should also not change the object returned by this method as it may
     * be the same object stored in the drawable.
     *
     * @return The bounds of the drawable (which may change later, so caller
     *         beware). DO NOT ALTER the returned object as it may change the
     *         stored bounds of this drawable.
     *
     * @see #copyBounds()
     * @see #copyBounds(android.graphics.Rect)
     */
    @NonNull
    public final Rect getBounds() {
        if (mBounds == ZERO_BOUNDS_RECT) {
            mBounds = new Rect();
        }

        return mBounds;
    }

这个方法的英文注释翻译过来就是(按照段落分为三段):

  • 返回绘制边界矩形。注意:为了提高效率,返回的对象可能是存储在可绘制对象中的相同对象(尽管这不会保证),因此如果需要边界的持久副本,则调用copyBounds(Rect)。

  • 你也不应该更改此方法返回的对象,因为它可能是存储在可绘制对象中的相同对象。

  • 这个方法最终会返回可绘制的边界(以后可能会更改,所以调用方小心)。不要更改返回的对象,因为它可能更改此可绘制对象的存储边界。

为了更好的理解相关内容,在贴一段Drawable的系统代码如下:

   /**
     * Draw in its bounds (set via setBounds) respecting optional effects such
     * as alpha (set via setAlpha) and color filter (set via setColorFilter).
     *
     * @param canvas The canvas to draw into
     */
    public abstract void draw(@NonNull Canvas canvas);

    /**
     * Specify a bounding rectangle for the Drawable. This is where the drawable
     * will draw when its draw() method is called.
     */
    public void setBounds(int left, int top, int right, int bottom) {
        Rect oldBounds = mBounds;

        if (oldBounds == ZERO_BOUNDS_RECT) {
            oldBounds = mBounds = new Rect();
        }

        if (oldBounds.left != left || oldBounds.top != top ||
                oldBounds.right != right || oldBounds.bottom != bottom) {
            if (!oldBounds.isEmpty()) {
                // first invalidate the previous bounds
                invalidateSelf();
            }
            mBounds.set(left, top, right, bottom);
            onBoundsChange(mBounds);
        }
    }

    /**
     * Specify a bounding rectangle for the Drawable. This is where the drawable
     * will draw when its draw() method is called.
     */
    public void setBounds(@NonNull Rect bounds) {
        setBounds(bounds.left, bounds.top, bounds.right, bounds.bottom);
    }

通过 getBounds()的注释以及Drawable的部分系统代码可以获取很多信息,由于父类的Drawable的draw方法是一个抽象方法,子类的PictureDrawable会对此进行重写,在PictureDrawable进行调用draw方法的时候,如果代码不进行手动设置Rect的相关属性,系统会内置一个Rect,上面的ZERO_BOUNDS_RECT,其实就是Drawable源码中定义的内容,源码如下:

private static final Rect ZERO_BOUNDS_RECT = new Rect( );

综上,关于PictureDrawable的实现 ,就可以有如下参考代码:

        // 将Picture包装成PictureDrawable 
        PictureDrawable drawable = new PictureDrawable(mPicture);
        // 设置绘制区域 -- 注意此处所绘制的实际内容不会缩放
        drawable.setBounds(0,0,100,mPicture.getHeight());
        // 绘制
        drawable.draw(mCanvas);

关于绘制图片的第一种实现方式Canvas.drawPicture(Picture picture)就介绍到这里。由于Canvas和Bitmap的搭配使用,会涉及到Bitmap的内容,而Bitmap的内容又比较多,因此会在下一篇文章做详细的说明,另外,Canvas的绘制文字内容也会在后面的文章进行说明。

如果这篇文章对您有开发or学习上的些许帮助,希望各位看官留下宝贵的star,谢谢。

Ps:著作权归作者所有,转载请注明作者, 商业转载请联系作者获得授权,非商业转载请注明出处(开头或结尾请添加转载出处,添加原文url地址),文章请勿滥用,也希望大家尊重笔者的劳动成果,谢谢。

你可能感兴趣的:(Android自定义控件探索之旅一4(笔记))