About the author
Guo Xiaoxing, programmer and guitarist, is mainly engaged in the infrastructure of Android platform. Welcome to exchange technical questions. You can go to my Github to raise an issue or send an email to [email protected] to communicate with me.
For the first time in this series, see the introduction and the table of contents for more articles.
The article directories
- View lifecycle
- 2. Measurement process of View
- View layout process
- Four View drawing process
- 5 View event distribution mechanism
This class represents the basic building block for user interface components. A Viewoccupies a rectangular area on the screen and is
responsible for drawing and event handling.
A View is a rectangular area on the screen that is responsible for drawing the interface and handling touch events. It is an abstraction of controls at the interface level. All controls inherit from the View.
View is a relatively complex part of the Android display framework. First of all, its life cycle will change with the life cycle of the Activity. Mastering the life cycle of View is of great significance for us to customize the View. . Another aspect of the View from the ViewRoot performTraversals () began to experience the measure, layout, the draw three processes the final show in front of the user, when users click on the screen, click on the events with the Activity into the Window, The ViewGroup/View handles the distribution. Today we are going to analyze around these themes.
View lifecycle
There are a number of callback methods in a View that are called at different stages of the View lifecycle. The following methods are commonly used.
Let’s write a simple custom View to observe the lifecycle of the View and Activity.
public class CustomView extends View {
private static final String TAG = "View";
public CustomView(Context context) {
super(context);
Log.d(TAG, "CustomView()");
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
Log.d(TAG, "CustomView()");
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Log.d(TAG, "CustomView()");
}
/** * View calls */ when loading is complete in the XML file
@Override
protected void onFinishInflate(a) {
super.onFinishInflate();
Log.d(TAG, "View onFinishInflate()");
}
/** * call */ when measuring the size of the View and its children
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, "View onMeasure()");
}
/** * The layout View and its children are called */
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.d(TAG, "View onLayout() left = " + left + " top = " + top + " right = " + right + " bottom = " + bottom);
}
/** * call */ when the View size changes
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.d(TAG, "View onSizeChanged() w = " + w + " h = " + h + " oldw = " + oldw + " oldh = " + oldh);
}
/** * call */ when drawing a View and its children
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d(TAG, "View onDraw()");
}
/** ** is called when a physical keystroke event occurs
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
Log.d(TAG, "View onKeyDown() event = " + event.getAction());
return super.onKeyDown(keyCode, event);
}
/** ** is called when a physical keystroke event occurs
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
Log.d(TAG, "View onKeyUp() event = " + event.getAction());
return super.onKeyUp(keyCode, event);
}
/** ** is called when a touch event occurs
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "View onTouchEvent() event = " + event.getAction());
return super.onTouchEvent(event);
}
/** * call */ when View gets focus or loses focus
@Override
protected void onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
Log.d(TAG, "View onFocusChanged() gainFocus = " + gainFocus);
}
/** * call */ when the View window gets focus or loses focus
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
Log.d(TAG, "View onWindowFocusChanged() hasWindowFocus = " + hasWindowFocus);
}
/** ** is called when a View is associated with a window
@Override
protected void onAttachedToWindow(a) {
super.onAttachedToWindow();
Log.d(TAG, "View onAttachedToWindow()");
}
/** ** is called when the View is separated from the window
@Override
protected void onDetachedFromWindow(a) {
super.onDetachedFromWindow();
Log.d(TAG, "View onDetachedFromWindow()");
}
Call */ when the View's visibility changes
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
Log.d(TAG, "View onVisibilityChanged() visibility = " + visibility);
}
/** * call */ when the visibility of the View window changes
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
Log.d(TAG, "View onWindowVisibilityChanged() visibility = "+ visibility); }}Copy the code
The Activity and View life cycles change at a glance.
Activity create
Activity pause
Activity resume
Activity destory
Let’s summarize how the View’s declaration cycle changes with the Activity lifecycle.
What do we know about these lifecycle approaches? 🤔
In fact, these methods play a big role in customizing views, so let’s give a few examples.
Scenario 1: Get the View width and height when the Activity starts, but onCreate, onStart, and onResume fail to get the correct result. This is because in these methods of the Activity, the Viewed draw may not be complete, which can be obtained in the View lifecycle method.
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
int width = view.getMeasuredWidth();
intheight = view.getMeasuredHeight(); }}Copy the code
Scenario 2: When the Activity life cycle changes, the View also responds. Typically, the VideoView saves and restores progress.
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
//TODO do something if activity lifecycle changed if necessary
//Activity onResume()
if(visibility == VISIBLE){
}
//Activity onPause()
else{}}@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
//TODO do something if activity lifecycle changed if necessary
//Activity onResume()
if (hasWindowFocus) {
}
//Activity onPause()
else{}}Copy the code
Scenario 3: Release threads and resources
@Override
protected void onDetachedFromWindow(a) {
super.onDetachedFromWindow();
//TODO release resources, thread, animation
}Copy the code
2. Measurement process of View
A View is a rectangular area that has its own position, size, and margins.
Position of the View
View position: The View position is determined by the upper-left coordinate (getLeft(), getTop()). This coordinate is based on the upper-left corner of its parent View. The unit is Pixels.
The View size
View size: The size of a View is represented by two pairs of values. The getMeasuredWidth()/getMeasuredHeight() values indicate the desired size of the View in its parent View, which is available after the measure() method completes. The getWidth()/getHeight() values represent the actual size of the View on the screen, which is available after the draw() method completes.
The View of padding
View inner margin: The inner margin of a View is denoted by the padding, which indicates how far the View’s content is from the View’s edge. Get from the getPaddingXXX() method. Note that we need to handle the padding separately when we customize the View, otherwise it won’t work. We’ll cover this in the View Customization practice series.
The View from the outside
Margin: The margin of a View is denoted by margin, which shows how far the edge of the View is from its neighboring View.
The Measure procedure determines the width and height of the View. Once this procedure is complete, the width and height can usually be obtained by using getMeasuredWith()/getMeasuredHeight().
With these concepts in mind, let’s look at the detailed measurement process.
View’s measurement process seems complicated, but actually follows simple logic.
During measurement, the measure() method is called by the parent View. After some preparation and optimization in measure(), onMeasure() is called to perform the actual self-measurement. OnMeasure () differs from ViewGroup:
- View: The View calculates its size in onMeasure() and saves it;
- ViewGroup: In onMeasure(), the ViewGroup will call the measure() of all the child views to make them self-measure, calculate their actual size and position based on the expected size calculated by the child View and save. At the same time, it will calculate its own size based on the size and position of the child View and save it.
Before introducing the measurement process, let’s look at MeasureSpec, which is used to pass measurement requirements from parent View to child View. We know that the size of a View is ultimately determined by the child View’s LayoutParams in common with the parent View’s measurement requirements, which refer to MeasureSpec, which is a 32-bit int value.
- High 2 bits: SpecMode, measurement mode
- Lower 30 bits: SpecSize, the size in a particular measurement mode
There are three measurement modes:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// The parent View does not impose any restrictions on the child View
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// The parent View has already detected the exact size required by the View. The final size of the View is the value specified by SpecSize, which corresponds to the match_parent and specific value modes in LayoutParams
public static final int EXACTLY = 1 << MODE_SHIFT;
The parent View provides the child View with a maximum available size, and the child View ADAPTS to this size.
public static final int AT_MOST = 2 << MODE_SHIFT;
}Copy the code
When a View is measured, LayoutParams are converted to a Parent View’s MeasureSpec to determine the size of the View. When a View is measured, LayoutParams are converted to a Parent View’s MeasureSpec to determine the size of the View.
View MeasureSpec = MeasureSpec;
public abstract class ViewGroup extends View implements ViewParent.ViewManager {
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
returnMeasureSpec.makeMeasureSpec(resultSize, resultMode); }}Copy the code
The child’s size is determined by the parent’s spec, the padding of the parent’s size, and the child’s childDimension.
From the above approach, we can summarize the creation rules for a Common View’s MeasureSpec.
- When a View is a MeasureSpec, resultSize is the specified size and resultMode is a MeasureSpec.EXACTLY.
- When the width and height of the View is match_parent, and the parent is MeasureSpec.EXACTLY, then the View is also MeasureSpec.EXACTLY, and its size is the remaining space of the parent. When the parent is MeasureSpec.AT_MOST then the View is also MeasureSpec.AT_MOST and does not exceed the size of the parent.
- When the width and height of a View is wrAP_content, regardless of whether the parent is MeasureSpec.EXACTLY or MeasureSpec.AT_MOST, the View is always MeasureSpec.
With the MeasureSpec concept in mind, I can start analyzing the measurement process.
- For a top-level View (DecorView) its MeasureSpec is determined by the size of the window and its own LayoutParams.
- For a normal View, a MeasureSpec is determined by both the parent’s Measure and its own LayoutParams.
The drawing of a View will first call the measure() method of the View, which is used to measure the size of the View. The actual measurement work is completed by the onMeasure() of ziView. OnMeasure (int widthMeasureSpec, int heightMeasureSpec)
OnMeasure (int widthMeasureSpec, int heightMeasureSpec)
public class View implements Drawable.Callback.KeyEvent.Callback.AccessibilityEventSource {
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
// Set the width and height of the View
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if(optical ! = isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets();int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
//measureSpec refers to the size measured by the View
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
/ / MeasureSpec. UNSPECIFIED commonly used system of internal measurement process
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// We are mainly concerned with two cases, which return the measured size of the View
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
// If the View does not set the background, the android:minWidth property is returned, which can be 0
// If the View sets the background, then return android:minWidth and background minimum width maximum.
protected int getSuggestedMinimumHeight(a) {
int suggestedMinHeight = mMinHeight;
if(mBGDrawable ! =null) {
final int bgMinHeight = mBGDrawable.getMinimumHeight();
if(suggestedMinHeight < bgMinHeight) { suggestedMinHeight = bgMinHeight; }}returnsuggestedMinHeight; }}Copy the code
The onMeasure() method of a View is simple to implement. It calls setMeasuredDimension() to set the measured size of the View. The measured size is obtained by getDefaultSize().
If we customize the View directly by inheriting the View, we need to override the onMeasure() method and set the size of wrap_content. Why is that? 🤔
When LayoutParams is wrap_content, SpecMode is AT_MOST, while LayoutParams is wrap_content
EtDefaultSize (int size, int measureSpec); / / getDefaultSize(int size, int measureSpec) This specSize is the size that the parent View can currently use. If left unchecked, wrap_content is equivalent to match_parent.
How to deal with it? 🤔
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, "widthMeasureSpec = " + widthMeasureSpec + " heightMeasureSpec = " + heightMeasureSpec);
// Specify a set of default widths and heights, depending on what you want in wrap_cotent mode
// How big should the control be
int mWidth = 200;
int mHeight = 200;
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if(heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, mHeight); }}Copy the code
Note: If you want to customize a View and not override the onMeasure() method, you’ll see that match_parent and wrap_content have the same effect. In fact, TextView, ImageView and other system components have their own processing in wrAP_content, you can go to the source code.
So once we’ve looked at the View’s measure process, let’s look at the ViewGroup’s measure process. ViewGroup inherits from View and is an abstract class. It does not override onMeasure() because the measurement process varies between layout types, so onMeasure() is implemented by its subclasses.
Let’s look at an implementation of FrameLayout’s onMeasure() method.
FrameLayout. OnMeasure (int widthMeasureSpec, int heightMeasureSpec)
The implementation of the view.onmeasure () method is usually done by subclasses. In the case of the application window’s top-level View, DecorView, it inherits from FrameLayout. Let’s look at the implementation of framelayout.onMeasure ().
public class FrameLayout extends ViewGroup {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int count = getChildCount();
int maxHeight = 0;
int maxWidth = 0;
// Find rightmost and bottommost child
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if(mMeasureAllChildren || child.getVisibility() ! = GONE) { measureChildWithMargins(child, widthMeasureSpec,0, heightMeasureSpec, 0); maxWidth = Math.max(maxWidth, child.getMeasuredWidth()); maxHeight = Math.max(maxHeight, child.getMeasuredHeight()); }}// Account for padding too
maxWidth += mPaddingLeft + mPaddingRight + mForegroundPaddingLeft + mForegroundPaddingRight;
maxHeight += mPaddingTop + mPaddingBottom + mForegroundPaddingTop + mForegroundPaddingBottom;
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if(drawable ! =null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec),
resolveSize(maxHeight, heightMeasureSpec));
}
public static int resolveSize(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:
result = Math.min(size, specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
returnresult; }}Copy the code
You can see that this method mainly does the following:
- Call measureChildWithMargins() to measure the size of each sub-view and find the maximum height and width saved in maxWidth/maxHeigth.
- Add the padding, mPaddingLeft, mPaddingRight, mPaddingTop to maxWidth/maxHeigth. MForegroundPaddingLeft, mForegroundPaddingRight, mForegroundPaddingRight, MForegroundPaddingTop, mForegroundPaddingBottom represents the distance between the left, right, upper, and lower edges of the area enclosed by each sub-view of the current view and the left, upper, and lower edges of the foreground area of the current view. The final width and height is obtained after calculation.
- Whether the current view is set to a minimum width and height. If they are set, and they are larger than the maxWidth and maxHeight values previously computed, they are used as the width and height values for the current view.
- Whether the current view is set to foreground. If they are set, and they are larger than the maxWidth and maxHeight values previously computed, they are used as the width and height values for the current view.
- Once you have the correct dimensions, call the resolveSize() method to get MeasureSpec, and then call the setMeasuredDimension() method of the parent class to take them as the size of the current view.
MeasureSpec (resolveSize(int size, int measureSpec));
The two parameters to this method are: int size: the width/height calculated previously, and int measureSpec, which is the size specified by the parent view.
- MeasureSpec. UNSPECIFIED: take the size
- MeasureSpec.AT_MOST: size, specSize minimum
- MeasureSpec.EXACTLY: 取specSize
To generate the final size.
Once the Measure process is complete, we can use getMeasuredWidth() and getMeasuredHeight() to Measure the width and height of the View. However, in some cases, the system needs to Measure several times to determine the final width and height, so the width and height obtained in onMeasure() method may be incorrect. It is better to obtain the width and height of the View in onLayout() method.
View layout process
During layout, the layout() method is called by the parent View. In Layout(), it saves its position and size as passed in by the parent View and calls onLayout() to do the actual internal layout. For onLayout(), View and ViewGroup differ:
- View: Since there are no child views, the View’s onLayout() does nothing.
- ViewGroup: In onLayout(), a ViewGroup calls the Layout() method of all its child views, passing them their dimensions and positions, and letting them complete their own internal layout.
The layout() method is used to determine the position of the View itself, and the onLayout() method is used to determine the position of child elements.
public class View implements Drawable.Callback.KeyEvent.Callback.AccessibilityEventSource {
public void layout(int l, int t, int r, int b) {
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;
//1 Call setFrame() to set the Ed position of the View's four vertices
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//2 Call onLayout() to locate the View child elements
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this); }}else {
mRoundScrollbarRenderer = null;
}
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); } } } mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; }}Copy the code
Key point 1: view.invalidate ()
public class View implements Drawable.Callback.KeyEvent.Callback.AccessibilityEventSource {
public void invalidate(a) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);
}
// Check whether the DRAWN bit of mPrivateFlags and HAS_BOUNDS are set to 1 to indicate that the last requested UI drawing has been completed before a new UI drawing operation can be performed
if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {
// Whether the DRAWN bit of mPrivateFlags and HAS_BOUNDS are set to 0
mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;
final ViewParent p = mParent;
final AttachInfo ai = mAttachInfo;
if(p ! =null&& ai ! =null) {
final Rect r = ai.mTmpInvalRect;
r.set(0.0, mRight - mLeft, mBottom - mTop);
// Don't call invalidate -- we don't want to internally scroll
// our own bounds
p.invalidateChild(this, r); }}}}Copy the code
This method checks whether the DRAWN bit of mPrivateFlags and HAS_BOUNDS are set to 1, indicating that the last REQUESTED UI draw has been completed, before a new UI draw can be performed. Before a new UI draw is performed, the DRAWN bit and HAS_BOUNDS are set to 0. Then call ViewParent. InvalidateChild () method to complete the drawing operation, this ViewParent point to an ViewRoot object.
OnLayout (Boolean changed, int left, int top, int right, int bottom)
The implementation of onLayout depends on the specific layout, so View/ViewGroup does not implement this method. Let’s look at the implementation of FrameLayout.
public class FrameLayout extends ViewGroup {
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int count = getChildCount();
final int parentLeft = mPaddingLeft + mForegroundPaddingLeft;
final int parentRight = right - left - mPaddingRight - mForegroundPaddingRight;
final int parentTop = mPaddingTop + mForegroundPaddingTop;
final int parentBottom = bottom - top - mPaddingBottom - mForegroundPaddingBottom;
mForegroundBoundsChanged = true;
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 = parentLeft;
int childTop = parentTop;
final int gravity = lp.gravity;
if(gravity ! = -1) {
final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (horizontalGravity) {
case Gravity.LEFT:
childLeft = parentLeft + lp.leftMargin;
break;
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = parentRight - width - lp.rightMargin;
break;
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; } } child.layout(childLeft, childTop, childLeft + width, childTop + height); }}}}Copy the code
Let’s first explain what the variables in this function mean.
- Int left, int top, int right, int bottom: Describes the margin of the current view, that is, its margin from the parent window.
- MPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom: Describes the inner margin of the current view.
With these parameters, we can get the area in which the current view’s children can be laid out.
The method then iterates through each of its child views and retrieves its upper-left coordinate position: childLeft, childTop. The two positions are calculated based on gravity. Finally, the layout() method of the child View is called to loop through the layout until all the layouts are complete.
View drawing process
The Draw process eventually draws the View on the screen.
Drawing starts with viewrot.draw (), which first creates a canvas, then draws the Android UI on the canvas, and passes the canvas’s contents to the SurfaceFlinger service for rendering.
Viewroot. draw(Boolean fullRedrawNeeded)
public final class ViewRoot extends Handler implements ViewParent.View.AttachInfo.Callbacks {
private void draw(boolean fullRedrawNeeded) {
// Surface is the drawing surface used to manipulate the application window
Surface surface = mSurface;
if (surface == null| |! surface.isValid()) {return;
}
if(! sFirstDrawComplete) {synchronized (sFirstDrawHandlers) {
sFirstDrawComplete = true;
for (int i=0; i<sFirstDrawHandlers.size(); i++) {
post(sFirstDrawHandlers.get(i));
}
}
}
scrollToRectOrFocus(null.false);
if (mAttachInfo.mViewScrollChanged) {
mAttachInfo.mViewScrollChanged = false;
mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
}
int yoff;
// Calculate whether the window is in scroll state
final booleanscrolling = mScroller ! =null && mScroller.computeScrollOffset();
if (scrolling) {
yoff = mScroller.getCurrY();
} else {
yoff = mScrollY;
}
if(mCurScrollY ! = yoff) { mCurScrollY = yoff; fullRedrawNeeded =true;
}
// Describes whether the window is requesting size scaling
float appScale = mAttachInfo.mApplicationScale;
boolean scalingRequired = mAttachInfo.mScalingRequired;
// Describe the dirty area of the window, that is, the area that needs to be redrawn
Rect dirty = mDirty;
if(mSurfaceHolder ! =null) {
// The app owns the surface, we won't draw.
dirty.setEmpty();
return;
}
/ / used to describe whether need to use OpenGL interface to map the user interface (UI), when the application window flag is equal to the WindowManager. LayoutParams. MEMORY_TYPE_GPU
// The OpenGL interface is used to draw the UI
if (mUseGL) {
if(! dirty.isEmpty()) { Canvas canvas = mGlCanvas;if(mGL ! =null&& canvas ! =null) {
mGL.glDisable(GL_SCISSOR_TEST);
mGL.glClearColor(0.0.0.0);
mGL.glClear(GL_COLOR_BUFFER_BIT);
mGL.glEnable(GL_SCISSOR_TEST);
mAttachInfo.mDrawingTime = SystemClock.uptimeMillis();
mAttachInfo.mIgnoreDirtyState = true;
mView.mPrivateFlags |= View.DRAWN;
int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
try {
canvas.translate(0, -yoff);
if(mTranslator ! =null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired
? DisplayMetrics.DENSITY_DEVICE : 0);
mView.draw(canvas);
if(Config.DEBUG && ViewDebug.consistencyCheckEnabled) { mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING); }}finally {
canvas.restoreToCount(saveCount);
}
mAttachInfo.mIgnoreDirtyState = false;
mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
checkEglErrors();
if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) {
int now = (int)SystemClock.elapsedRealtime();
if(sDrawTime ! =0) { nativeShowFPS(canvas, now - sDrawTime); } sDrawTime = now; }}}// If the window is in a scrolling state, the application window needs to be fully redrawn immediately, calling the scheduleTraversals() method
if (scrolling) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
return;
}
If yes, set the dirty area of the window to the entire window area, indicating that the entire window curve cloud needs to be repainted
if (fullRedrawNeeded) {
mAttachInfo.mIgnoreDirtyState = true;
dirty.union(0.0, (int) (mWidth * appScale + 0.5 f), (int) (mHeight * appScale + 0.5 f));
}
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(TAG, "Draw " + mView + "/"
+ mWindowAttributes.getTitle()
+ ": dirty={" + dirty.left + "," + dirty.top
+ "," + dirty.right + "," + dirty.bottom + "} surface="
+ surface + " surface.isValid()=" + surface.isValid() + ", appScale:" +
appScale + ", width=" + mWidth + ", height=" + mHeight);
}
if(! dirty.isEmpty() || mIsAnimating) { Canvas canvas;try {
int left = dirty.left;
int top = dirty.top;
int right = dirty.right;
int bottom = dirty.bottom;
// Call surface.lockCanvas () to create the canvas
canvas = surface.lockCanvas(dirty);
if(left ! = dirty.left || top ! = dirty.top || right ! = dirty.right || bottom ! = dirty.bottom) { mAttachInfo.mIgnoreDirtyState =true;
}
// TODO: Do this in native
canvas.setDensity(mDensity);
} catch (Surface.OutOfResourcesException e) {
Log.e(TAG, "OutOfResourcesException locking surface", e);
// TODO: we should ask the window manager to do something!
// for now we just do nothing
return;
} catch (IllegalArgumentException e) {
Log.e(TAG, "IllegalArgumentException locking surface", e);
// TODO: we should ask the window manager to do something!
// for now we just do nothing
return;
}
try {
if(! dirty.isEmpty() || mIsAnimating) {long startTime = 0L;
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(TAG, "Surface " + surface + " drawing to bitmap w="
+ canvas.getWidth() + ", h=" + canvas.getHeight());
//canvas.drawARGB(255, 255, 0, 0);
}
if (Config.DEBUG && ViewDebug.profileDrawing) {
startTime = SystemClock.elapsedRealtime();
}
// If this bitmap's format includes an alpha channel, we
// need to clear it before drawing so that the child will
// properly re-composite its drawing on a transparent
// background. This automatically respects the clip/dirty region
// or
// If we are applying an offset, we need to clear the area
// where the offset doesn't appear to avoid having garbage
// left in the blank areas.
if(! canvas.isOpaque() || yoff ! =0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
dirty.setEmpty();
mIsAnimating = false;
mAttachInfo.mDrawingTime = SystemClock.uptimeMillis();
mView.mPrivateFlags |= View.DRAWN;
if (DEBUG_DRAW) {
Context cxt = mView.getContext();
Log.i(TAG, "Drawing: package:" + cxt.getPackageName() +
", metrics=" + cxt.getResources().getDisplayMetrics() +
", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo());
}
int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
try {
canvas.translate(0, -yoff);
if(mTranslator ! =null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired
? DisplayMetrics.DENSITY_DEVICE : 0);
mView.draw(canvas);
} finally {
mAttachInfo.mIgnoreDirtyState = false;
canvas.restoreToCount(saveCount);
}
if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) {
mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING);
}
if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) {
int now = (int)SystemClock.elapsedRealtime();
if(sDrawTime ! =0) {
nativeShowFPS(canvas, now - sDrawTime);
}
sDrawTime = now;
}
if (Config.DEBUG && ViewDebug.profileDrawing) {
EventLog.writeEvent(60000, SystemClock.elapsedRealtime() - startTime); }}}finally {
/ / UI, after call urface. UnlockCanvasAndPost (canvas) S to request SurfaceFlinger rendering UIsurface.unlockCanvasAndPost(canvas); }}if (LOCAL_LOGV) {
Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost");
}
if (scrolling) {
mFullRedrawNeeded = true; scheduleTraversals(); }}}Copy the code
This function does the following:
- Call Scroller.com puteScrollOffset () method to calculate whether application is in sliding condition, and to obtain a real-time application window on the Y axis sliding yoff position.
- Determine whether the window needs to be scaled based on the data described in AttachInfo.
- The React mDirty member variable describes the size of the dirty area of the window. The dirty area refers to the area of the window that needs to be completely redrawn.
- According to the member variables Boolean mUserGL determine whether need to use OpenGL interface to map the UI, when an application window flag is equal to the WindowManager. LayoutParams. MEMORY_TYPE_GPU OpenGL interface has to be used to draw the UI.
- Call Surface.lockCanvas() to create the canvas. After the UI is drawn, Call again urface. UnlockCanvasAndPost (canvas) S to request SurfaceFlinger rendering UI
Note: the Surface object here corresponds to the Surface object in the C++ layer. The real function is in the C++ layer. We will further analyze the implementation of the C++ layer in a future article.
View.draw(Canvas Canvas)
public class View implements Drawable.Callback.KeyEvent.Callback.AccessibilityEventSource {
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
1 Check whether the value of DIRTY_OPAQUE is 1. If yes, a subview of the current view requested an opaque UI rendering operation
/ view/view will quilt cover 2 if mAttachInfo mIgnoreDirtyState = true said to ignore this flag
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null| |! mAttachInfo.mIgnoreDirtyState);// Set DIRTY_MASK and DRAWN to 1 to start drawing
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | 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) {// Draw the background of the current view
final Drawable background = mBGDrawable;
if(background ! =null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0.0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else{ canvas.translate(scrollX, scrollY); background.draw(canvas); canvas.translate(-scrollX, -scrollY); }}}// Check if you can skip steps 2 and 5, that is, draw the variable with FADING_EDGE_HORIZONTAL == 1 indicating that it is horizontal
FADING_EDGE_VERTICAL == 1 indicates that it is in the vertical sliding state, then the horizontal border gradient effect needs to be drawn
// The vertical border gradient needs to be drawn.
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
booleanhorizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) ! =0;
booleanverticalEdges = (viewFlags & FADING_EDGE_VERTICAL) ! =0;
if(! verticalEdges && ! horizontalEdges) {// When the window content is not transparent, it is not necessary to draw
// Step 3, draw the content
if(! dirtyOpaque) onDraw(canvas);// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(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) */
// Checking for disrepair requires saving the stack state of a canvas described by the canvas parameter and creating additional layers to draw the current view
// Border gradient effect when sliding
boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false;
float topFadeStrength = 0.0 f;
float bottomFadeStrength = 0.0 f;
float leftFadeStrength = 0.0 f;
float rightFadeStrength = 0.0 f;
// Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft;
int paddingTop = mPaddingTop;
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
paddingTop += getTopPaddingOffset();
}
// Represents the content area that the current view can be used to draw, excluding built-in and extended inner margins
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + paddingTop;
int bottom = top + mBottom - mTop - mPaddingBottom - paddingTop;
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
final ScrollabilityCache scrollabilityCache = mScrollCache;
int length = scrollabilityCache.fadingEdgeLength;
// 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.0 f, Math.min(1.0 f, getTopFadingEdgeStrength()));
drawTop = topFadeStrength >= 0.0 f;
bottomFadeStrength = Math.max(0.0 f, Math.min(1.0 f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength >= 0.0 f;
}
if (horizontalEdges) {
leftFadeStrength = Math.max(0.0 f, Math.min(1.0 f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength >= 0.0 f;
rightFadeStrength = Math.max(0.0 f, Math.min(1.0 f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength >= 0.0 f;
}
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);
// Draws a gradient effect for the top, bottom, left, and right borders of the current view
// Step 5, draw the fade effect and restore layers
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
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);
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);
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);
canvas.drawRect(right - length, top, right, bottom, p);
}
canvas.restoreToCount(saveCount);
// Draws a scroll bar for the current view
// Step 6, draw decorations (scrollbars)onDrawScrollBars(canvas); }}Copy the code
This method mainly accomplishes the following:
- Draws the background of the current view
- Save the state of the current canvas and create an extra highlight on the current canvas so that you can then draw the border gradient effect of the view as it slides.
- Draws the contents of the current view
- Draws a child view of the current view
- Draws the border gradient effect of the current view as it slides
- Draws the scroll bar for the current view
Viewgroup.dispatchdraw (Canvas Canvas)
DispatchDraw is used to loop the child View.
public abstract class ViewGroup extends View implements ViewParent.ViewManager {
protected void dispatchDraw(Canvas canvas) {
// Number of subviews of the current view
final int count = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
if((flags & FLAG_RUN_ANIMATION) ! =0 && canAnimate()) {
final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;
for (int i = 0; i < count; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
final LayoutParams params = child.getLayoutParams();
attachLayoutAnimationParameters(child, params, i, count);
bindLayoutAnimation(child);
if (cache) {
child.setDrawingCacheEnabled(true);
child.buildDrawingCache(true); }}}final LayoutAnimationController controller = mLayoutAnimationController;
if (controller.willOverlap()) {
mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
}
controller.start();
FLAG_RUN_ANIMATION == 1
mGroupFlags &= ~FLAG_RUN_ANIMATION;
mGroupFlags &= ~FLAG_ANIMATION_DONE;
if (cache) {
mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;
}
// Notify the animation listener that the animation is displayed
if(mAnimationListener ! =null) { mAnimationListener.onAnimationStart(controller.getAnimation()); }}int saveCount = 0;
// if CLIP_TO_PADDING_MASK! If = 1, the canvas parameter describes the clipping area of the canvas, which does not contain the inner margin of the current view group
final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
if (clipToPadding) {
saveCount = canvas.save();
// Crop the canvas
canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
mScrollX + mRight - mLeft - mPaddingRight,
mScrollY + mBottom - mTop - mPaddingBottom);
}
// We will draw our child's animation, let's reset the flag
mPrivateFlags &= ~DRAW_ANIMATION;
mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
boolean more = false;
final long drawingTime = getDrawingTime();
// If FLAG_USE_CHILD_DRAWING_ORDER == 0, the child views are drawn in the order they are drawn in the children array
// Otherwise, call getChildDrawingOrder to determine the drawing order
if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
for (int i = 0; i < count; i++) {
final View child = children[i];
// If the subview is visible, start drawing the subview
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); }}}//mDisappearingChildren is used to save the disappearing subviews, which also need to be drawn
// Draw any disappearing views that have animations
if(mDisappearingChildren ! =null) {
final ArrayList<View> disappearingChildren = mDisappearingChildren;
final int disappearingCount = disappearingChildren.size() - 1;
// Go backwards -- we may delete as animations finish
for (int i = disappearingCount; i >= 0; i--) {
finalView child = disappearingChildren.get(i); more |= drawChild(canvas, child, drawingTime); }}if (clipToPadding) {
canvas.restoreToCount(saveCount);
}
// mGroupFlags might have been updated by drawChild()
flags = mGroupFlags;
// If FLAG_INVALIDATE_REQUIRED == 1, a redrawing is required
if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
invalidate();
}
// Notify the animation listener that the animation has ended
if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0&& mLayoutAnimationController.isDone() && ! more) {// We want to erase the drawing cache and notify the listener after the
// next frame is drawn because one extra invalidate() is caused by
// drawChild() after the animation is over
mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
final Runnable end = new Runnable() {
public void run(a) { notifyAnimationListener(); }}; post(end); }}}Copy the code
DispatchDraw is used to loop over a child View, which does the following:
- FLAG_RUN_ANIMATION == 1 checks if an animation is required to display and notifies the animation listener that the animation has started.
- If FLAG_USE_CHILD_DRAWING_ORDER == 0, then the child views are drawn in the order in which they are drawn in the children array otherwise you need to call getChildDrawingOrder to determine the drawing order, The final call to drawChild() completes the drawing of the subview.
- Determines whether a redraw is required and notifies the animation listener that the animation has ended.
ViewGroup. DrawChild (Canvas, View child, long drawingTime)
ViewGroup. DrawChild (Canvas Canvas, View Child, long drawingTime) is used to draw subviews.
public abstract class ViewGroup extends View implements ViewParent.ViewManager {
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
// Indicates whether the child is still displaying the animation
boolean more = false;
// Get the drawing area and flag bit of the subview
final int cl = child.mLeft;
final int ct = child.mTop;
final int cr = child.mRight;
final int cb = child.mBottom;
final int flags = mGroupFlags;
if ((flags & FLAG_CLEAR_TRANSFORMATION) == FLAG_CLEAR_TRANSFORMATION) {
if(mChildTransformation ! =null) {
mChildTransformation.clear();
}
mGroupFlags &= ~FLAG_CLEAR_TRANSFORMATION;
}
// Get the subview's transformation matrix transformToApply
Transformation transformToApply = null;
// Get the animation of the subview
final Animation a = child.getAnimation();
boolean concatMatrix = false;
if(a ! =null) {
if (mInvalidateRegion == null) {
mInvalidateRegion = new RectF();
}
final RectF region = mInvalidateRegion;
final boolean initialized = a.isInitialized();
if(! initialized) { a.initialize(cr - cl, cb - ct, getWidth(), getHeight()); a.initializeInvalidateRegion(0.0, cr - cl, cb - ct);
child.onAnimationStart();
}
if (mChildTransformation == null) {
mChildTransformation = new Transformation();
}
// If the subview needs to play the animation, call getTransformation to start executing the animation. If the animation needs to continue, more == true and return the subview's
// mChildTransformation
more = a.getTransformation(drawingTime, mChildTransformation);
transformToApply = mChildTransformation;
concatMatrix = a.willChangeTransformationMatrix();
if (more) {
if(! a.willChangeBounds()) {if ((flags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) ==
FLAG_OPTIMIZE_INVALIDATE) {
mGroupFlags |= FLAG_INVALIDATE_REQUIRED;
} else if ((flags & FLAG_INVALIDATE_REQUIRED) == 0) {
// The child need to draw an animation, potentially offscreen, so
// make sure we do not cancel invalidate requestsmPrivateFlags |= DRAW_ANIMATION; invalidate(cl, ct, cr, cb); }}else {
a.getInvalidateRegion(0.0, cr - cl, cb - ct, region, transformToApply);
// The child need to draw an animation, potentially offscreen, so
// make sure we do not cancel invalidate requests
mPrivateFlags |= DRAW_ANIMATION;
final int left = cl + (int) region.left;
final int top = ct + (int) region.top;
invalidate(left, top, left + (int) region.width(), top + (int) region.height()); }}}/ / if FLAG_SUPPORT_STATIC_TRANSFORMATIONS = = 1, call getChildStaticTransformation () method to check whether is set a child view
// Transformation matrix, if set, i.e. HasTransform == true, mChildTransformation is the transformation matrix required by the child view
else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) ==
FLAG_SUPPORT_STATIC_TRANSFORMATIONS) {
if (mChildTransformation == null) {
mChildTransformation = new Transformation();
}
final boolean hasTransform = getChildStaticTransformation(child, mChildTransformation);
if (hasTransform) {
final inttransformType = mChildTransformation.getTransformationType(); transformToApply = transformType ! = Transformation.TYPE_IDENTITY ? mChildTransformation :null; concatMatrix = (transformType & Transformation.TYPE_MATRIX) ! =0; }}// Set the DRAWN bit of mPrivateFlags to 1 to indicate that it will start drawing.
// Sets the flag as early as possible to allow draw() implementations
// to call invalidate() successfully when doing animations
child.mPrivateFlags |= DRAWN;
if(! concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) && (child.mPrivateFlags & DRAW_ANIMATION) ==0) {
return more;
}
// Call computeScroll() to calculate the sliding position of the subview
child.computeScroll();
final int sx = child.mScrollX;
final int sy = child.mScrollY;
boolean scalingRequired = false;
Bitmap cache = null;
// If FLAG_CHILDREN_DRAWN_WITH_CACHE or FLAG_CHILDREN_DRAWN_WITH_CACHE is 1, it is buffered
It buffers its UI in a Bitmap, which can be obtained by calling getDrawingCache().
if((flags & FLAG_CHILDREN_DRAWN_WITH_CACHE) == FLAG_CHILDREN_DRAWN_WITH_CACHE || (flags & FLAG_ALWAYS_DRAWN_WITH_CACHE) == FLAG_ALWAYS_DRAWN_WITH_CACHE) { cache = child.getDrawingCache(true);
if(mAttachInfo ! =null) scalingRequired = mAttachInfo.mScalingRequired;
}
final boolean hasNoCache = cache == null;
// Sets the offset, Alpha channel, and clipping region of the child view
final int restoreTo = canvas.save();
if (hasNoCache) {
canvas.translate(cl - sx, ct - sy);
} else {
canvas.translate(cl, ct);
if (scalingRequired) {
// mAttachInfo cannot be null, otherwise scalingRequired == false
final float scale = 1.0 f/ mAttachInfo.mApplicationScale; canvas.scale(scale, scale); }}float alpha = 1.0 f;
if(transformToApply ! =null) {
if (concatMatrix) {
int transX = 0;
int transY = 0;
if (hasNoCache) {
transX = -sx;
transY = -sy;
}
// Undo the scroll translation, apply the transformation matrix,
// then redo the scroll translate to get the correct result.
canvas.translate(-transX, -transY);
canvas.concat(transformToApply.getMatrix());
canvas.translate(transX, transY);
mGroupFlags |= FLAG_CLEAR_TRANSFORMATION;
}
alpha = transformToApply.getAlpha();
if (alpha < 1.0 f) {
mGroupFlags |= FLAG_CLEAR_TRANSFORMATION;
}
if (alpha < 1.0 f && hasNoCache) {
final int multipliedAlpha = (int) (255 * alpha);
if(! child.onSetAlpha(multipliedAlpha)) { canvas.saveLayerAlpha(sx, sy, sx + cr - cl, sy + cb - ct, multipliedAlpha, Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); }else{ child.mPrivateFlags |= ALPHA_SET; }}}else if ((child.mPrivateFlags & ALPHA_SET) == ALPHA_SET) {
child.onSetAlpha(255);
}
// If FLAG_CLIP_CHILDREN == 1, the clipping area of the subview needs to be set
if ((flags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
if (hasNoCache) {
canvas.clipRect(sx, sy, sx + (cr - cl), sy + (cb - ct));
} else {
if(! scalingRequired) { canvas.clipRect(0.0, cr - cl, cb - ct);
} else {
canvas.clipRect(0.0, cache.getWidth(), cache.getHeight()); }}}// Draw the UI of the subview
if (hasNoCache) {
// Fast path for layouts with no backgrounds
if ((child.mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
child.mPrivateFlags &= ~DIRTY_MASK;
child.dispatchDraw(canvas);
} else{ child.draw(canvas); }}else {
final Paint cachePaint = mCachePaint;
if (alpha < 1.0 f) {
cachePaint.setAlpha((int) (alpha * 255));
mGroupFlags |= FLAG_ALPHA_LOWER_THAN_ONE;
} else if ((flags & FLAG_ALPHA_LOWER_THAN_ONE) == FLAG_ALPHA_LOWER_THAN_ONE) {
cachePaint.setAlpha(255);
mGroupFlags &= ~FLAG_ALPHA_LOWER_THAN_ONE;
}
if (Config.DEBUG && ViewDebug.profileDrawing) {
EventLog.writeEvent(60003, hashCode());
}
canvas.drawBitmap(cache, 0.0 f.0.0 f, cachePaint);
}
// Restore the stack state of the canvas so that after painting the UI of the current child view, you can continue painting the UI of other child views
canvas.restoreToCount(restoreTo);
if(a ! =null && !more) {
child.onSetAlpha(255);
finishAnimatingView(child, a);
}
returnmore; }}Copy the code
ViewGroup. DrawChild (Canvas Canvas, View Child, long drawingTime) is used to draw a subview. It does the following:
1 obtains the drawing area and flag bit of the subview. 2 Obtains the transformation matrix transformToApply of the subview. There are two cases:
- If the subview needs to play the animation, call getTransformation to start executing the animation, and if the animation needs to continue, more == true, and return the subview’s change matrix mChildTransformation
- If FLAG_SUPPORT_STATIC_TRANSFORMATIONS = = 1, call getChildStaticTransformation () method checks whether a child view is set a transformation matrix, if set, If hasTransform == true, mChildTransformation is the transformation matrix required by the child view
3 If FLAG_CHILDREN_DRAWN_WITH_CACHE or FLAG_CHILDREN_DRAWN_WITH_CACHE is 1, it draws in a buffer and buffers its UI in a Bitmap. The Bitmap can be obtained by calling the getDrawingCache() method. 4 Sets the offset, Alpha channel, and clipping region of the child view.
5. Draw the UI of the subview, which is divided into two cases:
- If it draws in a non-buffered fashion, if SKIP_DRAW == 1, then it needs to skip the current subview and draw its own, or else draw its view first and then its child view. Drawing itself is done using the draw() function, and drawing its subviews is done using dispatchDraw().
- In the case of buffered drawing, you only need to draw the last cached Bitmap cache onto the canvas
6 Restore the stack state of the canvas so that after drawing the UI of the current child view, you can continue drawing the UI of other child views.
conclusion
Now that the Android application window rendering process is analyzed, let’s summarize it again.
- The rendering process of the Android application view, the measurement process is used to determine the size of the view, the layout process is used to determine the position of the view, and the drawing process is finally used to draw the view on the application window.
- The Android application window UI is first drawn on a canvas using the Skia graphics library API, and is actually drawn on a graphics buffer inside the canvas, which is eventually handed over to the SurfaceFlinger service, The SurfaceFlinger service then uses the OpenGL Graphics library API to render this graphics buffer into the hardware frame buffer.
5 View event distribution mechanism
Before we introduce the event distribution mechanism of a View, we need to understand two concepts.
- MotionEvent: The object used to represent various events in Android, such as ACTION_DOWN, ACTION_MOVE, etc., we can also use it to get the coordinates of the event, getX/getY gets the coordinates relative to the upper left corner of the current View, GetRawX /getRawY gets the coordinates relative to the upper-left corner of the screen.
- TouchSlop: The minimum sliding distance recognized by the system. The value can be obtained from the viewConfiguration.get (context).getScaledTouchSlop() method.
Now let’s look at the event distribution mechanism in a View. In a nutshell, it can be represented by the following code:
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
// The parent View decides whether to intercept the event
if(onInterceptTouchEvent(event)){
// The parent View calls onTouchEvent(event) to consume the event
consume = onTouchEvent(event);
}else{
// Call the child's dispatchTouchEvent(event) method to continue distributing events
consume = child.dispatchTouchEvent(event);
}
return consume;
}Copy the code
Let’s take a closer look at event distribution in each scenario.
5.1 Event distribution of the Activity
When a click event occurs, the event is first passed to the Activity, which first processes the event to its Window by calling the superDispatchTouchEvent() method.
By looking at the chain of calls to the superDispatchTouchEvent() method, we can see the order in which events are delivered:
- PhoneWinodw.superDispatchTouchEvent()
- DecorView.dispatchTouchEvent(event)
- ViewGroup.dispatchTouchEvent(event)
The event is passed layer by layer to the ViewGroup, and we’ll say that if the superDispatchTouchEvent() method returns false, that is, the event was not processed, It continues to call the Activity’s onTouchEvent(EV) method to handle the event. As you can see, the Activity’s onTouchEvent(EV) has the lowest priority in event processing.
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2.Window.Callback.KeyEvent.Callback.OnCreateContextMenuListener.ComponentCallbacks2.Window.OnWindowDismissedCallback.WindowControllerCallback {
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
returnonTouchEvent(ev); }}Copy the code
5.2 Event Distribution of the ViewGroup
As a View container, the ViewGroup needs to consider whether its child views handled the event, specifically:
- If a ViewGroup intercepts an event, its onInterceptTouchEvent() returns true, then the event is handled by the ViewGroup, If the ViewGroup calls setOnTouchListener() the interface’s onTouch() method will be called otherwise the onTouchEvent() method will be called.
- If the ViewGroup does not intercept the event, then the event is passed to its child views, whose dispatchTouchEvent() is called. The view.dispatchTouchEvent () process was analyzed earlier.
public abstract class ViewGroup extends View implements ViewParent.ViewManager {
public boolean dispatchTouchEvent(MotionEvent ev) {...boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Reset to the initial state whenever an ACTION_DOWN event comes in
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
final boolean intercepted;
// MotionEvent.action_down events are always intercepted by the ViewGroup
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
/ / 1. To determine whether to allow ViewGroup intercept besides ACTION_DOWN of other events, through requestDisallowInterceptTouchEvent set () method
//FLAG_DISALLOW_INTERCEPT
final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
if(! disallowIntercept) {//2. Use the onInterceptTouchEvent(ev) method to check whether to intercept the event
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false; }}else {
// If mFirstTouchTarget == null, it is not accepted
intercepted = true;
}
// The ViewGroup stores its child views in a list, with mFirstTouchTarget representing the first one in the list
// The clicked child View
if(intercepted || mFirstTouchTarget ! =null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final booleansplit = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) ! =0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if(! canceled && ! intercepted) { ...if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null&& childrenCount ! =0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
//3. When a ViewGroup stops intercepting events, they are propagated down to its child views for processing.
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//4. To judge whether a sub-view can accept the click event, there are two criteria: ① Whether a sub-view can obtain the focus
// Check whether the click coordinates fall within the subview area
if(childWithAccessibilityFocus ! =null) {
if(childWithAccessibilityFocus ! = child) {continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if(! canViewReceivePointerEvents(child) || ! isTransformedTouchPointInView(x, y, child,null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if(newTouchTarget ! =null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
/ / 5. DispatchTransformedTouchEvent () method to call child View dispatchTouchEvent () method to handle events
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if(preorderedList ! =null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break; }}}else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if(preorderedList ! =null) preorderedList.clear(); }... }},...returnhandled; }}Copy the code
5.3 Event distribution of View
A View has no child elements and can’t pass events down. It can only handle events itself, so event passing is relatively simple.
If OnTouchListener is set and onTouchListener.ontouch (this, event) returns true, the method consumes the event and onTouchEvent(event) is no longer called. You can see that OnTouchListener takes precedence over onTouchEvent(Event) so that the outside world can handle the event.
public class View implements Drawable.Callback.KeyEvent.Callback.AccessibilityEventSource {
public boolean dispatchTouchEvent(MotionEvent event) {...if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
// If OnTouchListener is set and onTouchListener. onTouch(this, event) returns true
OnTouchEvent (event) is no longer called if the method consumes the event
ListenerInfo li = mListenerInfo;
// If setOnTouchListener() is called and
if(li ! =null&& li.mOnTouchListener ! =null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if(! result && onTouchEvent(event)) { result =true; }}...returnresult; }}Copy the code
Let’s take a look at the onTouchEvent(Event) method inside the View.
public class View implements Drawable.Callback.KeyEvent.Callback.AccessibilityEventSource {
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
The disable property of the View does not affect the return value of the onTouchEvent() method, even if the View is disable, as long as
// If the View's clickable or longClickable is true, onTouchEvent() will still return true
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if(action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) ! =0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if(mTouchDelegate ! =null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true; }}//2. The onTouchEvent() method consumes the event whenever clickable or longClickable is true
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
booleanprepressed = (mPrivateFlags & PFLAG_PREPRESSED) ! =0;
if((mPrivateFlags & PFLAG_PRESSED) ! =0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if(isFocusable() && isFocusableInTouchMode() && ! isFocused()) { focusTaken = requestFocus(); }if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if(! mHasPerformedLongPress && ! mIgnoreNextUpEvent) {// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if(! focusTaken) {// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//3. If the View has OnClickListener set, performClick() calls its onClick method
if(! post(mPerformClick)) { performClick(); }}}if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if(! post(mUnsetPressedState)) {// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
return false; }}Copy the code
There are two things to say about onTouchEvent:
- The Disable property of the View does not affect the return value of the onTouchEvent() method. Even if the View is disabled, onTouchEvent() will return true if the clickable or longClickable value of the View is true.
- The onTouchEvent() method consumes the event as long as clickable or longClickable is true
- If the View has OnClickListener set, performClick() calls its onClick method.
We mentioned above that CLICKABLE and LONG_CLICKABLE can be set in XML or code for viewFlags. The LONG_CLICKABLE of a View is set to true by default. CLICKABLE defaults to false, and it is worth noting that the setOnClickListener() and setOnLongClickListener() methods set both values to true.
Through the analysis of the source code, we have mastered the law of event distribution in various scenarios, and we will summarize the relevant conclusions of View event distribution.
- Events are passed in the order Activity -> Window -> View
- In general, a sequence of events can only be intercepted and consumed by a single View. Once a View intercepts the event, subsequent events of the sequence are handled by the View.
- ViewGroup does not intercept any events by default
- A View has no onInterceptTouchEvent() method, and its ouTouchEvent() method is called whenever a click event is passed to it.