Layout
前面讲述了measure过程和LayoutParams的生成问题,到这里View的整个绘制过程已经差不多了,剩下的就是layout过程和draw过程了。
另外对于布局,就是将View的位置固定下来。值得注意的是,这里的位置是相对于父View而言的,也就是说,以父View的左上角为坐标零点的相对位置。
// View.java
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;
// 最终会调用setFram来设置位置
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 若是位置发生了改变,则调用onLayout方法进行重新布局
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
...
}
...
}
同样的,layout方法虽然不是final修饰的,但是也是没有View覆写它,实际上,measure,layout,draw都没有被覆写,他们是android提供的一套布局框架。从它的实现来看,首先通过setFrame(setOpticalFrame内部也是调用setFrame)方法进行设置,该方法会判断View的位置是否发生变化,若是发生了变化就会返回true,然后在layout中就会调用onLayout 方法进行重新布局。
在View中,onLayout方法是个空实现,而在ViewGroup中,该方法是被重写为抽象方法。因为ViewGroup可以存在子View,因此必须实现该方法来对子View进行布局。
// View.java
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (DBG) {
Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
+ right + "," + bottom + ")");
}
// 新设置的位置与原来的坐标不一致
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
// 获取新旧的宽高,并且判断是否是宽高发生的变化
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// 若是不一致则进行重绘
invalidate(sizeChanged);
// 重新赋值新的位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
...
}
return changed;
}
从setFrame中可以看到的当layout的位置发生变化的时候,会保存新的位置,然后根据尺寸是否发生变化进行不同的重绘过程。这与View的measure一样,都是默认的一种方法,而我们想要自己布局的话,只需要在onLayout方法中改变即可。
在setFrame中,若是位置发生了变化,则会进行保存新的位置,这时候View的宽高就确定了,可以通过getWidth/Height方法进行获取。
// View.java
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
layout 的过程比较简单,就是先通过setFrame方法进行判断旧的位置是否和新设置的位置相同,不相同的话则会设置新的位置并调用onLayout方法。但是在View中onLayout是空实现,也就是说在View中可以不用重写这个方法进行布局,而单纯使用setFrame即可。但是在ViewGroup中,把layout给加了final,也不能够重写,并且把onLayout给加了abstract。
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
因为在ViewGroup中,可能会有子View,因而需要对子View进行布局。也就是说我们自定义的ViewGroup不能重写layout,而只能重写onLayout,并且应该在onLayout中对子View进行layout。
总结
对于我们自定义的View,我们不应该重写layout方法,而ViewGroup更是将其final化从而禁止重写。并且对于View,我们使用默认的layout方法即可完成布局任务,若是有其他需求,可以重写onLayout进行重新布局。对于ViewGroup,抽象化的onLayout方法要求我们必须重写它,并且我们应该根据要求在onLayout中对子View进行布局。
1,layout阶段只需要重写onLayout方法并在其中进行布局的操作
2,若是普通View甚至可以不重写onLayout方式,默认方式进行布局即可
3,若是ViewGroup,则需要在onLayout中进行循环遍历子View,然后计算子View的位置并调用子View的layout将布局分发下去
4,layout结束
Draw
对于三大过程的measure和layout我们已经分析完了,从测量到布局最后只剩下一个绘制了。其实绘制细节很复杂,因为这将会导致View展示在屏幕上,涉及到它的具体表现。但是他的流程却很简单,每一步都分得很清。
// View.java
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
...
}
从上面的Draw方法可以看到,绘制过程分为6个部分:
1,绘制背景
2, 如果必要,保存canvas的layer来准备fading
3, 绘制本身
4, 绘制孩子View
5, 如果必要,绘制衰退效果并恢复layer
6, 绘制装饰效果,例如滚动条等
而就上面的6条而言,2和5通常是不必要的,可以忽略的。那么绘制的重点就留在了1346这几条上了。
其中,绘制背景和绘制装饰一般也是不需要我们关注的,因为这两者也基本都是固定的。所以我们主要关注的就是绘制本身和绘制子View。
// View.java
// 绘制自身
protected void onDraw(Canvas canvas) {
}
// 绘制子View
protected void dispatchDraw(Canvas canvas) {
}
但是在View中这两个方法都是空实现,这是因为每个View的绘制都各不相同,因此绘制本身不可能有具体的实现,而是交由具体的子类去实现自身的绘制。
另外对于普通子View而言,它是不会有子View,那么dispatchDraw方法当然也是一个空实现。但是对于ViewGroup,由于它可能会含有子View,所以dispatchDraw就应该被重写,去实现子View的绘制过程。
// ViewGroup.java
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
final boolean buildCache = !isHardwareAccelerated();
// 循环子View,绑定本身带的动画
for (int i = 0; i < childrenCount; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
final LayoutParams params = child.getLayoutParams();
attachLayoutAnimationParameters(child, params, i, childrenCount);
bindLayoutAnimation(child);
}
}
...
}
boolean more = false;
...
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;
}
}
// 绘制子View
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
while (transientIndex >= 0) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
break;
}
}
if (mDisappearingChildren != null) {
final ArrayList<View> disappearingChildren = mDisappearingChildren;
final int disappearingCount = disappearingChildren.size() - 1;
for (int i = disappearingCount; i >= 0; i--) {
final View child = disappearingChildren.get(i);
more |= drawChild(canvas, child, drawingTime);
}
}
...
}
上面的dispatchDraw比较复杂,考虑的比较多,包括隐藏的View,以及View的动画,但是我们可以看到的是它最终都是是调用了drawChild方法从而进一步调用child的draw方法进行绘制子View。因此,对于绘制过程,我们也只需要关注onDraw方法,即只绘制自身,而不用担心子View的分发绘制。
总结
View的绘制过程比较复杂,但是对于我们而言,它的流程比较简单。并且,在我们自定义View的时候,只用考虑onDraw方法。只需要在onDraw中进行自身的绘制即可。对于ViewGroup,其中的dispatchDraw已经实现了子View的分发绘制,我们也不必进行其他操作。
1,重写onDraw,在其中做自身的绘制过程
总结:
View整个表现在屏幕上总共有三个过程,分别是measure,layout和draw。其中,measure过程要根据父View的模式和子View的宽高模式共同计算,得出measuredWidth/Height。而layout过程则根据measuredWidth/Height来设置View的位置,一旦layout结束,View的大小也就确定了。draw按照绘制背景->自身->子View->装饰,这个过程进行绘制。
在这个过程中我们也都总结了各个阶段重要的一些步骤方法,这些步骤都是我们自定义View所涉及到的,根据这些步骤,我们可以很轻松的自定义出想要的view。
相关文章目录:
从源码学习自定义View(一):Measure过程
从源码学习自定义View(二):LayoutParams
从源码学习自定义View(三):Layout和Draw过程