1、为什么要自定义View?
Android系统内置View无法实现我们的需求;
出于性能考虑。
2、自定义viewgroup步骤
自定义ViewGroup: 只需重写onMeasure()和onLayout()
onMeasure:
1、确定自身的大小;2、确定子view的大小
onMeasure流程:
1、ViewGroup开始测量自己的尺寸
2、为每个子View计算测量的限制信息
3、把上一步确定的限制信息,传递给每一个子view,然后子view开始measure自己的尺寸
4、获取子view测量完成后的尺寸
5、ViewGroup根据自身的情况,计算自己的尺寸
6、保存自身的尺寸
onLayout 最终是根据top left right bottom确定位置的
onMeasure()方法中常用的方法
1、getChildCount():获取子View的数量;
2、getChildAt(i):获取第i个子控件;
3、subView.getLayoutParams().width/height:设置或获取子控件的宽或高;
4、measureChild(child, widthMeasureSpec, heightMeasureSpec):测量子View的宽高;
5、child.getMeasuredHeight/width():执行完measureChild()方法后就可以通过这种方式获取子view的宽高值;
6、getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;
7、setMeasuredDimension(width, height):重新设置控件的宽高。
onLayout
1、根据规则确定子view位置
onLayout流程:
// 1、遍历子View // 2、确定自己的规则 // 3、子View的测量尺寸 // 4、left,top,right,bottom // 5、child.layout
示例一:
package com.huawei.javauidemo.widget;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import com.google.android.material.bottomappbar.BottomAppBar;
/**
* < 自定义ViewGroup> <功能详细描述>
* 下一行比上一行向右偏移100
*
* @author CYN
* @version [版本号, 2022/1/21]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
public class CustomViewGroup extends ViewGroup {
private static final int OFFSET = 100; // 表示缩进的尺寸
public CustomViewGroup(Context context) {
super(context);
}
public CustomViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1、测量自身
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 2、为每个子View计算测量的限制信息
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 3、把上一步确定的限制信息,传递给每一个子view,然后子view开始measure自己的尺寸
int childCount = getChildCount();
for(int i = 0; i < childCount; i ++) {
View child = getChildAt(i);
ViewGroup.LayoutParams lp = child.getLayoutParams();
int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);
child.measure(childWidthSpec, childHeightSpec);
int width = 0;
int height = 0;
// 4、获取子view测量完成后的尺寸
// 5、ViewGroup根据自身的情况,计算自己的尺寸
switch(widthMode) {
case MeasureSpec.EXACTLY:
width = widthSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
for(int i = 0; i < childCount; i ++) {
View child = getChildAt(i);
int widthAddOffset = i * OFFSET + child.getMeasuredWidth();
width = Math.max(width, widthAddOffset); // 取最大的宽度
}
break;
default:
break;
}
switch(heightMode) {
case MeasureSpec.EXACTLY:
height = heightSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
for(int i = 0; i < childCount; i ++) {
View child = getChildAt(i);
height += child.getMeasuredHeight();
}
break;
default:
break;
}
// 6、保存自身的尺寸
setMeasuredDimension(width, height);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 1、遍历子View
// 2、确定自己的规则
// 3、子View的测量尺寸
// 4、left,top,right,bottom
// 5、child.layout
int left = 0;
int top = 0;
int right = 0;
int bottom = 0;
int childCount = getChildCount();
for(int i = 0; i < childCount; i ++) {
View child = getChildAt(i);
left = i * OFFSET;
right = left + child.getMeasuredWidth();
bottom = top + child.getMeasuredHeight();
child.layout(left, top, right, bottom);
top += child.getMeasuredHeight();// 不考虑padding margin gravity ...
}
}
}
示例:
package com.huawei.javauidemo.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import com.huawei.javauidemo.R;
import java.util.ArrayList;
/**
* < 自定义ViewGroup> <把子控件从左到右排放,如果一行放不下,那么自动换行>
*
* 自定义布局流程
* 1、自定义属性:声明,设置,解析获取自定义值
* 在 attr.xml 声明
* 2、测量:在onMeasure MeasureSpec.AT_MOST/EXACTLY
* 自身的宽高/child的宽高
* 3、布局:在onLayout方法里面根据自己规则来确定children的位置
* 4、绘制:onDraw
* 5、处理LayoutParams
* 6、触摸反馈:滑动事件
*
* @author CYN
* @version [版本号, 2022/1/21]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
public class CustomFlowLayout extends ViewGroup {
private static final String TAG = "CustomFlowLayout";
private List<View> lineViews; // 每一行的子view
private list<List<View>> views; // 所有的行 一行一行的存储
private List<Integer> heights; // 存储每一行的高度
public CustomFlowLayout(Context context) {
super(context, null);
}
public CustomFlowLayout(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public CustomFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private void init() {
views = new ArrayList<>();
lineViews = new ArrayList<>();
heights = new ArrayList<>();
}
public static class LayoutParams extends MarginLayoutParams {
public int gravity = -1;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CustomFlowLayout_Layout);
try {
gravity = a.getInt(R.styleable.CustomFlowLayout_android_gravity, -1);
} finally {
a.recycle();
}
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
@Override
public String toString() {
return "LayoutParams{" +
"gravity=" + gravity +
", bottomMargin=" + bottomMargin +
", leftMargin=" + leftMargin +
", rightMargin=" + rightMargin +
", topMargin=" + topMargin +
", height=" + height +
", width=" + width +
"} ";
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1、测量自身
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 2、为每个子View计算测量的限制信息
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 记录当前行的宽度和高度
int lineWidth = 0; // 宽度是当前行的子view的宽度之和
int lineHeight = 0; // 高度是当前行所有子view中高度的最大值
// 记录整个流式布局的宽度和高度
int flowlayoutWidth = 0; // 所有行中宽度的最大值
int flowlayoutHeight = 0; // 所有行的高度的累加
// 初始化参数列表
init();
// 3、遍历所有的子view,对子view进行测量,分配到具体的行
int childCount = getChildCount();
for(int i = 0; i < childCount; i ++) {
View child = getChildAt(i);
// 测量子view 获取到当前子view的测量的宽度/高度
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 获取到当前子view的测量的高度/宽度
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
LayoutParams lp = child.getLayoutParams();
// 看下当前的行的剩余的宽度是否可以容纳下一个子view,如果放不下,换行
// 保存当前行的所有子view,累加行高,当前的宽度,高度 置零
if (lineWidth + childWidth > widthSize) { // 换行
views.add(lineViews);
lineViews = new ArrayList<>(); // 创建新的一行
flowlayoutWidth = Math.max(flowlayoutWidth, lineWidth);
flowlayoutHeight += lineHeight;
heights.add(lineHeight);
lineWidth = 0;
lineHeight = 0;
}
lineViews.add(child);
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
if (i == childCount - 1) { // 最后一行
flowlayoutHeight += lineHeight;
flowlayoutWidth = Math.max(flowlayoutWidth, lineWidth);
heights.add(lineHeight);
views.add(lineViews);
}
}
// FlowLayout最终宽高
setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : flowlayoutWidth,
heightMode == MeasureSpec.EXACTLY ? heightSize : flowlayoutHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int lineCount = views.size();
int currX = 0;
int currY = 0;
for(int i = 0; i < lineCount; i ++) { // 大循环,所有的子view 一行一行的布局
List<View> lineViews = views.get(i); // 取出一行
int lineHeight = heights.get(i); // 取出这一行的高度值
// 遍历当前行的子view
int size = lineViews.size();
for(int j = 0; j < size; j ++) { // 布局当前行的每一个view
View child = lineViews.get(j);
int left = currX;
int top = currY;
int right = left + child.getMeasuredWidth();
int bottom = top + child.getMeasuredHeight();
child.layout(left, top, right, bottom);
// 确定下一个view的left
currX = right;
}
currY +=lineHeight;
currX = 0;
}
}
}
3、自定义view步骤
自定义View: 只需重写onMeasure()和onDraw()