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. setting
View
Horizontal spacing - B. set
View
The vertical distance between - C. Set the alignment of subviews, currently supported
CENTER_HORIZONTAL
LEFT
和RIGHT
- D. Set the maximum number of children that can be displayed
View
Number, 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 child
View
The number of, will only affect the child that is displayedView
The 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