View体系详解(2)
前言:看了大概一个月SystemUI的相关源码,里面关于自定义View的知识比较多,迫使自己要去了解以前不太懂的显示子系统的知识,以前只知道一些粗略的view知识,如它是用来显示具体画面的,它的载体是window,它可以复写事件处理函数去处理某些点击事件,自定义view要实现onMeasure, onLayout, onDraw等,但是一直比较模糊,只是知道个大概,经过一阵子的源码和博客的阅读,对view体系有了许多新认识和领悟,因此记录下来。计划分以下几部分:
View体系详解(1):View体系总览
View体系详解(2):自定义View流程以及系统相关行为
View体系详解(3):View事件处理机制
写的不好请理解,由于自己知识水平和技术经验所限,不可避免有错漏的地方,恳请指正。
自定义View流程以及系统相关行为
1.首先,我们要知道系统绘制view的过程,我们才能知道view是怎么显示出来的,所以在写自定义view之前先缕清系统如何绘制view。在这里,我们可以从ViewManager的接口函数addView()来看看流程,分别看看ViewGroup和WindowManager这两个实现类对于这个函数的逻辑处理,我们就能知道系统是怎么管理这么多的view了。
从View体系详解(1)中我们知道了ViewGroup是view的根节点,每个view的mParent对象是它所属的ViewGroup对象的实例,在ViewGroup中的addView实现如下:
```java
@Override
public void addView(View child, LayoutParams params) {
addView(child, -1, params); //调到内部函数addView()
}
------------------------------------------------------------------------------------------------------
public void addView(View child, int index, LayoutParams params) {
***省略
requestLayout(); //该函数会重新触发绘制逻辑
invalidate(true);
addViewInner(child, index, params, false);
}
------------------------------------------------------------------------------------------------------
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
***省略
if (index < 0) {
index = mChildrenCount; //一般插入的index为-1,这里改变它的值,让它能插入到线性表的后面
}
addInArray(child, index); //把子view插入到线性表中
// tell our children
if (preventRequestLayout) {
child.assignParent(this);
} else {
child.mParent = this; //告诉子view的父节点是自己,也就是给View中的变量mParent赋值为自己
}
***省略
}
所以在ViewGroup的addView()实现中,是把子view加到自己的线性表中,构成view树去管理,然后触发一次绘制流程,把添加到view树中的子view去显示出来,触发绘制的流程是requestLayout(), 可以看到ViewGroup中并没有相关实现,而是在父类View中:
public void requestLayout() {
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout(); //调用到ViewRootImpl的requestLayout
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
回忆一下,一般子view的mParent,也就是父节点为ViewGroup,那么ViewGroup的父节点,是谁呢?就是ViewRootImpl。由此我们可以知道,绘制流程都在ViewRootImpl中,其实这也合理,一旦view树的状态改变了,我们就该从根节点去遍历一遍,然后递归绘制。如此一来,我们就再次验证了我们之前说过的view的层级关系,子view的parent是ViewGroup,在子view中调用requestLayout,那么是调用到了ViewGroup中的requestLayout,由于ViewGroup中没有相关实现,那么又调用到它的父类View的requestLayout,它里面的parent变量是ViewRootImpl,所以最终的绘制流程都是在ViewRootImpl中触发。
那么绘制过程是如何进行的呢?答案在requestLayout函数实现中。
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread(); //检查线程
mLayoutRequested = true;
scheduleTraversals();
}
}
------------------------------------------------------------------
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); //绘制任务触发
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
这个checkThread()是用来检查触发绘制的线程是否是创建这个view的线程,不是则抛出异常,所以一般我们需要在主线程绘制触发这个函数,否则会报错。那么非主线程,自己new的一个Thread对象去创建了view,然后在这个子线程触发绘制,能否可行?答案是可以的,只要你在创建的线程中去触发绘制流程就行了,只不过安卓一般是在主线程创建视图。绘制任务主要是下面的函数来执行,这里是我们view绘制的主逻辑:
private void performTraversals() {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); //开始测量流程
performLayout(lp, mWidth, mHeight); //开始布局流程
performDraw(); //开始绘画流程
}
该函数差不多800行代码,很长,但是我们不去扣细节,具体问题再具体分析,相关绘制的操作入口就在这个函数,如果遇到了相关问题,我们可以进来再看。这个函数关于view的,主要就是上面的三个函数,当然还有其他的,如dispatchAttachedToWindow(mAttachInfo, 0)的调用,是把一些重要信息通过父节点传递给子节点等,但是这些都挑出来说就太多了,至于surface相关,以及与native层的通信等,也一概先不谈,因为这些都不是我们可以操作的到的层面,与我们相关的就是三大流程,逐一分析一下,还是只挑出来重要的逻辑流程,而不去扣细节:
(1)performMeasure:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
-----------------------------------------------------------------------------------------------------
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
-----------------------------------------------------------------------------------------------------
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
Measure的过程就是从根节点调用Mesure,然后测量出来大小,把高度和宽度计算出来。至于MeasureSpec,是一个int型,32个bit位,它用两位去记录一个type,EXACTLY, AT_MOST,UNSPECIFIED,比如你在layout布局中定义了width为80dp,即view的宽度为80个像素点,那么它就对应了EXACTLY,如果定义了WRAP_CONTENT,那么就它对应了AT_MOST。至于后面的30位,就是用来保存size,即大小。
问题来了,从代码看,就测量了父节点一个view么?肯定不是,首先ViewGroup是无法实例化的,它是一个抽象类,真正添加到ViewRootImpl中的父节点是具体的ViewGroup的子类,比如FrameLayout,所以当我们在绘制流程触发的时候,它实际触发的时候FrameLayout中的onMeasure函数,下面以FrameLayout复写的onMeasure函数为例,看看父节点是怎么把子view的测量结果也确定下来:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); //递归测量,其中父节点的MeasureSpec会影响子view
}
}
// Account for padding too
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight()
- getPaddingTopWithForeground() - getPaddingBottomWithForeground()
- lp.topMargin - lp.bottomMargin);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec); //mode为MATCH_PARENT的再测量一遍
}
}
}
我们可以看到,这是一个递归遍历测量的过程,从根节点一直往下测,所有的子view都会被遍历。这里我们要注意影响长宽的因素,有:(1)父view的MeasureSpec的参数,这个一般是定义再xml文件中的属性,代码中也可以设置;(2)view自己本身的LayoutParam,即布局参数,基本的长宽等参数在这个属性类中。所以view 的大小,是由自己的属性值和父view的大小决定的,子view如果定义的值大于父view,它也最多只能获得父view的剩余空间。
(2)performLayout:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); //host就是根节点的view
}
----------------------------------------------------------------------------------------------
public void layout(int l, int t, int r, int b) { //屏幕的左边和上边均为0,右边和下边的值分别为宽度和高度,因为安卓的原点处于屏幕左上方
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
}
}
-----------------------------------------------------------------------------------------------
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
流程基本与Measure差不多,在View的layout被调用后,onLayout的方法紧接着也会被调用,该方法在View中为空实现,因为它不是必须要实现的,但是如果是根节点view,那么必须实现,因为子view很多,需要一个布局的方案,由于ViewGroup为抽象类,那么实现就在某个Layout类中了,再以FrameLayout为例:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
----------------------------------------------------------------------------------------------------------------------------
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
这里面就是计算出来坐标点的位置,然后遍历view树去计算自己的位置,影响的因素主要是父容器的剩余位置,可以看看Gravity的各个变量是怎么影响到layout布局的,以Gravity.Left为例,如果在xml文件中的layout_gravity中设置了这个值,那么在FrameLayout布局中的处理方式,就是把父View的左边的坐标值加上自己的间隔值(leftMargin),得到自己这个view的左边的坐标值,而且ViewGroup中设置了该值,会让子view都往左边去靠,因为是从left这个坐标点去开始去遍历去排序的。按这个思路,我们也可以实现自己的布局方式,只要复写就好了。
(3)performDraw
Draw是一个比较复杂的过程,还是说关于view的,如何绘制出一个特定的样子就不说了,因为api十分多。
private void performDraw() {
draw(fullRedrawNeeded);
}
-----------------------------------------------------------------------
private void draw(boolean fullRedrawNeeded) {
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
----------------------------------------------------------------------
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
// Draw with software renderer.
final Canvas canvas;
canvas = mSurface.lockCanvas(dirty);
mView.draw(canvas);
return true;
}
View中的draw方法:
public void draw(Canvas canvas) {
// Step 1, draw the background, if needed
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);
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
// we're done...
return;
}
***省略
}
方法很长,但是源码也给了注释,首先我们要把背景,也就是布局的drawable对象画出来,然后画父view的内容,再遍历画子view的内容,其他操作不是必须走的,与要实现的效果强相关。Canva可以理解为一个函数库,里面有各种api画出来我们的内容,此块也不涉及。
/*
Draw traversal performs several drawing steps which must be executed in the appropriate order: *
Draw the background
If necessary, save the canvas' layers to prepare for fading
Draw view's content
Draw children
If necessary, draw the fading edges and restore layers
Draw decorations (scrollbars for instance)
If necessary, draw the default focus highlight*/
至此,我们的绘制流程就走完了,需要注意的是,这是framework层的绘制流程,甚至都没涉及到native层的代码,安卓显示系统庞大而复杂,这里谈论的知识上层的view的绘制流程,不涉及具体实现,而且如果谈论细节相关的东西,繁琐且没有多大的意义。但是,知道这么多,我们就可以去自定义一个自己的view了。
那么,WindowManager的addView函数,又发生了什么呢?答案在WindowManager的实现类WindowManagerImpl中。
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
----------------------------------------------------------------------------------
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
// The previous removeView() had not completed executing. Now it has.
}
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;
}
}
}
交给WindowManagerGlobal的addView,然后我们看到了顶层view,ViewRootImpl的实例化,并调用它的setView(),再次说明层级关系:window > ViewRootImpl > ViewGroup > View. 下面的setView的主要代码:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
// 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(); //开始绘制
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
mInputChannel = new InputChannel(); //输入通道创建,用于接受诸如TouchEvent等事件
}
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel); //添加Window到WMS中
if (mInputChannel != null) {
if (mInputQueueCallback != null) {
mInputQueue = new InputQueue();
mInputQueueCallback.onInputQueueCreated(mInputQueue);
}
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, //java层的接受输入的地方
Looper.myLooper());
}
view.assignParent(this); //把自己设置到根节点view的parent变量中
// Set up the input pipeline.
CharSequence counterSuffix = attrs.getTitle();
mSyntheticInputStage = new SyntheticInputStage();
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage); //责任链模式,让input事件挨个流转直到找到处理者
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
"aq:native-post-ime:" + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
"aq:ime:" + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
"aq:native-pre-ime:" + counterSuffix);
mFirstInputStage = nativePreImeStage;
mFirstPostImeInputStage = earlyPostImeStage;
mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix;
}
}
}
在setView中,主要做了三个事:触发绘制流程,添加一个新的window到WMS,创建input接受器。这也是ViewRootImpl类的主要职责。WindowSession是对应一个窗口,是客户端与WMS调用的代理类。至于WMS怎么处理这个新添加的window等逻辑,以后再写,毕竟这部分的逻辑也很多,此处还是专注于View,input事件的接受和遍历发送以及处理等,在下面一篇讲。
至此,流程就明确了,下面的流程图就是在WindowManager的addView的调用流程,由于ViewGroup中的addView接口主要也是重新触发绘制流程,所以不单独画了,只要知道WM的addView是会创建顶层View和新窗口以及input channel,而ViewGroup的addView是把一个view加入到自己的view树中即可。
至于自定义view如何实现,这个就看需求场景了,源码里有许多定义好的,如TextView,Button等,可以参考,一般自定义View都要复写Measure,Layout,Draw。
总结:
1.ViewGroup中addView是把子view加到view树中,这期间没有window的创建等,会重新触发绘制流程,而WM的addView会创建顶层view和window等,也会触发绘制流程。
2.自定义View一般复写onMeasure,onLayout,onDraw,其中如果你是ViewGrup类型的子view,那么必须实现onLayout。
view绘制出来后,还有一个重要功能:响应用户的输入事件,这又涉及到View是如何知道派发给哪个view去处理这个input事件的,下节整理出来。其实关于View的显示,在上层的东西就这么多,但是底层,比如绘制频率,硬件如何刷新和同步,操作系统的显示模块接口如何工作的,在native层与java层相对应的东西,等等,还有很多知识点,只有了解完这些才能彻底了解view是如何显示出来的,这也是为什么draw相关的东西我没写多少,因为我也没彻底懂呢~~~