目录
自定义 View 的工作流程和内容
工作流程
测量阶段和布局阶段的工作内容
View 和 ViewGroup 在测量阶段和布局阶段的区别
绘制阶段的工作内容
上手: 实现继承 View 的自定义 View
上手: 自定义 ViewGroup
补充: 绘制内容的关键点
自定义 View, 可以分为具体的三大类:
自定义 View(继承系统控件 / 继承 View)
自定义 Viewgroup(继承系统特定的 Viewgroup / 继承 ViewGround)
自定义组合控件
自定义 View 的工作流程和内容
工作流程
无论是哪一类 View, 只要是 View, 都需要经过以下工作流程:
测量 measure->布局 layout->绘制 draw.
measure 阶段测量 View 的宽高
layout 阶段用来确定 View 的位置
draw 阶段则是用来绘制 View.
测量阶段和布局阶段的工作内容
测量阶段(measure): 从上到下递归地调用每个 View 或者 ViewGroup 的 measure() 方法, 测量他们的尺寸并计算它们的位置;
布局阶段(layout): 从上到下递归地调用每个 View 或者 ViewGroup 的 layout() 方法, 把测得的它们的尺寸和位置赋值给它们.
View 和 ViewGroup 在测量阶段和布局阶段的区别
测量阶段, measure() 方法被父 View 调用, 在 measure() 中做一些准备和优化工作后, 调用 onMeasure() 来进行实际的自我测量. onMeasure() 做的事, View 和 ViewGroup 不一样:
View:View 在 onMeasure() 中会计算出自己的尺寸然后保存;
ViewGroup:ViewGroup 在 onMeasure() 中会调用所有子 View 的 measure() 让它们进行自我测量, 并根据子 View 计算出的期望尺寸来计算出它们的实际尺寸和位置 (实际上 99.99% 的父 View 都会使用子 View 给出的期望尺寸来作为实际尺寸, 原因在下期或下下期会讲到) 然后保存. 同时, 它也会根据子 View 的尺寸和位置来计算出自己的尺寸然后保存;
布局阶段, layout() 方法被父 View 调用, 在 layout() 中它会保存父 View 传进来的自己的位置和尺寸, 并且调用 onLayout() 来进行实际的内部布局. onLayout() 做的事, View 和 ViewGroup 也不一样:
View: 由于没有子 View, 所以 View 的 onLayout() 什么也不做.
ViewGroup:ViewGroup 在 onLayout() 中会调用自己的所有子 View 的 layout() 方法, 把它们的尺寸和位置传给它们, 让它们完成自我的内部布局.
绘制阶段的工作内容
在官方注释中, 绘制分为以下步骤:
如果需要, 就绘制背景 --drawBackgrounp()
保存当前 canvas 层
绘制 View 的内容 --onDraw()
绘制子 View--dispatchView()
如果需要, 就绘制 View 的褪色边缘, 类似于阴影效果
绘制装饰, 比如滚动条 --onDrawForeground()
其中第 2,5 步可以跳过. 具体如何绘制内容将在文末补充.
上手: 实现继承 View 的自定义 View
我们通过继承 View 实现一个自定义 View, 往往需要实现以下内容:
绘制内容(draw)
对 Padding 进行处理(draw)
对 wrap_content 进行处理(measure)
创建自定义属性, 配置自己的自定义 View
重写 onTounchEvent()改变触摸反馈
括号为涉及到的工作流程.
例:
在界面中, 创建一个可以滑动的矩形
java 代码:
- public class CustomView extends View {
- Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
- int lastx;
- int lasty;
- int mColor;
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- int x= (int) event.getX();
- int y= (int) event.getY();
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- lastx=x;
- lasty=y;
- break;
- case MotionEvent.ACTION_MOVE:
- int offsetX=x-lastx;
- int offsetY=y-lasty;
- ((View)getParent()).scrollBy(-offsetX,-offsetY);
- break;
- case MotionEvent.ACTION_UP:
- break;
- }
- return true;
- }
- public CustomView(Context context) {
- super(context);
- }
- public CustomView(Context context, AttributeSet attrs) {
- super(context, attrs);
- // 提取 CustomView 属性集合的 rect_colot 属性, 如果不设置, 默认为红色.
- TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.CustomView);
- mColor=typedArray.getColor(R.styleable.CustomView_rect_color, Color.RED);
- typedArray.recycle();
- paint.setColor(mColor);
- paint.setStrokeWidth((float)1.5);
- }
- public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
- public CustomView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
- /*
- widthMeasureSpec 和 heightMeasureSpec 分别压缩了 mode 和 size 两个信息.
- mode 的分类:
- UNSPECIFIED: 不限制, 相当于 match_parent
- AT_MOST: 限制上限, 相当于 wrap_content
- EXACTLY: 限制固定值
- */
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- // 重新 onMeasure(), 并计算出 View 的尺寸;
- //(可以使用 resolveSize() 来让子 View 的计算结果符合父 View 的限制, 也可以用自己的方式来满足父 View 的限制也行), 本例子使用自己方式满足父 View 的限制.
- // 对 wrap_content 进行处理
- // 在 onMeasure 方法中指定一个默认的宽高, 在设置 wrap_content 属性时设置此默认宽高
- int widthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
- int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);
- int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec);
- int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec);
- if(widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){
- setMeasuredDimension(80,80);
- }else if(widthSpecMode==MeasureSpec.AT_MOST){
- setMeasuredDimension(80,heightSpecSize);
- }else if(heightSpecMode==MeasureSpec.AT_MOST){
- setMeasuredDimension(widthSpecSize,80);
- }
- /*resolveSize(int size, int widthMeasureSpec)
- 方法内部的实现方式与例子自定义实现方式相似, 从 widthMeasureSpec 得到
- mode 的类别作判断
- UNSPECIFIED: 不限制, 返回 size
- AT_MOST: 限制上限, size>MeasureSpec.getSize(widthMeasureSpec), 返回后者, 否则返回直接 size
- EXACTLY: 限制固定值, 返回 MeasureSpec.getSize(widthMeasureSpec)
- */
- /*setMeasuredDimension(
- resolveSize(int size,int widthMeasureSpec),
- resolveSize(int size,int heightMeasureSpec)
- );
- */
- }
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- // 对 padding 进行处理
- int paddingLeft=getPaddingLeft();
- int paddingRight=getPaddingRight();
- int paddingTop=getPaddingTop();
- int paddingBottom=getPaddingBottom();
- int width=getWidth()-paddingLeft-paddingRight;
- int height=getHeight()-paddingTop-paddingBottom;
- // 根据 padding 绘制矩形
- canvas.drawRect(0+paddingLeft,0+paddingTop,width+paddingLeft,height+paddingTop,paint);
- }
- }
xml 文件:
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout
- xmlns:Android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- xmlns:App="http://schemas.android.com/apk/res-auto"
- Android:layout_width="match_parent"
- Android:layout_height="match_parent"
- tools:context=".MainActivity">
- <com.example.drawtest.CustomView
- App:rect_color="@color/colorAccent"
- Android:layout_width="wrap_content"
- Android:layout_height="wrap_content"
- />
- </LinearLayout>
以 Android 开头的都是系统自带的属性, 自定义属性需要在 values 目录下创建 attrs.xml:
- <?xml version="1.0" encoding="utf-8"?>
- <resources>
- <declare-styleable name="CustomView">
- <attr name="rect_color" format="color"/>
- </declare-styleable>
- </resources>
上手: 自定义 ViewGroup
自定义 ViewGroup 的步骤其实就是对 View 工作流程中的测量阶段, 布局阶段对应方法进行重写:
重写 onMesure()
重写 onLayout()
重写 onMeasure() 的步骤:
1. 遍历子 View, 根据子 View 的 LayoutParams 属性以及 ViewGroup 的 MeasureSpec 模式得到子 View 的 MeasureSpec
(子 View 的 LayoutParams 就是开发者对子 View 宽高等与位置相关属性的要求, 而 ViewGroup 的 mode 则是开发者对 ViewGroup 宽高属性的要求. 更简单的说, 子 View 的 LayoutParams 保存了子 View 的 xml 代码中的 Layout_height,Layout_width 等有关的位置信息属性, ViewGroup 的 mode 保存了 ViewGroup 的 xml 代码中的 Layout_height,Layout_width 属性).
2. 把计算出的子 View 的 childWidthSpec 和 childHeightSpec 作为参数传入子 View 的 measure()方法 来计算子 View 的尺寸
3. 子 View 在 onMeasure()中计算自己最终的位置和尺寸利用 setMeasuredDimension()方法保存
4.ViewGroup 通过子 View 的位置和尺寸确定自己的尺寸并用 setMeasuredDimension() 保存
重写 onLayout() 的方式
在 onLayout() 里调用每个子 View 的 layout() , 让它们保存自己的位置和尺寸.
补充: 绘制内容的关键点
自定义绘制的方式最常用的方式是重写 onDraw()绘制方法
绘制的关键是 Canvas 的使用
Canvas 的绘制类方法: drawXXX() (关键参数: Paint)
Canvas 的辅助类方法: 范围裁切和几何变换
Paint:
Paint 的 API 大致可以分为 4 类: 颜色, 效果, drawText() 相关, 初始化.
颜色类的 API 作用包括: 直接设置颜色的 API 用来给图形和文字设置颜色(纯色, 渐变色); 加滤镜; 用来处理源图像和 View 已有内容的关系.
效果类的 API 可以实现抗锯齿, 填充 / 轮廓, 线条宽度, 线头形状, 线拐角, 线性过滤 (使图像过渡平缓) 等等.
drawText()与初始化使用较少不作介绍.
Canvas 范围裁剪和几何变换:
范围裁剪可以得到对原图像进行裁剪得到各种形状的图像, 比如输入方形图片, 输出圆形头像.
几何变换可以实现平移, 旋转, 缩放, 错切效果;
可以使用不同的绘制方法来控制遮盖关系
参考学习网站: HenCoder https://hencoder.com/
参考学习书籍: Android 进阶之光 - 刘望舒
来源: http://www.bubuko.com/infodetail-3060843.html