RecycleView的左滑实现

RecycleView的左滑实现

最终的效果图是这样的

swap2.gif
swap3.gif

要实现这样的一个效果,用到的关键技术:
自定义view的基本知识+事件处理+其它知识

一.右边的操作view

1.数据的组装

我们可以把右边的操作选项抽象出来数据对象即可,对于老司机的你们一看就懂。

public class SwipeMenuItem {

    private static final int TITLE_SIZE = 20;//sp
    private static final int WIDTH = 80;//dp
    private int id;
    private Context mContext;
    private String title;
    private Drawable icon;
    private Drawable background;
    private int titleColor;
    private int titleSize;
    private int width;

    public SwipeMenuItem(Context context) {
        mContext = context;
        //设置默认值
        DisplayMetrics dm = mContext.getResources().getDisplayMetrics();
        titleColor = Color.WHITE;
        titleSize = TITLE_SIZE;
        width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, WIDTH, dm);
    }
   }

2.SwipeMenuView的简单扩展(自定义view的一种吧)

public class SwipeMenuView extends LinearLayout implements View.OnClickListener {

    private SwipeMenuLayout mLayout;
    private SwipeMenu mMenu;
    private OnMenuItemClickListener mOnMenuItemClickListener;
    private int position;

    public int getPosition() {
        return position;
    }

    public void setPosition(int position) {
        this.position = position;
    }

    public SwipeMenuView(SwipeMenu menu) {
        super(menu.getContext());
        setOrientation(LinearLayout.HORIZONTAL);
        mMenu = menu;
        List<SwipeMenuItem> items = mMenu.getMenuItems();
        int id = 0;
        for (SwipeMenuItem item : items) {
            addItem(item, id++);
        }
    }

    private void addItem(SwipeMenuItem item, int id) {
        LayoutParams params = new LayoutParams(item.getWidth(),
                LayoutParams.MATCH_PARENT);
        LinearLayout parent = new LinearLayout(getContext());
        parent.setId(id);
        parent.setGravity(Gravity.CENTER);
        parent.setOrientation(LinearLayout.VERTICAL);
        parent.setLayoutParams(params);
        parent.setBackgroundDrawable(item.getBackground());
        parent.setOnClickListener(this);
        addView(parent);

        if (item.getIcon() != null) {
            parent.addView(createIcon(item));
        }
        if (!TextUtils.isEmpty(item.getTitle())) {
            parent.addView(createTitle(item));
        }

    }

    private ImageView createIcon(SwipeMenuItem item) {
        ImageView iv = new ImageView(getContext());
        iv.setImageDrawable(item.getIcon());
        return iv;
    }

    private TextView createTitle(SwipeMenuItem item) {
        TextView tv = new TextView(getContext());
        tv.setText(item.getTitle());
        tv.setGravity(Gravity.CENTER);
        tv.setTextSize(item.getTitleSize());
        tv.setTextColor(item.getTitleColor());
        return tv;
    }

    @Override
    public void onClick(View v) {
        if (mOnMenuItemClickListener != null && mLayout.isOpen()) {
            mOnMenuItemClickListener.onMenuItemClick(position, mMenu, v.getId());
        }
    }

    public interface OnMenuItemClickListener {
        void onMenuItemClick(int position, SwipeMenu menu, int index);
    }

    public void setOnMenuItemClickListener(
            OnMenuItemClickListener mOnMenuItemClickListener) {
        this.mOnMenuItemClickListener = mOnMenuItemClickListener;
    }

    public void setLayout(SwipeMenuLayout mLayout) {
        this.mLayout = mLayout;
    }
} 

说白了就是继承LinearLayout 加了一个回调接口,对于老司机的你们一看又懂了。对于SwipeMenuLayout是什么,我们后面会讲的,别着急吗?嘻嘻

二.RecyclerView.Adapter的处理

  • 我们本着在不影响用户原有的adapter的基础上尽量不改或者少改。
    对于RecyclerView的Adapter 我们都是继承RecyclerView.Adapter。<br />
  • 主要是重写onCreateViewHolder和onBindViewHolder方法。
  • 对于onBindViewHolder方法完美不错任何处理,也没有必要做。<br />
  • 主要是onCreateViewHolder方法,这个方法返回是一条item的布局ui,对于我们这个效果在不改动优惠正常的view布局的情况下,我们可以这么做呢????<br />
  • 咦! 我们可以在原来的基础上再套一层FrameLayout. 是的,没错,老司机!!
  @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //根据数据创建右边的操作view
        SwipeMenuView menuView = swipeMenuBuilder.create();
        //包装用户的item布局
        SwipeMenuLayout swipeMenuLayout = SwapWrapperUtils.wrap(parent, R.layout.item, menuView, new BounceInterpolator(), new LinearInterpolator());
        MyViewHolder holder = new MyViewHolder(swipeMenuLayout);
        setListener(parent, holder, viewType);
        return holder;
    }

SwapWrapperUtils.wrap 这个方法这里就不说了就是LayoutInflater加载布局。

三.SwipeMenuLayout-view的设计

继承自FrameLayout

讲用户的itemview这里我们叫Contentview,以及操作view我们叫MenuView,添加到这个FrameLayout上

   setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.WRAP_CONTENT));
        mMenuView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
                LayoutParams.WRAP_CONTENT));
        addView(mContentView);
        addView(mMenuView);
设置初始状态

我们要测量menuview的宽,高度就是Contentview的高。
我们要布局menuview,在Contentview的右侧。
如图:

layout.png
   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //测量mMenuView的宽,高为mContentView的高
        mMenuView.measure(MeasureSpec.makeMeasureSpec(0,
                MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(
                getMeasuredHeight(), MeasureSpec.EXACTLY));
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mContentView.layout(0, 0, getMeasuredWidth(),
                mContentView.getMeasuredHeight());
        //在mContentView的右侧
        mMenuView.layout(getMeasuredWidth(), 0,
                getMeasuredWidth() + mMenuView.getMeasuredWidth(),
                mContentView.getMeasuredHeight());
    }
控制滑动

在android中根据滑动来控制view有好多种,这里我们用layout方法
主要就是在recycleview滑动时找到其中一条的位置position在ontouch方法中合适的时机将事件传到该view上。什么时候触发这个方法呢
,下文会说recycleview的处理事件。
我们写一个方法将事件传递到此view上来控制menuView和contentView

    public void  onSwipe(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = (int) event.getX();
                isFling = false;
                break;
            case MotionEvent.ACTION_MOVE:
                //按下去-当前的位置
                int dis = (int) (mDownX - event.getX());
                //menuView打开状态dis+mMenuView宽
                if (state == STATE_OPEN) {
                    dis += mMenuView.getWidth();
                }
                swipe(dis);
                break;
            case MotionEvent.ACTION_UP:
                //快速滑动,或者超过了mMenuView宽的一半则打开,否则关闭
                if (isFling || (mDownX - event.getX()) > (mMenuView.getWidth() / 2)) {
                    smoothOpenMenu();
                } else {
                    smoothCloseMenu();
                }
                break;
        }
    }

    /**
     * 更改位置
     * @param dis dis
     */
    private void swipe(int dis) {
        //mContentView的最大为mMenuView的宽
        if (dis > mMenuView.getWidth()) {
            dis = mMenuView.getWidth();
        }
        //mContentView-left的最小值为0即正常值
        if (dis < 0) {
            dis = 0;
        }
        //设置完mContentView的left就可以得出right以及mMenuView的left和right了
        //主要是left,right
        //left 最大值为-mMenuView.getWidth()
        mContentView.layout(-dis, mContentView.getTop(),
                mContentView.getWidth() - dis, getMeasuredHeight());
        mMenuView.layout(mContentView.getWidth() - dis, mMenuView.getTop(),
                mContentView.getWidth() + mMenuView.getWidth() - dis,
                mMenuView.getBottom());
    }
打开与关闭

借助computeScroll方法来不停的layout设置位置,代码都对于位置的计算有注释,生怕解释不清楚。

   
    @Override
    public void computeScroll() {
        //让mMenuView打开
        if (state == STATE_OPEN) {
            //是否停止了滑动
            if (mOpenScroller.computeScrollOffset()) {
                swipe(mOpenScroller.getCurrX());
                //重绘UI
                postInvalidate();
            }
        } else {//让mMenuView关闭
               //mContentView的
            if (mCloseScroller.computeScrollOffset()) {
                //mBaseX为当前的mContentView的left,可以结合
                swipe(mBaseX - mCloseScroller.getCurrX());
                postInvalidate();
            }
        }
    }

    /**
     * 平滑的关闭mMenuView
     */
    public void smoothCloseMenu() {
        state = STATE_CLOSE;
        mBaseX = -mContentView.getLeft();
        //关闭是我们要让mContentView的慢慢的减小,
        //mCloseScroller.getCurrX()的范围是(0,mBaseX)
        mCloseScroller.startScroll(0, 0, mBaseX, 0, DURATION);
        postInvalidate();
    }
    /**
     * 平滑的打开mMenuView
     */
    public void smoothOpenMenu() {
        state = STATE_OPEN;
        //其实我们这里是用到了Scroller类产生的值(当然借助Interpolator来实现不同的值渐变,从而实现不同的效果)
        //打开的时候mContentView的left从当前的-mContentView.getLeft()到mMenuView.getWidth()
        //在computeScroll方法中 swipe(mOpenScroller.getCurrX());即可
        //mOpenScroller.getCurrX()的范围是(-mContentView.getLeft(),mMenuView.getWidth())
        //-mContentView.getLeft()为正值
        mOpenScroller.startScroll(-mContentView.getLeft(), 0,
                mMenuView.getWidth(), 0, DURATION);
        postInvalidate();
    }

四.RecyclerView的事件处理

首先我们要明白一点就是:我们要不影响用户原来的item的点击与长按等事件。

我们肯定要重新事件的拦截与处理方法。即onInterceptTouchEvent
与onTouchEvent方法。我们需要在这2个方法里做如下的处理。

  1. 找到按下去的那一条
  2. 什么时候拦截各种down,move,up事件
  3. 处理各种down,move,up事件
找到按下去的那一条
//找到当前点击坐标下的所处于SwapRecyclerView的位置

                int mFirstPosition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
                int count = getChildCount();
                for (int i = 0; i < count; i++) {
                    final View child = getChildAt(i);
                    if (child.getVisibility() == View.VISIBLE) {
                        child.getHitRect(mTouchFrame);
                        //判断是否点击到该控件上
                        if (mTouchFrame.contains(x, y)) {
                            mTouchPosition = mFirstPosition + i;
                            break;
                        }
                    }
                }

找到了pos位置就可以 View view = getChildAt(mTouchPosition - mFirstPosition);
来获取那个view了,就可以进行事件的处理了。
child.getHitRect方法 ,我们看下sdkapi的注释:

  /**
  找到控件占据的矩形区域的矩形坐标
     * Hit rectangle in parent's coordinates
     *
     返回的矩形   控件占据的矩形区域
     * @param outRect The hit rectangle of the view.
     */
    public void getHitRect(Rect outRect) {
        if (hasIdentityMatrix() || mAttachInfo == null) {
            outRect.set(mLeft, mTop, mRight, mBottom);
        } else {
            final RectF tmpRect = mAttachInfo.mTmpTransformRect;
            tmpRect.set(0, 0, getWidth(), getHeight());
            getMatrix().mapRect(tmpRect); // TODO: mRenderNode.mapRect(tmpRect)
            outRect.set((int) tmpRect.left + mLeft, (int) tmpRect.top + mTop,
                    (int) tmpRect.right + mLeft, (int) tmpRect.bottom + mTop);
        }
    }
onInterceptTouchEvent 拦截 onTouch的处理 的搞基生活

down拦截的时候:

  1. menuView处于打开且点击的不在menu区域
  2. 达到了滑动的临界值
  3. 这些情况都要交要我们处理,需要拦截(reutrn true),交给ontouch方法
  //找到了
                if (mTouchPosition != -1) {
                    //通过position得到item的viewHolder,并判断合法性
                    View view = getChildAt(mTouchPosition - mFirstPosition);
                    RecyclerView.ViewHolder viewHolder = getChildViewHolder(view);
                    if (viewHolder.itemView instanceof SwipeMenuLayout) {
                        //menuView处于打开且点击的不在menu区域
                        if (mTouchView != null && mTouchView.isOpen() && !inRangeOfView(mTouchView.getmMenuView(), event)) {
                            //拦截事件,交给自己的onTouch方法处理.
                            return true;
                        }
                        mTouchView = (SwipeMenuLayout) view;
                    } else {
                        throw new RuntimeException("viewHolder.itemView  must be SwipeMenuLayout layout");
                    }
                    //将事件交给SwipeMenuLayout处理down事件
                    mTouchView.onSwipe(event);
                }
                //down事件,如果没有打开menu,则不拦截,仍然交给系统
                return handled;

然后在onTouchEven方法里处理down:

 case MotionEvent.ACTION_DOWN:
                //如果当前是处于打开的且用户按下去正好是打开menu的那行
                if (mTouchPosition == oldPos && mTouchView != null
                        && mTouchView.isOpen()) {
                    mTouchState = TOUCH_STATE_X;
                    mTouchView.onSwipe(event);
                    return true;
                } else {
                    //如果不是直接关闭
                    if (mTouchView != null && mTouchView.isOpen()) {
                        mTouchView.smoothCloseMenu();
                        mTouchView = null;
                        return super.onTouchEvent(event);
                    }
                }
                break;

move拦截的时候:
达到滑动的临界值就可以拦截了return true了。
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();

   case MotionEvent.ACTION_MOVE:
               float dy = Math.abs((event.getY() - mDownY));
               float dx = Math.abs((event.getX() - mDownX));
               //达到了滑动的临界值
               if (Math.abs(dy) > mTouchSlop || Math.abs(dx) > mTouchSlop) {
                   if (mTouchState == TOUCH_STATE_NONE) {
                       if (Math.abs(dy) > mTouchSlop) {//上下滑动的
                           mTouchState = TOUCH_STATE_Y;
                       } else if (dx > mTouchSlop) {//左右滑动的
                           mTouchState = TOUCH_STATE_X;
                           if (mOnSwipeListener != null) {
                               mOnSwipeListener.onSwipeStart(mTouchPosition);
                           }
                       }
                   }
                   return true;//拦截事件,交给自己的onTouch方法处理.
               }

然后在onTouchEven方法里处理move:如果是左右我们才处理,否则拜拜了您。

case MotionEvent.ACTION_MOVE:
             //左右滑动交给mTouchView处理,事件消费了
             if (mTouchState == TOUCH_STATE_X) {
                 if (mTouchView != null) {
                     mTouchView.onSwipe(event);
                 }
                 event.setAction(MotionEvent.ACTION_CANCEL);
                 super.onTouchEvent(event);
                 return true;
             }
             break;

最后up事件就简单了不需要拦截,无非就是TOUCH_STATE_X状态交给我们之前的SwipeMenuLayout处理打开还是关闭 , 以及 将一些变量的恢复为初始化状态。
到此整个实现就完了。

这里只分析一些核心的关键技术,其它的都能看懂。

代码下载地址:

https://github.com/ta893115871/SwapMenuRecyclerView

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

推荐阅读更多精彩内容