This is the 28th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021
In the blog with questions to learn measure measurement of View in Android, we learned the principle of measure measurement of View and the solutions to some problems encountered in our actual projects. In this blog we also according to the problem to learn layout and draw source code.
Lead question
- Why is the onMeasure() method called multiple times in a custom view? What is the purpose?
- What is the drawing step of draw() in a custom view?
Layout method source code analysis |
In the custom view/viewGroup, we usually need to rewrite the onLayout method for layout, in this method we can according to our needs to the view position or the position of the view in the viewGroup. View source code:
/** * Called from layout when this view should * assign a size and position to each of its children. * * Derived classes with children should override * this method and call layout on each of * their children. * @param changed This is a new size or position for this view * @param left Left position, relative to parent * @param top Top position, relative to parent * @param right Right position, relative to parent * @param bottom Bottom position, relative to parent */ protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }Copy the code
We see that the protected method is an empty method with no lines of code, which requires us to rewrite the method and implement our layout. So where does this method work? We need to look it up.
The layout() method calls our overwritten onLayout() method and implements our custom layout. Let’s look at the source code:
/** * Assign a size and position to a view and all of its * descendants * * <p>This is the second phase of the layout mechanism. * (The first is measuring). In this phase, each parent calls * layout on all of its children to position them. * This is typically done using the child measurements * that were stored in the measure pass().</p> * * <p>Derived classes should not override this method. * Derived classes with children should override * onLayout. In that method, they should * call layout on each of their children.</p> * * @param l Left position, relative to parent * @param t Top position, relative to parent * @param r Right position, relative to parent * @param b Bottom position, relative to parent */ @SuppressWarnings({"unchecked"}) public void layout(int l, int t, int r, Int b) {//PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT is a constant, If ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT)! If (mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT)! = 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; The layoutMode property of the view's parent is LAYOUT_MODE_OPTICAL_BOUNDS, and the set Frame() method is called. Boolean changed = isLayoutModeOptical(mParent)? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); // If the size of the view changes or we ask for a layout and start rearranging it, Call us / / override onLayout method if (changed | | (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) = = PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); // Change the flag bit mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; ListenerInfo li = mListenerInfo; if (li ! = null && li.mOnLayoutChangeListeners ! = null) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); }} // Change the status bit mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; }Copy the code
Assign the size and position of a view or its children to a layout method. Layout is the second stage of the layout mechanism. The first stage is measure. At this stage the parent calls the Layout method to place their child views. A derived class of a view cannot override the Layout method. The derived class needs to override the onLayout method, which is called in the onL method to set the position of the child view. Parameters: l, t, r, and b are relative to the parent View.
The code above is also clearly commented, so let’s focus on the changed variable, which identifies whether or not our view size has changed. Let’s see what the system does. Which brings us to this code:
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
Copy the code
There are two methods involved in this code, setOpticalFrame and setFrame, so let’s look at the setFrame method.
/** * Assign a size and position to this view. * * This is called from layout. * * @param left Left position, relative to parent * @param top Top position, relative to parent * @param right Right position, relative to parent * @param bottom Bottom position, relative to parent * @return true if the new size and position are different than the * previous ones * {@hide} */ protected boolean setFrame(int left, int top, int right, int bottom) { boolean changed = false; if (DBG) { Log.d("View", this + " View.setFrame(" + left + "," + top + "," + right + "," + bottom + ")"); } // Determine if the view position has changed by comparing the current value with the previously stored value. = left || mRight ! = right || mTop ! = top || mBottom ! = bottom) {// change the size of the view to true changed = true; // Remember our drawn bit int drawn = mPrivateFlags & PFLAG_DRAWN; Int oldWidth = mright-mleft; int oldWidth = mright-mleft; int oldHeight = mBottom - mTop; int newWidth = right - left; int newHeight = bottom - top; SizeChanged = (newWidth! = oldWidth) || (newHeight ! = oldHeight); // Invalidate our old position, refresh view Invalidate (sizeChanged); MLeft = left; mLeft = left; mTop = top; mRight = right; mBottom = bottom; / / set the value of l, t, b, r mRenderNode. SetLeftTopRightBottom (mLeft, mTop, mRight, mBottom); mPrivateFlags |= PFLAG_HAS_BOUNDS; SizeChange if (sizeChanged) {sizeChange(newWidth, newHeight, oldWidth, oldHeight); // If view size changes, call sizeChange if (sizeChanged) {sizeChange(newWidth, newHeight, oldWidth, oldHeight); } if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView ! = null) { // If we are visible, force the DRAWN bit to on so that // this invalidate will go through (at least to our parent). // This is because someone may have invalidated this view // before this call to setFrame came in, thereby clearing // the DRAWN bit. mPrivateFlags |= PFLAG_DRAWN; invalidate(sizeChanged); // parent display list may need to be recreated based on a change in the bounds // of any child invalidateParentCaches(); } // Reset drawn bit to original value (invalidate turns it off) mPrivateFlags |= drawn; mBackgroundSizeChanged = true; notifySubtreeAccessibilityStateChangedIfNeeded(); } return changed; }Copy the code
The principle of this implementation is to compare the l, R, T, b values with the last values to determine whether the view position size changes. If the view size changes, call the sizeChange() method. So let’s look at what’s going on in this method.
private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) { onSizeChanged(newWidth, newHeight, oldWidth, oldHeight); if (mOverlay ! = null) { mOverlay.getOverlayView().setRight(newWidth); mOverlay.getOverlayView().setBottom(newHeight); } rebuildOutline(); }Copy the code
Inside this method we call onSizeChanged(), which is the onSizeChanged() method we may need to override while we customize the view to handle our view size changes. So one of the things that I’m going to cover here is ViewOverlay class, which maybe a lot of people don’t know, but if you look at this tutorial, ViewOverlay is the top transparent layer of the view, and you can add something on top of that without affecting the layout structure. This layer is the same size as our interface and can be thought of as a two-dimensional space floating on the surface of the interface. SizeChange: ViewOverlay object = null; ViewOverlay object = parentView; For details, you can go to setRight and setBottom source code, as well as setTop and setLeft methods. Let’s just take a look at the source code of setRight, several others are similar.
/** * Sets the right position of this view relative to its parent. This method is meant to be called * by the layout system and should not generally be called otherwise, because the property * may be changed at any time by the layout. * * @param right The right of this view, in pixels. */ public final void setRight(int right) { if (right ! Final Boolean matrixIsIdentity = matrixIsIdentity = matrixIsIdentity = matrixIsIdentity = matrixIsIdentity = matrixIsIdentity = matrixIsIdentity = matrixIsIdentity = matrixIsIdentity = matrixIsIdentity = matrixIsIdentity = hasIdentityMatrix(); If (matrixIsIdentity) {// Check whether the attached parent is null if (mAttachInfo! = null) { int maxRight; If (right < mRight) {maxRight = mRight; } else { maxRight = right; } // Refresh the calculated new region invalidate(0, 0, maxright-mleft, mbottom-mtop); } } else { // Double-invalidation is necessary to capture view's old and new areas invalidate(true); } // calculate the oldWidth and height values int oldWidth = mright-mleft; int height = mBottom - mTop; mRight = right; mRenderNode.setRight(mRight); sizeChange(mRight - mLeft, height, oldWidth, height); if (! MatrixIsIdentity) {// if not identity matrix. Forced to refresh time, according to sizeChange after changing the value of the mPrivateFlags | = PFLAG_DRAWN; // force another invalidation with the new orientation invalidate(true); } mBackgroundSizeChanged = true; invalidateParentIfNeeded(); if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) == PFLAG2_VIEW_QUICK_REJECTED) { // View was rejected last time it was drawn by its parent; this may have changed invalidateParentIfNeeded(); }}}Copy the code
Draw source code analysis |
Overview Through the above analysis, we have completed the custom view in the measure and layout of the two major processes, the following is the start of the process of DRAW, used for us to draw our transaction. In the custom view, we need to override the onDraw method to draw:
/** * Implement this to do your drawing. * * @param canvas the canvas on which the background will be drawn */ protected void onDraw(Canvas canvas) { }Copy the code
Another empty method, “Implement this method to implement our drawing.” Draw by calling our onD method from the Draw method in View. Next we’ll look at the draw step in the draw method, okay? And when to call our onD method to draw.
/**
* Manually render this view (and all of its children) to the given Canvas.
* The view must have already done a full layout before this function is
* called. When implementing a view, implement
* {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
* If you do need to override this method, call the superclass version.
*
* @param canvas The Canvas to which the View is rendered.
*/
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
//标识该view的背景是否不透明
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
//如果是不透明,绘制背景色
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
//判断view是否设置水平边界渐变和垂直方向边界渐变,fadingEdge属性用来设置拉滚动条时边框渐变的放向。
//none(边框颜色不变),horizontal(水平方向颜色变淡),vertical(垂直方向颜色变淡)。
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
//如果没有设置水平和垂直方向渐变,则跳过第二步和第五步
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content,如果不透明,进行绘制
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children,分发draw函数,绘制child view
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars),绘制scrollbars
onDrawScrollBars(canvas);
//如果ViewOverlay不为空,进行分发绘制
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// we're done...
return;
}
/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false;
//上下左右的渐变长度
float topFadeStrength = 0.0f;
float bottomFadeStrength = 0.0f;
float leftFadeStrength = 0.0f;
float rightFadeStrength = 0.0f;
// Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft;
//判断是否需要paddingOffset
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
//计算left、right、top、bottom的位置
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
final ScrollabilityCache scrollabilityCache = mScrollCache;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
int length = (int) fadeHeight;
// clip the fade length if top and bottom fades overlap
// overlapping fades produce odd-looking artifacts
if (verticalEdges && (top + length > bottom - length)) {
length = (bottom - top) / 2;
}
// also clip horizontal fades if necessary
if (horizontalEdges && (left + length > right - length)) {
length = (right - left) / 2;
}
//如果垂直渐变,进行计算渐变的起始位置长度
if (verticalEdges) {
topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
drawTop = topFadeStrength * fadeHeight > 1.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
}
//如果水平渐变,进行计算渐变的起始长度
if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength * fadeHeight > 1.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength * fadeHeight > 1.0f;
}
saveCount = canvas.getSaveCount();
//获取
int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
matrix.setScale(1, fadeHeight * bottomFadeStrength);
matrix.postRotate(180);
matrix.postTranslate(left, bottom);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
matrix.setScale(1, fadeHeight * leftFadeStrength);
matrix.postRotate(-90);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
matrix.setScale(1, fadeHeight * rightFadeStrength);
matrix.postRotate(90);
matrix.postTranslate(right, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(right - length, top, right, bottom, p);
}
canvas.restoreToCount(saveCount);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
}
Copy the code
You can see the order of draw draw in the code:
- Draw the backgroud
- If possible save cavas layers for FADING.
- Draw the contents of the View, at which point we call our overridden onDraw method
- Draw the child view
- If possible, draw both the fading edges and the restore Layout layers.
- Draw decorations
Draw() source code for a simple comment, in the process of drawing view, two important methods are onDraw() and dispatchDraw() two methods. The onDraw() method is a custom view in which we write our own function according to the requirements. The dispatchDraw() method is how we draw the child view. View.java dispatchDraw() defaults to an empty implementation because it contains no subviews and ViewGroup overrides dispatchDraw() to draw its subviews. Normally applications should not overload dispatchDraw(). Its default implementation reflects the process of View system drawing. ViewGroup dispatchDraw() dispatchDraw()
@Override protected void dispatchDraw(Canvas canvas) { ... if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) { for (int i = 0; i < count; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() ! = null) { more |= drawChild(canvas, child, drawingTime); } } } else { for (int i = 0; i < count; i++) { final View child = children[getChildDrawingOrder(count, i)]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() ! = null) { more |= drawChild(canvas, child, drawingTime); }}}... }Copy the code
FLAG_USE_CHILD_DRAWING_ORDER = true if FLAG_USE_CHILD_DRAWING_ORDER = true if FLAG_USE_CHILD_DRAWING_ORDER = true if FLAG_USE_CHILD_DRAWING_ORDER = true The default drawing order is the order in which the subviews are added to the ViewGroup. We can override getChildDrawingOrder to change the default drawing order, which will affect the overlapping relationship between the subviews. The core procedure of drawChild() is to assign the appropriate Cavas clipping area to the child view. The size of the clipping area is determined by the Layout procedure, and the location of the clipping area depends on the scroll value and the current animation of the child view. Once the clipping area is set, the draw() function of the subview is called for specific drawing. If the subview contains the SKIP_DRAW flag, then just dispatchDraw() is called, skipping the drawing of the subview itself but drawing the word view that the view may contain.
At this point, basically completed the analysis of the core code. Continue to study, feeling analysis is not thorough, skill is not good!