美文网首页
高级UI<第十二篇>:瀑布流布局的实现

高级UI<第十二篇>:瀑布流布局的实现

作者: NoBugException | 来源:发表于2019-11-28 13:45 被阅读0次

    本文将结合onMeasureonLayout两个方法手写瀑布流布局。onMeasure主要是测量自己本身的大小和子视图的大小,和位置无关。onLayout主要负责视图的摆放,和位置有关。

    图片.png

    如图所示,这就是瀑布流布局。

    手写瀑布流步骤如下:

    【第一步】:创建MyCustomView类,继承ViewGroup,由于MyCustomView是要写在xml中的,所以必须构造两个参数的构造方法,另外,事先测量好当前视图和所有子视图。代码如下:

    public class MyCustomView extends ViewGroup {
    
        public MyCustomView(Context context) {
            super(context);
        }
    
        public MyCustomView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
            //测量当前视图
            setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
            //测量所有的子视图
            measureChildren(widthMeasureSpec, heightMeasureSpec);//测量所有的子布局
    
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    
    
        }
    }
    

    xml中表现形式如下,其中MyCustomView的大小暂定为match_parent,它所有的额子视图的大小暂定为wrap_content

    <com.vrv.viewdemo.MyCustomView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="床"
            android:textSize="26sp"/>
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="床前明月光"
            android:textSize="26sp"/>
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="疑似地上写两个双"
            android:textSize="26sp"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="举头"
            android:textSize="26sp"/>
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="忘明月"
            android:textSize="26sp"/>
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="低"
            android:textSize="26sp"/>
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="头思故乡"
            android:textSize="26sp"/>
    
    </com.vrv.viewdemo.MyCustomView>
    

    【第二步】:在onLayout方法中摆放子视图,首先横向摆放,当水平方向的剩余空间不足以摆放下一个视图时,则换行摆放,直到最后一个视图摆放完成。

    代码如下:

    public class MyCustomView extends ViewGroup {
    
        public MyCustomView(Context context) {
            super(context);
        }
    
        public MyCustomView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
            //测量当前视图
            setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    
            //测量所有的子视图
            measureChildren(widthMeasureSpec, heightMeasureSpec);//测量所有的子布局
    
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    
            //在当前视图测量完毕之后,通过getMeasuredWidth可以精准的获取宽度
            int measuredWidth = getMeasuredWidth();
            //计算当前行视图的最大高度
            int curMaxHeight = 0;
            //当前摆放的宽度
            int curWidth = 0;
            //当前摆放的高度
            int curHeight = 0;
    
            View child;
            //遍历所有的子视图
            for(int i=0;i<getChildCount();i++){
    
                //获取子视图
                child = getChildAt(i);
    
                //当前行视图最大高度
                if(curMaxHeight < child.getMeasuredHeight()){
                    curMaxHeight = child.getMeasuredHeight();
                }
    
                //如果下一个子视图摆放之后超出当前视图的最大宽度,则换行
                if(curWidth + child.getMeasuredWidth() > measuredWidth){
                    curWidth = 0;
                    curHeight = curHeight + curMaxHeight;
                    curMaxHeight = 0;
                }
    
                //摆放
                child.layout(curWidth, curHeight, curWidth + child.getMeasuredWidth(), curHeight + child.getMeasuredHeight());
    
                //计算下一个子视图摆放的水平位置
                curWidth = curWidth + child.getMeasuredWidth();
                
            }
    
        }
    }
    

    效果如下:

    图片.png

    但是,如果将MyCustomView的大小和背景修改一下,如下:

    <com.vrv.viewdemo.MyCustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimaryDark">
    

    这时再来看一下效果,如下:

    图片.png

    显然,MyCustomView在大小设置为wrap_content的条件下依然沾满整个屏幕,这时一个严重的问题,这时需要重新测量MyCustomView视图了。

    【第三步】:解决当前视图大小设置为wrap_content不准确的问题

    首先,让所有子视图测量完毕,最终计算出最大当前宽度和高度,为了计算宽度和高度,显然以下测量子视图的代码并不合适

        //测量所有的子视图
        measureChildren(widthMeasureSpec, heightMeasureSpec);//测量所有的子布局
    

    我们现在要做的事情就是:遍历所有子视图,子视图一个一个的测量,顺便计算出包裹所有子视图的宽度和高度,代码如下:

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
            //获取宽度模式
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            //获取高度模式
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            //获取当前视图的宽度
            int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
            //获取当前视图的高度
            int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
            //当前视图的宽度测量规格
            int widthNewMeasureSpec = widthMeasureSpec;
            //当前视图的高度测量规格
            int heightNewMeasureSpec = heightMeasureSpec;
    
            if(widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY){
    
                //测量父视图
                setMeasuredDimension(widthNewMeasureSpec, heightNewMeasureSpec);
                //测量所有的子布局
                measureChildren(widthNewMeasureSpec, heightNewMeasureSpec);
    
            }else {
                //计算当前行视图的最大高度
                int curMaxHeight = 0;
                //当前摆放的宽度
                int curWidth = 0;
                //当前摆放的高度
                int curHeight = 0;
                //最大总宽度
                int maxWidth = 0;
                //最大总高度
                int maxHeight = 0;
    
                //遍历子视图
                for (int index=0;index<getChildCount();index++){
                    //获取子视图
                    View subview = getChildAt(index);
                    //测量子视图
                    measureChild(subview, widthMeasureSpec, heightMeasureSpec);
    
                    //当前行视图最大高度
                    if(curMaxHeight < subview.getMeasuredHeight()){
                        curMaxHeight = subview.getMeasuredHeight();
                    }
    
                    //计算最大宽度
                    if(maxWidth < curWidth){
                        maxWidth = curWidth;
                    }
    
                    //计算最大高度
                    maxHeight = curHeight + curMaxHeight;
    
                    //如果下一个子视图摆放之后超出当前视图的最大宽度,则换行
                    if(curWidth + subview.getMeasuredWidth() > measuredWidth){
    
                        curWidth = 0;
                        curHeight = curHeight + curMaxHeight;
                        curMaxHeight = 0;
                    }
    
                    //计算下一个子视图摆放的水平位置
                    curWidth = curWidth + subview.getMeasuredWidth();
                }
    
                //计算当前视图新的宽度测量规格
                if(widthMode == MeasureSpec.EXACTLY){
                    widthNewMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, widthMode);
                }else if(widthMode == MeasureSpec.AT_MOST){
                    widthNewMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, widthMode);
                }else{
                    widthNewMeasureSpec = 0;
                }
    
                //计算当前视图新的高度测量规格
                if(heightMode == MeasureSpec.EXACTLY){
                    heightNewMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight, heightMode);
                }else if(heightMode == MeasureSpec.AT_MOST){
                    heightNewMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, heightMode);
                }else{
                    heightNewMeasureSpec = 0;
                }
                setMeasuredDimension(widthNewMeasureSpec, heightNewMeasureSpec);
            }
    
        }
    

    效果如下:

    图片.png

    【第四步】:把padding的情况考虑进去

    <com.vrv.viewdemo.MyCustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="30dp"
        android:background="@color/colorPrimaryDark">
    

    如果在MyCustomView中天假padding,那么视图就变成了这样,如下:

    图片.png

    试问,这个效果谁能忍受?

    那么,该怎么去解决这个问题呢?

    通过以下方式可以获得padding值,如下:

            //获取左padding
            int paddingLeft = getPaddingLeft();
             //获取右padding
            int paddingRight = getPaddingRight();
            //获取上padding
            int paddingTop = getPaddingTop();
            //获取下padding
            int paddingBottom = getPaddingBottom();
    

    将padding考虑在内,调整代码:

    public class MyCustomView extends ViewGroup {
    
        public MyCustomView(Context context) {
            super(context);
        }
    
        public MyCustomView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            //获取宽度模式
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            //获取高度模式
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            //获取当前视图的宽度
            int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
            //获取当前视图的高度
            int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
            //当前视图的宽度测量规格
            int widthNewMeasureSpec = widthMeasureSpec;
            //当前视图的高度测量规格
            int heightNewMeasureSpec = heightMeasureSpec;
    
            if(widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY){
    
                //测量父视图
                setMeasuredDimension(widthNewMeasureSpec, heightNewMeasureSpec);
                //测量所有的子布局
                measureChildren(widthNewMeasureSpec, heightNewMeasureSpec);
    
            }else {
                //计算当前行视图的最大高度
                int curMaxHeight = 0;
                //最大总宽度
                int maxWidth = 0;
                //最大总高度
                int maxHeight = 0;
                //当前摆放的横向位置
                int curWidth = getPaddingLeft();
                //当前摆放的纵向位置
                int curHeight = getPaddingTop();
    
                //遍历子视图
                for (int index=0;index<getChildCount();index++){
                    //获取子视图
                    View subview = getChildAt(index);
                    //测量子视图
                    measureChild(subview, widthMeasureSpec, heightMeasureSpec);
    
                    //当前行视图最大高度
                    if(curMaxHeight < subview.getMeasuredHeight()){
                        curMaxHeight = subview.getMeasuredHeight();
                    }
    
                    //计算最大高度
                    maxHeight = curHeight + curMaxHeight;
    
                    if(index == getChildCount() - 1){
                        maxHeight += getPaddingBottom();
                    }
    
                    //如果下一个子视图摆放之后超出当前视图的最大宽度,则换行
                    if(curWidth + subview.getMeasuredWidth() + getPaddingRight()> measuredWidth){
                        //计算最大宽度
                        if(maxWidth < curWidth){
                            maxWidth = curWidth + getPaddingRight();
                        }
                        curWidth = getPaddingLeft();
                        curHeight = curHeight + curMaxHeight;
                        curMaxHeight = 0;
                    }else{
                        //计算最大宽度
                        if(maxWidth < curWidth){
                            maxWidth = curWidth;
                        }
                    }
    
                    //计算下一个子视图摆放的水平位置
                    curWidth = curWidth + subview.getMeasuredWidth();
                }
    
                //计算当前视图新的宽度测量规格
                if(widthMode == MeasureSpec.EXACTLY){
                    widthNewMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, widthMode);
                }else if(widthMode == MeasureSpec.AT_MOST){
                    widthNewMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, widthMode);
                }else{
                    widthNewMeasureSpec = 0;
                }
    
                //计算当前视图新的高度测量规格
                if(heightMode == MeasureSpec.EXACTLY){
                    heightNewMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight, heightMode);
                }else if(heightMode == MeasureSpec.AT_MOST){
                    heightNewMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, heightMode);
                }else{
                    heightNewMeasureSpec = 0;
                }
    
                setMeasuredDimension(widthNewMeasureSpec, heightNewMeasureSpec);
            }
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    
            //在当前视图测量完毕之后,通过getMeasuredWidth可以精准的获取宽度
            int measuredWidth = getMeasuredWidth();
            //计算当前行视图的最大高度
            int curMaxHeight = 0;
            //当前摆放的宽度
            int curWidth = getPaddingLeft();
            //当前摆放的高度
            int curHeight = getPaddingTop();
    
            View child;
            //遍历所有的子视图
            for(int i=0;i<getChildCount();i++){
    
                //获取子视图
                child = getChildAt(i);
    
                //如果下一个子视图摆放之后超出当前视图的最大宽度,则换行
                if(curWidth + child.getMeasuredWidth() + getPaddingRight()> measuredWidth){
                    curWidth = getPaddingLeft();
                    curHeight = curHeight + curMaxHeight;
                    curMaxHeight = 0;
                }
    
                //摆放
                child.layout(curWidth, curHeight, curWidth + child.getMeasuredWidth(), curHeight + child.getMeasuredHeight());
    
                //当前行视图最大高度
                if(curMaxHeight < child.getMeasuredHeight()){
                    curMaxHeight = child.getMeasuredHeight();
                }
    
                //计算下一个子视图摆放的水平位置
                curWidth = curWidth + child.getMeasuredWidth();
    
            }
        }
    }
    

    效果如下:

    图片.png

    当然,如果子视图的高度不一致时,以上代码也支持,如图:

    图片.png

    【第五步】:把marign的情况考虑进去

    如果在子视图中添加marign是无效的,比如:

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="床"
            android:layout_marginLeft="10dp"
            android:layout_marginTop="20dp"
            android:textSize="26sp"/>
    

    为了让marign有效,还需要做一些处理,使用以下代码可以获取margin值

        MarginLayoutParams marginLayoutParams = (MarginLayoutParams) subview.getLayoutParams();
    

    因为使用了MarginLayoutParams,所以当前自定义视图必须重写generateLayoutParams方法

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet params) {
        return new MarginLayoutParams(getContext(), params);
    }
    

    将获取到的margin加入代码,修改后的代码最终为:

    public class MyCustomView extends ViewGroup {
    
        public MyCustomView(Context context) {
            super(context);
        }
    
        public MyCustomView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public ViewGroup.LayoutParams generateLayoutParams(AttributeSet params) {
            return new MarginLayoutParams(getContext(), params);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            //获取宽度模式
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            //获取高度模式
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            //获取当前视图的宽度
            int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
            //获取当前视图的高度
            int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
            //当前视图的宽度测量规格
            int widthNewMeasureSpec = widthMeasureSpec;
            //当前视图的高度测量规格
            int heightNewMeasureSpec = heightMeasureSpec;
    
            if(widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY){
    
                //测量父视图
                setMeasuredDimension(widthNewMeasureSpec, heightNewMeasureSpec);
                //测量所有的子布局
                measureChildren(widthNewMeasureSpec, heightNewMeasureSpec);
    
            }else {
                //计算当前行视图的最大高度
                int curMaxHeight = 0;
                //最大总宽度
                int maxWidth = 0;
                //最大总高度
                int maxHeight = 0;
                //当前摆放的横向位置
                int curWidth = getPaddingLeft();
                //当前摆放的纵向位置
                int curHeight = getPaddingTop();
    
                //遍历子视图
                for (int index=0;index<getChildCount();index++){
                    //获取子视图
                    View subview = getChildAt(index);
                    //测量子视图
                    measureChild(subview, widthMeasureSpec, heightMeasureSpec);
    
                    //获取布局参数,可以获取margin
                    MarginLayoutParams marginLayoutParams = (MarginLayoutParams) subview.getLayoutParams();
                    curWidth += marginLayoutParams.leftMargin;
    
                    //当前行视图最大高度
                    if(curMaxHeight < subview.getMeasuredHeight() + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin){
                        curMaxHeight = subview.getMeasuredHeight() + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin;
                    }
    
                    //如果下一个子视图摆放之后超出当前视图的最大宽度,则换行
                    if(curWidth + subview.getMeasuredWidth() + marginLayoutParams.rightMargin + getPaddingRight()> measuredWidth){
                        //计算最大宽度
                        if(maxWidth < curWidth){
                            maxWidth = curWidth + marginLayoutParams.rightMargin + getPaddingRight();
                        }
                        curWidth = getPaddingLeft();
                        curHeight = curHeight + curMaxHeight;
                        maxHeight = curHeight + marginLayoutParams.bottomMargin + subview.getMeasuredHeight();
                        curMaxHeight = 0;
                    }else{
                        //计算最大宽度
                        if(maxWidth < curWidth){
                            maxWidth = curWidth + marginLayoutParams.rightMargin;
                        }
                        //计算最大高度
                        maxHeight = curHeight + curMaxHeight;
    
                        if(index == getChildCount() - 1){
                            maxHeight += getPaddingBottom();
                        }
                    }
    
                    //计算下一个子视图摆放的水平位置
                    curWidth = curWidth + subview.getMeasuredWidth() + marginLayoutParams.rightMargin;
                }
    
                //计算当前视图新的宽度测量规格
                if(widthMode == MeasureSpec.EXACTLY){
                    widthNewMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, widthMode);
                }else if(widthMode == MeasureSpec.AT_MOST){
                    widthNewMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, widthMode);
                }else{
                    widthNewMeasureSpec = 0;
                }
    
                //计算当前视图新的高度测量规格
                if(heightMode == MeasureSpec.EXACTLY){
                    heightNewMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight, heightMode);
                }else if(heightMode == MeasureSpec.AT_MOST){
                    heightNewMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, heightMode);
                }else{
                    heightNewMeasureSpec = 0;
                }
    
                setMeasuredDimension(widthNewMeasureSpec, heightNewMeasureSpec);
            }
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    
            //在当前视图测量完毕之后,通过getMeasuredWidth可以精准的获取宽度
            int measuredWidth = getMeasuredWidth();
            //计算当前行视图的最大高度
            int curMaxHeight = 0;
            //当前摆放的宽度
            int curWidth = getPaddingLeft();
            //当前摆放的高度
            int curHeight = getPaddingTop();
    
            View child;
            //遍历所有的子视图
            for(int i=0;i<getChildCount();i++){
    
                //获取子视图
                child = getChildAt(i);
    
                //获取布局参数,可以获取margin
                MarginLayoutParams marginLayoutParams = (MarginLayoutParams) child.getLayoutParams();
                curWidth += marginLayoutParams.leftMargin;
                //如果下一个子视图摆放之后超出当前视图的最大宽度,则换行
                if(curWidth + child.getMeasuredWidth() + marginLayoutParams.rightMargin + getPaddingRight()> measuredWidth){
                    curWidth = getPaddingLeft();
                    curHeight = curHeight + curMaxHeight;
                    curMaxHeight = 0;
                }
    
                //摆放
                child.layout(curWidth, curHeight + marginLayoutParams.topMargin, curWidth + child.getMeasuredWidth(), curHeight + marginLayoutParams.topMargin + child.getMeasuredHeight());
    
                //当前行视图最大高度
                if(curMaxHeight < child.getMeasuredHeight() + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin){
                    curMaxHeight = child.getMeasuredHeight() + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin;
                }
    
                //计算下一个子视图摆放的水平位置
                curWidth = curWidth + child.getMeasuredWidth() + marginLayoutParams.rightMargin;
    
            }
        }
    }
    

    在布局中设置padding和margin,如下:

    <com.vrv.viewdemo.MyCustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="20dp"
        android:background="@color/colorPrimaryDark">
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="床"
            android:layout_margin="20dp"
            android:textSize="26sp"/>
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="床前明月光"
            android:textSize="26sp"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="疑"
            android:layout_marginTop="30dp"
            android:textSize="26sp"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="似地上写两个双"
            android:textSize="26sp"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="举头"
            android:layout_marginBottom="20dp"
            android:textSize="26sp"/>
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="忘明月"
            android:layout_margin="40dp"
            android:textSize="26sp"/>
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="低"
            android:textSize="26sp"/>
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="头思故乡"
            android:layout_marginTop="10dp"
            android:textSize="26sp"/>
    
    </com.vrv.viewdemo.MyCustomView>
    

    最终效果为:

    图片.png

    声明:以上代码逻辑是按照本人的思维编写的,不建议直接抄袭,如果想要学习的话,强烈建议跟我一样自己动手,手写一个瀑布流布局。

    [本章完...]

    相关文章

      网友评论

          本文标题:高级UI<第十二篇>:瀑布流布局的实现

          本文链接:https://www.haomeiwen.com/subject/malywctx.html