一、初识ViewRoot和DecorView
Activity:
Activity并不负责视图控制,它只是控制生命周期和处理事件。真正控制视图的是Window。一个Activity包含了一个Window,Window才是真正代表一个窗口。Activity就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与Window、以及View进行交互。
Window:
Window是视图的承载器,内部持有一个 DecorView,而这个DecorView才是 view 的根布局。Window是一个抽象类,实际在Activity中持有的是其子类PhoneWindow。PhoneWindow中有个内部类DecorView,通过创建DecorView来加载Activity中设置的布局R.layout.activity_main。Window 通过WindowManager将DecorView加载其中,并将DecorView交给ViewRoot,进行视图绘制以及其他交互。
ViewRoot:
对应于ViewRootImpl类,他是连接WindowManager和DecorView的纽带,View的三大流程(测量(measure),布局(layout),绘制(draw))均是通过ViewRoot来完成的.在ActivityThread中,当Activity 对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。ViewRoot并不属于View树的一份子。从源码实现上来看,它既非View的子类,也非View的父类,但是,它实现了ViewParent接口,这让它可以作为View的名义上的父视图。RootView继承了Handler类,可以接收事件并分发,Android的所有触屏事件、按键事件、界面刷新等事件都是通过ViewRoot进行分发的。
DecorView:
作为顶级View,它内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两个部分,上面是标题栏,下面是内容栏.setContentView所设置的布局文件其实被加到内容栏之中的,内容栏id是contnet.其实DecorView和Content都是FrameLayout。
WindowManager
是一个接口,里面常用的方法有:添加View,更新View和删除View。主要是用来管理 Window 的。WindowManager 具体的实现类是WindowManagerImpl。最终,WindowManagerImpl 会将业务交给 WindowManagerGlobal 来处理。
WindowManagerService(WMS)
负责管理各 app 窗口的创建,更新,删除, 显示顺序。运行在 system_server 进程。
DecorView的创建
1、Activity的setContentView
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
2、PhoneWindow的setContentView:调用了getWindow().setContentView,其中getWindow返回的mWindow是在activity的attach方法中初始化的,实际类型是PhoneWindow;
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
3、调用installDecor方法
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor(-1);//return new DecorView...
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
4、调用generateDecor初始化了mDecor,这个DecorView就是Activity中的根View,继承FrameLayout
protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}
5、DecorView的onResourcesLoaded中通过调用LayoutInflater.inflate()解析布局赋值给mContentRoot,并在DecorView的onDraw中绘制
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
mBackgroundFallback.draw(this, mContentRoot, c, mWindow.mContentParent,
mStatusColorViewState.view, mNavigationColorViewState.view);
}
Decorview添加到屏幕中
1、ActivityThread 中的 handleResumeActivity 方法
// ActivityThreadpublic void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
...... final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);final int forwardBit = isForward
? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
// If the window hasn't yet been added to the window manager,
// and this guy didn't finish itself or start another activity,
// then go ahead and add the window.
boolean willBeVisible = !a.mStartedActivity;
if (!willBeVisible) {
try {
willBeVisible = ActivityManager.getService().willActivityBeVisible(
a.getActivityToken());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
if (r.window == null && !a.mFinished && willBeVisible) {
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 {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}
// If the window has already been added, but during resume
// we started another activity, then don't yet make the
// window visible.
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
// Get rid of anything left hanging around.
cleanUpPendingRemoveWindows(r, false /* force */);
// The window is now visible if it has been added, we are not
// simply finishing, and we are not starting another activity.
if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
if (r.newConfig != null) {
performConfigurationChangedForActivity(r, r.newConfig);
if (DEBUG_CONFIGURATION) {
Slog.v(TAG, "Resuming activity " + r.activityInfo.name + " with newConfig "
+ r.activity.mCurrentConfig);
}
r.newConfig = null;
}
if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward);
WindowManager.LayoutParams l = r.window.getAttributes();
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
!= forwardBit) {
l.softInputMode = (l.softInputMode
& (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
| forwardBit;
if (r.activity.mVisibleFromClient) {
ViewManager wm = a.getWindowManager();
View decor = r.window.getDecorView();
wm.updateViewLayout(decor, l);
}
}
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
// 这里也会调用addview
r.activity.makeVisible();
}
}
r.nextIdle = mNewActivities;
mNewActivities = r;
if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r);
Looper.myQueue().addIdleHandler(new Idler());
}
上面的代码主要做了以下几件事:
-
- 获取到 DecorView,设置不可见,然后通过 wm.addView(decor, l) 将 view 添加到 WindowManager;
-
- 在某些情况下,比如此时点击了输入框调起了键盘,就会调用 wm.updateViewLayout(decor, l) 来更新 View 的布局。
-
- 这些做完以后,会调用 activity 的 makeVisible ,让视图可见。如果此时 DecorView 没有添加到 WindowManager,那么会添加。
// Activity void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
2、wm.addView 的逻辑。 WindowManager 的实现类是 WindowManagerImpl,而它则是通过 WindowManagerGlobal 代理实现 addView 的,我们看下 addView 的方法:
// WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
// ......
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
在这里,实例化了 ViewRootImpl 。同时调用 ViewRootImpl 的 setView 方法来持有了 DecorView。此外这里还保存了 DecorView ,Params,以及 ViewRootImpl 的实例。
3、ViewRootImpl 的 setView 方法
/**
* We have one child
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
......
mAdded = true;
int res; /* = WindowManagerImpl.ADD_OKAY; */
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
......
}
}
}
这里先将 mView 保存了 DecorView 的实例,然后调用 requestLayout() 方法,以完成应用程序用户界面的初次布局。
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
因为是 UI 绘制,所以一定要确保是在主线程进行的,checkThread 主要是做一个校验。接着调用 scheduleTraversals 开始计划绘制了。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
这里主要关注两点:
mTraversalBarrier : Handler 的同步屏障。它的作用是可以拦截 Looper 对同步消息的获取和分发,加入同步屏障之后,Looper 只会获取和处理异步消息,如果没有异步消息那么就会进入阻塞状态。也就是说,对 View 绘制渲染的处理操作可以优先处理(设置为异步消息)。
mChoreographer: 编舞者。统一动画、输入和绘制时机。也是这章需要重点分析的内容。
mTraversalRunnable :TraversalRunnable 的实例,是一个Runnable,最终肯定会调用其 run 方法:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
doTraversal,如其名,开始绘制了,该方法内部最终会调用 performTraversals 进行绘制。
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
参考: Android View 绘制流程
问题:
- 首次 View 的绘制流程是在什么时候触发的?
- ViewRootImpl 创建的时机?
- ViewRootImpl 和 DecorView 的关系是什么?
- DecorView 的布局是什么样的?
- DecorView 的创建时机?
- setContentView 的流程
- Activity、PhoneWindow、DecorView、ViewRootImpl 的关系?
- PhoneWindow 的创建时机?
- 如何触发重新绘制?
二、理解MeasureSpec
1、MeasureSpec:测量规格,决定了一个View的尺寸规格;
父容器影响View的MeasureSpec的创建过程.系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个MeasureSpec来测量出View的宽高.
2、MeasureSpec代表一个32位int值;
高两位代表SpecMode,指测量模式;
低30位代表SpecSize,指在某种测量模式下的规格大小;
SpecMode和SpecSize可以打包为一个MeasureSpec,一个MeasureSpec也可以解包的形式来得出其原始的SpecMode和SpecSize;
3、SpecMode测量模式:
UNSPECIFIED= 0 << MODE_SHIFT:即: 00000000 00000000 00000000 00000000父容器不对子View有任何限制,子View要多大给多大,有系统内部调用,我们不需要研究,例如ScrollView
EXACTLY =1<< MODE_SHIFT:即: 01000000 00000000 00000000 00000000父容器已经测量出子View所需要的大小,即measureSpec中封装的specsize,对应于LayoutParams中的match_parent和设置的固定值
AT_MOST=2 << MODE_SHIFT:即: 10000000 00000000 00000000 00000000父窗口限定了一个最大值给子View即SpecSize,对应于LayoutParams中的wrap_content
size & ~MODE_MASK是获得SpecSize,mode & MODE_MASK是获得SpecMode,之后再或运算即可得到MeasureSpec。
/**
* A MeasureSpec encapsulates the layout requirements passed from parent to child.
* Each MeasureSpec represents a requirement for either the width or the height.
* A MeasureSpec is comprised of a size and a mode. There are three possible
*/
* MeasureSpec封装从父对象传递给孩子的布局要求。
* 每个MeasureSpec表示宽度或高度的要求。
* 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(int size,int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK)
}
}
}
4、MeasureSpec和LayoutParams的对应关系
子View的LayoutParams需要和父容器的MeasureSpec一起才能决定子View的MeasureSpec,从而进一步决定View的宽高.
- 当View采用固定宽/高时(即设置固定的dp/px),不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY模式,并且大小遵循我们设置的值。
当View的宽/高是match_parents时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式那么View也是最大模式并且其大小不会超过父容器的剩余空间 - 当View的宽/高是wrap_content时,View的MeasureSpec都是AT_MOST模式并且其大小不能超过父容器的剩余空间。
- 只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以确定出子元素的MeasureSpec,进一步便可以确定出测量后的大小。
三、View的工作流程
View的工作流程主要指测量measure,布局layout,绘制draw这三大流程,其中measure确定View的测量宽高,layout确定View的最终宽高和四个顶点的位置,而draw将View绘制到屏幕上.
要知道,当用户点击屏幕产生一个触摸行为,这个触摸行为则是通过底层硬件来传递捕获,然后交给ViewRootImpl,接着将事件传递给DecorView,而DecorView再交给PhoneWindow,PhoneWindow再交给Activity,然后接下来就是我们常见的View事件分发了。
硬件 -> ViewRootImpl -> DecorView -> PhoneWindow -> Activity
1、measure过程
measure过程主要分为两类:
1、ViewGroup的measure过程
ViewGroup的measureChildren会遍历子View调用measureChild方法;
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
//遍历子View调用measureChild方法;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
measureChild方法会获取子View的LayoutParams,并调用getChildMeasureSpec获取子元素的MeasureSpec,最后调用子View的measure()方法进行测量
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//获取子View的LayoutParams
final LayoutParams lp = child.getLayoutParams();
//获取子元素的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//调用子View的measure()方法进行测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
getChildMeasureSpec会根据父View的MeasureSpec,结合子View的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;
//根据父View的specMode判断
switch (specMode) {
case MeasureSpec.EXACTLY://精确模式,尺寸的值是多少组件的长或宽就是多少
//根据子View的size判断
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;
}
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;
}
//将size和mode拼接成一个int值
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
ViewGroup并没有提供onMeasure()方法,而是让其子类来各自实现测量的方法,究其原因就是ViewGroup有不同的布局的需要很难统一,具体的可以查看各个ViewGroup源码实现.
2、View的measure过程
viewGroup测量子view最终都会调用到child.measure,那么来看一下View.measure方法
View. View.measure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
if (forceLayout || needsLayout) {
...
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//如果没有缓存就调用onMeasure进行测量
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
} else {
//有缓存就从缓存中取
long value = mMeasureCache.valueAt(cacheIndex);
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
...
}
...
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
//缓存到数组中
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
View.onMeasure方法
上面的View.measure最终是调用了onMeasure方法进行测量的,其中又调用了setMeasuredDimension方法来设置View的宽高
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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);
}
上面onMeasure中调用了getDefaultSize方法,根据measureSpec的不同返回size
public static int getDefaultSize(int size, int measureSpec) {
//从onMeasure中可看到这个size是getSuggestedMinimumWidth,getSuggestedMinimumHeight
int result = size;
//specMode是View的测量模式
int specMode = MeasureSpec.getMode(measureSpec);
//specSize是View的测量大小
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;
}
//如果View没有设置背景则取值为mMinWidth,
//对应android:minWidth设置的值或setMinimumWidth的值
//如果View设置了背景在取值为mMinWidth, mBackground.getMinimumWidth()的最大值,
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
//mBackground是Drawable类型的, intrinsicWidth得到的是这个Drawable的固有的宽度,
//如果固有宽度大于0则返回固有宽度,否则返回0。
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
注意:四种办法获取View的宽高
- Activity/View#onWindowFocusChanged()
- View.post(runnable)投递到消息队列的尾部,然后等待Looper调用此Runnable.
- ViewTreeObserver#addonGlobalLayoutListener()
- View.measure();
2、layout过程
Layout的作用是ViewGroup用来确定子元素的位置.
layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置.
layout分为两类:
1、View的layout
View的layout()方法如下
//入参是View四个点的坐标(相对于父View的)
public void layout(int l, int t, int r, int b) {
...
//setFrame()设置View的四个顶点的值,也就是mLeft 、mTop、mRight和 mBottom的值
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//调用了onLayout
onLayout(changed, l, t, r, b);
...
}
...
}
layout中调用了onLayout方法,其中并没有具体的实现,因为确定位置时根据不同的控件有不同的实现,所以在View和ViewGroup中均没有实现onLayout()方法。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
2、ViewGroup的layout
View和ViewGroup中均没有实现onLayout()方法,所以具体的子类会重写onLayout方法,
具体的实现根据子类功能不同而不同.
3、draw过程
View的绘制过程遵循如下几步:
- 绘制背景,background.draw()
- 绘制自己,onDraw()
- 绘制children(dispatchDraw)
- 绘制装饰(onDrawScrollBars)
View.draw
view的draw方法代码如下,其中官方注释写的也很清除,总共分了7步
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)
* 7. If necessary, draw the default focus highlight
*/
// Step 1, draw the background, if needed
//如果设置了背景,则绘制背景
drawBackground(canvas);
...
// Step 2, save the canvas' layers
//保存canvas层
...
saveCount = canvas.getSaveCount();
int topSaveCount = -1;
int bottomSaveCount = -1;
int leftSaveCount = -1;
int rightSaveCount = -1;
int solidColor = getSolidColor();
if (solidColor == 0) {
if (drawTop) {
topSaveCount = canvas.saveUnclippedLayer(left, top, right, top + length);
}
if (drawBottom) {
bottomSaveCount = canvas.saveUnclippedLayer(left, bottom - length, right, bottom);
}
if (drawLeft) {
leftSaveCount = canvas.saveUnclippedLayer(left, top, left + length, bottom);
}
if (drawRight) {
rightSaveCount = canvas.saveUnclippedLayer(right - length, top, right, bottom);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// Step 3, draw the content
//调用onDraw绘制自身内容
onDraw(canvas);
// Step 4, draw the children
//调用dispatchDraw绘制子View
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
//绘制附加效果
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
...
// 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绘制装饰品
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
//绘制默认的焦点高亮显示
drawDefaultFocusHighlight(canvas);
if (isShowingLayoutBounds()) {
debugDrawFocus(canvas);
}
}
View.onDraw
draw中调用了onDraw,也是没有具体实现,需要子view自己去实现
protected void onDraw(Canvas canvas) {
}
注意:
setWillNotDraw方法:如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化.默认情况:View没有开启这个标记位,ViewGroup默认开启这个标记位;
四、自定义View
自定义View一般可以继承View,ViewGroup,已有的系统控件或其他自定义控件,根据需要重写onMeasure,onLayout,onDraw,onTouchEvent等方法;
自定义View三个构造方法使用
public class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
自定义View三个构造方法使用:
1、一个参数:java代码创建视图的时候被调用;
2、两个参数:这个是在xml创建但是没有指定style的时候被调用 ;
3、三个参数:给View设置了基本的style,就使用这个style中的属性。
1、自定义View的分类:
- 继承View重写onDraw()方法
不只是要实现onDraw()方法,而且在实现过程中还要考虑到wrap_content属性以及padding属性的设置;为了方便配置自己的自定义View还会对外提供自定义的属性,另外如果要改变触控的逻辑,还要重写onTouchEvent()等触控事件的方法。 - 继承系统控件(比如TextView)
在系统控件的基础上进行拓展,添加新的功能或修改显示效果,一般在onDraw方法中处理。 - 自定义组合控件
用已有的控件在xml中组合起来重新定义成一个新的控件,例如一个titleBar会在很多页面用到,那么就可以抽出一个组合控件进行封装,复用起来会方便很多。
2、自定义View须知:
- 让View支持wrap_content,否则会充满父布局剩余空间;
- 让View支持Padding,否则padding会失效
- 不要在View中使用Handler,View内部本身提供了post系列的方法,可以替代;
- View中线程或者动画要及时停止,参考View.onDetachedFromWindow;
- View如果可以滑动,处理好滑动冲突;
3、自定义View思想:
掌握自定义基本功,比如View的绘制原理,弹性滑动,滑动冲突等;
熟练掌握基本功以后,面对新的自定义View,能够对其分类并选择合适的实现思路.
五、LayoutInflater介绍
1、LayoutInflater获取方式
//1、getLayoutInflater()方式
// Activity中使用:PhoneWindow创建调用LayoutInflater.from()
LayoutInflater layoutInflater = getLayoutInflater();
// Fragment中使用:FragmentHostCallback创建调用LayoutInflater.from()
LayoutInflater layoutInflater = getLayoutInflater(savedInstanceState);
//2、getSystemService()方式
LayoutInflater inflater = (LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//3、LayoutInflater.from()方式
LayoutInflater inflater = LayoutInflater.from(this);
//4、View.inflate()方式:View中创建调用LayoutInflater.from()
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
LayoutInflater factory = LayoutInflater.from(context);
return factory.inflate(resource, root);
}
总结:所有方式最终都是调用LayoutInflater.from(this);静态方法
2、LayoutInflater静态方法
public static LayoutInflater from(@UiContext Context context) {
LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
3、LayoutInflater.inflate()方法重载
//第一种
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
//第二种
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
//第三种
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
return inflate(parser, root, root != null);
}
//第四种
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
advanceToRootNode(parser);
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(inflaterContext, attrs)
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
总结:最终都是调用第四种方法;最常用第二种方法;
参数详解:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
resource:布局资源id;
root:resource生成view对象要填入的父布局。
为null,则返回的view就是布局资源,并且它的根节点的宽高属性会失效;
不为null,需要参照第三个参数;
attachToRoot:是否将resource生成view对象填入root,以root作为最终返回view的根布局。
false,root不为null,则提供root的LayoutParams约束resource生成的view;true,root不为null,以root作为最终返回view的根布局;