前面已经介绍了View的事件分发以及处理机制,这次来学习下View的工作流程,学习之前要先了解一下View坐标系的相关知识。
1. View位置参数
我们知道安卓中坐标系是以屏幕的左上角为坐标原点,向右为x轴增大方向,向下为y轴增大方向。View坐标系也是这样,内部关系如下图所示:
其中getLeft()、getTop()、getRight()、 getBottom()分别对应View的四个属性:left、top、right、bottom,分别代表相对于View父容器的左上角的横坐标(left)、左上角的纵坐标(top)、右下角的横坐标(right)、右下角的纵坐标(bottom)。可以看出View的宽度width = getRight() - getLeft(),高度height = getBottom() - getTop(),安卓中还可以直接通过getWidth()和getHeight()方法用来获取View的宽度和高度。getX()、getY()、getRawX()、getRawY()分别代表触摸点相对于父容器以及屏幕而言的横坐标和纵坐标。
从安卓3.0开始,View增加了额外的几个参数:x,y,translationX、translationY。其中x和y是View左上角的坐标,translationX和translationY是View左上角相对于父容器的偏移量,这几个参数也是相对父容器的坐标,并且translationX和translationY的默认值为0。换算关系如下:
- x = left + translationX y = top + translationY
View 在平移的过程中,top 和left 表示的是原始左上角的位置信息,其值在绘制完毕后就不会再改变,此时发生改变的是x、y、translationX和translationY 这四个参数。具体关系见下图(图来自要点提炼|开发艺术之View):
2. View工作流程
在学习之前,首先对View的整体工作流程有个大概的了解,View的绘制基本由measure、layout、draw这三个步骤完成。
- measure:测量View的宽高
- layout:计算当前View以及子View的位置即确定View的最终宽高和四个顶点的位置
- draw:将View 绘制到屏幕上
在安卓开发之事件分发机制中我们提到过Android中通过在Activity中(具体来说在onCreate生命周期方法中)使用setContentView()方法来设置一个布局,在调用该方法后,ActivityManagerService会回调onResume()方法, 此时系统才会把整个DecorView 添加到PhoneWindow中,并让其显示出来,从而最终完成界面的绘制,布局内容就真正显示出来。而在DecorView被添加到Window的过程中,WindowManager起到了关键性的作用,最后交给ViewRootImpl做详细处理。
在之前的文章关于Hook相关知识的学习一中我们学习了Activity的启动过程,最后提到attachApplicationLocked(app)最终通过调用scheduleLaunchActivity方法创建启动Activity,当时没有继续跟踪scheduleLaunchActivity方法,其实在ApplicationThread的scheduleLaunchActivity()方法内内,会发送一个"LAUNCH_ACTIVITY"消息, mH (H对象,H继承自Handler,mH用来发送和处理ApplicationThread通过binder接受的AMS请求)处理"LAUNCH_ACTIVITY"时会调用handleLaunchActivity(), 而handleLaunchActivity()会分两步, 第一步调performLaunchActivity(),创建Activity的对象,依次调用它的onCreate(), onStart();第二步调handleResumeActivity(), 调用Activity对象的onResume()。那么结合前面说的在onCreate()方法中调用setContentView()将布局添加到PhoneWindow的内部类DecorView类之后,AMS会回调onResume()方法, 将DecorView 添加到PhoneWindow中,这里的回调onResume()方法,实际上还得从handleResumeActivity()进行分析。
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
...
//在这里执行performResumeActivity的方法中会执行Activity的onResume()方法
r = performResumeActivity(token, clearHide, reason);
...
if (r.window == null && !a.mFinished && willBeVisible) {
//获得当前Activity的PhoneWindow对象
r.window = r.activity.getWindow();
//获得当前phoneWindow内部类DecorView对象
View decor = r.window.getDecorView();
//设置窗口顶层视图DecorView可见度
decor.setVisibility(View.INVISIBLE);
//获取ViewManager对象,这里getWindowManager()实质上获取的是ViewManager的子类对象WindowManager
ViewManager wm = a.getWindowManager();
...
//获取ViewRootImpl对象
ViewRootImpl impl = decor.getViewRootImpl();
...
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
//标记根布局DecorView已经添加到窗口
a.mWindowAdded = true;
//在这里WindowManager将DecorView添加到PhoneWindow中
wm.addView(decor, l);
}
...
}
...
}
handleResumeActivity代码中,重要的在wm.addView(decor, l)这块,这里就是将DecorView添加到PhoneWindow中,那我们继续跟踪addView方法:
public void addView(View view, ViewGroup.LayoutParams params) {
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
继续跟踪WindowManagerGlobal类的实例方法addView:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
...
//获得ViewRootImpl对象root
root = new ViewRootImpl(view.getContext(), display);
...
// do this last because it fires off messages to start doing things
try {
//将传进来的参数DecorView设置到root中
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
...
}
}
addView中获得ViewRootImpl对象root,并调用了ViewRootImpl的setView方法,继续跟踪setView:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
//将顶层视图DecorView赋值给全局的mView
mView = view;
...
//标记已添加DecorView
mAdded = true;
...
//请求布局
requestLayout();
...
}
}
继续跟踪requestLayout()方法:
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
跟踪scheduleTraversals方法:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
}
}
...
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
...
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);
try {
performTraversals();
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
}
...
可以看到,ViewRootImpl的setView方法最终调用了performTraversals方法,该方法中重要的代码为:
private void performTraversals() {
//mView就是DecorView根布局
final View host = mView;
if (host == null || !mAdded)
return;
//是否正在遍历
mIsInTraversal = true;
//是否马上绘制View
mWillDrawSoon = true;
...
//顶层视图DecorView所需要窗口的宽度和高度
int desiredWindowWidth;
int desiredWindowHeight;
...
//在构造方法中mFirst已经设置为true,表示是否是第一次绘制DecorView
if (mFirst) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
//如果窗口的类型是有状态栏的,那么顶层视图DecorView所需要窗口的宽度和高度就是除了状态栏
if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
|| lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
//否则顶层视图DecorView所需要窗口的宽度和高度就是整个屏幕的宽高
DisplayMetrics packageMetrics =
mView.getContext().getResources().getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
}
}
...
//获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.width和lp.height表示DecorView根布局宽和高
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
//执行测量操作
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
//执行布局操作
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
//执行绘制操作
performDraw();
}
可以看到View的具体绘制就在performTraversals()方法中展开了,通过依次调用performMeasure()、performLayout()和performDraw()三个方法分别完成顶级View的measure、layout、draw三大流程。大致流程图如下图所示:
由上图可以看到在performMeasure()中又会调用measure()方法,在measure()方法中又会调用onMeasure()方法,在onMeasure()方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复完成整个View树的遍历。performLayout()、performDraw()的传递流程和performMeasure()是类似的。
接下来我们接着从源码角度对这个流程图进行分析,首先来看View的一个重要内部类MeasureSpec。
2.1. MeasureSpec
MeasureSpec是View的一个重要内部类,它参与了View的measure测量过程,在很大程度上决定了一个View的尺寸规格。我们就从performMeasure()方法入手看下MeasureSpec的作用:
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);
}
}
可以看到performMeasure方法中传入了childWidthMeasureSpec、childHeightMeasureSpec两个int类型的值,和前面说的一样,performMeasure中又调用了measure方法:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
//根据原有宽高计算获取不同模式下的具体宽高值
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
...
if (forceLayout || needsLayout) {
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//在该方法中子控件完成具体的测量
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
...
}
可以看到,widthMeasureSpec, heightMeasureSpec是MeasureSpec根据原有宽高计算获取不同模式下的具体宽高值。那么我们跟进看下MeasureSpec类:
public static class MeasureSpec {
//int类型占4个字节,其中高2位表示尺寸测量模式,低30位表示具体的宽高信息
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
...
//如下所示是MeasureSpec中的三种模式:UNSPECIFIED、EXACTLY、AT_MOST
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);
}
}
...
//获取尺寸模式
@MeasureSpecMode
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) {
// No need to adjust size for UNSPECIFIED mode.
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的值保存在一个int值(4个字节)当中。其中4字节的前两位表示模式mode,后30位表示大小size。即MeasureSpec = mode + size,在MeasureSpec当中一共存在三种mode:UNSPECIFIED、EXACTLY 和AT_MOST
- UNSPECIFIED:无限制,View对尺寸没有任何限制,View设置为多大就应当为多大,一般系统内部使用
- EXACTLY:精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Size所指定的值,对应LayoutParams中的match_parent
- AT_MOST:最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值,对应LayoutParams中的wrap_content
对于每一个View,MeasureSpec受自身的LayoutParams和父容器的MeasureSpec共同影响。而对顶级View来说,其MeasureSpec受窗口尺寸和自身的LayoutParams影响,MeasureSpec确定下来后,在下面要说的onMeasure方法中就可以确定View的测量宽高。
2.2. measure过程
2.2.1 View的measure
上面已经介绍过performMeasure方法调用了measure方法,这里就接着measure方法分析,该方法最后调用了onMeasure()方法完成子View的具体测量:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
可以看到onMeasure方法中涉及到以下三种方法:
- setMeasuredDimension:用来设置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);
}
- getDefaultSize:用来获取View测量后的宽高,View的最终大小在layout步骤确定,但是基本相等
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;
}
- getSuggestedMinimumWidth()/getSuggestedMinimumHeight():当View没有设置背景时,默认大小就是mMinWidth,这个值对应Android:minWidth属性,如果没有设置时默认为0;如果有设置背景,则默认大小为mMinWidth和mBackground.getMinimumWidth()当中的较大值。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
View的measure完成以后,通过getMeasuredWidth/Height方法就可以正确地获取到View的测量宽/高。需要注意的是,在某些极端情况
下,系统可能需要多次measure才能确定最终的测量宽/高,在这种情形下,最好在onLayout方法中去获取View的最终宽/高。
2.2.2 ViewGroup的measure
ViewGroup的测量过程与View有区别,因为不同的布局测量方式也都不同,除了要完成自己的measure过程外还需要遍历调用所有子元素的measure方法,各个子元素再去递归执行这个过程,因此没有对View的measure方法以及onMeasure方法进行重写。但是它提供了measureChildren()以及measureChild()方法帮助我们对子View进行测量。
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);//分别对单个view进行测量
}
}
}
measureChildren方法又最终调用了measureChild方法,对单个view进行测量:
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方法来进行测量。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) {
//当父View要求一个精确值时,为子View赋值
case MeasureSpec.EXACTLY:
//如果子view有自己的尺寸,则使用自己的尺寸
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//当子View是match_parent,将父View的大小赋值给子View
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
//如果子View是wrap_content,设置子View的最大尺寸为父View
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局给子View了一个最大界限
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//如果子view有自己的尺寸,则使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 父View的尺寸为子View的最大尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//父View的尺寸为子View的最大尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局对子View没有做任何限制
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//如果子view有自己的尺寸,则使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//因父布局没有对子View做出限制,当子View为MATCH_PARENT时则大小为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//因父布局没有对子View做出限制,当子View为WRAP_CONTENT时则大小为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
和前面说的一样,子元素的MeasureSpec受自身的LayoutParams和父容器的MeasureSpec共同影响:
- 父容器的MeasureSpec为UNSPECIFIED模式:父容器没有做出限制,如果子View有大小(自身的LayoutParams)则使用,如果没有则为0
- 父容器的MeasureSpec为EXACTLY模式:父容器采用精准模式有确切的大小,如果子View有大小则直接使用(自身的LayoutParams),如果子View为match_parent,那么子View也是精准模式并且大小等于父容器的剩余大小,如果子View为wrap_content,那么子View是AT_MOST模式,子View的大小不能大于父容器的剩余大小。
- 父容器的MeasureSpec为AT_MOST模式:父容器采用最大模式存在确切的大小,如果子View有大小则直接使用(自身LayoutParams),如果子View没有大小,子View不得超出父view的大小范围
综上所述,普通View的MeasureSpec的创建规则如下表所示:
了解完ViewGroup的measure过程后,我们返回到View measure过程中的getDefaultSize方法,从该方法中我们可以看到View 的宽/高由specSize决定,所以直接继承View的自定义控件需要重写onMeasure方法并设置wrap_ content时的自身大小,否则在布局中使用wrap_ content就相当于使用match_ parent。 因为如果自定义View在布局中使用wrap_ content,那么它的那么它的specMode是AT_ MOST模式,结合上表可知这种情况下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。很显然,View 的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用match_ parent 完全一致。
2.2.3 View的Measure过程中遇到的问题
例如我们想在Activity已启动的时候执行一个任务, 但是这一件任务需要获取某个View的宽/高。但是由于View的measure过程和Activity 的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量完毕了。如果View还没有测量完毕,那么获得的宽/高都是0。解决这个问题大致有四种办法:
- 1.Activity/View的onWindowsChanged()方法
onWindowFocusChanged()方法表示View已经初始化完毕了,宽/高已经准备好,这个时候去获取宽/高是没问题的。
onWindowFocusChanged()方法会被调用多次,具体来说,当Activity继续执行和暂停执行时,onWindowFocusChanged()均会被调用,如果频繁地进行onResume和onPause,那么onWindowFocusChanged()也会被频繁地调用。代码如下:
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if(hasWindowFocus){
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
}
- 2.View.post(runnable)方法
通过post将一个 Runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候View也已经初始化好了。代码如下:
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
});
}
- 3.ViewTreeObsever
使用 ViewTreeObserver 的众多回调方法可以完成这个功能,比如使用onGlobalLayoutListener 这个接口,当 View树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout 方法将被回调,这是获取View宽/高的好时机。伴随着View树的变化,这个方法也会被多次调用。
protected void onStart() {
super.onStart();
ViewTreeObserver viewTreeObserver=view.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
});
}
- 4.view.measure(int widthMeasureSpec, int heightMeasureSpec)
通过手动对View进行measure来得到View的宽/高。这种方法比较复杂,要分情况处理,根据View自身的LayoutParams来分:
(1)如果是match_parent的话,那么无法measure出具体的宽/高;
(2)如果是具体的数值(dp/px),比如宽/高都是100px,可通过如下方法:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec (100, MeasureSpec. EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec (100, MeasureSpec.EXACTLY);
view.measure (widthMeasureSpec, heightMeasureSpec);
(3)如果是wrap_parent
int widthMeasureSpec = MeasureSpec . makeMeasureSpec( (1 << 30)- 1 ,MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec . makeMeasureSpec( (1 << 30) - 1 ,MeasureSpec.AT_MOST);
view.measure (widthMeasureSpec, heightMeasureSpec);
详细说明可以参考《Android开发艺术探索》。
2.3 layout过程
layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup 的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout 方法,在layout方法中onLayout方法又会被调用。简单来说layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置,这里看下layout方法:
public void layout(int l, int t, int r, int b) {
...
//记录 view 原始位置
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//调用 setFrame 方法 设置新的 mLeft、mTop、mBottom、mRight 值,
//设置 View 本身四个顶点位置
//并返回 changed 用于判断 view 布局是否改变
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//第二步,如果 view 位置改变那么调用 onLayout 方法设置子 view 位置
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//调用 onLayout
onLayout(changed, l, t, r, b);
...
}
...
}
layout方法中首先通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft、mRight、 mTop和mBottom这四个值,View的四个顶点一旦确定, 那么View在父容器中的位置也就确定了;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,和onMeasure方法类似,onLayout的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。感兴趣的话可以去看下《Android开发艺术探索》中对于LinearLayout的onLayout方法的分析。
2.4 draw过程
draw的作用是将View绘制到屏幕上面。View的draw过程大致包含以下几步:
- 绘制背景。
- 如果有必要,保存当前canvas层。
- 绘制View的内容。
- 绘制子View。
- 如果有必要,绘制边缘、阴影等效果。
- 绘制装饰,例如滚动条等。
直接看draw的源码:
public void draw(Canvas canvas) {
int saveCount;
// 1. 绘制背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 2.如果可能的话跳过第2步和第5步(一般情况)
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {
// 3. 绘制View的内容
if (!dirtyOpaque) onDraw(canvas);
// 4. 绘制子View
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// 6. 绘制装饰,如滚动条等等。
onDrawForeground(canvas);
return;
}
}
/**
* 1.绘制View背景
*/
private void drawBackground(Canvas canvas) {
//获取背景
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
//获取便宜值scrollX和scrollY,如果scrollX和scrollY都不等于0,则会在平移后的canvas上面绘制背景
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
/**
* 3.绘制View的内容,该方法是一个空的实现,根据不同的内容进行不同的设置,自定义View中就需要重写该方法
*/
protected void onDraw(Canvas canvas) {
}
/**
* 4. 绘制子View。该方法在View当中是一个空的实现,在各个业务当中自行处理。
* 在ViewGroup当中对dispatchDraw方法做了实现,主要是遍历子View并调用子元素的draw方法,如此draw事件就一层层地传递了下去。
*/
protected void dispatchDraw(Canvas canvas) {
}
3.自定义View
《Android开发艺术探索》中把自定义View大致分为了4类,我们通过Demo学习下前两类,后两类和前两类是相似的:
- 1.继承View重写onDraw方法
该方法主要用于实现一些不规则的效果,即这种效果需要静态或者动态地显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写onDraw方法。采用这种方式需要自己支持wrap_ content, 并且padding也需要自己处理。 - 2.继承ViewGroup派生特殊的Layout
该方法主要用于实现自定义的布局,即除了LinearLayout 、RelativeLayout 、FrameLayout这几种系统的布局之外,我们重新定义一种新布局,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实现。采用这种方式需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。 - 3.继承特定的View (比如TextView)
该方法一般是用于扩展某种已有的View的功能,不需要自己支持wrap_content 和padding等。 - 4.继承特定的ViewGroup (比如LinearLayout)
该方法也比较常见,当某种效果看起来很像几种View组合在一起的时候, 可以采用这种方法来实现。采用这种方法不需要自已处理ViewGroup的测量和布局这两个过程。一般来说方法2能实现的效果方法4也都能实现,两者的主要差别在于方法2更接近View的底层。
在写Demo之前,还要了解下自定义View的一些注意事项:
- 1.View支持wrap_ content
直接继承View或者ViewGroup的控件,需要需要重写onMeasure方法并设置wrap_ content时的自身大小。 - 2.View支持padding
直接继承View的控件,需要在draw方法中处理padding;直接继承自ViewGroup 的控件需要在onMeasure和onLayout
中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。 - 3.尽量不要在View中使用Handler
除非很明确地要使用Handler来发送消息,否则View内部本身就提供了post系列的方法,完全可以替代Handler的作用。 - 4.View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
View 中如果有线程或者动画需要停止时,那么onDetachedFromWindow是一个很好的时机。当包含此View的Activity退出或者当前View被remove时,View 的onDetachedFromWindow方法会被调用。同时,当View变得不可见时我们也需要停止线程和动画,否则有可能会造成内存泄漏。 - 5.避免View滑动冲突
接下来通过Demo学习一下。
3.1.继承View重写onDraw方法,并对wrap_ content、padding进行处理
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#ffffff"
tools:context=".MainActivity">
<com.demo.myview.CircleView
android:id="@+id/circleView"
android:background="#000000"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
android:padding="20dp"
app:circle_color="@color/colorAccent"/>
</LinearLayout>
这里可以看到自定义View的layout_width为wrap_content,前面介绍过这里其实还是match_parent的效果,要在onMeasure中进行处理;然后这里指定的padding其实是不会起作用的,需要在draw方法中处理padding;最后 app:circle_color是自定义的属性,自定义属性的添加方法如下:
1)首先在values目录下新建attrs.xml,支持的自定义属性为color,除此之外还支持string、float、boolean等:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
2)在View的构造方法中解析自定义属性的值(这里是circle_color)并做相应处理
3)在布局文件中使用自定义属性,例如上面布局文件中app:circle_color=@color/colorAccent
接下来看自定义View,在这个自定义View的构造方法中要解析自定义属性的值,onDraw方法中要处理padding,onMeasure中要处理自定义View的layout_width为wrap_content的问题:
public class CircleView extends View {
private Paint paint;
private int color = Color.RED;
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CircleView);
color = a.getColor(R.styleable.CircleView_circle_color,Color.RED);
a.recycle();
init();
}
private void init() {
paint = new Paint();
paint.setColor(color);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(width,height)/2;
canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radius,paint);
}
@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);
}
}
}
在onMeasure中,我们给View指定了一个默认的内部宽/高( 200/200),并在wrap_ content 时设置此宽/高。对于非wrap_ content 情形,则沿用系统的测量值。最终效果如下:
3.2.继承ViewGroup派生特殊的Layout
前面提到过该方法需要合适地处理ViewGroup自己的测量、布局这两个过程,还要同时处理子元素的测量和布局过程。这里就通过继承ViewGroup模仿实现LinearLayout的垂直布局。
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.demo.myviewgroup.MyViewGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorAccent">
<Button
android:layout_width="50dp"
android:layout_height="wrap_content"
android:text="Button1"/>
<Button
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="Button2"/>
<Button
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Button3"/>
</com.demo.myviewgroup.MyViewGroup>
</LinearLayout>
MyViewGroup代码:
public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context) {
super(context);
}
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//对所有的子View进行测量,会触发每个子View的onMeasure函数 measureChildren中又会调用measureChild分别对单个View进行测量
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childCount = getChildCount();
if(childCount == 0){//判断是否有子元素
setMeasuredDimension(0, 0);//没有子元素就直接将宽高设置为0
}else if(widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
//如果宽高都采用了wrap_content的话,将高度设置为所有子View的高度相加,宽度设为子View中最大的宽度
int height = getTotalHeight();
int width = getMaxChildWidth();
setMeasuredDimension(width, height);
}else if(heightMode == MeasureSpec.AT_MOST){
//如果只有高采用了wrap_content,宽度设置为ViewGroup自己的测量宽度,高度设置为所有子View的高度总和
setMeasuredDimension(widthSize, getTotalHeight());
}else if(widthMode == MeasureSpec.AT_MOST){
//如果只有宽采用了wrap_content,宽度设置为子View中宽度最大的值,高度设置为ViewGroup自己的测量值
setMeasuredDimension(getMaxChildWidth(), heightSize);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {//完成子元素的定位
int count = getChildCount();
//记录当前的高度位置
int curHeight = t;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if(child.getVisibility()!=View.GONE){//如果这个子元素不处于GONE状态
int height = child.getMeasuredHeight();
int width = child.getMeasuredWidth();
//把子View放到合适的位置上,参数分别是子View矩形区域的左、上、右、下边
child.layout(l, curHeight, l + width, curHeight + height);
curHeight += height;//高度叠加 所以效果和垂直方向的LinearLayout相似
}
}
}
private int getMaxChildWidth() {
int childCount = getChildCount();
int maxWidth = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getMeasuredWidth() > maxWidth)
maxWidth = childView.getMeasuredWidth();
}
return maxWidth;
}
private int getTotalHeight() {
int childCount = getChildCount();
int height = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
height += childView.getMeasuredHeight();
}
return height;
}
}
效果如下:
4. SurfaceView
Android系统提供了View 进行绘图处理,View 可以满足大部分的绘图需求,但在某些时候,却也有些心有余而力不足,特别是在进行一.些开发的时候。我们知道,View 通过刷新来重绘视图,Android 系统通过发出VSYNC信号来进行屏幕的重绘,刷新的间隔时间为16ms。
如果在16ms内View完成了你所需要执行的所有操作,那么用户在视觉上,就不会产生卡顿的感觉;而如果执行的操作逻辑太多,特别是需要频繁刷新的界面上,例如游戏界面,那么就会不断阻塞主线程,从而导致画面卡顿。为了避免这一问题的产生,Android 系统提供了SurfaceView 组件来解决这个问题。SurfaceView与View的区别主要体现在如下几点:
- View主要适用于主动更新的情况下,而SurfaceView主要适用于被动更新,例如频繁地刷新。
- View在主线程中对画面进行刷新,而SurfaceView通常会通过一个子线程来进行页面的刷新。
- View在绘图时没有使用双缓冲机制,而SurfaceView 在底层实现机制中就已经实现了双缓冲机制。
总结成一句话就是,如果你的自定义View需要频繁刷新,或者刷新时数据处理量比较大,就可以使用SurfaceView来替代View。具体的使用方法可以参考《Android群英传》。
参考以下内容,如有理解错误之处还请指出:
- 自定义View心法——View工作流程
- Android自定义View全解
- 要点提炼|开发艺术之View
- 了解ViewRoot和DecorView
- 自定义View,有这一篇就够了
- 《Android开发艺术探索》
- 《Android群英传》