目录
一、起源
1、从ActivityThread开始
2、WindowManagerImpl
3、WindowManagerGlobal
4、ViewRootImpl
二、概念
三、绘制流程
1、measure
1)解析
2)demo
2、layout
1)解析
2)demo
3、draw
1)解析
四、实例
===================================================================
在梳理原理的过程中,穿插实例解析,更易理解;最后还有一个简单的自定义view实践
一、起源
先简单说下View的起源,有助于我们后续的分析理解。
1、从ActivityThread.java开始
下面只贴出源码中的关键代码,重点是vm.addView(decor, l)这句,那么有三个对象需要理解:
- vm:a.getWindowManager()即通过Activity.java的getWindowManager方法得到的对象;继续跟踪源码,发现此对象由Window.java的getWindowManager方法获得,即是((WindowManagerImpl)wm).createLocalWindowManager(this)。
- decor:通过r.activity.getWindow()可知r.window是PhoneWindow对象,在PhoneWindow中找到getDecorView方法,得知decor即DecorView对象。
- l:通过下面源码得知,l通过r.window.getAttributes获取到,由于PhoneWindow中没有getAttributes方法,故从他的父类Window中获取,得知宽高均为LayoutParams.MATCH_PARENT
ActivityThread.java
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
...
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
} else {
...
}
}
}
2、接上面wm.addView(decor, l),通过上面分析wm是WindowManagerImpl,则走进此类的addView中
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
3、WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
...
root = new ViewRootImpl(view.getContext(), display);
...
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
...
}
}
}
4、有第三步可知,最终走到了ViewRootImpl.java,曙光在前方
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
requestLayout();
...
}
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
...
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
...
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
...
performTraversals();
...
}
重点来了,performMeasure、performLayout、performDraw则是对根view的测量、布局
、draw;先说下mWidth、mHeight和lp,其中mWidth、mHeight是window的宽高,lp就是前面提到的WindowManager中的LayoutParam,跟踪代码发现均为LayoutParams.MATCH_PARENT
private void performTraversals() {
...
WindowManager.LayoutParams lp = mWindowAttributes;
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
...
}
根据以下源码,那么childWidthMeasureSpec、childHeightMeasureSpec的值就是MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
/**
* Figures out the measure spec for the root view in a window based on it's
* layout params.
*
* @param windowSize
* The available width or height of the window
*
* @param rootDimension
* The layout params for one dimension (width or height) of the
* window.
*
* @return The measure spec to use to measure the root view.
*/
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
..
}
return measureSpec;
}
下面的mView上面已分析过,就是DecorView,也就是根view
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
...
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
...
}
}
二、概念
首先需要理解MeasureSpec的概念,详见另一篇文章 https://www.jianshu.com/p/cecd0de7ec27
三、绘制流程
1、measure
(1)解析
接上面说到了performMeasure,即走到了DecorView的measure,而DecorView实际是FrameLayout,FrameLayout的父类是ViewGroup,而ViewGroup的父类是View,所以直接走 到了View的measure里面。
主角登场,接下来分析View的measure,measure是final的,也就是不能不能重写此方法,但是里面有一个onMeasure方法,到这里应该很熟悉了,我们自定义控件都会重写这个方法;
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
由于DecorView的父类的FrameLayout,那么我们来看FrameLayout的onMeasure方法;可以看到会测量所有的子View,最后测量自己。所以有两点:测量子View;测量自己。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
...
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
...
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
...
}
}
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
...
}
}
我们先来关注测量子View即measureChildWithMargins,大概的意思是:
父View的MeasureSpec+当前View的LayoutParams=得出当前View的MeasureSpec
parentWidthMeasureSpec、parentHeightMeasureSpec还记得是什么吗?就是之前说过的ViewRootImpl中的performTraversals方法里面的performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)传过去的,可以回顾下前面的分析;
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做了什么,在这篇文章有过详细分析https://www.jianshu.com/p/cecd0de7ec27,特别提一下,size是父view的size与padding的差值,而padding是父view的padding与子view的margin的总和,最终算出了当前View的MeasureSpec的int值;接着调用子View的measure方法传入计算出的宽高MeasureSpec,层层递归直到无子View为止。
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 = 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);
}
分析完测量子View,接下来看测量自己,即上面源码中提到的setMeasuredDimension
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
...
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
...
}
最后为mMeasureWidth、mMeasureSpec赋值,测量完毕。
我们来看下View类中的onMeasure(即若不覆写onMeasure的默认逻辑),同上调用了setMeasureSpec为测量结果赋值;这里有需要注意的地方,当我们自定义View覆写onMeasure时,最后一定要为测量结果赋值(setMeasuredDimension),否则会报错。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
看下getSuggestedMinimumWidth,mMinWidth就是我们在layout中设置的android:minWidth,也就是有background的话则取background的宽度,否则去minWidth;高度也一样。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
getDefaultSize获取结果,首先通过之前算好的MeasureSpec取出mode、size,若mod是AT_MOST、WXACTLY则结果为specSize,若是UNSPECIFIED则结果为getSuggestedMinimumWidth的默认值。
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;
}
重点源码分析完了,接下来通过一个例子串起来。
(2)demo
写了一个简单的布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="@android:color/holo_blue_light"
android:orientation="vertical"
android:paddingTop="50dp">
<TextView
android:id="@+id/textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_orange_light"
android:text="Hello World!"
android:textSize="20sp" />
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginBottom="30dp"
android:background="@android:color/holo_green_light" />
</LinearLayout>
首先需要弄清楚布局的整体结构,通过工具Tools > Layout Inspector可以看到,顶层是我们之前提到的DecorView(是一个FrameLayout);我们直接看重点部分,即content(ContentFrameLayout可知是FrameLayout),我们平时开发中调用setContentView设置我们的布局,就是放置到这里面,由下图也可知content下面包含的布局正是id为layout的LinearLayout。
接下来一步步测量(measure)所有控件,我们从R.id.content(ContentFrameLayout)这里开始说,ContentFrameLayout是FrameLayout,走到了FrameLayout的onMeasure中,这里会循环子View,也就是会计算出子View(即我们写的布局 R.id.layout)的MeasureSpec后调用子View的measure测量宽高;
父view(即R.id.content ContentFrameLayout)的MeasureSpec和子View(即我们写的布局R.id.layout LinearLayout)的LayoutParams共同决定 子View的MeasureSpec;
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) {
...
} 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;
...
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
如之前的分析,当父View是MeasureSpec.EXACTLY,子View的宽为LayoutParams.MATCH_PARENT,则size的结果为父View的Size,即1080,mode为MeasureSpec.EXACTLY;子View的高为LayoutParams.WRAP_CONTENT,则size为父view与padding的差值,padding是父view的padding与子view的margin的总和,即1710 - 53(marginTop值为20dp 换算值与手机像素有关) = 1657,mode为MeasureSpec.AT_MOST,表示高度最大值为1657。
此时走到了LinearLayout的onMeasure,我们的布局是android:orientation="vertical",所以我们看measureVertical
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
循环所有子view测量出宽高,最后再测量自己。
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
...
if (skippedMeasure
|| ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) {
...
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
...
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
final float childWeight = lp.weight;
if (childWeight > 0) {
...
final int childHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec(
Math.max(0, childHeight), View.MeasureSpec.EXACTLY);
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
lp.width);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
...
}
...
mTotalLength += mPaddingTop + mPaddingBottom;
} else {
...
}
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
...
}
再来看我们layout中的两个子view,TextView和View,先来看TextView
其父View的MeasureSpec与TextView的LayoutParams如下,两者共同决定TextView的MeasureSpec,来到TextView的onMeasure中,为了更关注整体的把控,这里不做详细拓展解析TextView了
算出TextView的MeasureSpec,如下
接下来看另一个子view,View,父View的MeasureSpec与子View的LayoutParams如下图,通过源码getChildMeasureSpec可知,当父view的宽是EXACTLY时,子view的width的值是match_parent,那么子view的size为父view的size,即1080,mode是MeasureSpec.EXACTLY;当父view的高是AT_MOST时,子view的height值是确切数值263,左移子view的size为263,mode是MeasureSpec.EXACTLY。
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) {
...
} 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) {
...
}
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) {
...
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
...
}
break;
...
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
所有子view测量完毕,最后测量父view的MeasureSpec,宽是1080,高是TextView的height+View的height+父view的paddingTop+View的margin_bottom = 544。
2、layout
(1)解析
上面说过的view的入口是ViewRootImpl
private void performTraversals() {
...
WindowManager.LayoutParams lp = mWindowAttributes;
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
...
}
performMeasureSpec说完了,接下来看performLayout,measure负责测量、layout负责位置摆放,performLayout里面调用了View.java的layout
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
...
final View host = mView;
if (host == null) {
return;
}
...
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
} finally {
...
}
...
}
这里根据传入的left、top、right、bottom值来确定摆放位置,right和bottom一般分别为left、top的值加上当前view的宽、高。
public void layout(int l, int t, int r, int b) {
...
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);
...
}
...
}
通过setFrame来设置上下左右四个值,setOpticalFrame最终也会走到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 oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
...
// Invalidate our old position
invalidate(sizeChanged);
...
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
...
}
return changed;
}
我们发现view.java的onLayout是空方法,但是在viewgroup中是抽象方法,也就是父类均需要重写该方法,说明该方法是用来摆放子view位置的。
View.java
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
ViewGroup.java
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
(2)demo
我们用LinearLayout来举例,只看核心代码,通过不同的gravity值算出其left,top值递增:下一个子view的top值为 当前子view的高度+其bottomMargin( childHeight + lp.bottomMargin + getNextLocationOffset(child) ),所以我们在使用android:orientation="vertical"的时候,控件在LinearLayout中竖直排列。
那么后两个值宽高是什么?就是我们在measure阶段算出的测量值,通过getMeasuredWidth、getMeasureSpecHeight获得,和我们之前的分析一致。
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
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;
final int count = getVirtualChildCount();
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();
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,接着调用子类的layout方法,上面说过如果子View无子类,则在layout中直接通过setFrame算出位置,若有子View,则在layout中算出自己的位置后,通过重写onLayout测量其子view。
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
3、draw
(1)解析
上面说过的view的入口是ViewRootImpl
private void performTraversals() {
...
WindowManager.LayoutParams lp = mWindowAttributes;
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
...
}
performMeasureSpec、performLayout说完了,measure负责测量、layout负责位置摆放,draw负责绘制。下面只贴出了关键代码,最后走到了mView的draw里面
private void performDraw() {
...
try {
boolean canUseAsync = draw(fullRedrawNeeded);
...
}
...
}
private boolean draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
...
return useAsyncReport;
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
...
mView.draw(canvas);
...
return true;
}
View中的draw方法注释和清晰,分为6步,其中2、5可以跳过:
1、绘制背景
2、跳过
3、绘制view内容
4、绘制子view
5、跳过
6、绘制
真正的绘制工作()在view中进行,
View.java
public void draw(Canvas canvas) {
...
/*
* 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;
}
}
四、实例
这是实现效果,是比较常见的一个控件,文字轮播。
用自定义view实现(其实有个现成的控件TextSwitcher也可以实现);如果用自定义view方案的话,继承FrameLayout会更简单些,但是为了能将前面的知识点串起来的话,这里自定义了viewgroup,代码逻辑不复杂。我们来看下,下面是使用方式,还是比较简单的。
<com.example.textswitcherdemo.SwitcherViewGroup
android:id="@+id/text_switcher_view"
android:layout_width="match_parent"
android:layout_height="35dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
val contents = ArrayList<String>()
contents.add("草帽一伙抵达新世界")
contents.add("机器猫一伙乘坐时光机来到侏罗纪")
contents.add("蜡笔小新一家吃到霸王餐")
val switcherView : SwitcherViewGroup = findViewById(R.id.text_switcher_view)
switcherView.addContents(contents)
先说下思路:自定义viewgroup其实就是个简易版的framelayout,这个我们后面具体说,使用者传过来arraylist字符串,字符串new出自己的textview添加到viewgroup,与动画结合,使用timer定时器发送消息给Handler,Handler接到消息后,为当前子view(TextView)设置向上移出动画,为next子View(TextView)设置移入动画,如此循环可实现文字滚动效果(要算好index以防数组越界)。
接下来看下SwitcherViewGroup,重写了onMeasure、onLayout,之前我们说过,onMeasure最后一定要调用setMeasuredDimension设置测量的宽高,否则报错,所以onMeasure中调用super.onMeasure走到了view.java的onMeasure里,目的是为了为测量宽高赋值;接下来通过for循环测量所有子view的宽高值,ViewGroup中的measureChild我们之前有说过,这里就不再赘述啦。
SwitcherViewGroup.java
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
for (i in 0 until childCount) {
val child = getChildAt(i)
measureChild(child, child.measuredWidth, child.measuredHeight)
}
}
View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
接下来计算放置位置,用刚刚所有子view的测量宽高值计算子view的位置,这里简化的将top、left值设置为0,bottom、right值分别设置为top+子view测量高,left+子view测量宽。onDraw在View中实现,即TextView,至此完成自定义View。
SwitcherViewGroup.java
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
for (i in 0 until childCount) {
val view = getChildAt(i)
setChildFrame(view, 0, 0, view.measuredWidth, view.measuredHeight)
view.visibility = View.INVISIBLE
}
startTimer()
}
private fun setChildFrame(view: View, left: Int, top: Int, width: Int, height: Int) {
view.layout(left, top, left + width, top + height)
}
接下来看下外部使用自定义view的addContents,将每个字符串创建一个TextView加入自定义view中。
fun addContents(contents: ArrayList<String>) {
for (i in 0 until contents.size) {
val textView = TextView(context)
textView.textSize = 25f
textView.setTextColor(context.resources.getColor(android.R.color.holo_blue_light))
textView.text = contents[i]
textView.gravity = Gravity.CENTER
this.addView(textView)
}
}
与动画结合,使用timer定时器发送消息给Handler,Handler接到消息后,为当前子view(TextView)设置向上移出动画,为next子View(TextView)设置移入动画
private val mHandler: Handler = object : Handler() {
override fun handleMessage(msg: Message?) {
super.handleMessage(msg)
when (msg?.what) {
UPDATE_VIEW -> {
val view = getChildAt(getContentIndex(mIndex))
outAnimation(view)
val nextView = getChildAt(getContentIndex(mIndex + 1))
inAnimation(nextView)
mIndex++
}
}
}
}
private fun startTimer() {
if (mTimer == null) {
mTimer = Timer()
}
mTimer!!.schedule(object : TimerTask() {
override fun run() {
mHandler.sendEmptyMessage(UPDATE_VIEW)
}
}, 0, mDuration)
}
private fun outAnimation(view: View) {
view.visibility = View.VISIBLE
val animator = ObjectAnimator.ofFloat(view, "translationY", 0f, -view.height.toFloat())
val set = AnimatorSet()
set.play(animator)
set.duration = mAnimationDuration
set.start()
}
private fun inAnimation(view: View) {
view.visibility = View.VISIBLE
val animator = ObjectAnimator.ofFloat(view, "translationY", view.height.toFloat(), 0f)
val set = AnimatorSet()
set.play(animator)
set.duration = mAnimationDuration
set.start()
}
计算出实际index
private fun getContentIndex(index: Int): Int {
var realIndex = index % childCount
if (realIndex > childCount - 1) {
realIndex -= childCount
return realIndex
}
return realIndex
}
当然,循环时间间隔、动画效果、都可以开放一个函数,外面可以改变,也更加灵活。
原理包括例子都已经说完了,原理只是开始,最重要的还是实践,实践,实践,任务来了,不要急着在网上找资料,自己尝试手动写,之后再看实现方案,对比自己的思路及代码差在哪,就能发现自己薄弱点在哪,通过不断的实践才会真正掌握它。