前言

View,有很多的名称。不论是你熟知的布局,还是控件,他们全部都继承自View
View部分继承图

工作流程

layout

在确定位置时,我们有一个非常需要主要的地方—— 坐标系。Android系统的坐标系和平时画的坐标系并不相同。
Android屏幕坐标轴

所以相对应的,我们的位置计算方法自然和我们原来的正好是相反的。
View位置计算

4个顶点的位置分别由4个值决定:

  • top:子View上边界到所在容器上边界的距离。
  • left:子View左边界到所在容器左边界的距离。
  • bottom:子View下边界到所在容器上边界的距离。
  • right:子View右边界到所在容器左边界的距离。

    所有的计算都是相对于所在容器才能够开始的。

measure

其实通过layout中的第二张图我们已经知道了控件大小的计算了。

  • height = bottom - top
  • width = right - left

    对于ViewGroup而言,就是对容器内子控件的遍历和计算了。

因为直接继承自View的控件使用wrap_cotentmatch_parent是显示出来的效果是相同的。需要我们使用MeasureSpec中的getMode()方法来对当前的模式进行区分和比较。

模式 状态
UNSPECIFIED 未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量
AT_MOST 最大模式,对应wrap_content,View的大小不大于SpecSize的值
EXACTLY 精确模式,对应match_parent,View的大小为SpecSize的值
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //用于获取设定的模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        // 用于获取设定的长度
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        // 类似这样的判断,后面不过多复述
        // 用于判断是不是wrap_content
        // 如果不进行处理,效果会是match_parent
        if(widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(20, 20);
        }
    }

draw

一共有6个步骤:

  1. 如果需要,则绘制背景
  2. 保存当前canvas层
  3. 绘制View的内容
  4. 绘制子View
  5. 如果需要,则绘制View的褪色边缘,类似于阴影效果
  6. 绘制装饰,比如滚动条

关于开发者需要重写的方法一般是第三步绘制View的内容对应的onDraw()

    private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        // 在画布上进行类似这样的操作
        canvas.drawLine(0, height/2, width,height/2, paint);
    }

自定义View入门

在日常项目的布局文件中我们经常会使用到xmlns:app="http://schemas.android.com/apk/res-auto"这样标签,其实他就是用来引入我们自定义的标签使用的。

  1. 在res/values目录下创建attrs

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
     <declare-styleable name="DefaultView">
         <attr name="color" format="color"/>
     </declare-styleable>
    </resources>
  2. DefaultView(Context context, @Nullable AttributeSet attrs)中获取。以下是整个完整代码。

    /**
    * author: ClericYi
    * time: 2020-01-30
    */
    public class DefaultView extends View {
     private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
     private int mColor = Color.RED;
    
     public DefaultView(Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
         initAttrs(context, attrs);
         initDraw();
     }
    
     private void initAttrs(Context context, @Nullable AttributeSet attrs) {
         TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DefaultView);
         // 从styleable中获取的名字是系统会生成的,一般是 类名_name 的形式
         mColor = array.getColor(R.styleable.DefaultView_color, Color.GREEN);
         // 获取完资源后即使回收
         array.recycle();
     }
    
     private void initDraw() {
         paint.setColor(mColor);
         paint.setStrokeWidth(3f);
     }
    
     @Override
     protected void onDraw(Canvas canvas) {
         super.onDraw(canvas);
         int width = getWidth();
         int height = getHeight();
         canvas.drawLine(0, height/2, width,height/2, paint);
     }
    
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    
         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
         int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
         if(widthMode == MeasureSpec.AT_MOST){
             setMeasuredDimension(20, 20);
         }
     }
    }

如果有什么我没有思考到的地方或是文章内存在错误,欢迎与我分享。