专栏电商日志财经减肥爱情
投稿投诉
爱情常识
搭配分娩
减肥两性
孕期塑形
财经教案
论文美文
日志体育
养生学堂
电商科学
头戴业界
专栏星座
用品音乐

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

  自定义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,这一期就此完结。

2022年甘肃科技工作取得新进步呈现新气象2023年全省科技工作会议现场中国甘肃网2月8日讯(本网记者张慧雅)2022年,我们坚持走特色化差异化的强科技之路,集聚有限资源,凝练关键任务,统筹推进重点创新平台建设重大科技项目桌面APU首次SoC化尝试,AMDAM1平台捡垃圾AMD在APU时代就逐渐开始推广SoC设计,移动端从2013年的Kabini系列开始就可以抛弃南桥,进一步简化主板设计。而对应的桌面平台,其实也有一个对应的,那就是本次捡垃圾的主角石墨烯作点金石,电子垃圾竟能变黄金?纵观历史,炼金术士们一直痴迷于可以把廉价物转变成高价黄金的点金石。而如今,来自英国曼彻斯特大学清华大学和中国科学院的科学家们已经证明,石墨烯就可以作为一种点金石,从只含有十亿分之几2023年奶奶衫又火了,推荐中年妈妈买这4款,配裙配裤太洋气随着冬季冷意的退却,春天不远了,对于爱美的中年妈妈们,咱们是时候将春季单品准备起来了。提到春季单品,像风衣西服奶奶衫都不能不提,这次我们就来看看温柔舒适的奶奶衫,该买哪些款式以及具这是我见过最体面的65岁奶奶白发微卷,仪态端庄,真正优雅老去很多人都觉得,女人到了60多岁的年龄阶段,就应该认真服老,好好的过自己的退休生活就可以了,就不要再折腾自己了,可是对于真正爱美的女性来说,精致感早就已经刻在了骨子里,60多岁照样可以太空美学闻名全球的时装设计大师去世,享年88岁曾在上世纪六七十年代以太空美学闻名全球的时装设计大师PacoRabanne在法国Portsall逝世,享年88岁。图片来源pacorabanne官方Instagram或许大家对Pa卤肥肠香味浓郁秘诀,全靠这锅卤水,三分卤五分洗是关键卤肥肠大家都不会陌生,小到街边小吃大到豪华宾馆都有它们的身影。肥肠有各种各样的做法,如红烧脆皮麻辣虽然做法不同,但每种口味都深受广大消费者的喜爱。俗话说得好肥肠好吃腥难除要想做好一铿锵玫瑰丨刘俊宏投资界的铁娘子在人们的印象中创投圈是男人的天下,然而深圳东方赛富投资有限公司董事长刘俊宏女士则让有这种印象的人大跌眼镜。举止温婉,经常面带微笑,把红色当作自己标志的刘俊宏,在投资圈中早就是尽人皆肾虚的人有哪些特征1肾虚的人有哪些特征?肾虚主要分为肾阴虚肾阳虚肾精亏虚三种。1。肾阴虚,主要会表现为腰膝酸软,两腿无力,心烦口渴,身体消瘦,耳鸣,失眠盗汗,男性患者会出现阳痿或者是阳强不倒,而女性早餐不可忽略一日三餐中,早餐是非常重要的,然而有很多人却恰恰省略了早餐。延年益寿的要素之一就是要每天坚持用早餐。在生活方式上应把早餐放到重要位置。如不吃早餐,易造成精神不振。人体所需要的能量,控制超敏C反应蛋白,必须限酒今天跟超敏C反应蛋白高而且还爱喝酒的人,说一说,你必须要控制酒精的摄入!为什么呢?第一,喝酒的人他的体型普遍都有这样一个特点,就是肚子大。腹部脂肪本身就是炎症发源地,它会往血管内持
台湾农产品被澳门按下暂停键,台当局的言应堪称奇葩不思悔改,一味甩锅,台农渔产品还要遭遇更多更大的打击。据台海网7月3日消息,澳门特区政府此前一天晚发布消息称,1日从中国台湾地区输入的芒果外包装验出新冠病毒核酸检测阳性,这是3天内神评天上飞机最快,地下眼镜最坏!哈哈每日更新,喜欢就关注我吧两个我都要了泪奔泪奔泪奔泪奔泪奔记得下月还我真是贵人啊不错不错机智机智机智机智机智机智社恐哈哈尬笑尬笑尬笑尬笑尬笑尬笑挺值得赞赞赞赞赞赞有道理哈哈可以辞职了家有长寿老人,对子女是福还是祸?69岁大妈说得难听,却有道理导语随着生活条件的改善,生活水平的提高,老人的平均寿命比过去长了很多,很多老人都希望自己能活得更久一点,过去常听到的一句话家有一老,如有一宝。也是儿女的福气。如果老人身体好,可以这乾隆88岁时嘉庆进献一13岁少女,乾隆笑而纳之,半年后她便守寡了船隐芦洲不见人,四弦风送到江滨。主宾僮仆齐倾耳,写出寻声暗问神。乾隆如果说现代女性最好的归宿是考公务员,过朝九晚五的生活,那么古代女性最好的归宿就是成为皇帝的妃子,一旦成功,不仅可美五名议员窜访台湾,解放军战机今天上午七度迫台警告美国五名参众议员继佩洛西之后窜访台湾,明摆着就是挑衅,解放军东部战区在台岛周边海空域组织强有力的军事反制行动,就是要立规矩,中国内政问题由中国说了算。资料图据台媒报道,14日晚间,西班牙水库大旱之后惊现巨石阵,比英国巨石阵早2000年因遭遇60年来最严重的干旱,位于西班牙中部的Valdecaas水库水体降至平时的四分之一,由150块石头组成的史前巨石阵从水库深处浮出,这是它被发现以来的第五次出现。据悉,这个石阵网传贾浅浅高考250分入211高校,博士未毕业入职副教授,孰真孰假贾平凹毕业于西北大学中文系,中国作家协会散文委员会主任,人生履历颇为丰富,但是在现今网络上,也要被冠上贾浅浅父亲的大名,才让许多网友想起,原来是他的女儿!所以说贾浅浅的大名最近可是那些旧玩具,是童年和故乡的见证者故乡是作家写作的一粒种子,也是种子最初的土壤。鲁迅在北京和厦门的时候写下朝花夕拾难忘百草园里的短短泥墙根的趣味,萧红也会在四十年代到香港后写呼兰河传回忆祖父的园子。作者立山1979捕鼠记前些日子,妈妈叫我到储藏室里拿一个箱子来装东西。我看到一个大箱子,便伸手去拿,却摸到了一个毛茸茸的东西,一看,妈呀!老鼠!我这一叫,全家人都赶来了,可是老鼠早已逃之夭夭。又一天,我大思政课总书记考察过的三涧溪村,这样描绘乡村治理新画卷2018年6月14日,习近平总书记在济南市章丘区双山街道三涧溪村考察时指出,乡村振兴,人才是关键。要积极培养本土人才,鼓励外出能人返乡创业,鼓励大学生村官扎根基层,为乡村振兴提供人河中医一附院举办第五届中国医师节表彰大会暨文艺汇演通讯员魏佳琳岐黄志笃担使命,薪火相传续征程。2022年8月18日,在第五个中国医师节即将来临之际,河南中医药大学第一附属医院以喜迎二十大奋进新征程为主题,举办医师节表彰大会暨文艺汇
友情链接:快好找快生活快百科快传网中准网文好找聚热点快软网