View/ViewGroup onMeasure()过程源码---记录用

package com.example.qxb_810.viewdrawdemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.ViewTreeObserver;

import butterknife.BindView;
import butterknife.ButterKnife;

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.mv_test)
    MyView mvTest;

    // 因为View的生命周期和Avticivt并不同步,所以在Activity的onCreate,onStart,onResume中均无法获取View的宽高
    // 解决办法: 1. onWindowFocusChanged() 方法中获取 2. view.post() 3. 使用ViewTreeObserver接口
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        // 方法2: 通过post将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,view已经初始化好了
        mvTest.post(new Runnable() {
            @Override
            public void run() {
                int measuredHeight = mvTest.getMeasuredHeight();
                int measuredWidth = mvTest.getMeasuredWidth();
            }
        });
    }
    // 方法1
    // 在这个方法里面可以获取View测量后的宽/高,这个方法含义是View已经初始化完毕,可以获取宽高
    // 需要注意在Activity窗口获取或者失去焦点的时候都会调用该方法。另外频繁的onResume和onPause也会调用。
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);

        int measuredHeight = mvTest.getMeasuredHeight();
        int measuredWidth = mvTest.getMeasuredWidth();
    }

    @Override
    protected void onStart() {
        super.onStart();
        // 方法3
        // 使用OnGlobalLayoutListener() 接口回调。当View树的状态发生改变或者树内部的View的可见性发生改变时onGlobalLayout() 将会被回调。
        // 需要注意随着view状态改变会调用多次
        ViewTreeObserver viewTreeObserver = mvTest.getViewTreeObserver();
        viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mvTest.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int measuredHeight = mvTest.getMeasuredHeight();
                int measuredWidth = mvTest.getMeasuredWidth();
            }
        });
    }
}

package com.example.qxb_810.viewdrawdemo;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;

/**
* author wlhao
* email wlhao@iflytek.com
*/

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

    }
    //  MeasureSpec 代表一个32位int值,高2位代表SpecMode,后30位代表SpecSize
    //  MeasureSpec.makeMeasureSpec(size, Mode);
    //  Mode 有三种模式 : UNSPECIFIED: 表示一种测量的状态,  EXACTLY : 适用 match_parent 或者精确数值,  AT_MOST : 适用于 wrap_content
    //  父容器影响View的MeasureSpec的创建过程。测量过程中系统会将View的layoutParams根据父容器所施加的规则转换成对应的MeasureSpec。
    //  即MeasureSpec 由LayoutParams和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽/高
    //  我理解的测量流程  ---
    //      1. 首先系统创建MeasureSpec(通过某种方式获取xml布局中的Layout_params,判断其布局方式,并设置成对应的Mode和size,再通过makeMeasureSpec()创建出MeasuerSpec)
    //      2. 对ViewGroup: 会调用其measureChildWidthMargins方法,在这个方法中会根据父容器的一些属性构造子元素的MeasuerSpec(这个顺序在子View.onMeasure()之前),并调用子元素的measure()方法进行绘制。
    //         所以ViewGroup中的子元素的MeasuerSpec被重新创建,受到margin和padding等属性影响。 具体方法在getChildMeasureSpec()方法中,没仔细看。
    //      3. View的measure过程 : measure()会调用onMesure(),onMeasure()会调用setMeasuredDimension()设置View宽高的测量值。setMeasuredDimension()会调用getDefaultSize()获取测量后的宽高并设置。
    //      4. ViewGroup的measure过程:ViewGroup除了完成自己的measure过程之外还需要遍历去调用子元素的measure方法。通过measureChilderen()调用measureChilde()对子元素进行measure()以完成遍历子元素measuer
    //         但是其ViewGroup自身的OnMeasure()并没有实现,因为无法满足众多ViewGroup的布局要求,因此都在其子类中单独实现。
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 处理直接继承View/ViewGroup时如果是 wrap_content 属性失效  ----  即按照源码如果设置成wrap_content会默认成match_parent,这里只需要给他指定一个默认的宽/高即可
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(200, 200);
        }else if(widthMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(200, heightSize);
        }else if (heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSize, 200);
        }


    }
    // 定位
    // 1. layout() 方法确定View本身位置,onLayout() 方法按确定所有子元素位置
    //    即 layout 方法流程: 首先通过setFrame方法设定View的四个顶点,确定后View在父容器的位置也就确定,之后调用onLayout方法,供父容器确定子元素的位置
    // 2. 父容器通过layout确定位置之后,通过onLayout会调用子元素的layout来确定子元素的位置。依次传递,实现整个View树的layout过程。

    // 注:View的测量宽/高和最终宽/高有什么区别?即View的getMeasureWidth/getMeasureHeight方法和getWidth/getHeight的区别
    //     答:在View的默认实现中两者相同。只不过前者形成于measure过程,后者形成在layout过程。
    //     1. 如下情况,会使最终宽高永远大于测量宽高100px
    //          protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    //              super.onLayout(changed, left, top, right+100, bottom+100);
    //          }
    //     2. 当View需要多次measure时,那么前几次的measure可能和最终宽高有所差异,但就最终结果看来,并无二异。
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    // 1. 绘制过程如下: 绘制背景background.draw(canvas)  -->  绘制自己(onDraw)  -->  绘制children(dispatchDraw)  --> 绘制装饰(onDrawScrollBars)
    // 2. View的绘制通过dispatchDraw来实现,dispatchDraw会遍历所有的子元素的draw方法,从而一层层传递。
    // 3. View中存在一个方法:setWillNotDraw() 即如果一个View不需要绘制任何内容,可以将其标记位设置为true,系统会进行相应的优化。
    //    这个标示为默认View是不开启,ViewGroup是开启的。
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 解决直接继承View的时候padding属性失效。
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        // 重新计算得到的宽高
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingTop;
        int radius = Math.min(width, height) / 2;
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, paint);

    }

    /*
      自定义View注意事项:
            1. 如果自定义控件直接继承View或者ViewGroup,需要在onMeasure中对wrap_content做特殊处理,否则外界使用该属性时无法达到预期效果(可能会实现match_parent效果)
               修改方法参考上面onMeasure方法
            2. 直接继承View的控件,需要在draw方法中处理padding(解决方法在onDraw)。直接继承ViewGroup的控件,需要在onMeasure和onLayout中考虑padding和子元素margin对其造成的影响。不然会失效。
            3. View中提供了post系列方法,所以尽量在自定义View中少用Handler,除非很明确需要Handler来发送消息。
            4. View中的两个对应方法: onAttchedToWindow/onDetachedFromWindow  ---  当包含此View的Activity启动/ 退出或者remove时会调用。可以在该方法中启动/停止一些线程和动画。
            5.
     */

}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值