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);
}
此处展示一个使用示例,仅仅监听了OnGestureListener
的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进行拖拽到任意位置
效果展示:
通过重写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的初始left
和top
,并且调用ViewDragHelper
的settleCapturedViewAt()
方法对子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
的。其中,若horizontalDragRange
和verticalDragRange
一直为0,则mDragState
无法设置为STATE_DRAGGING
。并且horizontalDragRange
和verticalDragRange
是在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_DOWN
:getOrderedChildIndex(findTopChildUnder)
>onEdgeTouched()
-
ACTION_MOVE
:getOrderedChildIndex(findTopChildUnder)
>getViewHorizontalDragRange() & getViewVerticalDragRange(checkTouchSlop)
(MOVE中可能不止一次) >clampViewPositionHorizontal() & clampViewPositionVertical()
>onEdgeDragStarted()
>tryCaptureView()
>onViewCaptured()
>onViewDragStateChanged()
processTouchEvent:
-
ACTION_DOWN
:getOrderedChildIndex(findTopChildUnder)
>tryCaptureView()
>onViewCaptured()
>onViewDragStateChanged()
>onEdgeTouched()
-
ACTION_MOVE
:STATE == 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()
方法时,如果没有配合RecyclerViewAdapter
的notifyItemRemoved()
方法则会让Item的内容给移除出去,但是此Item还在原地,也就是会留下一个空白的view!
3.2 ItemTouchHelper的使用方式
3.2.1 RecyclerView
初始化
实现RecyclerViewAdapter
和ViewHolder
类,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.Callback
的getMovementFlags()
方法、onMove()
方法和onSwiped()
方法,并且要配合RecyclerViewAdapter
的notifyItemMoved()
和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,布局文件如下:
其中用到了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