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

玩转仿探探的卡片式滑动效果

时间:2023-03-20 00:41:39 科技观察

说起这篇博客和这篇博客的历史渊源,估计有一段“历史”。估计有一段“历史”。这要追溯到我试玩探探APP。第一次进入软件界面,就被滑动卡片选择“喜欢/不喜欢”的设计所吸引。当时很想自己实现这种仿探探的效果,但是没有什么想法。但是毫无疑问,这个效果的原理肯定和ListView/RecyclerView类似,涉及到ItemView的回收再利用,否则就会因为大量的ItemView而导致OOM。后来看到很多大神也推出了同样仿探探效果的博客。从头到尾看完,写的通俗易懂,基本没有问题。于是,实现仿探探效果的想法又重新出现在我的脑海中。所以,还犹豫什么,趁热出发吧!这是一个多么幸福的决定。我们面临的第一个问题是实现View的考虑。毫无疑问。RecyclerView是最好的选择!RecyclerView是最好的选择!RecyclerView是最好的选择!重要的事情说三遍!!!原因是,***,RecyclerView自带ItemView回收再利用的功能,只是我们不需要考虑这个问题;其次,通过设置LayoutManager实现RecyclerView的布局,让布局和RecyclerView完全“解耦”。并且LayoutManager可以以自定义的方式实现。这正是我们想要的!!!此外,这也是不选择ListView的原因之一。接下来,让我们开始吧。带你见证奇迹的时刻。CardLayoutManager创建CardLayoutManager并继承自RecyclerView.LayoutManager。我们需要自己实现generateDefaultLayoutParams()方法:@OverridepublicRecyclerView.LayoutParamsgenerateDefaultLayoutParams(){returnnewRecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);}一般情况下,如上写即可。下面的方法是我们的??重点。onLayoutChildren(finalRecyclerView.Recyclerrecycler,RecyclerView.Statestate)方法用于实现ItemView布局:@OverridepublicvoidonLayoutChildren(finalRecyclerView.Recyclerrecycler,RecyclerView.Statestate){super.onLayoutChildren(recycler,state);//清除所有viewremoveAllViews();//在布局之前,detach所有的子View,放入Scrap缓存中detachAndScrapAttachedViews(recycler);intitemCount=getItemCount();//这里我们默认配置CardConfig.DEFAULT_SHOW_ITEM=3。即屏幕显示的卡片数量为3//当数据源数量大于显示数量时if(itemCount>CardConfig.DEFAULT_SHOW_ITEM){//将数据源倒序循环,使第0条数据为inthetopofthescreenisfor(intposition=CardConfig.DEFAULT_SHOW_ITEM;position>=0;position--){finalViewview=recycler.getViewForPosition(position);//添加ItemView到RecyclerViewaddView(view);//测量ItemViewmeasureChildWithMargins(view,0,0);//getDecoratedMeasuredWidth(view)可以得到ItemView的宽度//所以widthSpace是除了ItemView的剩余值intwidthSpace=getWidth()-getDecoratedMeasuredWidth(view);//同理在theightSpace=getHeight()-getDecoratedMeasuredHeight(view);//将ItemView放入RecyclerView布局//这里默认布局是放在RecyclerView的中心layoutDecoratedWithMargins(view,widthSpace/2,heightSpace/2,widthSpace/2+getDecoratedMeasuredWidth(view),heightSpace/2+getDecoratedMeasuredHeight(view));//其实t这里屏幕上有四张卡片,但是我们把第三张和第四张卡片重叠,所以只有三张。//第四张卡片主要是为了保持动画的连续性if(position==CardConfig.DEFAULT_SHOW_ITEM){//Scale按照一定的规则偏移Y轴。//CardConfig.DEFAULT_SCALE默认为0.1f,CardConfig.DEFAULT_TRANSLATE_Y默认为14view.setScaleX(1-(position-1)*CardConfig.DEFAULT_SCALE);view.setScaleY(1-(position-1)*CardConfig.DEFAULT_SCALE);view.setTranslationY((position-1)*view.getMeasuredHeight()/CardConfig.DEFAULT_TRANSLATE_Y);}elseif(position>0){view.setScaleX(1-position*CardConfig.DEFAULT_SCALE);view.setScaleY(1-position*CardConfig.DEFAULT_SCALE);view.setTranslationY(position*view.getMeasuredHeight()/CardConfig.DEFAULT_TRANSLATE_Y);}else{//设置mTouchListener的意义在于我们希望最上面的卡片能够随意滑动//和第二layer,第三层等是禁止滑动的卡片view.setOnTouchListener(mOnTouchListener);}}}else{//当数据源个数小于或等于显示的个数时,与上述代码类似for(intposition=itemCount-1;position>=0;position--){finalViewview=recycler.getViewForPosition(position);addView(view);measureChildWithMargins(view,0,0);intwidthSpace=getWidth()-getDecoratedMeasuredWidth(view);intheightSpace=getHeight()-getDecoratedMeasuredHeight(view);layoutDecoratedWithMargins(view,widthSpace/2,heightSpace/2,widthSpace/2+getDecoratedMeasuredWidth(view),heightSpace/2+getDecoratedMeasuredHeight(view));如果(position>0){view.setScaleX(1-position*CardConfig.DEFAULT_SCALE);view.setScaleY(1-position*CardConfig.DEFAULT_SCALE);view.setTranslationY(position*view.getMeasuredHeight()/CardConfig.DEFAULT_TRANSLATE_Y);}else{view.setOnTouchListener(mOnTouchListener);}}}}privateView.OnTouchListenermOnTouchListener=newView.OnTouchListener(){@Overridepublic(booleanViewv,MotionEventevent){RecyclerView.ViewHolderchildViewHolder=mRecyclerView.getChildViewHolder(v);//将触摸事件交给mItemTouchHelper处理卡片滑动事件if(MotionEventCompat.getActionMasked(event)==MotionEvent.ACTION_DOWN){mItemTouchHelper.startSwipe(childViewHolder);}returnfalse;}};一般来说,CardLayoutManager主要是对ItemView进行布局,然后根据位置做相应的偏移。缺少的是处理触摸和滑动事件。OnSwipeListener在看滑动事件的代码之前,我们先定义一个监听器。主要用于监听卡片滑动事件,代码如下,同时给出注释。你应该能理解:publicinterfaceOnSwipeListener{/***卡片还在滑动时的回调**@paramviewHolder滑动卡片的viewHolder*@paramratio滑动进度的比例*@paramdirection卡片滑动的方向,CardConfig.SWIPING_LEFT是向左滑动,CardConfig.SWIPING_RIGHT表示向右滑动,*CardConfig.SWIPING_NONE表示既不向左也不向右*/voidonSwiping(RecyclerView.ViewHolderviewHolder,floatratio,intdirection);/***回调时卡片完全滑出**@paramviewHolder滑出卡片的viewHolder*@paramt滑出卡片的数据*@paramdirection卡片滑出的方向,CardConfig.SWIPED_LEFT为滑出卡片上左边;CardConfig.SWIPED_RIGHT是右边的滑出*/voidonSwiped(RecyclerView.ViewHolderviewHolder,Tt,intdirection);/***所有卡片滑出时的回调*/voidonSwipedClear();}CardItemTouchHelperCallback现在,我们可以回去了看到刷卡。你必须熟悉ItemTouchHelper来处理ItemView的触摸和滑动事件!我们暂时将其命名为CardItemTouchHelperCallback。对于ItemTouchHelper.Callback,需要在getMovementFlags(RecyclerViewrecyclerView,RecyclerView.ViewHolderviewHolder)方法中配置swipeFlags和dragFlags。具体的方法如下,对于swipeFlags只关心左右两个方向:@OverridepublicintgetMovementFlags(RecyclerViewrecyclerView,RecyclerView.ViewHolderviewHolder){intdragFlags=0;intswipeFlags=0;RecyclerView.LayoutManagerlayoutManager=recyclerView.getLayoutManager();if(layoutManagerinstanceofCardLayoutManager){swipeFlags=ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT;}returnmakeMovementFlags(dragFlags,swipeFlags);}还有一件事需要注意。前面提到,为了防止第二层和第三层卡片滑动,我们需要设置isItemViewSwipeEnabled()返回false。@OverridepublicbooleanisItemViewSwipeEnabled(){returnfalse;}下一步是重写onMove(RecyclerViewrecyclerView,RecyclerView.ViewHolderviewHolder,RecyclerView.ViewHoldertarget)和onSwiped(RecyclerView.ViewHolderviewHolder,intdirection)方法。但是因为我们上面配置了dragFlags为0,所以我们可以在onMove(RecyclerViewrecyclerView,RecyclerView.ViewHolderviewHolder,RecyclerView.ViewHoldertarget)中直接返回false。@OverridepublicbooleanonMove(RecyclerViewrecyclerView,RecyclerView.ViewHolderviewHolder,RecyclerView.ViewHoldertarget){returnfalse;}这样我们就把目光投向了onSwiped(RecyclerView.ViewHolderviewHolder,intdirection)方法:@OverridepublicvoidonSwiped(RecyclerView.ViewHolderviewHolderremove){/intonTouchListenersetOnTouchListener(null);//删除对应的数据intlayoutPosition=viewHolder.getLayoutPosition();Tremove=dataList.remove(layoutPosition);adapter.notifyDataSetChanged();//卡片滑出后回调OnSwipeListener监听if(mListener!=null){mListener.onSwiped(viewHolder,remove,direction==ItemTouchHelper.LEFT?CardConfig.SWIPED_LEFT:CardConfig.SWIPED_RIGHT);}//出现时回调是没有数据OnSwipeListenerlistenerif(adapter.getItemCount()==0){if(mListener!=null){mListener.onSwipedClear();}}}写完了再来看看滑动效果:还是缺东西,没错!动画不见了。在滑动过程中,我们可以重写onChildDraw(Canvasc,RecyclerViewrecyclerView,RecyclerView.ViewHolderviewHolder,floatdX,floatdY,intactionState,booleanisCurrentlyActive)方法添加动画:@OverridepublicvoidonChildDraw(Canvasc,RecyclerViewrecyclerView,RecyclerView.ViewHolderviewHolder,floatdX,floatdY,intactionState,booleanisCurrentlyActive){super.onChildDraw(c,recyclerView,viewHolder,dX,dY,actionState,isCurrentlyActive);viewitemView=viewHolder.itemView;if(actionState==ItemTouchHelper.ACTION_STATE_SWIPE){//获取滑阀值floatratio=dX/getThreshold(recyclerView,viewHolder);//ratio***为1或-1if(ratio>1){ratio=1;}elseif(ratio<-1){ratio=-1;}//默认最大旋转角度为15度itemView.setRotation(ratio*CardConfig.DEFAULT_ROTATE_DEGREE);intchildCount=recyclerView.getChildCount();//当数据个数sources大于最大显示数/和之前的onLayoutChildren一样这意味着,但是要执行相反的动画view.setScaleX(1-index*CardConfig.DEFAULT_SCALE+Math.abs(ratio)*CardConfig.DEFAULT_SCALE);view.setScaleY(1-index*CardConfig.DEFAULT_SCALE+Math.abs(ratio)*CardConfig.DEFAULT_SCALE);view.setTranslationY((index-Math.abs(ratio))*itemView.getMeasuredHeight()/CardConfig.DEFAULT_TRANSLATE_Y);}}else{//当数据源个数小于等于时to***显示数字时for(intposition=0;position