最近楼主在忙碌于本身的毕设项目,在毕设当中须要实现一个滑动卡片的效果,楼主花了一点时间本身实现了一下,使用是ItemTouchHelper
和LayoutManager
方式实现的。咱们先来看一下效果: git
ViewPager
就能够实现了,这是没错的,可是
ViewPager
一直有一个诟病--那就是
View
的复用性不高。考虑到性能,
RecyclerView
天然是当之无愧的王者,既然咱们学过
RecyclerView
,为何不尝试着实现的呢?
看着这个动画麻烦,其实咱们将它分为两个部分实现就很是简单了。首先,每一个ItemView
是叠加样式展示的,这个效果在咱们经常使用到的LayoutManger
没有这种样式,因此得须要咱们自定义一个LayoutManager
来实现一个这种样式。这是其一。 其二,滑动切换的效果怎么实现呢?还记得咱们以前分析过ItemTouchHelper
这个类吗?这个类的做用是用来实现侧滑删除以及长按拖动的效果的,而这里切换卡片的效果就至关于侧滑删除,只不过是侧滑时作的动画不同。这里的动画主要包括卡片的位移和角度变化,而ItemTouchHelper
怎么实现根据手指滑动来作相应的动画呢?答案就在onChildDraw
方法里面。 其实,咱们从ItemTouchHelper
的onChildDraw
方法里面就知道,原生只是作了水平位置的变化,因此,咱们能够重写这个方法,从而加上咱们想要的动画。 这样来分析,这个动画是否是很是简单呢?接下来,咱们从看看代码吧。github
自定义LayoutManager
的相关知识,我在RecyclerView 源码分析(七) - 自定义LayoutManager及其相关组件的源码分析文章里面已经详细的解释了,这里我就不重复了。咱们直接来看代码吧,关键代码在于onLayoutChildren
方法里面:bash
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
final int layoutCount = Math.min(getItemCount(), mMaxVisibleCount);
detachAndScrapAttachedViews(recycler);
for (int i = layoutCount - 1; i >= 0; i--) {
final View view = recycler.getViewForPosition(i);
addView(view);
measureChildWithMargins(view, 0, 0);
int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
int heightSpace = getHeight() - getDecoratedMeasuredHeight(view);
layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2,
widthSpace / 2 + getDecoratedMeasuredWidth(view),
heightSpace / 2 + getDecoratedMeasuredHeight(view));
// 给每一个ItemView设置scale
view.setScaleX((float) Math.pow(DEFAULT_SCALE, i));
view.setScaleY((float) Math.pow(DEFAULT_SCALE, i));
if (i == 0) {
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
RecyclerView.ViewHolder childViewHolder = mRecyclerView.getChildViewHolder(v);
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
// 这里须要手动告诉ItemTouchHelper能够侧滑
mItemTouchHelper.startSwipe(childViewHolder);
}
return false;
}
});
} else {
// 因为ItemView会复用,因此必定要设置null
view.setOnTouchListener(null);
}
}
}
复制代码
相信上面的代码你们都能看的懂,这里我就不逐行的解释了。可是有一点须要咱们特别注意:ide
for (int i = layoutCount - 1; i >= 0; i--) {
// ······
}
复制代码
这里咱们是倒着添加View
,也就是一个ItemView
虽然在RecyclerView
的内部index为0,可是在Adapter
中,倒是layoutCount - 1
,这个在咱们自定义ItemTouchHelper.Callback
时,会有很大的做用。源码分析
关于ItemTouchHelper
的知识,我在RecyclerView 扩展(二) - 手把手教你认识ItemTouchHelper文章里面已经详细的解释过了,因此在这里我也不重复了。咱们直接来看实现代码,关键在onChildDraw
方法:性能
@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);
final View itemView = viewHolder.itemView;
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
float ratio = dX / getThreshold(recyclerView, viewHolder);
if (ratio > 1) {
ratio = 1;
} else if (ratio < -1) {
ratio = -1;
}
// 跟着角度旋转
itemView.setRotation(ratio * 15);
for (int i = 0; i < mMaxVisibleCount - 1; i++) {
// 下面的ItemView跟着手指缩放
View child = recyclerView.getChildAt(i);
final float currentScale = (float) Math.pow(DEFAULT_SCALE, 2 - i);
final float nextScale = currentScale / DEFAULT_SCALE;
final float scale = (nextScale - currentScale);
child.setScaleX(Math.min(1, currentScale + scale * Math.abs(ratio)));
child.setScaleY(Math.min(1, currentScale + scale * Math.abs(ratio)));
}
}
}
复制代码
上面代码的做用我在注释已经解释比较清楚了,这里就不解释了。不过这里还须要一点:动画
for (int i = 0; i < mMaxVisibleCount - 1; i++) {
// ······
}
复制代码
这里我缩放的也是0 ~ mMaxVisibleCount - 1的ItemView
,请记住,这个不是ItemView
在Adapter
中的position
,而是ItemView
在RecyclerView
内部的index值。在前面的LayoutManager
中,我已经解释过,这俩是反着的。因此这里应该是0 ~ mMaxVisibleCount - 1。 整个实现就是这么的简单,其实还有坑没有说,好比说:ui
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.setRotation(0f);
}
复制代码
在clearView
方法里面必须进行重置,由于ItemView
是复用的,不重置的话会出问题的。 在好比说,必须重写isItemViewSwipeEnabled
方法(虽然不重写也没有问题,可是官方文档建议重写):spa
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
复制代码
使用上面代码来实现效果以后,咱们会发现一个问题,若是将RecyclerView
放在SwipeRefreshLayout
内部,会出现事件冲突。 我简单的描述一下事件冲突的状况:当咱们左右滑动时,这是正常的,每一个ItemView
都是正常的侧换;可是一旦上下滑动时,正常来讲应该是SwipeRefreshLayout
滑动,可是实际上仍是ItemView
在侧滑。 关于解决方案的话,我有两种方案:1. 重写SwipeRefreshLayout
的onInterceptTouchEvent
方法,进行事件拦截,让事件不能传递到ItemView
中;2. 取消手动调用ItemTouchHelper
的startSwipe
方法,让ItemTouchHelper
本身来判断是否符合侧滑的条件。 这里,我特别的说明一下第一种方法。为何要特别说明第一种方法呢?由于此方法有很大的问题:1. 会重写SwipeRefreshLayout
,这个形成了没必要要的工做,这是其一;2. 重写了SwipeRefreshLayout
会破坏SwipeRefreshLayout
的结构,这个才是最大的缺点。 为何重写SwipeRefreshLayout
会破坏它的结构呢?咱们能够从SwipeRefreshLayout
的源码看出来,SwipeRefreshLayout
不会主动的拦截事件,由于SwipeRefreshLayout
是经过嵌套滑动机制来实现滑动,若是咱们在onInterceptTouchEvent
方法里面进行事件拦截,就违背了SwipeRefreshLayout
的设计。因此,第一种方法是特别不推荐的!!! 其次,咱们来看看第二种方案的实现方式,第二种方案很是简单,归根结底就是两句话:设计
- 在
Callback
里面不要重写isItemViewSwipeEnabled
方法,- 在
LayoutManager
里面不要在每一个ItemView
的OnTouchListener
里面调用ItemTouchHelper
的startSwipe
方法。
我在这里简单的解释第二种方式为何这样作就不会冲突了,不过要了解为何不冲突,必须得了解之前为何会冲突。 SwipeRefreshLayout
自己不会拦截事件,因此全部的事件均可以传递到RecyclerView
里面的每一个ItemView
里面。由于咱们在OnTouchListener
调用ItemTouchHelper
的startSwipe
表示选中了一个ItemView
能够侧滑,从而致使后面事件都会被该ItemView
消费,进而致使了事件冲突。 而取消startSwipe
方法的调用,让ItemTouchHelper
本身来选中一个能够侧滑的ItemView
,ItemTouchHelper
自己就处理了上下滑和左右滑的冲突的(若是没有处理,RecyclerView的上下滑跟ItemView的侧滑会冲突)。这就是第二种方式的原理。
为了方便你们的理解,我将本身的Demo代码上传到github,供你们参考:SlideCardDemo