Android、View视图与坐标系
View的滑动和属性动画
从源码解析View的事件分发机制
View的工作流程
Android自定义view
View的工作流程
View的工作流程
View的工作流程,指的就是measure、layout和draw。其中,measure用来测量View的宽和高,layout用来确定View的位置,draw则用来绘制View。
View的工作流程入口
上面的内容讲到了Activity的构成,最后讲到了DecorView的创建以及它加载的资源。这个时候DecorView的内容还无法显示,因为它还没有被加载到Window中。接下来我们来看看DecorView如何被加载到Window中。
DecorView被加载到Window中
当DecorView创建完毕,要加载到Window中时,我们需要先了解一下Activity的创建过程。当我们调用Activity的startActivity方法时,最终是调用ActivityThread的handleLaunchActivity方法来创建Activity的,代码如下所示:
private void handleLaunchActivity(ActivityClientRecord r,
Intent customIntent, String reason) {
Activity a = performLaunchActivity(r, customIntent);//1
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed, //2
r.lastProcessedSeq, reason);
...
}
上面代码注释 1 处调用 performLaunchActivity 方法来创建 Activity,在这里面会调用到Activity的onCreate方法,从而完成DecorView的创建。接着在上面代码注释2处调用handleResumeActivity方法,代码如下所示:
final void handleResumeActivity(IBinder token, boolean clearHide,
boolean isForward, boolean reallyResume, int seq, String reason) {
unscheduleGcIdler();
mSomeActivitiesChanged = true;
r = performResumeActivity(token, clearHide, reason);//1
if (r != null) {
final Activity a = r.activity;
...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();//2
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();//3
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient && !a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);//4
}
...
}
在上面代码注释1处的performResumeActivity方法中会调用Activity的onResume方法。接着往下看,注释
2处得到了DecorView。注释3处得到了WindowManager,WindowManager是一个接口并且继承了接口ViewManager。在注释4处调用WindowManager的addView方法,WindowManager 的实现类是
WindowManagerImpl,所以实际调用的是 WindowManagerImpl 的addView方法。具体代码如下所示:
public final class WindowManagerImpl implements WindowManager {
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
}
在 WindowManagerImpl 的 addView 方法中,又调用了 WindowManagerGlobal 的 addView方法,代码如下所示:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
root = new ViewRootImpl(view.getContext(), display);//1
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
try {
root.setView(view, wparams, panelParentView);//2
} catch (RuntimeException e) {
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
}
在上面代码注释1处创建了ViewRootImpl实例,在注释2处调用了ViewRootImpl的setView方法并将DecorView作为参数传进去,这样就把DecorView加载到了Window中。当然界面仍不会显示出什么来,因为View的工作流程还没有执行完,还需要经过measure、layout以及draw才会把View绘制出来。
ViewRootlmpl的PerformTraveals方法
前面讲到了将 DecorView 加载到 Window 中,是通过 ViewRootImpl 的 setView 方法。ViewRootImpl还
有一个方法PerformTraveals,这个方法使得ViewTree开始View的工作流程,代码如下所示:
private void performTraversals() {
...
if (!mStopped || mReportNextDraw) {
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || framesChanged || updatedConfiguration) {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);//1
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//2
}
}
if (didLayout) {
performLayout(lp, mWidth, mHeight);
...
}
if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
}
...
}
这里面主要执行了3个方法,分别是performMeasure、performLayout和performDraw,在其方法的内部又会分别调用View的measure、layout和draw方法。需要注意的是,performMeasure方法中需要传入两个参数,分别是 childWidthMeasureSpec 和 childHeightMeasureSpec。要了解这两个参数,需要了解MeasureSpec。
理解MeasureSpec
MeasureSpec是View的内部类,其封装了一个View的规格尺寸,包括View的宽和高的信息,它的作用是在Measure流程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure方法中根据这个MeasureSpec来确定View的宽和高。MeasureSpec的代码如下所示:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1)
int size, @MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
@MeasureSpecMode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);
StringBuilder sb = new StringBuilder("MeasureSpec: ");
if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");
sb.append(size);
return sb.toString();
}
}
从MeasureSpec的常量可以看出,它代表了32位的int值,其中高2位代表了SpecMode,低30位则代表SpecSize。SpecMode指的是测量模式,SpecSize指的是测量大小。SpecMode有3种模式,如下所示。
- UNSPECIFIED:未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量。
- AT_MOST:最大模式,对应于wrap_comtent属性,子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值。
- EXACTLY:精确模式,对应于 match_parent 属性和具体的数值,父容器测量出 View所需要的大小,也就是SpecSize的值。
对于每一个View,都持有一个MeasureSpec,而该MeasureSpec则保存了该View的尺寸规格。在View的测量流程中,通过makeMeasureSpec来保存宽和高的信息。通过getMode或getSize得到模式和宽、高。MeasureSpec是受自身LayoutParams和父容器的MeasureSpec共同影响的。作为顶层View的DecorView来说,其并没有父容器,那么它的MeasureSpec是如何得来的呢?为了解决这个疑问,我们再回到ViewRootImpl的PerformTraveals方法,代码注释 1 处调用了 getRootMeasureSpec(mWidth,lp.width)方法。下面来查看这方法做了什么:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
getRootMeasureSpec方法的第一个参数windowSize指的是窗口的尺寸,所以对于DecorView来说,它的MeasureSpec由自身的LayoutParams和窗口的尺寸决定,这一点和普通View是不同的。接着往下看,就会看到根据自身的LayoutParams来得到不同的MeasureSpec。performMeasure方法中需要传入两个参数,即childWidthMeasureSpec和childHeightMeasureSpec,这代表什么我们也应该明白了。接着回到PerformTraveals方法,查看在注释2处的performMeasure方法内部做了什么,
代码如下所示:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
其实,就算不看我们也应该知道里面调用了什么——是View的measure方法。接下来我们学习一下View的measure流程。
View的measure流程
measure 用来测量 View 的宽和高,它的流程分为 View 的 measure 流程和 ViewGroup 的measure流程,只不过ViewGroup的measure流程除了要完成自己的测量,还要遍历地调用子元素的measure()方法。
View的measure流程
首先来看一下View的onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
接着查看setMeasuredDimension方法,代码如下所示:
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);
}
这很显然是用来设置View的宽、高的,再回头看看getDefaultSize()方法处理了什么:
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;
}
SpecMode是View的测量模式,而SpecSize是View的测量大小,上面的内容让我们也了解了MeasureSpec。这里很显然根据不同的SpecMode值来返回不同的result值,也就是SpecSize。在AT_MOST和EXACTLY模式
下,都返回SpecSize这个值,即View在这两种模式下的测量宽和高直接取决于SpecSize。也就是说,对于一个直接继承自View的自定义View来说,它的wrap_content 和 match_parent 属性的效果是一样的。因此如果
要实现自定义 View 的wrap_content,则要重写onMeasure方法,并对自定义View的wrap_content属性进行处理。而在 UNSPECIFIED 模式下返回的是 getDefaultSize 方法的第一个参数 size 的值,size 的值从onMeasure
方法来看是getSuggestedMinimumWidth方法或者getSuggestedMinimumHeight方法得到的。我们来查看getSuggestedMinimumWidth方法做了什么。只需要弄懂getSuggestedMinimumWidth方法就可以了,因为这两个方法的原理是一样的。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth :
max(mMinWidth, mBackground.getMinimumWidth());
}
如果 View 没有设置背景,则取值为 mMinWidth,mMinWidth 是可以设置的,它对应于Android:minWidth这个属性设置的值或者View的setMinimumWidth的值;如果不指定的话,则默认为0。setMinimumWidth方法如下所示:
public void setMinimumWidth(int minWidth) {
mMinWidth = minWidth;
requestLayout();
}
如果View设置了背景,则取值为max(mMinWidth,mBackground.getMinimumWidth()),也就是取mMinWidth和mBackground.getMinimumWidth()之间的最大值。此前讲了mMinWidth,下面看看mBackground.getMinimumWidth()。这个mBackground是Drawable类型的,Drawable类的getMinimumWidth 方法如下所示:
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
intrinsicWidth得到的是这个Drawable的固有宽度,如果固有宽度大于0则返回固有宽度,否则返回0。总结一下,getSuggestedMinimumWidth方法就是:如果View没有设置背景,则返回mMinWidth;如果设置了背景,就返回mMinWidth和Drawable的最小宽度之间的最大值。
ViewGroup的measure流程
讲完了View的measure流程,接下来看看ViewGroup的measure流程。对于ViewGroup,它不只要测量自身,还要遍历地调用子元素的measure()方法。ViewGroup中没有定义onMeasure()方法,但却定义了measureChildren()方法:
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);
}
}
}
遍历子元素并调用measureChild方法,measureChild方法如下所示:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
调用 child.getLayoutParams()方法来获得子元素的 LayoutParams 属性,获取子元素的MeasureSpec 并调用子元素的 measure()方法进行测量。getChildMeasureSpec()方法里写了什么呢?其代码如下:
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) {
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;//1
}
break;
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
很显然,这是根据父容器的MeasureSpec模式再结合子元素的LayoutParams属性来得出的子元素的 MeasureSpec 属性。有一点需要注意的是,如果父容器的 MeasureSpec 属性为AT_MOST,子元素的LayoutParams属性为WRAP_CONTENT,那根据上面代码注释1处的代码,我们会发现子元素的MeasureSpec
属性也为AT_MOST,它的SpecSize值为父容器的SpecSize减去padding的值。换句话说,这和子元素设置LayoutParams属性为MATCH_PARENT效果是一样的。为了解决这个问题,需要在LayoutParams属性为WRAP_CONTENT时指定一下默认的宽和高。ViewGroup并没有提供onMeasure方法,而是让其子类来各自实现测量的方法,究其原因就是ViewGroup有不同布局的需要,很难统一。接下来我们简单分析一下ViewGroup的子类LinearLayout的measure流程。现在先来看看它的onMeasure方法,代码如下所示:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
这个方法的逻辑很简单,如果是垂直方向则调用 measureVertical 方法,否则就调用measureHorizontal方法。接着分析垂直measureVertical()方法的部分源码:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
...
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
if (useExcessSpace) {
lp.height = LayoutParams.WRAP_CONTENT;
}
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
lp.height = 0;
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
}
...
if (useLargestChild && (heightMode == MeasureSpec.AT_MOST ||
heightMode == MeasureSpec.UNSPECIFIED)) {
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
...
}
这里定义了mTotalLength用来存储LinearLayout在垂直方向的高度,然后遍历子元素,根据子元素的
MeasureSpec模式分别计算每个子元素的高度。如果是WRAP_CONTENT,则将每个子元素的高度和margin垂直高度等值相加并赋值给mTotalLength。当然,最后还要加上垂直方向padding的值。如果布局高度设置为MATCH_PARENT或者具体数值,则和View的测量方法是一样的。measure流程就讲到这里了,接下来讲解View的layout和draw流程。
View的layout流程
layout方法的作用是确定元素的位置。ViewGroup中的layout方法用来确定子元素的位置,View中的layout方法则用来确定自身的位置。首先我们看看View的layout方法:
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;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
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;
}
layout方法的4个参数l、t、r、b分别是View从左、上、右、下相对于其父容器的距离。接着来查看setFrame方法里做了什么,代码如下所示:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
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(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);
}
if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
mPrivateFlags |= PFLAG_DRAWN;
invalidate(sizeChanged);
invalidateParentCaches();
}
mPrivateFlags |= drawn;
mBackgroundSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}
setFrame方法用传进来的l、t、r、b这4个参数分别初始化mLeft、mTop、mRight、mBottom这4个值,这样就确定了该View在父容器中的位置。在调用setFrame方法后,会调用onLayout方法:
protected void onLayout(boolean changed, int left, int top,
int right, int bottom) {
}
onLayout方法是一个空方法,这和onMeasure方法类似。确定位置时根据不同的控件有不同的实现,所以在View和ViewGroup中均没有实现onLayout方法。既然这样,我们下面就来查看LinearLayout的onLayout方法:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
与 onMeasure 方法类似,根据方向来调用不同的方法。这里仍旧查看垂直方向的layoutVertical方法,如下所示:
void layoutVertical(int left, int top, int right, int bottom) {
...
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.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
这个方法会遍历子元素并调用setChildFrame方法。其中childTop值是不断累加的,这样子元素才会依次按照垂直方向一个接一个排列下去而不会是重叠的。接着看setChildFrame方法:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
在setChildFrame方法中调用子元素的layout方法来确定自己的位置。
View的draw流程
View的draw流程很简单,下面先来看看View的draw方法。官方注释清楚地说明了每一步的做法,它们分别是:
- 如果需要,则绘制背景。
- 保存当前canvas层。
- 绘制View的内容。
- 绘制子View。
- 如果需要,则绘制View的褪色边缘,这类似于阴影效果。
- 绘制装饰,比如滚动条。
其中第2步和第5步可以跳过,所以这里不做分析,重点分析其他步骤。
步骤1:绘制背景
绘制背景调用了View的drawBackground方法,如下所示:
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
...
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {//1
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
从上面代码注释1 处可看出绘制背景考虑了偏移参数 scrollX 和scrollY。如果有偏移值不为0,则会在偏移后的canvas绘制背景。
步骤3:绘制View的内容
步骤3调用了View的onDraw方法。这个方法是一个空实现,因为不同的View有着不同的内容,这需要我们自己去实现,即在自定义View中重写该方法来实现:
protected void onDraw(Canvas canvas) {
}
步骤4:绘制子View
步骤4调用了dispatchDraw方法,这个方法也是一个空实现,如下所示:
protected void dispatchDraw(Canvas canvas) {
}
ViewGroup重写了这个方法,紧接着看看ViewGroup的dispatchDraw方法:
@Override
protected void dispatchDraw(Canvas canvas) {
...
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
}
...
}
源码很长,这里截取了关键的部分,在 dispatchDraw 方法中对子类 View 进行遍历,并调用drawChild方法:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
这里主要调用了View的draw方法,代码如下所示:
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...
if (!drawingWithDrawingCache) {
if (drawingWithRenderNode) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
} else {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
draw(canvas);
}
}
} else if (cache != null) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
Paint cachePaint = parent.mCachePaint;
if (cachePaint == null) {
cachePaint = new Paint();
cachePaint.setDither(false);
parent.mCachePaint = cachePaint;
}
cachePaint.setAlpha((int) (alpha * 255));
canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
} else {
int layerPaintAlpha = mLayerPaint.getAlpha();
if (alpha < 1) {
mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
}
canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
if (alpha < 1) {
mLayerPaint.setAlpha(layerPaintAlpha);
}
}
}
}
源码很长,我们挑重点的看。在上面代码注释1处判断是否有缓存,如果没有则正常绘制,如果有则利用缓存显示。
步骤6:绘制装饰
绘制装饰的方法为View的onDrawForeground方法:
public void onDrawForeground(Canvas canvas) {
onDrawScrollIndicators(canvas);
onDrawScrollBars(canvas);
final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
if (foreground != null) {
if (mForegroundInfo.mBoundsChanged) {
mForegroundInfo.mBoundsChanged = false;
final Rect selfBounds = mForegroundInfo.mSelfBounds;
final Rect overlayBounds = mForegroundInfo.mOverlayBounds;
if (mForegroundInfo.mInsidePadding) {
selfBounds.set(0, 0, getWidth(), getHeight());
} else {
selfBounds.set(getPaddingLeft(), getPaddingTop(),
getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
}
final int ld = getLayoutDirection();
Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
foreground.setBounds(overlayBounds);
}
foreground.draw(canvas);
}
}
很明显这个方法用于绘制ScrollBar以及其他的装饰,并将它们绘制在视图内容的上层。
以上内容摘自《Android进阶之光》