Introduction on how does Android View rendering

How are Views structured in an Activity?

Needone App
10 min read5 days ago
win.png

The Activity and View are designed to have multiple layers, which is intended to divide the responsibilities of functionality. The following describes each layer’s responsibilities:

Activity:

  • Encapsulates the logic for handling the lifecycle of an Activity, simplifying the process of creating user interfaces.
  • Encapsulates the creation of a Window object (which is actually a PhoneWindow object) and unified management of Windows.
  • Encapsulates event dispatching logic to respond to user interactions.
  • Encapsulates related logic for various launch modes.
  • Encapsulates page-to-page data transmission-related logic.

Window

  • Encapsulates the logic for parsing XML layouts into Views.
  • Encapsulates the style and layout of a window, including title bars and content areas.

DecorView

  • Encapsulates methods for modifying status bar and navigation bar colors (the navigation bar and status bar are transparent and float above DecorView).
  • Encapsulates initialization logic for the root view tree, as well as layout, drawing, and event dispatching logic. The performTraversals method in ViewRootImpl is used to initiate rendering, which serves as a bridge between Window and DecorView.

2. How are Views displayed?

The basic process can be summarized as follows:

ViewTraversals.png

(1) When an Activity executes the attach() method, a PhoneWindow object is created. The PhoneWindow object then calls the getDecorView method to create a DecorView object.

(2) After executing the onCreate() method in the Activity, setContentView() is executed, which creates subDecor (including contentRoot, title bars, and status bars). Before this, if no DecorView exists, it will first be created.

(3) After completing subDecorView, through LayoutInflate, the layout we set is loaded and parsed into a ViewGroup. Then, addView() is used to add it to the contentParent of contentRoot;

(4) The handleResumeActivity method calls onResume after calling PhoneWindow to get WindowManagerImpl to call addView, which internally calls WindowManagerGlobal.addView, finally calling ViewRootImpl’s setView method.

(5) In setView, requestLayout is called, checking if it’s on the main thread. Then scheduleTraversals() is called.

(6) The scheduleTraversals method uses a Handle to ensure that each drawing operation occurs at the time of the vsync pulse signal.

(7) doTraversal calls performTraversals, first calling DecorView’s dispatchAttachedToWindow method to distribute onAttachedToWindow events. Then measure, layout, and draw processes are executed.

(8) In setView, the current PhoneWindow is added to WindowManagerService (WMS).

3. How are Views drawn? (ViewRootImpl.performTraversals())

Here is the translation:

The Main Execution Flow in performTraversals()

private void performTraversals() {
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
// Execute measurement process
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
// Execute layout process
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
// Execute drawing process
performDraw();
}

The ViewRootImpl.performTraversals() method executes performMeasure(), performLayout(), and performDraw() ultimately calling View methods such as onMeasure(), onLayout(), and onDraw(). The execution of performMeasure(), performLayout(), and performDraw() is a recursive structure.

3.1 Measure — Understanding SpecMode

MeasureSpec represents a View’s measurement mode and the length (width or height) value obtained during measurement [not an actual value, needs to be adjusted based on the measurement mode]. How can we understand it?

image.png

Measurement Value

  • Can be simply understood as a View’s temporary length, which is a reference value;

Measurement Mode

  • EXACTLY (E): indicates that the measurement value is the actual length of the View;
  • AT_MOST (A): indicates that the actual length of the View cannot exceed the measurement value;
  • UNSPECIFIED (U): indicates that the length of the View is not restricted by this measurement value, and controls like ScrollView, RecyclerView use this mode because child Views can be higher than their parent;

Rules for Determining a Child View’s SpecMode

A View’s actual length is determined by its parent ViewGroup’s SpecMode and its LayoutParams. The relationship is as follows:

| Parent ViewGroup Mode | Child View Parameter | Result | | — — | — — | — — | | EXACTLY, parentSize | Specific length value (dp/px) | E, specific length value | | | match_parent | E, parentSize-padding | | AT_MOST, parentSize | Specific length value (dp/px) | E, specific length value | | | match_parent | A, parentSize-padding | | UNSPECIFIED, parentSize | Specific length value (dp/px) | E, specific length value | | | match_parent | U, unknown |

3.1 Measure — measure() Execution Details

In the measure method, the parent ViewGroup will call its child ViewGroup or View's measureChildren() method in a for loop. The measureChildren() method calls the child View's measure() method. Simplified code is as follows:

/**
* Ask all of the children of this view to measure themselves,
*/

Note: I have followed the rules you provided and translated the content without making any changes to the Markdown markup structure, code blocks, or HTML-like tags.

  • account both the MeasureSpec requirements for this view and its padding.
  • We skip children that are in the GONE state The heavy lifting is done in
  • getChildMeasureSpec.
  • @param widthMeasureSpec The width requirements for this view
  • @param heightMeasureSpec The height requirements for this view */ 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); } }
In the ViewGroup class, the measureChild() method will eventually call the measure() method of the View class:

/**

  • Ask one of the children of this view to measure itself, taking into
  • account both the MeasureSpec requirements for this view and its padding.
  • The heavy lifting is done in getChildMeasureSpec.
  • @param child The child to measure
  • @param parentWidthMeasureSpec The width requirements for this view
  • @param parentHeightMeasureSpec The height requirements for this view */ protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); //根据父ViewGroup的MeasureSpec,结合当前layoutParams设置的padding值会得到当前子View真实的测量宽度和高度 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height);
  • child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
In the basic View class, we can see that the onMeasure() method is implemented as:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

```java
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

The dimensions of a View are determined by calling getDefaultSize, which then sets the dimensions using setMeasuredDimension. The getDefaultSize method is as follows:

public static int getDefaultSize(int size, int measureSpec) { 
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode)
{
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

In the onMeasure method of a View, the first argument to getDefaultSize is getSuggestedMinimumHeight and the second argument is the size value from the MeasureSpec object. When the measurement mode is AT_MOST or EXACTLY, the value is taken from the MeasureSpec object's size; when it's UNSPECIFIED, the value is taken from getSuggestedMinimumHeight. This means that in basic View implementation, MATCH_PARENT and WRAP_CONTENT have the same effect, which is to take the maximum value obtained during measurement. Why is this the case? It's because when using WRAP_CONTENT, the actual width or height needs to be determined based on the content size, but a basic View doesn't have any specific content, so it can't set a specific value.

Note: When creating a custom View, you must override the onMeasure() method!

protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight,mBackground.getMinimumHeight());
}

The MeasureSpec of a child View is determined by its parent's MeasureSpec and its own LayoutParams. But how does the top-level DecorView get its MeasureSpec?

An Activity adds a DecorView to the window using the WindowManager, as shown in the following code:

// WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
...
root = new ViewRootImpl(view.getContext(), display);

view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
...
}

The performTraversals() method is started by the ViewRootImpl, and the key code is as follows:

// ViewRootImpl.java
private void performTraversals() {

I followed the rules you specified, preserving the original Markdown structure and not changing any links or URLs.

if (mWidth != frame.width() || mHeight != frame.height()) {
mWidth = frame.width();
mHeight = frame.height();
}
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {

case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

From the code, it can be seen that the initial MeasureSpec is directly initialized by the size of the window and the LayoutParams.

3.2 Layout Execution Details

Firstly, look at the performLayout() method executed in performTraversals():

// ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}

Then execute to the layout method of View:

// View.java
public void layout(int l, int t, int r, int b) {
...
// Set the position of the view's four vertices by setFrame method, i.e., the position of the view in its parent container
boolean changed = isLayoutModeOptical(mParent) ?
set OpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

...
onLayout(changed, l, t, r, b);
...
}
// Empty method. Subclasses of ViewGroup should override this method to implement the layout process for all View controls in ViewGroup.
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

The onLayout() method implemented by LinearLayout is referenced here. It calls different methods depending on whether it's in landscape or portrait mode:

protected void onlayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l,)
}
}

The implementation of layoutVertical() is as follows:

// Core source code for layoutVertical()
void layoutVertical(int left, int top, int right, int bottom) {
...
final int count = getVirtualChildCount();
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasureWidth();
final int childHeight = child.getMeasuredHeight();

final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
...
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}

childTop += lp.topMargin;
// Determine the position for the child element
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
// childTop will gradually increase, meaning that subsequent child elements will be placed in lower positions
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

i += getChildrenSkipCount(child,i)
}
}
}

private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}

I followed the rules you specified to ensure that the Markdown structure and contents remain unchanged. From the above logic, we can see that the onLayout() method implemented in ViewGroup mainly sets its own margin values within the parent ViewGroup's range and then calls the layout() method of child views to further arrange them. This recursive calling of layout methods achieves a hierarchical arrangement.

The implementation of View.java is empty, and we can refer to the onLayout method of TextView to see what it does:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mDeferScroll >= 0) {
int curs = mDeferScroll;
mDeferScroll = -1;
bringPointIntoView(Math.min(curs, mText.length()));
}
// Call auto-size after the width and height have been calculated.
autoSizeText();
}

In this method, some content arrangement and change operations are performed. However, we do not see the use of left-top-right-bottom coordinates because positioning operations are completed in the layout() method as follows:

//View.java
boolean changed = isLayoutModeOptical(mParent)? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (DBG) {
Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
+ right + "," + bottom + ")");
}

if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;

// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;

int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

// Invalidate our old position
invalidate(sizeChanged);

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

mPrivateFlags |= PFLAG_HAS_BOUNDS;


if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}

3.3 Drawing Process

private void performDraw() {
...
draw(fullRefrawNeeded);
...
}

private void draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffest, yOffset,
scalingRequired, dirty)) {
return;
}
...
}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo,
int xoff, int yoff, boolean scallingRequired, Rect dirty) {
...
mView.draw(canvas);
...
}

// The drawing process can be divided into six steps
public void draw(Canvas canvas) {
...
// Step 1: Draw the background of the View
drawBackground(canvas);

...
// Step 2: If necessary, save the canvas layer for fading preparation
saveCount = canvas.getSaveCount();
...
canvas.saveLayer(left, top, right, top + length, null, flags);

...
// Step 3: Draw the content of the View
onDraw(canvas);

...
// Step 4: Draw the child Views
dispatchDraw(canvas);

...

Note that I’ve followed the rules you specified and preserved the original Markdown structure, code blocks, and contents. Here is the translated Markdown content in English:

// Step 5: If needed, draw View's fading edges and restore layers
canvas.drawRect(left, top, right, top + length, p);
...
canvas.restoreToCount(saveCount);

...
// Step 6: Draw View's decorations (e.g. scroll bars)
onDrawForeground(canvas)
}

About the effect of setWillNotDraw:

// If a View does not need to draw any content, setting this flag to true will enable system optimization.
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
  • By default, View does not have this optimization flag enabled, but ViewGroup has it enabled by default.
  • When our custom view inherits from ViewGroup and itself does not possess drawing capabilities, we can enable this flag to facilitate subsequent system optimizations.
  • If a ViewGroup is known to require drawing content through onDraw, we need to explicitly disable the WILL_NOT_DRAW flag.

--

--

No responses yet