1.前言
我的上一篇文章,介绍了如何自定义ViewGroup,演示了如何定义一个流式布局。那么今天我想阅读一下FrameLayout的源码,学习一下系统是如何自定义布局的。学会阅读源码是很有必要的。
2.直接上源码
在阅读源码中,我们只需要关注onMeasure()和onLayout()方法。
源码中,我已经加了注释,希望可以方便大家阅读。
我画了一个示意图,方便大家理解:
//我们只需要关注onMeasure()和onLayout()方法
public class FrameLayout extends ViewGroup {
private static final int DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START;
@ViewDebug.ExportedProperty(category = "measurement")
@UnsupportedAppUsage
boolean mMeasureAllChildren = false;
@ViewDebug.ExportedProperty(category = "padding")
@UnsupportedAppUsage
private int mForegroundPaddingLeft = 0;
@ViewDebug.ExportedProperty(category = "padding")
@UnsupportedAppUsage
private int mForegroundPaddingTop = 0;
@ViewDebug.ExportedProperty(category = "padding")
@UnsupportedAppUsage
private int mForegroundPaddingRight = 0;
@ViewDebug.ExportedProperty(category = "padding")
@UnsupportedAppUsage
private int mForegroundPaddingBottom = 0;
private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1);
public FrameLayout(@NonNull Context context) {
super(context);
}
public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
@AttrRes int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
@AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.FrameLayout, defStyleAttr, defStyleRes);
saveAttributeDataForStyleable(context, R.styleable.FrameLayout,
attrs, a, defStyleAttr, defStyleRes);
if (a.getBoolean(R.styleable.FrameLayout_measureAllChildren, false)) {
setMeasureAllChildren(true);
}
a.recycle();
}
/**
* Describes how the foreground is positioned. Defaults to START and TOP.
*
* @param foregroundGravity See {@link android.view.Gravity}
*
* @see #getForegroundGravity()
*
* @attr ref android.R.styleable#View_foregroundGravity
*/
@android.view.RemotableViewMethod
public void setForegroundGravity(int foregroundGravity) {
if (getForegroundGravity() != foregroundGravity) {
super.setForegroundGravity(foregroundGravity);
// calling get* again here because the set above may apply default constraints
final Drawable foreground = getForeground();
if (getForegroundGravity() == Gravity.FILL && foreground != null) {
Rect padding = new Rect();
if (foreground.getPadding(padding)) {
mForegroundPaddingLeft = padding.left;
mForegroundPaddingTop = padding.top;
mForegroundPaddingRight = padding.right;
mForegroundPaddingBottom = padding.bottom;
}
} else {
mForegroundPaddingLeft = 0;
mForegroundPaddingTop = 0;
mForegroundPaddingRight = 0;
mForegroundPaddingBottom = 0;
}
requestLayout();
}
}
/**
* Returns a set of layout parameters with a width of
* {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT},
* and a height of {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}.
*/
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
int getPaddingLeftWithForeground() {
return isForegroundInsidePadding() ? Math.max(mPaddingLeft, mForegroundPaddingLeft) :
mPaddingLeft + mForegroundPaddingLeft;
}
int getPaddingRightWithForeground() {
return isForegroundInsidePadding() ? Math.max(mPaddingRight, mForegroundPaddingRight) :
mPaddingRight + mForegroundPaddingRight;
}
private int getPaddingTopWithForeground() {
return isForegroundInsidePadding() ? Math.max(mPaddingTop, mForegroundPaddingTop) :
mPaddingTop + mForegroundPaddingTop;
}
private int getPaddingBottomWithForeground() {
return isForegroundInsidePadding() ? Math.max(mPaddingBottom, mForegroundPaddingBottom) :
mPaddingBottom + mForegroundPaddingBottom;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
//判断布局是否是精确模式
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
//这是用来保存继承父View长或者宽的子View的集合
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//保存子View测量结果的
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
//获取子View属性
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//计算布局宽度,子view的最大宽高就是布局的宽高
//父View的宽 = 子View宽 + 左外边距 + 右外边距
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
//高也是这样,同上
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
//上面我们说过,如果父View是不确定模式,那么子view继承父View的宽高也无法确定
//因此要先保存下来,后面进行处理
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
// 父View的宽,还要加上父View对子View的内边距
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// 如果设置了最小宽高,那么还要和最小宽高比较
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// 是否存在背景,是的话,还要跟背景的宽高比较
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
//保存布局的测量结果
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
//父View宽高确定了,那么继承父View宽高的子View的宽高也要确定下来
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec;
//如果子View继承父类宽度,那么
//子View宽 = 父View宽 - 外边距 - 内边距
//反之,通过getChildMeasureSpec()计算出宽度
if (lp.width == LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight()
- getPaddingTopWithForeground() - getPaddingBottomWithForeground()
- lp.topMargin - lp.bottomMargin);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
//保存子View测量结果
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
//父View的左上右下坐标
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
//获取位置信息,如:水平居中
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
//水平位置信息
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
//垂直位置信息
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
//水平居中
//子view左坐标 = 父左坐标 + (父宽-子宽)的一半 + 左外边距 - 右外边距
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
//水平居右
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
//水平居左
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
//垂直信息
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
//子View布局
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
/**
* Sets whether to consider all children, or just those in
* the VISIBLE or INVISIBLE state, when measuring. Defaults to false.
*
* @param measureAll true to consider children marked GONE, false otherwise.
* Default value is false.
*
* @attr ref android.R.styleable#FrameLayout_measureAllChildren
*/
@android.view.RemotableViewMethod
public void setMeasureAllChildren(boolean measureAll) {
mMeasureAllChildren = measureAll;
}
/**
* Determines whether all children, or just those in the VISIBLE or
* INVISIBLE state, are considered when measuring.
*
* @return Whether all children are considered when measuring.
*
* @deprecated This method is deprecated in favor of
* {@link #getMeasureAllChildren() getMeasureAllChildren()}, which was
* renamed for consistency with
* {@link #setMeasureAllChildren(boolean) setMeasureAllChildren()}.
*/
@Deprecated
public boolean getConsiderGoneChildrenWhenMeasuring() {
return getMeasureAllChildren();
}
/**
* Determines whether all children, or just those in the VISIBLE or
* INVISIBLE state, are considered when measuring.
*
* @return Whether all children are considered when measuring.
*/
@InspectableProperty
public boolean getMeasureAllChildren() {
return mMeasureAllChildren;
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new FrameLayout.LayoutParams(getContext(), attrs);
}
@Override
public boolean shouldDelayChildPressedState() {
return false;
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
if (sPreserveMarginParamsInLayoutParamConversion) {
if (lp instanceof LayoutParams) {
return new LayoutParams((LayoutParams) lp);
} else if (lp instanceof MarginLayoutParams) {
return new LayoutParams((MarginLayoutParams) lp);
}
}
return new LayoutParams(lp);
}
@Override
public CharSequence getAccessibilityClassName() {
return FrameLayout.class.getName();
}
/** @hide */
@Override
protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
super.encodeProperties(encoder);
encoder.addProperty("measurement:measureAllChildren", mMeasureAllChildren);
encoder.addProperty("padding:foregroundPaddingLeft", mForegroundPaddingLeft);
encoder.addProperty("padding:foregroundPaddingTop", mForegroundPaddingTop);
encoder.addProperty("padding:foregroundPaddingRight", mForegroundPaddingRight);
encoder.addProperty("padding:foregroundPaddingBottom", mForegroundPaddingBottom);
}
/**
* Per-child layout information for layouts that support margins.
* See {@link android.R.styleable#FrameLayout_Layout FrameLayout Layout Attributes}
* for a list of all child view attributes that this class supports.
*
* @attr ref android.R.styleable#FrameLayout_Layout_layout_gravity
*/
public static class LayoutParams extends MarginLayoutParams {
/**
* Value for {@link #gravity} indicating that a gravity has not been
* explicitly specified.
*/
public static final int UNSPECIFIED_GRAVITY = -1;
/**
* The gravity to apply with the View to which these layout parameters
* are associated.
* <p>
* The default value is {@link #UNSPECIFIED_GRAVITY}, which is treated
* by FrameLayout as {@code Gravity.TOP | Gravity.START}.
*
* @see android.view.Gravity
* @attr ref android.R.styleable#FrameLayout_Layout_layout_gravity
*/
@InspectableProperty(
name = "layout_gravity",
valueType = InspectableProperty.ValueType.GRAVITY)
public int gravity = UNSPECIFIED_GRAVITY;
public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
super(c, attrs);
final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FrameLayout_Layout);
gravity = a.getInt(R.styleable.FrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY);
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
/**
* Creates a new set of layout parameters with the specified width, height
* and weight.
*
* @param width the width, either {@link #MATCH_PARENT},
* {@link #WRAP_CONTENT} or a fixed size in pixels
* @param height the height, either {@link #MATCH_PARENT},
* {@link #WRAP_CONTENT} or a fixed size in pixels
* @param gravity the gravity
*
* @see android.view.Gravity
*/
public LayoutParams(int width, int height, int gravity) {
super(width, height);
this.gravity = gravity;
}
public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(@NonNull ViewGroup.MarginLayoutParams source) {
super(source);
}
/**
* Copy constructor. Clones the width, height, margin values, and
* gravity of the source.
*
* @param source The layout params to copy from.
*/
public LayoutParams(@NonNull LayoutParams source) {
super(source);
this.gravity = source.gravity;
}
}
}