zoukankan      html  css  js  c++  java
  • 自定义View(三)实现一个简单地流式布局

      Android中的流式布局也就是常说的瀑布流很是常见,不仅在很多项目中都能见到,而且面试中也有很多面试官问道,那么什么是流式布局呢?简单来说就是如果当前行的剩余宽度不足以摆放下一个控件的时候,则自动将控件填充到下一行中,如一些关键字的标签,热搜词列表等位置出现的比较多。而且控件的类型可以摆放不同的,如一行中可以放置TextView,或者是ImageView,布局应该根据行中最大宽度来自动确定自己的每一行的宽度。

      本次的Demo我们要实现如下的效果:

      其实思路还是比较简单容易理顺的,看代码的前几遍可能会感觉到懵逼,但是看过几遍就会有种"哦,就是这么回事。"的感觉了。先来说下我的思路吧:

      因为是流式布局么,布局布局,当然要继承自ViewGroup了,而且本次Demo中没有用到什么自定义属性,所以简单了不少。老样子,自定义View,肯定先是要自定义一个类,我们起名为FlowLayout,名字是不是很贴切?按照惯例,因为要在xml文件中使用,所以重写其一个参数的和两个参数的构造函数。然后:

      重写onMeasure(),在onMeasure()中计算出父布局和子控件的尺寸(还要计算出什么时候一行摆放不下的时候该换行,这部分是关键部分)

      我们定义了两个个全局变量List<List<View>> mAllChildViews和List<Integer> mLineHeight用来在onLayout中使用,前者是用来存放所有的子控件的集合,里层的List集合用来存放每一行的子控件;后者是用来存放每一行中的最大高度的那一个子控件。

      重写onLayout()方法,在onLayout()方法中先要根据控件的宽高和是否有外边距等来确定每行的childView和每行的最大行高,并将其填充到集合中,然后再遍历mAllChildViews集合,绘制每行的childView,这时候就不需要在判断是否需要换行了!因为之前的操作中我们已经确定了哪些childView是属于一行中的。最后,我们可能会在xml文件中使用margin属性,所以要重写三个generateLayoutParams方法,并返回一个MarginLayoutParams对象。下面我们来看一下关键代码:

      

      1 package com.example.customviewgroup;
      2 
      3 import android.content.Context;
      4 import android.util.AttributeSet;
      5 import android.util.Log;
      6 import android.view.View;
      7 import android.view.ViewGroup;
      8 
      9 import java.util.ArrayList;
     10 import java.util.List;
     11 
     12 /**
     13  * 流式布局
     14  */
     15 public class FlowLayout extends ViewGroup {
     16     //存储所有行的view视图  按行记录view  每行的view存储到list集合中
     17     private List<List<View>> mAllChildViews = new ArrayList<>();
     18     //记录每行的最大高度
     19     private List<Integer> mLineHeight = new ArrayList<>();
     20 
     21     public FlowLayout(Context context) {
     22         super(context);
     23     }
     24 
     25     public FlowLayout(Context context, AttributeSet attrs) {
     26         super(context, attrs);
     27     }
     28 
     29     /**
     30      * 负责子控件的测量模式和大小 根据所有的子控件的设置确定ViewGroup自己的宽度和高度
     31      * 首先得到父容器传入的测量模式和宽高的计算值 循环所有的子view 调用measureChild对
     32      * 所有的子view进行测量 然后根据所有的子view测量得出宽度和高度  如果ViewGroup设置为
     33      * warp_content时 ViewGroup的宽度和子view的总宽度一致  高度和子view的最大的高度一致
     34      * <p/>
     35      * 如果viewgroup设置为match_parent时直接根据父viewgroup传入的宽度和高度进行测量设置
     36      */
     37     @Override
     38     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     39         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     40         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
     41         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
     42         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
     43 //        Log.d("tag", "widthSize: "+widthSize);
     44         int heightSize = MeasureSpec.getSize(heightMeasureSpec);
     45 
     46         //定义一个不断累加的总高度 当布局中高度的设置为warp_content时使用该高度
     47         int totalHeight = 0;
     48         //定义不断累加的变量  存放当前行控件的宽度总和
     49         int lineWidth = 0;
     50         //获取当前行控件中最高的控件的高度总和
     51         int lineHeight = 0;
     52         //获取viewGroup中子控件总个数遍历
     53         int childCount = getChildCount();
     54         for (int i = 0; i < childCount; i++) {
     55             View childView = getChildAt(i);
     56             //测量子控件的宽度和高度
     57             // (因为每个子控件的margin可能不同,所以使用measureChild()方法测量每个子控件
     58             // 若没有特定的属性,使用measureChildren()方法即可)
     59             measureChild(childView, widthMeasureSpec, heightMeasureSpec);
     60             //获取每个子控件的布局参数
     61             MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
     62             //获取子控件的实际宽度和高度
     63             int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
     64             int chileHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
     65 
     66             //如果加入当前的childView超出最大的宽度  到目前为止最大的宽度给width 累加高度 开启新行
     67             if (lineWidth + childWidth > widthSize) {
     68                 //开启新行(相当于直接将当前控件摆放到下一行,所以行宽从此childView的宽度开始)
     69                 lineWidth = childWidth;
     70                 //累加高度
     71                 totalHeight += lineHeight;
     72                 lineHeight = chileHeight;//开启记录下一行的高度
     73             } else {
     74                 //累加lineWidth  取得最大的lineHeight
     75                 lineWidth += childWidth;
     76                 lineHeight = Math.max(lineHeight, chileHeight);
     77             }
     78 
     79 //            如果绘制最后一个,因为totalHeight是从0的位置开始的,所以要再加上一个行高
     80             if (i == childCount - 1) {
     81                 totalHeight += lineHeight;
     82             }
     83 
     84             //宽度就设置为与屏幕一致,高度用一个三目运算符,若是高度模式为MeasureSpec.EXACTLY,则与屏幕一致,否则为我们得到的总高度
     85             setMeasuredDimension(widthSize,
     86                     (heightMode == MeasureSpec.EXACTLY) ? heightSize : totalHeight);
     87         }
     88     }
     89 
     90     @Override
     91     protected LayoutParams generateDefaultLayoutParams() {
     92         return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
     93     }
     94 
     95     @Override
     96     public LayoutParams generateLayoutParams(AttributeSet attrs) {
     97         return new MarginLayoutParams(getContext(), attrs);
     98     }
     99 
    100     @Override
    101     protected LayoutParams generateLayoutParams(LayoutParams p) {
    102         return new MarginLayoutParams(p);
    103     }
    104 
    105     /**
    106      * 完成对viewgroup中所有childview的位置的指定
    107      */
    108     @Override
    109     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    110         //先把集合中的内容全部清除掉,然后再加载本次绘制内容(这个地方不太清楚理解的对不对)
    111         mAllChildViews.clear();
    112         mLineHeight.clear();
    113         Log.d("onLayout", "mAllChildViews: " + mAllChildViews);
    114         Log.d("onLayout", "mLineHeight: "+mLineHeight);
    115         //获取当前view的宽度(也就是父布局宽度)
    116         int layoutWidth = getWidth();
    117 //        Log.d("tag", "layoutWidth: "+layoutWidth);
    118         //声明变量 定义每行的宽度和高度
    119         int lineWidth = 0;
    120         int lineHeight = 0;
    121         //声明list集合存储每一行中所有的childView
    122         List<View> lineViews = new ArrayList<>();
    123         int childCount = getChildCount();
    124         for (int i = 0; i < childCount; i++) {
    125             View childView = getChildAt(i);
    126             MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
    127             int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
    128             int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
    129             //如果需要换行
    130             if (lineWidth + childWidth > layoutWidth) {
    131                 //记录这一行中所有的view以及最大高度
    132                 mLineHeight.add(lineHeight);
    133                 //将每一行的childview视图存储到集合中  然后开启新的arraylist记录下一行
    134                 mAllChildViews.add(lineViews);
    135                 //重置行宽度(这里说行宽其实有点不准确,lineWidth其实指的是绘制childView的左上角的点的横坐标)
    136                 lineWidth = 0;
    137                 //开启新的一行,则重新new一个来记录行View的集合
    138                 lineViews = new ArrayList<>();
    139             }
    140             //如果不需要换行 则累加行宽,找出本行最大行高
    141             lineWidth += childWidth;
    142             //lineHeight其实指的是childView的左上角的点的纵坐标
    143             lineHeight = Math.max(lineHeight, childHeight);
    144             //不换行则添加childView到行集合中
    145             lineViews.add(childView);
    146         }
    147 
    148         mLineHeight.add(lineHeight);
    149         mAllChildViews.add(lineViews);
    150 
    151         //声明绘制的起点坐标
    152         int mLeft = 0;
    153         int mTop = 0;
    154         int lineNums = mAllChildViews.size();//获取总行数
    155         for (int i = 0; i < lineNums; i++) {
    156             lineViews = mAllChildViews.get(i);//获取每一行中所有的view
    157             lineHeight = mLineHeight.get(i);//当前行的最大高度
    158             for (int j = 0; j < lineViews.size(); j++) {
    159                 View childView = lineViews.get(j);
    160                 MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
    161                 //计算childView中的left top right bottom
    162                 int left_child = mLeft + lp.leftMargin;
    163                 int top_child = mTop + lp.topMargin;
    164                 int right_child = left_child + childView.getMeasuredWidth();
    165                 int botton_child = top_child + childView.getMeasuredHeight();
    166                 childView.layout(left_child, top_child, right_child, botton_child);
    167                 //指定每个子控件的起始位置
    168                 mLeft += childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
    169             }
    170             //下一行开始,重置行宽(x),累加行高(y)
    171             mLeft = 0;
    172             mTop += lineHeight;
    173         }
    174     }
    175 }

    其实Android5.0之后可以使用RecycleView来实现,不用这么麻烦。

  • 相关阅读:
    mysql获取当前时间,及其相关操作
    notepad++ 样式随我变!
    MySQL索引的创建、删除和查看
    so easy 的弹出层——使用jquery
    mysql获取当前时间,及其相关操作
    侯捷大师畅谈技术人生与读书感悟
    《海量数据库解决方案》之聚簇表的代价
    博文视点大讲堂第44期——招聘真相全揭秘 圆满结束
    程序员修炼道路上的“葵花宝典”——博文视点大讲堂42期快乐结束
    众专家推荐
  • 原文地址:https://www.cnblogs.com/RabbitLx/p/5847148.html
Copyright © 2011-2022 走看看