Chinaunix首页 | 论坛 | 博客
  • 博客访问: 466750
  • 博文数量: 89
  • 博客积分: 1126
  • 博客等级: 少尉
  • 技术积分: 1432
  • 用 户 组: 普通用户
  • 注册时间: 2011-04-11 23:37
文章分类

全部博文(89)

文章存档

2016年(6)

2015年(2)

2014年(1)

2013年(3)

2012年(23)

2011年(54)

分类: Android平台

2016-07-08 21:46:43

一 介绍

 ListView 通常用来展示多个个体,比如QQ 微信中的联系人列表。一个比较常见的功能是侧滑删除。这个功能属于比较常见的一个菜单,网络上也有很多实现。

1 scroller 方式。

最常见的一个实现是ListView 的Item View 为一个LinerLayout, 菜单在LinerLayout的最右端超出屏幕的位置,当手指滑动的时候,通过scrollTo 的方法在ListView 中控制Item View 的滑动,使菜单滑动出来。但是在IOS 上菜单是隐藏在Item View 的下面,层叠式的,当滑动的时候不是拉出来的方式,而是显示出来。

2.NineOldAndroids

在属性动画没有加入android的远古时代,github 上有一个NineOldAndroids项目,有人通过这个实现一个和IOS接近,其原理是FrameLayout, context 为显示的内容,menu嵌套在context下面,属性动画的方式移动context。

3.SwipeMenuListView

在github 上有一个 实现效果和1 类似,但是View 移动采用layout 方式,我修改了下   实现效果和IOS 一样。但是总觉这几种方式都不太完美,要么效果打了折扣,要么代码量太大,方式复杂,通常需要重写ListView 和Adapter。

二 原理

 下面一个SwipeMenuLayout 大概不到300行的代码,完美实现ListView 的侧滑菜单。原理是继承FrameLayout, 作为ListView 的Item View 。在View中层叠两层,上层context View 为要显示的内容,下面menu View 为菜单。重写SwipeMenuLayout的OnTouchEvent ,在OnTouchEvent 中控制context 的移动。如图:

1. init View.

定义ListView 的Item View,如果使用左菜单:

  1. 左菜单的ID为:swipe_left_menu
  2. 右菜单ID:swipe_right_menu
  3. context 菜单ID:swipe_context

这个是默认ID,这样在SwipeMenuLayout会自动找到菜单View ID :

   

  1. public SwipeMenuLayout(Context context, AttributeSet attrs) {
  2.         super(context, attrs);
  3.         initAttrs(attrs);
  4.         initUI();
  5.     }
  6.     private void initUI() {
  7.         mScroller = ScrollerCompat.create(getContext());
  8.         ViewConfiguration config = ViewConfiguration.get(getContext());
  9.         mTouchSlop = config.getScaledTouchSlop();
  10.         mLeftMenuViewId = getContext().getResources().getIdentifier(LEFTMENUVIEW, "id", getContext().getPackageName());
  11.         mRightMenuViewId = getContext().getResources().getIdentifier(RIGHTMENUVIEW, "id", getContext().getPackageName());
  12.         mContextViewId = getContext().getResources().getIdentifier(CONTEXTVIEW, "id", getContext().getPackageName());
  13.         if (mLeftMenuViewId == 0 || mRightMenuViewId == 0 || mContextViewId == 0) {
  14.             throw new RuntimeException(String.format("initUI Exception" ));
  15.         }
  16.     }

重写 onMeasure方法, 在onMeasure 方法中调用findViewByID, 为什么在onMeasure 而不是在构造函数中,因为在构造函数中View 的子View还没有初始化,findViewByID 为空。需要强调的是菜单View 在init 的时候设置为不可以见,这样在开始滑动的时候,菜单View不会干扰SwipeMenuLayout的滑动事件

  

  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  2.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  3.         initView();
  4.         menuViewHide();
  5.     }
  6.      public void initView() {
  7.         if(mLeftMenuView == null && mLeftMenuViewId != View.NO_ID) {
  8.             mLeftMenuView = this.findViewById(mLeftMenuViewId);
  9.         }
  10.         if(mRightMenuView == null && mRightMenuViewId != View.NO_ID) {
  11.             mRightMenuView = this.findViewById(mRightMenuViewId);
  12.         }
  13.         if(mContextView == null && mContextViewId != View.NO_ID)
  14.             mContextView = this.findViewById(mContextViewId);
  15.         this.setOnClickListener(new OnClickListener() {
  16.             @Override
  17.             public void onClick(View v) {
  18.                 if(mOnMenuClickListener != null) {
  19.                     mOnMenuClickListener.onItemClick(v, mPosition);
  20.                 }
  21.             }
  22.         });
  23.     }

2 处理滑动事件

重写onTouchEvent 在down 事件中return true,拦截事件分发,在move 事件中判断滑动距离是否大于阀值,大于阀值,设置菜单View 可见,移动context View,在UP 事件中处理动画,根据滑动的距离和方向,开始对应的动画。

1. cancle 事件的处理, ListView 的 OnInteruptTouchEvent 会判断View 的Y轴滑动距离,如果大于一定的距离,拦截事件,响应ListView 的上下滑动。按照android 的标准处理,cancle 按照up 事件处理。

2. ListView 和 SwipeMenuLayout 的事件冲突,如果SwipeMenuLayout 进入侧滑后,上下滑动距离过大,ListView 会拦截事件,所以一旦进入侧滑模式,要禁止ListView 拦截事件.调用getParent().requestDisallowInterceptTouchEvent(true);

   

  1. public boolean onTouchEvent(MotionEvent event) {
  2.         switch(event.getAction()){
  3.             case MotionEvent.ACTION_DOWN:
  4.                 mDownX = event.getX();
  5.                 if(mLeftMenuView != null)
  6.                     mLeftMargin = mLeftMenuView.getWidth();
  7.                 if(mRightMenuView != null)
  8.                     mRightMargin = mRightMenuView.getWidth();
  9.                 if(mSlideView != null && this != mSlideView && mSlideView.isMenuOpen()) {
  10.                     mSlideView.closeMenu();
  11.                     event.setAction(MotionEvent.ACTION_CANCEL);
  12.                 }
  13.                 Log.d(TAG, "Event ACTION_DOWN mMenuShow:" + mMenuShow);
  14.                 super.onTouchEvent(event);
  15.                 return true;
  16.             case MotionEvent.ACTION_MOVE:
  17.                 int dx = (int) (event.getX() - mDownX);
  18.                 if(Math.abs(dx) < mTouchSlop)
  19.                     break;
  20.                 if(!mMenuShow ) {
  21.                     menuViewShow(dx);
  22.                     getParent().requestDisallowInterceptTouchEvent(true);
  23.                     mSlideView = this;
  24.                 }
  25.                 if(dx > 0 && mLeftMenuView == null) break;
  26.                 if(dx < 0 && mRightMenuView == null) break;
  27.                 if(dx > 0 && dx > mLeftMargin) {
  28.                     dx = mLeftMargin;
  29.                 } else if (dx < 0 && dx < -mRightMargin) {
  30.                     dx = -mRightMargin;
  31.                 }
  32.                 layoutContextView(dx);
  33.                 if(mMenuShow)
  34.                     event.setAction(MotionEvent.ACTION_CANCEL);
  35.                 return super.onTouchEvent(event);
  36.             case MotionEvent.ACTION_CANCEL:
  37.                 Log.d(TAG, "Event ACTION_CANCEL mMenuShow:" + mMenuShow);
  38.             case MotionEvent.ACTION_UP:
  39.                 int dis = mContextView.getLeft();
  40.                 /**
  41.                  * dis > 0, move to right, dis < mLeftMargin/2, close menu, dis < mLeftMargin open menu.
  42.                  * dis < 0, move to left, dis > -mRightMargin/2, close menu, dis > -mRightMargin, open menu
  43.                  */
  44.                 if(dis > 0 && mLeftMenuView != null) {
  45.                     if(dis < mLeftMargin/2) {
  46.                         mScroller.startScroll(dis, 0, -dis, 0, mScrollTime);
  47.                     } else if(dis < mLeftMargin) {
  48.                         mScroller.startScroll(dis, 0, mLeftMargin-dis, 0, mScrollTime);
  49.                     }
  50.                     postInvalidate();
  51.                 } else if(dis < 0 && mRightMenuView != null) {
  52.                     if(dis > -mRightMargin/2) {
  53.                         mScroller.startScroll(dis, 0, -dis, 0, mScrollTime); //close
  54.                     } else if(dis > -mRightMargin) {
  55.                         mScroller.startScroll(dis, 0, -mRightMargin-dis, 0, mScrollTime);
  56.                     }
  57.                     postInvalidate();
  58.                 }
  59.                 mDownX = 0;
  60.                 Log.d(TAG, "Event ACTION_UP mMenuShow:" + mMenuShow);
  61.                 if(mMenuShow)
  62.                     event.setAction(MotionEvent.ACTION_CANCEL);
  63.                 return super.onTouchEvent(event);
  64.             default:
  65.                 return super.onTouchEvent(event);
  66.         }
  67.         return super.onTouchEvent(event);
  68.     }
  69. 3 contextView 的移动和动画
  70. contextView 的移动方式采用layout 方法移动:
  71.     private void layoutContextView(int dx) {
  72.         if(mContextView != null)
  73.             mContextView.layout(dx, 0, mContextView.getMeasuredWidth()+dx, mContextView.getMeasuredHeight());
  74.         if(!mResterListener && (dx == mLeftMargin || dx == -mRightMargin)) {
  75.             Log.d(TAG, "registerListener dx:" + dx);
  76.             registerListener(dx);
  77.         } else if (mResterListener && (dx ==0 || (dx > 0 && dx != mLeftMargin ) || (dx < 0 && dx != -mRightMargin))) {
  78.             Log.d(TAG, "unregisterListener dx:" + dx);
  79.             unregisterListener(dx);
  80.         }
  81.     }

当滑动到一半的时候手里离开,这是菜单要关闭或者打开,动画采用Scroller 的方式,在UP 事件中调用mScroller.startScroll, 重写computerScroll

    public void computeScroll() {

        super.computeScroll();

        if(mScroller.computeScrollOffset()) {

            layoutContextView(mScroller.getCurrX());

            postInvalidate();

        }

    }

4 监听事件的处理

  1. 定义interface 和 事件注册,
  2. menu 菜单在init 的时候已经设置不可见,在 layoutContextView 的时候,判断滑动距离,如果大于0 设置菜单menu 可见,如果滑动到了菜单宽度的位置,菜单完全打开,注册监听事件。避免menu View 和SwipeMenuLayout 滑动事件冲突。
  3. 由于我们拦截了事件,ListView 的 OnItemClick 无法响应,因此需要我们单独写一个Listener,处理整个SwipeMenuLayout 的点击事件响应。把这个事件响应放到整个SwipeMenuLayout,在initView() 函数中设置了OnClickListener,所以在 OnTouchEvent 中每个事件都调用了 super.OnTouchEvent()。但是这样处理的话响应滑动的同时也响应了OnClickListener, 需要处理滑动和OnClickListener 的事件冲突,CANCAL 事件来了,一旦我们进入了滑动模式,在MOVE 事件中发送CANCAL事件给super,OnClickListener就无法响应。

   

  1. public interface OnMenuClickListener {
  2.         void onMenuClick(View v, int position);
  3.         void onItemClick(View v, int position);
  4.     }
  5.     private void registerListener(int dis) {
  6.         if(mLeftMenuView != null && dis == mLeftMargin) {
  7.             mLeftMenuView.setOnClickListener(new OnClickListener() {
  8.                 @Override
  9.                 public void onClick(View v) {
  10.                     if (mOnMenuClickListener != null)
  11.                         mOnMenuClickListener.onMenuClick(v, mPosition);
  12.                 }
  13.             });
  14.         }
  15.         if(mRightMenuView != null && dis == -mRightMargin) {
  16.             mRightMenuView.setOnClickListener(new OnClickListener() {
  17.                 @Override
  18.                 public void onClick(View v) {
  19.                     if (mOnMenuClickListener != null)
  20.                         mOnMenuClickListener.onMenuClick(v, mPosition);
  21.                 }
  22.             });
  23.         }
  24.         mResterListener = true;
  25.     }
  26.     private void unregisterListener(int dis) {
  27.         if(mLeftMenuView != null && dis > 0) {
  28.             mLeftMenuView.setOnClickListener(null);
  29.         }
  30.         if(mRightMenuView != null && dis < 0) {
  31.             mRightMenuView.setOnClickListener(null);
  32.         }
  33.         mResterListener = false;
  34.     }

5 使用

  1. 定义ListView 的Item XML 注意左右菜单 和context View的ID;左右菜单根据需要定义,不需要不定义即可
  2. 在重写Adapter 的时候 getView 中设置position, 重写OnMenuClickListener,设置监听事件: ((SwipeMenuLayout)convertView).setPosition(position); ((SwipeMenuLayout)convertView).setOnMenuClickListener(ListViewActivity.this);

三 总结

不需要重写ListView 和 Adapter,菜单可以用XML定义好即可,是不是很简单。 传送门:

阅读(3243) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~