早些天,我们主要是针对View自定义做了简单实践(主要是测量、绘制,单个控件没有特别的onLayout的处理);接着我们看了下自定义ViewGroup的官方案例,又了解了更多的东西,由于官方案例的很多测量方法,状态,以及一些个自定义布局属性类等,所以看起来相对费劲,不过作为初识,了解流程可能更重要,所以基本上算是短暂的可以了;但是官方的有点类似帧布局那样的方式,小白我想从一个简单的垂直布局开始,So然后到现在我们可以尝试参考官方定义一个垂直布局的容器控件!!!
那就开始吧?
一、创建基础自定义ViewGroup类
1. 自定义CustomViewGroup继承ViewGroup
2. 实现多个构造函数
3. 重写onMeasure和onLayout
看布局文件:custom_viewgroup_me.xml
看基础类: CustomViewGroup.java
package me.heyclock.hl.customcopy;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.ViewGroup;
/*
*@Description: 自定义ViewGroup + 纵向垂直布局 + 单列
*@Author: hl
*@Time: 2018/10/25 10:18
*/
public class CustomViewGroup extends ViewGroup {
private Context context;///< 上下文
/** 计算子控件的布局位置. */
private final Rect mTmpContainerRect = new Rect();
private final Rect mTmpChildRect = new Rect();
public CustomViewGroup(Context context) {
this(context, null);
}
public CustomViewGroup(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, 0, 0);
}
public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.context = context;
}
/**
* 测量容器的宽高 = 所有子控件的尺寸 + 容器本身的尺寸 -->综合考虑
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
此时不做测量的话,由于我们设置的是wrap_content*2,所以我们的就是match_parent效果,还记得我们之前自定义View的时候说过这个原理的吧 Android-自定义View-onMeasure方法 --- 一片赤红
二、开始进行容器(自定义ViewGroup)的测量
1.获取子控件的数量,进行子空间的测量
2.同时获取子控件中宽度的最大值,以及高度的累加值
3.与容器本身的宽高进行校正获得真正显示的宽高 - 同自定义View的测量方式
1. 1 然后我们看下如何获取子控件的宽高吧...(这里可能就先不过考虑margin..padding了...)
///< 获取子控件的宽高
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
1.2 然后调用getChildMeasureSpec进行子控件的MeasureSpec的获取
///< - 内部处理还是比我们自己的麻烦的,后面我们可能要研究和参考
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, layoutParams.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, layoutParams.height);
1.3 然后就可以进行子控件的宽高测量了
///< 然后真正测量下子控件 - 到这一步我们就对子控件进行了宽高的设置了咯
view.measure(childWidthMeasureSpec, childHeightMeasureSpec);
2.1 然后进行最大宽度和高度的计算
///< 然后再次获取测量后的子控件的属性
layoutParams = view.getLayoutParams();
///< 然后获取宽度的最大值、高度的累加
maxWidth = Math.max(maxWidth, layoutParams.width);
maxHeight += layoutParams.height;
3.1 然后与容器本身进行一个对比处理,比如容器本身wrap_content,但是带背景图片,需要考虑图片的尺寸
///< 然后再与容器本身的最小宽高对比,取其最大值 - 有一种情况就是带背景图片的容器,要考虑图片尺寸
maxWidth = Math.max(maxWidth, getMinimumWidth());
maxHeight = Math.max(maxHeight, getMinimumHeight());
3.2 然后根据容器具体模式设置尺寸
///< 然后再与容器本身的最小宽高对比,取其最大值 - 有一种情况就是带背景图片的容器,要考虑图片尺寸
maxWidth = Math.max(maxWidth, getMinimumWidth());
maxHeight = Math.max(maxHeight, getMinimumHeight());
///< 然后根据容器的模式进行对应的宽高设置 - 参考我们之前的自定义View的测试方式
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSize = MeasureSpec.getSize(heightMeasureSpec);
///< wrap_content的模式
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(maxWidth, maxHeight);
}
///< 精确尺寸的模式
else if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY){
setMeasuredDimension(wSize, hSize);
}
///< 宽度尺寸不确定,高度确定
else if (wSpecMode == MeasureSpec.UNSPECIFIED){
setMeasuredDimension(maxWidth, hSize);
}
///< 宽度确定,高度不确定
else if (hSpecMode == MeasureSpec.UNSPECIFIED){
setMeasuredDimension(wSize, maxHeight);
}
4. 然后准备跑下看看容器的背景范围是否符合预期
package me.heyclock.hl.customcopy;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/*
*@Description: 自定义ViewGroup + 纵向垂直布局 + 单列
*@Author: hl
*@Time: 2018/10/25 10:18
*/
public class CustomViewGroup extends ViewGroup {
private Context context;///< 上下文
/** 计算子控件的布局位置. */
private final Rect mTmpContainerRect = new Rect();
private final Rect mTmpChildRect = new Rect();
public CustomViewGroup(Context context) {
this(context, null);
}
public CustomViewGroup(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, 0, 0);
}
public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.context = context;
}
/**
* 测量容器的宽高 = 所有子控件的尺寸 + 容器本身的尺寸 -->综合考虑
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
///< 定义最大宽度和高度
int maxWidth = 0;
int maxHeight = 0;
///< 获取子控件的个数
int count = getChildCount();
for (int i =0; i < count; ++i){
View view = getChildAt(i);
///< 子控件如果是GONE - 不可见也不占据任何位置则不进行测量
if (view.getVisibility() != GONE){
///< 获取子控件的宽高
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
///< 调用子控件测量的方法getChildMeasureSpec(先不考虑margin、padding)
///< - 内部处理还是比我们自己的麻烦的,后面我们可能要研究和参考
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, layoutParams.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, layoutParams.height);
///< 然后真正测量下子控件 - 到这一步我们就对子控件进行了宽高的设置了咯
view.measure(childWidthMeasureSpec, childHeightMeasureSpec);
///< 然后再次获取测量后的子控件的属性
layoutParams = view.getLayoutParams();
///< 然后获取宽度的最大值、高度的累加
maxWidth = Math.max(maxWidth, layoutParams.width);
maxHeight += layoutParams.height;
}
}
///< 然后再与容器本身的最小宽高对比,取其最大值 - 有一种情况就是带背景图片的容器,要考虑图片尺寸
maxWidth = Math.max(maxWidth, getMinimumWidth());
maxHeight = Math.max(maxHeight, getMinimumHeight());
///< 然后根据容器的模式进行对应的宽高设置 - 参考我们之前的自定义View的测试方式
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSize = MeasureSpec.getSize(heightMeasureSpec);
///< wrap_content的模式
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(maxWidth, maxHeight);
}
///< 精确尺寸的模式
else if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY){
setMeasuredDimension(wSize, hSize);
}
///< 宽度尺寸不确定,高度确定
else if (wSpecMode == MeasureSpec.UNSPECIFIED){
setMeasuredDimension(maxWidth, hSize);
}
///< 宽度确定,高度不确定
else if (hSpecMode == MeasureSpec.UNSPECIFIED){
setMeasuredDimension(wSize, maxHeight);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
发现一片空白,肯定不对,我们设置了背景的...
4.1 然后仔细看了下发现获取控件宽高的地方是不对的,有问题,都是-2
再看下官方的 - 也就是我们获取测量后的子控件的宽高需要调用的是控件的getMeasureXXX()方法,而不是属性(具体文档没看前,我猜想目前容器测量没有完成是获取不到子控件的宽高的,一会我们onDraw里面看看,如果猜想不对?我们可以查查如何获取子控件的宽高...看是不是只能用这个方法);
4.2 然后我们纠正下
正确版本:
package me.heyclock.hl.customcopy;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/*
*@Description: 自定义ViewGroup + 纵向垂直布局 + 单列
*@Author: hl
*@Time: 2018/10/25 10:18
*/
public class CustomViewGroup extends ViewGroup {
private Context context;///< 上下文
/** 计算子控件的布局位置. */
private final Rect mTmpContainerRect = new Rect();
private final Rect mTmpChildRect = new Rect();
public CustomViewGroup(Context context) {
this(context, null);
}
public CustomViewGroup(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, 0, 0);
}
public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.context = context;
}
/**
* 测量容器的宽高 = 所有子控件的尺寸 + 容器本身的尺寸 -->综合考虑
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
///< 定义最大宽度和高度
int maxWidth = 0;
int maxHeight = 0;
///< 获取子控件的个数
int count = getChildCount();
for (int i =0; i < count; ++i){
View view = getChildAt(i);
///< 子控件如果是GONE - 不可见也不占据任何位置则不进行测量
if (view.getVisibility() != GONE){
///< 获取子控件的宽高
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
///< 调用子控件测量的方法getChildMeasureSpec(先不考虑margin、padding)
///< - 内部处理还是比我们自己的麻烦的,后面我们可能要研究和参考
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, layoutParams.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, layoutParams.height);
///< 然后真正测量下子控件 - 到这一步我们就对子控件进行了宽高的设置了咯
view.measure(childWidthMeasureSpec, childHeightMeasureSpec);
///< 然后再次获取测量后的子控件的属性
layoutParams = view.getLayoutParams();
///< 然后获取宽度的最大值、高度的累加
maxWidth = Math.max(maxWidth, view.getMeasuredWidth());
maxHeight += view.getMeasuredHeight();
}
}
///< 然后再与容器本身的最小宽高对比,取其最大值 - 有一种情况就是带背景图片的容器,要考虑图片尺寸
maxWidth = Math.max(maxWidth, getMinimumWidth());
maxHeight = Math.max(maxHeight, getMinimumHeight());
///< 然后根据容器的模式进行对应的宽高设置 - 参考我们之前的自定义View的测试方式
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSize = MeasureSpec.getSize(heightMeasureSpec);
///< wrap_content的模式
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(maxWidth, maxHeight);
}
///< 精确尺寸的模式
else if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY){
setMeasuredDimension(wSize, hSize);
}
///< 宽度尺寸不确定,高度确定
else if (wSpecMode == MeasureSpec.UNSPECIFIED){
setMeasuredDimension(maxWidth, hSize);
}
///< 宽度确定,高度不确定
else if (hSpecMode == MeasureSpec.UNSPECIFIED){
setMeasuredDimension(wSize, maxHeight);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
丫丫
我们布局里面添加了TextView,但是现在显示不了,很正常!因为我们没有摆正位置!!!对,没有摆正位置,哈哈。。。。。onLayout就得做点事情了....
开始前,我们看看onDraw方法能不能获取子控件的一些属性,然后获取宽高...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int count = getChildCount();
for (int i = 0; i < count; ++i){
///< 获取子控件的宽高
ViewGroup.LayoutParams layoutParams = getChildAt(i).getLayoutParams();
Log.e("test", "111=" + layoutParams.height);
Log.e("test", "111=" + layoutParams.width);
}
}
好像不行, 我们百度下吧...
这里有说法 获取控件(布局或View)宽度或高度的方法 - windroid之父 - CSDN博客
后面小白还会专门研究这块的,自定义View知识点还特么好多哟...
三、开始子控件的位置摆放咯
1. 计算子控件左上角(left, top)、右下角(right, bottom)
2. 然后调用View.layout进行摆放,之前可以调用Gravity.apply进行位置的纠正(这个一点点完善才能看到效果)
1.1 获得初始绘制左上角,右下角不用管? 因为我们仅仅是垂直布局,同时我们绘制默认都是从左上角,暂时不考虑靠右,居中等情况
也就是我们这样绘制:
这样我们就只需要知道左上角 + 控件的宽高 = 最后绘制的范围了
2.1 开始计算并摆放吧。。。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
///< 获取范围初始左上角 - 这个决定子控件绘制的位置,我们绘制理论可以从0,0开始,margin容器本身已经考虑过了...所以别和margin混淆了
int leftPos = getPaddingLeft();
int leftTop = getPaddingTop();
///< 获取范围初始右下角 - 如果考虑控件的位置,比如靠右,靠下等可能就要利用右下角范围来进行范围计算了...
///< 后面我们逐步完善控件的时候用会用到这里...
//int rightPos = right - left - getPaddingRight();
//int rightBottom = bottom - top - getPaddingBottom();
///< 由于我们是垂直布局,并且一律左上角开始绘制的情况下,我们只需要计算出leftPos, leftTop就可以了
int count = getChildCount();
for (int i = 0; i < count; ++i){
View childView = getChildAt(i);
///< 控件占位的情况下进行计算
if (childView.getVisibility() != GONE){
int childW = childView.getMeasuredWidth();
int childH = childView.getMeasuredHeight();
///< 先不管控件的margin哈!
int cleft = leftPos;
int cright = cleft + childW;
int ctop = leftPos;
int cbottom = ctop + childH;
///< 下一个控件的左上角需要向y轴移动上一个控件的高度 - 不然都重叠了!
leftTop += childH;
///< 需要一个范围,然后进行摆放
childView.layout(cleft, ctop, cright, cbottom);
}
}
}
然后之前的整体看下是不是初具雏形...
貌似雏形有了.....
四、貌似一个入门级的自定义ViewGroup就行了的样子...
但是离官方的还有一些距离?
- 首先我们还没有考虑margin,padding等一些情况;
- 我们还未考虑子控件的gravity位置,比如居中,比如靠右,靠左等
- 关于官方的一些个状态以及Gravity.apply等有必要我们去学习
- 只能注入深入完善以及学习实践更多更复杂的自定义控件才能更深的理解
- 下一步打算完善下,过程中回去了解下官方的一些方法,看起来看不懂,哎!!
- 点击事件的相关联系后面是要搞的,灰常重要的一个环节
Last,今天就暂时到这里。。。
心灵鸡汤 - 点点滴滴点点, 滴滴点点滴滴