View基础

450 阅读8分钟

Q1:什么是View?

View是屏幕上的一块矩形区域

Q2:Google为什么设计View?

设计View的目的是为了解决用户和应用之间交互

Q3:怎么样交互?

绘制自己事件处理两种方式与用户交互。

Q4:矩形区域在屏幕的位置和大小由谁来决定?

      一个界面有很多View或者ViewGroup,google用window先去加载一个超级复合View,用它来包含住所有的其他View,这个超级复合View就叫做DecorView。界面中被包裹的View将能直接或间接被DecorView中的FrameLayout去分配位置和大小(大小的控制还和view自身有关

Q5:DccorView中的FrameLayout怎么分配位置?

layout_*之类的配置虽然在书写上与子View的属性在一起,但它们并不是子View的属性,它们只是父ViewGroup提供给开发者用来安排该View在父ViewGroup中的位置和大小的,同时,这些值由ViewGroup读取,然后生成一个ViewGroup特定的LayoutParams对象,再把这个对象存入子View中的,这样,在页面渲染时候(也就是调用layout方法),就可以参考这个LayoutParams中的信息,然后统一出每个view的正确位置。

总结:普通View只能被安排,被嵌套的ViewGroup不仅要被安排,而且还要安排它的子View位置。子View的位置信息由开发者告诉父View直接控制,不需要子View的参与。

为了证明上总结我们画一个圆:

class MyView : View{

    val paint: Paint//画笔
    //构造方法
    constructor(context: Context?) : this(context, null)
    //构造方法
    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
    //构造方法
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        //抗锯齿
        paint = Paint(Paint.ANTI_ALIAS_FLAG)
        //设置画笔颜色
        paint.color = Color.BLACK

    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawCircle(100F,100F,100F,paint)

    }
}

然后布局使用layout开头的属性(除了宽度和高度的设置,后边讲解)照样管用,由此更加验证了上边总结。

Q6:怎么确定View的大小?

开发者向ViewGroup表达我这个子View需要的大小,然后ViewGroup会综合考虑自己的空间大小以及开发者的请求,然后生成两个MeasureSpec对象(生成这两个对象的方法在ViewGroup的getChildMeasureSpec方法中),通过子View的onMeasure方法传递给子类,这两个对象是ViewGroup向子View提出的要求,就相当于告诉子View:“我已经与你的使用者(开发者)商量过了,现在把我们商量确定的结果告诉你,你的宽度不能违反width MeasureSpec对象的要求,你的高度不能违反height MeasureSpec对象的要求,现在,你赶紧根据这个要求确定下自己要多大空间,只许少,不许多哦。然后告诉我

分析getChildMeasureSpec源码

/**
 * @param spec           父布局给的spec
 * @param padding        父布局的边距
 * @param childDimension 开发者在布局文件中要求的大小layout_width 和layout_height的值
 * @return返回父布局和开发者商量过后的布局大小
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //获取父View的测量模式和大小
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    //计算出我能给子View的最大值
    int size = Math.max(0, specSize - padding);
    //初始化子View的大小和模式
    int resultSize = 0;
    int resultMode = 0;
    //根据自身模式去给子view的大小和模式赋值
    switch (specMode) {
        case MeasureSpec.EXACTLY:
            // 当子view的LayoutParams>0,即有确切的值,把这个确切的值给子view,
            // 同时模式是:EXACTLY
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                //如果是撑满,就把父View能给他的最大值给子View,
                // 同时模式是:EXACTLY
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                //如果是包裹内容,也把父View能给他的最大值给它,
                //很明显这里是包裹内容,但是确给了最大值,记住这里是个错误值,
                // 它不是不想给,而是不确定也给不了,需要你自己测量后给我,然后我layout的时候才能修改成正确值
                // 也很好的解释了为什么MATCH_PARENT和WRAP_CONTENT默认是撑满父布局
                // 注意这里模式是不要超过这个值
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // 道理同上
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                //注意这里模式改变了
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {

                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
        //一般是滑动父布局
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // 道理同上
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                // 因为父view为UNSPECIFIED,可以滚动的,所以这里这个限制值没有意义6.0之前返回是0,6.0之后返回是开发者要求的大小
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                // 因为父view为UNSPECIFIED,可以滚动的,所以这里这个限制值没有意义6.0之前返回是0,6.0之后返回是开发者要求的大小
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

总结

只要给一个确定大小的值,全部都是恰好模式。值也是开发者给的值。其余只要遇到包裹内容或者包裹内容模式,子类全部是包裹内容。只要遇到不确定模式,全部是不确定模式。

Q7:MeasureSpec对象中的要求怎么获取以及含义是什么?

var widthMode = MeasureSpec.getMode(widthMeasureSpec)
var widthSize =  MeasureSpec.getSize(widthMeasureSpec)
var heightMode = MeasureSpec.getMode(heightMeasureSpec)
var heightSize =  MeasureSpec.getSize(heightMeasureSpec)

假设size是100dp,Mode的取值有三种,它们代表了ViewGroup的总体态度:

  • EXACTLY  标记,ViewGroup对View说,你只能用100dp,原因是多样的,可能是你的开发者说要你完全占据我的空间,而我只有100dp。也可能这是你的开发者的要求,他需要你占这么大的空间。
  • AT_MOST标记,你最多只能用100dp。这是因为你的开发者说让你占据wrap_content的大小,让我跟你商量,我又不知道你到底要占多大区域,但是我告诉你,我只有100dp,你最多也只能用这么多。
  • UNSPECIFIED标记,把你最理想的大小告诉我,我可以滚动,你随便来

这里就说明了必须要自己测量大小然后给父View否则wrap_content不管用。

Q8:子View怎么测量自己需要的大小?

不同的View有不同的态度,但是有几点基本的规矩是要遵守的:不要违反ViewGroup的规定,google给我们提供方便的方法来遵照这一固定resolveSize(真实需要的宽高(简单的数学算法), widthMeasureSpec(父类的要求)),返回值是优化后遵照这一规定的宽高。查看源码发现调用resolveSizeAndState(size, measureSpec, 0)方法。这里多了一个标记,目的是告诉父布局,我是委屈的宽高。

/**
 *
 * @param size 需要的真实大小
 * @param measureSpec 父布局的规定
 * @param childMeasuredState 不知道是什么
 * @return  返回一个优化后的宽高
 */
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    //获取父布局的模式
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {//如果测量的比能给的还要大,就给它我能提供的最大限度
                result = specSize | MEASURED_STATE_TOO_SMALL;//标记我想要的更大
            } else {
                result = size;//如果给的比要求的小,没有问题,直接赋值
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;//直接给我的最大限度,或者开发者要求的值
            break;
        case MeasureSpec.UNSPECIFIED://给你需要的真实值,因为我可以滚动
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

Q9:子View按照要求测量自己的大小后怎么告诉父View?

根据上边google提供的方法,就可以计算优化出一个满足父布局要求的自己的尺寸,这个时候就需要告诉父布局我需要的真实尺寸,进而去修改上边在包裹内容时候的错误数据,保证包裹内容的数据是正确的,在使用layout的时候就会用矫正过的正确值去渲染控件大小。

google给我们提供了方法setMeasuredDimension(计算优化后的真实宽,计算优化有的真实高);

到此当渲染的时候每个view都在自身储存了自己的位置和大小信息,关于View的绘制,就是一个方法onDraw,保证在当时测量的大小范围内去绘制就行。后边讲解绘制的细节。

总结:一般onMeasure方法中的代码都是告诉父布局,我真实的大小:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    setMeasuredDimension(resolveSize(真实需要大小, widthMeasureSpec), resolveSize(真实需要大小, heightMeasureSpec))
}

Q10:View中都有哪些方法?


方法基本执行顺序:https://blog.csdn.net/anhenzhufeng/article/details/72886181

小结:

  • View是一个矩形区域,设计它的目的是为了和用户交互,交互的方式有自我绘制和事件处理
  • View的位置由开发者告诉父布局,父布局直接决定位置
  • View的大小有开发者和父布局和自身大小共同商量决定
  • 自定义View中常重写的方法,以及这些方法会改变什么

认识了什么是View,知道大小和位置是由哪些代码决定的,接下来就是绘制和处理事件,接下来几篇文章准备写一些常用的绘制方法和技巧。