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

一文看懂Android自定义Viewgroup难点

时间:2023-03-17 22:33:47 科技观察

本文的目的是教大家如何自定义viewgroup,如何编写自定义layout和自定义measurement。很多在网上随便搜到的概念和流程图,这里不再赘述。建议大家在看这篇文章之前先看一下自定义viewgroup的基本流程,心里有个大概的了解。本文侧重实践。Viewgroup的测量和布局流程基本梳理完毕。回顾一下基本的viewgroup绘制和布局过程中的关键点:view是自己测量并保存在onMeasure()方法中的,也就是说对于view(不是viewgroup)来说,必须在onMeasure方法中计算自己的大小并保存它。其实viewgroup最终还是从上层size调用了子view的measure方法。注意,子视图的measure实际上是调用了子视图的onMeasure方法。所以我们把这个过程理解为:viewgroup循环调用所有子view的onmeasure方法,用onmeasure方法计算出的尺寸来决定这些子view的最终尺寸和布局位置。measuremethod是一个finalmethod,可以理解为measurement工作的准备。由于它是final方法,我们不能重写它。我们不需要太在意它,因为measure最终会调用onmeasure,我们可以重写。.密切关注这一点。layout和onlayout是同一种关系。父视图在调用子视图的布局方法时,会将上一个measure阶段确定的位置和大小传递给子视图。对于自定义view/viewgroup,我们几乎只需要关注以下三个需求:对于android自带的现有view,我们只需要重写它的onMeasure方法即可。修改此大小以满足要求。对于android系统来说,属于我们自定义的view,比上面的稍微复杂一点,onMeasure方法要完全重写。第三种最复杂,需要重写onmeasure和onlayout方法才能完成复杂viewgroup的测量和布局。onMeasure方法特别说明:如何理解父视图对子视图的限制?既然onMeasure的两个参数是父视图对子视图的限制,那么这个限制的值从何而来呢?实际上,父视图对子视图施加了限制。绝大多数视图限制来自我们开发人员设置的布局开头的属性。比如我们给一个imageview设置了layout_width和layout_height这两个属性,这两个属性其实就是我们开发者所期望的。width和height属性,但是注意,这两个属性是为父视图设置的。其实对于布局开始的大部分属性,这些属性都是为父视图设置的。为什么要将它们显示给父视图??因为父视图需要知道这些属性,才知道应该对子视图的测量进行哪些限制?它是无限的(未指定)吗?或者限制一个最大值(AT_MOST),让子view不超过这个值?或者直接限制死亡,我让你想怎么样就怎么样(EXACTLY)。自定义一个BannerImageView,修改onMeasure方法。所谓bannerImageview就是很多电商实际投放的广告图片。该广告图像的宽高比是可变的。我们在日常的开发过程中经常会遇到这样的需求:imageview中高保真标注了纵横比,但是考虑到很多手机的屏幕宽度或者高度是不确定的,所以我们通常要手动计算imageview的高度或者宽度,然后动态改变宽度或高度的值。这个方法可以用但是很麻烦。这里给出一个自定义的imageview,通过设置一个ratio属性可以动态设置iv的高度。看效果很方便***看代码,重要的部分都写在注释里了,就不多说了。publicclassBannerImageViewextendsImageView{//长宽比floatratio;publicBannerImageView(Contextcontext){super(context);}publicBannerImageView(Contextcontext,AttributeSetattrs){super(context,attrs);TypedArraytypedArray=context.obtainStyledAttributes(attrs,R.styleable.BannerImageView);ratio=typedArray.getFloat(R.styleable.BannerImageView_ratio,1.0f);typedArray.recycle();}publicBannerImageView(Contextcontext,AttributeSetattrs,intdefStyleAttr){super(context,attrs,defStyleAttr);}@OverrideprotectedvoidonMeasure(intwidthMeasure/Spec,intheightMeasure)/人还是要自己过measurement,因为这个方法内部会调用setMeasuredDimension方法保存测量结果//我们只有保存后才能得到这个测量结果,否则下面super.onMeasure(widthMeasureSpec,heightMeasureSpec);//是获取不到的获取测量结果intmWidth=getMeasuredWidth();intmHeight=(int)(mWidth*ratio);//保存后,父视图即可获取测量的宽高。不保存就得不到。setMeasuredDimension(mWidth,mHeight);}}自定义view,onMeasure方法完全自己写首先做一个结论:完全自定义的view,onMeasure方法完全自己写,保存的宽高必须符合parentviewLimit,否则会出现bug,而且父view的limit保存在子view上的方法也很简单,直接调用resolveSize方法即可。所以写一个完全自定义视图的onMeasure方法并不难。首先计算你想要的宽度和高度。例如,如果你画一个圆,宽度和高度必须是半径大小的两倍。如果圆下面有文字,那么高度除了半径的两倍外,还必须有字号。正确的。很简单。这完全取决于您的自定义视图是什么样的。计算出自己想要的宽高后,可以直接使用resolveSize方法进行处理。***setMeasuredDimension保存。例子:publicclassLoadingViewextendsView{//圆的半径intradius;//圆的外矩形rect的起点intleft=10,top=30;PaintmPaint=newPaint();publicLoadingView(Contextcontext){super(context);}publicLoadingView(Contextcontext,AttributeSetattrs){super(context,attrs);TypedArraytypedArray=context.obtainStyledAttributes(attrs,R.styleable.LoadingView);radius=typedArray.getInt(R.styleable.LoadingView_radius,0);}publicLoadingView(Contextcontext,AttributeSetattrs,intdefStyleAttr){super(context,attrs,defStyleAttr);}@OverrideprotectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){super.onMeasure(widthMeasureSpec,heightMeasureSpec);intwidth=left+radius*2;intheight=top+radius*2;//一定要使用resolveSize方法来格式化你view的宽高,否则遇到某些布局会出现奇怪的bug。//因为没有这个你根本就没有父视图的感觉***强调width=resolveSize(width,widthMeasureSpec);height=resolveSize(height,heightMeasureSpec);setMeasuredDimension(width,height);}@OverrideprotectedvoidonDraw(Canvascanvas){super.onDraw(canvas);RectFoval=newRectF(left,top,left+radius*2,top+radius*2);mPaint.setColor(Color.BLUE);canvas.drawRect(oval,mPaint);//先画圆弧mPaint.setColor(Color.RED);mPaint.setStyle(Paint.Style.STROKE);mPaint.setStrokeWidth(2);canvas.drawArc(oval,-90,♂,false,mPaint);}}布局文件:"wrap_content"android:layout_height="wrap_content"android:src="@mipmap/dly"app:radius="200">***作用:自定义viewgroup其实稍微复杂一点,不过还是有迹可循的,只是需要多一点耐心来自定义viewgroup。需要注意的点如下:一定要先重写onMeasure确定子view的宽高和自己的宽高才能继续写onlayout对这些子view进行布局~~onMeasureviewgroup的of其实就是遍历你自己的view,测量你的每一个子view。很多时候子视图的measure可以直接用measureChild()方法代替,这样可以简化我们的写法。如果你的viewgroup很复杂,你不能自己写measureChild而不调用measureChild。计算视图组本身的大小并保存。保存方式为setMeasuredDimension。不要忘记,当您必须重写measureChild方法时,其实并不难。这只是父视图的度量和子视图的度量之间的权衡。你在了解了基本的measureChild方法之后,以后肯定会自己写一个复杂的measureChild方法。下面是一个minimal的例子,非常简单的flowlayout实现,没有处理marginpadding,并且假设每个tag的高度都是固定的,可以说是极其简单,但是麻雀虽小,五脏俱全,足够你对自定义视图组的关键点有很好的理解。/***从左到右写一个简单的flowlayout。宽度不够就新建一行layout*类似的开源控件很多,很多都写的不错。我这里只实现了一个初级的flowlayout*,也是最简单的,目的是了解自定义viewgroup的关键核心点。*

*比如这里没有对padding或者margin做特殊处理。自己写viewgroup的时候,记得加上对这些属性的处理*否则,一旦有人用了这些属性,发现不生效,那就丑了。.....*/publicclassSimpleFlowLayoutextendsViewGroup{publicSimpleFlowLayout(Contextcontext){super(context);}publicSimpleFlowLayout(Contextcontext,AttributeSetattrs){super(context,attrs);}publicSimpleFlowLayout(Contextcontext,AttributeSetattrs,intdefStyleAttr){super(context/attrs,attrdef)Style}***布局算法其实放不下剩下的那一行,还得再写一行来理解这个过程。*每个人都有自己的写法,也许你的写法比开源项目好*其实不夸张的,不可能是前面的onMeasure结束后,就可以拿到所有的子view和自己测量宽高,然后计算**@paramchanged*@paraml*@paramt*@paramr*@paramb*/@OverrideprotectedvoidonLayout(booleanchanged,intl,intt,intr,intb){intchildTop=0;intchildLeft=0;intchildRight=0;intchildBottom=0;//usedwidthintusedWidth=0;//customlayout自己的可用宽度intlayoutWidth=getMeasuredWidth();Log.v("wuyue","layoutWidth=="+layoutWidth);for(inti=0;iremaining){//换行时,usedwidth当然要设置为0usedWidth=0;//换行后我们的总高度也要加上,否则高度不对totalHeight=totalHeight+lineHeight;}//已经使用的宽度累加usedWidth=usedWidth+childView.getMeasuredWidth();//TheheightofthecurrentviewlineHeight=childView.getMeasuredHeight();}//如果SimpleFlowLayout的高度是wrapcotent,我们就使用我们叠加的高度,否则,我们当然使用父view来限制高度,如果SimpleFlowLayout如果(heightMode==MeasureSpec.AT_MOST){heightSize=totalHeight;}setMeasuredDimension(widthSize,heightSize);}}***看下效果