楼主最近在复习自定义View,在复习到自定义ViewGroup这个知识点时,发现了一个问题--就是我们之前的定义ViewGroup在考虑Margin属性可能有问题。本文在解决该问题给出建议性的意见,但是不一定是正确的,如果有错误或者不当的地方,希望指正。
本文参考文章:
1.Android 手把手教您自定义ViewGroup(一)
2.你的自定义View是否真的支持Margin
1.提出问题
这里我举一个简单的例子来说,假设我们需要定义一个ViewGroup放置一个子View,同时这个子View支持Padding和Margin属性。
这里我先贴出一个常规的写法:
public class CustomViewGroup02 extends ViewGroup {
public CustomViewGroup02(Context context) {
super(context);
}
public CustomViewGroup02(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomViewGroup02(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//这里假设只有一个子View
measureChildren(widthMeasureSpec, heightMeasureSpec);
View view = getChildAt(0);
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
int width = view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin + getPaddingLeft() + getPaddingRight();
int height = view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin + getPaddingTop() + getPaddingBottom();
setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize:width, (heightMode == MeasureSpec.EXACTLY) ? heightSize:height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
View view = getChildAt(0);
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
int left = getPaddingLeft() + lp.leftMargin;
int top = getPaddingTop() + lp.topMargin;
view.layout(left, top, left + view.getMeasuredWidth(), top + view.getMeasuredHeight());
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
}
在代码中,我们考虑到了padding属性和Margin属性,同时我们可以在xml代码测试一下效果
xml中这样写:
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.apple.android_demo08.CustomViewGroup02
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginLeft="20dp"
android:background="#FFDAB9" />
</com.example.apple.android_demo08.CustomViewGroup02>
</android.support.constraint.ConstraintLayout>
模拟器上展示的效果图:
看上去似乎是没有问题的,我们给TextView设置了marginLeft为20dp,在手机上也能正常显示出来margin属性。但是,如果TextView的layout_width设置为match_parent会怎么样呢?
xml代码:
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.apple.android_demo08.CustomViewGroup02
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginLeft="20dp"
android:background="#FFDAB9" />
</com.example.apple.android_demo08.CustomViewGroup02>
</android.support.constraint.ConstraintLayout>
此时我们在Android studio右侧的预览界面来看看此时效果:
我们发现虽然TextVeiw向左移动了20dp,但是我们发现了一个问题,就是TextView右侧超出了屏幕,也就是说,TextView的layout_marginLeft 属性根本没有影响到它的width,只是单纯将TextView向右移动了20dp。这个是有问题的,我们去看看系统的LinearLayout布局,margin属性会影响View的宽和高的。从而得知,我们这里支持的Margin属性是假的!那怎么才能真正的支持Margin属性呢?
2.解决问题
要想解决问题,必须先知道问题出现在哪里。这个问题就出现在onMeasure方法中measureChildren方法。
我们先来看看measureChildren方法的源码:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
这个方法表达的意思非常简单,就是循环测量每个子View。然后我们再来看看measureChild方法:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
在measureChild方法里面,先利用父布局的XXXXMeasureSpec、padding值和子View向父布局申请的大小来生成子View的宽和高。这里我们就看出问题了,我们发现系统在测量子View的width和height时,只是考虑了padding的影响,没有考虑Margin对View的width和height的影响。
看到这里,我们明白了,为什么之前我们给TextView设置了marginLeft,同时设置TextView的layout_width为match_parent时,TextView只是单纯的向右移动了,而没有调整TextView的大小。因为我们通过measureChild方法来测量每个子View是不会考虑Margin属性对View的大小的影响。
知道的问题所在,解决问题就非常的容易。解决的问题的办法就是重写measureChildren方法,在测量每个View时,考虑到margin的影响。其实在ViewGroup还有一个方法那就是measureChildWidthMargins方法,这个方法测量每个View时,考虑到了每个View的margin属性的影响。我们来看看measureChildWidthMargins方法的源代码:
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);
}
我们发现在这个方法里面,将Margin属性的影响也考虑到的。那么我们就来重写measureChildren方法:
@Override
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
final View view = getChildAt(i);
if (view != null && view.getVisibility() != GONE){
measureChildWithMargins(view, widthMeasureSpec, 0, heightMeasureSpec, 0);
}
}
}
在这个重写的代码中,我们需要主要两点:
1.在原来的measureChildren方法的if判断条件是:(child.mViewFlags & VISIBILITY_MASK) != GONE,而我们这里是:view != null && view.getVisibility() != GONE。我们这里的依据是LinearLayout,系统的LinearLayout也重写了measureChildren方法的,它的判断条件就是:view != null && view.getVisibility() != GONE。
2.measureChildrenWithMargins方法多出两个参数,分别是:widthUsed,heightUsed,这里传入的是两个0,这里的依据还是LinearLayout,LinearLayout调用measureChildrenWithMargins传入就是两个0。
重写之后,我们来看看之前的match_parent的情况(记得Rebuild一下工程):
这下就变得正常得多了!
网友评论