精通Android自定义View(二)View绘制三部曲

1 View绘制的过程
  • View的测量——onMeasure()
  • View的位置确定——onLayout()
  • View的绘制——onDraw()

2 View的测量——onMeasure()

Android中View的绘制前,先要进行测量,会回调方法onMeasure()

@Override
public void oMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
  • MeasureSpec类 它是Android系统为我们设计的一个短小而又强悍的一个类,通过它来帮助我们测量View。它是一个32位的int值,其中前两位代表测量的模式,后30位代表测量出的大小。
  • 测量模式分为三种:
    • EXACTLY 精确值模式,即当我们在布局文件中为View指定了具体的大小,例如:android:layout_width=“100dp”,或者当我们将View的大小指定为充满父布局,即为match_parent时,此时,该View的测量模式即为EXACTLY模式。(View的默认测量模式为EXACTLY模式)
    • AT_MOST 最大值模式,此时View的尺寸不得大于父控件允许的最大尺寸即可。即对应我们给View的宽或高指定为wrap_content时。
    • UPSPECIFIED不指定测量模式,即父视图没有限制其大小,子View可以是任何尺寸。该模式一般用于系统内部,平时的Android开发基本用不到。
2.1 简述 View onMeasure中的测量值的获取

如前面所说,View的测量是从measure方法开始的,其是一个用final修饰的方法,这就说明此方法其子类是不能重写的,而在这个方法中,会调用View的onMeaure方法,如下所示

protect void onMeasure(int widthMeaureSpec, int heightMeasureSpec) {
         setMeasureDismension(getDefaultSize(getSuggestedMinimumWith(), widthMeaureSpec), 
                                        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

其中,setMeasureDismension方法会进行View的宽高的测量值的设置,而getDefaultSize方法如下:

public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        //获取当前View的测量模式
        int specMode = MeasureSpec.getMode(measureSpec);
        //精准模式获取当前Viwe测量后的值,如果是最大值模式,会获取父View的大小.
        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;
}

可以看到,该方法的内部实现很简单,其返回值即为View测量后的大小,即measureSpec中的specSize。(这里并不是View最终的大小,因为View最终的大小需要在layout过程中确定,但其实这里测量值的大小和最终的大小几乎所有的情况下都是一样的)。

2.2 简述 ViewGroup 中的测量值的获取

对于ViewGroup来说,它除了要完成自身的测量,还要遍历自己所有的子View,让它们去调用自身的measure方法完成测量。这里ViewGroup不同的一点是,ViewGroup是一个抽象类,并没有实现onMeasure方法,而它提供了一个measureChildren方法来遍历自身所有的子View去调用measureChild方法。方法如下

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {  
    final int size = mChildrenCount;  
    final View[] children = mChildren;  
    for (int i = 0; i < size; ++i) {  
        final View child = children[i];  
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {  
            measureChild(child, widthMeasureSpec, heightMeasureSpec);  
        }  
    }  
}  

而measureChild方法中,会获得子元素的LayoutParams属性,再通过getChildMeasureSpec方法来创建子元素的MeasureSpec,最后再调用measure方法并将获得的MeasureSpec传递给measure方法中。接着就是执行View的measure方法了,之后的流程就和前面说到的一样了。其具体的方法体如下:

protected void measureChild(View child, int parentWidthMeasureSpec,  int parentHeightMeasureSpec) {  
    final LayoutParams lp = child.getLayoutParams();  
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,  
            mPaddingLeft + mPaddingRight, lp.width);  
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,  
            mPaddingTop + mPaddingBottom, lp.height);  
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
}  

ViewGroup没有具体的实现onMeasure方法,是因为ViewGroup的子类有很多种,其对应的布局的特性也不尽相同,这就会导致他们的测量细节会有所不同,因此这里并没有对onMeaure方法做统一的实现。

3 View的位置确定——onLayout()

在测量好View的大小之后,我们就要确定一下View的最终位置了。那么就追溯到我们最开始说的,ViewRoot在执行过performMeasure方法后会执行performLayout方法,而再去调用layout方法来确定View的位置

public void layout(int l, int t, int r, int b) {  
    int oldL = mLeft;  
    int oldT = mTop;  
    int oldB = mBottom;  
    int oldR = mRight;  
    boolean changed = setFrame(l, t, r, b);  
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {  
        if (ViewDebug.TRACE_HIERARCHY) {  
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);  
        }  
        onLayout(changed, l, t, r, b);  
        mPrivateFlags &= ~LAYOUT_REQUIRED;  
        if (mOnLayoutChangeListeners != null) {  
            ArrayList<OnLayoutChangeListener> listenersCopy =  
                    (ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone();  
            int numListeners = listenersCopy.size();  
            for (int i = 0; i < numListeners; ++i) {  
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);  
            }  
        }  
    }  
    mPrivateFlags &= ~FORCE_LAYOUT;  
}  

在layout方法中,首先会通过setFrame方法对View的四个顶点的值进行赋值,即mLeft, mRight, mTop, mBottom。当各个顶点的坐标确定以后,View在ViewGroup中的位置也就确定了。接着就要调用onLayout方法用来确定子元素的位置了。而对于View的onLayout方法,这里要说的是,它是一个空方法,至于为什么,估计大家应该也能想的通,因为onLayouta方法就是为了确定子元素在ViewGroup中的位置,这个功能方然要有ViewGroup去实现了啊。而我们点击进入ViewGroup的onLayout方法,如下:

@Override  
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);  

在onLayout方法中,我们就可以通过getWidth和getHeight方法来获取View的宽和高了。这时,估计又有人会产生一个疑问了,在之前说的在调用过setMeaureDismension后,可以通过getMeasureWidth和getMeasureHeight方法获取View的宽高。那么这两种方式又有什么区别呢。getMeasureWidth的值是在onMeasure之后能获取到的,而getWidth的值是在onLayout方法之后才能获取到。而getWidth的值其实就是View的两边坐标的差值。二者在数值上几乎所有的情况下都是相同的。而如果有时我们刻意去重写layout方法,并修改方法中的参数huozhe,那么就会造成二者的值不相同

4 View的绘制——onDraw()

完成了测量和位置确立,那就差把View绘制出来以让我们看到了。这是就要开始进入我们的绘制流程了。在调用了layout方法后,接着ViewRoot会常见一个Canvas对象,接着调用View的draw方法来执行具体的绘制流程。

早起的年轻人 CSDN认证博客专家 移动开发 项目管理 Java
只要用心去做,每一件事情还是有可能成功的,当然成功是没有界限的,只不过是达到自己心里的那个目标,公众号:我的大前端生涯,一个爱喝茶的程序员,通常会搞搞SpringBoot 、Herbinate、Mybatiys、Android、iOS、Flutter、Vue、小程序等.
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页