自定义View学习笔记之详解onMeasure

网上对自定义view总结的文章都很大,但是自己还是写一篇,好记性不如多敲字!

其实自定义View就是三大流程,onMeasureonLayoutonDraw。看名字就知道,onMeasure是用来测量,onLayout布局,onDraw进行绘制。

那么何时开始进行view的绘制流程,这就要从ViewRoot和DecorView的概念说起。

ViewRoot对应于ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的三大绘制流程都是通过ViewRoot来完成的。在ActivityThread中,当Activity被创建时,会将DecorView添加到Window中,同时创建一个ViewRootImpl对象,病假ViewRootImpl对象和DecorView对象建立关联。

以上摘自《Android开发艺术探索》第4章View的工作原理

我们通常开发时,更新UI一般都是不能再子线程总进行,假如在子线程中更新,会抛出异常。这并不是因为只有UI线程才能更新UI,而是ViewRootImpl对象是在UI线程中创建。

View的绘制就是从ViewRoot的performTraversals方法开始的。

DecorView是一个顶级View,一般是一个竖直方向的LinearLayout,包含一个titlebar和内容区域。我们在Activity中setContentView中设置的布局文件就是加载到内容区域。内容区域是个FrameLayout。


DecorView的结构

onMeasure

大多数情况下,我们如果在布局文件中,对自定义View的layout_widthlayout_height不设置wrap_content,我们一般都是不需要进行处理的,但是如果要设置为wrap_content,我们需要在测量时,对宽高进行测量。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

重写onMeasure方法,我们可以看到两个传入的int值widthMeasureSpecheightMeasureSpec。Java中int类型是4个字节,也就是32位,这两个int值中的高2位代表SpecMode,也就是测量模式,低32位则是代表SpecSize也就是在某个测量模式下的大小。

我们不需要自己写代码进行位运算得到SpecMode和SpecSize, Android内置了MeasureSpec类来处理。

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

那SpecMode测量模式占2位,二进制2位可以表达最多4中情况,还好,测量模式只有三种情况,每一种情况有其特殊的意思。

SpecMode 含义
UNSPECIFIED 父容器对不对当前View有任何限制,就是说View可以取任意大小。
EXACTLY 父容器测量出View需要的精确大小,对应于match_parent和具体数值情况xxdp
AT_MOST 当前View所能取的最大尺寸,一般是给定一个大小,view的尺寸不能超过该大小,一般用于wrap_content

以下摘自实验室小伙伴的总结,自定义view,这一篇就够了。对于我们在布局中定义的尺寸和测量模式的对应关系,看了下面的总结,就不会有任何疑惑了。

match_parent--->EXACTLY。怎么理解呢?match_parent就是要利用父View给我们提供的所有剩余空间,而父View剩余空间是确定的,也就是这个测量模式的整数里面存放的尺寸。
wrap_content--->AT_MOST。怎么理解:就是我们想要将大小设置为包裹我们的view内容,那么尺寸大小就是父View给我们作为参考的尺寸,只要不超过这个尺寸就可以啦,具体尺寸就根据我们的需求去设定。
固定尺寸(如100dp)--->EXACTLY。用户自己指定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主啦。

**重写onMeasure **

通过前文的描述,我们已经可以动手重写onMeasure函数了。

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    int width = MeasureSpec.getSize(widthMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(WRAP_WIDTH, WRAP_HEIGHT);
    } else if (widthMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(WRAP_WIDTH, height);
    } else if (heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(width, WRAP_HEIGHT);
    }
}

只处理AT_MOST情况也就是wrap_content,其他情况则沿用系统的测量值即可。setMeasuredDimension会设置View宽高的测量值,只有setMeasuredDimension调用之后,才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。

上述是一个通用的写法,我们实现一个自定义view,画一个圆。
xml布局如下:

<com.zhu.testview.MyView
    android:id="@+id/my_view"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:background="#44ff0000" />

我们将其中的宽改为wrap_content,并且设置默认的宽高为200;

private final int WRAP_WIDTH = 200;
private final int WRAP_HEIGHT = 200;
<com.zhu.testview.MyView
    android:id="@+id/my_view"
    android:layout_width="wrap_content"
    android:layout_height="100dp"
    android:background="#44ff0000" />
width wrap-content heigth 100dp.jpg

我们看到宽度已经不是原先的match_parent了。

注意
如果我们不处理AT_MOST情况,那么即使设置了wrap_content,最终的效果也和match_parent一样,这是因为这种情况下,view的SpecSize就是父容器测量出来可用的大小。
如果我们设置了margin会有什么效果呢?我们来看看。

<com.zhu.testview.MyView
    android:id="@+id/my_view"
    android:layout_width="wrap_content"
    android:layout_height="100dp"
    android:layout_margin="20dp"
    android:background="#44ff0000" />
margin.jpg

看来margin属性的效果生效了,但是由于我们并没有处理margin属性,而margin属性是由父容器控制的,因此,我们自定义view中就不需要做特殊处理。但是padding属性就需要我们做处理。

int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();

到这里整个onMeasure过程就基本差不多了。

注意

1、某些极端情况下,系统可能要多次measure才能确定最终测量的宽高,这时onMeasure中拿到的不一定是准确的,所以在onLayout或者onSizeChanged中获取宽高。

protected void onSizeChanged(int w, int h, int oldw, int oldh)

log.png

我们看到onMeasure进行了两次测量。当开启了旋转时,每当手机旋转,我们就要重新measure,然后会调用onSizeChanged()方法。这个方法头两个参数是当前尺寸大小,后两个是上一次测量的尺寸。

2、在onLayout()过程结束后,我们就可以调用getWidth()方法和getHeight()方法来获取视图的宽高了。getWidth()方法和getMeasureWidth()的值基本相同。但getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。

3、Activity中需要view的宽高时,onCreate
onStartonResume中都是无法获取的。这是由于view的生命周期和Activity的生命周期不是同步的。解决方法如下有三种:
(1)Activity中在onWindowFocusChanged 中获取。这时View已经初始化完了,可以获取宽高。当Activity窗口获得焦点和失去焦点时均会被调用,因此该函数会被调用多次。

@Overridepublic void onWindowFocusChanged(boolean hasFocus) { 
   super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
        int width = myView.getWidth();
        int height = myView.getHeight();
        Log.d(TAG, "width: " + width);
        Log.d(TAG, "height: " + height);
        Log.d(TAG, "measuredWidth: " + myView.getMeasuredWidth());
        Log.d(TAG, "measuredHeight: " + myView.getMeasuredHeight());
    }
}

(2)view.post(runnable)
通过post将一个runnable放到消息队列尾部,等待looper调用此runnable,这时view也已经初始化好了。

myView.post(new Runnable() { 
   @Override    public void run() { 
       Log.d(TAG, "measuredWidth: " + myView.getMeasuredWidth());
       Log.d(TAG, "measuredHeight: " + myView.getMeasuredHeight());
    }
});

可以在onCreateonStartonResume中调用view.post(runnable)方法。

(3)ViewTreeObserver
使用ViewTreeObserver的回调可以完成获取view的宽高。

ViewTreeObserver observer = myView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override    public void onGlobalLayout() {
        Log.d(TAG, "observer measuredWidth: " + myView.getMeasuredWidth());
        Log.d(TAG, "observer measuredHeight: " + myView.getMeasuredHeight());
    }
});

这里使用了onGlobalLayoutListener接口,当view树的状态翻身改变或者view树内部的view可见性发生改变时,onGlobalLayout会被回调,这也说明onGlobalLayout会被调用多次。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,478评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,825评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,482评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,726评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,633评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,018评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,168评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,320评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,264评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,288评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,995评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,587评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,909评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,284评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,862评论 2 339

推荐阅读更多精彩内容