• 自己定义View Layout过程



    前言

    • 自己定义View是Android开发人员必须了解的基础
    • 网上有大量关于自己定义View原理的文章。但存在一些问题:内容不全、思路不清晰、无源代码分析、简单问题复杂化等等
    • 今天,我将全面总结自己定义View原理中的layout过程,我能保证这是市面上的最全面、最清晰、最易懂的
    • 文章较长。建议收藏等充足时间再进行阅读

    文件夹

    文件夹


    1. 知识基础

    具体请看我写的另外一篇文章:(1)自己定义View基础 - 最易懂的自己定义View原理系列


    2. 作用

    计算View视图的位置。

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


    3. layout过程具体解释

    同measure过程一样,layout过程依据View的类型分为两种情况:
    1. 假设View = 单一View,则仅计算本身View的位置;
    2. 假设View = VieGroup,除了计算自身View的位置外。还须要确定子View在父容器中的位置。

    View树的位置是由包括的每个子视图的位置所决定,所以想计算整个View树的位置,就须要递归去计算每个子视图的位置(与measure过程同理)

    接下来,我将具体分析这两种情况下的layout过程。

    3.1 单一View的layout过程

    • 应用场景
      在没有现成的View。须要自己实现的时候,就使用自己定义View。一般继承自View、SurfaceView等,特点是:不包括子View。
    1. 如:制作一个支持载入网络图片的ImageView
    2. 特别注意:自己定义View在大多数情况下都有替代方案。利用图片或者组合动画来实现,可是使用后者可能会面临内存耗费过大。制作麻烦更诸多问题。

    单一View的layout步骤例如以下图所看到的:

    单一View的layout过程

    以下我将一个个方法进行具体分析。

    3.1.1 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;  
    
        // setFrame() / setOpticalFrame():确定View的位置
        // 即初始化四个顶点的值,然后推断当前View大小和位置是否发生了变化并返回  (具体请看以下源代码分析)
     boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
        //假设视图的大小和位置发生变化,会调用onLayout()
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {  
    
            // onLayout():确定该View全部的子View在父容器的位置
            // 由于单一View是没有子View的。所以onLayout()是一个空实现(后面会具体说)
            onLayout(changed, l, t, r, b);  
    
            // 由于确定位置与具体布局有关。所以onLayout()在ViewGroup和View均没有实现。
            // 在单一View中,onLayout()是一个空实现(后面会具体说)
            // 在ViewGroup中,onLayout()被定义为抽象方法
            // 所以onLayout()须要ViewGroup的子类去重写实现(后面会具体说)
      ...
    
    }  
    
    
    /*
    * setOpticalFrame()源代码分析
    **/
    
    private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;
        Insets childInsets = getOpticalInsets();
    
    // setOpticalFrame()实际上是调用setFrame()
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }
    
    
    /*
    * setFrame()源代码分析
    **/
    
    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);
    }
    以下。我们继续分析在`layout()`中调用的`onLayout()`。

    3.2 onLayout()

    • 作用:空实现。

    对于单一View来说,由于在layout()中已经对自身View进行了位置计算,所以单一View的layout()已经完成了。

    • 源代码分析:
    // 当这个view和其子view被分配一个大小和位置时,被layout()调用。 即单个View的情况
    // View的onLayout()为空实现
    
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    
    // 參数说明
     * @param changed 当前View的大小和位置改变了 
     * @param left 左部位置
     * @param top 顶部位置
     * @param right 右部位置
     * @param bottom 底部位置
    
    }  

    至此,单一View的layout过程已经分析完成。

    3.1.3 总结

    单一View的layout过程解析例如以下:

    单一View的layout过程

    3.2 ViewGroup的layout过程

    • 应用场景
      自己定义ViewGroup通常是利用现有的组件依据特定的布局方式来组成新的组件。大多继承自ViewGroup或各种Layout(含有子View)。

      如:底部导航条中的条目,一般都是上图标(ImageView)、下文字(TextView)。那么这两个就能够用自己定义ViewGroup组合成为一个Veiw,提供两个属性分别用来设置文字和图片。使用起来会更加方便。
      Paste_Image.png

    • 原理(步骤)

    步骤1: ViewGroup调用layout()计算自身的位置;
    步骤2: ViewGroup调用onLayout()遍历子View并调用子View layout()确定自身子View的位置。

    步骤2相似于单一View的layout过程

    Paste_Image.png

    这样自上而下、一层层地传递下去,直到完成整个View树的layout()过程

    • ViewGroup的layout过程
      例如以下图所看到的:

    ViewGroup的layout过程

    这里须要注意的是:
    ViewGroup和View相同拥有layout()onLayout(),二者是不一样的。


    • 一開始计算ViewGroup位置时。调用的是ViewGroup的layout()onLayout()
    • 当開始遍历子View计算子View位置时,调用的是子View的layout()onLayout()

    相似于单一View的layout过程
    以下我将一个个方法进行具体分析。

    3.2.1 layout()

    • 作用:确定ViewGroup本身的位置。
      这里是ViewGroup的layout()
    • 源代码分析例如以下:(仅贴出关键代码)
    // 与单一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; // setFrame() / setOpticalFrame():确定View的位置 // 即初始化四个顶点的值,然后推断当前View大小和位置是否发生了变化并返回 (具体请看以下源代码分析) boolean changed = isLayoutModeOptical(mParent) ?

    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); //假设视图的大小和位置发生变化,会调用onLayout() if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { // onLayout():确定该ViewGroup全部子View在父容器的位置 // 由于单一View是没有子View的。所以onLayout()是一个空实现(后面会具体说) onLayout(changed, l, t, r, b); // 由于确定位置与具体布局有关。所以onLayout()在ViewGroup没有实现。(被定义为抽象方法) // 所以onLayout()须要ViewGroup的子类去重写实现(后面会具体说) ... } /* * setOpticalFrame()源代码分析 **/ private boolean setOpticalFrame(int left, int top, int right, int bottom) { Insets parentInsets = mParent instanceof View ? ((View) mParent).getOpticalInsets() : Insets.NONE; Insets childInsets = getOpticalInsets(); // setOpticalFrame()实际上是调用setFrame() return setFrame( left + parentInsets.left - childInsets.left, top + parentInsets.top - childInsets.top, right + parentInsets.left + childInsets.right, bottom + parentInsets.top + childInsets.bottom); } /* * setFrame()源代码分析 **/ 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); }

    以下,我们继续分析在`layout()`中调用的`onLayout()`。

    3.2.2 onLayout()

    • 作用:计算该ViewGroup包括全部的子View在父容器的位置()

      1. 定义为抽象方法。须要重写
      2. 原因:由于子View的确定位置与具体布局有关,所以onLayout()在ViewGroup没有实现。
    • 源代码分析:

    // 当中,ViewGroup的抽象方法onLayout()也被override标注,所以也是重写的方法
    // 重写的是其父类view中的onLayout(),即单一View中的onLayout()  (为空实现)
    
    @Override  
    protected abstract void onLayout(boolean changed, int l, int t, int r, int b);  
    
    // 參数说明
     * @param changed 当前View的大小和位置改变了 
     * @param left 父View的左部位置
     * @param top 父View的顶部位置
     * @param right 父View的右部位置
     * @param bottom 父View的底部位置
    • 不管是系统提供的LinearLayout还是我们自己定义的View视图,都须要继承自ViewGroup类
    • 假如须要确定该ViewGroup包括全部子View在父容器的位置。则须要重写onLayout方法(由于onLayout()在ViewGroup中被定义为抽象方法)

    所以在自己定义ViewGroup时必须重写onLayout()。!

    !!

    依据上面说的原理描写叙述,在ViewGroup调用layout()计算完自身的位置后。是须要ViewGroup调用onLayout()遍历子View并调用子View layout()确定自身子View的位置。

    所以。重写ViewGroup的onLayout()的本质是:遍历子View并调用子View的layout()确定子View的位置。复写的套路例如以下:

    
    @Override  
    protected void onLayout(boolean changed, int l, int t, int r, int b) {  
    
      // 參数说明
     * @param changed 当前View的大小和位置改变了 
     * @param l   即left。父View的左部位置
     * @param t   即top,父View的顶部位置
     * @param r   即right,父View的右部位置
     * @param b  即bottom,父View的底部位置
    
            // 循环全部子View
            for (int i=0; i<getChildCount(); i++) {
                View child = getChildAt(i);   
    
                // 计算当前子View的四个位置值
                // 计算的逻辑须要自己实现,也是自己定义View的关键
                ...
    
                // 对计算后的位置值进行赋值
                int mLeft  = Left
                int mTop  = Top
                int mRight = Right
                int mBottom = Bottom
    
                // 调用子view的layout()并传递计算过的參数
                // 从而计算出子View的位置
                child.layout(mLeft, mTop, mRight, mBottom);
            }
        }
    }
    在复写的onLayout()会调用子View的`layout()`和`onLayout()`,这两个过程相似于单一View的layout过程中的`layout()`和`onLayout()`。这里不作过多描写叙述

    具体请看上面的单一View的layout过程

    3.2.3 总结

    对于ViewGroup的layout过程,例如以下:

    ViewGroup的layout过程

    至此。ViewGroup的layout过程已经解说完成。


    4. 实例解说

    为了让大家更好地理解ViewGroup的layout过程(特别是复写onLayout()),接下来。我将用两个实例来加深对ViewGroup layout过程的理解。
    • 实例1:系统提供的LinearLayout(ViewGroup的子类)
    • 实例2:自己定义View(继承了ViewGroup类)

    4.1 实例解析1(LinearLayout)

    4.1.1 原理:

    1. 计算出LinearLayout在父布局的位置
    2. 计算出LinearLayout中子View在容器中的位置。

    4.1.2 具体流程

    LinearLayout的Layout流程

    4.1.2 源代码分析

    在上述流程中,对于LinearLayout的layout()的实现与上面所说是一样的。这里不作过多阐述。直接进入LinearLayout复写的onLayout()代码分析:

    
    // 复写的逻辑和LinearLayout measure过程的`onMeasure()`相似
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    
    // 先查看自身方向属性
    // 不同的方向处理方式不同
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }
    • 由于垂直 / 水平方向相似,所以此处仅分析垂直方向(Vertical)的处理过程
    • 源代码分析例如以下:(凝视很清晰)
    void layoutVertical(int left, int top, int right, int bottom) {
    
    
        // 子View的数量
        final int count = getVirtualChildCount();
    
        // 遍历子View
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
    
                // 子View的測量宽 / 高值
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
    
                // 递归调用子View的setChildFrame():对子View的位置信息进行測量计算
                // 实际上是调用了子View的layout()。请看以下源代码分析
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
    
                // childTop逐渐增大,即后面的子元素会被放置在靠下的位置
                // 这符合垂直方向的LinearLayout的特性
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
    
                i += getChildrenSkipCount(child, i);
            }
        }
    }
    
    
    /*
    *setChildFrame()代码分析
    **/
    
    private void setChildFrame( View child, int left, int top, int width, int height){
    
    // setChildFrame()仅仅仅仅是调用了子View的layout()而已
        child.layout(left, top, left ++ width, top + height);
    
        }
    
    // 在layout()又通过调用setFrame()确定View的四个顶点
    // 即确定了子View的位置
    // 如此不断循环确定全部子View的位置,终于确定ViewGroup的位置
    

    在setFrame()实际上是调用了子View的layout()从而实现子View的位置计算,和上面相似,这里就不作过多描写叙述。

    4.2 实例解析2:自己定义View

    • 上面讲的样例是系统提供的、已经封装好的ViewGroup - LinearLayout
    • 可是。一般来说我们使用的都是自己定义View;
    • 接下来。我用一个简单的样例讲下自己定义View的layout()过程

    4.2.1 实例视图说明

    实例的视图是一个ViewGroup(灰色视图),包括一个黄色的子View,例如以下图:
    自己定义View的视图

    4.2.2 原理

    1. 计算出ViewGroup在父布局的位置
    2. 计算出ViewGroup中子View在容器中的位置。

    原理流程

    4.2.3 具体计算逻辑

    • 具体计算逻辑是指计算子View的位置,即计算四顶点位置 = 计算Left、Top、Right和Bottom。
    • 主要是写在复写的onLayout()
    • 计算公式例如以下:

    Paste_Image.png

    • Left = (r - width) / 2
    • Top = (b - height) / 2
      r = Left + width + Left(由于左右间距一样)
      b = Top + height + Top(由于上下间距一样)
    • Right = width + Left;
    • Bottom = height + Top;

    4.2.3 代码分析

    由于其余方法同上,这里不作过多描写叙述,所以这里仅仅分析复写的`onLayout()`
    
    @Override  
    protected void onLayout(boolean changed, int l, int t, int r, int b) {  
    
      // 參数说明
     * @param changed 当前View的大小和位置改变了 
     * @param l   即left。父View的左部位置
     * @param t   即top,父View的顶部位置
     * @param r   即right,父View的右部位置
     * @param b  即bottom,父View的底部位置
    
            // 循环全部子View
            // 事实上就仅仅有一个
            for (int i=0; i<getChildCount(); i++) {
                View child = getChildAt(i);
    
                // 取出当前子View宽 / 高
                int width = child.getMeasuredWidth();
                int height = child.getMeasuredHeight();
    
                // 计算当前子View的mLeft和mTop值
                int mLeft = (r - width) / 2;
                int mTop = (b - height) / 2;
    
                // 调用子view的layout()并传递计算过的參数
                // 从而计算出子View的位置
                child.layout(mLeft, mTop, mLeft + width, mTop + height);
            }
        }
    }
    布局文件例如以下:
    <?xml version="1.0" encoding="utf-8"?>
    <scut.carson_ho.layout_demo.Demo_ViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#eee998"
        tools:context="scut.carson_ho.layout_demo.MainActivity">
    
        <Button
            android:text="ChildView"
            android:layout_width="200dip"
            android:layout_height="200dip"
            android:background="#333444"
            android:id="@+id/ChildView" />
    </scut.carson_ho.layout_demo.Demo_ViewGroup >
    
    

    效果图

    效果图

    好了,你是不是发现。粘了我的代码可是画不出来?。(例如以下图)

    实际示意图

    由于我还没说draw流程啊哈哈哈!

    draw流程是负责将View绘制出来的。

    layout()过程说到这里讲完了。接下来我将继续将自己定义View的最后一个流程draw流程,有兴趣就继续关注我啦啦!!


    5. 细节问题

    问: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的终于宽 / 高和測量宽 / 高大100px是能够不一样。

    特别注意

    网上流传这么一个原因描写叙述:

    • 实际上在当屏幕能够包裹内容的时候。他们的值是相等的。
    • 仅仅有当view超出屏幕后,才干看出他们的差别:getMeasuredWidth()是实际View的大小,与屏幕无关,而getHeight的大小此时则是屏幕的大小。当超出屏幕后getMeasuredWidth()等于getWidth()加上屏幕之外没有显示的大小

    这个结论是错的!具体请这个博客

    结论

    getWidth() ( getHeight())获得的宽 / 高与getMeasuredWidth() (getMeasuredHeight())获取的宽 (高)在非人为设置的情况下,永远是相等的。


    6. 总结

    • 对于ViewGroup的layout过程

      1. ViewGroup调用layout()计算自身的位置
      2. ViewGroup调用onLayout()遍历子View并调用子View layout()确定自身子View的位置

      此步骤就是复写onLayout()的逻辑

    • 如此不断循环确定全部子View的位置,直到全部确定即layout过程完成
    • 对于View的layout过程
      调用layout()计算自身的位置就可以。

    • 一个图总结自己定义View - Layout过程,例如以下图:

    总结

    请帮顶或评论点赞。由于你们的赞同/鼓舞是我写作的最大动力!

  • 相关阅读:
    初试PL/SQL并行编程
    SVN 让项目某些文件不受版本控制
    分析php获取客户端ip
    一道月薪3W的java面试题 (小明和小强都是张老师的学生,张老师的生日是某月某日,2人都不知道张老师的生日)
    js遍历对象的属性并且动态添加属性
    CodeForces 150B- Quantity of Strings 推算..
    linux 中多线程使用
    Microsoft Deployment Toolkit 2013 Preview Release Now Available
    谁有SMI-S Provider的一些源码,能参考一下吗
    hdu1395-2^x mod n = 1
  • 原文地址:https://www.cnblogs.com/cxchanpin/p/7382487.html
Copyright © 2020-2023  润新知