这交互炸了系列:仿小米音乐歌手详情页,自定义 Behavior实战

作者:彭冠铭
链接:
完全是使用嵌套滚动机制实现的 , 当时就有很多留言说CoordinatorLayout也能实现 , 确实 , 这不文章就来了 。
作者这个系列一共4篇 , 2篇基础 , 2篇实战 , 如果你能完全吸收 , 基本玩转嵌套滚动 。
PS:感谢大家 , 昨天很给力 , 让我褥了好几年...
1、概述之前的《浅析NestedScrolling嵌套滑动机制之CoordinatorLayout.Behavior》带大家了解CoordinatorLayout.Behavior的原理和基本使用 , 这篇文章手把手基于自定义Behavior实现小米音乐歌手详情页 。
效果预览 下面来说明上图中变量的意义:
topBarHeight;//topBar高度contentTransY;//滑动内容初始化TransYdownEndY;//content下滑的最大值content部分的上滑范围=[topBarHeight,contentTransY]content部分的下滑范围=[contentTransY,downEndY]2、代码实现布局
下面是布局要点 , 侧重于控件的尺寸和位置 , 完整布局请参考:
<android.support.design.widget.CoordinatorLayoutandroid:layout_width="match_parent"android:layout_height="match_parent">mimusicbehavior.widget.DrawableLeftTextView.../>ContentBehavior这个Behavior主要处理Content部分的Measure、嵌套滑动 。
绑定需要做效果的View、引入Dimens、测量Content部分的高度从上面图片能够分析出:
折叠状态时 , Content部分高度=满屏高度-TopBar部分的高度
publicclassContentBehaviorextendsCoordinatorLayout.Behavior{privateinttopBarHeight;//topBar内容高度privatefloatcontentTransY;//滑动内容初始化TransYprivatefloatdownEndY;//下滑时终点值privateViewmLlContent;//Content部分publicContentBehavior(Contextcontext){this(context,null);}publicContentBehavior(Contextcontext,AttributeSetattrs){super(context,attrs);//引入尺寸值intresourceId=context.getResources().getIdentifier("status_bar_height","dimen","android");intstatusBarHeight=context.getResources().getDimensionPixelSize(resourceId);topBarHeight=(int)context.getResources().getDimension(R.dimen.top_bar_height)+statusBarHeight;contentTransY=(int)context.getResources().getDimension(R.dimen.content_trans_y);downEndY=(int)context.getResources().getDimension(R.dimen.content_trans_down_end_y);...}@OverridepublicbooleanonMeasureChild(@NonNullCoordinatorLayoutparent,Viewchild,intparentWidthMeasureSpec,intwidthUsed,intparentHeightMeasureSpec,intheightUsed){finalintchildLpHeight=child.getLayoutParams().height;if(childLpHeight==ViewGroup.LayoutParams.MATCH_PARENT||childLpHeight==ViewGroup.LayoutParams.WRAP_CONTENT){//先获取CoordinatorLayout的测量规格信息 , 若不指定具体高度则使用CoordinatorLayout的高度intavailableHeight=View.MeasureSpec.getSize(parentHeightMeasureSpec);if(availableHeight==0){availableHeight=parent.getHeight();}//设置Content部分高度finalintheight=availableHeight-topBarHeight;finalintheightMeasureSpec=View.MeasureSpec.makeMeasureSpec(height,childLpHeight==ViewGroup.LayoutParams.MATCH_PARENT?View.MeasureSpec.EXACTLY:View.MeasureSpec.AT_MOST);//执行指定高度的测量 , 并返回true表示使用Behavior来代理测量子Viewparent.onMeasureChild(child,parentWidthMeasureSpec,widthUsed,heightMeasureSpec,heightUsed);returntrue;}returnfalse;}@OverridepublicbooleanonLayoutChild(@NonNullCoordinatorLayoutparent,@NonNullViewchild,intlayoutDirection){booleanhandleLayout=super.onLayoutChild(parent,child,layoutDirection);//绑定ContentViewmLlContent=child;returnhandleLayout;}}实现NestedScrollingParent2接口onStartNestedScroll()ContentBehavior只处理Content部分里可滑动View的垂直方向的滑动 。
publicbooleanonStartNestedScroll(@NonNullCoordinatorLayoutcoordinatorLayout,@NonNullViewchild,@NonNullViewdirectTargetChild,@NonNullViewtarget,intaxes,inttype){//只接受内容View的垂直滑动returndirectTargetChild.getId()==R.id.ll_content&&axes==ViewCompat.SCROLL_AXIS_VERTICAL;}onNestedPreScroll()接下来就是处理滑动 , 上面效果分析提过:
Content部分的:
上滑范围=[topBarHeight,contentTransY]、下滑范围=[contentTransY,downEndY]即滑动范围为[topBarHeight,downEndY];
ElemeNestedScrollLayout要控制Content部分的TransitionY值要在范围内 , 具体处理如下:
Content部分里可滑动View往上滑动时:
如果Content部分当前TransitionY+View滑动的dy>topBarHegiht , 设置Content部分的TransitionY为Content部分当前TransitionY+View滑动的dy达到移动的效果来消费View的dy 。 如果Content部分当前TransitionY+View滑动的dy=topBarHegiht , 同上操作 。 如果Content部分当前TransitionY+View滑动的dy<topBarHegiht , 只消费部分dy(即Content部分当前TransitionY到topBarHeight差值) , 剩余的dy让View滑动消费 。Content部分里可滑动View往下滑动并且View已经不能往下滑动(比如RecyclerView已经到顶部还往下滑)时:
如果Content部分当前TransitionY+View滑动的dy>=topBarHeight并且Content部分当前TransitionY+View滑动的dy<=downEndY , 设置Content部分的TransitionY为Content部分当前TransitionY+View滑动的dy达到移动的效果来消费View的dyContent部分当前TransitionY+View滑动的dy>downEndY,只消费部分dy(即Content部分当前TransitionY到downEndY差值)并停止NestedScrollingChild2的View滚动 。 publicvoidonNestedPreScroll(@NonNullCoordinatorLayoutcoordinatorLayout,@NonNullViewchild,@NonNullViewtarget,intdx,intdy,@NonNullint[]consumed,inttype){floattransY=child.getTranslationY()-dy;//处理上滑if(dy>0){if(transY>=topBarHeight){translationByConsume(child,transY,consumed,dy);}else{translationByConsume(child,topBarHeight,consumed,(child.getTranslationY()-topBarHeight));}}if(dy<0&&!target.canScrollVertically(-1)){//处理下滑if(transY>=topBarHeight&&transY<=downEndY){translationByConsume(child,transY,consumed,dy);}else{translationByConsume(child,downEndY,consumed,(downEndY-child.getTranslationY()));stopViewScroll(target);}}}privatevoidstopViewScroll(Viewtarget){if(targetinstanceofRecyclerView){((RecyclerView)target).stopScroll();}if(targetinstanceofNestedScrollView){try{ClassextendsNestedScrollView>clazz=((NestedScrollView)target).getClass();FieldmScroller=clazz.getDeclaredField("mScroller");mScroller.setAccessible(true);OverScrolleroverScroller=(OverScroller)mScroller.get(target);overScroller.abortAnimation();}catch(NoSuchFieldException|IllegalAccessExceptione){e.printStackTrace();}}}privatevoidtranslationByConsume(Viewview,floattranslationY,int[]consumed,floatconsumedDy){consumed[1]=(int)consumedDy;view.setTranslationY(translationY);}onStopNestedScroll()在下滑Content部分从初始状态转换到展开状态的过程中松手就会执行收起的动画 , 这逻辑在onStopNestedScroll()实现 , 但注意如果动画未执行完毕手指再落下滑动时 , 应该在onNestedScrollAccepted()取消当前执行中的动画 。
场景2:从初始化状态快速下滑转为展开状态 , 这也和和前面onNestedPreScroll()处理上滑的效果一模一样 , 因此可以复用逻辑 。
场景3:从折叠状态快速下滑转为初始化状态 , 这个过程如下图 , 看起来像是快速下滑停顿的效果 。
publicvoidonDetachedFromLayoutParams(){if(restoreAnimator.isStarted()){restoreAnimator.cancel();restoreAnimator.removeAllUpdateListeners();restoreAnimator.removeAllListeners();restoreAnimator=null;}super.onDetachedFromLayoutParams();}FaceBehavior
这个Behavior主要处理Face部分的ImageView的位移、蒙层的透明度变化 , 这里因为篇幅原因 , 只讲解关键方法 , 具体源码见
publicclassFaceBehaviorextendsCoordinatorLayout.Behavior{privateinttopBarHeight;//topBar内容高度privatefloatcontentTransY;//滑动内容初始化TransYprivatefloatdownEndY;//下滑时终点值privatefloatfaceTransY;//图片往上位移值publicFaceBehavior(Contextcontext,AttributeSetattrs){super(context,attrs);//引入尺寸值intresourceId=context.getResources().getIdentifier("status_bar_height","dimen","android");intstatusBarHeight=context.getResources().getDimensionPixelSize(resourceId);topBarHeight=(int)context.getResources().getDimension(R.dimen.top_bar_height)+statusBarHeight;contentTransY=(int)context.getResources().getDimension(R.dimen.content_trans_y);downEndY=(int)context.getResources().getDimension(R.dimen.content_trans_down_end_y);faceTransY=context.getResources().getDimension(R.dimen.face_trans_y);...}publicbooleanlayoutDependsOn(@NonNullCoordinatorLayoutparent,@NonNullViewchild,@NonNullViewdependency){//依赖ContentViewreturndependency.getId()==R.id.ll_content;}publicbooleanonDependentViewChanged(@NonNullCoordinatorLayoutparent,@NonNullViewchild,@NonNullViewdependency){//计算Content的上滑百分比、下滑百分比floatupPro=(contentTransY-MathUtils.clamp(dependency.getTranslationY(),topBarHeight,contentTransY))/(contentTransY-topBarHeight);floatdownPro=(downEndY-MathUtils.clamp(dependency.getTranslationY(),contentTransY,downEndY))/(downEndY-contentTransY);ImageViewiamgeview=child.findViewById(R.id.iv_face);ViewmaskView=child.findViewById(R.id.v_mask);if(dependency.getTranslationY()>=contentTransY){//根据Content上滑百分比位移图片TransitionYiamgeview.setTranslationY(downPro*faceTransY);}else{//根据Content下滑百分比位移图片TransitionYiamgeview.setTranslationY(faceTransY+4*upPro*faceTransY);}//根据Content上滑百分比设置图片和蒙层的透明度iamgeview.setAlpha(1-upPro);maskView.setAlpha(upPro);//因为改变了child的位置 , 所以返回truereturntrue;}}其实从上面代码也可以看出逻辑非常简单 , 在layoutDependsOn()依赖Content , 在onDependentViewChanged()里计算Content的上、下滑动百分比来处理图片和蒙层的位移、透明变化 。
TopBarBehavior这个Behavior主要处理TopBar部分的两个子View的透明度变化 ,
因为逻辑跟FaceBehavior十分类似就不细说了 。
publicclassTopBarBehaviorextendsCoordinatorLayout.Behavior{privatefloatcontentTransY;//滑动内容初始化TransYprivateinttopBarHeight;//topBar内容高度...publicTopBarBehavior(Contextcontext,AttributeSetattrs){super(context,attrs);//引入尺寸值contentTransY=(int)context.getResources().getDimension(R.dimen.content_trans_y);intresourceId=context.getResources().getIdentifier("status_bar_height","dimen","android");intstatusBarHeight=context.getResources().getDimensionPixelSize(resourceId);topBarHeight=(int)context.getResources().getDimension(R.dimen.top_bar_height)+statusBarHeight;}publicbooleanlayoutDependsOn(@NonNullCoordinatorLayoutparent,@NonNullViewchild,@NonNullViewdependency){//依赖Contentreturndependency.getId()==R.id.ll_content;}publicbooleanonDependentViewChanged(@NonNullCoordinatorLayoutparent,@NonNullViewchild,@NonNullViewdependency){//计算Content上滑的百分比 , 设置子view的透明度floatupPro=(contentTransY-MathUtils.clamp(dependency.getTranslationY(),topBarHeight,contentTransY))/(contentTransY-topBarHeight);ViewtvName=child.findViewById(R.id.tv_top_bar_name);ViewtvColl=child.findViewById(R.id.tv_top_bar_coll);tvName.setAlpha(upPro);tvColl.setAlpha(upPro);returntrue;}}TitleBarBehavior这个Behavior主要处理TitleBar部分在布局位置紧贴Content顶部和关联的View的透明度变化 。
publicclassTitleBarBehaviorextendsCoordinatorLayout.Behavior{privatefloatcontentTransY;//滑动内容初始化TransYprivateinttopBarHeight;//topBar内容高度publicTitleBarBehavior(Contextcontext,AttributeSetattrs){super(context,attrs);//引入尺寸值contentTransY=(int)context.getResources().getDimension(R.dimen.content_trans_y);intresourceId=context.getResources().getIdentifier("status_bar_height","dimen","android");intstatusBarHeight=context.getResources().getDimensionPixelSize(resourceId);topBarHeight=(int)context.getResources().getDimension(R.dimen.top_bar_height)+statusBarHeight;}publicbooleanlayoutDependsOn(@NonNullCoordinatorLayoutparent,@NonNullViewchild,@NonNullViewdependency){//依赖contentreturndependency.getId()==R.id.ll_content;}publicbooleanonDependentViewChanged(@NonNullCoordinatorLayoutparent,@NonNullViewchild,@NonNullViewdependency){//调整TitleBar布局位置紧贴Content顶部adjustPosition(parent,child,dependency);//这里只计算Content上滑范围一半的百分比floatstart=(contentTransY+topBarHeight)/2;floatupPro=(contentTransY-MathUtils.clamp(dependency.getTranslationY(),start,contentTransY))/(contentTransY-start);child.setAlpha(1-upPro);returntrue;}publicbooleanonLayoutChild(@NonNullCoordinatorLayoutparent,@NonNullViewchild,intlayoutDirection){//找到Content的依赖引用Listdependencies=parent.getDependencies(child);Viewdependency=null;for(Viewview:dependencies){if(view.getId()==R.id.ll_content){dependency=view;break;}}if(dependency!=null){//调整TitleBar布局位置紧贴Content顶部adjustPosition(parent,child,dependency);returntrue;}else{returnfalse;}}privatevoidadjustPosition(@NonNullCoordinatorLayoutparent,@NonNullViewchild,Viewdependency){finalCoordinatorLayout.LayoutParamslp=(CoordinatorLayout.LayoutParams)child.getLayoutParams();intleft=parent.getPaddingLeft()+lp.leftMargin;inttop=(int)(dependency.getY()-child.getMeasuredHeight()+lp.topMargin);intright=child.getMeasuredWidth()+left-parent.getPaddingRight()-lp.rightMargin;intbottom=(int)(dependency.getY()-lp.bottomMargin);child.layout(left,top,right,bottom);}}总结自定义Behavior可以实现各种神奇的效果 , 相对于自定义View实现NestedScrolling机制 , Behavior更能解耦逻辑 , 但同时又多了些约束 , 由于本人水平有限仅给各位提供参考 , 希望能够抛砖引玉 , 如果有什么可以讨论的问题可以在评论区留言或联系本人 。
实战系列话不多说 , Android实战系列集合 , 都已经系统分类好 , 由于文章篇幅私信我【666】查看详细文章以及获取学习笔记链接


    推荐阅读