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

2019-11-28  本文已影响0人  NoBugException

本文将结合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

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

[本章完...]

上一篇下一篇

猜你喜欢

热点阅读