前言:这是自定义控件探索之旅的第四篇,上一篇文章主要介绍的是Canvas画布的基本的位置操作,本篇文章主要介绍的是Canvas如何进行绘制的操作,既然是涉及到绘制的具体操作,总的来说就有两大内容,分别是:绘制图片和绘制文字,本篇文章主要介绍的是绘制图片。
绘制图片
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.
*
* <p class="note"><strong>Note:</strong> Prior to API level 23 a picture cannot
* be replayed on a hardware accelerated canvas.</p>
*/
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.
* <p>
* 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.
*
* <p>
* <strong>Note:</strong> 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.
*
* <p>
* <strong>Note:</strong> 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地址),文章请勿滥用,也希望大家尊重笔者的劳动成果,谢谢。
网友评论