最近项目有需要弄一个可以像手机QQ会话页一样可以滑动的小菜单,每一个Item当用户在向左滑动的时候右侧会出现一个小菜单当时就想在也不是很难心想着找个开源的使用就好呢,但是我的项目是用的RecyclerView网上基本没有类似的没办法只能自己弄一个。
先说一说我的实现原理我把每个Item看成一个LinearLayout它包含两个子控件一个是Item要显示的内容还有一个当然就是我们的右侧菜单了,这个顶部LinearLayout就是包含内容与菜单的容器,通过滚动这个容器的scrollX来显示我们的菜单,原理还是不难的,我把整个菜单的显示与隐藏把封闭在ItemSlideHelper里面这样想换一种方法实现也不用去别的代码很方便,当然ItemSlidHelper是要实现RecyclerView.OnItemTouchListener的因为我们的基本操作都是基于Touch事件的,有兴趣的可以看看,有BUG可以回复我。
关于RecyclerView.OnItemTouchListener的几个方法我也学习了下也不是很难主要是拦截与操作这两个东西一定要配合好,还有就是RecyclerView的滚动状态,因为在RecyclerVIew滚动的时候我们的滑动菜单是不能操作的不然就会产生混乱,在项目开发的时候由于我的Item是有onClick事件的,那么在用户滑出菜单的时候也要把onClick事件给拦截但是又不能拦截菜单的Onclick事件我是通过容器的rect与scrollX的偏移来解决这个问题的可以看源码就知道呢。下面是拦截代码。
@Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { if(!mCallback.isEnable()) return false; int action = MotionEventCompat.getActionMasked(e); int x = (int) e.getX(); int y = (int) e.getY(); /* * 当我们没有发生drag事件的时候cancel或up事件会发生interceptTouchEvent里面,如果TargetView等于空的时候直接 * 返回false,不拦截事件 * */ if(action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) if(mTargetView == null) return false; boolean needIntercept = false; switch (action) { case MotionEvent.ACTION_DOWN: mActivePointerId = MotionEventCompat.getPointerId(e, 0); mLastX = (int) e.getX(); mLastY = (int) e.getY(); //查找需要显示菜单的view; mTargetView = mCallback.findTargetView(x, y); /* * 如果正在动画则拦截事件 * */ if (mExpandAndCollapseAnim != null) { //mExpandAndCollapseAnim.cancel(); mExpandAndCollapseAnim = null; needIntercept = true; } break; case MotionEvent.ACTION_MOVE: int deltaX = (x - mLastX); int deltaY = (y - mLastY); if(Math.abs(deltaY) > Math.abs(deltaX)) return false; //如果移动距离达到要求,则拦截 needIntercept = mIsDragging = mTargetView != null && Math.abs(deltaX) >= mTouchSlop; break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: /* * 当一个up事件发生在正常的范围内且scrollX等于scrollRange则折叠view并拦截UP事件 * 防止view响应点击事件 * */ if(isExpanded()){ if (inView(x, y)) { //拦截事件,防止targetView执行onClick事件 needIntercept = true; }else{ //如果走这那行这个ACTION_UP的事件会发生在右侧的菜单中 } //折叠菜单 mTargetView.setScrollX(0); } dispatchCollapsedOrExpanded(); break; } return needIntercept && mTargetView != null; }
对于滚动和动画的一些操作就比较简单了,没有DOWN事件是因为RecyclerView会在转发这个事件的事件故意不转发的可以看RecyclerViewr的 dispatchOnItemTouch 方法就知道了,在MOVE里面直接算一下移动的距离之类的就好了,通过deltaX来滚动容器的scrollX这样就可以实现在拖动时候显示或隐藏右侧的菜单。
@Override public void onTouchEvent(RecyclerView rv, MotionEvent e) { //如果要响应fling事件设置将mIsDragging设为false if (mGestureDetector.onTouchEvent(e)) { mIsDragging = false; return; } int x = (int) e.getX(); int y = (int) e.getY(); int action = MotionEventCompat.getActionMasked(e); switch (action) { case MotionEvent.ACTION_DOWN: //RecyclerView 不会转发这个Down事件 break; case MotionEvent.ACTION_MOVE: int deltaX = (int) (mLastX - e.getX()); horizontalDrag(deltaX); mLastX = x; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if(mIsDragging){ smoothHorizontalExpandOrCollapse(0); } dispatchCollapsedOrExpanded(); mIsDragging = false; break; } }
提示一下就是在RecyclerView滚动的时候我们应该要保存每个Item的展开和折叠的状态因为RecyclerVIew会回收重复利用Holder如果不保存的放就会就会混乱,这个看一下SlideItemAdapter的实现就好了很简单。
@Override public void onCollapsed(RecyclerView.ViewHolder holder) { mItemState.put(holder.getAdapterPosition(), false); Log.d("slide", "onCollapsed"); } @Override public void onExpanded(RecyclerView.ViewHolder holder) { mItemState.put(holder.getAdapterPosition(), true); Log.d("slide", "onExpanded"); }
在操作的时候我也写了一个回调接口,有些方法不是必需的但我还是写在了回调里因为我实在是想保存RecyclerView的实例。
public interface Callback { void onCollapsed(RecyclerView.ViewHolder holder); void onExpanded(RecyclerView.ViewHolder holder); int getHorizontalRange(RecyclerView.ViewHolder holder); RecyclerView.ViewHolder getChildViewHolder(View childView); View findTargetView(float x, float y); boolean isEnable(); }
在使用的时候一定要调用下面代码将ItemslideHelper与RecyclerView关联起来.
mRecyclerView.addOnItemTouchListener(new ItemSlideHelper(mRecyclerView.getContext(), this));
项目我已经传到Github上了
https://github.com/qq542529039/slideitem.git