• 自定义View 布局过程(Layout)


    一、作用

    计算视图(View)的位置

    即计算View的四个顶点位置:Left、Top、Right 和 Bottom

    二、layout过程详解

    类似measure过程,layout过程根据View的类型分为2种情况:
    在这里插入图片描述
    接下来,我将详细分析这2种情况下的layout过程

    2.1单一View的layout过程

    具体使用

    继承自View、SurfaceView 或 其他View;不包含子View

    具体流程

    在这里插入图片描述

    源码分析

    layout过程的入口 = layout(),具体如下:

    /**
      * 源码分析起始点:layout()
      * 作用:确定View本身的位置,即设置View本身的四个顶点位置
      */ 
      public void layout(int l, int t, int r, int b) {  
    
        // 当前视图的四个顶点
        int oldL = mLeft;  
        int oldT = mTop;  
        int oldB = mBottom;  
        int oldR = mRight;  
          
        // 1. 确定View的位置:setFrame() / setOpticalFrame()
        // 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回 
        // setFrame() ->分析1
        // setOpticalFrame() ->分析2
        boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
        // 2. 若视图的大小 & 位置发生变化
        // 会重新确定该View所有的子View在父容器的位置:onLayout()
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {  
    
          onLayout(changed, l, t, r, b);  
          // 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现 ->分析3
          // 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需自定义重写实现(下面的章节会详细说明)
    }  
    
    /**
      * 分析1:setFrame()
      * 作用:根据传入的4个位置值,设置View本身的四个顶点位置
      * 即:最终确定View本身的位置
      */ 
      protected boolean setFrame(int left, int top, int right, int bottom) {
    
        // 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
        // 从而确定了视图的位置
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
    
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
    
       }
    
    /**
      * 分析2:setOpticalFrame()
      * 作用:根据传入的4个位置值,设置View本身的四个顶点位置
      * 即:最终确定View本身的位置
      */ 
      private boolean setOpticalFrame(int left, int top, int right, int bottom) {
    
            Insets parentInsets = mParent instanceof View ?
                    ((View) mParent).getOpticalInsets() : Insets.NONE;
    
            Insets childInsets = getOpticalInsets();
    
            // 内部实际上是调用setFrame()
            return setFrame(
                    left   + parentInsets.left - childInsets.left,
                    top    + parentInsets.top  - childInsets.top,
                    right  + parentInsets.left + childInsets.right,
                    bottom + parentInsets.top  + childInsets.bottom);
        }
        // 回到调用原处
    
    /**
      * 分析3:onLayout()
      * 注:对于单一View的laytou过程
      *    1. 由于单一View是没有子View的,故onLayout()是一个空实现
      *    2. 由于在layout()中已经对自身View进行了位置计算:setFrame() / setOpticalFrame()
      *    3. 所以单一View的layout过程在layout()后就已完成了
      */ 
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    
       // 参数说明
       // changed 当前View的大小和位置改变了 
       // left 左部位置
       // top 顶部位置
       // right 右部位置
       // bottom 底部位置
    }  
    

    总结

    单一View的layout过程解析如下:
    在这里插入图片描述

    2.2ViewGroup的layout过程

    具体使用

    继承自ViewGroup 或 各种Layout;含有子 View

    原理
    从ViewGroup至子View、自上而下遍历进行(即树形递归),通过计算整个ViewGroup中各个View的属性,从而最终确定整个ViewGroup的属性。即:

    • 计算自身ViewGroup的位置:layout()
    • 遍历包含的所有子View,确定所有子View在ViewGroup的位置:onLayout()

    a. 步骤2 类似于 单一View的layout过程
    b. 自上而下、一层层地传递下去,直到完成整个View树的layout()过程

    在这里插入图片描述

    具体流程

    在这里插入图片描述

    这里需要特别注意的是:
    ViewGroup 和 View 同样拥有方法:layout()、onLayout(),但二者应用场景是不一样的:

    • 一开始计算ViewGroup位置时,调用的是ViewGroup的layout()和onLayout();
    • 当开始遍历子View及计算子View位置时,调用的是子View的layout()和onLayout(),类似于单一View的layout过程。

    源码分析

    /**
      * 源码分析:layout()
      * 作用:确定View本身的位置,即设置View本身的四个顶点位置
      * 注:与单一View的layout()源码一致
      */ 
      public void layout(int l, int t, int r, int b) {  
    
        // 当前视图的四个顶点
        int oldL = mLeft;  
        int oldT = mTop;  
        int oldB = mBottom;  
        int oldR = mRight;  
          
        // 1. 确定View的位置:setFrame() / setOpticalFrame()
        // 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回 
        // setFrame() ->分析1
        // setOpticalFrame() ->分析2
        boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
        // 2. 若视图的大小 & 位置发生变化
        // 会重新确定该View所有的子View在父容器的位置:onLayout()
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {  
    
            onLayout(changed, l, t, r, b);  
            // 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现(上面已分析完毕)
            // 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现 ->分析3
      ...
    
    }  
    
    /**
      * 分析1:setFrame()
      * 作用:确定View本身的位置,即设置View本身的四个顶点位置
      */ 
      protected boolean setFrame(int left, int top, int right, int bottom) {
            ...
        // 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
        // 从而确定了视图的位置
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
    
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
    
        }
    
    /**
      * 分析2:setOpticalFrame()
      * 作用:确定View本身的位置,即设置View本身的四个顶点位置
      */ 
      private boolean setOpticalFrame(int left, int top, int right, int bottom) {
    
            Insets parentInsets = mParent instanceof View ?
                    ((View) mParent).getOpticalInsets() : Insets.NONE;
    
            Insets childInsets = getOpticalInsets();
    
            // 内部实际上是调用setFrame()
            return setFrame(
                    left   + parentInsets.left - childInsets.left,
                    top    + parentInsets.top  - childInsets.top,
                    right  + parentInsets.left + childInsets.right,
                    bottom + parentInsets.top  + childInsets.bottom);
        }
        // 回到调用原处
    
    /**
      * 分析3:onLayout()
      * 作用:计算该ViewGroup包含所有的子View在父容器的位置()
      * 注: 
      *    a. 定义为抽象方法,需重写,因:子View的确定位置与具体布局有关,所以onLayout()在ViewGroup没有实现
      *    b. 在自定义ViewGroup时必须复写onLayout()!!!!!
      *    c. 复写原理:遍历子View 、计算当前子View的四个位置值 & 确定自身子View的位置(调用子View layout())
      */ 
      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    
         // 参数说明
         // changed 当前View的大小和位置改变了 
         // left 左部位置
         // top 顶部位置
         // right 右部位置
         // bottom 底部位置
    
         // 1. 遍历子View:循环所有子View
          for (int i=0; i

    总结

    对于视图组ViewGroup的布局流程(Layout)流程及各个方法说明总结如下:
    在这里插入图片描述
    这里需要特别注意的是:
    ViewGroup 和 View 同样拥有方法:layout()、onLayout(),但二者应用场景是不一样的:

    • 一开始计算ViewGroup位置时,调用的是ViewGroup的layout()和onLayout();
    • 当开始遍历子View及计算子View位置时,调用的是子View的layout()和onLayout(),类似于单一View的layout过程。

    至此,ViewGroup的 layout过程已讲解完毕。

    三、细节问题:getWidth() ( getHeight())与 getMeasuredWidth() (getMeasuredHeight())获取的宽 (高)有什么区别?

    首先明确定义:

    getWidth() / getHeight():获得View最终的宽 / 高
    getMeasuredWidth() / getMeasuredHeight():获得 View测量的宽 / 高

    先看下各自的源码:

    // 获得View测量的宽 / 高
      public final int getMeasuredWidth() {  
          return mMeasuredWidth & MEASURED_SIZE_MASK;  
          // measure过程中返回的mMeasuredWidth
      }  
    
      public final int getMeasuredHeight() {  
          return mMeasuredHeight & MEASURED_SIZE_MASK;  
          // measure过程中返回的mMeasuredHeight
      }  
    
    
    // 获得View最终的宽 / 高
      public final int getWidth() {  
          return mRight - mLeft;  
          // View最终的宽 = 子View的右边界 - 子view的左边界。
      }  
    
      public final int getHeight() {  
          return mBottom - mTop;  
         // View最终的高 = 子View的下边界 - 子view的上边界。
      }  
    
    

    二者的区别:

    在这里插入图片描述
    **上面标红:一般情况下,二者获取的宽 / 高是相等的。**那么,“非一般”情况是什么?

    答:人为设置:通过重写View的 layout()强行设置

    
    @Override
    public void layout( int l , int t, int r , int b){
      
       // 改变传入的顶点位置参数
       super.layout(l,t,r+100,b+100);
    
       // 如此一来,在任何情况下,getWidth() / getHeight()获得的宽/高 总比 getMeasuredWidth() / getMeasuredHeight()获取的宽/高大100px
       // 即:View的最终宽/高 总比 测量宽/高 大100px
    
    }
    

    虽然这样的人为设置无实际意义,但证明了View的最终宽 / 高 与 测量宽 / 高是可以不一样

    结论

    在非人为设置的情况下,View的最终宽/高(getWidth() / getHeight())
    与 View的测量宽/高 (getMeasuredWidth() / getMeasuredHeight())永远是相等

  • 相关阅读:
    外包干了10个月,技术退步明显.......
    Vue前端框架09 计算属性、Class绑定、style绑定、侦听器、表单输入绑定、Dom操作模板引用
    如何在你的开源项目中添加开源许可证
    闲话Python编程-字符串
    代码重构:面向单元测试
    vue3_setup基础_渲染函数(ref,reactive)
    Ubuntu22.04 搭建 OpenHarmony 命令行开发环境
    【C++笔记】C++标准模板库(STL)之序列容器
    信号量的使用和示例
    卖家如何搭上独立站这趟快车
  • 原文地址:https://blog.csdn.net/qq_48435252/article/details/127080355