Android自定义View之onLayout源码分析.docx
- 文档编号:29615603
- 上传时间:2023-07-25
- 格式:DOCX
- 页数:12
- 大小:109.81KB
Android自定义View之onLayout源码分析.docx
《Android自定义View之onLayout源码分析.docx》由会员分享,可在线阅读,更多相关《Android自定义View之onLayout源码分析.docx(12页珍藏版)》请在冰豆网上搜索。
Android自定义View之onLayout源码分析
Android自定义View之onLayout源码分析
我们都知道,自定义ViewGroup是所有子View的父控件,而自定义View通常需要重写onMeasure(),onLayout(),onTouchEvent()等方法,当然了,我们都知道自定义最难的地方在于draw(即画)的过程,这难以理解,不过今天这一篇文章要说的不是draw,而是如何了解onLayout()方法。
我们都知道,自定义View的第一步是测量当前剩余空间,或者说是界面的大小,也就是measure了,这一点在上一篇通过讲解onMeasure()方法已经向大家解释了,所以今天我们要说的就是onLayout()方法的重写和分析了,也就是确定自定义View显示的位置。
android.view.View.layout()
既然要讲layout,确定子View在父控件的位置,那么我们就从View的layout()方法开始着手吧!
先来看一下Google官方文档对android.view.View.layout()这个方法的描述吧:
Assignasizeandpositiontoaviewandallofitsdescendants
也就是说这个方法的作用是为视图及其所有后代分配大小和位置。
Thisisthesecondphaseofthelayoutmechanism.(Thefirstismeasuring).Inthisphase,eachparentcallslayoutonallofitschildrentopositionthem.Thisistypicallydoneusingthechildmeasurementsthatwerestoredinthemeasurepass().
这是布局机制的第二阶段。
(第一个是测量measure)。
在这个阶段,每个父级调用所有子级的布局来定位它们。
这通常使用存储在度量pass()中的子测量来完成。
Derivedclassesshouldnotoverridethismethod.DerivedclasseswithchildrenshouldoverrideonLayout.Inthatmethod,theyshouldcalllayoutoneachoftheirchildren.
派生类不应覆盖此方法。
具有子代的派生类应该覆盖onLayout()方法。
在onLayout()方法方法中,他们应该在每个子View上调用layout()方法。
好了,以上都是通过官方文档而得知的内容,接下来我们直接看layout()方法的源码:
//参数l,t,r,b分别表示子View相对于父View的左、上、右、下的坐标
publicvoidlayout(intl,intt,intr,intb){
if((mPrivateFlags3&PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT)!
=0){
onMeasure(mOldWidthMeasureSpec,mOldHeightMeasureSpec);
mPrivateFlags3&=~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
intoldL=mLeft;
intoldT=mTop;
intoldB=mBottom;
intoldR=mRight;
booleanchanged=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);
mPrivateFlags&=~PFLAG_LAYOUT_REQUIRED;
ListenerInfoli=mListenerInfo;
if(li!
=null&&li.mOnLayoutChangeListeners!
=null){
ArrayList
(ArrayList
intnumListeners=listenersCopy.size();
for(inti=0;i listenersCopy.get(i).onLayoutChange(this,l,t,r,b,oldL,oldT,oldR,oldB); } } } mPrivateFlags&=~PFLAG_FORCE_LAYOUT; mPrivateFlags3|=PFLAG3_IS_LAID_OUT; } 方法的主要步骤如下: 确定子View在父View中的位置 判断子View位置是否发生变化,如果发生变化则调用onLayout()方法 步骤一中调用了setFrame()方法,把l,t,r,b分别与之前的mLeft,mTop,mRight,mBottom逐一比较,假若其中任意一个值发生了变化,那么就判定该View的位置发生了变化。 android.view.View.onLayout() 顺着源码的思路,既然发生变化时调用了onLayout()方法,那么接下来我们就从onLayout()方法的源码中来寻找答案吧! protectedvoidonLayout(booleanchanged,intleft,inttop,intright,intbottom){ } 居然是一个空方法,那我们只能看看官方文档对这个方法的解释了: Calledfromlayoutwhenthisviewshouldassignasizeandpositiontoeachofitschildren.Derivedclasseswithchildrenshouldoverridethismethodandcalllayoutoneachoftheirchildren. 在布局(layout()方法)中调用时,此视图(onLayout()方法)应为其每个子View分配大小和位置。 具有子代的派生类应该覆盖此方法并在其每个子View上调用布局。 那么谁有子View呢? 当然是ViewGroup了,ViewGroup是所有子View的管理器嘛! 它存在的目的就是为了对其子View进行管理,为其子View添加显示和响应的规则。 简单的,也就是说ViewGroup会调用onLayout()方法来确定View的显示位置。 既然是这样,那么我们就直接看ViewGroup的onLayout()方法是如何实现的吧! protectedabstractvoidonLayout(booleanchanged,intl,intt,intr,intb); 天啊,居然是一个抽象方法。 那我们只能继续看文档的解释了: Calledfromlayoutwhenthisviewshouldassignasizeandpositiontoeachofitschildren.Derivedclasseswithchildrenshouldoverridethismethodandcalllayoutoneachoftheirchildren. 和View.onLayout()方法的解释是一样的: 大概就是说ViewGroup的子类都必须重写这个方法,实现自己的逻辑。 比如: FrameLayou,LinearLayout,RelativeLayout等等布局都需要重写这个方法,在该方法内依据各自的布局规则确定子View的位置。 这里我们以LinearLayout为例,来看看它的onLayout()方法是如何实现的 protectedvoidonLayout(booleanchanged,intl,intt,intr,intb){ if(mOrientation==VERTICAL){ layoutVertical(l,t,r,b); }else{ layoutHorizontal(l,t,r,b); } } 可以看到在LinearLayout的onLayout()方法中分别处理了水平线性布局layoutVertical()和垂直线性布局layoutHorizontal()。 这里我们选择选择layoutVertical()继续往下看。 voidlayoutVertical(intleft,inttop,intright,intbottom){ finalintpaddingLeft=mPaddingLeft; intchildTop; intchildLeft; finalintwidth=right-left; intchildRight=width-mPaddingRight; intchildSpace=width-paddingLeft-mPaddingRight; finalintcount=getVirtualChildCount(); finalintmajorGravity=mGravity&Gravity.VERTICAL_GRAVITY_MASK; finalintminorGravity=mGravity&Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; switch(majorGravity){ caseGravity.BOTTOM: childTop=mPaddingTop+bottom-top-mTotalLength; break; caseGravity.CENTER_VERTICAL: childTop=mPaddingTop+(bottom-top-mTotalLength)/2; break; caseGravity.TOP: default: childTop=mPaddingTop; break; } for(inti=0;i finalViewchild=getVirtualChildAt(i); if(child==ull){ childTop+=measureNullChild(i); }elseif(child.getVisibility()! =GONE){ finalintchildWidth=child.getMeasuredWidth(); finalintchildHeight=child.getMeasuredHeight(); finalLinearLayout.LayoutParamslp= (LinearLayout.LayoutParams)child.getLayoutParams(); intgravity=lp.gravity; if(gravity<0){ gravity=minorGravity; } finalintlayoutDirection=getLayoutDirection(); finalintabsoluteGravity=Gravity.getAbsoluteGravity(gravity,layoutDirection); switch(absoluteGravity&Gravity.HORIZONTAL_GRAVITY_MASK){ caseGravity.CENTER_HORIZONTAL: childLeft=paddingLeft+((childSpace-childWidth)/2) +lp.leftMargin-lp.rightMargin; break; caseGravity.RIGHT: childLeft=childRight-childWidth-lp.rightMargin; break; caseGravity.LEFT: default: childLeft=paddingLeft+lp.leftMargin; break; } if(hasDividerBeforeChildAt(i)){ childTop+=mDividerHeight; } childTop+=lp.topMargin; setChildFrame(child,childLeft,childTop+getLocationOffset(child), childWdth,childHeight); childTop+=childHeight+lp.bottomMargin+getNextLocationOffset(child); i+=getChildrenSkipCount(child,i); } } } 我们来分析一下这个方法的主要实现步骤: 计算child可使用空间的大小(当前可使用空间的大小) 获取子View的个数 计算childTop从而确定子View的开始布局位置 确定每个子View的位置 这一步是最关键的,我们具体分析一下它的实现逻辑吧: 获取子View测量后的宽和高(这里获取到的childWidth和childHeight就是在measure阶段所确立的宽和高) 获取子View的LayoutParams 根据子View的LayoutParams确定子View的位置 我们可以发现在setChildFrame()中又调用了View的layout()方法来确定子View的位置。 很明显: ViewGroup首先调用layout()来确定自己本身在其父View中的位置,然后调用onLayout()确定每个子View的位置,每个子View又会调用View的layout()方法来确定自己在ViewGroup的位置。 简单的说: View的layout()方法用于View确定自己本身在其父View的位置 ViewGroup的onLayout()方法用于确定子View的位置 自定义ViewGroup 为了能够更好的理解,这里通过一个自定义的ViewGroup来模拟onLayout()的过程,重写onMeasure(),onLayout(),onDraw()方法,并实现构造方法: packagecom.example.jimcharles.customview; importandroid.content.Context; importandroid.graphics.Canvas; importandroid.util.AttributeSet; importandroid.view.View; importandroid.view.ViewGroup; publicclassMyViewGroupextendsViewGroup{ publicMyViewGroup(Contextcontext,AttributeSetattrs){ super(context,attrs); } @Override protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){ super.onMeasure(widthMeasureSpec,heightMeasureSpec); intchildCount=getChildCount(); if(childCount>0){ Viewild=getChildAt(0); measureChild(child,widthMeasureSpec,heightMeasureSpec); } } @Override protectedvoidonLayout(booleanchanged,intl,intt,intr,intb){ intchildCount=getChildCount(); if(childCount>0){ Viewchild=getChildAt(0); intmeasuredWidth=child.getMeasuredWidth(); intmeasuredHeight=child.getMeasuredHeight(); child.layout(0,0,measuredWidth,measuredHeight); } } @Override protectedvoidonDraw(Canvascanvas){ super.onDraw(canvas); } } 好了,代码的逻辑很简单 首先在onMeasure()方法中获取当前child的大小,即测量子View的大小;然后在在onLayout()中确定子View的位置。 好了,现在把我们定义的ViewGroup放进布局文件来看一下效果吧! xmlversion="1.0"encoding="utf-8"? > android: layout_width="match_parent" android: layout_height="match_parent" android: gravity="center" android: background="#A5FD01" custom: context="com.example.jimcharles.customview.MainActivity"> android: layout_width="wrap_content" android: layout_height="wrap_content" android: id="@+id/myViewGroup" android: layout_centerVertical="true" android: layout_centerHorizontal="true"> android: id="@+id/text_girl" android: layout_width="wrap_content" android: layout_height="wrap_content" android: src="@drawable/girl"/> 好了,用ImageView通过MyViewGroup在父控件上显示一张图片,很简单的一个布局 然后再MainActivity中通过findViewById()获取ImageView实例 packagecom.example.jimcharles.customview; importandroid.app.Activity; importandroid.os.Bundle; importandroid.widget.ImageView; publicclassMainActivityextendsActivity{ privateImageViewgirlImageView; @Override protectedvoidonCreate(BundlesavedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_viewgroup); girlImageView=(ImageView)findViewById(R.id.text_girl); } } 好了,我们已经分析完了measure和layout这两个过程,一起来回想一下 获取View的测量大小measuredWidth和measuredHeight的时机 在某些复杂或者极端的情况下系统会多次执行measure过程,所以在onMeasure()中去获取View的测量大小得到的是一个不准确的值。 为了避免该情况,最好在onMeasure()的下一阶段即onLayout()中去获取。 getMeasuredWidth()和getWidth()的区别 在绝大多数情况下这两者返回的值都是相同的,但是结果相同并不说明它们是同一个东西。 首先,它们的获取时机是不同的 在measure()过程结束后就可以调用getMeasuredWidth()方法获取到View的测量大小,而getWidth()方法要在layout()过程结束后才能被调用从而获取View的实际大小。 其次,它们返回值的计算方式不同 getMeasuredWidth()方法中的返回值是通过setMeasuredDimension()方法得到的;而getWidth()方法中的返回值是通过View的右坐标减去其左坐标(right-left)计算出来的。 刚才说到了关于View的坐标,下面是获取view的相对位置的四个方法: view.getLeft(),view.getRight(),view.getBottom(),view.getTop(); 这四个方法用于获取子View相对于父View的位置。 getLeft()用于获取子View的左边距离父View的左边的距离 getRight()用于获取子View的右边距离父View的左边的距离 getTop(用于获取子View的上边距离父View的上边的距离 getBottom()用于获取子View的下边距离父View的上边的距离 直接继承自ViewGroup可能带来的复杂处理 刚通过一个例子简单模拟了ViewGroup的onLayout()过程。 而项目开发中实际的情况可能远比这个复杂;比如,在ViewGroup中包含了多个View,每个View都设置了padding和margin,除此之
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Android 自定义 View onLayout 源码 分析