码迷,mamicode.com
首页 > 其他好文 > 详细

自定义SwipeLayout实现侧滑菜单

时间:2016-06-28 20:40:40      阅读:289      评论:0      收藏:0      [点我收藏+]

标签:

请尊重个人劳动成果,转载注明出处,谢谢!
http://blog.csdn.net/amazing7/article/details/51768942

先看 SwipeLayout的效果图

技术分享

图太多了,我这只上传一张,想看 listview和GridView效果的,和想看源码的 —> GitHub

怎么实现后面说,先说会废话。

  最近整理以前的项目,那时有一个这样的需求,要在ExpandableListView的ChildView上实现 编辑和删除的侧滑菜单。我当时并没有用别人的框架,实现出来大概是这个样子。

技术分享

滑动事件完好,没有冲突,子view可以获得点击事件。

实现起来非常简单,大概分为三步:

①、这个Item布局采用HorizontalScrollView来作为最外层布局(对,就是它,它可以横向滚动),注意 HorizontalScrollView继承的是FrameLayout,意味着只能有一个子布局。在这个子布局里,摆放两个子子布局,left用于显示内容,right显示菜单。

②、初始化中获取屏幕宽度

DisplayMetrics dm = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
mScreentWidth = dm.widthPixels;

在Adapter的getChildView方法中设置leftView的宽度为屏幕宽度

// 设置leftView的大小为屏幕宽度,这样右边的rightView就正好被挤出屏幕外
 holder.leftview= convertView.findViewById(R.id.left);
 LayoutParams lp = holder.leftview.getLayoutParams();
 lp.width = mScreentWidth;

③、getChildView方法中给convertView设置Touch监听事件

 convertView.setOnTouchListener(new View.OnTouchListener()
             {
                 @Override
                 public boolean onTouch(View v, MotionEvent event)
                 {
                     switch (event.getAction())
                     {
                         case MotionEvent.ACTION_DOWN:
                             if (view != null) {
                             //有view被滑开了,点击其他childview时,用于还原
                                 ViewHolder viewHolder = (ViewHolder) view.getTag();
                                 viewHolder.hsv.smoothScrollTo(0, 0);
                             }
                              break;
                         case MotionEvent.ACTION_UP:
                             ViewHolder viewHolder = (ViewHolder) v.getTag();
                             view = v;
                             // 获得HorizontalScrollView滑动的水平方向值.
                             int scrollX = viewHolder.hsv.getScrollX();
                             int rightW = viewHolder.rightView.getWidth();
                             if (scrollX < rightW / 4)
                             {  //滑动距离小于右边布局的1/4收缩
                                 viewHolder.hsv.smoothScrollTo(0, 0);
                             }else
                             {  //展开
                                 viewHolder.hSView.smoothScrollTo(rightW, 0);
                             }
                             break;
                     }
                     return true;
                 }
             });

             // 删除一条后更新状态
             if (holder.hsv.getScrollX() != 0) {
                 holder.hsv.scrollTo(0, 0);
             }

  简单说一下,HorizontalScrollView在dispatchTouchEvent的时候,如果发现时横向滑动就把事件交给onTouchEvent处理,而这个onTouchEvent方法是来自view的(viewGroup也是调用view的该方法),在view的dispatchTouchEvent中,是顺序调用OnTouchListener和onTouchEvent的。我们这setOnTouchListener中自己处理了滑动事件,并且返回true,就消耗掉了事件,不会再调用onTouchEvent。

  开始说主题,假设都对 view和viewGroup的事件分发机制 、自定义viewGroup 的流程 和 Scroller 都有了初步的了解。如果没有,我们就假设有!
  
  技术分享
  
  算了,还没有了解可以看看 View和ViewGroup事件分发机制源码分析自定义FlowLayout实现标签快捷输入框Android Scroller大揭秘这三篇文章。
  

1.自定义SwipeLayout

   我是继承LinearLayout实现的,为什么不继承HorizontalScrollView或是ViewGroup?
  HorizontalScrollView其实就是一个实现滚动功能的FrameLayout,view的onMeasure和onLayout是层层实现的,我不能在HorizontalScrollView的onLayout方法中对其子view的子view直接设置为屏幕宽度。
  
  继承ViewGroup就要自己测量和摆放子view,开什么玩笑,我这么懒得人。

  为了节省大家宝贵的时间,下面只说重点。

1.1 事件冲突解决

  为什么有事件冲突呢? 我们知道listview是上下滑动的,而我们的这个侧滑布局要左右滑动。当我们屏幕上横向滑动时,只要稍微斜了一点,那么listview就认为要上下滑动,它就把滑动事件拦截了自己交给自己的onTouchEvent处理。根本都分发不到SwipeLayout的局部中。产生效果:侧滑出来一点划不动了,卡住了…

  然而google早已看穿了一切,他们给我们提供了这个方法。

requestDisallowInterceptTouchEvent(true);

  看过源码的伙伴肯定知道,这其实就是设置一个标志位。当传入true时,驳回父view的中断请求。( 就是父view不能中断事件必须分发到子view。)

  那么事件就由我们处理了,因为listview需要上下滑动事件,而SwipeLayout需要左右滑动事件,刚好各取所需,复写dispatchTouchEvent(MotionEvent ev)方法来实现各取所需。

public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                /**
                 * 不允许父view对触摸事件的拦截
                 */
                disallowParentsInterceptTouchEvent(getParent());
                startX = ev.getX();
                startY = ev.getY();
                isHorizontalMove =false;
                break;
            case MotionEvent.ACTION_MOVE:
                if(!isHorizontalMove){
                curX = ev.getX();
                curY = ev.getY();
                float dx = curX - startX;
                float dy = curY - startY;
                    /**
                     * 认为发生了滑动
                     */
                    if(dx*dx+dy*dy > mTouchSlop*mTouchSlop){
                        /**
                         * 垂直滑动
                         */
                        if (Math.abs(dy) > Math.abs(dx)){
                            /**
                             * 允许父view对触摸事件拦截,让其他view去处理事件
                             */
                            allowParentsInterceptTouchEvent(getParent());
                            /**
                             * 垂直滚动复原所有item
                             */
                            shrinkAllView();
                        }else{
                            /**
                             * 水平滑动,拦截来自己处理
                             */
                            isHorizontalMove = true;
                            /**
                             * 为了在onTouchEvent的Move事件中第一次模拟滑动距离不要太大,
                             * 记录上一次发生move的位置
                             */
                            lastX = curX;
                        }
                    }
                }
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

  在ACTION_DOWN中 驳回父view拦截,那么我们就可以开心的在ACTION_MOVE对事件进行处理(先判断是否发生了滑动,再判断是 上下 还是 左右,是上下就取消对父类的驳回,让listview去处理事件,同时让所有划开的SwipeLayout关闭。是左右滑动就 中断继续往下层分发,拦截到自己的onTouchEvent做事件处理)。

中断分发:

  @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(isHorizontalMove){
            /**
             * 发生水平滑动,把事件中断到本层onTouchEvent中处理
             */
            return true;
        }
        return super.onInterceptTouchEvent(ev);
    }

  我这侧滑菜单里是两个imageView默认是不可clickable的,所以就算不拦截最后也会执行我的onTouchEvent处理。但是如果是Button,ImageButton等默认是可点击的,那么事件就传不上来了。

因为我们不知道是哪个父view对事件进行了拦截,所以要循环递归设置标志位(为了更好的兼容性,反正我这是listview拦截了)。

  /**
     * 因为不知道是父view那一层会拦截触摸事件,所以递归向上设置标志位
     * 直到顶层view,就直接返回
     */
    private void disallowParentsInterceptTouchEvent(ViewParent parent) {
        if (null == parent) {
            return;
        }
        parent.requestDisallowInterceptTouchEvent(true);
        disallowParentsInterceptTouchEvent(parent.getParent());
    }
    private void allowParentsInterceptTouchEvent(ViewParent parent) {
        if (null == parent) {
            return;
        }
        parent.requestDisallowInterceptTouchEvent(false);
        allowParentsInterceptTouchEvent(parent.getParent());
    }

1.2 onTouchEvent处理

  当标志位表明是横向滑动时,我们需要在ACTION_MOVE里面模拟滚动。触发一次ACTION_MOVE就让内容滚动一点点,实现效果就是内容跟随手指移动。

 public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE:
                if(isHorizontalMove){
                    curX = ev.getX();
                    float dX = curX-lastX;
                    /**
                     * 不断更新lastX的位置,用于模拟滑动
                     */
                    lastX = curX;
                    /**
                     * 滑动的距离与实际相反,因为滚动的时候移动的是内容,不是view
                     */
                    int disX = getScrollX() + (int)(-dX);
                    /**
                     * 手指向右移动
                     */
                    if(disX<0){
                        /**
                         * 如果菜单收缩,防止越界(越界后ACTION_UP又会滚动回来,但还是不越界的好)
                         * 如果菜单展开,,我们希望迅速关闭菜单,不需要模拟滚动
                         */
                        scrollTo(0, 0);
                    }
                    /**
                     * 手指向左移动,如果累加的移动距离已经大于menu的宽度,就让menu显示出来。
                     * 如果移动距离还不到,就模拟滚动
                     */
                    else if(disX>rightViewWidth){
                        scrollTo(rightViewWidth,0);
                    }
                    else{
                        scrollTo(disX, 0);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                float endX = ev.getX();
                float dis =endX -startX;
                /**
                 * 手指向左滑动,模拟展开
                 */
                if(dis<0){
                    SimulateScroll(EXPAND);
                }
                /**
                 * 手指向右滑动,模拟关闭
                 */
                else{
                    SimulateScroll(SHRINK);
                }
            default:
                break;
        }

        return true;
    }

  先说一下这个lastX,这个坐标是onInterceptTouchEvent中判断为横向移动后的最近坐标。(事件不能被消费掉,但是会随着时间消失,我们可以在最内层到最外层的所有view的onTouchEvent中对一个事件进行处理,全部返回false。但是这里是 在onInterceptTouchEvent中已经触发了11个(大概)ACTION_MOVE事件,然后onTouchEvent才进行处理。简单的说就是我滑动的前一段距离拿去做判断了,判断好了才跟这手指移动。)

  那这么办呢?要么  

 float dX = curX-lastX;

滑动顺畅,不足的距离由最后scroller模拟滚动补回。

 float dX = curX-startX;

刚发生移动那一下移动约10个ACTION_MOVE事件的距离,效果不好。

1.2.1 onTouchEvent的ACTION_MOVE中模拟滚动

给不了解的恶补一下概念:

技术分享

getX() 和getY()是view的内部坐标,大小补回超过长宽。

技术分享

getScrollX()

获取的是view左上角到内容左上角的距离。至于正负表示方向。
Positive numbers will scroll the content to the left.  

 屏幕刚显示,未发生移动之前getScrollX()等于0,每一次scrollTo后会刷新getScrollX()的值。

 在一次ACTION_MOVE事件中,手指移动了dx距离,那么让内容也一起移动dx,就实现了跟随手指移动的效果。

相当于 getScrollX()+=dx。

1.3 用scroller模拟继续滑动

  当我们手指拿起来的时候,要判断SwipeLayout的菜单栏是应该收缩还是应该展开。当确定了状态后就要模拟手指触摸滑动到指定位置。

  /**
     * move事件里模拟滑动完成后,判断展开状态
     * 再模拟滚动到目标位置
     */
    public void SimulateScroll(int type){
        int dx =0;
        switch (type){
            case EXPAND:
            //手指向左滑动getScrollX为正
                dx = rightViewWidth-getScrollX();
                break;
            case SHRINK:
             //手指向右滑动getScrollX为负
                dx = 0-getScrollX();
                break;
            default:
                break;
        }
        scroller.startScroll(getScrollX(),0,dx,0,Math.abs(dx)/2);
        invalidate();
    }

    @Override
    public void computeScroll() {
        /**
         * Call this when you want to know the new location.  If it returns true,
         * the animation is not yet finished.
         *
         * 返回true代表正在模拟数据,false 已经停止模拟数据
         */
        if (scroller.computeScrollOffset()) {
            /**
             * 更新X轴的偏移量
             */
            scrollTo(scroller.getCurrX(), 0);
            /**
             * 递归调用computeScroll()方法,直到模拟滚动完成
             */
            invalidate();
        }
    }

 这里调用invalidate()重绘UI,会再次调用computeScroll(),递归 直到 模拟滚动完成。

2. 上下滑动和删除时状态改变

  listview滑动时所有SwipeLayout复原。
  我们知道listview删除了一个item,不一定会回收view,有可能只是重新装载了数据。那么显示的时候要SwipeLayout复原。

  /**
     * 用于上下滑动和删除item时的,状态改变
     */
    static List<SwipeLayout> swipelayouts = new ArrayList<>();
    public  static void addSwipeView(SwipeLayout v){
            if(null==v){
                return;
            }
            swipelayouts.add(v);
        }
        public static void removeSwipeView(SwipeLayout v){
            if(null==v){
                return;
            }
            v.SimulateScroll(SwipeLayout.SHRINK);
        }
        private void shrinkAllView(){
            for(SwipeLayout s :swipelayouts){
                if(null==s){
                    swipelayouts.remove(s);
                    continue;
                }else {
                    s.SimulateScroll(SwipeLayout.SHRINK);
                }

            }
        }

内部定义了三个方法,当删除item时调用removeSwipeView方法使该view复原。在adapte的getview方法添加新item时调用addSwipeView,把当前显示的所有SwipeLayout都装在这个list里面。在上面dispatchTouchEvent中判断为上下滑动事件的时候调用shrinkAllView复原所有。

基本上就完了,有木有很简单!哈哈

3. 其他

  我们这里是解决了上下滑动和左右滑动的冲突,那么listview 中item为scrollview时,两个都要竖着滑动。
  我们为什么想要在listview 中使用scrollview,因为我们的item中需要显示的太多。我们知道当scrollview里的内容高度 小于它父view给他的高度的时候,它是完全展开的,不需要也不能滑动。那么使listview给item足够大的高度,让scrollview不必以滚动的方式来展现,就可以解决这个滑动冲突,反正滚动事件也会被listview 拦截。
  新建一个新的NoScrollListView继承与listview,并复写onMeasure方法。
  

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       //这好比你有一个炒鸡有钱的爹,你想要多少都能满足你,对! 是满足你...
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 7, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }

那么我有一个问题,viewpage+fragment+listview +SwipeLayout,我们知道viewpage要左右滑动,当发生滑动事件的时候,我是该移动SwipeLayout呢?还是让viewpage子去翻页呢?

自定义SwipeLayout实现侧滑菜单

标签:

原文地址:http://blog.csdn.net/amazing7/article/details/51768942

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!