Android开发艺术探索 第四章
4.1 ViewRoot和DecorView
ViewRoot对应实现类是ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程是由ViewRoot来完成的。
在ActivityThread中,当Activity对象被创建后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams,panelParentView);
View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout、draw三个过程将一个View绘制出来。
performTraversals会依次调用performMeasure、performLayout、performDraw三个方法,然后分别调用measure、layout、draw完成顶级View的流程,这其中又会调用onMeasure、onLayout、onDraw方法对所有子元素进行对应的measure、layout、draw流程,重复这个流程,直到完成整个View树的遍历。
measure:测量View的宽和高,在测量完成后,可以通过getMeasuredWidth和getMeasuredHeight获取测量后的宽高,出了在特殊情况下,这基本等于view的宽高。
layout: 确定View在父容器中的放置位置,也就是View的四个顶点坐标和实际的View的宽高,完成后可以拿到四个顶点top、buttom、left、right,并且可以通过getWidth和getHeight获取最终的宽高。
draw: 负责将View绘制在屏幕上。
DecorView作为顶级View,含有竖直方向的linearLayout,这个里面分为上下两部分(和版本主题有关系)。Activity的setContentView就是添加布局到内容栏content中。
4.2理解MeasureSpec
4.2.1
MeasureSpec是一个32位的int值,高两位代表测量模式SpecMode,低30位代表测量模式下的规格大小SpecSize。
MeasureSpec由大小和模式组成。有三种可能模式:
* UNSPECIFIED:父对象未对子对象施加任何约束。它可以是任何大小它想要。这种一般用于系统内部,表示一种测量状态
* EXACTLY:父项已确定子项的精确大小。view的最终大小是SpecSize所指定的值,对应match_parent和具体数值。
* AT_MOST:子对象可以任意大到SpecSize指定的大小,不能大于这个值。对应wrap_content
*MeasureSpec封装了从父级传递到子级的布局要求。
*每个度量值表示对宽度或高度的要求。
*
* measurespec作为int实现,以减少对象分配。此类用于对<;大小、模式>;将元组转换为int。
*/
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
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) {//低于17版本的时候
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//类似于{@link#makeMeasureSpec(int,int)},但任何模式为未指定的规范将自动获得0的大小。较旧的应用程序预计会出现这种情况。
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
//从提供的测量规格中提取模式。
public static int getMode(int measureSpec) {
//noinspection ResourceType
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:新尺寸将为负数! (" + 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();
}
}
4.2.2
MeasureSpec和LayoutParams的关系,
MeasureSpec一般由自己LayoutParams和父容器来定义,对于顶级View(DecorView),他没有父容器,所以只由自身的LayoutParams来确定。对于DecorView来说,它的宽高基本就是屏幕的尺寸,具体代码可以参考DecorView的MeasureSpec的创建过程。具体的规则如下:
1.MATCH_PARENT:精确模式,大小就是窗口的大小。
2.WRAP_CONTENT:最大模式,大小不确定,但是最大不能超过窗口的大小。
3.固定大小(例如80dp): 精确模式,大小为LayoutParams中指定的大小。
接下来我们看一下普通View,容器中的View,View的measure测量过程由ViewGroup容器传递过来,先看一下ViewGroup的measureChildWithMargins方法:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
上述方法会对子元素进行测量,在调用测量之前会通过getChildMeasureSpec获得子元素的MeasureSpec,子元素的MeasureSpec除了与父容器的MeasureSpec和子元素的LayoutParams有关系外,还和View的margin及padding有关
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 them 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 = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
子元素的处理逻辑就是根据父容器占用的控件,减去间距padding剩下的可用空间,具体处理逻辑总结可以看表,
其实对于这一节内容,大家熟悉View的布局应该也能好了解,只不过代码实现细节不太清楚。稍微需要注意,当View取固定大小,那么不管父容器的模式是什么,View都会是那么大,不会受父容器影响。
4.3 View 的工作流程
View界面的工作流程主要是只measure、layout、draw这三大流程,即测量、布局和绘制。其中measure测量是确定View的测量宽高,layout布局确定View的最终宽高和四个顶点的位置,而draw绘制是将View绘制到屏幕上。
4.3.1.measure测量过程
单独的VIew测量自己就可以了,如果是ViewGroup,需要遍历所有子元素的measure测量方法,各个子元素在递归执行这个流程,首先看View的测量
1.View的onMeasure过程
View的onMeasure过程由final类型的measure方法完成的,意味不能继承重写,测量的时候会调用onMeasure方法,所以重点看看这个方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
主要看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;
}
主要需要理解AT_MOST和EXACTLY过程,这个specSize就是View测量后的大小,大部分情况下这个和layout布局后的最终大小是相等的。
UNSPECIFIED主要是用于系统内部测量过程,如果View没有设置背景,那么View的宽度为minWidth这个属性设定的值,如果没有指定,它的默认值是0.如果View设置了背景,那么它的值是这两者之间的最大值,
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
需要留意一下背景图的宽度,如果是BitmapDrawable是有原始宽高的,图片的尺寸,但是ShapeDrawable是没有原始宽高的。
自定义控件如果继承View,需要重写onMeasure方法并设置wrap_content时的自身大小,否则就相当于match_parent。我们需要给View指定一个默认的内部宽高,并在wrap_content是设置此宽高即可。具体代码可以参考TextView、ImageView等的源码就可以知道了,针对wrap_content的情形,onMeasure方法均做了特殊处理,
2.ViewGrop的onMeasure过程
ViewGrop需要遍历子元素的measure方法,和VIew不一样的地方,ViewGrop是一个抽象类,因此没有重写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);
}
}
}
循环遍历测量子元素
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);
}
measureChild的思想是取出子元素的LayoutParams,在通过getChildMeasureSpec去创建子元素的MeasureSpec,接着讲MeasureSpec传递给View的measure方法进行测量。
ViewGrop没有定义具体的onMeasure测量过程,因为ViewGrop是一个抽象类,具体onMeasure实现需要子类去实现,例如LinearLayout等布局,因为每个布局特性不一样,测量方式也不一样。
接下来看看LinearLayout的onMeasure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
线性布局分方向,选个measureVertical看看,源码过长不在贴代码。
final int usedWidth = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, usedWidth,
heightMeasureSpec, 0);
final int childWidth = child.getMeasuredWidth();
if (useExcessSpace) {
lp.width = 0;
usedExcessSpace += childWidth;
}
if (isExactly) {
mTotalLength += childWidth + lp.leftMargin + lp.rightMargin
+ getNextLocationOffset(child);
} else {
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childWidth + lp.leftMargin
+ lp.rightMargin + getNextLocationOffset(child));
}
主要流程就是遍历各个子元素执行measureChildBeforeLayout,这个方法会调用子元素的measure方法,这样各个子元素完成测量过程,然后系统通过mTotalLength记录总方向的初步高度。高度高开子元素高度、方向上的margin等,最后LinearLayout根据子元素的测量结果给出测量自己的大小。
可以看看LinearLayout测量自己的大小和View的过程是差不多的,主要是自适应的时候是用所有子元素的大小,而View是用自己的背景和本身。
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
三个流程里面最复杂的measure完成后,就可以通过getMeasureWidth/Height就可以正常的获取到View的测量宽高了,需要注意,measure可能存在多次调用,大部分是调用一次就可以,稳妥起见最好在onLayout方法中或者界面的测量宽高。
因为View的measure方法过程和活动的声明周期不是同步执行,所有无法在onCreate\onStart\onResume中准确获取到测量宽高。
(1)Activity/View:onWindowFocusChanged
这个方法调用的时候是View已经初始化完成,需要留意这个方法可以被多次调用,因为焦点事件变化。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus){
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
}
(2)View.post
通过post可以将一个线程投递到消息队列末尾,等Looper调用这个线程的时候,View肯定也初始化好了
view.post(new Runnable() {
@Override
public void run() {
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
});
(3)ViewTreeObserver
当View树的状态发生改变,onGlobalLayout方法会被回调,因此也是个很好的获取机会,同样需要注意,会被调用多次,有改变就会调用
ViewTreeObserver observer=view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
});
(4)View.measure(int widthMeasureSpec, int heightMeasureSpec)
可以手动进行测量得到高度,需要区分情况
match_parent:直接方法,需要父控件大小
具体的数值:直接调用,例如下面
//100px
int widthMeasureSpec= View.MeasureSpec.makeMeasureSpec(100,View.MeasureSpec.EXACTLY);
int heightMeasureSpec= View.MeasureSpec.makeMeasureSpec(100,View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
wrap_content:按照View理论支持的最大值去构造
int widthMeasureSpec= View.MeasureSpec.makeMeasureSpec((1<<30)-1,View.MeasureSpec.EXACTLY);
int heightMeasureSpec= View.MeasureSpec.makeMeasureSpec((1<<30)-1,View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
需要注意,其他写法不一定可以保证measure出正确的结果
4.3.2 layout 过程
Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有子元素并调用其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);
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);
}
}
}
final boolean wasLayoutValid = isLayoutValid();
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
首先通过setFrame方法来设置View的四个顶点位置,View位置一旦确定,那么View在父容器的位置也就确定;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,类似onMeasure方法。所有View和ViewGroup均没有实现onLayout方法
接下来看一下 LinearLayout的onLayout方法,实现和onMeasure逻辑类似,我们选择layoutVertical继续看
@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);
}
}
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
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);
}
}
}
layoutVertical的代码逻辑,会遍历所有子元素并调用setChildFrame方法为子元素来指定位置,其中childTop会逐步增大,这样就意味着后面的子元素会被放在考下的位置,符合LinearLayout的特性,setChildFrame调用了子元素的layout,子元素又通过自己的layout方法来确定自己位置,层层传递下去完成整个View树的layout过程
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
setChildFrame中的width和height实际上就是子元素的测量宽高,
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
layout方法中会通过setFrame去设置子元素的四个顶点的位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
下面来说测量宽高和最终宽高有什么区别
测量时间不一样吗,测量宽高是在measure过程,最终宽高是形成与View的layout过程,两者赋值的时机不同,日常开发中两者基本相等,如果重写layout方法,进行修改,那么他两可以不一样。
4.3.3 draw过程
draw过程就是讲界面绘制到屏幕上,绘制流程如下:
(1) 绘制背景background.draw(canvas).
(2) 绘制自己(onDraw).
(2) 绘制children (dispatchDraw).
(2) 绘制装饰(onDrawScrollBars).
代码draw如下
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
*绘制遍历执行几个必须执行的绘制步骤
*按照适当的顺序:
*
*1.绘制背景
*2.如有必要,保存画布层以备褪色
*3.绘制视图的内容
*4.画子元素
*5.如有必要,绘制褪色边缘并恢复层
*6.绘制装饰(例如滚动条)
*7.如有必要,绘制默认焦点突出显示
*/
// Step 1, draw the background, if needed
int saveCount;
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
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 (isShowingLayoutBounds()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
...
}
dispatchDraw来传递绘制流程,dispatchDraw会遍历所有子元素的Draw方法,一层层传递下去。
setWillNotDraw方法,如果一个View不需要绘制任何内容,可以设置这个标记位为true,系统会默认进行优化。ViewGroup默认启用这个标记位,如果知道一个viewGroup需要通过onDraw来绘制内容的时候,我们需要显示关闭WILL_NOT_DRWA这个标记位
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
4.4 自定义View
前面讲了View的绘制流程,那么我们可以自定义View实现各种效果。
4.4.1 自定义View的分类
-
继承View,重新 onDrew方法:主要重写onDraw实现以下不规则的效果,需要自己支持wrap_content,并且padding也需要自己处理。
-
继承ViewGroup派生特殊的Layout:主要是实现自定义布局,需要合适处理ViewGroup的测量、布局两个过程,还有子元素的测量和布局过程。
-
继承特定的View(比如TextView):扩展已有View的功能,在已有的View添加自己需要的逻辑。
-
继承特定的ViewGroup(比如LinearLayout):扩展已有容器的功能,和3类似。比2少了自己处理测量和布局过程。
4.4.2 自定义View注意事项
-
让View支持wrap_content:如果不支持,那么控件在自适应的时候无法达到预期效果。
-
让View支持padding:如果不支持,那么padding属性失效。直接继承ViewGroup的控件在测量和布局过程中考虑padding和子元素的margin对其造成的影响,不然导致子元素margin也会失效。
-
View中尽量不要使用Handler:View本身提供了post系列的方法,完全可以替代Handler的作用。
-
View中如果有线程或者动画,需要及时停止:参考View#onDetachedFromWindow,当包含此View的Activity退出或者当前View被remove时候,onDetachedFromWindow会被调用,和这个方法对应的是onAttachedToWindow,当包含此View的Activity启动时候,View的onAttachedToWindow会被调用,同时,View变得不可见的时候我们也需要停止动画和动画。如果不及时处理线程和动画,可能造成内测泄漏。
-
View带有滑动嵌套情况时,需要处理好滑动冲突:如果有滑动冲突,需要合适解决,否则严重影响view的效果。
4.4.3 自定义View示例
1.继承View,重新 onDrew方法
这里是自定义一个圆,会在界面的最中心位置,需要考虑一下View四周的padding就可以了。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(width, height);
canvas.drawCircle(paddingLeft + width / 2f, paddingTop + height / 2f, radius, mPaint);
}
测量的时候需要适配一下wrap_content,简单来说自适应需要安排大小,因为View已经没有子元素了,如果不定具体值,那么他会为零。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
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 && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
添加自定义属性,雷士下面这种,定义自己的风格和属性就好了,这个属性是什么格式,reference是ID,dimension是尺寸,color是颜色等可以自己设置。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color" />
</declare-styleable>
</resources>
然后就是在代码里面使用,从构造的AttributeSet 读取就好了
private void loadAttrs(AttributeSet attrs) {
if (attrs != null) {
TypedArray attributes = mContext.obtainStyledAttributes(attrs, R.styleable.BnLoading);
mColor= attributes.getColor(R.styleable.CircleView_circle_color, Color.RED);
}
}
最后是在代码里面使用,需要注意使用自定义属性,需要声明xmlns:app="http://scheamas.android.com/apk/res-auto",这个声明,app是自定义的前缀,可以修改自定义,但是CircleView的自定义属性的前缀必须和这里的一致。
<LineraLayout
xmlns:app="http://scheamas.android.com/apk/res-auto"
>
<CircleView
app:circle_color="@color/green"
/>
</LineraLayout>
2.继承ViewGroup派生特殊的Layout
这种方法需要合适的处理测量、布局这两个过程,并且同时处理子元素的测量和布局流程,具体可以看看LinearLayout等源码参考,实现抖很复杂。
回顾一下之前HorizontalScrollViewEx的功能,主要是内部子元素可以水平滑动并且还可以进行垂直滑动,这需要处理水平和垂直滑动冲突的问题。
这里主要是看测量和布局过程,先看测量onMeasure,首先会判断是否有子元素,如果没有直接就是宽高都是0,然后判断是不是采用了自适应,如果是就等于所有元素宽度之和,高度等于第一个元素高度。这样写法不规范有两点,1 没有根据根元素中宽高做处理 2没有考虑自己的间距和子元素的间距,因为这些都是会影响宽高的。
其次看一下布局onLayout方法,首先一样先遍历所有的子元素,如果这个控件看可见,需要把它放在合适的位置,从这个放置过程是从左到右的,规范上的问题和上面测量是一样的问题。
4.4.4 自定义View的思想
自定义界面主要是需要熟练的基本功,view的弹性滑动、滑动冲突、绘制原理灯光,这些在加上对控件实施适当的思路,就可以实现看起来很炫酷的自定义控件了。