🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] 自定义 View 系列参考学习资料:[https://github.com/GcsSloop/AndroidNote](https://github.com/GcsSloop/AndroidNote) # 基础掌握 ## 坐标系 * 移动设备定义屏幕左上角为坐标原点,向右为x轴增大方向,向下为y轴增大方向。 * View 的坐标系是相对父控件而言,getTop()方法获取子 View 左上角距父 View 顶部的距离,getLeft()方法获取子 View 左上角距父 View 左侧的距离。getBottom() 和 getRight()同理。 * MotionEvent 中 event 的 getX()和 get()方法获取的是触摸点相对于其所在组件坐标系的坐标;event.getRawX() 和 event.getRawY()获取的是触摸点相对于屏幕坐标系的坐标。 ![](https://box.kancloud.cn/9ccf77af2345fd7b21a5daec8ee0c9cc_316x480.png) ## 角度弧度 ![](https://box.kancloud.cn/f5db71f6ea089fd3c994d5e569955a96_662x510.jpg) 360(角度)= 2π(弧度) 180(角度)= π(弧度) ## 几种使用颜色的方式 1、java 中定义 ```java int color = Color.GRAY; int color = Color.argb(127, 255, 0, 0); int color = 0xaaff0000; ``` 2、xml 中定义 ```xml <color name="red">#ff0000</color> ``` 3、java 中引用 xml 中定义的颜色 ```java int color = ContextCompat.getColor(mContext, Color.RED); int color = getColor(R.color.red); // API 需要大于23 ``` 4、xml 中引用颜色 ```xml android:background="@color/red" // 使用 xml 中定义的颜色 android:background="#ff0000" // 直接创建并使用颜色 ``` # 自定义 View 分类及流程 ## 分类 1、继承View重写onDraw方法 一般用于实现一些不规则的效果,通过重写onDraw方法进行绘制。采用这种方式需要自己支持wrap_content和padding的处理。 2、继承ViewGroup派生特殊的Layout 一般用于实现自定义布局,当某种效果看起来很像几种View组合在一起的时候,可使用此种方法实现。需要合适的处理ViewGroup的测量、布局过程,并同时处理子View的测量和布局过程。 3、继承特定的View(比如TextView) 一般扩展某种已有的View的功能,比如TextView。不需要自己支持wrap_content和padding等 4、继承特定的ViewGroup(比如LinearLayout) 当某种效果看起来很像几种View组合在一起的时候,采用此方法实现。不需要自己处理ViewGroup的测量和布局。 ## 流程 ![](https://box.kancloud.cn/4d7c7a3bdc5f8fff8fa9ac975676e15c_552x626.jpeg) 1、View 初始化(构造函数) 构造函数是View的入口,可以用于初始化一些的内容,和获取自定义属性。 View的构造函数有四种重载分别如下: ```java // 一般在直接 new 一个 View 的时候调用(如在 Activity 中) public void SloopView(Context context) {} // 一般在 layout 文件中使用的时候会调用,关于它的所有属性(包括自定义属性)都会包含在 attrs 中传递进来(如在布局文件中) public void SloopView(Context context, AttributeSet attrs) {} // 第三个参数是默认的 Style,这里的默认的 Style 是指它在当前 Application 或 Activity 所用的 Theme 中的默认 Style,且只有在明确调用的时候才会生效 public void SloopView(Context context, AttributeSet attrs, int defStyleAttr) {} // 在 API21 的时候才添加上,可以暂不考虑 public void SloopView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {} ``` 2、测量 View 大小(onMeasure 方法) 因为 View 的大小不仅由自身所决定,同时也会受到父控件的影响,所以为了我们的控件能更好的适应各种情况,一般会自己进行测量。 可以从onMeasure的两个参数中取出宽高的相关数据: ```java @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthsize = MeasureSpec.getSize(widthMeasureSpec); //取出宽度的确切数值 int widthmode = MeasureSpec.getMode(widthMeasureSpec); //取出宽度的测量模式 int heightsize = MeasureSpec.getSize(heightMeasureSpec); //取出高度的确切数值 int heightmode = MeasureSpec.getMode(heightMeasureSpec); //取出高度的测量模式 } ``` 测量模式有如下三种: | 模式 | 描述 | | --- | --- | | UNSPPECIFIED | 默认值,父控件没有给子view任何限制,子View可以设置为任意大小。 | | EXACTLY | 表示父控件已经确切的指定了子View的大小。| | AT_MOST | 表示子View具体大小没有尺寸限制,但是存在上限,上限一般为父View大小。 | 注意: 如果对View的宽高进行修改了,不要调用super.onMeasure(widthMeasureSpec,heightMeasureSpec);要调用setMeasuredDimension(widthsize,heightsize); 这个函数。 3、确定 View 大小(onSizeChanged 方法) 这个函数在视图大小发生改变时调用。因为View的大小不仅由View本身控制,而且受父控件的影响,所以我们在确定View大小的时候最好使用系统提供的onSizeChanged回调函数。 ```java @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); } ``` 4、确定子 View 布局(onLayout 方法) 用于确定子 View 位置,在自定义 ViewGroup 中会用到,它调用的是子 View的 layout 函数。 在自定义 ViewGroup 中,onLayout 一般是循环取出子 View,然后经过计算得出各个子 View 位置的坐标值,然后用以下函数设置子 View 位置。 ```java child.layout(l, t, r, b); ``` 四个参数分别为: |名称|说明|对应函数| |---|----|---| l|View 左侧距父 View 左侧的距离|getLeft() t|View 顶部距父 View 顶部的距离|getTop) r|View 右侧距父 View 右侧的距离|getRight() b|View 底部距父 View 底部的距离|getBottom() 5、绘制内容(onDraw 方法) onDraw 方法使用的是Canvas绘图。 ```java @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } ``` 6、对外提供操作方法和监听回调 自定义完View之后,一般会对外暴露一些接口,用于控制View的状态等,或者监听View的变化。 # Canvas 基本绘制 ## 画布 Canvas Canvas我们可以称之为画布,能够在上面绘制各种东西,是安卓平台2D图形绘制的基础 ## 画笔 Paint ```java // 1.创建一个画笔 private Paint mPaint = new Paint(); // 2.初始化画笔 private void initPaint() { mPaint.setColor(Color.BLACK); //设置画笔颜色 mPaint.setStyle(Paint.Style.FILL); //设置画笔模式为填充 mPaint.setStrokeWidth(10f); //设置画笔宽度为10px } // 3.在构造函数中初始化 public SloopView(Context context, AttributeSet attrs) { super(context, attrs); initPaint(); } ``` 画笔相关常用方法表: |标题|相关方法|备注| |---|---|---| |色彩|setColor setARGB setAlpha|设置颜色,透明度| |大小|setTextSize|设置文本字体大小| |字体|setTypeface|设置或清除字体样式| |样式|setStyle|填充(FILL),描边(STROKE),填充加描边(FILL_AND_STROKE)| |对齐|setTextAlign|左对齐(LEFT),居中对齐(CENTER),右对齐(RIGHT)| |测量|measureText|测量文本大小(注意,请在设置完文本各项参数后调用)| 画笔的三种填充模式如下: ```plain STROKE //描边 FILL //填充 FILL_AND_STROKE //描边加填充 ``` 效果图如下: ![](https://box.kancloud.cn/1f5c17054e3186822bc069f86ccf1978_1080x1920.jpeg) ## Canvas 的常用操作速查表 |操作类型|相关 API|备注 |---|---|---| |绘制颜色|drawColor, drawRGB, drawARGB|使用单一颜色填充整个画布| |绘制基本形状|drawPoint, drawPoints, drawLine, drawLines, drawRect, drawRoundRect, drawOval, drawCircle, drawArc|依次为 点、线、矩形、圆角矩形、椭圆、圆、圆弧| |绘制图片|drawBitmap, drawPicture|绘制位图和图片| |绘制文本|drawText, drawPosText, drawTextOnPath|依次为 绘制文字、绘制文字时指定每个文字位置、根据路径绘制文字| |绘制路径|drawPath|绘制路径,绘制贝塞尔曲线时也需要用到该函数| |顶点操作|drawVertices, drawBitmapMesh|通过对顶点操作可以使图像形变,drawVertices直接对画布作用、 drawBitmapMesh只对绘制的Bitmap作用| |画布剪裁|clipPath, clipRect|设置画布的显示区域| |画布快照|save, restore, saveLayerXxx, restoreToCount, getSaveCount|依次为 保存当前状态、 回滚到上一次保存的状态、 保存图层状态、 回滚到指定状态、 获取保存次数| |画布变换|translate, scale, rotate, skew|依次为 位移、缩放、 旋转、错切 |Matrix(矩阵)|getMatrix, setMatrix, concat|实际画布的位移,缩放等操作的都是图像矩阵Matrix,只不过Matrix比较难以理解和使用,故封装了一些常用的方法。| ## Canvas 绘制部分图形 1、绘制圆角矩形 ```java // 第一种 RectF rectF = new RectF(100,100,800,400); canvas.drawRoundRect(rectF,30,30,mPaint); // 第二种 canvas.drawRoundRect(100,100,800,400,30,30,mPaint); ``` 与绘制矩形相比,圆角矩形多出来了两个参数rx 和 ry,这两个参数是椭圆的两个半径,如图所示: ![](https://box.kancloud.cn/03922874925c5167260a3b766ef1847c_300x500.jpeg) 2、绘制圆弧 方法如下: ```java // 第一种 public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint){} // 第二种 public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint) {} ``` 多出的三个参数: ```plain startAngle // 开始角度 sweepAngle // 扫过角度 useCenter // 是否使用中心 ``` 3、绘制位图 推荐通过 BitmapFactory 获取 Bitmap 常用方法有: ```java // 第一种,后两个参数(matrix, paint)是在绘制的时候对图片进行一些改变 public void drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint) // 第二种,在绘制时指定了图片左上角的坐标(距离坐标原点的距离) public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint) // 第三种,参数 src 指定绘制图片的区域,参数 dst 指定图片在屏幕上显示(绘制)的区域 public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint) public void drawBitmap (Bitmap bitmap, Rect src, RectF dst, Paint paint) ``` 4、绘制文字 方法如下: ```java // 第一类:只能指定文本基线位置(基线x默认在字符串左侧,基线y默认在字符串下方) public void drawText (String text, float x, float y, Paint paint) public void drawText (String text, int start, int end, float x, float y, Paint paint) public void drawText (CharSequence text, int start, int end, float x, float y, Paint paint) public void drawText (char[] text, int index, int count, float x, float y, Paint paint) // 第二类:可以分别指定每个文字的位置 public void drawPosText (String text, float[] pos, Paint paint) public void drawPosText (char[] text, int index, int count, float[] pos, Paint paint) // 第三类:指定一个路径,根据路径绘制文字 public void drawTextOnPath (String text, Path path, float hOffset, float vOffset, Paint paint) public void drawTextOnPath (char[] text, int index, int count, Path path, float hOffset, float vOffset, Paint paint) ``` 注意:关于文字居中问题! > drawText()方法中x,y指的是基线中间的那个点,是因为setTextAlign(Paint.Align.Center) 那么要想在正中间显示文字,x只要为矩形的中点x坐标即可 x = rect.centerX() 要计算的就是基线中间图上红色点的y坐标了,看图可以发现红色点的y为矩形中点黑色点的y坐标+图中黑色点和红色点之间的距离 矩形y坐标为 rect.centerY() 黑色点和红色点之间的距离为相对于基线的(top+bottom)/2 - bottom 而 top是相对于基线的所以为负数,所以公式为 (-top+bottom)/2 - bottom简化下为-top/2 - bottom/2 所以最后计算为rect.centerY - top/2 - bottom/2. ![](https://box.kancloud.cn/ea3e7d2beec7477600c75ffd9fba8bf2_545x452.jpg) 示例代码如下: ```java Rect rect = new Rect(100,100,500,500);//画一个矩形 Paint rectPaint = new Paint(); rectPaint.setColor(Color.BLUE); rectPaint.setStyle(Paint.Style.FILL); canvas.drawRect(rect, rectPaint); Paint textPaint = new Paint(); textPaint.setColor(Color.WHITE); textPaint.setTextSize(50); textPaint.setStyle(Paint.Style.FILL); //该方法即为设置基线上那个点究竟是left,center,还是right 这里我设置为center textPaint.setTextAlign(Paint.Align.CENTER); Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); float top = fontMetrics.top;//为基线到字体上边框的距离,即上图中的top float bottom = fontMetrics.bottom;//为基线到字体下边框的距离,即上图中的bottom int baseLineY = (int) (rect.centerY() - top/2 - bottom/2);//基线中间点的y轴计算公式 canvas.drawText("你好世界",rect.centerX(),baseLineY,textPaint); ``` 参考链接:[https://blog.csdn.net/zly921112/article/details/50401976](https://blog.csdn.net/zly921112/article/details/50401976) ## Canvas 操作 注意:所有的画布操作都只影响后续的绘制,对之前已经绘制过的内容没有影响。 1、位移(translate) translate 是坐标系的移动 可以为图形绘制选择一个合适的坐标系。 注意,位移是基于当前位置移动,而不是每次基于屏幕左上角的(0,0)点移动 2、缩放(scale) 方法如下: ```java public void scale (float sx, float sy) public final void scale (float sx, float sy, float px, float py) ``` 前两个参数是相同的分别为x轴和y轴的缩放比例。而第二种方法比前一种多了两个参数,用来控制缩放中心位置的。 缩放比例(sx,sy)取值范围详解: |取值范围|说明| |---|---| |(-∞, -1)|先根据缩放中心放大 n 倍,再根据中心轴进行翻转| |-1|根据缩放中心轴进行翻转| |(-1, 0)|先根据缩放中心缩小到 n,再根据中心轴进行翻转| |0|不会显示,若 sx 为 0,则宽度为 0,不会显示,sy 同理| |(0, 1)|根据缩放中心缩小到 n| |1|没有变化| |(1, +∞)|根据缩放中心放大 n 倍| 3、旋转(rotate) 方法如下: ```java public void rotate (float degrees) // 多出来的两个参数依旧是控制旋转中心点的 public final void rotate (float degrees, float px, float py) ``` 4、错切(skew) 错切是特殊类型的线性变换,方法如下: ```java public void skew (float sx, float sy) ``` 参数含义: float sx:将画布在 x 方向上倾斜相应的角度,sx 倾斜角度的 tan 值 float sy:将画布在 y 轴方向上倾斜相应的角度,sy 为倾斜角度的 tan 值 5、快照与回滚(save 与 restore) 画布的操作是不可逆的,而且很多画布操作会影响后续的步骤,例如第一个例子,两个圆形都是在坐标原点绘制的,而因为坐标系的移动绘制出来的实际位置不同。所以会对画布的一些状态进行保存和回滚。 相关 API: |相关 API|简介| |---|---| |save|把当前的画布的状态进行保存,然后放入特定的栈中| |saveLayerXxx|新建一个图层,并放入特定的栈中| |restore|把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布| |restoreToCount|弹出指定位置及其以上所有的状态,并按照指定位置的状态进行恢复| |getSaveCount|获取栈中内容的数量(即保存次数)|