Android群英传读书笔记第三章(Android控件架构与自定义View)

本文深入解析了Android中View的测量机制,重点介绍了View默认onMeasure方法仅支持EXACTLY模式的原因,以及自定义View时为何需要重写measure方法及其重要性。

概述

Android群英传这本书已经入手一段时间了,没有抽出时间来好好看,从今天开始,准备仔细研读一下,并且把每章值得留意的地方以笔记的形式记录下来,方便以后复习,这是第一篇,因为前两章分别讲的是Android的体系结构与相关的开发工具,都是一些概念性的东西,没有什么难度,大家知道就行了。好了,开始我们的笔记吧!

笔记

View的测量
1.为什么说View默认的onMeasure方法只支持EXACTLY模式?

解答这个问题,我们先看看View中onMeasure方法的定义,

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

measure方法接收两个参数,这两个参数分别封装了宽度的大小和模式以及高度的大小和模式,是由父类计算后,传递下来的,那么谁调用了这个方法呢?
View的测量是从ViewRootImpl的performTraversals函数开始的,在这个方法里面依次会调用View的measure,layout,draw方法。我们看看ViewRootImpe中调用View的measure方法的地方。


             mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

那么这个mView是什么呢?通过查找源码,我们发现有如下的代码

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                ....
                }

mView是从setView这个方法传过来的,那么setView中的这个View是什么呢?是DecorView,这里我就不详细分析了,有兴趣的同学可以看我的另一篇文章,这里会有介绍,Activity启动流程以及View的绘制流程,调用DecorView的measure方法,然后传入两个参数,我们看这两个参数是如何定义的。

 childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);

都调用了getRootMeasureSpec方法,传入两个参数,第一个参数为Window的尺寸,很显然这个尺寸是固定的,第二个参数是代表Window的布局参数,

private int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

这个方法根据传进来的rootDimension返回一个封装了模式和大小的变量,很显然我们传进来的rootDimension是MATCH_PARENT的,因为根View的大小是Window的大小,然后我们回到调用measure方法的地方,

  mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

这里的两个参数的之就是getRootMeasureSpec方法的返回值,经过上面的分析,我们知道这两个参数的中的模式都是EXACTLY的

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {

            // first clears the measured dimension flag
            mPrivateFlags &= ~MEASURED_DIMENSION_SET;

            if (ViewDebug.TRACE_HIERARCHY) {
                ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);
            }

            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            ....

然后这两个参数会传递给onMeasure方法

  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
//获取默认的大小
  public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize =  MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

从上面的分析我们可以看出,默认onMeasure方法的模式是EXACTLY的。

2.自定义View的时候为什要重写measure方法,不重写有什么后果?

当我们制定宽高为wrap_content时,如果不重写onMeasure方法,那么系统就不知道该使用默认多大的尺寸,因此,默认它会填充整个父布局,下面我会通过代码给大家验证。因所以重写onMeasure方法的目的,就是为了能够给view一个wrap_content属性下的默认大小。

下面通过一个例子讲解一下,我们自定一个View

public class MyView extends View {
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //保存测量结果,此方法必须调用
        setMeasuredDimension(getMeasuredWidth(widthMeasureSpec),getMeasuredHeight(heightMeasureSpec));
    }
    public int getMeasuredWidth(int widthMeasureSpec){
        int size = 0;
        //获取尺寸
        int specSize = MeasureSpec.getSize(widthMeasureSpec);
        //获取模式
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        switch (mode){
            case MeasureSpec.EXACTLY://在布局中写死的数值,或者match_parent
                size = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
                size = 200;
                break;
            case MeasureSpec.AT_MOST://布局中属性为wrap_content
                //当属性为wrap_content时,specSize为屏幕大小
                System.out.println("==========="+specSize);//720
                size = 200;
                size = Math.min(size,specSize);
                break;
        }
        return size;
    }
    public int getMeasuredHeight(int heightMeasureSpec){
        int size = 0;
        int specSize = MeasureSpec.getSize(heightMeasureSpec);
        int mode = MeasureSpec.getMode(heightMeasureSpec);
        switch (mode){
            case MeasureSpec.EXACTLY:
                size = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
                size = 200;
                break;
            case MeasureSpec.AT_MOST:
                size = 200;
                size = Math.min(size,specSize);
                break;
        }
        return size;
    }
}

我们重写了onMeasure方法,最后通过setMeasuredDimension将测量后的结果保存了起来,这里的重点在getMeasuredWidth方法,这个方法的作用是,根据父类传过来的规格计算子View的规格,我们看看是如何实现的,
首先,分别取出模式和尺寸,然后我们根据模式去判断,如果是精确的,说明布局文件里面写的是固定尺寸或者match_parent,我们就将取出的值设置给最终结果,如果是UNSPECIFIED,我们给它设置200,如果是AT_MOST,说明布局文件里面写的是wrap_content,这时候我们要取specsize和200的最小值,这里有一点需要注意,当模式为AT_MOST的时候,spesize1的值是和当前屏幕尺寸一样的,所以,在这里我们需要取一个比较小的值

上面过程分析完毕之后,我们看看运行结果,首先设定模式为精确的

<com.example.administrator.myapplication.MyView
    android:layout_width="200px"
    android:layout_height="200px"
    android:background="#ff0000"
    />

这里写图片描述

可以看到,显示除了长宽为200px的正方形

接下来,我们设定模式为AT_MOST,

<com.example.administrator.myapplication.MyView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="#ff0000"
    />

按照上面的代码,我们给了一个默认的大小200,然后从specsize之间取最小值,很明显,结果大家能猜到,和上面一样,
最后我们看看不重写onMeasure方法的结果,
这里写图片描述

此时,整个屏幕都被占领了,也验证了我们之前的结论。

自定义View
View中常用的回调:

//XML加载组件后调用
  @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
    }
    //组件大小改变时
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    }

实现自定义的三种方式

  • 对现有控件进行扩展
  • 通过组合来实现新的控件
  • 重写View来实现全新的控件

    重新onTouchEvent方法时要注意它的返回值,一般情况下我们要返回true来处理事件,如果返回false或者super.onTouchEvent(event),则只能响应ACTION_DOWN事件,后面的MOVE,UP都不能响应。

事件传递
事件传递是从上向下,事件处理是从下向上,事件处理都是onTouEvent方法,默认都是返回false。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值