重写方法介绍
onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-
遍历所有子 View :根据业务规则,为每个子 View 计算并设置
MeasureSpec
,然后调用child.measure()
。 -
合并结果:把所有子 View 的测量结果合并,计算出自身想要的宽高。
-
调用
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 必须重写。
-
-
changed 基本不用管,除非你想在首次摆放时做一次性工作。
-
l、t、r、b 是父容器给本 ViewGroup 的绝对坐标,不是子 View 的!
-
对每个孩子:
3.1 取 margin → 算 left/top;
3.2 调用
child.layout(left, top, left + width, top + height)
;3.3 千万别忘了 GONE 的孩子直接跳过。
-
结束前不要再改自己的宽高;所有子 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 添加并使用自定义属性
-
在
res/values/attrs.xml
中声明属性集合 -
在自定义 View 的构造函数里用
obtainStyledAttributes
解析 -
在布局 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 本身不画内容,留空即可
}
}
最终效果如下
