RecyclerView是一个用来替换以前的ListView和GridView的控件,使用的时候,虽然比之前的ListView看起来麻烦,可是其实做为一个高度解耦的控件,复杂一点点换来极大的灵活性,丰富的可操做性,何乐而不为呢。不过今天主要说说它的一个辅助类ItemTouchHelper来实现列表的拖动和滑动删除。java
compile 'com.android.support:support-v13:25.+'
复制代码
<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>
复制代码
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(R.id.tv_test);
}
}
}
复制代码
这里定义RecyclerView的Adapter适配器,必须继承自RecyclerView.Adapter,并且须要在内部定义ViewHolder类,这个跟咱们以前使用ListView是同样的,不过在RecyclerView里面这个是必须实现的。还有就是这里我并无用static,不影响复用,可是内存会不会泄漏呢?android
而后里面还有两个在拖拽和滑动时的回调,这里是咱们本身定义的一个接口TouchCallbackListenerbash
TouchCallbackListeneride
public interface TouchCallbackListener {
/** * 长按拖拽时的回调 * @param fromPosition 拖拽前的位置 * @param toPosition 拖拽后的位置 */
void onItemMove(int fromPosition, int toPosition);
/** * 滑动时的回调 * @param position 滑动的位置 */
void onItemSwipe(int position);
}
复制代码
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;
}
}
复制代码
最后在Activity中来使用RecyclerViewpost
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(R.id.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);
}
复制代码
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);
}
}
}
复制代码
到此就实现了一个支持长按拖拽和滑动删除的列表,很简单,效果就不截图了。fetch
实现拖拽和滑动删除的过程的很简单,而且还有很是流畅的动画。只须要给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 -= animation.mY;
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 -= animation.mY;
//从动画列表里移除条目对应的动画
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的基础上来改进,后面再说吧。