当前位置: 首页 > 科技观察

从自定义ViewGroup查看Layout的效果

时间:2023-03-13 01:57:08 科技观察

回来了,这个感冒占用了太多时间。断断续续去了几家医院和诊所,终于差不多好了,于是我在心里暗下决定,一定要好好照顾自己的身体(可能过两天就忘了??)总之,大家要多注意身体。如果身体坏了,你什么也做不了。废话不多说,开始今天的Android之旅吧~前言上次我们讲了View的Mearsure过程,今天我们来说说layout。关于布局,很多朋友都知道它负责布局,那么它是如何布局的呢?viewGroup和view的布局方式有什么区别?让我们来看看。View布局方法首先,从ViewRootImpl开始,界面的绘制会触发performMeasure和performLayout方法,在performLayout方法中会调用mView的layout方法,开始逐层的View布局工作。privatevoidperformLayout(WindowManager.LayoutParamslp,intdesiredWindowWidth,intdesiredWindowHeight){finalViewhost=mView;host.layout(0,0,host.getMeasuredWidth(),host.getMeasuredHeight());}mView我们都知道,它是顶层View——DecorView,然后进去看看DecorView的布局方法:抱歉,DecorView中没有布局方法...所以,我们直接看View的布局方法:publicvoidlayout(intl,intt,intr,intb){booleanchanged=isLayoutModeOptical(mParent)?setOpticalFrame(l,t,r,b):setFrame(l,t,r,b);if(已更改||(mPrivateFlags&PFLAG_LAYOUT_REQUIRED)==PFLAG_LAYOUT_REQUIRED){onLayout(已更改,l,t,r,b);}}protectedvoidonLayout(booleanchanged,intleft,inttop,intright,intbottom){}该方法首先传入四个参数,分别代表视图的左、上、下、右值。然后使用setOpticalFrame方法或者setFrame方法判断layout参数是否发生变化。具体判断过程是将旧的上下左右值与新的上下左右值进行比较。逻辑在setFrame方法中:=true;//记住ourdrawnbitintdrawn=mPrivateFlags&PFLAG_DRAWN;intoldWidth=mRight-mLeft;intoldHeight=mBottom-mTop;intnewWidth=right-left;intnewHeight=bottom-top;booleansizeChanged=(newWidth!=oldWidth)||(newHeight!=oldHeight);//Invalidateouroldpositioninvalidate(sizeChanged);mLeft=left;mTop=top;mRight=right;mBottom=bottom;mRenderNode.setLeftTopRightBottom(mLeft,mTop,mRight,mBottom);}returnchanged;}如果a的值有变化top、bottom、left、right参数,表示View的布局发生了变化,然后重新计算View的宽高(newWidth,newHeight),新的top,bottom,left,和对View的值赋参数值。在这个布局方法中,主要涉及到四个参数:mLeft、mTop、mBottom、mRight,分别代表View的左坐标、上坐标、下坐标、右坐标。你可以把View理解成一个矩形,确定这四个参数。值,可以确定View矩形的四个顶点值,也可以确定View在画布中的具体位置。那么,layout方法到底是做什么的呢?就是传入上、下、左、右值,然后赋上、下、左、右值,就搞定了。然后我们可以根据这些值得到View的一系列参数,比如Viewwidth:publicfinalintgetWidth(){returnmRight-mLeft;}到此View的布局方法就结束了,主要是通过upper,lower,left和right参数来完成视图。布局,非常简单。下面我们看看ViewGroup。ViewGroup布局方法@Overridepublicfinalvoidlayout(intl,intt,intr,intb){if(!mSuppressLayout&&(mTransition==null||!mTransition.isChangingLayout())){if(mTransition!=null){mTransition.layoutChange(this);}super.layout(l,t,r,b);}else{mLayoutCalledWhileSuppressed=true;}}嗯,还是调用View的layout方法。难道说ViewGroup和View的布局过程是一样的,就是确定自己的Position?ViewGroup的子View呢?别着急,刚才讲布局方法的时候漏掉了一个onLayout方法,但是这个方法在View中实现为空,在ViewGroup中变成了一个抽象方法:@OverrideprotectedabstractvoidonLayout(booleanchanged,intl,intt,intr,诠释);即任何ViewGroup都必须实现该方法才能完成子View的布局。具体的布局放置逻辑是在onLayout方法中依次调用子View的布局方法,然后完成各个子View的布局,最后完成绘制工作。下面我们来实现一个垂直的线性布局(类似LinearLayout),只是复习一下上一节的onMearsure和本节的onLayout。自定义垂直布局VerticalLayout首先要确定我们自定义ViewGroup的作用,类似于垂直方向的LinearLayout作用。ViewGroup下的子View可以垂直线性排列。姑且命名为VerticalLayout~InheritViewGroup首先我们的layout要继承ViewGroup并实现相应的构造方法:publicclassVerticalLayout:ViewGroup{constructor(context:Context,attrs:AttributeSet?,defStyleAttr:Int=0):super(context,attrs,defStyleAttr)constructor(context:Context,attrs:AttributeSet?):super(context,attrs){}}重写generateLayoutParams方法自定义ViewGroup还需要重写一个方法是generateLayoutParams,这一步是为了让我们的ViewGroup支持Margin,我们后面可以通过MarginLayoutParams获取子View的Margin值。overridefungenerateLayoutParams(attrs:AttributeSet?):LayoutParams?{returnMarginLayoutParams(context,attrs)}重写测量方法onMeasure然后,我们需要测量我们的布局,也就是重写onMeasure方法。在这个方法中,我们需要对我们的布局进行测量,将测量出来的宽高传入setMeasuredDimension方法中,完成测量。protectedfinalvoidsetMeasuredDimension(intmeasuredWidth,intmeasuredHeight)我们之前说过onMeasure方法会传入两个参数,widthMeasureSpec和heightMeasureSpec。包含了父View根据当前View的LayoutParams和父View的测量规格的计算,得到当前View期望的测量模式和测量尺寸:当测量模式为MeasureSpec.EXACTLY时,即当width或height为某个值,则将当前布局View的宽高设置为父View设置的测量尺寸。比如宽度是400dp,那么我们就不需要重新测量,直接调用setMeasuredDimension传入这个固定值即可。当measurementmode为MeasureSpec.AT_MOST或UNSPECIFIED时:此时表示父View对当前View的要求不是固定的,可以是任意尺寸,也可以不超过最大值。例如,将VerticalLayout的高度设置为wrap_content。那么我们就得重新测量高度,因为只有我们的设计人员才知道如何计算自适应高度。具体来说,VerticalLayout是垂直线性布局,所以高度自然是所有子View的高度之和。至此,onMeasure方法的逻辑基本搞清楚了:overridefunonMeasure(widthMeasureSpec:Int,heightMeasureSpec:Int){super.onMeasure(widthMeasureSpec,heightMeasureSpec)//获取宽高测量方式和测量尺寸valwidthMode=MeasureSpec。getMode(widthMeasureSpec)valheightMode=MeasureSpec.getMode(heightMeasureSpec)valsizeWidth=MeasureSpec.getSize(widthMeasureSpec)valsizeHeight=MeasureSpec.getSize(heightMeasureSpec)varmHeight=0varmWidth=0//遍历子View获取总高度for(iin0untilchildCount){valchildView=getChildAt(i)//测量子View的宽高measureChild(childView,widthMeasureSpec,heightMeasureSpec)vallp=childView.layoutParamsasMarginLayoutParamsvalchildWidth=childView.measuredWidth+lp.leftMargin+lp.rightMarginvalchildHeight=childView.measuredHeight+lp.topMargin+lp.bottomMargin//计算最大宽度mWidth=Math.max(mWidth,childWidth)//累计计算高度mHeight+=childHeight}//设置width和heightsetMeasuredDimension(if(widthMode==MeasureSpec.EXACTLY)sizeWidthelsemWidth,if(heightMode==MeasureSpec.EXACTLY)sizeHeightelsemHeight)}主要逻辑是遍历子View获取VerticalLayout的实际宽高:最终ViewGroup的高度=所有子View的(高度+margin值)ViewGroup的FinalWidth=最大子View的(width+margin值)最后调用setMeasuredDimension根据测量方式,传入宽高,重写布局方法onLayout如上所述,作为一个ViewGroup必须重写onLayout方法才能保证子View的正常布局。VerticalLayout也是如此,那么这个布局中onLayout方法的关键逻辑是什么?同一句话,确定位置,也就是确定left、upper、right、lower这四个参数值,而在VerticalLayout中,最关键的参数就是这个top,也就是top的值。每个View的top值必须是上一个View的bottom值,也就是放在上一个View的旁边,这样才会有垂直线性的效果,所以我们要做的就是动态计算top值每个View,其实就是不断累加这个View的高度,作为下一个View的top值。overridefunonLayout(changed:Boolean,l:Int,t:Int,r:Int,b:Int){varchildWidth=0varchildHeight=0varchildTop=0varlp:MarginLayoutParams//遍历子View,布局每个子Viewfor(iin0untilchildCount){valchildView=getChildAt(i)childHeight=childView.measuredHeightchildWidth=childView.measuredWidthlp=childView.layoutParamsasMarginLayoutParams//累计计算top值childTop+=lp.topMargin//布局childViewchildView.layout(lp.leftMargin,childTop,lp.leftMargin+childWidth,childTop+childHeight);childTop+=childHeight+lp.bottomMargin}}逻辑很简单,left就是固定子View的leftMargin。top为累计计算的子View的height+Margin值。right是left+childView的宽度。bottom是top+childView的高度。最后调用子View的layout方法对各个子View进行布局。大功告成,最后来看看我们自定义垂直线性布局的效果~效果展示layout_height="100dp"android:text="lalala"android:textSize="20sp"android:textColor="@color/white"android:background="@color/design_default_color_primary"/>本文转载自微信公众号》楼码上积木》,可通过以下二维码关注和转载本文,请联系码上积木公众号。