To read this article, you need to have a good understanding of the View drawing process

I. Business background

Have you customized a ViewGroup? I am sorry to tell you that I am not very good at it. During my working time in Douyu, I found many interesting UI components, and I am also very interested in them. However, enterprises are oriented by business, and few of them implement a ViewGroup by themselves. This layout makes the child View look like float:left in CSS, lining the child View from left to right and wrapping it. Supports the following features:

  • You can control the vertical/horizontal spacing of child views
  • You can control the maximum number of child views or the maximum number of rows
  • XML custom attributes are supported

Two. Implementation ideas

2.1 Define a MkFloatLayout that inherits ViewGroup and overrides three important constructors

    public MkFloatLayout(Context context) {
        this(context, null);
    }

    public MkFloatLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MkFloatLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }
Copy the code

2.2 Customize the MkFloatLayout property and then define the property value in styles

    <declare-styleable name="MkFloatLayout">
        <attr name="android:gravity" />
        <attr name="dimision" name="mk_childHorizontalSpacing" />
        <attr name="dimision" name="mk_childVerticalSpacing" />
        <attr name="android:maxLines" />
        <attr name="mk_maxNumber" />
    </declare-styleable>
Copy the code

2.3 Initializing the MkFlayout attribute

  • A. settingViewHorizontal spacing
  • B. setViewThe vertical distance between
  • C. Set the alignment of subviews, currently supportedCENTER_HORIZONTAL LEFTRIGHT
  • D. Set the maximum number of children that can be displayedViewNumber, note that this method does not change the subView, will only affect the number of child views displayed
  • E. Set the maximum number of lines that can be displayed. Note that this method does not change the childViewThe number of, will only affect the child that is displayedViewThe number of
    private void init(Context context, AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs,
                R.styleable.MkFloatLayout);
        mChildHorizontalSpacing = array.getDimensionPixelSize(
                R.styleable.MkFloatLayout_mk_childHorizontalSpacing, 0);
        mChildVerticalSpacing = array.getDimensionPixelSize(
                R.styleable.MkFloatLayout_mk_childVerticalSpacing, 0);
        mGravity = array.getInteger(R.styleable.MkFloatLayout_android_gravity, Gravity.START);
        int maxLines = array.getInt(R.styleable.MkFloatLayout_android_maxLines, -1);
        if (maxLines >= 0) {
            setMaxLines(maxLines);
        }
        int maxNumber = array.getInt(R.styleable.MkFloatLayout_android_max, -1);
        if (maxNumber >= 0) {
            setMaxNumber(maxNumber);
        }
        array.recycle();
        array =null;
    }
    
       public void setMaxNumber(int maxNumber) {
        mMaximum = maxNumber;
        mMaxMode = NUMBER;
        requestLayout();
    }
    
     public void setMaxLines(int maxLines) {
        mMaximum = maxLines;
        mMaxMode = LINES;
        requestLayout();
    }
    
        /** * Get the maximum number of child views */
    public int getMaxNumber(a) {
        return mMaxMode == NUMBER ? mMaximum : -1;
    }
    
    /** * gets the maximum number of rows that can be displayed **@returnReturns -1 */ if there is no limit
    public int getMaxLines(a) {
        return mMaxMode == LINES ? mMaximum : -1;
    }
    
        /** * Sets the horizontal spacing of child views */
    public void setChildHorizontalSpacing(int spacing) {
        mChildHorizontalSpacing = spacing;
        invalidate();
    }
    
        /** * Sets the vertical spacing of child views */
    public void setChildVerticalSpacing(int spacing) {
        mChildVerticalSpacing = spacing;
        invalidate();
    }


    
    public void setGravity(int gravity) {
        if (mGravity != gravity) {
            mGravity = gravity;
            requestLayout();
        }
    }

    public int getGravity(a) {
        return mGravity;
    }

    
Copy the code

2.4 Overriding the onMeasure Method

2.4.1 Get the measurement specifications of MkFlayout

    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);   
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
Copy the code

2.4.2 If FloatLayout specifies MATCH_PARENT or a fixed width, you need to wrap the child View

// Maximum height of subview
       int maxLineHeight = 0;
// Measure the width of or
        int resultWidth;
 // The measured height
        int resultHeight;

// Get the number of child Views
        final int count = getChildCount();
// The number of items per line, the subscript representing the line subscript, is calculated when onMeasured and used by onLayout.
// If mItemNumberInEachLine[x]==0, there is no item in line x
        mItemNumberInEachLine = new int[count];
        // The width and (including the direct spacing between items) of each line, with subscripts indicating row subscripts,
        // mWidthSumInEachLine[x] specifies the width and spacing of the item on line x.
        // Calculate onMeasured for onLayout
        mWidthSumInEachLine = new int[count];
        // Index of rows
        int lineIndex = 0;
        // If FloatLayout specifies MATCH_PARENT or a fixed width, you need to wrap the child View
        if (widthSpecMode == MeasureSpec.EXACTLY) {
            resultWidth = widthSpecSize;

            measuredChildCount = 0;

            // Position of the next child View
            int childPositionX = getPaddingLeft();
            int childPositionY = getPaddingTop();

            // The maximum reachable x coordinate of the child View's Right
            int childMaxRight = widthSpecSize - getPaddingRight();

            for (int i = 0; i < count; i++) {
                if (mMaxMode == NUMBER && measuredChildCount >= mMaximum) {
                    // If the number exceeds the maximum, no further action is required
                    break;
                } else if (mMaxMode == LINES && lineIndex >= mMaximum) {
                    // If the maximum number of rows is exceeded, no further action is required
                    break;
                }

                // Get the object of the child view
                final View child = getChildAt(i);
                if (child.getVisibility() == GONE) {
                    continue;
                }
// Get the measurement parameters of the child view
                final LayoutParams childLayoutParams = child.getLayoutParams();

                // Get the width measurement of the child View
                final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeft() + getPaddingRight(), childLayoutParams.width);

                // Get the height measurement specification of the child View
                final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                        getPaddingTop() + getPaddingBottom(), childLayoutParams.height);

                / / get the View
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

                // Get the measured width of the child view
                final int childw = child.getMeasuredWidth();

                // Get the maximum height of the child view
                maxLineHeight = Math.max(maxLineHeight, child.getMeasuredHeight());

                PaddingLeft + childw
                if (childPositionX + childw > childMaxRight) {
                    // If the maximum number of lines is exceeded after a line break, no further action is taken
                    if (mMaxMode == LINES) {
                        if (lineIndex + 1 >= mMaximum) {
                            break; }}// Add a space to each item, so each line adds another space to the last item, so subtract one space here
                    mWidthSumInEachLine[lineIndex] -= mChildHorizontalSpacing;
                    / / a newline
                    lineIndex++;
                    // The x of the first item in the next line
                    childPositionX = getPaddingLeft();
                    // the y of the first item in the next line
                    childPositionY += maxLineHeight + mChildVerticalSpacing;
                }
                mItemNumberInEachLine[lineIndex]++;
                mWidthSumInEachLine[lineIndex] += (childw + mChildHorizontalSpacing);
                childPositionX += (childw + mChildHorizontalSpacing);
                measuredChildCount++;
            }
            // If the last item is not at the end of the line (i.e. LineCount does not end with +1, i.e. MWidthSumInEachLine [lineCount] is non-0), then the space of the last item is subtracted
            if (mWidthSumInEachLine.length > 0 && mWidthSumInEachLine[lineIndex] > 0) {
                mWidthSumInEachLine[lineIndex] -= mChildHorizontalSpacing;
            }
            if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
                resultHeight = childPositionY + maxLineHeight + getPaddingBottom();
            } else if (heightSpecMode == MeasureSpec.AT_MOST) {
                resultHeight = childPositionY + maxLineHeight + getPaddingBottom();
                resultHeight = Math.min(resultHeight, heightSpecSize);
            } else{ resultHeight = heightSpecSize; }}Copy the code

2.4.3 Do not calculate the newline, directly spread a line

resultWidth = getPaddingLeft() + getPaddingRight();
            measuredChildCount = 0;

            for (int i = 0; i < count; i++) {
                if (mMaxMode == NUMBER) {
                    // If the number exceeds the maximum, no further action is required
                    if (measuredChildCount > mMaximum) {
                        break; }}else if (mMaxMode == LINES) {
                    // If the number of rows exceeds the maximum, no further action is required
                    if (1 > mMaximum) {
                        break; }}final View child = getChildAt(i);
                if (child.getVisibility() == GONE) {
                    continue;
                }
                final LayoutParams childLayoutParams = child.getLayoutParams();
                final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeft() + getPaddingRight(), childLayoutParams.width);
                final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                        getPaddingTop() + getPaddingBottom(), childLayoutParams.height);
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
                resultWidth += child.getMeasuredWidth();
                maxLineHeight = Math.max(maxLineHeight, child.getMeasuredHeight());
                measuredChildCount++;
            }
            if (measuredChildCount > 0) {
                resultWidth += mChildHorizontalSpacing * (measuredChildCount - 1);
            }
            resultHeight = maxLineHeight + getPaddingTop() + getPaddingBottom();
            if (mItemNumberInEachLine.length > 0) {
                mItemNumberInEachLine[lineIndex] = count;
            }
            if (mWidthSumInEachLine.length > 0) {
                mWidthSumInEachLine[0] = resultWidth;
            }
        }
        setMeasuredDimension(resultWidth, resultHeight);
        int meausureLineCount = lineIndex + 1;
        if(mLineCount ! = meausureLineCount) {if(mOnLineCountChangeListener ! =null) {
                mOnLineCountChangeListener.onChange(mLineCount, meausureLineCount);
            }
            mLineCount = meausureLineCount;
        }
        
Copy the code

2.5 Rewrite the onLayout method

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int width = right - left;
        // Use different layouts according to gravity, default is left
        switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
            case Gravity.START:
                layoutWithGravityLeft(width);
                break;
            case Gravity.END:
                layoutWithGravityRight(width);
                break;
            case Gravity.CENTER_HORIZONTAL:
                layoutWithGravityCenterHorizontal(width);
                break;
            default:
                layoutWithGravityLeft(width);
                break; }}Copy the code

2.5.1 Center the layout of subviews

    private void layoutWithGravityCenterHorizontal(int parentWidth) {
        int nextChildIndex = 0;
        int nextChildPositionX;
        int nextChildPositionY = getPaddingTop();
        int lineHeight = 0;
        int layoutChildCount = 0;
        int layoutChildEachLine = 0;

        // Iterate over each line
        for (int i = 0; i < mItemNumberInEachLine.length; i++) {
            // If there is no item left in the line, exit the loop
            if (mItemNumberInEachLine[i] == 0) {
                break;
            }

            // Walk through the elements in the row, laying out each element
            nextChildPositionX = (parentWidth - getPaddingLeft() - getPaddingRight() - mWidthSumInEachLine[i]) / 2 + getPaddingLeft(); // The minimum x value of the child View
            while (layoutChildEachLine < mItemNumberInEachLine[i]) {
                final View childView = getChildAt(nextChildIndex);
                if (childView.getVisibility() == GONE) {
                    nextChildIndex++;
                    continue;
                }
                final int childw = childView.getMeasuredWidth();
                final int childh = childView.getMeasuredHeight();
                childView.layout(nextChildPositionX, nextChildPositionY, nextChildPositionX + childw, nextChildPositionY + childh);
                lineHeight = Math.max(lineHeight, childh);
                nextChildPositionX += childw + mChildHorizontalSpacing;
                layoutChildCount++;
                layoutChildEachLine++;
                nextChildIndex++;
                if (layoutChildCount == measuredChildCount) {
                    break; }}if (layoutChildCount == measuredChildCount) {
                break;
            }

            // The next line is ready
            nextChildPositionY += (lineHeight + mChildVerticalSpacing);
            lineHeight = 0;
            layoutChildEachLine = 0;
        }

        int childCount = getChildCount();
        for (int i = nextChildIndex; i < childCount; i++) {
            final View childView = getChildAt(i);
            if (childView.getVisibility() == View.GONE) {
                continue;
            }
            childView.layout(0.0.0.0); }}Copy the code

2.5.2 Layout the sub-View to the right

    private void layoutWithGravityRight(int parentWidth) {
        int nextChildIndex = 0;
        int nextChildPositionX;
        int nextChildPositionY = getPaddingTop();
        int lineHeight = 0;
        int layoutChildCount = 0;
        int layoutChildEachLine = 0;

        // Iterate over each line
        for (int i = 0; i < mItemNumberInEachLine.length; i++) {
            // If there is no item left in the line, exit the loop
            if (mItemNumberInEachLine[i] == 0) {
                break;
            }

            // Walk through the elements in the row, laying out each element
            nextChildPositionX = parentWidth - getPaddingRight() - mWidthSumInEachLine[i]; // The initial value is the minimum x value of the child View
            while (layoutChildEachLine < mItemNumberInEachLine[i]) {
                final View childView = getChildAt(nextChildIndex);
                if (childView.getVisibility() == GONE) {
                    nextChildIndex++;
                    continue;
                }
                final int childw = childView.getMeasuredWidth();
                final int childh = childView.getMeasuredHeight();
                childView.layout(nextChildPositionX, nextChildPositionY, nextChildPositionX + childw, nextChildPositionY + childh);
                lineHeight = Math.max(lineHeight, childh);
                nextChildPositionX += childw + mChildHorizontalSpacing;
                layoutChildCount++;
                layoutChildEachLine++;
                nextChildIndex++;
                if (layoutChildCount == measuredChildCount) {
                    break; }}if (layoutChildCount == measuredChildCount) {
                break;
            }

            // The next line is ready
            nextChildPositionY += (lineHeight + mChildVerticalSpacing);
            lineHeight = 0;
            layoutChildEachLine = 0;
        }

        int childCount = getChildCount();
        for (int i = nextChildIndex; i < childCount; i++) {
            final View childView = getChildAt(i);
            if (childView.getVisibility() == View.GONE) {
                continue;
            }
            childView.layout(0.0.0.0); }}Copy the code

2.5.3 Layout sub-Views to the left

        private void layoutWithGravityLeft(int parentWidth) {
        int childMaxRight = parentWidth - getPaddingRight();
        int childPositionX = getPaddingLeft();
        int childPositionY = getPaddingTop();
        int lineHeight = 0;
        final int childCount = getChildCount();
        int layoutChildCount = 0;
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() == GONE) {
                continue;
            }
            if (layoutChildCount < measuredChildCount) {
                final int childw = child.getMeasuredWidth();
                final int childh = child.getMeasuredHeight();
                if (childPositionX + childw > childMaxRight) {
                    / / a newline
                    childPositionX = getPaddingLeft();
                    childPositionY += (lineHeight + mChildVerticalSpacing);
                    lineHeight = 0;
                }
                child.layout(childPositionX, childPositionY, childPositionX + childw, childPositionY + childh);
                childPositionX += childw + mChildHorizontalSpacing;
                lineHeight = Math.max(lineHeight, childh);
                layoutChildCount++;
            } else {
                child.layout(0.0.0.0); }}}Copy the code

3. Usage Guide

  • Set height maximum row height and maximum width height
     <com.github.microkibaco.mk_video_view.MkFloatLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/test"
            android:layout_marginLeft="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="12dp"
            android:layout_marginRight="12dp"
            android:layout_marginBottom="4dp"
            android:gravity="end"
            app:mk_childHorizontalSpacing="12dp"
            app:mk_childVerticalSpacing="12dp"
            />
Copy the code

Java code


         MkFloatLayout mkfloatLayout = findViewById(R.id.hot_list);
        
        LayoutInflater layoutInflater = LayoutInflater.from(this);
        TextView hotWordTag = (TextView) layoutInflater.inflate(R.layout.view_search_word_tag, mkfloatLayout, false);
        hotWordTag.setText("Hello");
        ImageView imgTag = (ImageView)         layoutInflater.inflate(R.layout.view_image_word_tag, mkfloatLayout, false);
         mkfloatLayout.addView(hotWordTag);
        mkfloatLayout.addView(imgTag);
Copy the code