preface
By learning the source code of Android official Layout, you can help yourself to better understand the Android UI framework system, understand the internal convenient encapsulation good API call, help to Layout optimization and custom view implementation and other work. Here the learning results through writing blog summary, easy to remember, not forgotten in the future.
The source code in this blog is based on Android 8.1
FrameLayout characteristics
FrameLayout is one of the most commonly used layouts in Android development. It is characterized by the overlay of child views, and the added child views will be overlaid on top of other child views.
The source code to explore
The constructor
The FrameLayout constructor is simple, handling a FrameLayout property measureAllChildren:
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);
if (a.getBoolean(R.styleable.FrameLayout_measureAllChildren, false)) {
setMeasureAllChildren(true);
}
a.recycle();
}
Copy the code
The measureAllChildren property sets whether all subviews are counted when measuring width and height. The default is false, that is, child views in the GONE state are not considered in the measure phase.
LayoutParams
Gravity FrameLayout defines a static inner class called LayoutParams that inherits from MarginLayoutParams and contains a member called gravity:
public int gravity = UNSPECIFIED_GRAVITY;
Copy the code
Therefore, child views are supported to set the parent layout alignment.
Measuring onMeasure
Because of FrameLayout FrameLayout, unlike LinearLayout and RelativeLayout, which require weights or relative relationships, you just need to traverse the sub-view, call the child measurement in turn, and set its own size. But there is a refinement difference, and when FrameLayout’s MeasureSpec mode is EXACTLY, just follow the normal process. When the mode is AT_MOST, it means that the size of FrameLayout is not clear and needs to rely on the size of the largest child in reverse. Therefore, the maximum size needs to be recorded while traversing. If LayoutParams with children are MATCH_PARENT, it means that the child is dependent on the parent layout size, so they need to be measured again after FrameLayout has set its own size.
The measurement of width and height in FrameLayout is divided into two parts. The upper part is divided into calculating the maximum width and height in the sub-view, so as to set its own width and height. The lower part is divided into secondary calculation. In the upper part, the width and height of the sub-view cannot be accurately calculated. At this time, the measurement specification passed to the child is generated according to the width and height measured by FrameLayout.
First, the upper part: calculate the maximum width and height
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
// This variable is used to determine whether the record requires a secondary measurement child view (true if the exact size is not specified in the measurement specification given by the parent layout of FrameLayout).
final booleanmeasureMatchParentChildren = MeasureSpec.getMode(widthMeasureSpec) ! = MeasureSpec.EXACTLY || MeasureSpec.getMode(heightMeasureSpec) ! = MeasureSpec.EXACTLY;// mMatchParentChildren is an ArrayList collection used to cache subviews that require secondary measurements of width and height.
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
// Iterate over the subview
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
// Determine if Child is GONE, or if the measureAllChildren attribute is set.
if(mMeasureAllChildren || child.getVisibility() ! = GONE) {// Call the child measure.
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// After the measurement is completed, obtain the measurement width and height value of child and add margin value to calculate the maximum width and height value.
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
// Merge the measurement state of child. GetMeasuredState gets the measured state of the Child. There are wide states and high states,
// Stores the upper 8 bits of the mMeasuredWidth and mMeasuredHeight members, respectively. After obtaining the stored state,
// Set the wide state to the first byte of an int, the high state to the third byte of an int, and return the combined int.)
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
/ / if measureMatchParentChildren is true, and the child LayoutParams set to fill the parent layout,
// Add FrameLayout to the List. After FrameLayout calculates the width and height of FrameLayout, perform the second measurement.mMatchParentChildren.add(child); }}}}// Account for padding too
// Add compute padding
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// Check against our minimum height and width
// Compare with the minimum width and height value. Must not be less than the minimum width and height.
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
// If there is a foreground image (as opposed to the background image), it cannot be smaller than the minimum width and height of the foreground image.
final Drawable drawable = getForeground();
if(drawable ! =null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
// Set FrameLayout's own measurement width and height
// The resolveSizeAndState method returns a new measurement-width value based on the parent's measurement size and its calculated measurement-width value, and sets MeasuredState on this measurement-width value.
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
// Next section...
}
Copy the code
FrameLayout in onMeasure method, first traverses the sub-view, measures the sub-view, compares the width and height of the largest sub-view, and adds the view without precise width and height value to the list cache. Then set the FrameLayout itself with the maximum width and height value.
Among them, the ACQUISITION of padding, Minimum width and height, combined size value and state apis provide great convenience for measurement operations, and you can learn to flexibly call when customizing the layout.
Note on MeasuredState: The upper 8 bits of the mMeasuredWidth and mMeasuredHeight member variables are used to store MeasuredState, and the remaining 24 bits store size values. Somewhat similar to MeasureSpec, the higher two bits store the size values, and the remaining 30 bits store the size values. When the view measures its width and height, if the MeasuredState value exceeds the MEASURED_STATE_TOO_SMALL size specified by the parent layout, the MeasuredState value can be set to MEASURED_STATE_TOO_SMALL and request the parent layout to loosen the MeasuredState size limit.
Second, the next part: secondary measurement sub-view in the completion of FrameLayout on its own width and height calculation, and then the list of sub-view secondary measurement.
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// The upper part...
count = mMatchParentChildren.size();
if (count > 1) {
// Perform secondary measurement only when there are at least two child views with MATCH_PARENT set.
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// Re-specify the width gauge for the child
final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
// If the width is MATCH_PARENT, get FrameLayout's own width, subtract the padding and margin values,
// Calculate the new width value (less than 0, 0), set the measurement mode to EXACTLY, and combine the new measurement.
final int width = Math.max(0. getMeasuredWidth() - getPaddingLeftWithForeground() - getPaddingRightWithForeground() - lp.leftMargin - lp.rightMargin); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( width, MeasureSpec.EXACTLY); }else {
// When the width value is the exact PX, DP, or WRAP_CONTENT, measure the size in combination with the size passed in by the parent layout.
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
// Specify a new height gauge, the same logic as the width gauge.
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);
}
// Call child to measure again.child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }}}Copy the code
In this section, we iterate over the child views in the cache collection, generating new measurement specifications in turn, and then call the child View to measure again. The getChildMeasureSpec method is responsible for generating a new measurement gauge from the measurement gauge passed in by the parent layout and the LayoutParams values of the padding and child.
GetChildMeasureSpec method
/ * * *@paramSpec Parent layout The given measurement specification *@paramPadding Sum of padding and margin *@paramChildDimension LayoutParams width, height *@returnNew measurement specifications */
int getChildMeasureSpec(int spec, int padding, int childDimension)
Copy the code
In this method, specMode and specSize are first extracted from the parent layout measurement rule, and the size is obtained by subtracting the padding from specSize (0 if less than 0). After that, new measurement specifications will be generated based on specMode, size and childDimension combined with the conditions:
SpecMode ⬇ ️ \ childDimension ➡ ️ | MATCH_PARENT | WRAP_CONTENT | x px/dp |
---|---|---|---|
EXACTLY | EXACTLY+size | AT_MOST+size | EXACTLY+childDimension |
AT_MOST | AT_MOST+size | AT_MOST+size | EXACTLY+childDimension |
UNSPECIFIED | UNSPECIFIED+size | UNSPECIFIED+size | EXACTLY+childDimension |
Note: FrameLayout is only remeasured using FrameLayout measurements if there are at least two child views of LayoutParams whose width or height is MATCH_PARENT. If there is only one View, it is measured with the measurement specification passed in from the parent layout of FrameLayout, and no secondary measurement is made.
Layout onLayout
FrameLayout onLayout according to the parent layout given up, down, left and right, combined with the child view of gravity, width, height, margin and other child view layout.
@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();
// Calculate l, t, r, b after subtracting the padding
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);
// Skip the GONE child
if(child.getVisibility() ! = GONE) {final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// Get the measurement width and height of child
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
// Default alignment (upper left corner)
gravity = DEFAULT_CHILD_GRAVITY;
}
// Get the layout direction (left to right or right to left)
final int layoutDirection = getLayoutDirection();
// Get the relative layout direction (for Gravity.START and Gravity.END, convert to LEFT and RIGHT according to the layout direction)
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
// Get the vertical alignment
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
// Determine the horizontal alignment
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
// Horizontal center
// Calculate the left margin of the child.
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
// align to the right
// Determine whether to force left alignment (default false, that is, not to force left alignment)
if(! forceLeftGravity) { childLeft = parentRight - width - lp.rightMargin;break;
}
case Gravity.LEFT:
default:
// Left, default alignment
childLeft = parentLeft + lp.leftMargin;
}
// Determine the vertical alignment
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
// Vertical center (logic is similar to horizontal center)
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;
}
// Call the child layoutchild.layout(childLeft, childTop, childLeft + width, childTop + height); }}}Copy the code
In the layoutChildren layout method, the alignment method is extracted from int through bit operation. If START and END are set, they will be converted into the corresponding LEFT and RIGHT according to RTL and LTR, and then the horizontal alignment and vertical alignment will be determined successively. Because width and height of the child to ensure that the horizontal direction only calculate childLeft, only vertical calculation childTop can be.
conclusion
The core logic of FrameLayout is onMeasure and onLayout methods. The onMeasure method compares the maximum width and height of the child when distributing the child measurement. When the LayoutParams with child is set to MATCH_PARENT, it means that it needs to rely on the size of the parent layout. If the measurement mode of the parent layout does not specify a specific size, Add the child to the list under test. After traversing the Child, set its width and height. After that, if the number of cached children in the test judgment list is at least 2, the measurement specifications will be generated using the width and height of FrameLayout itself, and then child will be called for secondary measurement. The onLayout method iterates through the child, modifying childLeft and childTop based on alignment, and finally calling the Child layout.