首先面临的问题就是关于实现 View 上的考虑。毫无疑问,
RecyclerView 是最佳选择!
究其原因,第一,RecyclerView 是自带 Item View 回收和重用功能的,就不需要我们考虑这个问题了;第二,RecyclerView 的布局方式是通过设置 LayoutManager 来实现的,这样就充分地把布局和 RecyclerView “解耦”开来了。而 LayoutManager 是可以通过自定义的方式来实现的。这恰恰是我们想要的!!!再说一点,这也正是不选用 ListView 的原因之一。
下面,我们就开始动手了。带你见证奇迹的时刻。
CardLayoutManager
创建 CardLayoutManager
并继承自 RecyclerView.LayoutManager
。需要我们自己实现 generateDefaultLayoutParams()
方法:
1 2 3 4
| @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); }
|
一般情况下,像上面这样写即可。
下面这个方法就是我们的重点了。 onLayoutChildren(final RecyclerView.Recycler recycler, RecyclerView.State state)
方法就是用来实现 Item View 布局的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| @Override public void onLayoutChildren(final RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); removeAllViews(); detachAndScrapAttachedViews(recycler); int itemCount = getItemCount(); if (itemCount > CardConfig.DEFAULT_SHOW_ITEM) { for (int position = CardConfig.DEFAULT_SHOW_ITEM; position >= 0; position--) { final View view = recycler.getViewForPosition(position); 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)); if (position == CardConfig.DEFAULT_SHOW_ITEM) { view.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); } else if (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); } } } else { for (int position = itemCount - 1; position >= 0; position--) { final View view = recycler.getViewForPosition(position); 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));
if (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); } } } }
private View.OnTouchListener mOnTouchListener = new View.OnTouchListener() {
@Override public boolean onTouch(View v, MotionEvent event) { RecyclerView.ViewHolder childViewHolder = mRecyclerView.getChildViewHolder(v); if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) { mItemTouchHelper.startSwipe(childViewHolder); } return false; } };
|
总体来说,CardLayoutManager
主要就是为 Item View 布局,然后根据 position
做相对应的偏差。我们一起来看下完成的效果图:
可以看出,大致的效果已经有了。缺少的就是处理触摸滑动事件了。
OnSwipeListener
在看滑动事件的代码之前,我们先定义一个监听器。主要用于监听卡片滑动事件,代码就如下所示,注释也给出来了。应该都看得懂吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public interface OnSwipeListener<T> {
void onSwiping(RecyclerView.ViewHolder viewHolder, float ratio, int direction);
void onSwiped(RecyclerView.ViewHolder viewHolder, T t, int direction);
void onSwipedClear();
}
|
CardItemTouchHelperCallback
现在,我们可以回过头来看看卡片滑动了。使用 ItemTouchHelper 来处理 Item View 的触摸滑动事件。
我们暂且命名为 CardItemTouchHelperCallback 。对于 ItemTouchHelper.Callback 而言,需要在 getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
方法中配置 swipeFlags
和 dragFlags
。
具体的方法如下,对于 swipeFlags
只关心左右两个方向:
1 2 3 4 5 6 7 8 9 10
| @Override public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { int dragFlags = 0; int swipeFlags = 0; RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if (layoutManager instanceof CardLayoutManager) { swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; } return makeMovementFlags(dragFlags, swipeFlags); }
|
还有一点需要注意,前面说过,为了防止第二层和第三层卡片也能滑动,因此我们需要设置 isItemViewSwipeEnabled()
返回 false 。
1 2 3 4
| @Override public boolean isItemViewSwipeEnabled() { return false; }
|
接下来,就是去重写 onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
和 onSwiped(RecyclerView.ViewHolder viewHolder, int direction)
方法。但是因为在上面我们对于 dragFlags
配置的是 0 ,所以在 onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
中直接返回 false 即可。
1 2 3 4
| @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { return false; }
|
这样,我们就把目光投向 onSwiped(RecyclerView.ViewHolder viewHolder, int direction)
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { viewHolder.itemView.setOnTouchListener(null); int layoutPosition = viewHolder.getLayoutPosition(); T remove = dataList.remove(layoutPosition); adapter.notifyDataSetChanged(); if (mListener != null) { mListener.onSwiped(viewHolder, remove, direction == ItemTouchHelper.LEFT ? CardConfig.SWIPED_LEFT : CardConfig.SWIPED_RIGHT); } if (adapter.getItemCount() == 0) { if (mListener != null) { mListener.onSwipedClear(); } } }
|
写好后,我们先来看看滑动效果:
发现还是差了点什么,没错!是缺少了动画。在滑动的过程中我们可以重写 onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive)
方法来添加动画:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| @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); 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 * CardConfig.DEFAULT_ROTATE_DEGREE); int childCount = recyclerView.getChildCount(); if (childCount > CardConfig.DEFAULT_SHOW_ITEM) { for (int position = 1; position < childCount - 1; position++) { int index = childCount - position - 1; View view = recyclerView.getChildAt(position); 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 { for (int position = 0; position < childCount - 1; position++) { int index = childCount - position - 1; View view = recyclerView.getChildAt(position); 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); } } if (mListener != null) { if (ratio != 0) { mListener.onSwiping(viewHolder, ratio, ratio < 0 ? CardConfig.SWIPING_LEFT : CardConfig.SWIPING_RIGHT); } else { mListener.onSwiping(viewHolder, ratio, CardConfig.SWIPING_NONE); } } } }
private float getThreshold(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { return recyclerView.getWidth() * getSwipeThreshold(viewHolder); }
|
现在我们加上动画后,来看看效果:
发现还是有问题,第一层的卡片滑出去之后第二层的就莫名其妙地偏了。这正是因为 Item View 重用机制“捣鬼”。所以我们应该在 clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
方法中重置一下:
1 2 3 4 5
| @Override public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { super.clearView(recyclerView, viewHolder); viewHolder.itemView.setRotation(0f); }
|
大功告成,我们试一下效果:
Perfect !
这正是我们梦寐以求的效果。我们终于实现了!!!
总结一下,在这整个代码流程中我们主要是运用了自定义 LayoutManager 以及 ItemTouchHelper.Callback 。总体来说还是比较简单的,相信你已经会啦。