自定义ViewGroup全屏选中效果前言 事情是这个样子的,前几天产品丢给我一个视频,你觉得这个效果怎么样?我们的App也做一个这个效果吧! 我当时的反应: 开什么玩笑!就没见过这么玩的,这不是坑人吗? 此时产品幽幽的回了一句,别人都能做,你怎么不能做,并且iOS说可以做,还很简单。 我心里一万个不信,糟老头子太坏了,想骗我? 我立马和iOS同事统一战线,说不能做,实现不了吧。结果iOS同事幽幽的说了一句已经做了,四行代码完成。 我勒个去,就指着我卷是吧。 这也没办法了,群里问问大神有什么好的方案,xdm,车先减个速,(图片)这个效果怎么实现? 做不了。。。 让产品滚。。。 没做过,也没见过。。。 性能不好,不推荐,换方案吧。 GridView嵌套ScrollView,要不RV嵌套RV?。。。 不理他,继续开车。。。 。。。群里技术氛围果然没有让我失望,哎,看来还是得靠自己,抬头望了望天天,扣了扣脑阔,无语啊。 好了,说了这么多玩笑话,回归正题,其实关于标题的这种效果,确实是对性能的开销更大,且网上相关开源的项目也几乎没找到。 到底怎么做呢?相信跟着我一起复习的小伙伴们心里都有了一点雏形。自定义ViewGroup。 下面跟着我一起再次巩固一次ViewGroup的测量与布局,加上事件的处理,就能完成对应的功能。 话不多说,Letsgo 一、布局的测量与布局 首先GridView嵌套ScrollView,RV嵌套RV什么的,就宽度就限制死了,其次滚动方向也固定死了,不好做。 肯定是选用自定义ViewGroup的方案,自己测量,自己布局,自己实现滚动与缩放逻辑。 从产品发的竞品App的视频来看,我们需要先明确三个变量,一行显示多少个Item、垂直距离每一个Item的间距,水平距离每一个Item的间距。 然后我们测量每一个ItemView的宽度,每一个Item的宽度加起来就是ViewGroup的宽度,每一个Item的高度加起来就是ViewGroup的高度。 我们目前先不限定Item的宽高,先试着测量一下:classCurtainViewContrainerextendsViewGroup{privateinthorizontalSpacing20;每一个Item的左右间距privateintverticalSpacing20;每一个Item的上下间距privateintmRowCount6;一行多少个ItemprivateAdaptermAdapter;publicCurtainViewContrainer(Contextcontext){this(context,null);}publicCurtainViewContrainer(Contextcontext,AttributeSetattrs){this(context,attrs,0);}publicCurtainViewContrainer(Contextcontext,AttributeSetattrs,intdefStyleAttr){super(context,attrs,defStyleAttr);init();}privatevoidinit(){setClipChildren(false);setClipToPadding(false);}SuppressLint(DrawAllocation)OverrideprotectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){finalintsizeWidthMeasureSpec。getSize(widthMeasureSpec)this。getPaddingRight()this。getPaddingLeft();finalintmodeWidthMeasureSpec。getMode(widthMeasureSpec);finalintsizeHeightMeasureSpec。getSize(heightMeasureSpec)this。getPaddingTop()this。getPaddingBottom();finalintmodeHeightMeasureSpec。getMode(heightMeasureSpec);intchildCountgetChildCount();if(mAdapternullmAdapter。getItemCount()0childCount0){setMeasuredDimension(sizeWidth,0);return;}intcurCount1;inttotalControlHeight0;inttotalControlWidth0;intlayoutChildViewCurXthis。getPaddingLeft();intcurRow0;intcurColumn0;SparseArrayIntegerrowWidthnewSparseArray();全部行的宽度开始遍历for(inti0;ichildCount;i){ViewchildViewgetChildAt(i);introwcurCountmRowCount;当前子View是第几行intcolumncurCountmRowCount;当前子View是第几列测量每一个子View宽度measureChild(childView,widthMeasureSpec,heightMeasureSpec);intwidthchildView。getMeasuredWidth();intheightchildView。getMeasuredHeight();booleanisLast(curCount1)mRowCount0;if(rowcurRow){layoutChildViewCurXwidthhorizontalSpacing;totalControlWidthwidthhorizontalSpacing;rowWidth。put(row,totalControlWidth);}else{已经换行了layoutChildViewCurXthis。getPaddingLeft();totalControlWidthwidthhorizontalSpacing;rowWidth。put(row,totalControlWidth);添加高度totalControlHeightheightverticalSpacing;}最多只摆放9个curCount;curRowrow;curColumncolumn;}循环结束之后开始计算真正的宽度ListIntegerwidthListnewArrayList(rowWidth。size());for(inti0;irowWidth。size();i){IntegerintegerrowWidth。get(i);widthList。add(integer);}IntegermaxWidthCollections。max(widthList);setMeasuredDimension(maxWidth,totalControlHeight);}复制代码 当遇到高度不统一的情况下,就会遇到问题,所以我们记录一下每一行的最高高度,用于计算控件的测量高度。 虽然这样测量是没有问题的,但是布局还是有坑,姑且先这么测量:OverrideprotectedvoidonLayout(booleanchanged,intl,intt,intr,intb){intchildCountgetChildCount();intcurCount1;intlayoutChildViewCurXl;intlayoutChildViewCurYt;intcurRow0;intcurColumn0;SparseArrayIntegerrowWidthnewSparseArray();全部行的宽度开始遍历for(inti0;ichildCount;i){ViewchildViewgetChildAt(i);introwcurCountmRowCount;当前子View是第几行intcolumncurCountmRowCount;当前子View是第几列每一个子View宽度intwidthchildView。getMeasuredWidth();intheightchildView。getMeasuredHeight();childView。layout(layoutChildViewCurX,layoutChildViewCurY,layoutChildViewCurXwidth,layoutChildViewCurYheight);if(rowcurRow){同一行layoutChildViewCurXwidthhorizontalSpacing;}else{换行了layoutChildViewCurXl;layoutChildViewCurYheightverticalSpacing;}最多只摆放9个curCount;curRowrow;curColumncolumn;}performBindData();}复制代码 这样做并没有紧挨着头上的Item,目前我们把Item的宽高都使用同样的大小,是勉强能看的,一旦高度不统一,就不能看了。 先不管那么多,先固定大小显示出来看看效果。 反正是能看了,一个寨版的GridView,但是超出了宽度的限制。接下来我们先做事件的处理,让他动起来。二、全屏滚动逻辑 首先我们需要把显示的ViewGroup控件封装为一个类,让此ViewGroup在另一个ViewGroup内部移动,不然还能让内部的每一个子View单独移动吗?肯定是整体一起移动更方便一点。 然后我们触摸容器ViewGroup中控制子ViewGroup移动即可,那怎么移动呢? 我知道,用MotionEventScroller就可以滚动啦! 可以!又不可以,Scroller确实是可以动起来,但是在我们拖动与缩放之后,不能影响到内部的点击事件。 那可以不可以用ViewDragHelper来实现动作效果? 也不行,虽然ViewDragHelper是ViewGroup专门用于移动的帮助类,但是它内部其实还是封装的MotionEventScroller。 而Scroller为什么不行? 这种效果我们不能使用Canvas的移动,不能使用Sroller去移动,因为它们不能记录移动后的View变化矩阵,我们需要使用基本的setTranslation来实现,自己控制矩阵的变化从而控制整个视图树。 我们把触摸的拦截与事件的处理放到一个公用的事件处理类中:publicclassTouchEventHandler{privatestaticfinalfloatMAXSCALE1。5f;最大能缩放值privatestaticfinalfloatMINSCALE0。8f;最小能缩放值当前的触摸事件类型privatestaticfinalintTOUCHMODEUNSET1;privatestaticfinalintTOUCHMODERELEASE0;privatestaticfinalintTOUCHMODESINGLE1;privatestaticfinalintTOUCHMODEDOUBLE2;privateViewmView;privateintmode0;privatefloatscaleFactor1。0f;privatefloatscaleBaseR;privateGestureDetectormGestureDetector;privatefloatmTouchSlop;privateMotionEventpreMovingTouchEventnull;privateMotionEventpreInterceptTouchEventnull;privatebooleanmIsMoving;privatefloatminScaleMINSCALE;privateFlingAnimationflingYnull;privateFlingAnimationflingXnull;privateViewBoxlayoutLocationInParentnewViewBox();移动中不断变化的盒模型privatefinalViewBoxviewportBoxnewViewBox();初始化的盒模型privatePointFpreFocusCenternewPointF();privatePointFpostFocusCenternewPointF();privatePointFpreTranslatenewPointF();privatefloatpreScaleFactor1f;privatefinalDynamicAnimation。OnAnimationUpdateListenerflingAnimateListener;privatebooleanisKeepInViewportfalse;privateTouchEventListenercontrolListenernull;privateintscalePercentOnlyForControlListener0;publicTouchEventHandler(Contextcontext,Viewview){this。mViewview;flingAnimateListener(animation,value,velocity)keepWithinBoundaries();mGestureDetectornewGestureDetector(context,newGestureDetector。SimpleOnGestureListener(){OverridepublicbooleanonFling(MotionEvente1,MotionEvente2,floatvelocityX,floatvelocityY){flingXnewFlingAnimation(mView,DynamicAnimation。TRANSLATIONX);flingX。setStartVelocity(velocityX)。addUpdateListener(flingAnimateListener)。start();flingYnewFlingAnimation(mView,DynamicAnimation。TRANSLATIONY);flingY。setStartVelocity(velocityY)。addUpdateListener(flingAnimateListener)。start();returnfalse;}});ViewConfigurationvcViewConfiguration。get(view。getContext());mTouchSlopvc。getScaledTouchSlop()0。8f;}设置内部布局视图窗口高度和宽度publicvoidsetViewport(intwinWidth,intwinHeight){viewportBox。setValues(0,0,winWidth,winHeight);}暴露的方法,内部处理事件并判断是否拦截事件publicbooleandetectInterceptTouchEvent(MotionEventevent){finalintactionevent。getAction()MotionEvent。ACTIONMASK;onTouchEvent(event);if(actionMotionEvent。ACTIONDOWN){preInterceptTouchEventMotionEvent。obtain(event);mIsMovingfalse;}if(actionMotionEvent。ACTIONCANCELactionMotionEvent。ACTIONUP){mIsMovingfalse;}if(actionMotionEvent。ACTIONMOVEmTouchSlopcalculateMoveDistance(event,preInterceptTouchEvent)){mIsMovingtrue;}returnmIsMoving;}当前事件的真正处理逻辑publicbooleanonTouchEvent(MotionEventevent){mGestureDetector。onTouchEvent(event);intactionevent。getAction()MotionEvent。ACTIONMASK;switch(action){caseMotionEvent。ACTIONDOWN:modeTOUCHMODESINGLE;preMovingTouchEventMotionEvent。obtain(event);if(flingX!null){flingX。cancel();}if(flingY!null){flingY。cancel();}break;caseMotionEvent。ACTIONUP:modeTOUCHMODERELEASE;break;caseMotionEvent。ACTIONPOINTERUP:caseMotionEvent。ACTIONCANCEL:modeTOUCHMODEUNSET;break;caseMotionEvent。ACTIONPOINTERDOWN:mode;if(modeTOUCHMODEDOUBLE){scaleFactorpreScaleFactormView。getScaleX();preTranslate。set(mView。getTranslationX(),mView。getTranslationY());scaleBaseR(float)distanceBetweenFingers(event);centerPointBetweenFingers(event,preFocusCenter);centerPointBetweenFingers(event,postFocusCenter);}break;caseMotionEvent。ACTIONMOVE:if(modeTOUCHMODEDOUBLE){双指缩放floatscaleNewR(float)distanceBetweenFingers(event);centerPointBetweenFingers(event,postFocusCenter);if(scaleBaseR0){break;}scaleFactor(scaleNewRscaleBaseR)preScaleFactor0。15fscaleFactor0。85f;intscaleStateTouchEventListener。FREESCALE;floatfinalMinScaleisKeepInViewport?minScale:minScale0。8f;if(scaleFactorMAXSCALE){scaleFactorMAXSCALE;scaleStateTouchEventListener。MAXSCALE;}elseif(scaleFactorfinalMinScale){scaleFactorfinalMinScale;scaleStateTouchEventListener。MINSCALE;}if(controlListener!null){intcurrent(int)(scaleFactor100);回调if(scalePercentOnlyForControlListener!current){scalePercentOnlyForControlListenercurrent;controlListener。onScaling(scaleState,scalePercentOnlyForControlListener);}}mView。setPivotX(0);mView。setPivotY(0);mView。setScaleX(scaleFactor);mView。setScaleY(scaleFactor);floattxpostFocusCenter。x(preFocusCenter。xpreTranslate。x)scaleFactorpreScaleFactor;floattypostFocusCenter。y(preFocusCenter。ypreTranslate。y)scaleFactorpreScaleFactor;mView。setTranslationX(tx);mView。setTranslationY(ty);keepWithinBoundaries();}elseif(modeTOUCHMODESINGLE){单指移动floatdeltaXevent。getRawX()preMovingTouchEvent。getRawX();floatdeltaYevent。getRawY()preMovingTouchEvent。getRawY();onSinglePointMoving(deltaX,deltaY);}break;caseMotionEvent。ACTIONOUTSIDE:外界的事件break;}preMovingTouchEventMotionEvent。obtain(event);returntrue;}计算两个事件的移动距离privatefloatcalculateMoveDistance(MotionEventevent1,MotionEventevent2){if(event1nullevent2null){return0f;}floatdisXMath。abs(event1。getRawX()event2。getRawX());floatdisYMath。abs(event1。getRawX()event2。getRawX());return(float)Math。sqrt(disXdisXdisYdisY);}单指移动privatevoidonSinglePointMoving(floatdeltaX,floatdeltaY){floattranslationXmView。getTranslationX()deltaX;mView。setTranslationX(translationX);floattranslationYmView。getTranslationY()deltaY;mView。setTranslationY(translationY);keepWithinBoundaries();}需要保持在界限之内privatevoidkeepWithinBoundaries(){默认不在界限内,不做限制,直接返回if(!isKeepInViewport){return;}calculateBound();intdBottomlayoutLocationInParent。bottomviewportBox。bottom;intdToplayoutLocationInParent。topviewportBox。top;intdLeftlayoutLocationInParent。leftviewportBox。left;intdRightlayoutLocationInParent。rightviewportBox。right;floattranslationXmView。getTranslationX();floattranslationYmView。getTranslationY();边界限制if(dLeft0){mView。setTranslationX(translationXdLeft);}if(dRight0){mView。setTranslationX(translationXdRight);}if(dBottom0){mView。setTranslationY(translationYdBottom);}if(dTop0){mView。setTranslationY(translationYdTop);}}移动时计算边界,赋值给本地的视图privatevoidcalculateBound(){ViewvmView;floatleftv。getLeft()v。getScaleX()v。getTranslationX();floattopv。getTop()v。getScaleY()v。getTranslationY();floatrightv。getRight()v。getScaleX()v。getTranslationX();floatbottomv。getBottom()v。getScaleY()v。getTranslationY();layoutLocationInParent。setValues((int)top,(int)left,(int)right,(int)bottom);}计算两个手指之间的距离privatedoubledistanceBetweenFingers(MotionEventevent){if(event。getPointerCount()1){floatdisXMath。abs(event。getX(0)event。getX(1));floatdisYMath。abs(event。getY(0)event。getY(1));returnMath。sqrt(disXdisXdisYdisY);}return1;}计算两个手指之间的中心点privatevoidcenterPointBetweenFingers(MotionEventevent,PointFpoint){floatxPoint0event。getX(0);floatyPoint0event。getY(0);floatxPoint1event。getX(1);floatyPoint1event。getY(1);point。set((xPoint0xPoint1)2f,(yPoint0yPoint1)2f);}设置视图是否要保持在窗口中publicvoidsetKeepInViewport(booleankeepInViewport){isKeepInViewportkeepInViewport;}设置控制的监听回调publicvoidsetControlListener(TouchEventListenercontrolListener){this。controlListenercontrolListener;}}复制代码 由于内部封装了移动与缩放的处理,所以我们只需要在事件容器内部调用这个方法即可:publicclassCurtainLayoutextendsFrameLayout{privatefinalTouchEventHandlermGestureHandler;privateCurtainViewContrainermCurtainViewContrainer;privatebooleandisallowInterceptfalse;publicCurtainLayout(NonNullContextcontext){this(context,null);}publicCurtainLayout(NonNullContextcontext,NullableAttributeSetattrs){this(context,attrs,0);}publicCurtainLayout(NonNullContextcontext,NullableAttributeSetattrs,intdefStyleAttr){super(context,attrs,defStyleAttr);setClipChildren(false);setClipToPadding(false);mCurtainViewContrainernewCurtainViewContrainer(getContext());addView(mCurtainViewContrainer);mGestureHandlernewTouchEventHandler(getContext(),mCurtainViewContrainer);设置是否在窗口内移动mGestureHandler。setKeepInViewport(false);}OverridepublicvoidrequestDisallowInterceptTouchEvent(booleandisallowIntercept){super。requestDisallowInterceptTouchEvent(disallowIntercept);this。disallowInterceptdisallowIntercept;}OverridepublicbooleanonInterceptTouchEvent(MotionEventevent){return(!disallowInterceptmGestureHandler。detectInterceptTouchEvent(event))super。onInterceptTouchEvent(event);}OverridepublicbooleanonTouchEvent(MotionEventevent){return!disallowInterceptmGestureHandler。onTouchEvent(event);}OverrideprotectedvoidonSizeChanged(intw,inth,intoldw,intoldh){mGestureHandler。setViewport(w,h);}}复制代码 对于一些复杂的处理都做了相关的注释,接下来看看加了事件处理之后的效果: 已经可以自由拖动与缩放了,但是目前的测量与布局是有问题的,加下来我们抽取与优化一下。三、抽取Adapter与LayoutManager 首先,内部的子View肯定是不能直接写在xml中的,太不优雅了,加下来我们定义一个Adapter,用于填充数据,顺便做一个多类型的布局。publicabstractclassCurtainAdapter{返回总共子View的数量publicabstractintgetItemCount();根据索引创建不同的布局类型,如果都是一样的布局则不需要重写publicintgetItemViewType(intposition){return0;}根据类型创建对应的View布局publicabstractViewonCreateItemView(NonNullContextcontext,NonNullViewGroupparent,intitemType);可以根据类型或索引绑定数据publicabstractvoidonBindItemView(NonNullViewitemView,intitemType,intposition);}复制代码 然后就是在绘制布局中通过设置Apdater来实现布局的添加与绑定逻辑。publicvoidsetAdapter(CurtainAdapteradapter){mAdapteradapter;inflateAllViews();}publicCurtainAdaptergetAdapter(){returnmAdapter;}填充Adapter布局privatevoidinflateAllViews(){removeAllViewsInLayout();if(mAdapternullmAdapter。getItemCount()0){return;}添加布局for(inti0;imAdapter。getItemCount();i){intitemTypemAdapter。getItemViewType(i);ViewviewmAdapter。onCreateItemView(getContext(),this,itemType);addView(view);}requestLayout();}绑定布局中的数据privatevoidperformBindData(){if(mAdapternullmAdapter。getItemCount()0){return;}post((){for(inti0;imAdapter。getItemCount();i){intitemTypemAdapter。getItemViewType(i);ViewviewgetChildAt(i);mAdapter。onBindItemView(view,itemType,i);}});}复制代码 当然需要在指定的地方调用了,测量与布局中都需要处理。OverrideprotectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){intchildCountgetChildCount();if(mAdapternullmAdapter。getItemCount()0childCount0){setMeasuredDimension(0,0);return;}。。。}OverrideprotectedvoidonLayout(booleanchanged,intl,intt,intr,intb){if(mAdapternullmAdapter。getItemCount()0){return;}performLayout();performBindData();}复制代码 接下来的重点就是我们对布局的方式进行抽象化,最简单的肯定是上面这种宽高固定的,如果是垂直的排列,我们设置一个垂直的瀑布流管理器,设置宽度固定,高度自适应,如果宽度不固定,那么是无法到达瀑布流的效果的。 同理对另一种水平排列的瀑布流我们设置高度固定,宽度自适应。 所以必须要设置LayoutManager,如果不设置就抛异常。 接下来就是LayoutManager的接口与具体调用:publicinterfaceILayoutManager{publicstaticfinalintDIRECTIONVERITICAL0;publicstaticfinalintDIRECTIONHORIZONTAL1;publicabstractint〔〕performMeasure(ViewGroupviewGroup,introwCount,inthorizontalSpacing,intverticalSpacing,intfixedValue);publicabstractvoidperformLayout(ViewGroupviewGroup,introwCount,inthorizontalSpacing,intverticalSpacing,intfixedValue);publicabstractintgetLayoutDirection();}复制代码 有了接口之后我们就可以先写调用了:classCurtainViewContrainerextendsViewGroup{privateILayoutManagermLayoutManager;privateinthorizontalSpacing20;每一个Item的左右间距privateintverticalSpacing20;每一个Item的上下间距privateintmRowCount6;一行多少个ItemprivateintfixedWidthCommUtils。dip2px(150);如果是垂直瀑布流,需要设置宽度固定privateintfixedHeightCommUtils。dip2px(180);先写死,后期在抽取属性privateCurtainAdaptermAdapter;SuppressLint(DrawAllocation)OverrideprotectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){intchildCountgetChildCount();if(mAdapternullmAdapter。getItemCount()0childCount0){setMeasuredDimension(0,0);return;}measureChildren(widthMeasureSpec,heightMeasureSpec);if(mLayoutManager!null(fixedWidth0fixedHeight0)){for(inti0;ichildCount;i){ViewchildViewgetChildAt(i);if(mLayoutManager。getLayoutDirection()ILayoutManager。DIRECTIONVERITICAL){measureChild(childView,MeasureSpec。makeMeasureSpec(fixedWidth,MeasureSpec。EXACTLY),heightMeasureSpec);}else{measureChild(childView,widthMeasureSpec,MeasureSpec。makeMeasureSpec(fixedHeight,MeasureSpec。EXACTLY));}}int〔〕dimensionsmLayoutManager。performMeasure(this,mRowCount,horizontalSpacing,verticalSpacing,mLayoutManager。getLayoutDirection()ILayoutManager。DIRECTIONVERITICAL?fixedWidth:fixedHeight);setMeasuredDimension(dimensions〔0〕,dimensions〔1〕);}else{thrownewRuntimeException(YouneedtosetthelayoutManagerfirst);}}OverrideprotectedvoidonLayout(booleanchanged,intl,intt,intr,intb){if(mAdapternullmAdapter。getItemCount()0){return;}if(mLayoutManager!null(fixedWidth0fixedHeight0)){mLayoutManager。performLayout(this,mRowCount,horizontalSpacing,verticalSpacing,mLayoutManager。getLayoutDirection()ILayoutManager。DIRECTIONVERITICAL?fixedWidth:fixedHeight);performBindData();}else{thrownewRuntimeException(YouneedtosetthelayoutManagerfirst);}}复制代码 那么我们先来水平的LayoutManager,相对简单一些,看看如何具体实现:publicclassHorizontalLayoutManagerimplementsILayoutManager{Overridepublicint〔〕performMeasure(ViewGroupviewGroup,introwCount,inthorizontalSpacing,intverticalSpacing,intfixedHeight){intchildCountviewGroup。getChildCount();intcurCount0;inttotalControlHeight0;inttotalControlWidth0;intcurRow0;SparseArrayIntegerrowTotalWidthnewSparseArray();每一行的总宽度开始遍历for(inti0;ichildCount;i){ViewchildViewviewGroup。getChildAt(i);introwcurCountrowCount;当前子View是第几行已经测量过了,直接取宽高intwidthchildView。getMeasuredWidth();if(rowcurRow){当前行totalControlWidthwidthhorizontalSpacing;}else{换行了totalControlWidthwidthhorizontalSpacing;}rowTotalWidth。put(row,totalControlWidth);赋值curCount;curRowrow;}循环结束之后开始计算真正的宽高totalControlHeight(rowCount(fixedHeightverticalSpacing))verticalSpacingviewGroup。getPaddingTop()viewGroup。getPaddingBottom();ListIntegerwidthListnewArrayList();for(inti0;irowTotalWidth。size();i){IntegerwidthrowTotalWidth。get(i);widthList。add(width);}totalControlWidthCollections。max(widthList);rowTotalWidth。clear();rowTotalWidthnull;returnnewint〔〕{totalControlWidthhorizontalSpacing,totalControlHeightverticalSpacing};}OverridepublicvoidperformLayout(ViewGroupviewGroup,introwCount,inthorizontalSpacing,intverticalSpacing,intfixedHeight){intchildCountviewGroup。getChildCount();intcurCount1;intlayoutChildViewCurXviewGroup。getPaddingLeft();intlayoutChildViewCurYviewGroup。getPaddingTop();intcurRow0;开始遍历for(inti0;ichildCount;i){ViewchildViewviewGroup。getChildAt(i);introwcurCountrowCount;当前子View是第几行每一个子View宽度intwidthchildView。getMeasuredWidth();childView。layout(layoutChildViewCurX,layoutChildViewCurY,layoutChildViewCurXwidth,layoutChildViewCurYfixedHeight);if(rowcurRow){同一行layoutChildViewCurXwidthhorizontalSpacing;}else{换行了layoutChildViewCurXchildView。getPaddingLeft();layoutChildViewCurYfixedHeightverticalSpacing;}赋值curCount;curRowrow;}}OverridepublicintgetLayoutDirection(){returnDIRECTIONHORIZONTAL;}}复制代码 对于水平的布局方式来说,高度是固定的,我们很容易的就能计算出来,但是宽度每一行的可能都不一样,我们用一个List记录每一行的总宽度,在最后设置的时候取出最大的一行作为容器的宽度,记得要减去一个间距哦。 那么不同宽度的水平布局方式效果的实现就是这样: 实现是实现了,但是这么计算是不是有问题?每一行的最高高度好像不是太准确,如果每一列都有一个最大高度,但是不是同一列,那么测量的高度就比实际高度要更高。 加一个灰色背景就可以看到效果: 我们再优化一下,它应该是计算每一列的总共高度,然后选出最大高度才对:Overridepublicint〔〕performMeasure(ViewGroupviewGroup,introwCount,inthorizontalSpacing,intverticalSpacing,intfixedWidth){intchildCountviewGroup。getChildCount();intcurPosition0;inttotalControlHeight0;inttotalControlWidth0;SparseArrayListIntegercolumnAllHeightnewSparseArray();每一列的全部高度开始遍历for(inti0;ichildCount;i){ViewchildViewviewGroup。getChildAt(i);introwcurPositionrowCount;当前子View是第几行intcolumncurPositionrowCount;当前子View是第几列已经测量过了,直接取宽高intheightchildView。getMeasuredHeight();ListIntegerintegerscolumnAllHeight。get(column);if(integersnullintegers。isEmpty()){integersnewArrayList();}integers。add(heightverticalSpacing);columnAllHeight。put(column,integers);赋值curPosition;}循环结束之后开始计算真正的宽高totalControlWidth(rowCount(fixedWidthhorizontalSpacing)viewGroup。getPaddingLeft()viewGroup。getPaddingRight());ListIntegertotalHeightsnewArrayList();for(inti0;icolumnAllHeight。size();i){ListIntegerheightscolumnAllHeight。get(i);inttotalHeight0;for(intj0;jheights。size();j){totalHeightheights。get(j);}totalHeights。add(totalHeight);}totalControlHeightCollections。max(totalHeights);columnAllHeight。clear();columnAllHeightnull;returnnewint〔〕{totalControlWidthhorizontalSpacing,totalControlHeightverticalSpacing};}复制代码 再看看效果: 宽高真正的测量准确之后我们接下来就开始属性的抽取与封装了。四、自定义属性 我们先前都是使用的成员变量来控制一些间距与逻辑的触发,这就跟业务耦合了,如果想做到通用的一个效果,肯定还是要抽取自定义属性,做到对应的配置开关,就可以适应更多的场景使用,也是开源项目的必备技能。 细数一下我们需要控制的属性:enableScale是否支持缩放maxScale缩放的最大比例minScale缩放的最小比例moveInViewport是否只能在布局内部移动horizontalSpacingitem的水平间距verticalSpacingitem的垂直间距fixedwidth竖向的排列宽度定死并设置对应的LayoutManagerfixedheight横向的排列高度定死并设置对应的LayoutManager 定义属性如下:!全屏幕布布局自定义属性declarestyleablenameCurtainLayout!Item的横向间距!Item的垂直间距!每行需要展示多少数量的Item!垂直方向瀑布流布局,固定宽度为多少!水平方向瀑布流布局,固定高度为多少!是否只能在布局内部移动当为false时候为自由移动!是否可以缩放!最大与最小的缩放比例declarestyleable复制代码 取出属性并对容器布局与触摸处理器做赋值的操作:publicclassCurtainLayoutextendsFrameLayout{privateinthorizontalSpacing;privateintverticalSpacing;privateintrowCount;privateintfixedWidth;privateintfixedHeight;privatebooleanmoveInViewport;privatebooleanenableScale;privatefloatmaxScale;privatefloatminScale;publicCurtainLayout(NonNullContextcontext,NullableAttributeSetattrs,intdefStyleAttr){super(context,attrs,defStyleAttr);setClipChildren(false);setClipToPadding(false);mCurtainViewContrainernewCurtainViewContrainer(getContext());addView(mCurtainViewContrainer);initAttr(context,attrs);mGestureHandlernewTouchEventHandler(getContext(),mCurtainViewContrainer);设置是否在窗口内移动mGestureHandler。setKeepInViewport(moveInViewport);mGestureHandler。setEnableScale(enableScale);mGestureHandler。setMinScale(minScale);mGestureHandler。setMaxScale(maxScale);mCurtainViewContrainer。setHorizontalSpacing(horizontalSpacing);mCurtainViewContrainer。setVerticalSpacing(verticalSpacing);mCurtainViewContrainer。setRowCount(rowCount);mCurtainViewContrainer。setFixedWidth(fixedWidth);mCurtainViewContrainer。setFixedHeight(fixedHeight);if(fixedWidth0fixedHeight0){if(fixedWidth0){mCurtainViewContrainer。setLayoutDirectionVertical(fixedWidth);}else{mCurtainViewContrainer。setLayoutDirectionHorizontal(fixedHeight);}}}获取自定义属性privatevoidinitAttr(Contextcontext,AttributeSetattrs){TypedArraymTypedArraycontext。obtainStyledAttributes(attrs,R。styleable。CurtainLayout);this。horizontalSpacingmTypedArray。getDimensionPixelSize(R。styleable。CurtainLayouthorizontalSpacing,20);this。verticalSpacingmTypedArray。getDimensionPixelSize(R。styleable。CurtainLayoutverticalSpacing,20);this。rowCountmTypedArray。getInteger(R。styleable。CurtainLayoutrowCount,6);this。fixedWidthmTypedArray。getDimensionPixelOffset(R。styleable。CurtainLayoutfixedWidth,150);this。fixedHeightmTypedArray。getDimensionPixelSize(R。styleable。CurtainLayoutfixedHeight,180);this。moveInViewportmTypedArray。getBoolean(R。styleable。CurtainLayoutmoveInViewport,false);this。enableScalemTypedArray。getBoolean(R。styleable。CurtainLayoutenableScale,true);this。minScalemTypedArray。getFloat(R。styleable。CurtainLayoutminScale,0。7f);this。maxScalemTypedArray。getFloat(R。styleable。CurtainLayoutmaxScale,1。5f);mTypedArray。recycle();}。。。publicvoidsetMoveInViewportInViewport(booleanmoveInViewport){this。moveInViewportmoveInViewport;mGestureHandler。setKeepInViewport(moveInViewport);}publicvoidsetEnableScale(booleanenableScale){this。enableScaleenableScale;mGestureHandler。setEnableScale(enableScale);}publicvoidsetMinScale(floatminScale){this。minScaleminScale;mGestureHandler。setMinScale(minScale);}publicvoidsetMaxScale(floatmaxScale){this。maxScalemaxScale;mGestureHandler。setMaxScale(maxScale);}publicvoidsetHorizontalSpacing(inthorizontalSpacing){mCurtainViewContrainer。setHorizontalSpacing(horizontalSpacing);}publicvoidsetVerticalSpacing(intverticalSpacing){mCurtainViewContrainer。setVerticalSpacing(verticalSpacing);}publicvoidsetRowCount(introwCount){mCurtainViewContrainer。setRowCount(rowCount);}publicvoidsetFixedWidth(intfixedWidth){mCurtainViewContrainer。setLayoutDirectionVertical(fixedWidth);}publicvoidsetFixedHeight(intfixedHeight){mCurtainViewContrainer。setLayoutDirectionHorizontal(fixedHeight);}复制代码 然后在布局容器与事件处理类中做对应的赋值操作即可。 如何使用?CurtainLayoutandroid:ididcurtainviewandroid:layoutwidthmatchparentandroid:layoutheightmatchparentapp:enableScaletrueapp:fixedWidth150dpapp:horizontalSpacing10dpapp:maxScale1。5app:minScale0。8app:moveInViewporttrueapp:rowCount6app:verticalSpacing10dpCurtainLayout复制代码 如果在xml中设置过fixedWidth或者fixedHeight,那么在Activity中也可以不设置LayoutManager了。vallistlistOfString(。。。)valadapterViewgroup6Adapter(list)valcurtainViewfindViewByIdCurtainLayout(R。id。curtainview)curtainView。adapteradapter复制代码 最终效果: 后记 关于ViewGroup的测量与布局与事件,我们已经从易到难复习了四期了,相信同学应该是能掌握了。 话说到里就应该到了完结时刻,关于自定义View与自定义ViewGroup的复习与回顾就到此告一段落了,对于市面上能见到的一些布局效果,基本上能通过自定义ViewGroup与自定义View来实现。其实很早就想完结了,因为感觉这些东西有一点过于基础了,好像大家都不是很有兴趣看这些基础的东西, 自定义View可以很方便的做自定义的绘制与本身与内部的一些移动,而对于一些多View移动的特效,我们就算用自定义View难以实现或实现的比较复杂的话,也能使用Behivor或者MotionLayot来实现,当然这就是另一个篇章了。 如果有兴趣也可以看看我之前的Behivor文章【传送门】或者MotionLayot的文章,【传送门】。 同时也可以搜索与翻看之前的文章哦。 本文的代码均可以在我的Kotlin测试项目中看到,【传送门】。你也可以关注我的这个Kotlin项目,我有时间都会持续更新。 关于本文的全屏滑动效果,我也会开源传到MavenCentral供大家依赖使用,【传送门】 使用:Gradle中直接依赖即可: implementationcom。gitee。newki123456:curtainlayout:1。0。0 好了,如果类似的效果有更多的更好的其他方式,也希望大家能评论区交流一下。 惯例,我如有讲解不到位或错漏的地方,希望同学们可以指出。 如果感觉本文对你有一点点的帮助,还望你能点赞支持一下,你的支持是我最大的动力。 哎,找图片都找了接近一个小时,如果大家想要对应的图片也可以去项目中拿哦! Ok,这一期就此完结。