前言
之前在Github上看见了一个卡片式滑动的效果,非常的炫酷,当时就想着怎么去实现,刚开始我的构思是自定义一个ViewGroup,但通过自定义ViewGroup实现起来会非常复杂,要对子View位置进行摆放、重写onTouchEvent结合Scroller进行拖拽、对子View进行动画设置、最后还要对子View进行复用,然后我就想有没有其他的实现方式,首先考虑到了RecyclerView,因为RecyclerView.LayoutManager可以对子View位置进行设置,而且可以通过ItemTouchHelper拖拽子View,同时RecyclerView具备复用功能,所以我就尝试着通过RecyclerView来实现卡片式滑动,最终效果还不错,特此开一篇文章与大家分享实现流程。
在真正描述 自定义LayoutManager实现前我先把效果图亮出来让大家爽一波
阅读本篇文章的你需要具备的技能:RecyclerView基本使用、LayoutManager、ItemTouchHelper、RecyclerView复用机制、View基本动画,如果对这些知识点还不熟悉推荐阅读启舰大神的RecyclerView系列
1.自定义LayoutManager
首先定义一个类CardManager继承RecyclerView.LayoutManager,重写generateDefaultLayoutParams()方法和onLayoutChildren()方法,代码如下
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
int itemCount = getItemCount();
//如果RecyclerView中只有一个item或者没有,什么都不做
if(itemCount<1){
return;
}
//最底部item的角标
int bottomPosition;
if(itemCount<MAX_COUNT){
bottomPosition = 0;
}else {
bottomPosition = itemCount - MAX_COUNT;
}
//从最底层的item开始摆放
for(int i =bottomPosition;i<itemCount;i++){
//从缓冲池中获取到itemView
View view = recycler.getViewForPosition(i);
//将itemView添加到RecyclerView中
addView(view);
//测量itemView
measureChildWithMargins(view,0,0);
//recyclerView宽度-itemView宽度
int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
int heightSpace = getHeight() - getDecoratedMeasuredHeight(view);
//将itemView水平居中
layoutDecoratedWithMargins(view,widthSpace/2,heightSpace/2
,widthSpace/2+getDecoratedMeasuredWidth(view)
,heightSpace/2+getDecoratedMeasuredHeight(view));
//改变View的大小跟位置
int level = Math.abs(i-itemCount+1);
if(level>MAX_COUNT-3){
view.setScaleX(1-SCALE_RATIO*(MAX_COUNT-2));
view.setTranslationY(TRANS_RATIO*(MAX_COUNT-2));
}else if(level>0){
view.setScaleX(1-SCALE_RATIO*level);
view.setTranslationY(TRANS_RATIO*level);
}
}
}
generateDefaultLayoutParams()方法的作用是来设置子Item的LayoutParams,此处我们直接设置为自适应wrap_content。
onLayoutChildren()为自定义item位置的核心方法,首先执行detachAndScrapAttachedViews(recycler)方法,将所有的holderView从RecyclerView中剥离出来,等待重新布局。MAX_COUNT为最大View个数,这个取决于用户个人需求,本篇文章中MAX_COUNT为4,通过MAX_COUNT计算出最底部item的角标。然后通过for循环对item进行布局,for循环中内容我来分步为大家解析:
- 调用getViewForPosition(i)从RecyclerView的缓冲池中获取到itemView并通过addView()加入到RecyclerView中
- 调用measureChildWithMargins()测量itemView宽高,如不进行测量获取到的宽高为0
- 调用layoutDecoratedWithMargins()将itemView居中,参数分别为itemView、LEFT、TOP、RIGHT、BOTTOM四种边距
- 对itemView进行平移和缩放操作,注意:最底层的两个View大小和位置全部一致,因为在进行拖动的时候倒数第二个itemView会逐渐平移和缩放,所以要多添加一个看不见的itemView让底部动画看起来更加的和谐。
LayoutManager我么定义好了,将它设置到RecyclerView的setLayoutManager(manager)中,适配器和常规的定义方法相同,我们来看一下效果图:
一不小心就实现了,是不是很简单?但还不够,这样只能看到我
大威少
一人,是不能拖拽滑动的,怎么实现拖拽?聪明的同学可能已经想到,通过ItemTouchHelper,没错,就是它,我们接着往下看。
2.定义ItemTouchHelper
创建一个类,继承自ItemTouchHelper.Callback,实现其抽象方法,并重写onChildDraw()方法,代码如下:
//定义滑动方向
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = 0;
int swipeFlags = 0;
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof CardManager) {
//允许上下滑动
swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT |
ItemTouchHelper.UP | ItemTouchHelper.DOWN ;
}
return makeMovementFlags(dragFlags, swipeFlags);
}
//itemView滑出了屏幕
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
Log.i("zs","direction "+direction);
if(mListener!=null){
mListener.onSwiped(viewHolder.getAdapterPosition(),direction);
}
}
//拖动itemView时对部分itemView施加动画效果
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
float trans;
//以偏移量大的方向为标准
if(Math.abs(dX)>Math.abs(dY)){
trans = Math.abs(dX);
}else {
trans = Math.abs(dY);
}
//滑动比例
float ratio = trans/getThreshold(recyclerView,viewHolder);
if(ratio>1){
ratio = 1;
}
//获取itemView总量
int itemCount = recyclerView.getChildCount();
//移除时为底部显示的View增加动画
for (int i = 1;i<CardManager.MAX_COUNT-1;i++){
View view = recyclerView.getChildAt(i);
float t = 1/(1-CardManager.SCALE_RATIO*ratio)-CardManager.SCALE_RATIO*(itemCount-i-1);
view.setScaleX(t);
view.setTranslationY(-CardManager.TRANS_RATIO*ratio+CardManager.TRANS_RATIO*(itemCount-i-1));
}
//为被拖动的View增加透明度动画
View view = recyclerView.getChildAt(itemCount-1);
view.setAlpha(1 - Math.abs(ratio) * 0.2f);
}
//获取划出屏幕的距离阈值
private float getThreshold(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
return recyclerView.getWidth() * getSwipeThreshold(viewHolder);
}
- getMovementFlags():用来设置滑动和拖动方向,两个参数为滑动和拖动方向,本例中只支持滑动不支持拖动,滑动支持上下左右四个方向。
- onSwiped():当itemView滑出屏幕后会回调该方法,我定义了一个回调接口,在onSwiped()中调用该接口通知适配器做更新操作。
- onChildDraw():滑动时动画可以在该方法内进行。
- getThreshold():用来获取划出屏幕距离阈值,比如通过setThreshold()设置了滑动阈值为100像素,当itemView滑动超过100像素时松手itemView会自定滑出屏幕。
以上几个方法只有onChildDraw()略微麻烦,其他几个都比较简单,所以我只对onChildDraw()中内容进行消息描述:
- 从方法中获取到x、y轴平移的距离dx、dy,然后从二者中选出一个绝对值较大的作为滑动距离。
- 计算滑动比例ratio,用来设置动画
- 获取到总item数,开启一个for循环逐个为itemView设置平缓度过的动画 (除去第一个和最后一个),进行平移和缩放的时候一定要建立在之前的基础上
- 获取到正在被滑动的itemVIew,为其施加透明度动画
在实现平移和缩放动画的时候一定要在已经进行的平移和缩放的基础上进行,一定一定一定,重要的事情说三遍。以上为过度动画的内容,难度也不是很高。
Activity中的应用
与RecyclerView进行绑定
CardHelperCallback itemTouchHelpCallback = new CardHelperCallback();
ItemTouchHelper helper = new ItemTouchHelper(itemTouchHelpCallback);
//将ItemTouchHelper和RecyclerView进行绑定
helper.attachToRecyclerView(recyclerView);
一般要在自定义ItemTouchHelper.Callback中定义一个接口,用来itemView被滑出屏幕后即onSwiped()方法被调用的与Adapter通信
public interface OnItemTouchCallbackListener {
//item被滑动的回调方法
void onSwiped(int position, int direction);
}
在需要的地方(一般都是在Activity中)实现接口中的方法onSwiped(int position, int direction)
//实现OnItemTouchCallbackListener 接口
@Override
public void onSwiped(int position,int direction) {
if(direction==CardManager.MAX_COUNT){//左滑
//Toast.makeText(this,"left",Toast.LENGTH_SHORT).show();
}else {//右滑
//Toast.makeText(this,"right",Toast.LENGTH_SHORT).show();
}
if(mCardBeanList!=null){
for (int i = 0;i<recyclerView.getChildCount();i++){
View view = recyclerView.getChildAt(i);
view.setAlpha(1);
}
mCardBeanList.remove(position);
//加载更多
if(mCardBeanList.size()<CardManager.MAX_COUNT){
loadMore();
}
mCardAdapter.notifyDataSetChanged();
}
}
改方法中有三点需要注意:
- 重置itemView的透明度,因为itemView是要进行复用的,所以放入缓冲池中时要进行重置
- 删除List中对用的元素
- 更新适配器,让数据重新填充
拓展
我们都知道RecyclerView是可以对View进行复用的,那么它的复用原理是什么呢?在这我就简单说一下,虽然View缓冲池由RecyclerView掌控,但是想要完成整个View的复用需要LayoutManager配合,因为RecyclerView并不知道什么时候需要将View回收,所以需要LayoutManager告诉RecyclerView回收哪个View,通过recyclerView.removeAndRecycleView(child, recycler)来实现,所以在本例中应该在onSwiped()中调用该方法,但细心的同学可能发现我并未调用这个方法通知RecyclerView回收,其实是因为itemView被滑出屏幕后ItemTouchHelper内部会对该itemView进行回收操作,关于这一块内容大家做一个了解就行了,如果想要深入了解RecyclerView复用机制可以参考文章开头推荐的启舰大神RecyclerView系列。
Demo已托管至github,可运行
总结
RecyclerView是Android中非常重要的一个控件,功能十分强大,并且Google对它的封装只能用完美的不能再完美
来形容,Adapter、ItemTouchHepler、LayoutManager职责明确,完全独立于RecyclerView存在,严格遵守低耦合
,所以在我们制作卡片式滑动时RecyclerView和Adapter未受到任何影响,这也使得开发者更加清晰的去使用RecyclerView。看了本篇文章相信你对ItemTouchHepler和LayoutManager的理解又更加的深入了一些,如果觉得我帮助到你了,就去github上给我一个start,再次万分感谢。