关闭

Android开发ViewDragHelper打造不一样的recyclerview

3692人阅读 评论(2) 收藏 举报
分类:

概述

前面我有一篇是讲到了viewdraghelper,http://blog.csdn.net/sw950729/article/details/53352587。对viewdraghelper不了解,可以看完再说。有人说viewdraghelper这个不就是个手势处理类么,怎么打造不一样的recyclerview?不不不,不要小瞧所有的手势处理,包括那啥GestureDetector也是。但是本文重点还是用viewdraghelper处理。处理啥呢?没错,就是recyclerview的侧滑删除功能。
网上侧滑的轮子不要太多,各种swipelayout。各种开源,各种嗨~。不过被众人评价“最爱作死的人”的我。不是很喜欢直接用轮子,轮子,有2种,一种是直接用别人的轮子,意味着别人的思路强加在你身上,另一种就是自己写轮子or改轮子,把你思路强加在别人身上。我更倾向于后者,所以导致,写这个布局的时候爬了好久的坑,综合了不知道几十个甚至上百个布局来改写。最后形成了自己的布局。下面我们一步步来进行分析以及实现。

侧滑删除的效果

这里写图片描述

思路

上图应该就是我们做侧滑的效果,就如QQ的一样。屏幕内是内容,右边是影藏的控件,需要通过滑动来进行打开和关闭。那么我们正常一个控件显示整个屏幕的时候我们无法滑动这个控件,但我们通过viewdraghelper可以实现整个布局的滑动。那么,我们需要考虑另外一种情况,那就是越界问题。其实viewdraghelper用一个方法可以帮助我们很好的处理越界问题。

使用布局

侧滑应该用什么布局写?自定义listview?linearlayout?framelayout?还是viewgroup?要不我来个新鲜的?自定义recyclerview如何?好像有点夸张了- - 根据效果我觉得还是通过自定义LinearLayout进行比较合理。

大致功能

1.侧滑显示侧滑菜单

2.点击侧滑区域以外的地方影藏侧滑菜单

3.侧滑时禁止上下滑动,同时上下滑动时也禁止左右滑动

4.不会同时打开2个菜单,这里有2种效果,一个是打开另一个的时候,手指松开关闭上一个打开的item。第二种则是高仿QQ,只要有侧滑菜单下次触摸直接关闭无法打开第二个item。So,我进行了高仿QQ的处理。

5.侧滑时无法进行点击事件,需要先关闭item才能进行对应的点击事件。

分析详解

主要我们需要实现上面5点,这样就可以实现一个完美的侧滑删除~~~下面我们一步步的来分析下如何实现。

两个子view

 protected void onFinishInflate() {
        super.onFinishInflate();
        if (getChildCount() != 2) {
            throw new NullPointerException("you only need two child view!");
        }
        itemView = getChildAt(0);
        hiddenView = getChildAt(1);
    }

    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        hiddenViewWidth = hiddenView.getMeasuredWidth();
    }

我们需要在onFinishInflate进行对item的获取,这个就是xml映射完成之后获取对应item。然后在onsizechange进行宽度的获取。

侧滑显示侧滑菜单

这个是完完全全用viewdraghelper来实现。话不多说,我们直接看代码:

 ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {

        public boolean tryCaptureView(View view, int arg1) {
            return view == itemView;
        }

        public int clampViewPositionHorizontal(View child, int left, int dx) {
            if (child == itemView) {
                if (left > 0) {
                    return 0;
                } else {
                    left = Math.max(left, -hiddenViewWidth);
                    return left;
                }
            }
            return 0;
        }

        public int getViewHorizontalDragRange(View child) {
            return hiddenViewWidth;
        }

        public void onViewPositionChanged(View changedView, int left, int top,
                                          int dx, int dy) {

            if (itemView == changedView) {
                hiddenView.offsetLeftAndRight(dx);
            } else {
                itemView.offsetLeftAndRight(dx);
            }
            invalidate();
        }

        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            if (releasedChild == itemView) {
                if (xvel == 0 && Math.abs(itemView.getLeft()) > hiddenViewWidth / 2.0f || xvel < 0) {
                    open();
                } else {
                    close();
                }
            }
        }

    };

hiddenwidth是影藏区域的宽度,整个布局是item的全屏宽度。我们让这个布局可控宽度为hiddenwidth+itemwidth。这样我们就可以愉快的滑动了~而且还不会越界。

影藏侧滑菜单

我们需要点击侧滑以外的任何位置,让他影藏。那这个应该如何处理呢。我们需要通过onInterceptTouchEvent来进行拦截处理。具体代码如下:

  public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean value = helper.shouldInterceptTouchEvent(event);
        //if you open is not the current item,close
        if (!SWSlipeManager.getInstance().haveOpened(this)) {
            SWSlipeManager.getInstance().close();
        }

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downIX = event.getX();
                downIY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                moveX = event.getX();
                moveY = event.getY();
                if (Math.abs(moveX - downIX) > 1 || Math.abs(moveY - downIY) > 1) {
                    value = true;
                }
                break;
        }
        return value;
    }

SWSlipeManager这个是记录item的状态的,我们后面在说,这段具体代码就是只要有item打开了。我们就进行关闭处理。

滑动事件处理

其实就是在onViewPositionChanged里面就行处理,参数里有dx和dy。其实这个x,y的偏移量。所以我们需要在x!=0的时候就行拦截。所以onViewPositionChanged的代码改成了这样:

   public void onViewPositionChanged(View changedView, int left, int top,
                                          int dx, int dy) {
            if (dx != 0) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            if (itemView == changedView) {
                hiddenView.offsetLeftAndRight(dx);
            } else {
                itemView.offsetLeftAndRight(dx);
            }
            invalidate();
        }

禁止同时打开多个item

上面我们说到了一个管理侧滑开关的管理类。下面看看我们如何进行具体的处理。先看看我们是怎么管理的:

public class SWSlipeManager {

    private SWSlipeLayout swSlipeLayout;
    private static SWSlipeManager SWSlipeManager = new SWSlipeManager();

    public static SWSlipeManager getInstance() {
        return SWSlipeManager;
    }

    public void setSwSlipeLayout(SWSlipeLayout swSlipeLayout) {
        this.swSlipeLayout = swSlipeLayout;
    }

    public void clear() {
        swSlipeLayout = null;
    }

    public void close() {
        if (swSlipeLayout != null) {
            swSlipeLayout.close();
        }
    }

    /**
     * if s==null means no item is open
     *
     * @return ture means open else close
     */
    public boolean haveOpened() {
        return swSlipeLayout != null;
    }

    /**
     * if s==null means no item is open
     *
     * @return true means two item is not the same one and one item is open
     */
    public boolean haveOpened(SWSlipeLayout s) {
        return swSlipeLayout != null && swSlipeLayout == s;
    }
}

这边我们通过来进行布局是否打开来进行处理。这边我们还要继续修改OnviewPositionChanged这个方法,修改完成如下:

 public void onViewPositionChanged(View changedView, int left, int top,
                                          int dx, int dy) {
            if (dx != 0) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            if (itemView == changedView) {
                hiddenView.offsetLeftAndRight(dx);
            } else {
                itemView.offsetLeftAndRight(dx);
            }
            if (itemView.getLeft() != 0) {
                SWSlipeManager.getInstance().setSwSlipeLayout(SWSlipeLayout.this);
            } else {
                SWSlipeManager.getInstance().clear();
            }
            if (itemView.getLeft() == 0 && changeStatus != Status.Close) {
                changeStatus = Status.Close;
            } else if (itemView.getLeft() == -hiddenViewWidth && changeStatus != Status.Open) {
                changeStatus = Status.Open;
            }
            invalidate();
        }

这边我们进行对item.getleft()来处理我们的管理类,告诉它我们是打开了还是关闭了。既然是管理类,我们需要在打开的时候告诉它我们打开了。关闭的时候告诉它已经关闭了,需要清空。所以我们的打开和关闭需写成如下:

    /**
     * slide close
     */
    public void close() {
        if (helper.smoothSlideViewTo(itemView, 0, 0)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
        SWSlipeManager.getInstance().clear();
    }

    /**
     * slide open
     */
    public void open() {
        SWSlipeManager.getInstance().setSwSlipeLayout(this);
        if (helper.smoothSlideViewTo(itemView, -hiddenViewWidth, 0)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

这边既然用到了ViewCompat.postInvalidateOnAnimation(this)这个方法,所以我们一定不能少了这个方法。我记得刚开始的时候我就在这边进坑里去了。

    public void computeScroll() {
        super.computeScroll();
        // start animation
        if (helper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

这样我们是不是就算彻底解决了。不不不。我们还需要在ontouchevent里面进行判断。怎能少了这一步的拦截。少了它。前功尽弃。我们看看如何处理ontouchevent的。

 public boolean onTouchEvent(MotionEvent event) {
        if (SWSlipeManager.getInstance().haveOpened(this)) {
            getParent().requestDisallowInterceptTouchEvent(true);
        } else if (SWSlipeManager.getInstance().haveOpened()) {
            getParent().requestDisallowInterceptTouchEvent(true);
            return true;
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = event.getX();
                downY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                float moveX = event.getX();
                float moveY = event.getY();
                float dx = Math.abs(moveX - downX);
                float dy = Math.abs(moveY - downY);
                if (dx > dy) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                downX = moveX;
                downY = moveY;
                break;
        }
        helper.processTouchEvent(event);
        return true;
    }

事件问题

看似解决了所以的问题。but,你们难道忘了点击事件了么?如果我在侧滑的时候点击,此时进行的是item的关闭处理,还是点击事件?按照我们的写法是啥?没错,他会同时执行。所以我们需要在进行点击时候加一个判断。


        text.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                if (SWSlipeManager.getInstance().haveOpened()) {
                    SWSlipeManager.getInstance().close();
                } else {
                    Toast.makeText(context, position + 1 + "", 1000).show();
                }
            }
        });

这样我们的布局就算解决了。下面看看我们是如何进行删除和置顶的。


        text_delete.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                delete(holder.getLayoutPosition());
                SWSlipeManager.getInstance().close();
            }
        });
        text_top.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                SWSlipeManager.getInstance().close();
                setTop(holder.getLayoutPosition());
            }
        });

具体代码如下:


    public void delete(int position) {
        list.remove(position);
        notifyItemRemoved(position);
    }

    public void setTop(int position) {
        list.add(0, list.get(position));
        list.remove(position + 1);
        notifyDataSetChanged();
    }

总结

这个布局我已经上传到了我的开源布局SWPullRecyclerLayout中。地址:SWPullRecyclerLayout
老铁们,喜欢的话,随手点个star。多谢 。
我进行了为了防止QQ类似的bug来进行了优化。那么QQ的bug是什么呢?没图说了瘠薄,下面上图:
这里写图片描述
这里写图片描述
就是当执行刷新的时候,进行侧滑,刷新成功后,侧滑没归位,刷新也没归位。而我的就没问题了。每次执行刷新和加载后,执行如下几句代码:

   public void OnRefreshing() {
        Log.i("angel", "OnRefreshing: 正在刷新");
//        recycler.setIsScrollRefresh(false);
//        recycler.setScrollTo(recycler.getTotal(), 0);
//        SWSlipeManager.getInstance().close();
    }

    public void OnLoading() {
        Log.i("angel", "OnLoading: 正在加载");
//        recycler.setIsScrollLoad(false);
//        recycler.setScrollTo(recycler.getTotal(), 0);
//        SWSlipeManager.getInstance().close();
    }

先后顺序可调整。
关于SWPullRecyclerLayout,大家使用中如有一切问题,可以进行反馈,我会进行优化。也可以提议,如果合理,我也会进行后续优化。

4
0
查看评论

Android ViewDragHelper及移动处理总结

概述2013年谷歌i/o大会上介绍了两个新的layout: SlidingPaneLayout和DrawerLayout,现在这俩个类被广泛的运用。我们知道在我们实际的开发中往往会涉及到很多的拖动效果,而ViewDragHelper解决了android中手势处理过于复杂的问题。 其实ViewDra...
  • xiangzhihong8
  • xiangzhihong8
  • 2017-01-05 22:35
  • 4200

Android ViewDragHelper完全解析 自定义ViewGroup神器

转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/46858663; 本文出自:【张鸿洋的博客】 一、概述在自定义ViewGroup中,很多效果都包含用户手指去拖动其内部的某个View(eg:侧滑菜单等),针对具体的需要...
  • lmj623565791
  • lmj623565791
  • 2015-07-13 10:16
  • 107156

Android开发ViewDragHelper打造不一样的recyclerview

概述前面我有一篇是讲到了viewdraghelper,http://blog.csdn.net/sw950729/article/details/53352587。对viewdraghelper不了解,可以看完再说。有人说viewdraghelper这个不就是个手势处理类么,怎么打造不一样的recy...
  • sw950729
  • sw950729
  • 2017-05-08 11:06
  • 3692

RecyclerView实现可拖拽的GridView

  • 2016-04-18 15:01
  • 19.17MB
  • 下载

DragRecyclerView-master

  • 2017-07-12 15:29
  • 245KB
  • 下载

ItemTouchHelper 使用RecyclerView打造可拖拽的GridView

以下是RecyclerView结合ItemTouchHelper实现的列表和网格布局的拖拽效果。 效果图如下:(gif图有点顿卡,其实运行是很流畅的) demo下载地址: DragRecyclerView 如何实现 那么是如何实现的呢?主要就要使用到ItemTouchHelper...
  • u014651216
  • u014651216
  • 2016-05-19 20:12
  • 4197

支持下拉刷新和上划加载更多的自定义RecyclerView(仿XListView效果)

在项目更新的过程中,遇到了一个将XListView换成recyclerView的需求,而且更换完之后大体效果不能变,但是对于下拉刷新这样的效果,谷歌给出的解决方案是把RecyclerView放在一个SwipeRefreshLayout中,但是这样其实是拉下一个小圆形控件实现的,和XListView的...
  • lvshaorong
  • lvshaorong
  • 2016-05-18 10:44
  • 9789

Android使用ItemTouchHelper打造可拖拽的RecyclerView

前言ItemTouchHelper是support v7包提供的处理关于在RecyclerView上添加拖动排序与滑动删除的非常强大的工具类。它是RecyclerView.ItemDecoration的子类,也就是说它可以轻易的添加到几乎所有的LayoutManager和Adapter中。下面来看一...
  • aiynmimi
  • aiynmimi
  • 2017-08-31 15:47
  • 523

使用少量代码实现自己的RecyclerView侧滑菜单

没有找到自己想要的效果的侧滑菜单,花了些时间研究了一下能完成项目需求就行了。效果如下: 因为逻辑比较简单,总代码量500行左右,所以各种各样的定制都通过修改源码能实现,而且不需要继承特定的Adapter,使用方式和普通的RecyclerView没有区别。一. 实现一个侧滑菜单这里我使用DragHe...
  • u010386612
  • u010386612
  • 2016-10-28 16:00
  • 3406

解决DragViewHelper和RecyclerView滑动冲突

解决DragViewHelper和RecyclerView滑动冲突当没有recyclerview的时候 点击拖动的view 会直接走onTouchEvent回调,也就是走DragViewHelper的processTouchEvent 如果有recyclerview的时候 点击会走onInterc...
  • qq_28195645
  • qq_28195645
  • 2017-02-06 19:23
  • 453