Android中系统触摸相关辅助类总结

Android中系统触摸相关辅助类总结

Android中的触摸事件,我们可以通过重写View的OnTouchEvent()等事件,通过事件类型MotionEvent来进行我们想要实现的逻辑操作,有时候一些简单的需求很容易实现,但是有时候,一些很困难的需求,我们需要编写大量的代码来实现。好在Android官方就给我们提供了很丰富的触摸相关的辅助类,今天,我就把我知道的分享给大家。

1、GestureDetector(手势探测器)

顾名思义,GestureDetector可以很方便的帮我们识别出我们当前在屏幕上的手势操作,比如按下、长按、抬起、双击、滚动、快速滑动(惯性滑动)等,甚至它还能识别出是否是鼠标右键点击的动作,省去了我们大量的手势判断语句,非常的实用。

1.1 相关的方法及接口简介

GestureDetector的手势识别有三个接口来处理我们当前的手势动作,还有一个集成了三个接口的实现类方便我们去处理多种事件。

1.1.1 GestureDetector.OnGestureListener

作用: 用来监听单击、长按、滑动等操作,我们可以在这些操作的回调方法中处理我们自己的逻辑。

具体实现方法:
见下面代码,注释已经写的很清楚了,示例代码中仅仅对滚动进行了处理。

    private static class MyGestureDetectorListener implements GestureDetector.OnGestureListener{

        private GestureDetectorView mView;

        public MyGestureDetectorListener(GestureDetectorView view){
            this.mView = view;
        }

        /**
         * 用户按下屏幕的时候回调
         * @param motionEvent
         * @return
         */
        @Override
        public boolean onDown(MotionEvent motionEvent) {
            Log.e("ll", "MyGestureDetectorListener : onDown");
            return true;
        }

        /**
         * 用户按下屏幕100ms后,如果还没有松手或者移动就会回调
         * @param motionEvent
         */
        @Override
        public void onShowPress(MotionEvent motionEvent) {
            Log.e("ll", "MyGestureDetectorListener : onShowPress");
        }

        /**
         * 单纯的点击再抬手时调用。用户手指松开(UP事件)的时候如果没有执行onScroll()和onLongPress()这两个回调的话,就会回调
         * @param motionEvent
         * @return
         */
        @Override
        public boolean onSingleTapUp(MotionEvent motionEvent) {
            Log.e("ll", "MyGestureDetectorListener : onSingleTapUp");
            return false;
        }

        /**
         * 屏幕拖动事件,如果按下的时间过长,调用了onLongPress,再拖动屏幕不会触发onScroll。
         * 拖动屏幕会多次触发
         * @param motionEvent 开始拖动的第一次按下down操作,也就是第一个ACTION_DOWN
         * @param motionEvent1 触发当前onScroll方法的ACTION_MOVE
         * @param distanceX 当前的x坐标与最后一次触发scroll方法的x坐标的差值。
         * @param distanceY 当前的y坐标与最后一次触发scroll方法的y坐标的差值。
         * @return
         */
        @Override
        public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float distanceX, float distanceY) {
            Log.e("ll", "MyGestureDetectorListener : onScroll : " + distanceY);
            mView.scrollBy(0, (int) distanceY);
            return false;
        }

        /**
         * 用户长按后(好像不同手机的时间不同,源码里默认是100ms+500ms)触发,触发之后不会触发其他回调,直至松开(UP事件)。
         * @param motionEvent
         */
        @Override
        public void onLongPress(MotionEvent motionEvent) {
            Log.e("ll", "MyGestureDetectorListener : onLongPress");
        }

        /**
         * 按下屏幕,在屏幕上快速滑动后松开,由一个down,多个move,一个up触发
         * @param motionEvent 开始快速滑动的第一次按下down操作,也就是第一个ACTION_DOWN
         * @param motionEvent1 触发当前onFling方法的move操作,也就是最后一个ACTION_MOVE
         * @param velocityX X轴上的移动速度,像素/秒
         * @param velocityY Y轴上的移动速度,像素/秒
         * @return
         */
        @Override
        public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float velocityX, float velocityY) {
            Log.e("ll", "MyGestureDetectorListener : onFling");
            return false;
        }
    }

注意事项:

1、onDown()方法返回true时,onScroll()onFling()方法才能回调。
2、onLongPress()若触发了,则不会再触发其他回调。
3、事件回调顺序:

  • 快速点击:onDown() > onSingleTapUp()
  • 慢按屏幕:onDown() > onShowPress() > onSingleTapUp()
  • 长按屏幕:onDown() > onShowPress() > onLongPress()
  • 滑动屏幕:onDown() > onShowPress() > onScroll()(多个) > onFling()
  • 快速拖动屏幕后松手:onDown() > onScroll()(多个) > onFling()

1.1.2 GestureDetector.OnDoubleTapListener

作用: 用来监听双击事件操作,我们可以在这些操作的回调方法中处理我们自己的逻辑。

具体实现方法:
见下面代码,注释已经写的很清楚了。

private static class MyGestureDetectorDoubleTapListener implements GestureDetector.OnDoubleTapListener{

    /**
     * 单击事件。用来判定该次点击是单纯的SingleTap而不是DoubleTap,如果连续点击两次就是DoubleTap手势,
     * 如果只点击一次,系统等待一段时间后没有收到第二次点击则判定该次点击为SingleTap而不是DoubleTap,然后触发SingleTapConfirmed事件。
     * @param motionEvent
     * @return
     */
    @Override
    public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
        Log.e("ll","MyGestureDetectorDoubleTapListener : onSingleTapConfirmed");
        return false;
    }

    /**
     * 双击触发
     * @param motionEvent
     * @return
     */
    @Override
    public boolean onDoubleTap(MotionEvent motionEvent) {
        Log.e("ll","MyGestureDetectorDoubleTapListener : onDoubleTap");
        return false;
    }

    /**
     * 双击间隔中发生的动作。指触发onDoubleTap以后,在双击之间发生的其它动作,包含down、up和move事件
     * @param motionEvent
     * @return
     */
    @Override
    public boolean onDoubleTapEvent(MotionEvent motionEvent) {
        Log.e("ll","MyGestureDetectorDoubleTapListener : onDoubleTapEvent");
        return false;
    }
}

注意事项:
1、onSingleTapConfirmed()用来判断是不是单点事件,双击不会执行此方法。
2、事件调用顺序:

  • 单击屏幕时: OnDown() > OnsingleTapUp() > OnsingleTapConfirmed()
  • 双击屏幕时: OnDown() > onSingleTapUp() > onDoubleTap() > onDoubleTapEvent() > onDown() > onDoubleTapEvent()

1.1.3 GestureDetector.OnContextClickListener

作用: 用来监听鼠标右键按下的事件,我们可以在这些操作的回调方法中处理我们自己的逻辑。

具体实现方法:
见下面代码,注释已经写的很清楚了。

private static class MyGestureDetectorContextClickListener implements GestureDetector.OnContextClickListener{

    /**
     * 当鼠标/触摸板,右键点击时候的回调。
     * @param motionEvent
     * @return
     */
    @Override
    public boolean onContextClick(MotionEvent motionEvent) {
        return false;
    }
}

注意事项:
1、OnContextClickListener接口最低支持的API是23。

1.1.4 GestureDetector.SimpleOnGestureListener(实现类)

SimpleOnGestureListener实现了上述三个接口,我们可以直接使用此类,并重写我们想要的方法就行。

GestureDetector.SimpleOnGestureListener simpleOnGestureListener = new GestureDetector.SimpleOnGestureListener(){

    @Override
    public boolean onDown(MotionEvent e) {
        return true;
    }
};

1.2 GestureDetector使用方法

GestureDetector可以作用于多个地方,比如Activity的OnTouchEvent()方法,View的onTouch()方法和onTouchEvent()方法上,其中,以 后两者使用较多。
这里以View.onTouchEvent()方法举例:

1.2.1 在View的初始化时初始化GestureDetector

public GestureDetectorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
}

private void init(Context context){
    //传入View方便对View进行操作
    mGestureDetector = new GestureDetector(context, new MyGestureDetectorListener(this));
    //设置双击事件监听
    mGestureDetector.setOnDoubleTapListener(new MyGestureDetectorDoubleTapListener());
}

1.2.1 在View的onTouchEvent()方法中,将触摸事件交给GestureDetector去处理

@Override
public boolean onTouchEvent(MotionEvent event) {
    return mGestureDetector.onTouchEvent(event);
}

此处展示一个使用示例,仅仅监听了OnGestureListeneronScroll()方法:

监听onScroll()

1.3 小结

GestureDetector可以很方便的为我们检测各种触摸事件,我们只需要再相应的回调方法中处理我们自己的逻辑即可,可以说是触摸辅助类第一神器!

2、ViewDragHelper(子控件拖拽帮手)

顾名思义,ViewDragHelper肯定是用来帮助View进行Drag(拖拽)的。它主要是用来给ViewGroup处理它内部子View的拖拽动作使用的,有了它,我们可以省去写ViewGroup中烦人的触摸拦截方法(onInterceptTouchEvent())和子控件的onTouchEvent()方法了!

2.1 相关方法及接口介绍

2.1.1 ViewDragHelper.Callback

ViewDragHelper的主要方法都在它内部类ViewDragHelper.Callback中,这里,我写了一个类MyViewDragHelperCallback实现了ViewDragHelper.Callback中的比较重要的方法,每个方法的具体作用,都已在注释中写明:

private static class MyViewDragHelperCallback extends ViewDragHelper.Callback{

    //传入父ViewGroup和ViewDragHelper,便于对子View的操作进行判断
    private RelativeLayout mViewGroup;
    private ViewDragHelper mDragHelper;

    public MyViewDragHelperCallback(ViewDragHelper dragHelper, RelativeLayout view){
        this.mDragHelper = dragHelper;
        this.mViewGroup = view;
    }


    /**
     * 根据传入的child确定是否需要捕获此child(对它进行操作)
     * @param child 被触摸的子View
     * @param pointerId 按下手指的id,一般多点触摸时会用来判断是哪根手指触摸
     * @return 返回true表示将要捕获这个child
     */
    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        return true;
    }

    /**
     * 返回被横向移动的子控件child的上坐标top,和移动距离dy,我们可以根据这些值来返回child的新的top。
     * @param child
     * @param top 拖拽动作后系统建议的距离父布上侧的距离
     * @param dy Y轴方向拖拽移动的距离
     * @return 返回拖拽后child距离父布局上侧的距离
     */
    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {
        return top;
    }

    /**
     * 返回被横向移动的子控件child的左坐标left,和移动距离dx,我们可以根据这些值来返回child的新的left。
     * @param child
     * @param left 拖拽动作后系统建议的距离父布局左侧的距离
     * @param dx X轴方向拖拽移动的距离
     * @return 返回拖拽后child距离父布局左侧的距离
     */
    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        return left;
    }

    /**
     * 这个用来控制垂直移动的边界范围,单位是像素。
     * @param child
     * @return 返回值大于0时才能捕获子View
     */
    @Override
    public int getViewVerticalDragRange(View child) {
        return super.getViewVerticalDragRange(child);
    }

    /**
     * 这个用来控制横向移动的边界范围,单位是像素。
     * @param child
     * @return 返回值大于0时才能捕获子View
     */
    @Override
    public int getViewHorizontalDragRange(View child) {
        return super.getViewHorizontalDragRange(child);
    }

    /**
     * 当releasedChild被释放的时候回调
     * @param releasedChild
     * @param xvel x轴方向的加速度
     * @param yvel y轴方向的加速度
     */
    @Override
    public void onViewReleased(View releasedChild, float xvel, float yvel) {
        super.onViewReleased(releasedChild, xvel, yvel);
    }

    /**
     * 若ViewDragHelper设置了setEdgeTrackingEnabled()此方法,则调用此方法
     * @param edgeFlags 边缘触摸方向
     * @param pointerId
     */
    @Override
    public void onEdgeDragStarted(int edgeFlags, int pointerId) {
        super.onEdgeDragStarted(edgeFlags, pointerId);
    }
}

2.1.2 ViewDragHelper.captureChildView()

直接对子View进行捕获,可以绕开Callback.tryCaptureView()方法。

2.1.3 ViewDragHelper.setEdgeTrackingEnabled()

设置ViewGroup的边缘可以被拖拽,可以设置左、上、右、下四个方向或者任意几个方向,一般配合Callback.onEdgeDragStarted()ViewDragHelper.captureChildView()使用(后面有示例)。

2.1.4 ViewDragHelper.settleCapturedViewAt()

对ViewGroup的某子View进行回弹处理,需要配合ViewGroup的computeScroll()方法和invalidate()方法。

2.2 ViewDragHelper使用方式

2.2.1 初始化ViewDragHelper

在ViewGroup的初始化时,通过ViewDragHelper的静态初始化方法create()进行创建:

public ViewDragHelperView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
}

private void init(){
    mViewDragHelper = ViewDragHelper.create(this,1.0f, new MyViewDragHelperCallback(this));
}

创建时的三个参数的意义分别是:

  • forParent:给哪个ViewGroup使用的,一般传入当前的ViewGroup;
  • sensitivity:拖拽灵敏度,传入值越大,灵敏度越小,一般传入1.0f即可;
  • ViewDragHelperCallback:ViewDragHelper的实现类Callback,基本所有的拖拽后的回调都在此方法内。

2.2.2 重写onInterceptTouchEvent()

重写ViewGroup的onInterceptTouchEvent()方法,把它交给ViewDragHelper的shouldInterceptTouchEvent()来处理。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return mViewDragHelper.shouldInterceptTouchEvent(ev);
}

注意:ViewDragHelper只对ViewGroup有效,并且它也不能直接操作子View,而是借助的ViewGroup!

2.2.3 重写ViewGroup的onTouchEvent()

重写ViewGroup的onTouchEvent(),把它交给ViewDragHelper的processTouchEvent()来处理,并返回true

@Override
public boolean onTouchEvent(MotionEvent event) {
    mViewDragHelper.processTouchEvent(event);
    return true;
}

使用方式是非常简单的,最重要的是在Callback中实现自己的拖拽逻辑。

2.3 使用场景简介

使用场景都是在ViewDragHelper的实现类Callback中进行处理的。

2.3.1 对子View进行拖拽到任意位置

效果展示:

捕捉子View

通过重写tryCaptureView()clampViewPositionVertical()clampViewPositionHorizontal()对子View进行拖拽处理。
其中,tryCaptureView()用来判断是否是需要拖拽的子View,clampViewPositionVertical()clampViewPositionHorizontal()用来返回子View拖拽后的位置。

示例代码表示,可以拖拽任意子View到任意位置,但是不能超过父ViewGroup的左侧和右侧:

    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        //直接返回true,表示可以拖拽任意子View
        return true;
    }

    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {
        //直接返回top,不对垂直方向的拖拽进行拦截,交给ViewDragHelper去处理
        return top;
    }

    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        //不能超过父View左侧
        if(left < mViewGroup.getPaddingLeft()){
            left = mViewGroup.getPaddingLeft();
        }

        //不能超过父View右侧
        if(left + child.getWidth() > mViewGroup.getWidth() - mViewGroup.getPaddingRight()){
            left = mViewGroup.getWidth() - mViewGroup.getPaddingRight() - child.getWidth();
        }

        return left;
    }

2.3.2 对ViewGroup边缘触摸事件进行捕获子View的行为

效果展示:

边缘捕获

通过ViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)对左侧边缘进行触摸,通过Callback.onEdgeDragStarted()ViewDragHelper.captureChildView()对需要捕获的子View使用操作。

示例代码表示,可以在左侧或者上侧边缘拖拽id为tv2的子View:

ViewDragHelper:

//对屏幕左侧或上侧进行边缘触摸
mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT|ViewDragHelper.EDGE_TOP);

Callback:

@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
    super.onEdgeDragStarted(edgeFlags, pointerId);
    View childView = mViewGroup.findViewById(R.id.tv2);
    mDragHelper.captureChildView(childView,pointerId);
}

2.3.3 子View拖拽,在松手后回到原位

效果展示:

回弹效果

这里需要重写ViewGroup的computeScroll()方法,并且调用ViewDragHelper的continueSettling()方法对子View滚动位置实时刷新。
除此之外,还需要在tryCaptureView()的时候记录子View的初始lefttop,并且调用ViewDragHelpersettleCapturedViewAt()方法对子View进行复位。

示例代码是复位子View的id为tv1的View:

ViewGroup:
对子View的位置进行判断,并不断刷新。

@Override
public void computeScroll() {
    if(mViewDragHelper.continueSettling(true)){
        invalidate();
    }
    super.computeScroll();
}

Callback:

//捕获View时记录其left和top
@Override
public boolean tryCaptureView(View child, int pointerId) {
    if(child.getId() == R.id.tv1){
        originLeft = child.getLeft();
        originTop = child.getTop();
    }

    return true;
}

//松手时交给ViewDragHelper的settleCapturedViewAt()进行复位。
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
    super.onViewReleased(releasedChild, xvel, yvel);
    if(releasedChild.getId() == R.id.tv1){
        mDragHelper.settleCapturedViewAt(originLeft,originTop);
        mViewGroup.invalidate();
    }
}

2.4 注意事项

在我们的举例中,如果你将我们的子控件换成Button或者将子控件的clickable设置为true(子View能消费触摸事件),你会发现子View无法被拖拽了!原因在于在ViewGroup的onInterceptTouchEvent()方法中,我们返回的是ViewDragHelper.shouldInterceptTouchEvent(ev),这样会导致ViewGroup的onTouchEvent()不被调用,这样就导致我们的ViewDragHelper.processTouchEvent()不被调用!

如果将onInterceptTouchEvent()的返回值直接为true的话,又会导致子View的点击事件被拦截不被触发!

查看ViewDragHelper.shouldInterceptTouchEvent(ev)的源码:

public boolean shouldInterceptTouchEvent(MotionEvent ev) {
    final int action = MotionEventCompat.getActionMasked(ev);
    switch (action) {
        ...
        case MotionEvent.ACTION_MOVE: {          
            final int pointerCount = ev.getPointerCount();
            for (int i = 0; i < pointerCount; i++) {           
                final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
                            toCapture);
                final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
                // 如果getViewHorizontalDragRange和getViewVerticalDragRange的返回值都为0,则break
                if (horizontalDragRange == 0 && verticalDragRange == 0) {
                    break;
                }
                
                // tryCaptureViewForDrag方法中会设置mDragState=STATE_DRAGGING
                if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
                    break;
                }
            }
            break;
        }
    }
    ...
    return mDragState == STATE_DRAGGING;
}

ViewDragHelper.shouldInterceptTouchEvent(ev)的返回为true的条件是ViewDragHelper.mDragState == STATE_DRAGGING,然而mDragState是在tryCaptureViewForDrag()方法中被设置为STATE_DRAGGING的。其中,若horizontalDragRangeverticalDragRange一直为0,则mDragState无法设置为STATE_DRAGGING。并且horizontalDragRangeverticalDragRange是在ViewDragHelper.getViewVerticalDragRange()ViewDragHelper.getViewHorizontalDragRange()设置的,因此,只要这两个方法的返回值大于0就可以正常捕获了!

    @Override
    public int getViewVerticalDragRange(View child) {
        return 1;
    }

    /**
     * 这个用来控制横向移动的边界范围,单位是像素。
     * @param child
     * @return 返回值大于0时才能捕获子View
     */
    @Override
    public int getViewHorizontalDragRange(View child) {
        return 1;
    }

2.5 ViewDragHelper的一些其它回调及回调调用顺序

2.5.1 其它回调方法

/**
 * mDragState改变时回调
 * STATE_IDLE:所有的View处于静止空闲状态
 * STATE_DRAGGING:某个View正在被用户拖动(用户正在与设备交互)
 * STATE_SETTLING:某个View正在安置状态中(用户并没有交互操作),就是自动滚动的过程中
 * @param state
 */
@Override
public void onViewDragStateChanged(int state) {
    super.onViewDragStateChanged(state);

}

/**
 * 当捕获的子View的位置发生改变时回调
 * @param changedView
 * @param left
 * @param top
 * @param dx
 * @param dy
 */
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
    super.onViewPositionChanged(changedView, left, top, dx, dy);
}

/**
 * 当子View被捕获时回调
 * @param capturedChild
 * @param activePointerId
 */
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
    super.onViewCaptured(capturedChild, activePointerId);
}

/**
 * 当触摸到边界时回调。
 * @param edgeFlags
 * @param pointerId
 */
@Override
public void onEdgeTouched(int edgeFlags, int pointerId) {
    super.onEdgeTouched(edgeFlags, pointerId);
}

/**
 *
 * @param edgeFlags
 * @return true的时候会锁住当前的边界,false则unLock。
 */
@Override
public boolean onEdgeLock(int edgeFlags) {
    return super.onEdgeLock(edgeFlags);
}

/**
 * 改变同一个坐标(x,y)去寻找captureView位置的方法。(具体作用暂时未知)
 * @param index
 * @return
 */
@Override
public int getOrderedChildIndex(int index) {
    return super.getOrderedChildIndex(index);
}

2.5.2 回调调用顺序:

shouldInterceptTouchEvent:

  • ACTION_DOWNgetOrderedChildIndex(findTopChildUnder) > onEdgeTouched()
  • ACTION_MOVEgetOrderedChildIndex(findTopChildUnder) > getViewHorizontalDragRange() & getViewVerticalDragRange(checkTouchSlop)(MOVE中可能不止一次) > clampViewPositionHorizontal() & clampViewPositionVertical() > onEdgeDragStarted() > tryCaptureView() > onViewCaptured() > onViewDragStateChanged()

processTouchEvent:

  • ACTION_DOWNgetOrderedChildIndex(findTopChildUnder) > tryCaptureView() > onViewCaptured() > onViewDragStateChanged() > onEdgeTouched()
  • ACTION_MOVESTATE == DRAGGING > STATE!=DRAGGING > onEdgeDragStarted > getOrderedChildIndex(findTopChildUnder) > getViewHorizontalDragRange& getViewVerticalDragRange(checkTouchSlop) > tryCaptureView() > onViewCaptured() > onViewDragStateChanged()

2.6 小结

ViewDragHelper是一个强大的拖拽子View的辅助神器,大家用好这个类,可以大大提高我们的触摸手势的效率!

3、ItemTouchHelper(item触摸帮手)

ItemTouchHelper是一个触摸item时对item进行处理的类。既然涉及到条目,那肯定是和一些和item有关的控件有关了,没错,这个类就是一个针对RecyclerView的工具类,用来处理RecyclerView内部的条目拖拽和滑动事件的,它可以让条目拖拽到一个新的位置,也可以让条目滑动出来删除等等场景!

3.1 相关方法和接口介绍

ItemTouchHelper类的具体实现方法都在它的内部类ItemTouchHelper.Callback中,ItemTouchHelper.Callback中有三个必须重写的方法,也是核心方法。
我写了一个类MyItemTouchHelperCallback继承ItemTouchHelper.Callback,它们的意义都在注释中已标明:

public static class MyItemTouchHelperCallback extends ItemTouchHelper.Callback{

    //RecyclerView的Adapter
    private ItemTouchHelperAdapter mAdapter;

    public MyItemTouchHelperCallback(ItemTouchHelperAdapter adapter){
        this.mAdapter = adapter;
    }

    /**
     * 允许哪个方向的拖拽和滑动,一般配合makeMovementFlags(int,int)去使用
     * @param recyclerView
     * @param viewHolder
     * @return
     */
    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        //允许上下拖拽,和从右向左滑动
        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        int swipeFlags = ItemTouchHelper.LEFT;
        //计算拖拽和滑动的方向
        return makeMovementFlags(dragFlags,swipeFlags);
    }

    /**
     * 拖拽一个item到新位置时会调用此方法,一般配合Adapter的notifyItemMoved()方法来交换两个ViewHolder的位置
     * @param recyclerView
     * @param viewHolder
     * @param target
     * @return true表示已经到达移动目的地
     */
    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        //交换item位置
        Collections.swap(mAdapter.mDatas,viewHolder.getAdapterPosition(),target.getAdapterPosition());
        mAdapter.notifyItemMoved(viewHolder.getAdapterPosition(),target.getAdapterPosition());
        return true;
    }

    /**
     * 当滑动到一定程度时,松手会继续滑动,然后调用此方法,反之item会回到原位,不调用此方法
     * @param viewHolder
     * @param direction 滑动的方向
     */
    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        Log.e("zw", "direction : " + direction);
        //删除此item
        mAdapter.mDatas.remove(viewHolder.getAdapterPosition());
        mAdapter.notifyItemRemoved(viewHolder.getAdapterPosition());
    }
}

ItemTouchHelper.Callback中其它可以选择重写的方法:

    /**
     * 是否长按时才使拖拽生效
     * @return 默认返回true
     */
    @Override
    public boolean isLongPressDragEnabled() {
        return super.isLongPressDragEnabled();
    }

    /**
     * 是否可以进行滑动时的删除动作,也就是是否能调用onSwiped()方法
     * @return 默认返回true
     */
    @Override
    public boolean isItemViewSwipeEnabled() {
        return super.isItemViewSwipeEnabled();
    }

    /**
     * 静止状态变为拖拽或者滑动的时候会回调
     * @param viewHolder
     * @param actionState 当前的状态
     */
    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
        super.onSelectedChanged(viewHolder, actionState);
    }

    /**
     * 当用户操作完毕某个item并且其动画也结束后会调用该方法,一般我们在该方法内恢复ItemView的初始状态,防止由于复用而产生的显示错乱问题。
     * @param recyclerView
     * @param viewHolder
     */
    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        super.clearView(recyclerView, viewHolder);
    }

    /**
     * 实现我们自定义的交互规则或者自定义的动画效果
     * @param c
     * @param recyclerView
     * @param viewHolder
     * @param dX
     * @param dY
     * @param actionState
     * @param isCurrentlyActive
     */
    @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);
    }

还有一些方法没有做过多研究,这里就不列举了。

注意事项:调用onSwiped()方法时,如果没有配合RecyclerViewAdapternotifyItemRemoved()方法则会让Item的内容给移除出去,但是此Item还在原地,也就是会留下一个空白的view!

3.2 ItemTouchHelper的使用方式

3.2.1 RecyclerView初始化

实现RecyclerViewAdapterViewHolder类,RecyclerView设置LayoutManager等,很常规的写法,这里就不贴代码了。

3.2.1 RecyclerView设置ItemTouchHelper

ItemTouchHelperAdapter.MyItemTouchHelperCallback touchHelperCallback
        = new ItemTouchHelperAdapter.MyItemTouchHelperCallback(adapter);

ItemTouchHelper itemTouchHelper = new ItemTouchHelper(touchHelperCallback);

itemTouchHelper.attachToRecyclerView(recyclerView);

3.3 ItemTouchHelper的使用场景简介

3.3.1 实现上下拖拽换位置功能和向左滑动删除功能

此功能需要重写ItemTouchHelper.CallbackgetMovementFlags()方法、onMove()方法和onSwiped()方法,并且要配合RecyclerViewAdapternotifyItemMoved()notifyItemRemoved()方法使用。

ItemTouchHelper.Callback关键代码:

    //RecyclerView的Adapter
    private ItemTouchHelperAdapter mAdapter;

    public MyItemTouchHelperCallback(ItemTouchHelperAdapter adapter){
        this.mAdapter = adapter;
    }

    /**
     * 允许哪个方向的拖拽和滑动,一般配合makeMovementFlags(int,int)去使用
     * @param recyclerView
     * @param viewHolder
     * @return
     */
    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        //允许上下拖拽,和从右向左滑动
        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        int swipeFlags = ItemTouchHelper.LEFT;
        //计算拖拽和滑动的方向
        return makeMovementFlags(dragFlags,swipeFlags);
    }

    /**
     * 拖拽一个item到新位置时会调用此方法,一般配合Adapter的notifyItemMoved()方法来交换两个ViewHolder的位置
     * @param recyclerView
     * @param viewHolder
     * @param target
     * @return true表示已经到达移动目的地
     */
    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        //交换item位置
        Collections.swap(mAdapter.mDatas,viewHolder.getAdapterPosition(),target.getAdapterPosition());
        mAdapter.notifyItemMoved(viewHolder.getAdapterPosition(),target.getAdapterPosition());
        return true;
    }

    /**
     * 当滑动到一定程度时,松手会继续滑动,然后调用此方法,反之item会回到原位,不调用此方法
     * @param viewHolder
     * @param direction 滑动的方向
     */
    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        Log.e("zw", "direction : " + direction);
        //删除此item
        mAdapter.mDatas.remove(viewHolder.getAdapterPosition());
        mAdapter.notifyItemRemoved(viewHolder.getAdapterPosition());
    }

实现效果:

拖拽换位置和滑动删除功能

3.3.1 向左滑动删除时带有动画功能(透明度)

此示例为向左滑动时,先显示黑色的View,当黑色的TextView显示完全时,再向右滑动时,更改黑色TextView的文字颜色透明度。

先上显示效果:

带动画的效果

更改我们的布局文件,在item布局的屏幕右边放置我们的TextView,布局文件如下:

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:padding="10dp"
        android:gravity="center"
        android:id="@+id/tv_item"
        android:textStyle="bold"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintWidth_percent="0.5"
        />

    <TextView
        android:id="@+id/tv_item1"
        android:layout_width="wrap_content"
        android:layout_height="50dp"
        android:background="#000000"
        android:gravity="center"
        android:padding="10dp"
        android:text="继续滑动删除"
        android:textColor="@android:color/white"
        android:textStyle="bold"
        app:layout_constraintLeft_toRightOf="parent"
         />

</android.support.constraint.ConstraintLayout>

其中用到了ConstraintLayout,这个控件对于布局十分的方便,并且性能也很优秀,关于此控件的更多使用方法可以参考我这篇博客内容: ConstraintLayout——约束性布局学习

这时候我们会发现向右侧滑动时,第二个TextView根本出不来,系统调用的是当前屏幕可见部分布局的setTranslationX()方法,因此无论如何,我们的第二个TextVeiw都不会显示,因此我们要将setTranslationX()改变成scrollTo()方法。

另外,我们还要重写clearView()方法,来将有动画效果的条目给复位到初始化状态,以免复用的时候产生显示误差。

ItemTouchHelper.Callback关键代码:

@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
    //仅仅针对Item的滑动事件
    if(actionState == ItemTouchHelper.ACTION_STATE_SWIPE){
        TextView alphaView = viewHolder.itemView.findViewById(R.id.tv_item1);
        int viewWidth = alphaView.getWidth();
        if(Math.abs(dX) < viewWidth){
            //滚动
            viewHolder.itemView.scrollTo((int) -dX,0);

        } else if(Math.abs(dX) < recyclerView.getWidth() / 2){
            //滑动的透明度
            float childAlpha = Math.abs(dX) - viewWidth;
            float fatherAlpha = recyclerView.getWidth() / 2 - viewWidth;
            float alpha = childAlpha / fatherAlpha;

            int color = Color.argb((int)(alpha * 255),255,255,255);
            alphaView.setTextColor(color);
        }
    } else {
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
    }
}

@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
    super.clearView(recyclerView, viewHolder);
    //复位
    viewHolder.itemView.setScrollX(0);
    TextView alphaView = viewHolder.itemView.findViewById(R.id.tv_item1);
    alphaView.setTextColor(Color.parseColor("#00FFFFFF"));
}

3.4 小结

ItemTouchHelper是RecyclerView自带的一个帮助我们实现拖拽和滑动的工具类,通过这个类我们可以很轻松的实现在RecyclerView上炫酷的拖拽和滑动效果!

总结

上面介绍了三种和触摸有关的辅助类:

  • GestureDetector:用来监听我们触摸屏幕的手势。

  • ViewDragHelper:帮助我们处理子控件的拖拽事件。

  • ItemTouchHelper:RecyclerView上帮助我们拖拽和滑动子条目的利器。

相信通过上述的例子,你应该也学会了怎么使用它们,在工作中用上这些辅助类,可以让我们的工作效率大大提高,实现事半功倍的效果!

最后,本示例所有效果,见此GitHub Demo

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容