不过由于我们没有考虑margin,所以暂时还不能支持margin属性的设置,即使你设置了,但是如果onLayout没有做处理也是不行滴...自定义的呀!
首先我们拿第一个控件设置一些个margin,然后内部去处理下:
custom_viewgroup_me.xml
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ffff00ee">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp"
android:background="#ff87addd"
android:text="aaaaaaaa哇咔咔哇咔咔"
android:textColor="#ff000000"
android:textSize="22sp" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ffa43dee"
android:text="sssssss"
android:textColor="#ff000000"
android:textSize="12sp" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ffa43dee"
android:text="wwwwwww"
android:textColor="#ff000000"
android:textSize="18sp" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ffa43dee"
android:text="大幅度发"
android:textColor="#ff000000"
android:textSize="12sp" />
Then,我们要拿到margin属性,然后做几件事情:
1. 在onMeasure中增加容器的左右宽度,也就是之前计算的maxWidth = maxWidth + leftMargin + rightMargin; ---因为你设置了控件的左右间距,相当于增大了控件的宽度
2. 在onMeasure中增加容器的上下高度(就是高度),同理maxWidth = maxWidth + topMargin + bottomMargin;
3. 在onLayout中,绘制的left, top自然也要跟着进行位移,凸显出间距来 leftPos + leftMargin leftTop + rightMargin
4. 在onLayout中,之前累积的下一个控件的top值相应的要多增加top/bottom的间距leftTop += childH + topMargin + bottomMargin;
5. onLayout中的绘制范围右下角还是和之前一样(除非你有的控件可能受到对应影响,那样可能就是相对复杂的布局了)
A. 现在需要获取整个margin属性??
我们下一篇专门针对这个做一些学习...
下面我们就直接参考官方之前的做法自定义个LayoutParams继承MarginLayoutParams,然后在generateLayoutParams方法中返回(之前我有提到过这个方法):
@Override
public CustomViewGroupLastNew.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new CustomViewGroupLastNew.LayoutParams(getContext(), attrs);
}
/**
* 这个是布局相关的属性,最终继承的是ViewGroup.LayoutParams,所以上面我们可以直接进行转换
* --目的是获取自定义属性以及一些使用常量的自定义
*/
public static class LayoutParams extends MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
// Pull the layout param values from the layout XML during
// inflation. This is not needed if you don't care about
// changing the layout behavior in XML.
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CustomLayoutLP);
///< TODO 一些属性的自定义
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
简单说明:至于怎么就获取的margin,然后怎么返给子控件的,大概可以跟到MarginLayoutParams里面去看.. next page,let't do it...
B. 获取到属性了我们就可以直接累代码了,相信积累了前面几篇文章的过渡,这些思考下应该问题不大了吧...直接看代码:
package me.heyclock.hl.customcopy;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
/*
*@Description: 自定义ViewGroup + 纵向垂直布局 + 单列
*@Author: hl
*@Time: 2018/10/25 10:18
*/
public class CustomViewGroupLastNew extends ViewGroup {
private Context context;///< 上下文
/**
* 计算子控件的布局位置.
*/
private final Rect mTmpContainerRect = new Rect();
private final Rect mTmpChildRect = new Rect();
public CustomViewGroupLastNew(Context context) {
this(context, null);
}
public CustomViewGroupLastNew(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomViewGroupLastNew(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, 0, 0);
}
public CustomViewGroupLastNew(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) {
///< 定义最大宽度和高度
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) {
///< 获取子控件的属性 - margin、padding
CustomViewGroupLastNew.LayoutParams layoutParams = (CustomViewGroupLastNew.LayoutParams) view.getLayoutParams();
///< 调用子控件测量的方法getChildMeasureSpec(先不考虑margin、padding)
///< - 内部处理还是比我们自己的麻烦的,后面我们可能要研究和参考
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, layoutParams.leftMargin + layoutParams.rightMargin, layoutParams.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, layoutParams.topMargin + layoutParams.bottomMargin, layoutParams.height);
///< 然后真正测量下子控件 - 到这一步我们就对子控件进行了宽高的设置了咯
view.measure(childWidthMeasureSpec, childHeightMeasureSpec);
///< 然后再次获取测量后的子控件的属性
layoutParams = (CustomViewGroupLastNew.LayoutParams) view.getLayoutParams();
///< 然后获取宽度的最大值、高度的累加
maxWidth = Math.max(maxWidth, view.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin);
maxHeight += view.getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin;
}
}
///< 然后再与容器本身的最小宽高对比,取其最大值 - 有一种情况就是带背景图片的容器,要考虑图片尺寸
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 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){
///< 获取子控件的属性 - margin、padding
CustomViewGroupLastNew.LayoutParams layoutParams = (CustomViewGroupLastNew.LayoutParams) childView.getLayoutParams();
int childW = childView.getMeasuredWidth();
int childH = childView.getMeasuredHeight();
///< 先不管控件的margin哈!
int cleft = leftPos + layoutParams.leftMargin;
int cright = cleft + childW;
int ctop = leftTop + layoutParams.topMargin;
int cbottom = ctop + childH;
///< 下一个控件的左上角需要向y轴移动上一个控件的高度 - 不然都重叠了!
leftTop += childH + layoutParams.topMargin + layoutParams.bottomMargin;
///< 需要一个范围,然后进行摆放
childView.layout(cleft, ctop, cright, cbottom);
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int count = getChildCount();
for (int i = 0; i < count; ++i) {
///< 获取子控件的宽高
View view = getChildAt(i);
CustomViewGroupLastNew.LayoutParams layoutParams = (CustomViewGroupLastNew.LayoutParams) view.getLayoutParams();
Log.e("test", "layoutParams.height=" + layoutParams.height);
Log.e("test", "layoutParams.width=" + layoutParams.width);
Log.e("test", "view.getHeight()=" + view.getHeight());
Log.e("test", "view.getWidth()=" + view.getWidth());
Log.e("test", "view.getMeasuredHeight()=" + view.getMeasuredHeight());
Log.e("test", "view.getMeasuredWidth()=" + view.getMeasuredWidth());
}
}
@Override
public CustomViewGroupLastNew.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new CustomViewGroupLastNew.LayoutParams(getContext(), attrs);
}
/**
* 这个是布局相关的属性,最终继承的是ViewGroup.LayoutParams,所以上面我们可以直接进行转换
* --目的是获取自定义属性以及一些使用常量的自定义
*/
public static class LayoutParams extends MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
// Pull the layout param values from the layout XML during
// inflation. This is not needed if you don't care about
// changing the layout behavior in XML.
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CustomLayoutLP);
///< TODO 一些属性的自定义
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
}
当然这个我们是比官方的简化了些,以为你有些属性我们还没有定义,也就是用来完善布局的属性,比如gravity、posion(就是靠左靠右啥的....当然随着控件越来越复杂肯定也需要做更多的处理...) - 里面增加的地方,前面的几点说明的注意看下:
image
image
效果:
image
心灵鸡汤 - 自定义控件是一条蛮长的路滴
黄磊