Android 事件分发ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 )

Posted 韩曙亮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 事件分发ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 )相关的知识,希望对你有一定的参考价值。

android 事件分发 系列文章目录


【Android 事件分发】事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 )
【Android 事件分发】事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )
【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 一 )
【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 二 )
【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 三 )
【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 四 | View 事件传递机制 )
【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 五 )
【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 六 )
【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 七 )

【Android 事件分发】ItemTouchHelper 简介 ( 拖动/滑动事件 | ItemTouchHelper.Callback 回调 )
【Android 事件分发】ItemTouchHelper 实现侧滑删除 ( 设置滑动方向 | 启用滑动操作 | 滑动距离判定 | 滑动速度判定 | 设置动画时间 | 设置侧滑触发操作 )
【Android 事件分发】ItemTouchHelper 实现拖动排序 ( 设置滑动方向 | 启启用长按拖动功能 | 拖动距离判定 | 设置拖动触发操作 )

【Android 事件分发】ItemTouchHelper 事件分发源码分析 ( 绑定 RecyclerView )
【Android 事件分发】ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 )






一、OnItemTouchListener 事件监听器引入



在上一篇博客 【Android 事件分发】ItemTouchHelper 事件分发源码分析 ( 绑定 RecyclerView ) 分析了 ItemTouchHelper 添加时 , 调用了 mItemTouchHelper.attachToRecyclerView(recycler_view) 方法 , 将 ItemTouchHelper 与 RecyclerView 进行关联 , 并在 attachToRecyclerView 方法的最后 , 调用了 setupCallbacks 方法 ;

	// 该方法与 destroyCallbacks 方法相对应
    private void setupCallbacks() {
    	// 配置相关  
        ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
        mSlop = vc.getScaledTouchSlop();
        // 设置 RecyclerView 条目中的装饰 , 可以在条目组件 底部 上层 绘制 Canvas 图形 
        // ItemTouchHelper 继承 RecyclerView.ItemDecoration
        mRecyclerView.addItemDecoration(this);
        // 添加了每个条目上的触摸监听器 mOnItemTouchListener 
        // 该监听器是定义在 ItemTouchHelper 中的成员变量 
        mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
        mRecyclerView.addOnChildAttachStateChangeListener(this);
        startGestureDetection();
    }

上一篇博客分析到 mRecyclerView.addItemDecoration(this) 位置 , 本篇博客重点分析 mRecyclerView.addOnItemTouchListener(mOnItemTouchListener) 中的 mOnItemTouchListener , 这是 RecyclerView 的 ItemTouchHelper 的核心 ;


OnItemTouchListener 是 RecyclerView 中定义的作用与条目组件的触摸监听器 , 主要是拦截触摸事件方法 onInterceptTouchEvent 和 消费触摸事件方法 onTouchEvent ;

public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {
    public interface OnItemTouchListener {
        boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
        void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
        void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);
    }
}

ItemTouchHelper 事件分发 , 分析手指触摸的 按下 , 移动 , 抬起 事件 ;

在 OnItemTouchListener 触摸监听器中 , onInterceptTouchEvent 方法处理的是事件拦截机制 , onTouchEvent 方法是最终消费事件的方法 ;

onInterceptTouchEvent 事件拦截 中 , 只拦截 MotionEvent.ACTION_DOWN / MotionEvent.ACTION_CANCEL / MotionEvent.ACTION_UP 3 3 3 种事件 , 不拦截 MotionEvent.ACTION_MOVE 事件 ;

onTouchEvent 事件消费 中 , 才处理 MotionEvent.ACTION_MOVE 事件 ;

ItemTouchHelper 没有对子控件进行事件分发 , 其只识别一层组件 , 如果内部有多层组件 , 内层的组件无法分发到事件 ;





二、OnItemTouchListener 触摸事件拦截方法 onInterceptTouchEvent




1、onInterceptTouchEvent 方法简介


在 ItemTouchHelper 中定义的成员变量

    private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
  		// 拦截触摸事件 , 处理拦截机制 
        @Override
        public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
                @NonNull MotionEvent event) {
		}
}

中实现的 RecyclerView.OnItemTouchListener 接口的 onInterceptTouchEvent 方法 , 主要是用于作用与条目上的触摸事件的拦截 ;

注意此处拦截的动作 , 只拦截 DOWN / UP / CANCEL 三种动作 , MOVE 动作不拦截 , 取消操作很少遇到 , 因此 , 拦截机制中 , 只负责拦截手指按下 和 抬起 操作 , 在 ItemTouchHelper 的业务逻辑中 , 不需要处理移动事件 ;


2、处理按下事件


当检测到 MotionEvent.ACTION_DOWN 按下操作时 , 获取按下的 XY 坐标 , 并进行滑动速度检测 ;

            // 注意此处拦截的动作 , 只拦截 DOWN / UP / CANCEL 三种动作 , MOVE 动作不拦截 
            // 取消操作很少遇到 
            // 因此 , 拦截机制中 , 只负责拦截手指按下 和 抬起 操作 
            // 在 ItemTouchHelper 的业务逻辑中 , 不需要处理移动事件 
            if (action == MotionEvent.ACTION_DOWN) {
            	// 按下操作 , 得到初始 XY 坐标位置 
                mActivePointerId = event.getPointerId(0);
                mInitialTouchX = event.getX();
                mInitialTouchY = event.getY();

				// 滑动速度检测 
                obtainVelocityTracker();

ViewHolder mSelected = null; 成员是当前正在点击的 ViewHolder 条目 , 如果该 mSelected 成员为空 , 则执行后续操作 ;

                // mSelected 是当前正在点击的条目的 ViewHolder 
                if (mSelected == null) {
                	// 恢复动画 , 查找手指按下的 View 子组件 , 该子组件时 RecyclerView 中的一个条目 
                	// 用户按下 RecyclerView 中的某个条目 
					// findAnimation 方法用于找到按下的条目 View , 并设置给 RecoverAnimation 恢复动画 
                    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);
                        }
                        // 为动画选择 item 项 
                        select(animation.mViewHolder, animation.mActionState);
                        // 计算当前移动的位置 , 初始位置 与 最后一次事件的位置 偏移值 .  
                        updateDxDy(event, mSelectedFlags, 0);
                    }
                }

先恢复动画 , 查找手指按下的 View 子组件 , 该子组件是 RecyclerView 中的一个条目 , 用户按下 RecyclerView 中的某个条目 , findAnimation 方法用于找到按下的条目 View , 并设置给 RecoverAnimation 恢复动画 ;


3、findAnimation 方法


在 findAnimation 方法中 , 先调用了 findChildView 方法 , 查找手指按下的 View 子组件 , 该子组件是 RecyclerView 中的一个条目 ;

        // 找到手指按下所在位置的条目的 View 组件
        // 查找手指按下的 View 子组件 , 该子组件时 RecyclerView 中的一个条目 
        View target = findChildView(event);

找到该条目对应的 View 组件后 , 遍历恢复动画 , 动画中有 mViewHolder 成员 , mViewHolder 中有 itemView 成员 , 设置 anim.mViewHolder.itemView 为手指按下的子组件 , 即设置该动画作用于 RecyclerView 的哪个条目上 ;

        for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) {
            final RecoverAnimation anim = mRecoverAnimations.get(i);
            if (anim.mViewHolder.itemView == target) {
                return anim;
            }
        }

findAnimation 方法完整代码注释 :

	// 该方法作用 : 
	// 用户按下 RecyclerView 中的某个条目 
	// 该方法用于找到按下的条目 View , 并设置给 RecoverAnimation 恢复动画 
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    RecoverAnimation findAnimation(MotionEvent event) {
        if (mRecoverAnimations.isEmpty()) {
            return null;
        }
        // 找到手指按下所在位置的条目的 View 组件
        // 查找手指按下的 View 子组件 , 该子组件时 RecyclerView 中的一个条目 
        View target = findChildView(event);
        // 遍历恢复动画 
        // 动画中有 mViewHolder 成员 , mViewHolder 中有 itemView 成员 
        // 设置  anim.mViewHolder.itemView 为手指按下的子组件 
        // 即设置该动画作用于 RecyclerView 的哪个条目上 ; 
        for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) {
            final RecoverAnimation anim = mRecoverAnimations.get(i);
            if (anim.mViewHolder.itemView == target) {
                return anim;
            }
        }
        return null;
    }

4、findChildView 方法


根据按下的 X, Y 坐标 , 查找对应的条目组件 , 先获取触摸的 XY 坐标 ;

        final float x = event.getX();
        final float y = event.getY();

如果 mSelected 成员不为空 , 则直接使用 , 分支中直接返回了 ;

        if (mSelected != null) {
            final View selectedView = mSelected.itemView;
            if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) {
                return selectedView;
            }
        }

如果 mSelected 为空 , 则开始遍历进行检测 ;

        for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) {
            final RecoverAnimation anim = mRecoverAnimations.get(i);
            final View view = anim.mViewHolder.itemView;
            // 根据当前按下的坐标 , 找到列表条目对应的 View 组件 
            if (hitTest(view, x, y, anim.mX, anim.mY)) {
                return view;
            }
        }

findChildView 完整代码注释 :

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    View findChildView(MotionEvent event) {
        // first check elevated views, if none, then call RV
        // 根据按下的 X, Y 坐标 , 查找对应的条目 
        final float x = event.getX();
        final float y = event.getY();
        // 如果 mSelected 成员不为空 , 则直接使用 , 分支中直接返回了 
        if (mSelected != null) {
            final View selectedView = mSelected.itemView;
            if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) {
                return selectedView;
            }
        }
        // 如果 mSelected 为空 , 则开始遍历进行检测 
        for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) {
            final RecoverAnimation anim = mRecoverAnimations.get(i);
            final View view = anim.mViewHolder.itemView;
            // 根据当前按下的坐标 , 找到列表条目对应的 View 组件 
            if (hitTest(view, x, y, anim.mX, anim.mY)) {
                return view;
            }
        }
        return mRecyclerView.findChildViewUnder(x, y);
    }

5、动作取消


当拦截的动作是 MotionEvent.ACTION_CANCEL 或 action == MotionEvent.ACTION_UP 动作时 , 说明用户取消了该动作 , 将选择的组件置空 ;

            } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
                mActivePointerId = ACTIVE_POINTER_ID_NONE;
                // 抬起 / 取消 时 , 选择项 置空 . 
                select(null, ACTION_STATE_IDLE);
            }

6、动作完成


ACTIVE_POINTER_ID_NONE 表示是否完成了滑动 , 如果滑动完成 , 触发了侧滑事件 , 才会进入该 else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) 分支 , 如果滑动没有完成 , 滑到半路 , 松开手 , 条目组件缩回去了 , 则不会进入该分支 ;

            } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
            	// 该分支表示滑动操作完成的分支 
            	// 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);
                }
            }




三、ItemTouchHelper 涉及到的本博客相关源码



public class ItemTouchHelper extends RecyclerView.ItemDecoration
        implements RecyclerView.OnChildAttachStateChangeListener {
    /**
     * Currently selected view holder
     */
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    ViewHolder mSelected = null;

    /**
     * The diff between the last event and initial touch.
     * 最后的触摸事件和初始触摸事件之间的坐标差异 , 偏移值 .
     */
    float mDx;

    float mDy;

    private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
  		// 拦截触摸事件 , 处理拦截机制 
        @Override
        public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
                @NonNull MotionEvent event) {
            mGestureDetector.onTouchEvent(event);
            if (DEBUG) {
                Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event);
            }
            final int action = event.getActionMasked();
            // 注意此处拦截的动作 , 只拦截 DOWN / UP / CANCEL 三种动作 , MOVE 动作不拦截 
            // 取消操作很少遇到 
            // 因此 , 拦截机制中 , 只负责拦截手指按下 和 抬起 操作 
            // 在 ItemTouchHelper 的业务逻辑中 , 不需要处理移动事件 
            if (action == MotionEvent.ACTION_DOWN) {
            	// 按下操作 , 得到初始 XY 坐标位置 
                mActivePointerId = event.getPointerId(0);
                mInitialTouchX = event.getX();
                mInitialTouchY = event.getY();

				// 滑动速度检测 
                obtainVelocityTracker();
                // mSelected 是当前正在点击的条目的 ViewHolder 
                if (mSelected == null) {
                	// 恢复动画 , 查找手指按下的 View 子组件 , 该子组件时 RecyclerView 中的一个条目 
                	// 用户按下 RecyclerView 中的某个条目 
					// findAnimation 方法用于找到按下的条目 View , 并设置给 RecoverAnimation 恢复动画 
                    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);
                        }
                        // 为动画选择 item 项 
                        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) {
            	// 该分支表示滑动操作完成的分支 
            	// 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;
        }
    };


	// 该方法作用 : 
	// 用户按下 RecyclerView 中的某个条目 
	// 该方法用于找到按下的条目 View , 并设置给 RecoverAnimation 恢复动画 
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    RecoverAnimation findAnimation(MotionEvent event) {
        if (mRecoverAnimations.isEmpty()) {
            return null;
        }
        // 找到手指按下所在位置的条目的 View 组件
        // 查找手指按下的 View 子组件 , 该子组件时 RecyclerView 中的一个条目 
        View target = findChildView(event);
        // 遍历恢复动画 
        // 动画中有 mViewHolder 成员 , mViewHolder 中有 itemView 成员 
        // 设置  anim.mViewHolder.itemView 为手指按下的子组件 
        // 即设置该动画作用于 RecyclerView 的哪个条目上 ; 
        for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) {
            final RecoverAnimation anim = mRecoverAnimations.get(i);
            if (anim.mViewHolder.itemView == target) {
                return anim;
            }
        }
        return null;
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    View findChildView(MotionEvent event) {
        // first check elevated views, if none, then call RV
        // 根据按下的 X, Y 坐标 , 查找对应的条目 
        final float x = event.getX();
        final float y = event.getY();
        // 如果 mSelected 成员不为空 , 则直接使用 , 分支中直接返回了 
        if (mSelected != null) {
            final View selectedView = mSelected.itemView;
            if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) {
                return selectedView;
            }
        }
        // 如果 mSelected 为空 , 则开始遍历进行检测 
        for (int i = mRecoverAnimations.Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 三 )

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 二 )

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 一 )

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 七 )

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 六 )

Android 事件分发ItemTouchHelper 事件分发源码分析 ( 绑定 RecyclerView )