View和ViewGroup的自定义流程基本相同,区别是View只需要绘制自己,而ViewGroup不仅要绘制自己,还要绘制其子View,因此ViewGroup的自定义绘制是重点。
View.java部分源码:
package android.view;
@UiThread
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
// 省略代码
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(@NonNull Canvas canvas) {// 该方法为空实现
}
public void layout(int l, int t, int r, int b) {
// 省略代码
}
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {// 该方法为空实现
}
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 省略代码
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
public static int resolveSize(int size, int measureSpec) {
return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
// 省略代码
}
ViewGroup.java部分源码,它没有onDraw()、measure()、onMeasure()这个三个方法:
package android.view;
@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
// 省略代码
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
// 省略代码
}
一套完整的自定义View涉及测量、布局和绘制这三个步骤,分别对应onMeasure()、onLayout()、onDraw()。
它是View.java中的方法,用于测量当前控件的大小,为正式布局提供建议(是否使用要看onLayout()的逻辑)。
它是View树自顶向下的遍历,每个View在循环过程中将尺寸细节往下传递,当测量过程完成之后,所有的View都保存了自己的尺寸。
测量完成后,要通过setMeasuredDimension(width, height)设置给系统。
setMeasuredDimension()提供的测量结果只是为布局提供建议的,最终的取用与否要看layout()。
onLayout():使用layout()对所有子控件布局。它也是自顶向下的,每个父View负责通过计算好的尺寸放置它的子View。
getMeasuredWidth()与getWidth()的区别:
1、getMeasuredWidth()在measure()过程结束后就可以获取到宽度值,getWidth()要在layout()过程结束后才能获取到宽度值。
2、getMeasuredWidth()中的值是通过setMeasuredDimension()来进行设置的,而getWidth()中的值是通过layout()来设置的。
如果在调用layout()时传入的宽度值不与getMeasuredWidth()的返回值相同,那么getMeasuredWidth()与getWidth()的返回值就不一样了,否则它们的值可能是一样的。
onDraw():根据布局的位置绘图
如果要自定义ViewGroup支持子控件的layout_margin参数,则自定义的ViewGroup类必须重写generateLayoutParams(),且在函数中返回一个ViewGroup.MarginLayoutParams派生类对象。
注意,View.java中没有generateLayoutParams()、MarginLayoutParams。
ViewGroup部分源码:
package android.view;
@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
// 省略代码
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return p;
}
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
protected void measureChildWithMargins(
View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed
) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(
parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed,
lp.width
);
final int childHeightMeasureSpec = getChildMeasureSpec(
parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed,
lp.height
);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
public static class LayoutParams {
// 省略代码
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
setBaseAttributes(
a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height
);
a.recycle();
}
public LayoutParams(int width, int height) {
this.width = width;
this.height = height;
}
public LayoutParams(LayoutParams source) {
this.width = source.width;
this.height = source.height;
}
/**
* Used internally by MarginLayoutParams.
* @hide
*/
@UnsupportedAppUsage
LayoutParams() { //空参构造
}
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
width = a.getLayoutDimension(widthAttr, "layout_width");
height = a.getLayoutDimension(heightAttr, "layout_height");
}
// 省略代码
}
public static class MarginLayoutParams extends ViewGroup.LayoutParams {
// 省略代码
// 其中一个构造方法
public MarginLayoutParams(Context c, AttributeSet attrs) {
super();
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
setBaseAttributes(
a,
R.styleable.ViewGroup_MarginLayout_layout_width,
R.styleable.ViewGroup_MarginLayout_layout_height
);
int margin = a.getDimensionPixelSize(com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
if (margin >= 0) {
leftMargin = margin;
topMargin = margin;
rightMargin= margin;
bottomMargin = margin;
} else {
int horizontalMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1);
int verticalMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginVertical, -1);
if (horizontalMargin >= 0) {
leftMargin = horizontalMargin;
rightMargin = horizontalMargin;
} else {
leftMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
UNDEFINED_MARGIN);
if (leftMargin == UNDEFINED_MARGIN) {
mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK;
leftMargin = DEFAULT_MARGIN_RESOLVED;
}
rightMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginRight,
UNDEFINED_MARGIN);
if (rightMargin == UNDEFINED_MARGIN) {
mMarginFlags |= RIGHT_MARGIN_UNDEFINED_MASK;
rightMargin = DEFAULT_MARGIN_RESOLVED;
}
}
startMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginStart,
DEFAULT_MARGIN_RELATIVE);
endMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
DEFAULT_MARGIN_RELATIVE);
if (verticalMargin >= 0) {
topMargin = verticalMargin;
bottomMargin = verticalMargin;
} else {
topMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginTop,
DEFAULT_MARGIN_RESOLVED);
bottomMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginBottom,
DEFAULT_MARGIN_RESOLVED);
}
if (isMarginRelative()) {
mMarginFlags |= NEED_RESOLUTION_MASK;
}
}
final boolean hasRtlSupport = c.getApplicationInfo().hasRtlSupport();
final int targetSdkVersion = c.getApplicationInfo().targetSdkVersion;
if (targetSdkVersion < JELLY_BEAN_MR1 || !hasRtlSupport) {
mMarginFlags |= RTL_COMPATIBILITY_MODE_MASK;
}
// Layout direction is LTR by default
mMarginFlags |= LAYOUT_DIRECTION_LTR;
a.recycle();
}
// 省略代码
}
// 省略代码
}
重写方法:
/**
* 父容器生成 子view 的布局LayoutParams;
* 一句话道出LayoutParams的本质:LayoutParams是Layout提供给其中的Children使用的。
* 如果要自定义ViewGroup支持子控件的layout_margin参数,则自定义的ViewGroup类必须重载generateLayoutParams()函数,
* 并且在该函数中返回一个ViewGroup.MarginLayoutParams派生类对象,这样才能使用margin参数。
*/
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
注意,如果在onLayout()中根据margin来布局,那么在onMeasure()中计算container大小时,要加上layout_margin参数,否则会导致container太小而控件显示不全的问题。即在onMeasure()和onLayout()中都需要考虑margin。
为什么要重写generateLayoutParams()呢?
因为默认的generateLayoutParams()只会提取layout_width和layout_height的值,只有MarginLayoutParams()才具有提取margin值的功能,具体可参见其源码。
继承已有的View,如ImageView,简单改写它们的尺寸:重写onMeasure()
1、重写onMeasure()
2、用getMeasuredWidth()和getMeasuredHeight()获取到测量出的尺寸
3、计算出最终需要的尺寸
4、用setMeasuredDimension(width, height)保存结果
Talk is cheap. Show me the code.
package com.example.customview.layoutsize
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
import kotlin.math.min
/**
* 直接继承自一个ImageView
*/
class SquareImageView(context: Context, attrs: AttributeSet?) : AppCompatImageView(context, attrs) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val size = min(measuredWidth, measuredHeight)
setMeasuredDimension(size, size)
}
}
布局中直接引用
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.customview.layoutsize.SquareImageView
android:src="@drawable/tech"
android:layout_width="200dp"
android:layout_height="200dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
androidx.constraintlayout.widget.ConstraintLayout>
直接继承View,对自定义View完全进行自定义尺寸计算:重写onMeasure()
1、重写onMeasure()
2、计算出自己的尺寸
3、用resolveSize()或者resolveSizeAndState()修正结果
4、使用setMeasuredDimension(width, height)保存结果
Talk is cheap. Show me the code.
package com.example.customview.layoutsize
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import com.example.customview.func.dp
/**
* 直接继承自View
*/
private val RADIUS = 100.dp
private val PADDING = 100.dp
class CircleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val size = ((PADDING + RADIUS) * 2).toInt()
val width = resolveSize(size, widthMeasureSpec)
val height = resolveSize(size, heightMeasureSpec)
setMeasuredDimension(width, height)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(PADDING + RADIUS, PADDING + RADIUS, RADIUS, paint)
}
}
布局引用
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.customview.layoutsize.CircleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
androidx.constraintlayout.widget.ConstraintLayout>
直接继承ViewGroup,自定义Layout:重写onMeasure()和onLayout()
1、重写onMeasure()
1.1 遍历每个子View,测量子View
1.1.1 测量完成后,得出子View的实际位置和尺寸,并暂时保存
1.1.2 有些子View可能需要重新测量
1.2 测量出所有子View的位置和尺寸后,计算自己的尺寸,并用setMeasuredDimension(width, height)保存
2、重写onLayout()
2.1 遍历每个子View,调用它们的layout()方法将位置和尺寸传给它们
Talk is cheap. Show me the code.
先画一个ColoredTextView
package com.example.customview.layoutlayout
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import com.example.customview.func.dp
import java.util.Random
private val COLORS = intArrayOf(
Color.parseColor("#E91E63"),
Color.parseColor("#673AB7"),
Color.parseColor("#3F51B5"),
Color.parseColor("#2196F3"),
Color.parseColor("#009688"),
Color.parseColor("#FF9800"),
Color.parseColor("#FF5722"),
Color.parseColor("#795548")
)
/**
* 给定不同大小的字体
*/
//private val TEXT_SIZES = intArrayOf(22, 22, 22)
private val TEXT_SIZES = intArrayOf(16, 22, 28)
private val CORNER_RADIUS = 4.dp
private val X_PADDING = 16.dp.toInt()
private val Y_PADDING = 8.dp.toInt()
class ColoredTextView(context: Context, attrs: AttributeSet?) : AppCompatTextView(context, attrs) {
private var paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val random = Random()
init {
setTextColor(Color.WHITE)
textSize = TEXT_SIZES[random.nextInt(3)].toFloat()
paint.color = COLORS[random.nextInt(COLORS.size)]
setPadding(X_PADDING, Y_PADDING, X_PADDING, Y_PADDING)
}
override fun onDraw(canvas: Canvas) {
canvas.drawRoundRect(
0f,
0f,
width.toFloat(),
height.toFloat(),
CORNER_RADIUS,
CORNER_RADIUS,
paint
)
super.onDraw(canvas)
}
}
自定义TagLayout,用于展示多个ColoredTextView
package com.example.customview.layoutlayout
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.ViewGroup
import androidx.core.view.children
import kotlin.math.max
/**
* 注意:本例未处理TextView的宽、高的margin,因此效果图中的TextView会挤在一起
*/
class TagLayout(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
/**
* 子View集合
*/
private val childrenBounds = mutableListOf<Rect>()
/**
* 1、重写onMeasure()
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 非当前行的历史最大行宽
var widthUsed = 0
// 非当前行的累积行高(每行不断累加,不重置)
var heightUsed = 0
// 当前行行宽(当前行已绘制的子View累加,换行时重置)
var lineWidthUsed = 0
// 当前行最大行高(换行时重置)
var lineMaxHeight = 0
// 当前期望行宽的尺寸
val specWidthSize = MeasureSpec.getSize(widthMeasureSpec)
// 当前期望行宽的模式
val specWidthMode = MeasureSpec.getMode(widthMeasureSpec)
// 1.1 遍历每个子View,测量子View
for ((index, child) in children.withIndex()) {
// 1.1.1 测量单行子View的实际位置和尺寸
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed)
// 处理换行
if (specWidthMode != MeasureSpec.UNSPECIFIED && lineWidthUsed + child.measuredWidth > specWidthSize) {
lineWidthUsed = 0
heightUsed += lineMaxHeight
lineMaxHeight = 0
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed)
}
/*
第一次childrenBounds为空,index = childrenBounds.size,需要添加。
再往后index < childrenBounds.size,故不会出现大于的情况。
即该条件只会执行一次,因此可以写在for循环中。
*/
if (index >= childrenBounds.size) {
childrenBounds.add(Rect())
}
// 1.1.1 测量完成后,暂时保存子View的实际位置和尺寸
val childBounds = childrenBounds[index]
childBounds.set(
lineWidthUsed,
heightUsed,
lineWidthUsed + child.measuredWidth,
heightUsed + child.measuredHeight
)
lineWidthUsed += child.measuredWidth
widthUsed = max(widthUsed, lineWidthUsed)
lineMaxHeight = max(lineMaxHeight, child.measuredHeight)
}
// 1.2 测量出所有子View的位置和尺寸后,计算自己的尺寸,并用setMeasuredDimension(width, height)保存
val selfWidth = widthUsed
val selfHeight = heightUsed + lineMaxHeight
setMeasuredDimension(selfWidth, selfHeight)
}
/**
* 2、重写onLayout()
*/
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
// 2.1 遍历每个子View,调用它们的layout()方法将位置和尺寸传给它们
for ((index, child) in children.withIndex()) {
val childBounds = childrenBounds[index]
child.layout(childBounds.left, childBounds.top, childBounds.right, childBounds.bottom)
}
}
/**
* measureChildWithMargins()的源码中有以下代码进行强转,
* final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
* 重写generateLayoutParams()后可避免强转时报错
*/
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
}
布局文件
<com.example.customview.layoutlayout.TagLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="北京市" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="天津市" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="上海市" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="重庆市" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="河北省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="山西省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="辽宁省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="吉林省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="黑龙江省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="江苏省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="浙江省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="安徽省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="福建省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="江西省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="山东省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="河南省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="湖北省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="湖南省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="广东省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="海南省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="四川省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="贵州省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="云南省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="陕西省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="甘肃省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="青海省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="台湾省" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="内蒙古自治区" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="广西壮族自治区" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="西藏自治区" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="宁夏回族自治区" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="新疆维吾尔自治区" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="香港特别行政区" />
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="澳门特别行政区" />
com.example.customview.layoutlayout.TagLayout>
给每个ColoredTextView添加一个margin属性,如
<com.example.customview.layoutlayout.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:text="北京市" />
设置margin后的代码,改动较小,可对比查看
package com.example.customview.layoutlayout
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.ViewGroup
import androidx.core.view.children
import kotlin.math.max
/**
* 设置了TextView的margin,仅修改了onMeasure()中的部分参数,其余代码未改动
*/
class TagLayout(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
/**
* 子View集合
*/
private val childrenBounds = mutableListOf<Rect>()
/**
* 1、重写onMeasure()
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 非当前行的历史最大行宽
var widthUsed = 0
// 非当前行的累积行高(每行不断累加,不重置)
var heightUsed = 0
// 当前行行宽(当前行已绘制的子View累加,换行时重置)
var lineWidthUsed = 0
// 当前行最大行高(换行时重置)
var lineMaxHeight = 0
// 当前期望行宽的尺寸
val specWidthSize = MeasureSpec.getSize(widthMeasureSpec)
// 当前期望行宽的模式
val specWidthMode = MeasureSpec.getMode(widthMeasureSpec)
// 1.1 遍历每个子View,测量子View
for ((index, child) in children.withIndex()) {
// 1.1.1 测量单行子View的实际位置和尺寸
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed)
// 设置margin
var mlp: MarginLayoutParams = child.layoutParams as MarginLayoutParams
var childWidth = child.measuredWidth + mlp.leftMargin + mlp.rightMargin
var childHeight = child.measuredHeight + mlp.topMargin + mlp.bottomMargin
// 处理换行
if (specWidthMode != MeasureSpec.UNSPECIFIED && lineWidthUsed + childWidth > specWidthSize) {
lineWidthUsed = 0
heightUsed += lineMaxHeight
lineMaxHeight = 0
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed)
}
/*
第一次childrenBounds为空,index = childrenBounds.size,需要添加。
再往后index < childrenBounds.size,故不会出现大于的情况。
即该条件只会执行一次,因此可以写在for循环中。
*/
if (index >= childrenBounds.size) {
childrenBounds.add(Rect())
}
// 1.1.1 测量完成后,暂时保存子View的实际位置和尺寸
val childBounds = childrenBounds[index]
childBounds.set(
lineWidthUsed + mlp.leftMargin,
heightUsed + mlp.topMargin,
lineWidthUsed + childWidth,
heightUsed + childHeight
)
lineWidthUsed += childWidth
widthUsed = max(widthUsed, lineWidthUsed)
lineMaxHeight = max(lineMaxHeight, childHeight)
}
// 1.2 测量出所有子View的位置和尺寸后,计算自己的尺寸,并用setMeasuredDimension(width, height)保存
val selfWidth = widthUsed
val selfHeight = heightUsed + lineMaxHeight
setMeasuredDimension(selfWidth, selfHeight)
}
/**
* 2、重写onLayout()
*/
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
// 2.1 遍历每个子View,调用它们的layout()方法将位置和尺寸传给它们
for ((index, child) in children.withIndex()) {
val childBounds = childrenBounds[index]
child.layout(childBounds.left, childBounds.top, childBounds.right, childBounds.bottom)
}
}
/**
* measureChildWithMargins()的源码中有以下代码进行强转,
* final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
* 重写generateLayoutParams()后可避免强转时报错
*/
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
}
添加margin后的效果图:
添加margin,且统一字体大小:
参考文献
[1] 扔物线官网
[2] 启舰.Android自定义控件开发入门与实战[M].北京:电子工业出版社,2018