zoukankan      html  css  js  c++  java
  • View学习(三)- View的布局(layout)过程

    前段开始学习View的工作原理,前两篇博客的草稿都已经写好了,本想一鼓作气写完所有的相关文章,然后经历了一段连续加班,结果今天准备继续写文章时,把之前写好的东西都忘记了,又重新梳理了一遍,所以说那怕就是已经掌握的知识,也要记得温故而知新。

    言归正传,之前我们讨论过了measure过程,measure过程完成之后,我们就可以通过 getMeasuredWidthgetMeasuredHeight来得到View的宽高尺寸了。而知道了宽高尺寸之后,剩下的就是布局(layout)过程了。说直白点,怎么把具一个宽高已定的View摆放在屏幕上。

    我们先来看一个demo。

    /**
     *@author www.yaoxiaowen.com
     */
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/linearLayout1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:background="@color/google_yellow"
        >
    
        <LinearLayout
            android:id="@+id/linearLayout2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:background="@color/white"
            >
    
            <TextView
                android:id="@+id/textView1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="textView1"
                android:background="@color/green"
                android:paddingLeft="20dp"
                android:paddingRight="40dp"
                android:paddingTop="10dp"
                />
    
            <Button
                android:id="@+id/button1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="button1"
                android:layout_marginLeft="20dp"
                android:background="@color/blue"
                />
        </LinearLayout>
    
        <TextView
            android:id="@+id/text2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="text2"
            android:layout_marginLeft="40dp"
            android:background="@color/red"
            />
    
    </LinearLayout>
    

    具体的实现效果,是这个样子。

    我们暂且不讨论代码当中的实现原理之类的,仅仅就想想一种场景,给你一些尺寸固定但不同的盒子,摆放在一个大房间的地面上,我们应该怎么摆放呢。

    之所以要根据这个demo来设想这种场景,是因为我觉的,不管代码怎么写,但是基本的道理都不复杂,正所谓大道至简,(毕竟我们不是做那些高大上的算法的)。自己根据场景去设想怎么摆放,同样的也有助于理解代码。
    假设我们按照 LinearLayout竖直方式的规则来摆放,那么开始摆放第一个盒子肯定从房间的左上角开始计算位置,根据父容器padding,本身的margin等来决定具体摆放的坐标。然后再在竖直方向上 向前推进摆放第二个盒子。

    我并不知道上面那段文字有没有能清楚的表达出我的意思, 但是这是我前段时间关于这个layout的思考过程。思考过程本身就很难表达,但是我想这样思考是有助于理解这个过程的。

    和measure过程一样,layout过程也是一个递归过程,并且ViewGroup类本身不 应该具体实现onLayout,具体实现过程应该放在Framelayout,LinearLayout,RelativieLayout等容器类中。

    当layout过程完成之后,我们就可以得到View的左上角和右下角的坐标了。(就是left,top,right,bottom)。值得注意的是这个坐标是相对坐标,就是相对于View父容器的坐标。所以通过递归来计算是最自然最方便的方式。

    这几天看了关于编程语言历史方面的文章, 在早期的编程语言中,是没有递归这个概念的,是后来有人想出了这个概念,才逐渐的在各种编程语言中实现。所以说现在我们来看稀奇平常很自然的概念,但是当初第一个想出这个概念的人,那就是天才了。

    下面我们就从源码角度来进行分析。
    performTraversals调用了PerformLayout方法。

     //ViewRootImpl.java  www.yaoxiaowen.com
     private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
                int desiredWindowHeight) {
          
            //...
            final View host = mView;
            //...
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
            //...
    }
    

    host其实就是DecorView了,在执行layout时,注意传递的头两个参数都是0,这说明 整个layout过程是从屏幕坐标系的 左上角开始执行的。

    下一步还是走到了View#Layout(), View#Layout方法如下:

    //View.java     www.yaoxiaowen.com
      public void layout(int l, int t, int r, int b) {
            //...
    
            int oldL = mLeft;
            int oldT = mTop;
            int oldB = mBottom;
            int oldR = mRight;
    
            //setOpticalFrame方法内部还是调用了 setFrame()方法,所以无论如何,最终都会执行setFrame()方法。
            // setFrame()方法会将View新的left,top,right,bottom存储到View的成员变量中,并且返回一个boolean值,
            //返回true,则表示View的位置或尺寸发生了变化;否则就是未发生变化。
            boolean changed = isLayoutModeOptical(mParent) ?
                    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                //如果View布局发生了变化,或者存在 PFLAG_LAYOUT_REQUIRED 标记,则执行以下代码
                //首先 触发onLayout 方法,View中默认的onLayout是个空方法。
                //(因为摆放位置是父容器负责的,View中存在该方法是为了遍历循环的需要)。
                //但是 容器类的ViewGroup则需要实现onLayout方法,从而在 onLayout()方法中依次循环子View,
                //并调用他们的Layout方法。
                onLayout(changed, l, t, r, b);
    
              //...
    
                //我们可以通过 View的addOnLayoutChangeListener(View.onLayoutChangeListener)方法
                //向View 中添加多个 Layout 发生变化的事件监听器。
                //这些监听器都存储在 mListenerInfo.mOnLayoutChangeListeners 这个List当中
    
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnLayoutChangeListeners != null) {
                    // 首先对 mOnLayoutChangeListeners 中的事件监听器 进行拷贝。
                    ArrayList<OnLayoutChangeListener> listenersCopy =
                            (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                    int numListeners = listenersCopy.size();
                    for (int i = 0; i < numListeners; ++i) {
                        // 遍历注册的事件监听器,依次调用其 onLayoutChange 方法,这样 Layout事件监听器就得到了相应。
                        listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                    }
                }
            }
    
           //....
        }
    

    setFrame()方法的作用也不复杂。

    //View.java           www.yaoxiaowen.com
    // setFrame()方法会将View新的left,top,right,bottom存储到View的成员变量中,并且返回一个boolean值,
    //返回true,则表示View的位置或尺寸发生了变化;否则就是未发生变化。
    protected boolean setFrame(int left, int top, int right, int bottom) {
        //...
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
        //...
    }
    

    通过分析以上代码可以知道,在layout()方法当中,主要做的就是将left,top,right,bottom方法保存起来,而View自身怎么被布局(其实我觉的用摆放这个词更合适)。则是父容器需要完成的工作。

    因为总所周知的原因(其实就是各个容器类ViewGroup布局子元素的方式差异很大)。ViewGroup并没有实现具体的onLayout方法,各个具体的ViewGroup,比如LinearLayout,RelativeLayout,FrameLayout等,它们都有它们具体的 onLayout实现方式。
    我们以LinearLayout为例来进行分析。

        @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);
            }
        }
    

    和measure过程是一样的套路,也是分为两种不同的情况,我们就以 竖直方向为例。(源码比较多,并且实际也要考虑使用layout_weight的情况,不过我们仅仅只抽取一部分分析基本原理)。

        //LinearLayout.java      www.yaoxiaowen.com    
        void layoutVertical(int left, int top, int right, int bottom) {
            final int paddingLeft = mPaddingLeft;
    
            int childTop;
            int childLeft;
            //...
    
            //遍历子元素
            for (int i = 0; i < count; i++) {
                final View child = getVirtualChildAt(i);
                if (child == null) {
                    childTop += measureNullChild(i);
                } else if (child.getVisibility() != GONE) {
    
                    //经过了measure过程,我们已经知道了 View的宽高
                    final int childWidth = child.getMeasuredWidth();
                    final int childHeight = child.getMeasuredHeight();
                    
                    //布局文件中,可能设置了 margin等
                    final LinearLayout.LayoutParams lp =
                            (LinearLayout.LayoutParams) child.getLayoutParams();
                    
                   //...
    
                    //随着遍历过程,childTop是逐步增加的。所以说在竖直方向上,后续的View都被摆放在靠下的位置。
                    childTop += lp.topMargin;
                    childLeft = mPaddingLeft + lp.leftMargin;
    
                    //在设置子View时,传递的是 子View 左上角的坐标和宽高尺寸
                    setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                            childWidth, childHeight);
    
                    //childTop逐步的向下增加,并且在增加的过程中,也考虑了 子View的margin,以及偏移量。 
                    childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
    
                    i += getChildrenSkipCount(child, i);
                }
            }
        }
    

    在该方法中,通过调用setChildFrame来为子元素指定相应的位置。
    setChildFrame只是很简单的一句话。

    private void setChildFrame(View child, int left, int top, int width, int height) {        
        child.layout(left, top, left + width, top + height);
    }
        
    

    child.layout其实不就是继续调用 onLayout之类的嘛。所以这样就形成了完美的递归。直到把整个View树都完成了layout过程。

    自此,我们就分析完了layout过程,相比与measure过程,简单了很多。并且理解起来也更容易,就想想在一个大房间的地面上,我们怎么使用递归的方式来摆放很多的盒子。

    最后再来看看几个方法。

      public final int getWidth() {
            return mRight - mLeft;
        }
    
        public final int getHeight() {
            return mBottom - mTop;
        }
        
         public final int getMeasuredHeight() {
            return mMeasuredHeight & MEASURED_SIZE_MASK;
        }
    
         public final int getMeasuredWidth() {
            return mMeasuredWidth & MEASURED_SIZE_MASK;
        }
    
    

    同样都是得到宽高的方式,这两组方法有什么区别呢?现在我们了解了Measure和layout过程,那么我们就知道了,getMeasuredWidth()方法是在measure过程完成之后可以调用的,而getWidth()方法则是在 layout过程之后可以调用的。并且几乎在所有情况下,两者的返回值都是相等的。


    作者: www.yaoxiaowen.com

    github: https://github.com/yaowen369

    欢迎对于本人的博客内容批评指点,如果问题,可评论或邮件(yaowen369@gmail.com)联系

    <p >
    		 欢迎转载,转载请注明出处.谢谢
    </p>
    
    
    <script type="text/javascript">
     function    Curgo()   
     {   
         window.open(window.location.href);
     }   
    </script>
    
  • 相关阅读:
    python中F/f表达式优于format()表达式
    java8新特性-foreach&lambda
    Java实现多线程的四种方式
    Java中的字符串常量池,栈和堆的概念
    java对象只有值传递,为什么?
    面向对象和面向过程程序设计理解及区别
    String对象为什么不可变
    mybatis_plus插件——生成器
    基于grpc的流式方式实现双向通讯(python)
    Django使用DataTables插件总结
  • 原文地址:https://www.cnblogs.com/yaoxiaowen/p/7141855.html
Copyright © 2011-2022 走看看