当前位置: 首页 > java >正文

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.ColorCircleViewandroid: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);}/* ----------------- 生命周期 ----------------- */@Overrideprotected void onAttachedToWindow() {super.onAttachedToWindow();// 当前 View 被加载到窗口时调用,可在这里做初始化/注册监听}@Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();// 当前 View 从窗口移除时调用,可在这里释放资源/反注册}/* ----------------- 测量 ----------------- */@Overrideprotected 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;// 当前行最高子 Viewint 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. 必须调用,否则抛 IllegalStateExceptionsetMeasuredDimension(measuredWidth, measuredHeight);}@Overrideprotected 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:坐标相对于 FlowLayoutint left = childLeft;int top = childTop;int right = left + measuredWidth;int bottom = top + measuredHeight;// 真正摆位置child.layout(left, top, right, bottom);childLeft += measuredWidth;}}/* ----------------- 绘制 ----------------- */@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);// FlowLayout 本身不画内容,留空即可}}

最终效果如下

http://www.xdnf.cn/news/20066.html

相关文章:

  • 亚洲数字能源独角兽的 “安全密码”:Parasoft为星星充电筑牢软件防线
  • MongoDB 高可用部署:Replica Set 搭建与故障转移测试
  • SpringCloud微服务基于nacos注册中心的服务发现模式及OpenFeign的使用
  • Redis在商城开发中起到什么作用?
  • 漏洞修复 Nginx TLSSSL 弱密码套件
  • 2025国赛C题保姆级教程思路分析 NIPT 的时点选择与胎儿的异常判定
  • 【完整源码+数据集+部署教程】陶瓷物品实例分割系统源码和数据集:改进yolo11-LVMB
  • 第22节:性能监控与内存管理——构建高性能3D应用
  • 3ds Max流体模拟终极指南:打造逼真液体效果,从瀑布到杯中溢出的饮料!
  • 240. 搜索二维矩阵 II
  • 2025年含金量高的经济学专业证书工科!【纯干货分享】
  • 文件系统-哈希结构文件
  • 食物分类案例优化 调整学习率和迁移学习
  • Paraverse平行云实时云渲染助力第82届威尼斯电影节XR沉浸式体验
  • 火山引擎数据智能体DataAgent总结分享
  • 小型企业MES软件开发的核心要点
  • 遥感语义分割辅导
  • PWM正相输出和PWM反相输出的各是怎样的工作原理
  • 别再和正则表达式死磕了!这套AI工具集让你的开发效率翻倍⚙️[特殊字符]
  • OPENCV复习第二期
  • 【ffmepg+ AI 】从mp3歌曲提取伴奏(纯音乐)
  • SQL常见索引失效导致慢查询情况
  • mysql集群部署(Mysql Group Replication)
  • 如何将数据从 Infinix 转移到 Infinix ?
  • 生活在数字世界:一份人人都能看懂的网络安全生存指南
  • @Percona XtraBackup 进行 MySQL 备份恢复
  • Day35 TCP实时聊天程序实现(多线程)
  • 3 步搞定顶刊科研插图!用 GPT-5 反推提示词,Nano Banana 模型一键出图,附实操演示
  • 国内外开源大模型 LLM整理
  • 2025 年高教社杯全国大学生数学建模竞赛E 题 AI 辅助智能体测完整成品 思路 模型 代码 结果分享!全网首发高质量!!!