Recently, I was looking at the customization of viwe and saw a lot of examples explaining FlowLayout custom ViewGroup. Then I reviewed the example in "Introduction and Practical Combat of Android Custom Control Development" and wanted to find a kotlin one to take a look at, but there were no No, so I wrote this blog to record it
There should be a lot of articles about customizing views, so I will simply record the key points.
One is written in java and the other is written in kotlin. I am currently learning kotlin.
renderings
Calculation relationship of coordinate position
1. The drawing of View generally follows
构造函数->onMeasure()(测量View的大小)-onSizeChanged()()->onLayout()(确定子View布局)->onDraw()(开始绘制内容)->invalidate()(重绘刷新)
view主要实现:onMeasure() + onDraw()
vierGroup主要实现:onMeasure()+onLayout()
(The picture is a screenshot from: https://blog.csdn.net/heng615975867/article/details/80379393)
2. The key point is that the onMeasure() method calculates the width and height of the container, the onLayout() method calculates the position (left, top, right, bottom) of each childViwe, and calls the view.onLayout() method to draw
step one
To extract the margin value, you must first rewrite the generateLayoutParams() method.
To extract the margin value, you must first rewrite the generateLayoutParams() method.
To extract the margin value, you must first rewrite the generateLayoutParams() method.
/**
* 重写generateLayoutParams方法 为了提取Margin
*
* @param p
* @return
*/
@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);
}
Step 2: The onMeasure() method calculates the width and height of the container and sets it to setMeasuredDimension()
To calculate the width and height of the container, you need to traverse all childViews to determine the maximum width and height. At the same time, you need to judge based on measureWidthMode. The
idea is to record the width and height of the current row, and then calculate the situation of each childView in a for loop, and take out the final maximum width and height. height, and then focus on calling the setMeasuredDimension() method
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int lineWidth = 0;//记录每一行的宽度
int linHeight = 0;//记录每一行的高度
int totalWidth = 0;//记录整体的宽度
int totalHeight = 0;//记录整体的高度
int count = getChildCount();
for (int i = 0; i < count; i++) {
View view = getChildAt(i);
//一定要先调用measureChild(),调用getMeasuredWidth() 才生效
measureChild(view, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
int viewWidth = view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int viewHeight = view.getHeight() + lp.topMargin + lp.bottomMargin;
if (lineWidth + viewWidth > measureWidth) { //当前的行宽+child的宽大于最大的测量宽度
//换行的情况
totalWidth = Math.max(lineWidth, viewWidth);
totalHeight += linHeight;
lineWidth = viewWidth;
linHeight = viewHeight;
} else {
//不换行的情况
linHeight = Math.max(linHeight, viewHeight);
lineWidth += viewWidth;
}
if (i == count - 1) {
totalHeight += linHeight;
totalWidth = Math.max(totalWidth, lineWidth);
}
}
//所以的工作都是为了确定容器的宽高
setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : totalWidth, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : totalHeight);
Step 3: Record and save the position of each childView in the onLayout() method, and then call view.layout(l,t,r,b) according to the coordinates to draw the view
int count = getChildCount();
int lineWidth = 0;//累加当前行的行宽
int linwHeight = 0;//累加当前的行高
int top = 0, left = 0;//当前空间的top坐标和left坐标
for (int i = 0; i < count; i++) {
View view = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
int viewWidth = view.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
int viewHeight = view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (viewWidth + lineWidth > getMeasuredWidth()) {
//如果换行
top += linwHeight;
left = 0;
linwHeight = viewHeight;
lineWidth = viewWidth;
} else {
linwHeight = Math.max(linwHeight, viewHeight);
lineWidth += viewWidth;
}
//计算view的left top right bottom
int lc = left + lp.leftMargin;
int tc = top + lp.topMargin;
int rc = lc + view.getMeasuredWidth();
int bc = tc + view.getMeasuredHeight();
view.layout(lc, tc, rc, bc);
//将left置为下一个子控件的起点
left += viewWidth;
use
kotlin version class FlowLayoutKotlin
package com.example.flowlayoutdemo
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
class FlowLayoutKotlin : ViewGroup {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, def: Int) : super(context, attrs, def)
override fun generateLayoutParams(p: LayoutParams?): LayoutParams {
return MarginLayoutParams(p)
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
override fun generateDefaultLayoutParams(): LayoutParams {
return MarginLayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val measureWidth = MeasureSpec.getSize(widthMeasureSpec)
val measureHeight = MeasureSpec.getSize(heightMeasureSpec)
val measureWidthMode = MeasureSpec.getMode(widthMeasureSpec)
val measureHeightMode = MeasureSpec.getMode(heightMeasureSpec)
var lineWidth = 0 //记录每一行的宽度
var linHeight = 0 //记录每一行的高度
var totalWidth = 0 //记录整体的宽度
var totalHeight = 0 //记录整体的高度
val count = childCount
for (i in 0 until count) {
val view = getChildAt(i)
//一定要先调用measureChild(),调用getMeasuredWidth() 才生效
measureChild(view, widthMeasureSpec, heightMeasureSpec)
val lp = view.layoutParams as MarginLayoutParams
val viewWidth = view.measuredWidth + lp.leftMargin + lp.rightMargin
val viewHeight = view.height + lp.topMargin + lp.bottomMargin
if (lineWidth + viewWidth > measureWidth) { //当前的行宽+child的宽大于最大的测量宽度
//换行的情况
totalWidth = Math.max(lineWidth, viewWidth)
totalHeight += linHeight
lineWidth = viewWidth
linHeight = viewHeight
} else {
//不换行的情况
linHeight = Math.max(linHeight, viewHeight)
lineWidth += viewWidth
}
if (i == count - 1) {
totalHeight += linHeight
totalWidth = Math.max(totalWidth, lineWidth)
}
}
//所以的工作都是为了确定容器的宽高
//所以的工作都是为了确定容器的宽高
setMeasuredDimension(
if (measureWidthMode == MeasureSpec.EXACTLY) measureWidth else totalWidth,
if (measureHeightMode == MeasureSpec.EXACTLY) measureHeight else totalHeight
)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
//四个参数 当前行的宽高 容器的累计宽高 即宽度是取能获取的最大值,高度方向是累加的值
val count = childCount
var lineWidth = 0 //累加当前行的行宽
var linwHeight = 0 //累加当前的行高
var top = 0
var left = 0 //当前空间的top坐标和left坐标
for (i in 0 until count) {
val view = getChildAt(i)
val lp = view.layoutParams as MarginLayoutParams
val viewWidth = view.measuredWidth + lp.rightMargin + lp.leftMargin
val viewHeight = view.measuredHeight + lp.topMargin + lp.bottomMargin
if (viewWidth + lineWidth > measuredWidth) {
//如果换行
top += linwHeight
left = 0
linwHeight = viewHeight
lineWidth = viewWidth
} else {
linwHeight = Math.max(linwHeight, viewHeight)
lineWidth += viewWidth
}
//计算view的left top right bottom
val lc = left + lp.leftMargin
val tc = top + lp.topMargin
val rc = lc + view.measuredWidth
val bc = tc + view.measuredHeight
view.layout(lc, tc, rc, bc)
//将left置为下一个子控件的起点
left += viewWidth
}
}
}
The java version of the class FlowLayoutJava
package com.example.flowlayoutdemo;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
public class FlowLayoutJava extends ViewGroup {
public FlowLayoutJava(Context context) {
super(context);
}
public FlowLayoutJava(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlowLayoutJava(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 重写generateLayoutParams方法 为了提取Margin
*
* @param p
* @return
*/
@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);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int lineWidth = 0;//记录每一行的宽度
int linHeight = 0;//记录每一行的高度
int totalWidth = 0;//记录整体的宽度
int totalHeight = 0;//记录整体的高度
int count = getChildCount();
for (int i = 0; i < count; i++) {
View view = getChildAt(i);
//一定要先调用measureChild(),调用getMeasuredWidth() 才生效
measureChild(view, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
int viewWidth = view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int viewHeight = view.getHeight() + lp.topMargin + lp.bottomMargin;
if (lineWidth + viewWidth > measureWidth) { //当前的行宽+child的宽大于最大的测量宽度
//换行的情况
totalWidth = Math.max(lineWidth, viewWidth);
totalHeight += linHeight;
lineWidth = viewWidth;
linHeight = viewHeight;
} else {
//不换行的情况
linHeight = Math.max(linHeight, viewHeight);
lineWidth += viewWidth;
}
if (i == count - 1) {
totalHeight += linHeight;
totalWidth = Math.max(totalWidth, lineWidth);
}
}
//所以的工作都是为了确定容器的宽高
setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : totalWidth, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : totalHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int lineWidth = 0;//累加当前行的行宽
int linwHeight = 0;//累加当前的行高
int top = 0, left = 0;//当前空间的top坐标和left坐标
for (int i = 0; i < count; i++) {
View view = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
int viewWidth = view.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
int viewHeight = view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (viewWidth + lineWidth > getMeasuredWidth()) {
//如果换行
top += linwHeight;
left = 0;
linwHeight = viewHeight;
lineWidth = viewWidth;
} else {
linwHeight = Math.max(linwHeight, viewHeight);
lineWidth += viewWidth;
}
//计算view的left top right bottom
int lc = left + lp.leftMargin;
int tc = top + lp.topMargin;
int rc = lc + view.getMeasuredWidth();
int bc = tc + view.getMeasuredHeight();
view.layout(lc, tc, rc, bc);
//将left置为下一个子控件的起点
left += viewWidth;
}
}
}
1.Create textview_shape
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
android:shape="rectangle">
<corners android:radius="3dp" />
<stroke android:color="#cc0033" android:width="1dp"/>
<padding android:top="2dp" android:bottom="2dp" android:left="2dp" android:right="2dp" />
</shape>
2. Referenced in layout
<?xml version="1.0" encoding="utf-8"?>
<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.flowlayoutdemo.FlowLayoutJava
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:background="@drawable/textview_shape">
<TextView
style="@style/text_flow_style"
android:background="@drawable/textview_shape"
android:text="白茶清欢无别事"
android:textSize="20sp" />
<TextView
style="@style/text_flow_style"
android:background="@drawable/textview_shape"
android:text="我在等风也等你"
android:textSize="20sp" />
<TextView
style="@style/text_flow_style"
android:background="@drawable/textview_shape"
android:text="山高决定人为峰"
android:textSize="20sp" />
<TextView
style="@style/text_flow_style"
android:background="@drawable/textview_shape"
android:text="海阔无涯天作岸"
android:textSize="20sp" />
<TextView
style="@style/text_flow_style"
android:background="@drawable/textview_shape"
android:text="不应该"
android:textSize="20sp" />
<TextView
style="@style/text_flow_style"
android:background="@drawable/textview_shape"
android:text="在"
android:textSize="20sp" />
<TextView
style="@style/text_flow_style"
android:background="@drawable/textview_shape"
android:text="自怜中沉沦"
android:textColor="@android:color/black"
android:textSize="20sp" />
<TextView
style="@style/text_flow_style"
android:background="@drawable/textview_shape"
android:text="每一次困难"
android:textSize="20sp" />
<TextView
style="@style/text_flow_style"
android:background="@drawable/textview_shape"
android:text="都看成是可以改变的机会"
android:textSize="20sp" />
</com.example.flowlayoutdemo.FlowLayoutJava>
</androidx.constraintlayout.widget.ConstraintLayout>