2023年7月15日发(作者:)
⾯试官:谈⼀谈你对View的认识和View的⼯作流程都2022年了,不会还有⼈不知道什么是View吧,不会吧,不会吧。按我以往的⾯试经验来看,View被问到的概率不⽐Activity低多少哦,个⼈感觉View在Android中的重要性也和Activity不相上下,所以这篇⽂章将介绍下View的基础知识及其⼯作原理,如有帮助到你,可以点个赞表⽰⿎励,谢谢各位!⼀、初识View什么是View ?所谓的View,就是Android中所有控件的基类(例如:Button、TextView、LinearLayout等等),甚⾄是ViewGroup也是继承⾃View的,然⽽ViewGroup是⼀个控件组,⾥⾯包含许多控件(也就是⼀组View),所以这就意味着View本⾝既可以是单个控件,也可以是由许多控件组成的⼀个控件,这样看来View其实也没那么⽞乎啊。View的位置参数⾸先我们要知道,在Android⼿机中,坐标系是以⼿机屏幕的最左上⾓为原点⽽建⽴的,⼤家可以参考下⾯的图⽚理解下。其次,View的初始位置由四个属性决定,分别是:top、left、right、bottom。其中top和left为View左上⾓的纵坐标和横坐标,⽽bottom和right为View右下⾓的纵坐标和横坐标。(看官注意:这些坐标全都是相对于View所在的⽗容器来说的,是⼀个相对坐标,并不是在⼿机屏幕中的实际坐标)同样,在下⾯放上图⽰帮助⼤家理解:所以,通过上⾯的⼀通分析,我们可以得出View的宽⾼和坐标之间的关系:width = right - leftheight = bottom -top细⼼的看官可能发现了,在上⾯我把”初始位置“四个字给标红了。没错,那四个属性不仅仅是初始位置,⽽且在你的View不论是发⽣旋转或者平移时候,他们都不会改变,改变的其实是另外的位置参数。从Android3.0开始,View增加了⼏个额外的参数,它们分别是x、y、translationX、translationY,其中x和y是View左上⾓的坐标,⽽translationX、translationY是View左上⾓相对于⽗容器的偏移量,(注意:这⼏个参数同样是针对⽗容器⽽⾔的)并且translationX和translationY的默认值是0,它们有以下的换算关系:x = left + translationXy = top +translationY所以不难看出,当View发⽣位置改变时,改变的其实是x、y、translationX、translationY这四个参数。好了,以上就是View位置参数的全部内容,如果以上内容各位看起来⽐较轻松的话,那么接下来的内容可能⽐较费劲,接下来继续发车了。⼆、DecorView和MeasureSpecView的三⼤流程⽆⾮就是Measure、Layout、Draw,但这三⼤流程都是基于DecorView中呈现的,然⽽想要呈现出View,还需要知道View的⼤⼩,在测量过程中MeasureSpec⼜是其中的关键,所以接下来我们有必要了解下他们。初识DecorViewDecorView是Activity⾥的顶级View,它⼀般来说是⼀个竖直⽅向的LinearLayout(这与Android的版本和主题有关),在这个LinearLayout⾥⾯有上下两部分,上⾯是标题栏,下⾯是内容栏。我们在Activity的onCreate()⽅法中通过setContentView所设置的布局⽂件就被加⼊到了DecorView的内容栏之中,内容栏的id为content,通过ViewGroup content = findViewById(t)可以得到content,View层的事件都会先经过DecorView之后才继续向下传递的。同样是⼀图胜千⾔:理解MeasureSpec第⼀个问题,什么是MeasureSpec?在《Android开发艺术探索》⼀书中对它的解释是这样的:MeasureSpec翻译过来是”测量规格“或者”测量说明书“,是⼀个32位的int值,⾼2位代表SpecMode,低30位代表SpecSize。⽽SpecMode指测量模式,SpecSize指在某种测量模式下的规格⼤⼩。第⼆个问题,MeasureSpec是⼲啥的?同样书中的解释是:它在很⼤程度上决定了⼀个View的尺⼨规格,之所以说是很⼤程度上是因为这个过程还受⽗容器的影响,因为⽗容器影响View的MeasureSpec的创建过程。在测量过程中,系统会将View的LayoutParams根据⽗容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽⾼。可谓是听君⼀席话,胜似听君⼀席话,我⼀开始看也是云⾥雾⾥的,接下来我尽量不深⼊代码,带⼤家通俗的理解⼀下:我回答下第⼀个问题,MeasureSpec其实就是⼀串数字,⼀串长度为32位的数字,⾥⾯包含着⼀些View的测量信息。第⼆个问题的答案就是,通过这⼀串数字可以帮助系统测量出View的宽和⾼。这够通俗了吧,这就跟你⽹购的快递⼀样的,快递单号就是那⼀串莫名其妙的数字(MeasureSpec),通过这⼀串数字能帮助你查询到快递运到哪了,换算到View上,不就是帮助系统知道View的宽和⾼嘛,这是⼀个道理,听懂就来点掌声。那么接下来就要详细了解下系统是怎么通过这串数字(MeasureSpec)来测量出View的宽⾼的。在上⾯提到SpecMode和SpecSize。SpecSize⽐较简单,通俗理解就是View在⽗容器下的实际⼤⼩或者是可⽤⼤⼩,有⼈可能会问为什么还会有个可⽤⼤⼩?这⾥我解释下:当你在布局⽂件中给定⼀个View⼀个确切的⼤⼩时,那么SpecSize就是实际⼤⼩,例如:android:layout_width = "x dp"、android:layout_height = "x dp"。反之如果这两个属性给的值为”match_parent“、或者是"wrap_content"时,此时的SpecSize就是⽗容器下的可⽤⼤⼩。SpecMode相对于SpecSize⽽⾔削微有那么点抽象,它分为三类,分别如下:1. UNSPECIFIE:⽗容器不对View有任何限制,要多⼤就给⼤多,这⼀般⽤于系统内部,表⽰⼀种测量的状态,⼀般来说可忽视。2. EXACTLY:⽗容器已经检测出View所需要的精确⼤⼩,这个时候View的最终⼤⼩就是SpecSize所指定的值,它对应于LayoutParams中的match_parent和赋予具体数值的这两种模式3. AT_MOST:⽗容器指定了⼀个可⽤⼤⼩即SpecSize,View的⼤⼩不能⼤于这个值,具体是什么值需要看不同View的具体实现。它对应于LayoutParams中的wrap_content通俗的说,你在layout布局⽂件中给View的”match_parent“、和"wrap_content"属性设置不同的值,再根据⽗容器的SpecMode加以对应,就会得到View实际的SpecMode,具体的对应关系如下:图来(图中的parentSize指的是⽗容器中⽬前可使⽤的⼤⼩,表格中的UNSPECIFIED中的Size为 0 表⽰忽略,在普通的View中是不会出现的,只会在例如DecorView这种系统级别的才会出现)所以,只要提供了⽗容器的MeasureSpec和⼦元素的LayoutParams,就可以确定出⼦View的MeasureSpec了,从⽽就可以进⼀步确定出View测量后的⼤⼩了,⾄此MeasureSpec扫盲结束。让我们喝⼝⽔,继续讲述View的三⼤流程。等等,你刚才说,喝什么三、View的三⼤流程此为⾯试热点,⾯试官⼀般会从这⾥引⼊,然后不断对你进⾏摸底,各位要跳槽的看官要注意了,View的三⼤流程是指measure、layout、draw,即测量、布局和绘制。其中measure确定View的测量宽⾼,layout确定View的最终宽⾼和四个顶点位置,⽽draw则是将View绘制到屏幕上。所以我们逐⼀进⾏分析,同样我也不深⼊代码,有想深⼊了解代码的可以⾃⼰查阅相关信息或者参考《Android开发艺术探索》⼀书。measure过程measure过程主要分为两类,⼀类是单个View的measure,另⼀类是对于ViewGroup的measure。单个View的measure⽐较简单,直接通过调⽤⾃⾝的measure⽅法就完成了测量过程。然⽽对于ViewGroup⽽⾔,除了会完成⾃⾝的measure过程外,还会去遍历调⽤所有⼦元素的measure⽅法,各个⼦元素再递归去执⾏这个流程。1. View的measure过程上⽂有提到View的measure通过调⽤⾃⾝的measure()⽅法就完成了测量过程,然⽽measure()⽅法中⼜会去调⽤onMeasure(),具体代码如下://View的measure⽅法public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); ...
}//View的onMeasure⽅法protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}从上⾯的代码我们可以知道两点,⼀是宽和⾼分别有⾃⼰的MeasureSpec,⾄于什么是MeasureSpec,上⽂有提到过了。⼆是宽或⾼的测量⼤⼩是通过getDefaultSize()⽅法得到的,⾄于他是怎么得到的,我们继续往下看:public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = e(measureSpec);
int specSize = e(measureSpec);
switch (specMode) { case IFIED:
result = size;
break; case _MOST:
case Y:
result = specSize;
break;
}
return result;
}对于e()和e()⽅法我详细解释下,这是系统提供的⼀个解包⽅法,也可以理解为解密吧(其实也就是⼆进制的与操作),将⼀个measureSpec通过解包,从⽽得到sepcMode和specSize,再往后就是根据specMode返回对应的⼤⼩了。另外,getDefaultSize()⽅法还传⼊了⼀个getSuggestedMinimumHeight()或getSuggestedMinimumWidth()参数,这看名字应该是⼀个系统推荐的默认值,⾄于这个默认值怎么来的,我就不带着⼤家分析代码了,我直接给出结论,有兴趣的看官可以⾃⾏了解,我这⾥以getSuggestedMinimumWidth()为例给结论就⾏,因为getSuggestedMinimumHeight()也是⼀模⼀样的。getSuggestedMinimumWidth()结论: 如果View没有设置背景,那么返回android:minWidth这个属性所指定的值(这个值可以为0),如果View设置了背景,则返回android:minWidth和背景的最⼩宽度这两者中的最⼤值.⾯试点细节总结敲⿊板了,注意了,此为魔⿁细节和⾯试问点,各位看官要注意了⾯试官:直接继承⾃View的⾃定义控件需要注意什么?我:当然是需要重写onDraw()⽅法啦!⾯试官:.....这简直是⼀句犀利的废话。直接继承View的⾃定义View需要重写onMeasure⽅法并设置wrap_content时的⼤⼩,否则在布局中使⽤wrap_content时就相当于match_parent,导致使⽤match_parent和使⽤wrap_content时完全没有区别。之所以会出现这样的现象是因为View在布局中使⽤wrap_content时,那么它的specMode是AT_MOST模式,在这种模式下,它的宽、⾼specSize都是parentSize(这⼀部分前⾯有讲过,可以查阅表格,如果懒得往上翻的朋友,我会在下⾯再放⼀次表格),⽽parentSize代表的是⽗容器中⽬前可以使⽤的⼤⼩。所以在这种情况下,View的宽⾼就会等于⽗容器可使⽤空间⼤⼩,我们可以再看表格,艾,巧了,当我们使⽤match_parent时,specSize同样也是parentSize,所以呈现的效果完全⼀致,这下⼤家都明⽩了吧?呐,还是给⼤家举个栗⼦,帮助理解。 来啊,上栗⼦!!⼤家可以看到我在布局中加了两个控件,⼀个是普通的View,背景⾊为⿊⾊(这⾥我们也可以看成⾃定义View),另⼀个是TextView,背景⾊为⽩⾊。这控件的宽⾼属性全部是wrap_content,然⽽我们⾃定义的View却撑满了整个屏幕,TextView却没有。这是因为TextView中已经重写了onMeasure()⽅法,在⽅法中对specMode为AT_MOST时,做了特殊处理,⼤家感兴趣可以⾃⼰查看源码,⽽View中没有处理。所以出现了上述的问题。所以当⼤家在写⾃定义View时,记得也加⼊这样的处理,我在下⾯为⼤家放上⼀个解决⽅案,具体值得⼤⼩还需要你们⾃⼰去灵活定义://代码来源于《Android开发艺术探索》protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
ure(widthMeasureSpec, heightMeasureSpec);
int widthSize = e(widthMeasureSpec);
int widthMode = e(widthMeasureSpec);
int heightSize = e(heightMeasureSpec);
int heightMode = e(heightMeasureSpec);
if (widthMode == _MOST && heightMode == _MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthMode == _MOST) {
setMeasuredDimension(mWidth, heightSize);
} else if (heightMode == _MOST) {
setMeasuredDimension(widthSize, mHeight);
}
}2. ViewGroup的measure过程ViewGroup⾃⾝是没有重写onMeasure()⽅法的,⽽View是有重写的。但是ViewGroup提供了⼀个measureChild⽅法,其作⽤就是取出⼦元素的LayoutParams,进⼀步获得⼦元素的MeasureSpec,接着将MeasureSpec传递给View的measure⽅法进⾏测量,View的measure测量流程已经在上⾯做了详细分析了。⼤家看到这应该也明⽩了,为什么⾃定义View不⽤必须重写onMeasure,⽽⾃定义ViewGroup必须重写onMeasure⽅法的原因了所以ViewGroup的测量流程简单⽽⾔可以分为两块内容,第⼀块递归对⼦View进⾏measure,第⼆块根据每个⼦View的测量结果,累计加总测量出ViewGroup⾃⾝的宽⾼。第⼀块内容在上⽂详细介绍过,因此我们主要关注第⼆块内容,接下来以LinearLayout为例⼦进⾏介绍,我们先来看下LinearLayout的onMeasure⽅法:@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); }}上述代码可谓简单明了,我们就只以VERTICAL⽅向上去看⼀下,另⼀个也⼤同⼩异,⼤家可以⾃⾏了解,由于measureVertical⽅法⽐较长,我就截取部分源码,描述下⼤概逻辑,⾸先看代码:void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { mTotalLength = 0; ... // See how tall everyone is. Also remember max width. //(遍历每个⼦View的⾼度,并且记录下总⾼度,其中mTotalLength就是总⾼度) for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); ... // Determine how big this child would like to be. If this or // previous children have given a weight, then we allow it to // use all available space (and we will shrink things later // if needed). final int usedHeight = totalWeight == 0 ? mTotalLength : 0; measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight); final int childHeight = suredHeight(); final int totalLength = mTotalLength; mTotalLength = (totalLength, totalLength + childHeight + gin + Margin + getNextLocationOffset(child)); ... } //所有⼦元素遍历结束,开始测量⾃⾝⼤⼩ // Add in our padding,加顶部和底部的padding统计进总⾼度 mTotalLength += mPaddingTop + mPaddingBottom; int heightSize = mTotalLength; // Check against our minimum height heightSize = (heightSize, getSuggestedMinimumHeight()); // Reconcile our calculated size with the heightMeasureSpec // 根据⽗容器的⼤⼩和⾃⾝的MeasureSpec计算出最终⾼度,因为⼦元素⾼度总和是不能超过⽗元素剩余空间的 int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0); ... maxWidth += mPaddingLeft + mPaddingRight; // Check against our minimum width maxWidth = (maxWidth, getSuggestedMinimumWidth()); //传⼊最终测量出的宽⾼尺⼨,从⽽设置ViewGroup的宽⾼ setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);}上述代码的主要逻辑就是系统会遍历所有⼦元素并对他们执⾏measureChildBeforeLayout⽅法,在这个⽅法内部会调⽤⼦元素的measure⽅法,也就是进⼊到了第⼀块内容中(上⽂已经讲解过),系统还通过mTotalLength来记录LinearLayout在竖直⽅向的总⾼度,每测量出⼀个⼦元素,mTotalLength就会增加,增加的部分主要包括⼦元素的⾼度以及⼦元素在竖直⽅向上的margin、padding等。最终设置ViewGroup的测量宽⾼,⾄此测量完成!⾯试点细节总结1. ⾃定义ViewGroup,继承ViewGroup后,必须要重写onMeasure⽅法测量⾃⾝和⼦View,进⽽重写onLayout,这点与⾃定义View差别较⼤,需要特别注意.2. 不论是⾃定义View还是⾃定义ViewGroup,他们在measure过程得到的宽⾼都不是最终宽⾼,仅仅是测量宽⾼。最终宽⾼是在onLayout过程中才真正确定的,所以要获取⼀个控件的宽⾼,最好在onLayout⽅法中去获取。当然⼤多数情况下,控件的测量宽⾼和最终宽⾼是相等的.3. 由于View的measure过程和Activity的⽣命周期⽅法是不同步执⾏的,因此⽆法保证Activity执⾏了onCreate、onStart、onResume时某个View已经被测量完毕了,如果此时View还没有测量完毕,我们获取到的宽⾼值将会是0,要解决这种问题,我们可以采⽤(runnable)⽅法,通过post将⼀个runnable投递到消息队列尾部,等View初始化完成后,就会从Looper中调⽤此runnable,从⽽拿到测量出的宽⾼值,代码⽰例如下: (new Runnable() { @Override public void run() { int width = suredWidth(); int height = suredHeight(); } });layout过程Layout流程是⽤于确定View或ViewGroup的位置,因为layout流程相对于measure⽽⾔⽐较简单,我们先看看view的layout⼤致代码逻辑: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); ... } ...}在上述代码中,⼤致流程是,layout⽅法⾸先会通过setFrame⽅法来设定View的四个顶点位置,即初始化mLeft、mTop、mRight、mBottom这四个值,View的四个顶点⼀旦确定,View在⽗容器中的位置也就随之确定了。接下来就会调⽤onLayout⽅法,这个⽅法的⽤途是⽗容器确定⼦元素位置的,通俗⽽⾔就是layout是确定⾃⾝的位置,onLayout是确定其⼦View的位置,因为单个View没有⼦元素,ViewGroup类布局的不确定性,所以他们均对onLayout⽅法都是空实现,即如下所⽰:/** * Called from layout when this view should * assign a size and position to each of its children. * * Derived classes with children should override * this method and call layout on each of * their children. */protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}因为LinearLayout继承⾃ViewGroup,所以它必然实现了onLayout⽅法,所以我们继续以它为例,看看它是如何实现的,代码如下所⽰:@Overrideprotected 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); }}啊哈,看来和onMeasure()中的实现逻辑类似,我们还是以layoutVertical进⾏讲解,同样继续给出主要代码逻辑:void layoutVertical(int left, int top, int right, int bottom) { ... final int count = getVirtualChildCount(); ... for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { childTop += measureNullChild(i); } else if (ibility() != GONE) { final int childWidth = suredWidth(); final int childHeight = suredHeight(); final Params lp = (Params) outParams(); ... if (hasDividerBeforeChildAt(i)) { childTop += mDividerHeight; } childTop += gin; setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); childTop += childHeight + Margin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i); } }}所以综上所述:layoutVertical⽅法会遍历所有⼦元素并调⽤setChildFrame⽅法来为⼦元素指定对应位置,其中childTop记录当前末端⼦元素的⾼度位置,childTop会逐渐增⼤,意味着后⾯的⼦元素会被放置在靠下的位置,这正是竖直⽅向上LinearLayout的特性。再简单说⼀下setChildFrame⽅法,它仅仅是调⽤⼦元素(这⾥我们可以看成是⼉⼦元素)的layout⽅法,然后当该元素(⼉⼦元素)确定了⾃⼰的位置以后⼜调⽤onLayout⽅法安排其⼦元素(孙⼦元素)的位置。好家伙,这简直就是俄罗斯套娃(递归)。draw过程Draw过程就更简单了,尤其是对于做了许多⾃定义View的友友来说。它的作⽤就是将View绘制到屏幕上⾯,绘制过程⼤致如下:1. 绘制背景(canvas).2. 绘制⾃⼰ (onDraw).3. 绘制children (dispatchDraw).4. 绘制装饰 (onDrawScrollBars).关于绘制过程,是三⼤流程中最为简单的了,看官可以⾃⾏查看源码,这⾥就不再赘述了,另外ViewGroup⼀般不⽤重写onDraw来绘制⾃⼰,只需要对⼦View进⾏绘制就可以。但明确知道⼀个ViewGroup需要通过onDraw来绘制内容时,我们需要调⽤setViewNotDraw(false)来进⾏设置;好了,⾄此View三⼤⼯作流程已经讲解完毕,没错,接下来当然是划重点啦。四、⾃定义View相关总结⾃定义View的分类1. 继承View:需重写onDraw⽅法,⼀般⽤于实现⼀些不规则的效果。2. 继承ViewGroup:需重写onMeasure、onLayout⽅法,即⾃⼰定义⼀种除了像linearLayout、RelativeLayout等,这⼏种系统布局之外的布局,这种情况⽐较少,但感兴趣的朋友可以参考下⾯这篇⽂章,个⼈感觉⾮常不错。 ViewGroup实战Demo3. 继承特定的View:⽐如继承ImgView等,⼀般⽤于拓展某种已有的View的功能。4. 继承特定的ViewGroup: ⽐如继承LinearLayout,⼀般也是⽤于拓展功能。但好处是它不需要⾃⼰重写onMeasure和onLayout⽅法,并且也⽐较常⽤。⾃定义View的注意事项1. 让View⽀持wrap_content,这⼀点在上⾯View的measure过程的⾯试点细节总结⾥详细介绍过。2. 在⾃定义View时,不要在onDraw()⽅法中定义变量和执⾏循环操作,不然会导致内存溢出和卡顿掉帧的现象。3. 如果View中有线程或者动画,需要及时停⽌,否则会造成内存泄露的情况。4. View带有滑动嵌套情形时,需要处理好滑动冲突。5. 不要在View中使⽤Handler,可以使⽤(runnable)⽅法进⾏替代。6. 如果有必要,让你的View⽀持padding。对于直接继承View的控件,如果不在onDraw()⽅法中处理padding,那么padding属性是没有效果的。对于直接继承⾃ViewGroup的控件,需要在onMeasure()和onLayout()中考虑padding和⼦元素的margin对其造成的影响,否则也会导致padding和⼦元素的margin失效。
发布者:admin,转转请注明出处:http://www.yc00.com/xiaochengxu/1689429991a246911.html
评论列表(0条)