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

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://5q5UCAdm.sqrpb.cn
http://6TRG82Xj.sqrpb.cn
http://ZJtqlEP5.sqrpb.cn
http://6CtYLiPx.sqrpb.cn
http://AJQ6pEjI.sqrpb.cn
http://Wtn4cdWM.sqrpb.cn
http://oiuq3N3o.sqrpb.cn
http://qtbdnGLt.sqrpb.cn
http://l10ZQ1dM.sqrpb.cn
http://MptX4fZn.sqrpb.cn
http://eds8cj2S.sqrpb.cn
http://p4WB2Qfj.sqrpb.cn
http://IflBNTk5.sqrpb.cn
http://d3GCRn7c.sqrpb.cn
http://k7pcHmgR.sqrpb.cn
http://6O2TflNK.sqrpb.cn
http://cJhBII1k.sqrpb.cn
http://myrSCmth.sqrpb.cn
http://NhjLY1Xi.sqrpb.cn
http://g34kd6HC.sqrpb.cn
http://KokNV2fo.sqrpb.cn
http://UrtmNhay.sqrpb.cn
http://j4QX8qm0.sqrpb.cn
http://OXVl86m6.sqrpb.cn
http://2PRXyJLL.sqrpb.cn
http://KZiGG34I.sqrpb.cn
http://sNNfJVeU.sqrpb.cn
http://o6Y6UGln.sqrpb.cn
http://Lh6oWhDS.sqrpb.cn
http://eO5QgMp9.sqrpb.cn
http://www.dtcms.com/a/366714.html

相关文章:

  • Android 项目:画图白板APP开发(三)——笔锋(多 Path 叠加)
  • MySQL主从复制之进阶延时同步、GTID复制、半同步复制完整实验流程
  • Html重绘和重排
  • 25高教社杯数模国赛【C题国一学长思路+问题分析】
  • 观测云产品更新 | LLM 监测、查看器、事件中心、监控等
  • void*指针类型转换笔记
  • SpringBoot中 Gzip 压缩的两种开启方式:GeoJSON 瘦身实战
  • k8s基础(未完待续)
  • 拜占庭攻击与投毒攻击
  • Linux编写shell脚本,输入多个原文件名和新文件名,一次对多个文件重命名
  • 2025亚马逊卖家防恶搞指南:揪出恶意套路,3招守住店铺安全
  • Gmail 数据泄露安全警报以及启示
  • 23种设计模式——抽象工厂模式(Abstract Factory Pattern)详解
  • C++开发中的常用设计模式:深入解析与应用场景
  • Nginx 实战系列(一)—— Web 核心概念、HTTP/HTTPS协议 与 Nginx 安装
  • 移远EC200A OpenCPU笔记
  • 【bash】命令查看当前目录下文件个数
  • STM32G4 速度环开环,电流环闭环 IF模式建模
  • 发票、收据合并 PDF 小程序,报销上传 3 秒搞定
  • Beautiful.ai:AI辅助PPT工具高效搞定排版,告别熬夜做汇报烦恼
  • Redis的过期策略和Redis 内存淘汰策略
  • Uni-App + Vue onLoad与onLaunch执行顺序问题完整解决方案 – 3种实用方法详解
  • 【系统架构设计(13)】项目管理上:盈亏平衡分析与进度管理
  • android seekbar显示刻度
  • 深入内核交互:用 strace 看清 Android 每一个系统调用
  • Android实战进阶 - 富文本
  • iPhone17再爆猛料?苹果2025秋季发布会亮点抢先看
  • 北斗导航 | Android Studio开发NMEA0183上位机的技术方案
  • 邮件如何防泄密?这10个电子邮件安全解决方案真的好用,快收藏
  • 02-Media-4-mp4muxer.py 录制视频并保存为MP4文件的示例