android View详解—自定义ViewGroup,流式布局

重写方法介绍

onMeasure

复制代码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  1. 遍历所有子 View :根据业务规则,为每个子 View 计算并设置 MeasureSpec,然后调用 child.measure()

  2. 合并结果:把所有子 View 的测量结果合并,计算出自身想要的宽高。

  3. 调用 setMeasuredDimension() :把最终宽高传进去,否则运行时报 IllegalStateException

onLayout

复制代码
protected void onLayout(boolean changed, int l, int t, int r, int b) {
  • 调用时机

    measure 阶段结束onMeasure 已算出每个子 View 的宽高)之后、draw 阶段之前 触发,系统会把父容器 可用矩形 (l,t,r,b) 传进来。

  • 职责

    • 仅负责摆放 (layout)子 View,不能 在这里再次测量或修改子 View 尺寸。

    • 对每个子 View 调用 child.layout(left, top, right, bottom),坐标 相对于父容器

    • 如果自身是 View(非 ViewGroup),则基本不用重写;ViewGroup 必须重写。

  1. changed 基本不用管,除非你想在首次摆放时做一次性工作。

  2. l、t、r、b 是父容器给本 ViewGroup 的绝对坐标,不是子 View 的!

  3. 对每个孩子:

    3.1 取 margin → 算 left/top;

    3.2 调用 child.layout(left, top, left + width, top + height)

    3.3 千万别忘了 GONE 的孩子直接跳过。

  4. 结束前不要再改自己的宽高;所有子 View 位置必须用绝对坐标

onDraw

复制代码
protected void onDraw(Canvas canvas) {
  • 调用时机

    layout 完成后,系统需要 真正绘制像素 时触发;只有 View 才需要重写 ,ViewGroup 默认不画任何东西(willNotDraw=true)。

    若想强制让 ViewGroup 走 onDraw,需:

    • 调用 setWillNotDraw(false)(通常写在构造函数);

    • 或者设置 android:background

  • 职责

    • 使用 Canvas 绘制自身内容(线条、文字、Bitmap、Path 等)。

    • 负责绘制子 View;子 View 由系统递归调用它们的 onDraw

    • 记得在 必要时canvas.save() / canvas.restore(),避免污染坐标系。

自定义 View 添加并使用自定义属性

  1. res/values/attrs.xml 中声明属性集合

  2. 在自定义 View 的构造函数里用 obtainStyledAttributes 解析

  3. 在布局 XML 中引用并使用这些属性

一、声明属性(res/values/attrs.xml)

复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 自定义属性集合,名字随意 -->
    <declare-styleable name="ColorCircleView">
        <!-- 颜色:支持 #RGB / #ARGB / #RRGGBB / #AARRGGBB -->
        <attr name="circleColor" format="color" />
        <!-- 半径:支持 dp/sp/px -->
        <attr name="circleRadius" format="dimension" />
        <!-- 是否显示描边 -->
        <attr name="showStroke" format="boolean" />
    </declare-styleable>
</resources>

二、自定义 View 解析属性

复制代码
public class ColorCircleView extends View {
    private int circleColor = Color.RED;
    private float circleRadius = dp2px(50);
    private boolean showStroke = false;

    private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

    public ColorCircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ColorCircleView);
        try {
            circleColor = ta.getColor(R.styleable.ColorCircleView_circleColor, Color.RED);
            circleRadius = ta.getDimension(R.styleable.ColorCircleView_circleRadius, dp2px(50));
            showStroke = ta.getBoolean(R.styleable.ColorCircleView_showStroke, false);
        } finally {
            ta.recycle();
        }
        paint.setColor(circleColor);
        paint.setStyle(showStroke ? Paint.Style.STROKE : Paint.Style.FILL);
        paint.setStrokeWidth(dp2px(4));
    }

    @Override protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(getWidth()/2f, getHeight()/2f, circleRadius, paint);
    }

    private float dp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }
}

三、在布局 XML 中使用

复制代码
<!-- 根布局必须添加自定义命名空间,通常用 app: -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.app.ColorCircleView
        android:layout_width="120dp"
        android:layout_height="120dp"
        app:circleColor="#FF5722"
        app:circleRadius="40dp"
        app:showStroke="true"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

自定义流式布局

复制代码
/**
 * 流式布局:像 HTML 的 wrap 一样自动换行。
 * 子 View 依次从左到右摆放,一行放不下就换到下一行。
 */
public class FlowLayout extends ViewGroup {
    public static final String TAG = "FlowLayout";

    /* ----------------- 构造函数 ----------------- */
    public FlowLayout(Context context) {
        super(context);
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    /* ----------------- 生命周期 ----------------- */

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        // 当前 View 被加载到窗口时调用,可在这里做初始化/注册监听
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        // 当前 View 从窗口移除时调用,可在这里释放资源/反注册
    }

    /* ----------------- 测量 ----------------- */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 1. 解析父容器给的约束
//        MeasureSpec.EXACTLY =固定死
//        MeasureSpec.AT_MOST =最多到多少(上限)
//        MeasureSpec.UNSPECIFIED =随便你(无限)
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        Log.d(TAG, "onMeasure mode widthMode " + widthMode
                + " widthSize " + widthSize
                + " heightMode " + heightMode
                + " heightSize " + heightSize
        );
        int mode = MeasureSpec.getMode(widthMeasureSpec);

        switch (mode) {
            case MeasureSpec.EXACTLY:
                // 用这个 size 就行
                Log.d(TAG, "onMeasure mode widthMode " + widthMode
                        + " widthSize " + widthSize
                        + " EXACTLY "
                );
                break;
            case MeasureSpec.AT_MOST:
                // 自己计算 desired,再取 min(desired, size)
                // 用这个 size 就行
                Log.d(TAG, "onMeasure mode widthMode " + widthMode
                        + " widthSize " + widthSize
                        + " AT_MOST "
                );
                break;
            case MeasureSpec.UNSPECIFIED:
                // 完全按自己需要
                Log.d(TAG, "onMeasure mode widthMode " + widthMode
                        + " widthSize " + widthSize
                        + " UNSPECIFIED "
                );
                break;
        }

        // 2. 根据"内容"计算我想要的宽高
        int paddingWidth = getPaddingLeft() + getPaddingRight();
        int paddingHeight = getPaddingTop() + getPaddingBottom();
        int parentHeight = 0;

        int line = 0; // 行号
        int lineMaxWidth = paddingWidth; // 当前行已用宽度
        int lineMaxHeight = 0;// 当前行最高子 View
        int maxChildCount = getChildCount() - 1;// 累计高度(不含最后一行)
        for (int i = 0; i < getChildCount(); i++) {

            View child = getChildAt(i);
            // 根据父约束 + 自身 LayoutParams 产生子 View 的 spec
            // 让子 View 自己测量
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            if (i == 0) {
                lineMaxWidth += childWidth;
                lineMaxHeight = Math.max(childHeight, lineMaxHeight);
            } else {
                lineMaxWidth += childWidth;
                if (lineMaxWidth > widthSize) {
                    line++;
                    parentHeight += lineMaxHeight;
                    Log.d(TAG, "onMeasure "
                            + " height " + childHeight
                            + " line " + line
                            + " lineMaxHeight " + lineMaxHeight
                            + " parentHeight " + parentHeight
                            + " lineMaxWidth " + lineMaxWidth
                            + " 换行 "
                            + "child " + i


                    );

                    lineMaxHeight = paddingHeight;
                    lineMaxWidth = paddingWidth;
                    lineMaxWidth += childWidth;
                }
                lineMaxHeight = Math.max(childHeight, lineMaxHeight);


            }

            if (i == maxChildCount) {
                line++;
                parentHeight += lineMaxHeight;
                Log.d(TAG, "onMeasure "
                        + " height " + childHeight
                        + " line " + line
                        + " lineMaxHeight " + lineMaxHeight
                        + " parentHeight " + parentHeight
                        + " lineMaxWidth " + lineMaxWidth
                        + " 换行 "
                        + "child " + i


                );
            }
            Log.d(TAG, "onMeasure "
                    + " height " + childHeight
                    + " line " + line
                    + " lineMaxHeight " + lineMaxHeight
                    + " parentHeight " + parentHeight
                    + " lineMaxWidth " + lineMaxWidth
                    + " all "
                    + "child " + i
            );
        }

        int parentWidth;
        if (line == 0) {
            parentWidth = lineMaxWidth;
        } else {
            parentWidth = widthSize;
        }

        Log.d(TAG, "onMeasure size " + " parentWidth " + parentWidth
                + " parentHeight " + parentHeight + " line " + line
        );

        // 3. 结合父容器约束得出最终尺寸
        int measuredWidth = resolveSize(parentWidth, widthMeasureSpec);
        int measuredHeight = resolveSize(parentHeight, heightMeasureSpec);

        // 4. 必须调用,否则抛 IllegalStateException
        setMeasuredDimension(measuredWidth, measuredHeight);

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = getPaddingLeft();
        int childTop = getPaddingTop();
        int maxHeight = 0;

        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == GONE) continue;
            int measuredWidth = child.getMeasuredWidth();
            int measuredHeight = child.getMeasuredHeight();

            int tempChildLeft = childLeft + measuredWidth;
            // 需要换行
            if (tempChildLeft > getWidth()) {
                childTop += maxHeight;
                maxHeight = 0;
                childLeft = getPaddingLeft();
            }
            maxHeight = Math.max(measuredHeight, maxHeight);
            // 摆放子 View:坐标相对于 FlowLayout
            int left = childLeft;
            int top = childTop;
            int right = left + measuredWidth;
            int bottom = top + measuredHeight;
            // 真正摆位置
            child.layout(left, top, right, bottom);
            childLeft += measuredWidth;
        }
    }

    /* ----------------- 绘制 ----------------- */

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // FlowLayout 本身不画内容,留空即可
    }

}

最终效果如下

相关推荐
胖虎14 小时前
Android入门到实战(八):从发现页到详情页——跳转、传值与RecyclerView多类型布局
android·recyclerview·多类型布局
MT_1254 小时前
大小端存储的理解与判断方法
数据结构·算法
我想_iwant9 小时前
android集成unity后动态导入 assetsBundle
android·unity·游戏引擎
耳总是一颗苹果10 小时前
数据结构---选择排序
c语言·数据结构·学习·算法·排序算法
CYH&JK10 小时前
数据结构---双向链表
数据结构·链表
CYH&JK10 小时前
数据结构之---线性表其二---单向链表
数据结构·链表
IAM四十二13 小时前
Android Json 解析你还在用 fastjson 吗?
android·json·fastjson
alexhilton13 小时前
Android ViewModel数据加载:基于Flow架构的最佳实践
android·kotlin·android jetpack