如何通过扩展 ViewGroup 类在 Android 中创建自定义布局

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

ViewGroup简介

Android 中的 ViewGroup 是一种特殊的视图,可以包含其他视图。一个 ViewGroup 可以包含一个或多个孩子。所有其他标准布局管理器(如 LinearLayout FrameLayout RelativeLayout ) 都是 ViewGroup 类的专门子类,它们以特定格式布局其子类。例如,LinearLayout 将其子项布局为垂直或水平相邻。

有时,由于需求的特殊性,标准的布局管理器是不够的。您需要扩展 ViewGroup 类来创建您自己的自定义布局管理器。

这篇文章将帮助您处理创建自定义布局管理器类 TagLayout 的问题,该类将用于显示标签列表,如以下屏幕截图所示。

创建自定义布局管理器时,您必须执行以下步骤。

  1. ViewGroup 类扩展您的类。
  2. 您必须覆盖 onLayout() 方法。此方法用于放置子视图。
  3. 覆盖 onMeasure() 方法。 onMeasure() 方法将用于父级根据计算子视图大小来确定视图组的大小。
  4. onMeasure() 和 onLayout() 方法将包含在父布局中组织子视图的逻辑。

您可以通过调用 getMeasuredWidth() getMeasuredHeight() 方法测量每个子视图的高度和宽度。

创建自定义视图组


 public class TagLayout extends ViewGroup {
    int deviceWidth;
public TagLayout(Context context) {
    this(context, null, 0);
}

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

public TagLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
}

private void init(Context context) {
    final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
    Point deviceDisplay = new Point();
    display.getSize(deviceDisplay);
    deviceWidth = deviceDisplay.x;
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int count = getChildCount();
    int curWidth, curHeight, curLeft, curTop, maxHeight;

    //get the available size of child view
    final int childLeft = this.getPaddingLeft();
    final int childTop = this.getPaddingTop();
    final int childRight = this.getMeasuredWidth() - this.getPaddingRight();
    final int childBottom = this.getMeasuredHeight() - this.getPaddingBottom();
    final int childWidth = childRight - childLeft;
    final int childHeight = childBottom - childTop;

    maxHeight = 0;
    curLeft = childLeft;
    curTop = childTop;

    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);

        if (child.getVisibility() == GONE)
            return;

        //Get the maximum size of the child
        child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
        curWidth = child.getMeasuredWidth();
        curHeight = child.getMeasuredHeight();
        //wrap is reach to the end
        if (curLeft + curWidth >= childRight) {
            curLeft = childLeft;
            curTop += maxHeight;
            maxHeight = 0;
        }
        //do the layout
        child.layout(curLeft, curTop, curLeft + curWidth, curTop + curHeight);
        //store the max height
        if (maxHeight < curHeight)
            maxHeight = curHeight;
        curLeft += curWidth;
    }
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    // Measurement will ultimately be computing these values.
    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;
    int mLeftWidth = 0;
    int rowCount = 0;

    // Iterate through all children, measuring them and computing our dimensions
    // from their size.
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);

        if (child.getVisibility() == GONE)
            continue;

        // Measure the child.
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
        maxWidth += Math.max(maxWidth, child.getMeasuredWidth());
        mLeftWidth += child.getMeasuredWidth();

        if ((mLeftWidth / deviceWidth) > rowCount) {
            maxHeight += child.getMeasuredHeight();
            rowCount++;
        } else {
            maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
        }
        childState = combineMeasuredStates(childState, child.getMeasuredState());
    }

    // Check against our minimum height and width
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // Report our final dimensions.
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
}

}

在 Activity 布局上添加自定义 ViewGroup

现在我们已经准备好视图组,让我们将它添加到活动布局中。


 public class TagLayout extends ViewGroup {
    int deviceWidth;
public TagLayout(Context context) {
    this(context, null, 0);
}

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

public TagLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
}

private void init(Context context) {
    final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
    Point deviceDisplay = new Point();
    display.getSize(deviceDisplay);
    deviceWidth = deviceDisplay.x;
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int count = getChildCount();
    int curWidth, curHeight, curLeft, curTop, maxHeight;

    //get the available size of child view
    final int childLeft = this.getPaddingLeft();
    final int childTop = this.getPaddingTop();
    final int childRight = this.getMeasuredWidth() - this.getPaddingRight();
    final int childBottom = this.getMeasuredHeight() - this.getPaddingBottom();
    final int childWidth = childRight - childLeft;
    final int childHeight = childBottom - childTop;

    maxHeight = 0;
    curLeft = childLeft;
    curTop = childTop;

    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);

        if (child.getVisibility() == GONE)
            return;

        //Get the maximum size of the child
        child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
        curWidth = child.getMeasuredWidth();
        curHeight = child.getMeasuredHeight();
        //wrap is reach to the end
        if (curLeft + curWidth >= childRight) {
            curLeft = childLeft;
            curTop += maxHeight;
            maxHeight = 0;
        }
        //do the layout
        child.layout(curLeft, curTop, curLeft + curWidth, curTop + curHeight);
        //store the max height
        if (maxHeight < curHeight)
            maxHeight = curHeight;
        curLeft += curWidth;
    }
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    // Measurement will ultimately be computing these values.
    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;
    int mLeftWidth = 0;
    int rowCount = 0;

    // Iterate through all children, measuring them and computing our dimensions
    // from their size.
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);

        if (child.getVisibility() == GONE)
            continue;

        // Measure the child.
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
        maxWidth += Math.max(maxWidth, child.getMeasuredWidth());
        mLeftWidth += child.getMeasuredWidth();

        if ((mLeftWidth / deviceWidth) > rowCount) {
            maxHeight += child.getMeasuredHeight();
            rowCount++;
        } else {
            maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
        }
        childState = combineMeasuredStates(childState, child.getMeasuredState());
    }

    // Check against our minimum height and width
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // Report our final dimensions.
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
}

}

定义子视图布局

您可能已经从上面的布局中注意到,我们将为视图组子级创建自定义布局。在这种情况下,标签项目。


 public class TagLayout extends ViewGroup {
    int deviceWidth;
public TagLayout(Context context) {
    this(context, null, 0);
}

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

public TagLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
}

private void init(Context context) {
    final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
    Point deviceDisplay = new Point();
    display.getSize(deviceDisplay);
    deviceWidth = deviceDisplay.x;
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int count = getChildCount();
    int curWidth, curHeight, curLeft, curTop, maxHeight;

    //get the available size of child view
    final int childLeft = this.getPaddingLeft();
    final int childTop = this.getPaddingTop();
    final int childRight = this.getMeasuredWidth() - this.getPaddingRight();
    final int childBottom = this.getMeasuredHeight() - this.getPaddingBottom();
    final int childWidth = childRight - childLeft;
    final int childHeight = childBottom - childTop;

    maxHeight = 0;
    curLeft = childLeft;
    curTop = childTop;

    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);

        if (child.getVisibility() == GONE)
            return;

        //Get the maximum size of the child
        child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
        curWidth = child.getMeasuredWidth();
        curHeight = child.getMeasuredHeight();
        //wrap is reach to the end
        if (curLeft + curWidth >= childRight) {
            curLeft = childLeft;
            curTop += maxHeight;
            maxHeight = 0;
        }
        //do the layout
        child.layout(curLeft, curTop, curLeft + curWidth, curTop + curHeight);
        //store the max height
        if (maxHeight < curHeight)
            maxHeight = curHeight;
        curLeft += curWidth;
    }
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    // Measurement will ultimately be computing these values.
    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;
    int mLeftWidth = 0;
    int rowCount = 0;

    // Iterate through all children, measuring them and computing our dimensions
    // from their size.
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);

        if (child.getVisibility() == GONE)
            continue;

        // Measure the child.
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
        maxWidth += Math.max(maxWidth, child.getMeasuredWidth());
        mLeftWidth += child.getMeasuredWidth();

        if ((mLeftWidth / deviceWidth) > rowCount) {
            maxHeight += child.getMeasuredHeight();
            rowCount++;
        } else {
            maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
        }
        childState = combineMeasuredStates(childState, child.getMeasuredState());
    }

    // Check against our minimum height and width
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // Report our final dimensions.
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
}

}

将子视图添加到自定义 ViewGroup

这是我的活动中发生的事情。为了简单起见,我从 for 循环内部膨胀了 20 个子视图。您可能有一些复杂的逻辑来从其他来源获取数据。


 public class TagLayout extends ViewGroup {
    int deviceWidth;
public TagLayout(Context context) {
    this(context, null, 0);
}

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

public TagLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
}

private void init(Context context) {
    final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
    Point deviceDisplay = new Point();
    display.getSize(deviceDisplay);
    deviceWidth = deviceDisplay.x;
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int count = getChildCount();
    int curWidth, curHeight, curLeft, curTop, maxHeight;

    //get the available size of child view
    final int childLeft = this.getPaddingLeft();
    final int childTop = this.getPaddingTop();
    final int childRight = this.getMeasuredWidth() - this.getPaddingRight();
    final int childBottom = this.getMeasuredHeight() - this.getPaddingBottom();
    final int childWidth = childRight - childLeft;
    final int childHeight = childBottom - childTop;

    maxHeight = 0;
    curLeft = childLeft;
    curTop = childTop;

    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);

        if (child.getVisibility() == GONE)
            return;

        //Get the maximum size of the child
        child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
        curWidth = child.getMeasuredWidth();
        curHeight = child.getMeasuredHeight();
        //wrap is reach to the end
        if (curLeft + curWidth >= childRight) {
            curLeft = childLeft;
            curTop += maxHeight;
            maxHeight = 0;
        }
        //do the layout
        child.layout(curLeft, curTop, curLeft + curWidth, curTop + curHeight);
        //store the max height
        if (maxHeight < curHeight)
            maxHeight = curHeight;
        curLeft += curWidth;
    }
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    // Measurement will ultimately be computing these values.
    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;
    int mLeftWidth = 0;
    int rowCount = 0;

    // Iterate through all children, measuring them and computing our dimensions
    // from their size.
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);

        if (child.getVisibility() == GONE)
            continue;

        // Measure the child.
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
        maxWidth += Math.max(maxWidth, child.getMeasuredWidth());
        mLeftWidth += child.getMeasuredWidth();

        if ((mLeftWidth / deviceWidth) > rowCount) {
            maxHeight += child.getMeasuredHeight();
            rowCount++;
        } else {
            maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
        }
        childState = combineMeasuredStates(childState, child.getMeasuredState());
    }

    // Check against our minimum height and width
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // Report our final dimensions.
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
}

}


从 GitHub 下载。