首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >带你玩转自定义view系列

带你玩转自定义view系列

作者头像
Android技术干货分享
发布2019-03-26 17:50:52
1.5K0
发布2019-03-26 17:50:52
举报
文章被收录于专栏:Android技术分享Android技术分享

View 的简介

View是Android所有控件的基类,接下来借鉴网上的一张图片让大家一目了然(图片出自:http://blog.51cto.com/wangzhaoli/1292313

image

Android 的相关坐标系

图片内容有点多,自定义 View 做得好事可以提升用户对 APP 的体验感的。接下来就学习一下 Android 的相关坐标系。

Android 坐标系

在物理中,要描述一个物体的运动,就必须选定一个参考系。所谓滑动,正式相对于参考系的运动。在Android中,将屏幕最左上角的顶点作为Android坐标系的原点,从原点向右是X轴正方向,从原点向下是Y轴正方向:

image

系统提供了 getLocationOnScreen(intLocation[]) 这样的方法来获取 Android 坐标中点的位置,即该视图左上角在 Android 坐标系中的坐标。另外,在触控事件中使用 getRawX() 和 getRawY() 方法所获取的坐标同样是 Android 坐标系中的坐标。

视图坐标

Android 中除了上面所说的这种坐标系之外,还有一个视图坐标系,他描述了子视图在父视图中的位置关系。这两种坐标系并不矛盾也不复杂,他们的作用是相辅相成的。

与 Android 坐标系类似,视图坐标系同样是以原点向右为X轴正方向,以原点向下为Y轴正方向。

只不过在视图坐标系中,原点不再是 Android 坐标系中的屏幕左上角,而是以父视图左上角为坐标原点。

image

在触控事件中,通过 getX() 和 getY() 所获得的坐标就是视图坐标中的坐标。

在 Android 中,系统提供了非常多的方法来获取坐标值、相对距离等。方法多是好,但是不方便初学者学习,不知道什么情况下使用。下面就总结了一些 API,结合 Android 坐标系来看看该如何使用它们。

image

这些方法可以分成如下两个类别:

View提供的获取坐标方法: **getTop(): **获取到的是View自身的顶边到其父布局顶边的距离 **getLeft(): **获取到的是View自身的左边到其父布局左边的距离 **getRight(): **获取到的是View自身的右边到其父布局左边的距离 **getBottom(): **获取到的是View自身的底边到其父布局顶边的距离

另外View获取自身宽高

**getHeight(): **获取View自身高度 **getWidth(): **获取View自身宽度 MotionEvent提供的方法: **getX(): **获取点击事件距离控件左边的距离,即视图坐标 **getY(): **获取点击事件距离控件东边的距离,即视图坐标 **getRawX(): **获取点击事件距离整个屏幕左边的距离,即绝对坐标 **getRawY(): **获取点击事件距离整个屏幕顶边的距离,即绝对坐标

以上就是简单的 Android 坐标和 View 的视图坐标。

Android画笔的详解

Android提供了2D图形绘制的各种工具,如Canvas(画布)、Point(点)、Paint(画笔)、Rectangles(矩形)等,利用这些工具可以直接在界面上进行绘制。

在自定义View中,我们经常用到的Canvas(画布)和Paint(画笔),像我们画画一样,需要画布和画笔,在View中绘制控件,Canvas就代表着画布,Paint就代表着画笔。

这是的Android的的的官网里画的API:https://developer.android.com/reference/android/graphics/Paint

官网中的API有很多,下面是比较常用的一些API:

Paint.setAntiAlias(boolean flag);//设置抗锯齿效果 设置true的时边缘会将锯齿模糊化Paint.setDither(boolean flag);//设置防抖动,设置true的时图片看上去会更柔和点Paint.setColor(int color);//设置画笔颜色Paint.setARGB(int a, int r, int g, int b); //设置画笔的ARGB值Paint.setAlpha(int alpha);//设置画笔的Alpha值Paint.setStyle(); //设置画笔的style (三种:FILL填充 FILL_AND_STROKE填充加描边 STROKE描边 )Paint.setStrokeWidth(float width);//设置描边宽度Paint.setXfermode(Xfermode xfermode);//设置图形重叠时的处理方式,如合并,取交集或并集,经常用来制作橡皮的擦除效果Paint.setShader(Shader shader);//设置图像效果,使用Shader可以绘制出各种渐变效果Paint.setShadowLayer(float radius ,float dx,float dy,int color);//在图形下面设置阴影层,产生阴影效果,radius为阴影的半径,dx和dy为阴影在x轴和y轴上的距离,color为阴影的颜色 //下面写文本的时候经常用到的Paint.setTextSize(float textSize);//设置画笔文字大小Paint.measureText(String text);//测试文本的长度Paint.setTextAlign(Paint.Align align);// CENTER(文本居中) LEFT(文本左对齐) RIGHT(文本右对齐)

下面就演示一下上面这几个API的效果。

Paint.setStye()

Paint.setStyle() //设置画笔的style,有三种:

  • Paint.Style.FILL //将填充使用此样式绘制的几何和文本,忽略绘画中与笔划相关的所有设置
  • Paint.Style.FILL_AND_STROKE //使用此样式绘制的几何和文本将同时填充和描边,尊重绘画中与笔划相关的字段
  • Paint.Style.STROKE //使用此样式绘制的几何和文本将被描边,尊重绘画上与笔划相关的字段

演示一个小demo:

paint = new Paint();       paint.setColor(Color.RED);//画笔颜色为红色       paint.setStrokeWidth(80); //描边宽度为80(为了区分效果,特意设置特别大)       float radius = 100f;       //将填充使用此样式绘制的几何和文本,忽略绘画中与笔划相关的所有设置       paint.setStyle(Paint.Style.FILL);       canvas.drawCircle(400, 200, radius, paint);       //使用此样式绘制的几何和文本将同时填充和描边,尊重绘画中与笔划相关的字段       paint.setStyle(Paint.Style.FILL_AND_STROKE);       canvas.drawCircle(400, 500, radius, paint);       //使用此样式绘制的几何和文本将被描边,尊重绘画上与笔划相关的字段       paint.setStyle(Paint.Style.STROKE);       canvas.drawCircle(400, 900, radius, paint);

结果:

image

Paint.setShader(Shader shader)

Paint.setShader(Shader shader) //设置图像效果,使用Shader可以绘制出各种渐变效果

Shader:着色器,用来给图像着色,此类是基类, Shader的API (https://developer.android.com/reference/android/graphics/Shader)。有5个子类:

  • BitmapShader
  • ComposeShader
  • LinearGradient
  • RadialGradient
  • SweepGradient

在了解上面5个类之前,先了解一下Shader.TileMode这个枚举,有三个值:

  • Shader.TileMode.CLAMP :如果着色器在其原始边界之外绘制,则复制边缘颜色
  • Shader.TileMode.MIRROR :水平和垂直重复着色器的图像,交替镜像,使相邻的图像始终接缝
  • Shader.TileMode.REPEAT :水平和垂直重复着色器的图像
BitmapShader

这里只介绍一种着色器,其余的点击阅读原文进行查看。

其实这个Shader用于绘制bitmap作为纹理,然后通过平铺模式进行填充

/**        * 构造函数        * @bitmap 用来填充图形的Bitmap        * @tileX X轴Bitmap用Shader.TileMode模式填充        * @tileY Y轴Bitmap用Shader.TileMode模式填充        */       BitmapShader(Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)

演示一下:

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.flower);       BitmapShader shader = new BitmapShader(bitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.MIRROR);       paint.setShader(shader);       canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);

结果:

image

X轴用Shader.TileMode.CLAMP模式,就是用bitmap的右边缘去填充X轴的其余空间 Y轴用Shader.TileMode.MIRROR模式,就是在用相邻两张图像互为镜像的方式填充整个Y轴其余空间

接下来XY轴换一下模式:

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.flower);       BitmapShader shader = new BitmapShader(bitmap, BitmapShader.TileMode.MIRROR, BitmapShader.TileMode.REPEAT);       paint.setShader(shader);       canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);

结果:

image

X轴用Shader.TileMode.MIRROR模式,就是在用相邻两张图像互为镜像的方式填充整个X轴其余空间 Y轴用Shader.TileMode.REPEAT模式,就是用相同的图像重复填充整个Y轴其余空间

其余四种着色器示例请阅读原文进行查看。

Paint.setShadowLayer(float radius ,float dx,float dy,int color)

Paint.setShadowLayer(float radius ,float dx,float dy,int color) //在图形下面设置阴影层,产生阴影效果

/**        * @radius radius为阴影半径,半径越大,阴影面积越大,越模糊;反之,半径越小,阴影面积越小,也越清晰,radius=0时,阴影消失        * @dx dx为阴影在x轴上的偏移值        * @dy dy为阴影在y轴上的偏移值        * @color color为阴影的颜色        */       Paint.setShadowLayer( float radius, float dx, float dy, int color);

演示一下:

paint.setColor(Color.RED);       paint.setShadowLayer(20, 0, 0, Color.YELLOW);       paint.setTextSize(100);       canvas.drawText("I am Layne", 200, 300, paint);

结果:

image

改一下:

paint.setShadowLayer(20,50, 50, Color.YELLOW);

结果:

image

改一下:

paint.setShadowLayer(1,50, 50, Color.YELLOW);

结果:

image

再改一下:

paint.setShadowLayer(0,50, 50, Color.YELLOW);

结果:

image

添加阴影:

paint.setColor(Color.RED);       paint.setShadowLayer(30, 0, 0, Color.BLACK);       setLayerType(LAYER_TYPE_SOFTWARE, paint);//要注意加上这句       canvas.drawCircle(400, 800, 100, paint);

**结果: **

image

常用画笔的 API 介绍完了

Android画布的详解

接下来学习一下自定义View之Canvas(画布)的详解

先来看看Canvas常用方法:

| 功能分类 | Canvas常用方法 | 备注 | | 绘制颜色 | drawARGB | 通过设置ARGB值绘制颜色 | | drawRGB | 通过设置RGB值绘制颜色 | | drawColor | 绘制颜色 | | 绘制图形 | drawPoint,drawPoints | 绘制点,点集合 | | drawLine,drawLines | 绘制线,线集合 | | drawRect | 绘制矩形 | | drawCircle | 绘制圆 | | drawOval | 绘制椭圆 | | drawArc | 绘制弧 | | 画布操作 | translate、rotate、scale、save、restore | 依次为位移、旋转、缩放、保存画布和恢复画布 | | drawPath | 按路径绘制 |

Canvas 绘制颜色的 API

canvas.drawARGB(int a, int r, int g, int b)canvas.drawRGB(int r, int g, int b)canvas.drawColor(int color) canvas.drawColor(int color, PorterDuff.Mode mode)

颜色的四种模式:

颜色模式

备注

ARGB8888

四通道高精度(32位)

ARGB4444

四通道低精度(16位)

RGB565

屏幕默认模式(16位)

Alpha8

仅有透明通道(8位)

ARGB分别代表的类型:

类型

备注

A(Alpha)

透明度,取值范围 [0,255],0代表完全透明,255代表完全不透明

R(Red)

红色,取值范围 [0,255],0代表无色,255代表红色

G(Green)

绿色,取值范围 [0,255],0代表无色,255代表绿色

B(Blue)

蓝色,取值范围 [0,255],0代表无色,255代表蓝色

从表中可以看出,ARGB的取值范围都是[0,255],其实就是16进制中的[0x00,0xff] A从0x00到0xff对应表示从透明到不透明 RGB从0x00到0xff对应表示颜色从浅到深

示例代码:

canvas.drawARGB(0, 0, 0, 0);canvas.drawARGB(255, 255, 0, 0);canvas.drawARGB(255, 0, 255, 0);canvas.drawARGB(255, 0, 0, 255);

**结果: **

image

Canvas 绘制形状

canvas.drawPoint(float x, float y, Paint paint) //绘制点canvas.drawPoints( float[] pts, Paint paint) //绘制多个点canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint) //绘制线canvas.drawLines(float[] pts, Paint paint)//绘制多条线canvas.drawRect(float left, float top, float right, float bottom, Paint paint) //绘制矩形canvas.drawRect(RectF rect, Paint paint) //绘制矩形canvas.drawRoundRect(float left, float top, float right, float bottom, float rx, float ry,Paint paint) //绘制圆角矩形canvas.drawRoundRect(RectF rect, float rx, float ry, Paint paint) //绘制圆角矩形canvas.drawCircle(float cx, float cy, float radius,Paint paint) //绘制圆canvas.drawOval(float left, float top, float right, float bottom,Paint paint) //绘制椭圆canvas.drawOval(RectF oval,Paint paint) //绘制椭圆canvas.drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter,Paint paint) //绘制圆弧canvas.drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter,Paint paint) //绘制圆弧

画点

Paint paint = new Paint();       paint.setColor(Color.GREEN);       paint.setStrokeWidth(20f);       canvas.drawPoint(300, 300, paint);       float[] pts = {400, 400, 500, 400, 600, 400};       canvas.drawPoints(pts, paint);

drawPoint的两个参数就是(x,y)。而drawPoints的第一个参数是float数组,每两个就代表一个坐标,如上面的例子有(400,400)、(500,400)和(600,400)。结果:

image

画线

Paint paint = new Paint();       paint.setColor(Color.GREEN);       paint.setStrokeWidth(20f);       canvas.drawLine(200, 500, 1000, 500, paint);       float[] pts = {800, 400, 1000, 500,               800, 600, 1000, 500};       canvas.drawLines(pts, paint);

drawLine的前四个参数就代表起始坐标(x1,y1),和终点坐标(x2,y2)。而drawLines第一个参数也是float数组,如上面的例子前四个起始点(800, 400),终点(1000, 500)。接着下一条线的起点(800, 600),终点(1000, 500)。结果:

image

画矩形

Paint paint = new Paint();       paint.setColor(Color.GRAY);       paint.setStyle(Paint.Style.STROKE); //只描边       paint.setStrokeWidth(10f);       //1.直接通过坐标画矩形       canvas.drawRect(100, 100, 800, 500, paint);       //2.通过Rect 画矩形       Rect mRect = new Rect(100, 550, 800, 950);       paint.setStyle(Paint.Style.FILL); //只描内容       canvas.drawRect(mRect, paint);       //3.通过RectF 画矩形       RectF mRectF = new RectF(100, 1000, 800, 1400);       paint.setStyle(Paint.Style.FILL_AND_STROKE); //描边和内容       paint.setStrokeWidth(10f);       canvas.drawRect(mRectF, paint);

以上的坐标参数都是四个的,即用前面两个代表左上角坐标(x1,y1)和右下角坐标(x2,y2)来确定矩形的。结果:

image

剩余的画圆、椭圆、圆角矩形和圆弧的操作点击原文进行查看,基本差不多,这里就不做多演示。

Canvas 画布的操作

canvas.translate(float dx, float dy) //平移canvas.rotate(float degrees) //旋转canvas.rotate(float degrees, float px, float py) //改变旋转中心并旋转canvas.scale(float sx, float sy) //缩放canvas.scale(float sx, float sy, float px, float py) //改变缩放中心并缩放canvas.save(); //保存画布canvas.restore(); //恢复画布

Canvas画布的操作可以让我们更加容易绘制图形,Canvas画布操作只会对后面的绘制起作用,对前面已经绘制的是不影响的。

translate

translate是对坐标系的平移,且是可以多次重叠的,默认是在屏幕的左上角(0,0)。

Paint paint = new Paint();       paint.setStyle(Paint.Style.FILL); //只内容       paint.setStrokeWidth(10f);       paint.setAntiAlias(true); //设置抗锯齿       paint.setDither(true); //设置防抖动       //将坐标系移到Canvas宽度的一半,高度400的位置       canvas.translate(getMeasuredWidth() / 2, 400);       //绘制一个红色圆       paint.setColor(Color.RED);       canvas.drawCircle(0, 0, 100, paint);       //坐标系原点在前面位置的基础上再往下移动250像素       canvas.translate(0, 250);       paint.setColor(Color.GREEN);       canvas.drawCircle(0, 0, 100, paint);       //坐标系原点在前面位置的基础上再往下移动250像素       canvas.translate(0, 250);       paint.setColor(Color.BLUE);       canvas.drawCircle(0, 0, 100, paint);

**结果: **

image

rotate

rotate是对坐标系的旋转,rotate有两个方法

canvas.rotate(float degrees) canvas.rotate(float degrees, float px, float py)

rotate第一个参数是旋转的角度,后面两个参数是可以确定旋转中心,如果不填默认是(0,0)。

演示一下:

Paint paint = new Paint();       paint.setStyle(Paint.Style.FILL); //只内容       paint.setStrokeWidth(10f);       paint.setAntiAlias(true); //设置抗锯齿       paint.setDither(true); //设置防抖动       //将坐标系移动到屏幕中心       canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2);       RectF rectF = new RectF(-300, -300, 0, 0);       //绘制一个红色矩形       paint.setColor(Color.RED);       canvas.drawRect(rectF, paint);       //将坐标系旋转180度,不会影响前面已经绘制的图形       canvas.rotate(180);       paint.setStyle(Paint.Style.FILL);       //绘制一个蓝色矩形       paint.setColor(Color.BLUE);       canvas.drawRect(rectF, paint);

结果:

image

scale

scale也有两个方法

canvas.scale(float sx, float sy)canvas.scale(float sx, float sy, float px, float py)

scale是缩放,缩放中心默认是坐标原点,且多次缩放是可以重叠的。scale的前两个参数sx和sy是X轴和Y轴的缩放倍数,后两个参数px和py是控制缩放中心的位置。演示一下:

Paint paint = new Paint();       paint.setStyle(Paint.Style.FILL); //只内容       paint.setStrokeWidth(10f);       paint.setAntiAlias(true); //设置抗锯齿       paint.setDither(true); //设置防抖动       //将坐标系移动到屏幕中心       canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2);       RectF rectF = new RectF(-300, -300, 0, 0);       paint.setColor(Color.RED);       //左上角绘制红色矩形       canvas.drawRect(rectF, paint);       //X轴 Y轴分别缩放到原来的1/2并以原点(0,0)位对称点进行翻转       canvas.scale(-0.5f, -0.5f);       //绘制绿色矩形       paint.setColor(Color.GREEN);       canvas.drawRect(rectF, paint);

结果:

image

这里的sx和sy参数取值挺有考究的,可以自己写个 demo 试试

sx sy取值范围

备注

(1,+∞)

根据缩放中心放大到原来的n倍

1

跟原来大小一样,没变化

(0,1)

根据缩放中心缩放

0

sx sy有一个取0图形就消失了

(-1,0)

根据缩放中心缩放并翻转

-1

翻转

(-∞,-1)

根据缩放中心放大并翻转

save和restore

save:表示保存Canvas之前的状态。save保存之后,可以调用Canvas的平移、缩放、旋转、错切和裁剪等操作。 restore:恢复Canvas之前保存的状态,防止save后对Canvas执行的操作对后续的绘制有影响。

Paint paint = new Paint();       paint.setStyle(Paint.Style.FILL); //只内容       paint.setStrokeWidth(10f);       paint.setAntiAlias(true); //设置抗锯齿       paint.setDither(true); //设置防抖动       //保存画布       canvas.save();       //坐标原点移到屏幕中心       canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2);       //以屏幕中心为坐标原点在(100,100)为圆心处绘制红色圆       paint.setColor(Color.RED);       RectF rectF = new RectF(0, 0, 200, 200);       canvas.drawRect(rectF, paint);       //恢复画布       canvas.restore();       //恢复画布后,坐标原点(0,0)默认在屏幕左上角,       //即以屏幕左上角为坐标原点在(100,100)为圆心处绘制蓝色圆       paint.setColor(Color.BLUE);       RectF rectf = new RectF(50, 50, 100, 100);       canvas.drawRect(rectf, paint);

**结果: **

image

以上的代码去掉save()和restore()方法,再运行,结果:

image

如果去掉save()和restore(),那么所有的图像都在坐标原点移动到屏幕中心后绘制;如果有save()和restore(),在restore()之后,图像的坐标原点又回到了屏幕的左上角了。

drawPath按路径绘制。

Canvas之Path的详解

Canvas的绘制图形只能绘制一些常规的,比如点、线、圆、椭圆、矩形等的。如果想要绘制更复杂的图形,那么就得靠Path了。

Path的定义: Path类将多种符合路径(多个轮廓,如直线段、二次曲线、立方曲线等)封装在其内部的几何路径。

Path的绘制: 通过设置Paint.Style的FILL(只描内容)、STROKE(只描边)、FILL_AND_STROKE(描边和内容),然后调用canvas.drawPath(path, paint);Path还可以用于剪切或者在路径上绘制文本canvas.drawTextOnPath()。

Path有两个构造函数

Path() // 空的构造函数Path(Path src) //创建一个新的路径,并且从src路径里赋值内容

Path一些常用的API:

| 功能分类 | Path的常用API | 备注 | | 线操作 | lineTo、rLineTo | 绘制线 | | 点操作 | moveTo、rMoveTo | 改变后面操作的起始点位置 | | setLastPoint | 改变前面操作中最后点的位置 | | 常规图形操作 | addRect | 绘制矩形 | | addRoundRect | 绘制圆角矩形 | | addCircle | 绘制圆 | | addOval | 绘制椭圆 | | addArc、arcTo | 绘制圆弧 | | 闭合path操作 | close | 如果连接Path起点和终点能形成一个闭合图形,则会将起点和终点连接起来形成一个闭合图形 | | 贝塞尔曲线 | quadTo、rQuadTo、cubicTo、rCubicTo | 贝塞尔曲线 |

线操作

lineTo(float x, float y) //添加当前点到目标点(x,y)构成的直线到pathrLineTo(float dx, float dy) //基于当前坐标系,即以path最后的那个点//为坐标系原点(0,0),如果前面没有path的点,默认是屏幕左上角(0,0)

注:lineTo、rLineTo起始点默认是屏幕左上角的坐标系原点(0,0)

演示一下:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描边        paint.setColor(Color.BLUE);        paint.setStrokeWidth(10f);        paint.setAntiAlias(true); //设置抗锯齿        paint.setDither(true); //设置防抖动        Path path = new Path();        //屏幕左上角(0,0)到(200,200)画一条直线        path.lineTo(200, 200);        //(200, 200)到(200, 400)画一条直线        path.lineTo(200, 400);        //以(200, 400)为起始点(0,0)偏移量为(200, 400)画一条直线,        //其终点坐标实际在屏幕的位置为(400, 800)        path.rLineTo(400, 800);        canvas.drawPath(path, paint);

结果:

image

点操作

moveTo(float x, float y) //改变接下来操作的起点位置为(x,y)rMoveTo(float dx, float dy) //接下来要操作的起点位置为(x+dx,y+dy)setLastPoint(float dx, float dy) //改变前一步操作点的位置,会改变前一步的操作

先对比一下moveTo()和rMoveTo()的区别,演示一下:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描边        paint.setColor(Color.BLUE);        paint.setStrokeWidth(10f);        paint.setAntiAlias(true); //设置抗锯齿        paint.setDither(true); //设置防抖动        Path path = new Path();        //将坐标系原点从(0,0)移动到(200,200)        path.moveTo(200,200);        //画从(200,200)到(400,400)之间的直线        path.lineTo(400, 400);//        path.rMoveTo(100, 0); //暂时注释        path.lineTo(800, 400);        canvas.drawPath(path, paint);

暂时注释了rMoveTo()方法,看看结果:

image

然后解掉rMoveTo()方法的注释,意思是下一步的起点位置由(400, 400)变为(400+100, 400+0),即(500,400),结果:

image

再来看看moveTo()和setLastPoint()的区别,同样以上的代码,加上path.setLastPoint(800, 200)方法:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描边        paint.setColor(Color.BLUE);        paint.setStrokeWidth(10f);        paint.setAntiAlias(true); //设置抗锯齿        paint.setDither(true); //设置防抖动        Path path = new Path();        //将坐标系原点从(0,0)移动到(200,200)        path.moveTo(200,200);        //画从(200,200)到(400,400)之间的直线        path.lineTo(400, 400);        path.setLastPoint(800, 200);        path.lineTo(800, 400);        canvas.drawPath(path, paint);

结果:

image

红线原本是设置setLastPoint(800, 200)之前的路径,在设置之后,影响到了前一步lineTo(400, 400)的操作,使之变成了lineTo(800, 200),结果就如图了。

得出结论:rMoveTo()影响的是后面操作的起点位置,并不会影响之前的操作;而setLastPoint()改变前一步操作最后一个点的位置,不仅影响前一步操作,同时也会影响后一步操作。

绘制常规图形

//绘制圆addCircle(float x, float y, float radius, Direction dir)  //绘制椭圆addOval(RectF oval, Direction dir)addOval(float left, float top, float right, float bottom, Direction dir) //绘制矩形addRect(RectF rect, Direction dir) addRect(float left, float top, float right, float bottom, Direction dir) //绘制圆角矩形addRoundRect(RectF rect, float rx, float ry, Direction dir) addRoundRect(float left, float top, float right, float bottom, float rx, float ry,Direction dir)addRoundRect(RectF rect, float[] radii, Direction dir)addRoundRect(float left, float top, float right, float bottom, float[] radii,Direction dir)

所有API里面都有一个共同的参数Direction:

Direction

备注

Path.Direction.CCW

counter-clockwise ,沿逆时针方向绘制

Path.Direction.CW

clockwise ,沿顺时针方向绘制

Direction其用法演示:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描边        paint.setColor(Color.BLUE);        paint.setStrokeWidth(2f);        paint.setTextSize(40f);        paint.setAntiAlias(true); //设置抗锯齿        paint.setDither(true); //设置防抖动        Path path = new Path();        //以(600,600)为圆心,300为半径绘制圆        //Path.Direction.CW顺时针绘制圆 Path.Direction.CCW逆时针绘制圆        path.addCircle(600, 600, 300, Path.Direction.CCW);        //沿path绘制文字        canvas.drawTextOnPath("我是Layne,在测试Direction,这是CCW逆时针绘制圆", path, 0, 0, paint);        canvas.drawPath(path, paint);

结果对比:

image

其他图形绘制示例:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描边        paint.setColor(Color.BLUE);        paint.setStrokeWidth(10f);        paint.setAntiAlias(true); //设置抗锯齿        paint.setDither(true); //设置防抖动        Path path = new Path();        //以(400,200)为圆心,半径为100绘制圆        path.addCircle(550, 200, 100, Path.Direction.CW);        //绘制椭圆        RectF rectF = new RectF(100, 350, 500, 600);        //第一种方法绘制椭圆        path.addOval(rectF, Path.Direction.CW);        //第二种方法绘制椭圆        path.addOval(600, 350, 1000, 600, Path.Direction.CW);        //绘制矩形        RectF rect = new RectF(100, 650, 500, 900);        //第一种方法绘制矩形        path.addRect(rect, Path.Direction.CW);        //第一种方法绘制矩形        path.addRect(600, 650, 1000, 900, Path.Direction.CCW);        //绘制圆角矩形        RectF roundRect = new RectF(100, 950, 300, 1100);        //第一种方法绘制圆角矩形        path.addRoundRect(roundRect, 20, 20, Path.Direction.CW);        //第二种方法绘制圆角矩形        path.addRoundRect(350, 950, 550, 1100, 10, 50, Path.Direction.CCW);        //第三种方法绘制圆角矩形        //float[] radii中有8个值,依次为左上角,右上角,右下角,左下角的rx,ry        RectF roundRectT = new RectF(600, 950, 800, 1100);        path.addRoundRect(roundRectT, new float[]{50, 50, 50, 50, 50, 50, 0, 0}, Path.Direction.CCW);        //第四种方法绘制圆角矩形        path.addRoundRect(850, 950, 1050, 1100,new float[]{0, 0, 0, 0,50, 50, 50, 50}, Path.Direction.CCW);        canvas.drawPath(path, paint);

结果:

image

绘制圆弧

//绘制圆弧addArc(RectF oval, float startAngle, float sweepAngle)addArc(float left, float top, float right, float bottom, float startAngle,float sweepAngle)//forceMoveTo:是否强制将path最后一个点移动到圆弧起点,//true是强制移动,即为不连接两个点;false则连接两个点arcTo(RectF oval, float startAngle, float sweepAngle,boolean forceMoveTo)arcTo(RectF oval, float startAngle, float sweepAngle)arcTo(float left, float top, float right, float bottom, float startAngle,float sweepAngle, boolean forceMoveTo)

addArc()和arcTo()都是添加圆弧到path中,不过他们之间还是有区别的。addArc()是直接添加圆弧到path中;而arcTo()会判断要绘制圆弧的起点与绘制圆弧之前path中最后的点是否是同一个点,如果不是同一个点的话,就会连接两个点。

演示一下:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描边        paint.setColor(Color.BLUE);        paint.setStrokeWidth(20f);        paint.setAntiAlias(true); //设置抗锯齿        paint.setDither(true); //设置防抖动        Path path = new Path();        //在F(400, 200, 700, 500)区域内绘制一个270度的圆弧        RectF rectF = new RectF(400, 200, 700, 500);        path.addArc(rectF, 0, 270);        //在(400, 600, 600, 800)区域内绘制一个90度的圆弧,并且不连接两个点        RectF rectFTo = new RectF(400, 600, 700, 900);        path.arcTo(rectFTo, 0, 180, true); //等价于path.addArc(rectFTo, 0, 180);        canvas.drawPath(path, paint);

结果:

image

把上面代码修改成连接两点:

path.arcTo(rectFTo, 0, 180, false); //等价于path.addArc(rectFTo, 0, 180);

结果:

image

闭合path操作

如果path的重点和起始点不是同一个点的话,那么path.close()就会连接这两个点,形成一个封闭的图形。演示一下:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描边        paint.setColor(Color.BLUE);        paint.setStrokeWidth(20f);        paint.setAntiAlias(true); //设置抗锯齿        paint.setDither(true); //设置防抖动        Path path = new Path();        //在F(400, 200, 700, 500)区域内绘制一个270度的圆弧        RectF rectF = new RectF(400, 200, 700, 500);        path.addArc(rectF, 0, 270);//        path.close(); //先注释        canvas.drawPath(path, paint);

结果:

image

接下来解掉path.close()的注释,再运行一次,结果:

image

贝塞尔曲线

贝塞尔曲线就麻烦多了,也复杂多了。这里不详细说明了,再网上找到一篇说明比较详细的文章。 安卓自定义View进阶 - 贝塞尔曲线:(https://blog.csdn.net/u013831257/article/details/51281136)

这里直接复制了里面的二阶曲线的实现:

public class CanvasView extends View {    private Paint mPaint;    private int centerX, centerY;    private PointF start, end, control;    public CanvasView(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);        mPaint = new Paint();        mPaint.setColor(Color.BLACK);        mPaint.setStrokeWidth(8);        mPaint.setStyle(Paint.Style.STROKE);        mPaint.setTextSize(60);        start = new PointF(0, 0);        end = new PointF(0, 0);        control = new PointF(0, 0);    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        centerX = w / 2;        centerY = h / 2;        // 初始化数据点和控制点的位置        start.x = centerX - 200;        start.y = centerY;        end.x = centerX + 200;        end.y = centerY;        control.x = centerX;        control.y = centerY - 100;    }    @Override    public boolean onTouchEvent(MotionEvent event) {        // 根据触摸位置更新控制点,并提示重绘        control.x = event.getX();        control.y = event.getY();        invalidate();//刷新View,重新绘制        return true;    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        // 绘制数据点和控制点        mPaint.setColor(Color.GRAY);        mPaint.setStrokeWidth(20);        canvas.drawPoint(start.x, start.y, mPaint);        canvas.drawPoint(end.x, end.y, mPaint);        canvas.drawPoint(control.x, control.y, mPaint);        // 绘制辅助线        mPaint.setStrokeWidth(4);        canvas.drawLine(start.x, start.y, control.x, control.y, mPaint);        canvas.drawLine(end.x, end.y, control.x, control.y, mPaint);        // 绘制贝塞尔曲线        mPaint.setColor(Color.RED);        mPaint.setStrokeWidth(8);        Path path = new Path();        path.moveTo(start.x, start.y);        path.quadTo(control.x, control.y, end.x, end.y);        canvas.drawPath(path, mPaint);    }}

结果:

<figure style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important; font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: 0.544000029563904px; orphans: auto; text-align: justify; text-indent: 0px; text-transform: none; white-space: normal; widows: 1; word-spacing: 0px; -webkit-text-stroke-width: 0px; font-size: 16px; color: rgb(62, 62, 62); line-height: inherit; background-color: rgb(255, 255, 255);">

image

</figure>

PathMeasure的详解

PathMeasure是什么? PathMeasure是用来对Path进行测量的工具,一般来说PathMeasure是和Path配合着使用的。通过PathMeasure,我们可以知道Path路径上某讴歌点的坐标、Path的长度等的。

PathMeasure有两个构造函数:

//构建一个空的PathMeasurePathMeasure() //构建一个PathMeasure并关联一个指定的创建好的PathPathMeasure(Path path, boolean forceClosed) 

无参构造函数PathMeasure()在使用前必须调用setPath(Path path, boolean forceClosed)来关联一个Path。其实就是等价于有参数的构造函数PathMeasure(Path path, boolean forceClosed)了。如果关联之后的Path有所更改,那么就需要调用setPath(Path path, boolean forceClosed)重新关联。

PathMeasure常用的API:

setPath(Path path, boolean forceClosed) isClosed()getLength()getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)getMatrix(float distance, Matrix matrix, int flags)getPosTan(float distance, float pos[], float tan[])nextContour()

setPath(Path path, boolean forceClosed): 此方法是关联一个预先创建好的Path。第二个参数forceClosed如果为true,并且关联的Path未闭合时,测量的Path长度可能比Path实际长度长一点,因为测量的是Path闭合的长度。但是关联的Path不会有任何变化

isClosed(): 判断关联的Path是否是闭合状态。如果forceClosed为true,那么此方法一定返回true。

getLength(): 返回已关联的Path总长度。如果setPath()时设置的forceClosed为true,则返回的值可能会比实际长度的长。

getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo): 截取Path的一段。如果截取成功,则返回true,反之则返回false。

参数

备注

startD

起点在Path的位置,取值范围0<=startD < stopD<=getLength()

stopD

终点在Path的位置,取值范围0<=startD < stopD<=getLength()

dst

将截取的path的片段添加到dst中

startWithMoveTo

起点是否使用MoveTo,如果为true,则截取的path的第一个点不会变化,截取的path也不会改变,如果为false,则截取的path可能会发生形变。

注: 1.如果截取Path的长度为0,则返回false,大于0则返回true; 2.startD、stopD必须为和法制(0,getLength()),如果startD>=stopD,则返回false; 3.在Android 4.4或之前的版本在开启硬件加速时,绘制可能会不显示,请关闭硬件加速或者给dst添加一个简单的操作,如:dst.rLineTo(0,0)

演示一下:

        Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描边        paint.setColor(Color.BLUE);        paint.setStrokeWidth(10f);        paint.setAntiAlias(true); //设置抗锯齿        paint.setDither(true); //设置防抖动        //初始化Path并顺时针绘制一个矩形        Path sourcePath = new Path();        sourcePath.addRect(300, 300, 800, 800, Path.Direction.CW);        PathMeasure measure = new PathMeasure();        measure.setPath(sourcePath, false);        Log.e("PathMeasure", "measure.getLength():" + measure.getLength());        canvas.drawPath(sourcePath, paint);

结果:

image

image

现在要截取上图中的下半部分,总长度是2000,中间的部分就是[750,1750]。演示一下:

        //初始化Path并顺时针绘制一个矩形        Path sourcePath = new Path();        sourcePath.addRect(300, 300, 800, 800, Path.Direction.CW);        PathMeasure measure = new PathMeasure();        measure.setPath(sourcePath, false);//        Log.e("PathMeasure", "measure.getLength():" + measure.getLength());//        canvas.drawPath(sourcePath, paint);        //初始化一个空的Path        Path dstPath = new Path();        //截取sourcePath的一部分添加到dstPath中        measure.getSegment(750, 1750, dstPath, true);        dstPath.rLineTo(0, 0);//真机刚刚好是4.4版本的,加个简单的操作        canvas.drawPath(dstPath, paint);

结果:

image

上面代码中的dstPath初始化完之后,并没有内容的,试试有内容的情况:

        //初始化Path并顺时针绘制一个矩形        Path sourcePath = new Path();        sourcePath.addRect(300, 300, 800, 800, Path.Direction.CW);        PathMeasure measure = new PathMeasure();        measure.setPath(sourcePath, false);//        Log.e("PathMeasure", "measure.getLength():" + measure.getLength());//        canvas.drawPath(sourcePath, paint);        //初始化一个空的Path        Path dstPath = new Path();        dstPath.rLineTo(200, 200);        dstPath.lineTo(800, 200);        //截取sourcePath的一部分添加到dstPath中        measure.getSegment(750, 1750, dstPath, true);        canvas.drawPath(dstPath, paint);

结果:

image

有意思的是,将dstPath的rLineTo()和lineTo()放在getSegment()之后,后面内容的起点就成了getSegment()的终点了。演示一下:

        //初始化Path并顺时针绘制一个矩形        Path sourcePath = new Path();        sourcePath.addRect(300, 300, 800, 800, Path.Direction.CW);        PathMeasure measure = new PathMeasure();        measure.setPath(sourcePath, false);//        Log.e("PathMeasure", "measure.getLength():" + measure.getLength());//        canvas.drawPath(sourcePath, paint);        //初始化一个空的Path        Path dstPath = new Path();        //截取sourcePath的一部分添加到dstPath中        measure.getSegment(750, 1750, dstPath, true);        dstPath.rLineTo(200, 200);        dstPath.lineTo(800, 200);        canvas.drawPath(dstPath, paint);

结果:

image

再把上面代码中的getSegment()的startWithMoveTo参数改成false会变成什么样呢?演示一下:

          //初始化Path并顺时针绘制一个矩形        Path sourcePath = new Path();        sourcePath.addRect(300, 300, 800, 800, Path.Direction.CW);        PathMeasure measure = new PathMeasure();        measure.setPath(sourcePath, false);//        Log.e("PathMeasure", "measure.getLength():" + measure.getLength());//        canvas.drawPath(sourcePath, paint);        //初始化一个空的Path        Path dstPath = new Path();        //为了易于理解,把这两个方法放回getSegment()前面        dstPath.rLineTo(200, 200);        dstPath.lineTo(800, 200);        //截取sourcePath的一部分添加到dstPath中        measure.getSegment(750, 1750, dstPath, false);        canvas.drawPath(dstPath, paint);

结果:

image

和上上结果对比可得出:startWithMoveTo参数为true时,被截取的path片段会保持原状;startWithMoveTo参数为false时,会将截取的path片段的起始点移动到dstPath的终点,以保持dstPath的连续性。

getMatrix(float distance, Matrix matrix, int flags): 距离Path起始点的一段长度distance,通过计算得到该位置坐标并返回一个处理好的矩阵,该矩阵以左上角为旋转点,如果Path不存在或者长度为0,该方法返回false。

参数

备注

distance

距离Path起始点的距离,取值范围0 <= distance <=getLength()

matrix

根据 flags封装matrix,flags不同,存入matrix的就不同

flags

PathMeasure.POSITION_MATRIX_FLAG:位置信息 ,PathMeasure.TANGENT_MATRIX_FLAG:切边信息,方位角信息,使得图片按path旋转。

getPosTan(float distance, float pos[], float tan[]): 距离Path起始点的长度distance,通过计算返回该长度在Path上的坐标及该坐标的正切值分别复制给pos[]、tan[]

参数

备注

distance

距离Path起始点的距离,取值范围0 <= distance <= getLength()

pos[]

distance在path上的坐标,即pos[]存的该点的坐标x,y值

tan[]

distance在path上对应坐标点在path上的方向,tan[0]是邻边边长,tan[1]是对边边长。通过Math.atan2(tan[1], tan[0])*180.0/Math.PI 可以得到正切角的弧度值。

nextContour(): 如果Path有多条曲线组成,且彼此不连接,那么getLength()、getSegment()、getMatrix()和getPosTan()这些方法,都只是针对当前正在操作的。举个例子,Path由多条曲线组成,且彼此不连接,那么getLength()返回的只是当前操作曲线的长度,并不是所有曲线的长度。那么怎么获取下一条曲线的长度呢?这时就得用nextContour()跳转到下一条曲线了,跳转成功返回true,失败就返回false。演示一下:

 Paint paint = new Paint();        paint.setStyle(Paint.Style.STROKE); //只描边        paint.setColor(Color.BLUE);        paint.setStrokeWidth(10f);        paint.setAntiAlias(true); //设置抗锯齿        paint.setDither(true); //设置防抖动        Path path = new Path();        PathMeasure measure = new PathMeasure();        //绘制一条从(100,100)到(900,100)的直线,长度为800        path.moveTo(100, 100);        path.lineTo(900, 100);        //绘制一条从(100,200)到(500,100)的直线,长度为400        path.moveTo(100, 200);        path.lineTo(500, 200);        measure.setPath(path, false);        //输出第一条曲线的长度        Log.e("PathMeasure", "measure.getLength():" + measure.getLength());        measure.nextContour(); //跳转到下一条曲线        //输出第二条曲线的长度        Log.e("PathMeasure", "next measure.getLength():" + measure.getLength());        canvas.drawPath(path, paint);

输出的结果:

image

Region区域

Region 在 Android 的绘制中是区域的意思,使用 Region 可以对图形进行很多操作,比如区域的合并,取交集、或抑或等等。

Region 的构造函数有以下四个:

public Region()  //无参构造public Region(Region region) //传入指定一个区域public Region(Rect r)  //传入一个矩形public Region(int left, int top, int right, int bottom) //传入两个点,其实就是一个矩形

方法二就是传入一个 Region 指定区域,方法三四都是一个意思,就是传入一个矩形。方法一是无参构造,Region 是可以后期指定代表区域的,以下是后期指定代表区域的方法:

public void setEmpty()  //清空public boolean set(Region region)   public boolean set(Rect r)   public boolean set(int left, int top, int right, int bottom)   public boolean setPath(Path path, Region clip)//将path和clip的两个区域取交集

以上的 set 方法都是指定新的区域来代替 Region 对象中原有的区域。

还有以上的方法在绘制图像过程中,cavas 没有直接绘制 Region 的方法,要绘制指定的 Region 需要使用 RegionIterator,RegionIterator 是一个迭代器,其主要作用是从指定的 Region 中获取 rect,也就是矩形。

1SetPath()

public boolean setPath(Path path, Region clip)//将path和clip的两个区域取交集

如注释,该方法的作用是将 path 区域和 clip 区域取交集。接下来演示一下:

public class RegionView extends View {   private Paint mPaint;   public RegionView(Context context) {       this(context, null);   }   public RegionView(Context context, AttributeSet attrs) {       this(context, attrs, 0);   }   public RegionView(Context context, AttributeSet attrs, int defStyleAttr) {       super(context, attrs, defStyleAttr);       initPaint();   }   private void initPaint() {       mPaint = new Paint();       mPaint.setColor(Color.GREEN);       mPaint.setStrokeWidth(3);       mPaint.setStyle(Paint.Style.STROKE);   }   @Override   protected void onDraw(Canvas canvas) {       super.onDraw(canvas);       //画一个圆       Path path = new Path();       path.addCircle(500, 500, 300, Path.Direction.CW);       //指定一个区域,然后取与圆的交集       Region region = new Region();       region.setPath(path, new Region(0, 0, 1000, 1000));       //绘制交集区域       RegionIterator iterator = new RegionIterator(region);       Rect rect = new Rect();       while (iterator.next(rect)) {           canvas.drawRect(rect, mPaint);       }   }}

上面代码可以看到,ReginIterator 类是依次取出构成区域大小不同的矩形,然后由 cavas 绘制,从而组成一个图形,下面看结果:

image

从图中可以看出,圆形是由若干个矩形组成,依次排列成圆形,因为代码中画笔使用的风格是 STROKE(描边),所以中间一些就是空的。如果使用 Fill(填充),那么组成的就是一个实心圆。

2区域的操作

public final boolean union(Rect r)   public boolean op(Rect r, Op op) {  public boolean op(int left, int top, int right, int bottom, Op op)   public boolean op(Region region, Op op)   public boolean op(Rect rect, Region region, Op op)

区域的操作有很多种,上面第一种 union() 主要是取并集,后面的 op() 方法就是 operation,操作的意思,具体如何操作?还要看后面的 op 参数来决定。Op 是一个枚举类,具体取值如下:

public enum Op {       DIFFERENCE(0), //取补集       INTERSECT(1),  //取交集       UNION(2),  //取并集       XOR(3),    //取异并集       REVERSE_DIFFERENCE(4),  //取反转补集       REPLACE(5);    //取后者区域代替前者       Op(int nativeInt) {           this.nativeInt = nativeInt;       }       /**        * @hide        */       public final int nativeInt;   }

代码示例如下:

public class RegionView extends View {   private Paint mPaint;   public RegionView(Context context) {       this(context, null);   }   public RegionView(Context context, AttributeSet attrs) {       this(context, attrs, 0);   }   public RegionView(Context context, AttributeSet attrs, int defStyleAttr) {       super(context, attrs, defStyleAttr);       initPaint();   }   private void initPaint() {       mPaint = new Paint();       mPaint.setColor(Color.GREEN);       mPaint.setStrokeWidth(3);       mPaint.setStyle(Paint.Style.STROKE);   }   @Override   protected void onDraw(Canvas canvas) {       super.onDraw(canvas);       Path path1 = new Path();       Path path2 = new Path();       path1.addCircle(500, 500, 300, Path.Direction.CW);       path2.addCircle(500, 800, 300, Path.Direction.CW);       canvas.drawPath(path1, mPaint);       canvas.drawPath(path2, mPaint);       Region region1 = new Region();       Region region2 = new Region();       region1.setPath(path1, new Region(0, 0, 1500, 1500));       region2.setPath(path2, new Region(0, 0, 1500, 1500));       region1.op(region2, Region.Op.REPLACE);       mPaint.setColor(Color.GREEN);       mPaint.setStyle(Paint.Style.FILL);       RegionIterator iterator = new RegionIterator(region1);       Rect rect = new Rect();       while (iterator.next(rect)) {           canvas.drawRect(rect, mPaint);       }   }}

各结果如下:

image

结尾

喜欢的朋友可以收藏点个赞哦。欢迎转发

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.12.07 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • View 的简介
  • Android 的相关坐标系
  • Android画笔的详解
    • BitmapShader
    • Android画布的详解
    • Canvas之Path的详解
      • 线操作
        • 点操作
          • 绘制常规图形
            • 绘制圆弧
              • 闭合path操作
                • 贝塞尔曲线
                • PathMeasure的详解
                • Region区域
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档