沧州三亚菏泽经济预测自然
投稿投诉
自然科学
知识物理
化学生物
地理解释
预测理解
本质社会
人类现象
行为研究
经济政治
心理结构
关系指导
人文遗产
菏泽德阳
山西湖州
宝鸡上海
茂名内江
三亚信阳
长春北海
西安安徽
黄石烟台
沧州湛江
肇庆鹤壁
六安韶关
成都钦州

Android自定义ViewGroup嵌套与交互实战,幕布全

7月27日 火凤派投稿
  自定义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;一行多少个ItemprivateAdaptermApublicCurtainViewContrainer(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);}intcurCount1;inttotalControlHeight0;inttotalControlWidth0;intlayoutChildViewCurXthis。getPaddingLeft();intcurRow0;intcurColumn0;SparseArrayIntegerrowWidthnewSparseArray();全部行的宽度开始遍历for(inti0;ichildCi){ViewchildViewgetChildAt(i);introwcurCountmRowC当前子View是第几行intcolumncurCountmRowC当前子View是第几列测量每一个子View宽度measureChild(childView,widthMeasureSpec,heightMeasureSpec);intwidthchildView。getMeasuredWidth();intheightchildView。getMeasuredHeight();booleanisLast(curCount1)mRowCount0;if(rowcurRow){layoutChildViewCurXwidthhorizontalStotalControlWidthwidthhorizontalSrowWidth。put(row,totalControlWidth);}else{已经换行了layoutChildViewCurXthis。getPaddingLeft();totalControlWidthwidthhorizontalSrowWidth。put(row,totalControlWidth);添加高度totalControlHeightheightverticalS}最多只摆放9个curCcurRcurC}循环结束之后开始计算真正的宽度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;ichildCi){ViewchildViewgetChildAt(i);introwcurCountmRowC当前子View是第几行intcolumncurCountmRowC当前子View是第几列每一个子View宽度intwidthchildView。getMeasuredWidth();intheightchildView。getMeasuredHeight();childView。layout(layoutChildViewCurX,layoutChildViewCurY,layoutChildViewCurXwidth,layoutChildViewCurYheight);if(rowcurRow){同一行layoutChildViewCurXwidthhorizontalS}else{换行了layoutChildViewCurXl;layoutChildViewCurYheightverticalS}最多只摆放9个curCcurRcurC}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;privateViewmVprivateintmode0;privatefloatscaleFactor1。0f;privatefloatscaleBaseR;privateGestureDetectormGestureDprivatefloatmTouchSprivateMotionEventpreMovingTouchEprivateMotionEventpreInterceptTouchEprivatebooleanmIsMprivatefloatminScaleMINSCALE;privateFlingAnimationflingYprivateFlingAnimationflingXprivateViewBoxlayoutLocationInParentnewViewBox();移动中不断变化的盒模型privatefinalViewBoxviewportBoxnewViewBox();初始化的盒模型privatePointFpreFocusCenternewPointF();privatePointFpostFocusCenternewPointF();privatePointFpreTranslatenewPointF();privatefloatpreScaleFactor1f;privatefinalDynamicAnimation。OnAnimationUpdateListenerflingAnimateLprivatebooleanisKeepInVprivateTouchEventListenercontrolLprivateintscalePercentOnlyForControlListener0;publicTouchEventHandler(Contextcontext,Viewview){this。mVflingAnimateListener(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();}});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);mIsM}if(actionMotionEvent。ACTIONCANCELactionMotionEvent。ACTIONUP){mIsM}if(actionMotionEvent。ACTIONMOVEmTouchSlopcalculateMoveDistance(event,preInterceptTouchEvent)){mIsM}returnmIsM}当前事件的真正处理逻辑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();}caseMotionEvent。ACTIONUP:modeTOUCHMODERELEASE;caseMotionEvent。ACTIONPOINTERUP:caseMotionEvent。ACTIONCANCEL:modeTOUCHMODEUNSET;caseMotionEvent。ACTIONPOINTERDOWN:if(modeTOUCHMODEDOUBLE){scaleFactorpreScaleFactormView。getScaleX();preTranslate。set(mView。getTranslationX(),mView。getTranslationY());scaleBaseR(float)distanceBetweenFingers(event);centerPointBetweenFingers(event,preFocusCenter);centerPointBetweenFingers(event,postFocusCenter);}caseMotionEvent。ACTIONMOVE:if(modeTOUCHMODEDOUBLE){双指缩放floatscaleNewR(float)distanceBetweenFingers(event);centerPointBetweenFingers(event,postFocusCenter);if(scaleBaseR0){}scaleFactor(scaleNewRscaleBaseR)preScaleFactor0。15fscaleFactor0。85f;intscaleStateTouchEventListener。FREESCALE;floatfinalMinScaleisKeepInViewport?minScale:minScale0。8f;if(scaleFactorMAXSCALE){scaleFactorMAXSCALE;scaleStateTouchEventListener。MAXSCALE;}elseif(scaleFactorfinalMinScale){scaleFactorfinalMinSscaleStateTouchEventListener。MINSCALE;}if(controlListener!null){intcurrent(int)(scaleFactor100);回调if(scalePercentOnlyForControlListener!current){scalePercentOnlyForControlLcontrolListener。onScaling(scaleState,scalePercentOnlyForControlListener);}}mView。setPivotX(0);mView。setPivotY(0);mView。setScaleX(scaleFactor);mView。setScaleY(scaleFactor);floattxpostFocusCenter。x(preFocusCenter。xpreTranslate。x)scaleFactorpreScaleFfloattypostFocusCenter。y(preFocusCenter。ypreTranslate。y)scaleFactorpreScaleFmView。setTranslationX(tx);mView。setTranslationY(ty);keepWithinBoundaries();}elseif(modeTOUCHMODESINGLE){单指移动floatdeltaXevent。getRawX()preMovingTouchEvent。getRawX();floatdeltaYevent。getRawY()preMovingTouchEvent。getRawY();onSinglePointMoving(deltaX,deltaY);}caseMotionEvent。ACTIONOUTSIDE:外界的事件}preMovingTouchEventMotionEvent。obtain(event);}计算两个事件的移动距离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){}calculateBound();intdBottomlayoutLocationInParent。bottomviewportBox。intdToplayoutLocationInParent。topviewportBox。intdLeftlayoutLocationInParent。leftviewportBox。intdRightlayoutLocationInParent。rightviewportBox。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(){ViewvmVfloatleftv。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){isKeepInViewportkeepInV}设置控制的监听回调publicvoidsetControlListener(TouchEventListenercontrolListener){this。controlListenercontrolL}}复制代码
  由于内部封装了移动与缩放的处理,所以我们只需要在事件容器内部调用这个方法即可:publicclassCurtainLayoutextendsFrameLayout{privatefinalTouchEventHandlermGestureHprivateCurtainViewContrainermCurtainViewCprivatebooleandisallowIpublicCurtainLayout(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。disallowInterceptdisallowI}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){mAinflateAllViews();}publicCurtainAdaptergetAdapter(){returnmA}填充Adapter布局privatevoidinflateAllViews(){removeAllViewsInLayout();if(mAdapternullmAdapter。getItemCount()0){}添加布局for(inti0;imAdapter。getItemCount();i){intitemTypemAdapter。getItemViewType(i);ViewviewmAdapter。onCreateItemView(getContext(),this,itemType);addView(view);}requestLayout();}绑定布局中的数据privatevoidperformBindData(){if(mAdapternullmAdapter。getItemCount()0){}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);}。。。}OverrideprotectedvoidonLayout(booleanchanged,intl,intt,intr,intb){if(mAdapternullmAdapter。getItemCount()0){}performLayout();performBindData();}复制代码
  接下来的重点就是我们对布局的方式进行抽象化,最简单的肯定是上面这种宽高固定的,如果是垂直的排列,我们设置一个垂直的瀑布流管理器,设置宽度固定,高度自适应,如果宽度不固定,那么是无法到达瀑布流的效果的。
  同理对另一种水平排列的瀑布流我们设置高度固定,宽度自适应。
  所以必须要设置LayoutManager,如果不设置就抛异常。
  接下来就是LayoutManager的接口与具体调用:publicinterfaceILayoutManager{publicstaticfinalintDIRECTIONVERITICAL0;publicstaticfinalintDIRECTIONHORIZONTAL1;publicabstractint〔〕performMeasure(ViewGroupviewGroup,introwCount,inthorizontalSpacing,intverticalSpacing,intfixedValue);publicabstractvoidperformLayout(ViewGroupviewGroup,introwCount,inthorizontalSpacing,intverticalSpacing,intfixedValue);publicabstractintgetLayoutDirection();}复制代码
  有了接口之后我们就可以先写调用了:classCurtainViewContrainerextendsViewGroup{privateILayoutManagermLayoutMprivateinthorizontalSpacing20;每一个Item的左右间距privateintverticalSpacing20;每一个Item的上下间距privateintmRowCount6;一行多少个ItemprivateintfixedWidthCommUtils。dip2px(150);如果是垂直瀑布流,需要设置宽度固定privateintfixedHeightCommUtils。dip2px(180);先写死,后期在抽取属性privateCurtainAdaptermASuppressLint(DrawAllocation)OverrideprotectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){intchildCountgetChildCount();if(mAdapternullmAdapter。getItemCount()0childCount0){setMeasuredDimension(0,0);}measureChildren(widthMeasureSpec,heightMeasureSpec);if(mLayoutManager!null(fixedWidth0fixedHeight0)){for(inti0;ichildCi){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){}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;ichildCi){ViewchildViewviewGroup。getChildAt(i);introwcurCountrowC当前子View是第几行已经测量过了,直接取宽高intwidthchildView。getMeasuredWidth();if(rowcurRow){当前行totalControlWidthwidthhorizontalS}else{换行了totalControlWidthwidthhorizontalS}rowTotalWidth。put(row,totalControlWidth);赋值curCcurR}循环结束之后开始计算真正的宽高totalControlHeight(rowCount(fixedHeightverticalSpacing))verticalSpacingviewGroup。getPaddingTop()viewGroup。getPaddingBottom();ListIntegerwidthListnewArrayList();for(inti0;irowTotalWidth。size();i){IntegerwidthrowTotalWidth。get(i);widthList。add(width);}totalControlWidthCollections。max(widthList);rowTotalWidth。clear();rowTotalWreturnnewint〔〕{totalControlWidthhorizontalSpacing,totalControlHeightverticalSpacing};}OverridepublicvoidperformLayout(ViewGroupviewGroup,introwCount,inthorizontalSpacing,intverticalSpacing,intfixedHeight){intchildCountviewGroup。getChildCount();intcurCount1;intlayoutChildViewCurXviewGroup。getPaddingLeft();intlayoutChildViewCurYviewGroup。getPaddingTop();intcurRow0;开始遍历for(inti0;ichildCi){ViewchildViewviewGroup。getChildAt(i);introwcurCountrowC当前子View是第几行每一个子View宽度intwidthchildView。getMeasuredWidth();childView。layout(layoutChildViewCurX,layoutChildViewCurY,layoutChildViewCurXwidth,layoutChildViewCurYfixedHeight);if(rowcurRow){同一行layoutChildViewCurXwidthhorizontalS}else{换行了layoutChildViewCurXchildView。getPaddingLeft();layoutChildViewCurYfixedHeightverticalS}赋值curCcurR}}OverridepublicintgetLayoutDirection(){returnDIRECTIONHORIZONTAL;}}复制代码
  对于水平的布局方式来说,高度是固定的,我们很容易的就能计算出来,但是宽度每一行的可能都不一样,我们用一个List记录每一行的总宽度,在最后设置的时候取出最大的一行作为容器的宽度,记得要减去一个间距哦。
  那么不同宽度的水平布局方式效果的实现就是这样:
  实现是实现了,但是这么计算是不是有问题?每一行的最高高度好像不是太准确,如果每一列都有一个最大高度,但是不是同一列,那么测量的高度就比实际高度要更高。
  加一个灰色背景就可以看到效果:
  我们再优化一下,它应该是计算每一列的总共高度,然后选出最大高度才对:Overridepublicint〔〕performMeasure(ViewGroupviewGroup,introwCount,inthorizontalSpacing,intverticalSpacing,intfixedWidth){intchildCountviewGroup。getChildCount();intcurPosition0;inttotalControlHeight0;inttotalControlWidth0;SparseArrayListIntegercolumnAllHeightnewSparseArray();每一列的全部高度开始遍历for(inti0;ichildCi){ViewchildViewviewGroup。getChildAt(i);introwcurPositionrowC当前子View是第几行intcolumncurPositionrowC当前子View是第几列已经测量过了,直接取宽高intheightchildView。getMeasuredHeight();ListIntegerintegerscolumnAllHeight。get(column);if(integersnullintegers。isEmpty()){integersnewArrayList();}integers。add(heightverticalSpacing);columnAllHeight。put(column,integers);赋值curP}循环结束之后开始计算真正的宽高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();columnAllHreturnnewint〔〕{totalControlWidthhorizontalSpacing,totalControlHeightverticalSpacing};}复制代码
  再看看效果:
  宽高真正的测量准确之后我们接下来就开始属性的抽取与封装了。四、自定义属性
  我们先前都是使用的成员变量来控制一些间距与逻辑的触发,这就跟业务耦合了,如果想做到通用的一个效果,肯定还是要抽取自定义属性,做到对应的配置开关,就可以适应更多的场景使用,也是开源项目的必备技能。
  细数一下我们需要控制的属性:enableScale是否支持缩放maxScale缩放的最大比例minScale缩放的最小比例moveInViewport是否只能在布局内部移动horizontalSpacingitem的水平间距verticalSpacingitem的垂直间距fixedwidth竖向的排列宽度定死并设置对应的LayoutManagerfixedheight横向的排列高度定死并设置对应的LayoutManager
  定义属性如下:!全屏幕布布局自定义属性declarestyleablenameCurtainLayout!Item的横向间距!Item的垂直间距!每行需要展示多少数量的Item!垂直方向瀑布流布局,固定宽度为多少!水平方向瀑布流布局,固定高度为多少!是否只能在布局内部移动当为false时候为自由移动!是否可以缩放!最大与最小的缩放比例declarestyleable复制代码
  取出属性并对容器布局与触摸处理器做赋值的操作:publicclassCurtainLayoutextendsFrameLayout{privateinthorizontalSprivateintverticalSprivateintrowCprivateintfixedWprivateintfixedHprivatebooleanmoveInVprivatebooleanenableSprivatefloatmaxSprivatefloatminSpublicCurtainLayout(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。moveInViewportmoveInVmGestureHandler。setKeepInViewport(moveInViewport);}publicvoidsetEnableScale(booleanenableScale){this。enableScaleenableSmGestureHandler。setEnableScale(enableScale);}publicvoidsetMinScale(floatminScale){this。minScaleminSmGestureHandler。setMinScale(minScale);}publicvoidsetMaxScale(floatmaxScale){this。maxScalemaxSmGestureHandler。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,这一期就此完结。
投诉 评论

再见湖人,再见戴维斯!美媒拟詹姆斯7换2交易,利拉德迎冲冠机前言:再见湖人,再见戴维斯!美媒拟詹姆斯7换2交易,利拉德迎冲冠机会!如果说詹姆斯被湖人交易的话,球迷朋友会感到意外吗?于咱看来,这非常的震惊,湖人为何会接受其他球队将詹……减肥清肠胃吃什么东西比较好现在很多人在追求健康的同时也在追求一个完美的身材,但是很多人由于肠胃不好因此导致一些身体疾病,而且吃下去的东西也不能被消化,因此导致身体发福。那么到底有哪些东西可以帮助我们减肥……Android自定义ViewGroup嵌套与交互实战,幕布全自定义ViewGroup全屏选中效果前言事情是这个样子的,前几天产品丢给我一个视频,你觉得这个效果怎么样?我们的App也做一个这个效果吧!我当时的反应:开什么……云阳龙缸游记辛丑维夏,既望之日,数新友相携,游于云阳之龙缸。龙缸者,渝东北之胜迹也,予未至之时,但见云阳各处以此为名而誉之,私揣此或为一大潭,水府幽冥,或伏龙焉,缘何名之以缸,实为不解。……早生大白菜育苗的栽培技术室内育苗。全生育期需要90至100天,其中育苗期30天。在育苗期所需温度在15至28之间,2月上旬育苗,育苗需备地热加温设施。育苗器具可用低筒或塑料钵,也可划营养方。播种间距保……搭建公司云盘,打造NAS多媒体中心4盘位的铁威马F4423够因为公司发展,原来的双盘位NAS开始有些许吃力了。虽然日常下载文件没问题,但系统就是变得很卡,而且公司也有了多人在线办公,同步文档资料的需求。然后也不知领导去哪里视察回来,意思……发朋友圈喝咖啡的句子发朋友圈喝咖啡的句子有哪些?一个女人喝咖啡想要发个朋友圈哪种心情说说更合适呢?一起去看看吧。一、发朋友圈喝咖啡的句子1。咖啡香味在空气中回旋,令我想念那逝去的情缘。……烈烈造句用烈烈造句大全211、你陪了我多少年,穿林打叶,过程轰轰烈烈。花开花落,一路上起起跌跌。江南212、空前绝后女武皇,把持朝政韦皇后。惊世爱恨属高阳,旷世女相有上官。更有那一生传奇是太平……迪丽热巴挺惨!一袭碎花绿裙靓丽出场,结果不当动作引来嘲讽说起娱乐圈的性感女星,那迪丽热巴不得不提,她出道时间不算长,但凭借出色的美貌又加上适合的角色,热度瞬间暴涨。如今已经是当之无愧的一线小花了,资源不断。除了影视资源,热巴的……她与邓亚萍乔红做过队友,后与丈夫入日籍,儿子成国乒最强对手还记得1995年的天津世乒赛吗?那届世乒赛,有一位叫张凌的女子,她与邓亚萍、乔红等人一起参加世乒赛,对于邓亚萍和乔红两人的名字,了解中国乒乓球的人基本上都知道,那是如雷贯耳的国……宝藏城市郴州,有谁还没来过?走遍五大洲,最美有郴州。2022年十一黄金周到了,宝藏城市郴州谁还没来过?郴州,山水秀丽。莽山国家森林公园里,可以赏林海、云海。国庆期间,景区可安排车辆从游客所住酒店至景……走遍世界相机为伴如果旅行独自环绕世界旅行,你只能带一样东西供自己娱乐,你会怎样选择呢?一部相机?一本故事书?一支口琴,还是一台笔记本电脑?这的确让人很难作出选择。但是如果你问我,我会毫不……
不能只靠孙颖莎!日乒打造双保险压制国乒,刘诗雯式悲剧要重演日乒未雨绸缪刘国梁要学习学习!在结束了萨格勒布站的比赛后,国乒的17名球员又开始前往斯洛文尼亚奥托赛克参加下一站的比赛,而在这站的比赛中,国乒17名球员将会参加全部5个项目的比……脑机接口在物联网领域的应用最近国外学者开发一套轻量级的EEG采集系统和信号处理系统,并在物联网领域进行了探索。该系统包括8个采集电极(可根据实际情况进行拓展)和1个参考电极,放大器核心采用的是INA33……细数那些曾经辉煌现在无球可打的现役球星!谁让你觉得最为遗憾?卡梅隆安东尼:年龄38岁,生涯到现在为止一共征战了19个赛季,拿到了28289分,7808个篮板,3422次助攻,10次入选全明星,6次最佳阵容,NBA历史75大巨星之一,没有……男人过了75岁,只要做好这些事情,一般都会长寿前言:75岁已经不再年轻,但是也没有完全进入老年状态,我们活在这个世界上,只要心态好的话一切都好,如果心态不好,可以说已经没得救了,在这个世界上很多事情,我们都必须认真的……故宫600年1723年,养心殿成为内廷最重要的宫殿养心殿,是留存下来的唯一的皇帝寝宫。所以,在它开放的日子里,这里都是最吸引人的景点。它闭馆大修也有好几年了。养心殿建于明嘉靖年间,最初就是西路一处普通的宫苑。康熙年间,作……针对南京医生掌掴幼童的几点疑问1。我看了很多评论说,是被打的儿童拿尖锐物体数次猛戳医生孩子的后脑,导致医生孩子后脑破了一个大口子。问题1:尖锐物是什么?问题2:打起来的原因是什么?问题3:……短道速滑世界杯多德雷赫特站首日赛况2月11日,20222023赛季国际滑联短道速滑世界杯荷兰多德雷赫特站比赛开赛。加拿大选手布廷(右)在女子1000米第一次比赛A组决赛中,她以1分29秒807的成绩夺得冠……转会动态广州队新星留洋葡萄牙,海港名帅或3选1,两大归化回归前言北京时间2月9日,笔者为国内球迷汇总了几条关于中超球队转会的消息,其中涉及到2022赛季降级的广州队,经济实力雄厚的上海海港,以及北京国安和成都蓉城,还有两位归化球员……华为MateXs2开售!京东下单享30天无忧退365天只换不5月6日,华为新一代折叠旗舰MateXs2正式开售。华为MateXs2聚焦折叠屏手机的使用场景与用户痛点,以超轻薄、超平整、超可靠的产品特性为消费者带来惊艳折叠大屏体验,一经发……华府神经刀获费城兴趣合同年赶紧打回身价吧76人真不是个好去处虽然只是一名次轮秀,但巴顿在从开拓者转投掘金后,确实是将自己的进攻天赋展现地淋漓尽致,巅峰时期他曾有过单赛季场均15的输出,不过在后面掘金人员更加充沛后,巴顿这样偶尔抽风的神经……实锤,白喜林空降足协,孙雯高洪波完败在杜兆才和陈戌源双双被抓之后,足协目前是群龙无首,虽然高洪波和孙雯都是名义上的副主席,但是两人在足协并没有实权,面对错综复杂的人事关系网,怕是也捋不清头绪,最终两人都无缘执掌足……玩辅助太没安全感怎么办?近卫项羽全新黑科技,血量直逼一万七大家好,我是名侦探15号。今天我们来聊一点黑科技的辅助英雄打法。这个打法在很多个赛季之前,经常被认为是坑强行补位放飞自我没什么用。但现在和以前不一样了,现在是……
友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找