zoukankan      html  css  js  c++  java
  • 自定义流式布局:ViewGroup的测量与布局

    1、View生命周期以及View层级

    1.1、View生命周期

    ​ View的主要生命周期如下所示, 包括创建、测量(onMeasure)、布局(onLayout)、绘制(onDraw)以及销毁等流程。

    ​ 自定义View主要涉及到onMeasure、onLayout和onDraw这三个过程,其中

    ​ (1)自定义View(继承自View类):主要实现onMeasure和onDraw,

    ​ (2)自定义ViewGroup(继承自ViewGroup类):主要实现onMeasure和onLayout。

    1.2、View层级

    ​ View层级是一个树形结构。

    ​ onMeasure、onLayout和onDraw这三个过程都是按照View层级从上到下进行的:(1)ViewGroup主要负责onMeasure和onLayout,确定自身及其子View的大小和放置方式,例如LinearLayout通过onMeasure确定尺寸,通过onLayout对子View进行横向或者纵向布局;(2)View主要负责onMeasure和onDraw,例如TextView通过onMeasure确定自身尺寸,通过onDraw绘制文字。

    2、View测量与MeasureSpec类

    ​ View测量中最难的一点就是如何根据View的LayoutParams参数确定其实际的宽高,如:

    android:layout_width="10dp"
    android:layout_width="match_parent"
    android:layout_width="wrap_content"
    

    这三种情况,View的宽度究竟应该是多少?这就要从View的测量过程分析了,

    2.1、MeasureSpec类

    ​ View类的内部类MeasureSpec用来辅助View的测量,使用一个int型变量measureSpec来表示View测量的模式和具体的尺寸(宽和高各一个measureSpec值)。measureSpec共32位,用高两位表示测量模式mode, 通过MeasureSpec.getMode(measureSpec)计算获得, 低30位表示尺寸size,通过MeasureSpec.getSize(measureSpec)计算获得。

    ​ mode共有三种情况:

    MeasureSpec.UNSPECIFIED:不对View大小做限制,系统使用
    MeasureSpec.EXACTLY:确切的大小,如:10dp
    MeasureSpec.AT_MOST:大小不可超过某数值,最大不能超过其父类

    2.2、父View的限制 :测量约束,限制最大宽度、最大高度等

    ​ View的测量过程受到父View的限制,如对一个ViewGroup测量时,其高度测量模式mode为EXACTLY,高度尺寸size为100dp,其子View的高度测量依据对应的android:layout_height参数来确定:

    ​ (1)具体尺寸值,如50dp,则该子View高度测量中mode为EXACTLY,尺寸为50dp;

    ​ (2)match_parent,则该子View高度和其父View高度相同,也是确定的,高度测量中mode为EXACTLY,尺寸为100dp;

    ​ (3)wrap_content, 则该子View最大高度为100dp, 确切高度需要根据内部逻辑确定,像TextView需要根据文字内容、宽度等综合确定,于是高度测量中mode为AT_MOST, 尺寸size为100dp。

    ​ 其他情况类似,如父View的mode分别为AT_MOST、UNSPECIFIED,具体见下表:

    ​ 高度测量中mode和size确定后,可通过MeasureSpec.makeMeasureSpec(size, mode)来确定heightMeasureSpec,widthMeasureSpec使用同样的方法确定。该方法的具体实现为ViewGroup.getChildMeasureSpec()方法。

    2.3、子View的影响:实际测量

    ​ 测量过程以LinearLayout作为例子说明:

    ​ (1) LinearLayout根据父View的measureSpec以及自身的LayoutParams确定了自身的widthMeasureSpec、heightMeasureSpec后, 调用measure(widthMeasureSpec, heightMeasureSpec) -----> onMeasure(widthMeasureSpec, heightMeasureSpec)来进行实际的测量;

    ​ (2) 当该LinearLayout方向为vertical时,实际测量中应该计算所有子View的高度之和,作为LinearLayout的测量高度needHeight;

    ​ (3) heightMeasureSpec中size为父类给该LinearLayout的限制高度,根据heightMeasureSpec中mode判断是取needHeight, 还是heightMeasureSpec中size, 然后调用setMeasuredDimension将测量的高度和宽度设置进去。

    2.4、View的测量过程

    ​ Android中View测量是一种递归的过程(见下图),首先View调用measure方法,内部调用了自身的onMeasure方法,这个方法内部调用子View的measure方法(子View同样会调用自身的onMeasure方法),对子View进行测量,保存子View的测量尺寸,测量完所有的子View后再对自身测量,保存测量尺寸,之后便可以通过View.getMeasuredWidth()和View.getMeasuredHeight()来获取View的测量宽高。

    3、自定义流式布局FlowLayout

    主要思路

    ​ 对FlowLayout的所有子View逐个进行测量,获得measuredHeight和measuredWidth,在水平方向上根据这个尺寸依次对View进行放置,放不下则另起一行,每一行的高度取该行所有View的measuredHeight最大值。

    3.1、单个子View测量

    ​ 对其指定子View----child的测量代码如下,其中paddingLeft、paddingRight、paddingTop、paddingBottom分别是FlowLayout四边上的padding,widthMeasureSpec以及heightMeasureSpec是FlowLayout中onMeasure中的两个参数。

    int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, 
               paddingLeft + paddingRight, child.getLayoutParams().width);
    int childHeightSpec = ViewGroup.getChildMeasureSpec(heightMeasureSpec, 
               paddingTop + paddingBottom, child.getLayoutParams().height);
    child.measure(childWidthSpec, childHeightSpec);
    

    ​ 于是子View的测量宽、高分别可以通过child.getMeasuredWidth() 和child.getMeasuredHeight()来进行获得。

    3.2、onMeasure:测量与模拟布局View

       //子View的横向间隔、纵向间隔
       private final int horizontalSpace = dp2px(20);
       private final int verticalSpace = dp2px(10);
       
       //保存测量的子View, 每一个元素为一行的子View数组
       private final List<List<View>> allLines = new ArrayList<>();
       //记录每一行的最大高度,用于布局
       private final List<Integer> heights = new ArrayList<>();
    
       @Override
       protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
           allLines.clear();
           heights.clear();
    
           int paddingLeft = getPaddingLeft();
           int paddingRight = getPaddingRight();
           int paddingTop = getPaddingTop();
           int paddingBottom = getPaddingBottom();
    
           int usedWidth = 0;
           int height = 0;
    
           //父布局对FlowLayout的约束宽高
           int seftWidth = MeasureSpec.getSize(widthMeasureSpec) - paddingLeft -
                      paddingRight;
           int seftHeight = MeasureSpec.getSize(heightMeasureSpec) - paddingTop - 
                      paddingBottom;
    
           //FlowLayout的测量宽高
           int needHeight = 0;
           int needWidth = 0;
    
           List<View> line = new ArrayList<>();
    
    
           int count = getChildCount();
           for (int i = 0; i < count; i++) {
               View child = getChildAt(i);
    
               int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
                      paddingLeft + paddingRight, child.getLayoutParams().width);
               int childHeightSpec = ViewGroup.getChildMeasureSpec(heightMeasureSpec, 
                      paddingTop + paddingBottom, child.getLayoutParams().height);
               child.measure(childWidthSpec, childHeightSpec);
    
               if (usedWidth + horizontalSpace + child.getMeasuredWidth() > seftWidth) {
                   //当前行无法在放下下一个view,则保存当前行的Views集合以及当前行的最大高度,
                   heights.add(height + verticalSpace);
                   allLines.add(line);
                   //所有行的最大宽度
                   needWidth = Math.max(needWidth, usedWidth);
                   //所有行的高度之和
                   needHeight += height + verticalSpace;
    
                   //重置下一行的使用宽度、高度、View集合
                   usedWidth = 0;
                   height = 0;
                   line = new ArrayList<>();
               }
               //获取当前行的最大高度,作为当前行的高度
               height = Math.max(height, child.getMeasuredHeight());
               //记录已经使用的宽度(第一个元素不需要加横向间隔
               usedWidth += child.getMeasuredWidth() + (line.size() == 0 ? 0 : 
                     horizontalSpace);
               //保存已经测量及模拟布局的View
               line.add(child);
    
               //记录最后一行的数据
               if (i == count - 1) {
                   heights.add(height + verticalSpace);
                   allLines.add(line);
                   needWidth = Math.max(needWidth, usedWidth);
                   needHeight += height + verticalSpace;
               }
           }
    
           int widthMode = MeasureSpec.getMode(widthMeasureSpec);
           int heightMode = MeasureSpec.getMode(heightMeasureSpec);
           //如果mode为MeasureSpec.EXACTLY, 则使用widthMeasureSpec中的size,
           //不然使用测量得到的size, 宽高同理
           int realWidth = widthMode == MeasureSpec.EXACTLY ? seftWidth : needWidth;
           int realHeight = heightMode == MeasureSpec.EXACTLY ? seftHeight : needHeight;
    
           //保存测量的宽和高
           setMeasuredDimension(realWidth + paddingLeft + paddingRight,
                   //如果只有一行,不需要纵向间隔
                   realHeight + paddingTop + paddingBottom - (allLines.size() > 0 ?
                   verticalSpace : 0));
       }
    

    3.3、布局:onLayout

        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            int left = getPaddingLeft();
            int top = getPaddingTop();
            for (int i = 0; i < allLines.size(); i++) {
                List<View> line = allLines.get(i);
                for (int j = 0; j < line.size(); j++) {
                    View child = line.get(j);
                    child.layout(left, top, left + child.getMeasuredWidth(), 
                            top + child.getMeasuredHeight());
                    //一行中View布局后每次向后移动child的测量宽 + 横向间隔
                    left += child.getMeasuredWidth() + horizontalSpace;
                }
                //每一行布局从paddingLeft开始
                left = getPaddingLeft();
                //布局完成一行,向下移动当前行的最大高度
                top += heights.get(i);
            }
        }
    

    3.4、测试

    测试代码如下:

        private final List<String> words = Arrays.asList("家用电器", "手机", "运营商", "数码",
          "电脑", "办公", "电子书", "惠普星系列高清一体机", "格力2匹移动空调");
    
        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.layout_flow);
    
            FlowLayout layout = findViewById(R.id.flow_layout);
    
    
            for (int i = 0; i < words.size(); i++) {
                TextView textView = new TextView(this);
                textView.setText(words.get(i));
                textView.setBackground(ContextCompat.getDrawable(this,
                        R.drawable.round_background));
                textView.setLayoutParams(new ViewGroup.LayoutParams(
                        ViewGroup.LayoutParams.WRAP_CONTENT, FlowLayout.dp2px(60)));
                //textView.setLayoutParams(new ViewGroup.LayoutParams(
                //          ViewGroup.LayoutParams.WRAP_CONTENT, 
                //          ViewGroup.LayoutParams.WRAP_CONTENT));
                int padding = FlowLayout.dp2px(5);
                textView.setPadding(padding, padding, padding, padding);
    
                layout.addView(textView);
            }
        }
    

    效果图:

    demo

    链接

  • 相关阅读:
    centos7.6 redis
    centos7.5 rabbitmq3.7.4
    centos7.6 nfs
    nginx跨域
    Linux进阶教程丨第1章:访问命令行
    Representation Learning for Event-based Visuomotor Policies
    Goland 2019下载和安装(带破解补丁和汉化包)
    Adobe XD CC 2020中文破解版(附破解教程)
    SSH 三步解决免密登录
    cygwin命令行安装
  • 原文地址:https://www.cnblogs.com/wushengwuxi/p/15159259.html
Copyright © 2011-2022 走看看