RecyclerView是一个用来替换之前的ListView和GridView的控件,使用的时候,虽然比以前的ListView看起来麻烦,但是其实作为一个高度解耦的控件,复杂一点点换来极大的灵活性,丰富的可操作性,何乐而不为呢。不过今天主要说说它的一个辅助类ItemTouchHelper来实现列表的拖动和滑动删除。
RecyclerView用法(ListView)
1.导入控件包
compile 'com.android.support:support-v13:25.+'
2.布局文件加入控件
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_test"
android:layout_width="match_parent"
android:layout_height="match_parent"></android.support.v7.widget.RecyclerView>
3.定义Adapter
public class TestAdapter extends RecyclerView.Adapter implements TouchCallbackListener {
/**
* 数据源列表
*/
private List<String> mData;
/**
* 构造方法传入数据
* @param mData
*/
public TestAdapter(List<String> mData) {
this.mData = mData;
}
/**
* 创建用于复用的ViewHolder
* @param parent
* @param viewType
* @return
*/
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewHolder vh = new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item,parent,false));
return vh;
}
/**
* 对ViewHolder的控件进行操作
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(holder instanceof ViewHolder){
ViewHolder holder1 = (ViewHolder) holder;
holder1.tv_test.setText(mData.get(position));
}
}
/**
*
* @return 数据的总数
*/
@Override
public int getItemCount() {
return mData.size();
}
/**
* 长按拖拽时的回调
* @param fromPosition 拖拽前的位置
* @param toPosition 拖拽后的位置
*/
@Override
public void onItemMove(int fromPosition, int toPosition) {
Collections.swap(mData, fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);//通知Adapter更新
}
/**
* 滑动时的回调
* @param position 滑动的位置
*/
@Override
public void onItemSwipe(int position) {
mData.remove(position);
notifyItemRemoved(position);通知Adapter更新
}
/**
* 自定义的ViewHolder内部类,必须继承RecyclerView.ViewHolder(这里用不用static存在争议,没有专门的测试,
* 从内存占用来看微乎其微,但是不知道有没有内存泄露的问题)
*/
public class ViewHolder extends RecyclerView.ViewHolder{
private TextView tv_test;
public ViewHolder(View itemView) {
super(itemView);
tv_test = (TextView) itemView.findViewById(.tv_test);
}
}
}
这里定义RecyclerView的Adapter适配器,必须继承自RecyclerView.Adapter,而且需要在内部定义ViewHolder类,这个跟我们之前使用ListView是一样的,不过在RecyclerView里面这个是必须实现的。还有就是这里我并没有用static,不影响复用,但是内存会不会泄漏呢?
然后里面还有两个在拖拽和滑动时的回调,这里是我们自己定义的一个接口TouchCallbackListener
TouchCallbackListener
public interface TouchCallbackListener {
/**
* 长按拖拽时的回调
* @param fromPosition 拖拽前的位置
* @param toPosition 拖拽后的位置
*/
void onItemMove(int fromPosition, int toPosition);
/**
* 滑动时的回调
* @param position 滑动的位置
*/
void onItemSwipe(int position);
}
4.使用ItemTouchHelper实现上下拖拽和滑动删除功能
ItemTouchHelper的构造方法需要传入ItemTouchHelper.Callback来自己定义各种动作时的处理,我们自定义的类如下:
TouchCallback
public class TouchCallback extends ItemTouchHelper.Callback {
/**
* 自定义的监听接口
*/
private TouchCallbackListener mListener;
public TouchCallback(TouchCallbackListener listener) {
this.mListener = listener;
}
/**
* 定义列表可以怎么滑动(上下左右)
* @param recyclerView
* @param viewHolder
* @return
*/
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
//上下滑动
int dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
//左右滑动
int swipeFlag = ItemTouchHelper.LEFT| ItemTouchHelper.RIGHT;
//使用此方法生成标志返回
return makeMovementFlags(dragFlag, swipeFlag);
}
/**
* 拖拽移动时调用的方法
* @param recyclerView 控件
* @param viewHolder 移动之前的条目
* @param target 移动之后的条目
* @return
*/
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
mListener.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true;
}
/**
* 滑动时调用的方法
* @param viewHolder 滑动的条目
* @param direction 方向
*/
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mListener.onItemSwipe(viewHolder.getAdapterPosition());
}
/**
* 是否允许长按拖拽
* @return true or false
*/
@Override
public boolean isLongPressDragEnabled() {
return true;
}
/**
* 是否允许滑动
* @return true or false
*/
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
}
5.使用RecyclerView绑定Adapter和ItemTouchHelper
最后在Activity中来使用RecyclerView
public class MainActivity extends AppCompatActivity{
private RecyclerView mRecyclerView;
private TestAdapter mTestAdapter;
private List<String> mData;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
mRecyclerView = (RecyclerView) findViewById(.rv_test);
mRecyclerView.setAdapter(mTestAdapter);
//定义布局管理器,这里是ListView。GridLayoutManager对应GridView
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
//ListView的方向,纵向
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(linearLayoutManager);
//添加每一行的分割线
// mRecyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
ItemTouchHelper helper = new ItemTouchHelper(new TouchCallback(mTestAdapter));
helper.attachToRecyclerView(mRecyclerView);
}
/**
* 初始化模拟数据
*/
private void initData() {
mData = new ArrayList<>();
String temp;
for(int i = 0; i < 99; ++i){
temp = i + "*";
mData.add(temp);
}
mTestAdapter = new TestAdapter(mData);
}
6.添加分割线
RecyclerView默认每一行是没有分割线的,如果需要分割线的话要自己去定义ItemDecoration,这个类可以为每个条目添加额外的视图与效果,我们自己定义的代码如下:
DividerItemDecoration
public class DividerItemDecoration extends RecyclerView.ItemDecoration{
private static final int[] ATTRS = new int[]{
android.R.attr.listDivider//Android默认的分割线效果
};
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
private Drawable mDivider;
private int mOrientation;
public DividerItemDecoration(Context context, int oritation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(oritation);
}
public void setOrientation(int orientation) {
if(orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST){
throw new IllegalArgumentException("invalid orientation");
}
this.mOrientation = orientation;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if(mOrientation == VERTICAL_LIST){
drawVertical(c, parent);
}else {
drawHorizontal(c,parent);
}
}
/**
* 纵向的列表
* @param c
* @param parent
*/
public void drawVertical(Canvas c, RecyclerView parent){
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for(int i = 0; i < childCount; i++){
final View child = parent.getChildAt(i);
RecyclerView v = new RecyclerView(parent.getContext());
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
/**
* 横向的列表
* @param c
* @param parent
*/
public void drawHorizontal(Canvas c, RecyclerView parent){
final int top = parent.getPaddingTop();
final int bottom = parent.getHeight() - parent.getPaddingBottom();
final int childCount = parent.getChildCount();
for(int i = 0; i < childCount; i++){
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int left = child.getRight() + params.rightMargin;
final int right = left + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if(mOrientation == VERTICAL_LIST){
outRect.set(0,0,0,mDivider.getIntrinsicHeight());
}else {
outRect.set(0,0,mDivider.getIntrinsicWidth(), 0);
}
}
}
到此就实现了一个支持长按拖拽和滑动删除的列表,很简单,效果就不截图了。
ItemTouchHelper原理
实现拖拽和滑动删除的过程的很简单,并且还有非常流畅的动画。只需要给ItemTouchHelper传入一个我们自己定义的回调即可,但是它的内部是怎么实现的呢?来一步一步看看代码。
首先看看它的类定义:
public class ItemTouchHelper extends RecyclerView.ItemDecoration
implements RecyclerView.OnChildAttachStateChangeListener
继承自RecyclerView.ItemDecoration,跟分割线一样,也是通过继承这个类来给每个条目添加效果
然后从它的在外层的使用开始:
ItemTouchHelper helper = new ItemTouchHelper(new TouchCallback(mTestAdapter));
helper.attachToRecyclerView(mRecyclerView);
RecyclerView和ItemTouchHelper的关联是ItemTouchHelper的attachToRecyclerView方法,进入这个方法:
ItemTouchHelper.attachToRecyclerView
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
final Resources resources = recyclerView.getResources();
mSwipeEscapeVelocity = resources
.getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
mMaxSwipeVelocity = resources
.getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity);
setupCallbacks();
}
}
首先判断传入的RecyclerView是否跟已经绑定的相等,如果相等,就直接返回,不过不相等,销毁之前的回调,然后将传入的RecyclerView赋值给全局变量,设置速率,最后调用setupCallbacks初始化
ItemTouchHelper.setupCallbacks
private void setupCallbacks() {
ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
mSlop = vc.getScaledTouchSlop();
mRecyclerView.addItemDecoration(this);
mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
mRecyclerView.addOnChildAttachStateChangeListener(this);
initGestureDetector();
}
前两句是获取TouchSlop的值,这个值用于判断是滑动还是点击,然后给RecyclerView添加ItemDecoration(也就是自己),条目的触摸监听,条目的关联状态监听。这里最主要的就是看看mOnItemTouchListener的实现:
ItemTouchHelper.mOnItemTouchListener
private final OnItemTouchListener mOnItemTouchListener
= new OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
if (DEBUG) {
Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event);
}
//用于处理多点触控
final int action = MotionEventCompat.getActionMasked(event);
if (action == MotionEvent.ACTION_DOWN) {
mActivePointerId = event.getPointerId(0);
mInitialTouchX = event.getX();
mInitialTouchY = event.getY();
obtainVelocityTracker();
if (mSelected == null) {
final RecoverAnimation animation = findAnimation(event);
if (animation != null) {
mInitialTouchX -= animation.mX;
mInitialTouchY -= ;
endRecoverAnimation(animation.mViewHolder, true);
if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
mCallback.clearView(mRecyclerView, animation.mViewHolder);
}
select(animation.mViewHolder, animation.mActionState);
updateDxDy(event, mSelectedFlags, 0);
}
}
} else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mActivePointerId = ACTIVE_POINTER_ID_NONE;
select(null, ACTION_STATE_IDLE);
} else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
// in a non scroll orientation, if distance change is above threshold, we
// can select the item
final int index = event.findPointerIndex(mActivePointerId);
if (DEBUG) {
Log.d(TAG, "pointer index " + index);
}
if (index >= 0) {
checkSelectForSwipe(action, event, index);
}
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
return mSelected != null;
}
@Override
public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
if (DEBUG) {
Log.d(TAG,
"on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event);
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
return;
}
final int action = MotionEventCompat.getActionMasked(event);
final int activePointerIndex = event.findPointerIndex(mActivePointerId);
if (activePointerIndex >= 0) {
checkSelectForSwipe(action, event, activePointerIndex);
}
ViewHolder viewHolder = mSelected;
if (viewHolder == null) {
return;
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
// Find the index of the active pointer and fetch its position
if (activePointerIndex >= 0) {
updateDxDy(event, mSelectedFlags, activePointerIndex);
moveIfNecessary(viewHolder);
mRecyclerView.removeCallbacks(mScrollRunnable);
mScrollRunnable.run();
mRecyclerView.invalidate();
}
break;
}
case MotionEvent.ACTION_CANCEL:
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
// fall through
case MotionEvent.ACTION_UP:
select(null, ACTION_STATE_IDLE);
mActivePointerId = ACTIVE_POINTER_ID_NONE;
break;
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = MotionEventCompat.getActionIndex(event);
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = event.getPointerId(newPointerIndex);
updateDxDy(event, mSelectedFlags, pointerIndex);
}
break;
}
}
}
这里主要重写了两个方法onInterceptTouchEvent和onTouchEvent,先来看看onInterceptTouchEvent,拦截屏幕事触控的事件,首先是判断单点按下
if (action == MotionEvent.ACTION_DOWN) {
//现在追踪的触摸事件
mActivePointerId = event.getPointerId(0);
//获取最开始按下的坐标值
mInitialTouchX = event.getX();
mInitialTouchY = event.getY();
//获取速度追踪器(此方法避免重复创建)
obtainVelocityTracker();
//如果选择的条目为空
if (mSelected == null) {
//查找对应的动画(避免重复动画)
final RecoverAnimation animation = findAnimation(event);
//执行动画,
if (animation != null) {
//更新初始值
mInitialTouchX -= animation.mX;
mInitialTouchY -= ;
//从动画列表里移除条目对应的动画
endRecoverAnimation(animation.mViewHolder, true);
//从回收列表里移除条目视图
if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
mCallback.clearView(mRecyclerView, animation.mViewHolder);
}
//执行选择动画
select(animation.mViewHolder, animation.mActionState);
//更新移动距离x,y的值
updateDxDy(event, mSelectedFlags, 0);
}
}
}
然后是判断取消和单点抬起:
else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mActivePointerId = ACTIVE_POINTER_ID_NONE;
select(null, ACTION_STATE_IDLE);//清除动画
最后执行下面判断点击状态为空:
else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
// 移动距离超过了临界值,判断是否滑动选择的条目
final int index = event.findPointerIndex(mActivePointerId);
if (DEBUG) {
Log.d(TAG, "pointer index " + index);
}
if (index >= 0) {
//判断是否滑选择的条目
checkSelectForSwipe(action, event, index);
}
}
最后如果选择的条目不等于null,返回true,表示拦截触摸事件,接下来执行onTouchEvent方法,只看对触摸动作的判断:
1.按下移动手指:
case MotionEvent.ACTION_MOVE: {
// 如果点击序号大于0,表示有点击事件
if (activePointerIndex >= 0) {
//更新移动距离
updateDxDy(event, mSelectedFlags, activePointerIndex);
//移动ViewHolder
moveIfNecessary(viewHolder);
//先移除动画
mRecyclerView.removeCallbacks(mScrollRunnable);
//执行动画
mScrollRunnable.run();
//重绘RecyclerView
mRecyclerView.invalidate();
}
break;
}
这里来看看mScrollRunnable.run():
final Runnable mScrollRunnable = new Runnable() {
@Override
public void run() {
if (mSelected != null && scrollIfNecessary()) {
if (mSelected != null) { //it might be lost during scrolling
moveIfNecessary(mSelected);
}
mRecyclerView.removeCallbacks(mScrollRunnable);
//递归调用
ViewCompat.postOnAnimation(mRecyclerView, this);
}
}
};
这里的run方法相当于是一个死循环,在里面又不断调用自己,不断的执行动画,因为选中的条目需要不停的跟随手指的移动,直到判断条件返回FALSE停止执行,然后回到onTouchEvent继续判断
2.当用户保持按下操作,并从你的控件转移到外层控件时,会触发ACTION_CANCEL:
case MotionEvent.ACTION_CANCEL:
if (mVelocityTracker != null) {
//清除速度追踪器
mVelocityTracker.clear();
}
3.抬起手指
case MotionEvent.ACTION_UP:
//清理选择动画
select(null, ACTION_STATE_IDLE);
//手指状态置空
mActivePointerId = ACTIVE_POINTER_ID_NONE;
break;
4.多点触控抬起
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = MotionEventCompat.getActionIndex(event);
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
//选择一个新的手指活动点,并且更新x,y的距离
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = event.getPointerId(newPointerIndex);
updateDxDy(event, mSelectedFlags, pointerIndex);
}
break;
}
根据对OnItemTouchListener的源码分析,我们知道了跟随手指的动画是怎么来实现的,简单来说,就是检测手指的动作,然后不断的重绘,最终就展现在我们面前,在长按上下拖拽时,按住的条目随着手指移动,左右滑动时,条目“飞”出屏幕。不过在实际的项目中,这种侧滑删除的操作肯定不是直接侧滑就执行删除,需要右边有一个删除的按钮来确认,这个也可以在ItemTouchHelper的基础上来改进,后面再说吧。