Android UI实现拖拽及滑动处理

一.简介

       本文主角是ItemTouchHelper,它是RecyclerView对于item交互处理的一个辅助类,主要用于拖拽以及滑动处理。关于RecyclerView的分析可参考文章RecyclerView显示及缓存机制
       以接口实现的方式,起到了配置简单、逻辑解耦、职责分明的效果,并且支持所有的布局方式。
       功能包括如下:

image.png

二.功能实现

2.1.实现接口

       自定义一个类,实现ItemTouchHelper.Callback接口,然后在实现方法中根据需求简单配置即可,代码如下:

public class TaskItemTouchHelper extends ItemTouchHelper.Callback {
}

       ItemTouchHelper.Callback必须实现的3个方法:
       • getMovementFlags
       • onMove
       • onSwiped
       其他方法还有onSelectedChanged、clearView、getSwipeThreshold等
       接下来对上述方法进行一一描述分析:

2.1.1.getMovementFlags

       用于创建交互方式,交互方式分为两种:
       a.拖拽:网格布局支持上下左右,列表只支持上下(UP、DOWN)
       b.滑动:支持上下左右滑动(LEFT、UP、RIGHT、DOWN)
       方法实现如下:

/**
 * Called first when Callback, it is used to determine action and direction for current
 * function: drag and swipe action
 */
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
    //direction:up,down,left,right
    //constants
    // ItemTouchHelper.UP    0x0001
    // ItemTouchHelper.DOWN  0x0010
    // ItemTouchHelper.LEFT  0x0100
    // ItemTouchHelper.RIGHT 0x1000

    //listen for direction of drag
    int dragFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
    //listen for direction of swipe
    int swipeFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;

    return makeMovementFlags(dragFlags, swipeFlags);
}

       通过makeMovementFlags把结果返回回去,makeMovementFlags接收两个参数,dragFlagsswipeFlags,即拖拽和滑动组合的标志位。

2.1.2.onMove

       拖拽时回调,这里我们主要对起始位置和目标位置的item做一个数据交换,然后刷新视图显示。

@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder srcHolder,RecyclerView.ViewHolder targetHolder) {
    // call adapter.notifyItemMoved(from,to) continuously when drag process
    if (srcHolder.getItemViewType() != targetHolder.getItemViewType()) {
        return false;
    }
    // call adapter.notifyItemMoved(from,to) continuously when drag process,
    // callback onItemMove to implements class
    return moveListener.onItemMove(srcHolder.getAdapterPosition(),
            targetHolder.getAdapterPosition());
}

       通过获取起始位置,不断调用adapter的notifyItemMoved()对UI进行刷新。

2.1.3.onSwiped

       滑动时回调,这个回调方法里主要是做数据和视图的更新操作。

@Override
public void onSwiped(RecyclerView.ViewHolder holder, int direction) {
    //listen for swipe up:1.delete data;2.call adapter.notifyItemRemove(position);
    moveListener.onItemRemove(holder.getAdapterPosition());
}}

       比如:滑动删除某个item,监听到满足swipe()的阈值后进行删除操作;

2.2.关联RecyclerView

       上面接口实现部分已经实现了,那么如何将ItemTouchHelper与RecyclerView建立关联呢?
       接下来就是把这个辅助类绑定到RecyclerView,代码实现如下:

mTaskAdapter = new TaskAdapter(this, availableList);
ItemTouchHelper.Callback callback = new TaskItemTouchHelper(mTaskAdapter);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(mRecyclerView);

       关联只需要调用attachToRecyclerView就好了。

2.3.设置分割线

       通过RecyclerView的抽象静态内部类ItemDecoration来实现;

public class SpaceItemDecoration extends RecyclerView.ItemDecoration {
    private int mItemSpace;
    private int mTopSpace;

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        calculateOutRect(outRect, view, parent);
    }

    public SpaceItemDecoration(int space, int topSpace) {
        this.mItemSpace = space;
        this.mTopSpace = topSpace;
    }

    private void calculateOutRect(Rect outRect, View view, RecyclerView parent) {
        RecyclerView.Adapter adapter = parent.getAdapter();
        if (adapter != null) {
            int count = adapter.getItemCount();
            int position = parent.getChildPosition(view);
            switch (count) {
                case 1:
                    outRect.top = (SCREEN_HEIGHT - ITEM_HEIGHT) / 2;
                    outRect.right = (SCREEN_WIDTH - ITEM_WIDTH) / 2;
                    break;
                case 2:
                    if (position % 2 == 0) {
                        outRect.top = mTopSpace;
                    } else {
                        outRect.top = 0;
                    }
                    outRect.right = (SCREEN_WIDTH - ITEM_WIDTH) / 2;
                    break;
                case 3:
                case 4:
                    if (position % 2 == 0) {
                        outRect.top = mTopSpace;
                    } else {
                        outRect.top = 0;
                    }
                    if (position < 2) {
                        outRect.right = (SCREEN_WIDTH - ITEM_WIDTH * 2 - mItemSpace) / 2;
                    }
                    outRect.left = mItemSpace;
                    break;
                default:
                    if (position % 2 == 0) {
                        outRect.top = mTopSpace;
                    } else {
                        outRect.top = 0;
                    }
                    if (position < 2) {
                        outRect.right = mItemSpace;
                    }
                    outRect.left = mItemSpace;
                    break;
            }
        }
    }
}

       代码实现也比较简单,通过addItemDecoration()来建立关联就可以了;

TaskGridLayoutManager gridLayoutManager = new TaskGridLayoutManager(this, 2,
                RecyclerView.HORIZONTAL, true);
mRecyclerView.setLayoutManager(gridLayoutManager);
mRecyclerView.addItemDecoration(new SpaceItemDecoration(100, 150));

       传入不同Item之间的间隔,比如:top和gap,在加载Item时会回调getItemOffsets来获取该Item对应的Rect,然后再内部进行处理最终确定Item对应的Rect就可以了。

2.4.UI强调

       在平时的滑动或拖拽交互中,为了区分选中的Item,会对其进行强调,比如:选中的item放大、背景高亮等。
       此处会用到ItemTouchHelper.Callback中的其他两个方法,onSelectedChangedclearView,在选中时改变视图显示,结束时再进行恢复。

2.4.1.onSelectedChanged

       拖拽或滑动发生改变时回调,这时我们可以修改item的视图;

@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
    //judge state for selected
    if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
        swipeAnimation(viewHolder, true);
    }
    super.onSelectedChanged(viewHolder, actionState);
}

       actionState对应三种state:
       ACTION_STATE_IDLE:空闲状态
       ACTION_STATE_SWIPE:滑动状态
       ACTION_STATE_DRAG:拖拽状态

2.4.2.clearView

       拖拽或滑动结束时回调,这时我们要把改变后的item视图恢复到初始状态

@Override
 public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
    swipeAnimation(viewHolder, false);
    super.clearView(recyclerView, viewHolder);
}

       在拖拽或滑动开始和结束时,通过swipeAnimation()进行强调:

private void swipeAnimation(RecyclerView.ViewHolder viewHolder, boolean isStart) {
    float[] floatPram;
    if (isStart) {
        floatPram = new float[]{1.0f, 1.05f};
    } else {
        floatPram = new float[]{1.05f, 1.0f};
    }
    AnimatorSet animatorSet = new AnimatorSet();
    ObjectAnimator scaleX = ObjectAnimator.ofFloat(viewHolder.itemView, "scaleX", floatPram);
    ObjectAnimator scaleY = ObjectAnimator.ofFloat(viewHolder.itemView, "scaleY", floatPram);
    scaleX.setDuration(200);
    scaleY.setDuration(200);
    animatorSet.playTogether(scaleX,scaleY);
    animatorSet.start();
}

三.源码分析

       在前面的功能实现代码中,可以看到,在创建完ItenTouchHelper及Callback后,会通过attachToRecyclerView()建立关联,本文就从该方法开始分析:

3.1.attachToRecyclerView

public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
    if (mRecyclerView == recyclerView) {
        return; // nothing to do
    }
    if (mRecyclerView != null) {
        destroyCallbacks();
    }
    mRecyclerView = recyclerView;
    if (recyclerView != 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();
    }
}

       从该方法可以看到,主要操作如下:
       a.首先进行重复判断,如果是相同的recyclerview,直接返回;
       b.如果mRecyclerView不为空,调用了destroyCallbacks,在destroyCallbacks里面对mRecyclerView进行了一些移除和回收操作,说明只能绑定到一个RecyclerView;同时,注意这里判断的主体是mRecyclerView,不是我们传进来的recyclerView,而且我们传进来的recyclerView是支持Nullable的,所以我们可以传个空值走到destroyCallbacks里来做解绑操作
       c.最后当传入的recyclerView不为空时,调用setupCallbacks();
       前面分析到,destroyCallbacks会进行一些移除和回收操作,那么setupCallbacks()应该是执行初始化操作,一起看一下具体实现:

3.2.setupCallbacks

private void setupCallbacks() {
    ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
    mSlop = vc.getScaledTouchSlop();        
    mRecyclerView.addItemDecoration(this);        
    mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);       
    mRecyclerView.addOnChildAttachStateChangeListener(this);       
    startGestureDetection();    
}

       可以看到,在setupCallbacks()内部会执行了四个方法:
       1.执行addItemDecoration(this)将自身加入到RecyclerView中的mItemDecorations进行管理,注意:ItemTouchHelper是继承了RecyclerView.ItemDecoration;
       2.执行addOnItemTouchListener()对RecyclerView的Item触摸事件进行监听处理;
       3.执行addOnChildAttachStateChangeListener对child View是否移除Window进行回调监听;
       4.执行startGestureDetection()来进行手势识别监听;

3.3.mOnItemTouchListener

private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
    @Override
    public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
            @NonNull MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        final int action = event.getActionMasked();
        if (action == MotionEvent.ACTION_DOWN) {
            .................
            if (mSelected == null) {
                final RecoverAnimation animation = findAnimation(event);
                if (animation != null) {
                    .................
                    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) {
            final int index = event.findPointerIndex(mActivePointerId);
            if (index >= 0) {
                checkSelectForSwipe(action, event, index);
            }
        }
        
   }

    @Override
    public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        ..................
        final int action = event.getActionMasked();
        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) {
                    moveIfNecessary(viewHolder);
                }
                break;
            }
        }

    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (!disallowIntercept) {
            return;
        }
        select(null, ACTION_STATE_IDLE);
    }
};

       该方法主要功能就是监听MotionEvent进行判断处理,然后执行对不同的手势进行不同的处理,主要有以下三个方法:
       a.select
       b.checkSelectForSwipe
       c.moveIfNecessary

3.3.1.select

    void select(@Nullable ViewHolder selected, int actionState) {
        if (selected == mSelected && actionState == mActionState) {
            return;
        }
        .........        
        if (mSelected != null) {
            if (prevSelected.itemView.getParent() != null) {
                final float targetTranslateX, targetTranslateY;
                switch (swipeDir) {
                    case LEFT:
                    case RIGHT:
                    case START:
                    case END:
                        targetTranslateY = 0;
                        targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth();                        break;
                    ............
                }
                ...........
            } else {
                removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);                  mCallback.clearView(mRecyclerView, prevSelected);
            }
        }
        ..........        
        mCallback.onSelectedChanged(mSelected, mActionState);
        mRecyclerView.invalidate();
    }

       在该方法内,这里面主要是在拖拽或滑动时对translateX/Y的计算和处理,然后通过mCallback.clearView和mCallback.onSelectedChanged进行回调,最后调用invalidate()实时刷新。

3.3.2.checkSelectForSwipe

void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
    ............//进行过滤判断
    final ViewHolder vh = findSwipedView(motionEvent);
    
    final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);

    final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK)
                >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);
    if (swipeFlags == 0) {
        return;
    }
    ......................
    //最终满足swipe的操作
    select(vh, ACTION_STATE_SWIPE);
}

       该方法时对滑动处理的check,最后也是收敛到select()方法统一处理。

3.3.3.moveIfNecessary

void moveIfNecessary(ViewHolder viewHolder) {
    if (mRecyclerView.isLayoutRequested()) {
        return;
    }
    if (mActionState != ACTION_STATE_DRAG) {
        return;
    }
    .............
    //最终满足onMove交换的操作
    if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
        // keep target visible
        mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,target, toPosition, x, y);
    }
}

       该方法时检查拖拽时是否需要交换item,通过mCallback.onMoved进行回调。

3.4.startGestureDetection

private void startGestureDetection() {
    mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener();
    mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
                mItemTouchHelperGestureListener);
}

3.4.1.ItemTouchHelperGestureListener

private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
    ...........
    @Override
    public void onLongPress(MotionEvent e) {
        .......
        View child = findChildView(e);
        if (child != null) {
            ViewHolder vh = mRecyclerView.getChildViewHolder(child);
            if (vh != null) {
                ...........
                if (pointerId == mActivePointerId) {
                    ...............
                    if (mCallback.isLongPressDragEnabled()) {
                        select(vh, ACTION_STATE_DRAG);
                    }
                }
            }
        }
    }
}

       此处主要是对长按事件的处理,最后也是收敛到select()方法统一处理。

简单总结

       a.绑定RecyclerView
       b.注册触摸手势监听
       c.根据手势,先是内部处理各种校验、位置计算、动画处理、刷新等,然后回调给ItemTouchHelper.Callback

3.5.ItemDecoration

       前面讲到,本地继承RecyclerView.ItemDecoration,重写getItemOffsets可以设置分割线,接下来看一下具体的逻辑实现:

3.5.1.addItemDecoration

public void addItemDecoration(@NonNull ItemDecoration decor) {
    addItemDecoration(decor, -1);
}
public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
    if (mLayout != null) {
        mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll  or"+ " layout");
    }
    if (mItemDecorations.isEmpty()) {
        setWillNotDraw(false);
    }
    if (index < 0) {
        mItemDecorations.add(decor);
    } else {
        mItemDecorations.add(index, decor);
    }
    markItemDecorInsetsDirty();
    requestLayout();
}

       可以看到,在执行addItemDecoration()会将该decor加入到mItemDecorations中进行管理,从此处可以看到,ItemDecoration可以是多个,可以单独对Rect的各个区域进行单独处理,在存储之后,看一下是如何用到的?
       我们知道,ItemDecoration是在RecyclerView进行加载ViewHolder时进行使用,因为要确定各个ViewHolder的显示位置及显示大小,那根据这个思路就好分析了,这里要说明一下,RecyclerView的ViewHolder的显示是由LayoutManager来进行管理的,中间的过程就不一一陈述了,可以直接阅读源码,此处直接从LayoutManager的measureChild进行分析:

3.5.2.measureChild

public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    widthUsed += insets.left + insets.right;
    heightUsed += insets.top + insets.bottom;
    final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
                    getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
                    canScrollHorizontally());
    final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
                    getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
                    canScrollVertically());
    if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
        child.measure(widthSpec, heightSpec);
    }
}

       可以看到,在该方法内会调用RecyclerView的getItemDecorInsetsForChild()来获取到child view对应的Rect,然后来计算child view需要显示的区域;

3.5.3.getItemDecorInsetsForChild

Rect getItemDecorInsetsForChild(View child) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    .................
    final Rect insets = lp.mDecorInsets;
    insets.set(0, 0, 0, 0);
    final int decorCount = mItemDecorations.size();
    for (int i = 0; i < decorCount; i++) {
        mTempRect.set(0, 0, 0, 0);
        mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
        insets.left += mTempRect.left;
        insets.top += mTempRect.top;
        insets.right += mTempRect.right;
        insets.bottom += mTempRect.bottom;
    }
    lp.mInsetsDirty = false;
    return insets;
}

       可以看到,在该方法内会遍历mItemDecorations,通过ItemDecoration的getItemOffsets来获取对应的mTempRect,在赋值给insets,最终返回;

3.5.4.getItemOffsets

       在本地实现getItemOffsets时,会先调用super.getItemOffsets(),主要用来对Rect进行置空,主要作用是确保每个Item不会相互影响,即每个item的显示区域可以随意定义;

@Deprecated
public void getItemOffsets(@NonNull Rect outRect, int itemPosition,@NonNull RecyclerView parent) {
    outRect.set(0, 0, 0, 0);
}

public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,@NonNull RecyclerView parent, @NonNull State state) {
   getItemOffsets(outRect, ((LayoutParams)view.getLayoutParams()).getViewLayoutPosition(),
                    parent);
}

       以上就是RecycleView UI实现拖拽即滑动处理的实现及部分源码实现分析!

你可能感兴趣的:(Android UI实现拖拽及滑动处理)