Canvas(一) Canvas基本操作

编程入门 行业动态 更新时间:2024-10-08 19:48:59

Canvas(一)  Canvas基本<a href=https://www.elefans.com/category/jswz/34/1770947.html style=操作"/>

Canvas(一) Canvas基本操作

       在Android开发中,经常会需要自定义一些自绘View,而绘制自绘View就离不开Canvas(画布),本篇主要讲解Canvas本身以及与其相关的类。

开篇

首先看一下Android源码中, Canvas.java开头的介绍:

The Canvas class holds the “draw” calls. To draw something, you need
4 basic components: A Bitmap to hold the pixels, a Canvas to host
the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect,
Path, text, Bitmap), and a paint (to describe the colors and styles for the
drawing).

       从这一段中,我们可以了解到,如果我们想要绘制视图,需要四样东西:首先是Bitmap,用来保存像素,也就是Bitmap用来呈现绘制的结果;然后是Canvas,Canvas持有各种”draw”调用,比如drawText, drawRect等,它的作用就是这些Text,Rect,Color等写入到Bitmap中;第三是drawing primitive,可以理解成绘制的素材,颜色,图片,文字,路径都是要通过Canvas绘制到Bitmap中的素材;最后是画笔,主要描述绘画时的颜色或者风格。
       从官方文档中,我们可以看出来,只要拥有了这四样东西,我们就可以绘画了。其中Canvas持有了绘画的所有调用,所以理所当然的成为绘画开始的地方,只有调用了canvas.drawXXX() 方法,才可以开启一次绘画。

Canvas的来源

       在开发中,Canvas主要有三个来源;

onDraw(Canvas canvas)

       也就是我们绘制视图的地方,我们知道视图的绘制是从ViewRootImpl开始的,那么canvas也是从ViewRootImpl中传过来的,在ViewRootImpldraw()方法中调用了drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,boolean scalingRequired, Rect dirty) ,我们看一下这个方法的代码,

/*** @return true if drawing was successful, false if an error occurred*/private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,boolean scalingRequired, Rect dirty) {// Draw with software renderer.final Canvas canvas;try {final int left = dirty.left;final int top = dirty.top;final int right = dirty.right;final int bottom = dirty.bottom;canvas = mSurface.lockCanvas(dirty);// The dirty rectangle can be modified by Surface.lockCanvas()//noinspection ConstantConditionsif (left != dirty.left || top != dirty.top || right != dirty.right|| bottom != dirty.bottom) {attachInfo.mIgnoreDirtyState = true;}// TODO: Do this in nativecanvas.setDensity(mDensity);} catch (Surface.OutOfResourcesException e) {handleOutOfResourcesException(e);return false;} catch (IllegalArgumentException e) {Log.e(mTag, "Could not lock surface", e);// Don't assume this is due to out of memory, it could be// something else, and if it is something else then we could// kill stuff (or ourself) for no reason.mLayoutRequested = true;    // ask wm for a new surface next time.return false;}try {if (DEBUG_ORIENTATION || DEBUG_DRAW) {Log.v(mTag, "Surface " + surface + " drawing to bitmap w="+ canvas.getWidth() + ", h=" + canvas.getHeight());//canvas.drawARGB(255, 255, 0, 0);}// If this bitmap's format includes an alpha channel, we// need to clear it before drawing so that the child will// properly re-composite its drawing on a transparent// background. This automatically respects the clip/dirty region// or// If we are applying an offset, we need to clear the area// where the offset doesn't appear to avoid having garbage// left in the blank areas.if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {canvas.drawColor(0, PorterDuff.Mode.CLEAR);}dirty.setEmpty();mIsAnimating = false;mView.mPrivateFlags |= View.PFLAG_DRAWN;if (DEBUG_DRAW) {Context cxt = mView.getContext();Log.i(mTag, "Drawing: package:" + cxt.getPackageName() +", metrics=" + cxt.getResources().getDisplayMetrics() +", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo());}try {canvas.translate(-xoff, -yoff);if (mTranslator != null) {mTranslator.translateCanvas(canvas);}canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);attachInfo.mSetIgnoreDirtyState = false;mView.draw(canvas);drawAccessibilityFocusedDrawableIfNeeded(canvas);} finally {if (!attachInfo.mSetIgnoreDirtyState) {// Only clear the flag if it was not set during the mView.draw() callattachInfo.mIgnoreDirtyState = false;}}} finally {try {surface.unlockCanvasAndPost(canvas);} catch (IllegalArgumentException e) {Log.e(mTag, "Could not unlock surface", e);mLayoutRequested = true;    // ask wm for a new surface next time.//noinspection ReturnInsideFinallyBlockreturn false;}if (LOCAL_LOGV) {Log.v(mTag, "Surface " + surface + " unlockCanvasAndPost");}}return true;}

我们不关系具体的实现,只关注其中两句canvas = mSurface.lockCanvas(dirty);mView.draw(canvas);,我们可以看到,显示从mSurface(绘图表面)中获取了canvas,然后传给了mView的draw方法,之后会继续讲canvas传给onDraw(Canvas canvas)方法。我们只了解大致流程,具体实现不在本篇的讨论范围之内。

Canvas canvas = new Canvas(bitmap)

       第二个来源就是Canvas的构造器,也就是我们自己创建的Canvas,根据开篇的介绍,我们可以看出来这里传入的bitmap就是要保存绘制结果的Bitmap,对于这个bitmap有特殊的要求,也就是这个bitmap必须是mutable(易变的),可以理解为这个bitmap是空的,初始化的,而不是保存了具体内容的bitmap,我们可以通过createBitmap(Bitmap source, int x, int y, int width, int height)方法创建一个bitmap,如果我们使用已经包含具体内容的bitmap就会报以下错误;
java.lang.IllegalStateException: Immutable bitmap passed to Canvas constructor
       另外需要注意的地方是,我们通过这种方式创建的Canvas最终会将绘制内容保存到传入的Bitmap中,如果我们想要绘制到View上,则需要在onDraw(Canvas sysCanvas)方法中调用sysCanvas.drawBitmap(bitmap)将这个bitmap绘制到View上。根据开篇的介绍我们知道,Canvas是把内容绘制到Bitmap上的,通过实例化自己的Canvas,我们将内容绘制到了自己的Bitmap上,而onDraw(Canvas canvas)我们可以看成是将所有的内容绘制到系统的Bitmap中去,想要让View显示绘制的内容,就必须调用canvas的drawXXX系列方法。

SurfaceHolder.lockCanvas()

       在SurfaceView中使用到,本篇不做讨论。

Canvas的drawXXX()方法

canvas.drawRect

1. drawRect(float left, float top, float right, float bottom, @NonNull Paint paint)
2. drawRect(@NonNull Rect r, @NonNull Paint paint)
3. drawRect(@NonNull RectF rect, @NonNull Paint paint)

       作用就是绘制一个矩形区域,第一个重载方法需要传入四个float类型的点,第二个传入Rect对象,这个对象保存四个int类型的顶点,第三个传入RectF对象,这个对象保存四个float类型的对象。三个重载方法的效果是完全一样的。
栗子:
自定义一个CanvasView,继承自View,重写onDraw(Canvas canvas)方法,后边的其他方法也将在此控件上修改。

@Overrideprotected void onDraw(Canvas canvas) {Paint paint = new Paint();paint.setColor(Color.BLUE);paint.setStyle(Paint.Style.STROKE);canvas.drawRect(0, 0, 200, 200, paint);}

效果,

canvas.drawArc

drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint)
// oval绘制范围RectF,startAngle开始的角度,sweepAngle结束的角度,userCenter是否使用圆点drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint)
// left,top,right,bottom绘制范围顶点坐标, startAngle,sweepAngle, userCenter同上

       作用是绘制一个弧形,使用起来很简单,主要注意参数userCenter,如果为true的话,绘制的结果将包含圆点,也就是弓形的首位和圆点相连组成的扇形,为false的话,则是将startAngle和sweepAngle连起来,两种效果如下;

1.userCenter为false canvas.drawArc(0, 0, 500, 500, 30, 120, true, paint);

2.userCenter为true canvas.drawArc(0, 0, 500, 500, 30, 120, false, paint);


可以看到,弧形绘制的角度是顺时针的,第二张图中蓝线描出了圆点的位置,绘制的时候没有包含圆点。

drawARGB

drawARGB(int a, int r, int g, int b)
// 参数分别是ARGB的四个分量

绘制颜色背景,没什么特别的。和drawColor功能一样。

drwaBitmap

1. drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint)// bitmap 将要被绘制的bitmap, left,top bitmap的左边和上边顶点

栗子:
       我们创建一个CanvasView,继承自View,加入到Activity的布局文件中,宽高为300dp,然后重写onDraw方法。

@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);Paint paint = new Paint();paint.setColor(Color.RED);// 画出整个View的背景canvas.drawColor(Color.GREEN);Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_manage);// 1canvas.drawBitmap(bitmap, 0, 0, paint);// 2// canvas.drawBitmap(bitmap, 600, 600, paint);} 

1放开,如图;

bitmap的图片完全贴着左上角,
2放开,如图;

bitmap的左上角移动,向下向右移动,并且如果bitmap长宽加上左上角坐标大于View的宽高会导致显示不全。

 2. drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull RectF dst, @Nullable Paint paint)```
// bitmap被绘制的bitmap, src bitmap的子集区域, dst将要将bitmap绘制到的区域,会被拉伸或者缩放

栗子:

@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);Paint paint = new Paint();paint.setColor(Color.RED);canvas.drawColor(Color.GREEN);Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_manage);// 左上坐标为(0, 0)绘制的bitmapcanvas.drawBitmap(bitmap, 0, 0, paint);// 本方法绘制的bitmap, (0, 0, 100, 100)是相对于bitmap而言的区域,相当于bitmap的子集,(200, 200, 500, 500)是相对于View的区域,这个方法的目的就是将bitmap的某个子集绘制到View的指定区域上canvas.drawBitmap(bitmap, new Rect(0, 0, 100, 100), new Rect(200, 200, 500, 500), paint);}

效果如图,

另外这个方法还有一个类似的重载方法

drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst, @Nullable Paint paint)

不同点就是dst一个是RectF,一个是Rect

3. drawBitmap(@NonNull Bitmap bitmap, @NonNull Matrix matrix, @Nullable Paint paint)
// bitmap 源bitmap matrix对这个bitmap做变换的变换矩阵

栗子:

@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);Paint paint = new Paint();paint.setColor(Color.RED);canvas.drawColor(Color.GREEN);Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_manage);canvas.drawBitmap(bitmap, 0, 0, paint);Matrix matrix = new Matrix();matrix.postTranslate(300, 300);canvas.drawBitmap(bitmap, matrix, paint);}

效果,

第二个bitmap由于使用了矩阵,导致发生了位移,Matrix就是变换矩阵,具体用法以后再写。

drawCircle

drawCircle(float cx, float cy, float radius, @NonNull Paint paint)
// cx,cy圆点坐标,radius半径

栗子:

@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);Paint paint = new Paint();paint.setColor(Color.RED);canvas.drawColor(Color.GREEN);canvas.drawCircle(200, 200, 100, paint);}

效果:

drawLine

drawLine(float startX, float startY, float stopX, float stopY,
@NonNull Paint paint)
// startX,startY 划线开始的坐标, stopX, stopY线结束的坐标         

栗子:

 @Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);Paint paint = new Paint();paint.setColor(Color.RED);canvas.drawColor(Color.GREEN);canvas.drawLine(0, 0, 300, 300, paint);}

效果,

drawLines

1. drawLines(@Size(multiple=4) @NonNull float[] pts, int offset, int count, @NonNull Paint paint)
// pts 线段开始和结束的坐标,每四个点确定一个线段比如
// float[] pts = new float[]{//  0, 0,// 100, 100,// 200, 200, // 300, 300,
//}这个数组可以确定两个线段,分别为开始坐标为(0, 0),结束为(100, 100)和开始为(200, 200),结束为(300, 300)的线段
// offset为跳过的点的个数, count为除了跳过的点以外可以用于绘制线段的点
// 其中有这样的关系 pts.length >= offset + count,如果两者之和超过pts.length
// 就会报数组越界
// 另外,由于必须由4个点确定一个线段,所以如果count<4,那么就不会绘制线段,并且count必须为4的倍数,那么线段数就是count >> 2
// 如上,如果offset=3,那么最后的300就会被抛弃

栗子:

 @Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);Paint paint = new Paint();paint.setColor(Color.RED);canvas.drawColor(Color.GREEN);float[] points = new float[] {0, 0,100, 100,200, 200,300, 300,400, 400,500, 500};canvas.drawLines(points, 4, 8, paint);}

效果:

另外一个绘制线段的方法;

public void drawLines(@Size(multiple=4) @NonNull float[] pts, @NonNull Paint paint) {drawLines(pts, 0, pts.length, paint);}// 只是offset == 0, count == pts.length

drawOval

drawOval(@NonNull RectF oval, @NonNull Paint paint)
drawOval(float left, float top, float right, float bottom, @NonNull Paint paint)
// 绘制椭圆, 和drawRect类似

drawPath

drawPath(@NonNull Path path, @NonNull Paint paint)
// 绘制一个path,没啥特别的

drawPoint和drawPoints

drawPoint(float x, float y, @NonNull Paint paint)
// 画单个点,没啥特别的
drawPoints(@Size(multiple=2) @NonNull float[] pts, @NonNull Paint paint)
drawPoints(@Size(multiple=2) float[] pts, int offset, int count,@NonNull Paint paint)
// 使用和drawLines基本一样,不过drawLines需要四个值确定一条线,drawPoints需要两个值确定一个点,所以count必须为2的倍数,否则最后不成对的点会被抛弃,点的总数等于conut>>1

栗子:

@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);Paint paint = new Paint();paint.setStrokeWidth(10);paint.setColor(Color.RED);canvas.drawColor(Color.GREEN);float[] points = new float[] {0, 0,100, 100,200, 200,300, 300,400, 400,500, 500};canvas.drawPoints(points, 3, 8, paint);}

效果,

drawRoundRect

drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, @NonNull Paint paint)
// left, top, right, bottom为四个顶点, rx,ry为圆角的圆点,值越大则圆角越明显
drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint)
// rect为圆角矩形范围,rx,ry同上

栗子:

  @Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);Paint paint = new Paint();paint.setStrokeWidth(10);paint.setColor(Color.RED);canvas.drawColor(Color.GREEN);canvas.drawRoundRect(new RectF(0, 0, 300, 300), 10, 10, paint);canvas.drawRoundRect(300, 300, 600, 600, 50, 50, paint);}

效果,

drawText

1. drawText(@NonNull char[] text, int index, int count, float x, float y,@NonNull Paint paint)
// text 要绘制的字符数组,index数组开始的下标,count绘制的字符个数, x为开始绘制字符串的x坐标,y需要注意一下是基线的y坐标
2. drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
// text 要绘制的字符串,x,y同上
3. drawText(@NonNull String text, int start, int end, float x, float y,@NonNull Paint paint)
// text同上, start开始的字符索引,end-1为结束的字符索引,需要注意一下, x, y同上
4. drawText(@NonNull CharSequence text, int start, int end, float x, float y,@NonNull Paint paint)
// text要绘制的字符串,start,end同上,x, y同上

基线不好解释,还是画出来比较方便

@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);Paint paint = new Paint();paint.setStrokeWidth(2);paint.setTextSize(50);paint.setColor(Color.RED);int baselineX = 0;int baselineY = 100;// 画出x坐标为0-1000,y为100的线canvas.drawLine(0, baselineY, 1000, baselineY, paint);paint.setColor(Color.BLUE);canvas.drawText("Talk is cheap, show me your code!", baselineX, baselineY, paint);}

效果,
可以看到,红线的Y坐标和drawText的Y坐标相同,这条线就是基线,基线有点类似于写英文单词时的四线三格,用来规范字母的位置。这样只要我们只要知道了基线位置,绘制字符串开始的X坐标,还有字体大小就可以确定字体的位置了。

FontMetrics

       在Android中,为了规范字体的位置,除了基线还有其他的几条线,如下,

图片来源感谢作者

  1. top 可以绘制的最高位置所在的线
  2. ascent 建议的绘制的最高位置所在的线
  3. baseline 基线
  4. descent建议的绘制的最低位置所在的线
  5. bottom 可绘制的最低位置所在的线

那么,如何得到这些线的坐标那?
系统给我们提供了FontMetric类,它是Paint的一个内部类,我们看一下实现;

/*** Class that describes the various metrics for a font at a given text size.* Remember, Y values increase going down, so those values will be positive,* and values that measure distances going up will be negative. This class* is returned by getFontMetrics().*/public static class FontMetrics {/*** The maximum distance above the baseline for the tallest glyph in* the font at a given text size.*/public float   top;/*** The recommended distance above the baseline for singled spaced text.*/public float   ascent;/*** The recommended distance below the baseline for singled spaced text.*/public float   descent;/*** The maximum distance below the baseline for the lowest glyph in* the font at a given text size.*/public float   bottom;/*** The recommended additional space to add between lines of text.*/public float   leading;}

       从源码中我们可以看出来,类中的变量描述的都是以baseLine(基线)为基础的,top表示baseLine上方最高距离所在的线,ascent表示baseLine上方系统建议距离所在的线,descent表示baseLine下方系统建议距离所在的线,bottom表示baseLine下方最低距离所在的线, leading表示文本线之间建议的附加空间。这里我们需要注意一点,这些变量都是描述的distance(距离,有正负),而且以baseLine为基础,所以我们应该通过这两个特点计算出各个线的真实y坐标。
       我们可以通过getFontMetrics()方法获得FontMetrics对象,然后获得各个变量,根据上图的位置,我们可以得出以下结论;

  1. top的y坐标 = baseLine的y坐标 + fontMetrics.top
  2. ascent的y坐标 = baseLine的y坐标 + fontMetric.ascent
  3. descent的y坐标 = baseLine的y坐标 + fontMetric.descent
  4. bottom的y坐标 = baseLine的y坐标 + fontMetric.bottom

根据以上的结论,我们画出这四条线,如下;

@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);Paint paint = new Paint();paint.setAntiAlias(true);paint.setDither(true);paint.setTextSize(150);paint.setColor(Color.RED);int baselineY = 200;int baselineX = 0;canvas.drawText("Talk is Cheap", baselineX, baselineY, paint);//计算各线在位置Paint.FontMetrics fm = paint.getFontMetrics();float ascent = baselineY + fm.ascent;float descent = baselineY + fm.descent;float top = baselineY + fm.top;float bottom = baselineY + fm.bottom;
////画基线paint.setColor(Color.RED);canvas.drawLine(baselineX, baselineY, 3000, baselineY, paint);paint.setColor(Color.BLUE);canvas.drawLine(baselineX, top, 1000, top, paint); // top线paint.setColor(Color.GREEN);canvas.drawLine(baselineX, ascent, 1000, ascent, paint); // ascentpaint.setColor(Color.YELLOW);canvas.drawLine(baselineX, descent, 1000, descent, paint); // descentpaint.setColor(Color.RED);canvas.drawLine(baselineX, bottom, 1000, descent, paint); // bottom}

效果,
注意一点,最后的descent所在的线和bottom距离很近,几乎重合在了一起,可以看到颜色混在了一起。

drawTextOnPath

1. /*** Draw the text, with origin at (x,y), using the specified paint, along* the specified path. The paint's Align setting determins where along the* path to start the text.** @param text     The text to be drawn* @param path     The path the text should follow for its baseline* @param hOffset  The distance along the path to add to the text's*                 starting position* @param vOffset  The distance above(-) or below(+) the path to position*                 the text* @param paint    The paint used for the text (e.g. color, size, style)*/
drawTextOnPath(@NonNull char[] text, int index, int count, @NonNull Path path,float hOffset, float vOffset, @NonNull Paint paint)2./*** Draw the text, with origin at (x,y), using the specified paint, along* the specified path. The paint's Align setting determins where along the* path to start the text.** @param text     The text to be drawn* @param path     The path the text should follow for its baseline* @param hOffset  The distance along the path to add to the text's*                 starting position* @param vOffset  The distance above(-) or below(+) the path to position*                 the text* @param paint    The paint used for the text (e.g. color, size, style)*/drawTextOnPath(@NonNull String text, @NonNull Path path, float hOffset,float vOffset, @NonNull Paint paint)

       两个重载方法的不同之处在于接收的内容不同,1接收字符数组,index表示开始绘制的字符下标,count表示绘制字符的数量,其他参数的意义是一样的,我们一个一个分析;

  • path: The path the text should follow for its baseline
    也就是说以这个path作为text的基线绘制
  • hOffset: The distance along the path to add to the text’s starting position
    在path上绘制文本的时候开始的位移,也就是先空出hOffset的距离,然后再开始绘制,有点类似于段落开头的空格。
  • vOffset:The distance above(-) or below(+) the path to position
    顾名思义,vOffset代表了垂直方向上的位移,参考物为path,也就是基线,有符号,在path以上为负号,在path以下为正号。

       另外,在方法的注释中我们可以看到,开始绘制的位置受paint的Align影响

栗子:

 @Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);Paint paint = new Paint();paint.setStyle(Paint.Style.STROKE);paint.setAntiAlias(true);paint.setDither(true);paint.setTextSize(50);canvas.drawColor(Color.parseColor("#8EE5EE"));paint.setColor(Color.BLACK);Path path = new Path();path.addCircle(300, 300, 200, Path.Direction.CCW);canvas.drawPath(path, paint);//        1paint.setColor(Color.RED);canvas.drawTextOnPath("Talk is cheap, show me your code!", path, 0, 0, paint);//        2
//        paint.setColor(Color.BLUE);
//        canvas.drawTextOnPath("Talk is cheap, show me your code!", path, 200, 0, paint);//        3
//        paint.setColor(Color.BLACK);
//        canvas.drawTextOnPath("Talk is cheap, show me your code!", path, 0, -30, paint);
//
//        4
//        paint.setColor(Color.BLACK);
//        canvas.drawTextOnPath("Talk is cheap, show me your code!", path, 0, 30, paint);}

放开这是1下的代码,效果如下;
可以看出,path在基线的位置上。

放开2的代码,效果如下;
对比上图,可以看出来,文字开始的位置向后移动了一段距离

放开3的代码,效果如下;
可以看出来,相对于基线,文字向上移动了一段距离,图中看似是向下,主要是因为Path是圆形导致的。

放开4的代码,效果如下;
对比3可以看出,相对于基线,文字向下移动了一段距离。

以上就是Canvas相关的基本操作。

更多推荐

Canvas(一) Canvas基本操作

本文发布于:2024-02-11 14:42:08,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1681516.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:操作   Canvas

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!