Android SwipeMenuListView框架详解分析

周末 特地把Android SwipeMenuListView(滑动菜单)的知识资料整理一番,以下是整理内容:

SwipeMenuListView(滑动菜单)

A swipe menu for ListView.--一个非常好的滑动菜单开源项目。

Demo

 一、简介

看了挺长时间的自定义View和事件分发,想找一个项目练习下。。正好印证自己所学。

在github上找到了这个项目:SwipeMenuListView这的真不错,对事件分发和自定义View都很有启发性,虽然还有点小瑕疵,后面说明。想了解滑动菜单怎么实现的同学,这篇文章绝对对你有帮助,从宏观微观角度详细分析了每个文件。

项目地址:https://github.com/baoyongzhang/SwipeMenuListView/tree/b00e0fe8c2b8d6f08607bfe2ab390c7cee8df274 版本:b00e0fe 它的使用很简单只需要三步,在github上就可以看懂就不占用篇幅啦,本文只分析原理。另外如果你看代码感觉和我不一样,看着困难的话,可以看我加了注释的:http://download.csdn.net/detail/jycboy/9667699

先看两个图:有一个大体的了解

这是框架中所有的类。

1.下面的图是视图层次:

上面的图中:SwipeMenuLayout是ListView中item的布局,分左右两部分,一部分是正常显示的contentView,一部分是滑出来的menuView;滑出来的SwipeMenuView继承自LinearLayout,添加view时,就是横向添加,可以横向添加多个。

2.下面的图是类图结构:

上面是类之间的调用关系,类旁边注明了类的主要作用。

二、源码分析

SwipeMenu​、SwipeMenuItem是实体类,定义了属性和setter、getter方法,看下就行。基本上源码的注释很清楚。

2.1 SwipeMenuView​: 代码中注释的很清楚

/**
 * 横向的LinearLayout,就是整个swipemenu的父布局
 * 主要定义了添加Item的方法及Item的属性设置
 * @author baoyz
 * @date 2014-8-23
 *
 */
public class SwipeMenuView extends LinearLayout implements OnClickListener {

  private SwipeMenuListView mListView;
  private SwipeMenuLayout mLayout;
  private SwipeMenu mMenu;
  private OnSwipeItemClickListener onItemClickListener;
  private int position;

  public int getPosition() {
    return position;
  }

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

  public SwipeMenuView(SwipeMenu menu, SwipeMenuListView listView) {
    super(menu.getContext());
    mListView = listView;
    mMenu = menu; //
    // MenuItem的list集合
    List<SwipeMenuItem> items = menu.getMenuItems();
    int id = 0;
    //通过item构造出View添加到SwipeMenuView中
    for (SwipeMenuItem item : items) {
      addItem(item, id++);
    }
  }
  /**
   * 将 MenuItem 转换成 UI控件,一个item就相当于一个垂直的LinearLayout,
   * SwipeMenuView就是横向的LinearLayout,
   */
  private void addItem(SwipeMenuItem item, int id) {
    //布局参数
    LayoutParams params = new LayoutParams(item.getWidth(),
        LayoutParams.MATCH_PARENT);

    LinearLayout parent = new LinearLayout(getContext());
    //设置menuitem的id,用于后边的点击事件区分item用的
    parent.setId(id);
    parent.setGravity(Gravity.CENTER);
    parent.setOrientation(LinearLayout.VERTICAL);
    parent.setLayoutParams(params);
    parent.setBackgroundDrawable(item.getBackground());
    //设置监听器
    parent.setOnClickListener(this);
    addView(parent); //加入到SwipeMenuView中,横向的

    if (item.getIcon() != null) {
      parent.addView(createIcon(item));
    }
    if (!TextUtils.isEmpty(item.getTitle())) {
      parent.addView(createTitle(item));
    }
  }
  //创建img
  private ImageView createIcon(SwipeMenuItem item) {
    ImageView iv = new ImageView(getContext());
    iv.setImageDrawable(item.getIcon());
    return iv;
  }
  /*根据参数创建title
   */
  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
  /**
   * 用传来的mLayout判断是否打开
   * 调用onItemClick点击事件
   */
  public void onClick(View v) {
    if (onItemClickListener != null && mLayout.isOpen()) {
      onItemClickListener.onItemClick(this, mMenu, v.getId());
    }
  }

  public OnSwipeItemClickListener getOnSwipeItemClickListener() {
    return onItemClickListener;
  }

  /**
   * 设置item的点击事件
   * @param onItemClickListener
   */
  public void setOnSwipeItemClickListener(OnSwipeItemClickListener onItemClickListener) {
    this.onItemClickListener = onItemClickListener;
  }

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

  /**
   * 点击事件的回调接口
   */
  public static interface OnSwipeItemClickListener {
    /**
     * onClick点击事件中调用onItemClick
     * @param view 父布局
     * @param menu menu实体类
     * @param index menuItem的id
     */
    void onItemClick(SwipeMenuView view, SwipeMenu menu, int index);
  }
}

**SwipeMenuView​就是滑动时显示的View,看他的构造函数SwipeMenuView(SwipeMenu menu, SwipeMenuListView listView)​;遍历Items:menu.getMenuItems();调用addItem方法向​SwipeMenuView中添加item。

在addItem方法中:每一个item都是一个LinearLayout​。

2.2 SwipeMenuLayout​:

这个类代码有点长,我们分成三部分看,只粘贴核心代码,剩下的看一下应该就懂啦。

public class SwipeMenuLayout extends FrameLayout {

  private static final int CONTENT_VIEW_ID = 1;
  private static final int MENU_VIEW_ID = 2;

  private static final int STATE_CLOSE = 0;
  private static final int STATE_OPEN = 1;
  //方向
  private int mSwipeDirection;
  private View mContentView;
  private SwipeMenuView mMenuView;
  。。。。。
  public SwipeMenuLayout(View contentView, SwipeMenuView menuView) {
    this(contentView, menuView, null, null);
  }

  public SwipeMenuLayout(View contentView, SwipeMenuView menuView,
      Interpolator closeInterpolator, Interpolator openInterpolator) {
    super(contentView.getContext());
    mCloseInterpolator = closeInterpolator;
    mOpenInterpolator = openInterpolator;
    mContentView = contentView;
    mMenuView = menuView;
    //将SwipeMenuLayout设置给SwipeMenuView,用于判断是否打开
    mMenuView.setLayout(this);
    init();
  }
  private void init() {
    setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT,
        LayoutParams.WRAP_CONTENT));

    mGestureListener = new SimpleOnGestureListener() {
      @Override
      public boolean onDown(MotionEvent e) {
        isFling = false;
        return true;
      }

      @Override
      //velocityX这个参数是x轴方向的速率,向左是负的,向右是正的
      public boolean onFling(MotionEvent e1, MotionEvent e2,
          float velocityX, float velocityY) {
        // TODO
        if (Math.abs(e1.getX() - e2.getX()) > MIN_FLING
            && velocityX < MAX_VELOCITYX) {
          isFling = true;
        }
        Log.i("tag","isFling="+isFling+" e1.getX()="+e1.getX()+" e2.getX()="+e2.getX()+
            " velocityX="+velocityX+" MAX_VELOCITYX="+MAX_VELOCITYX);
        // Log.i("byz", MAX_VELOCITYX + ", velocityX = " + velocityX);
        return super.onFling(e1, e2, velocityX, velocityY);
      }
    };
    mGestureDetector = new GestureDetectorCompat(getContext(),
        mGestureListener);

    。。。。

    LayoutParams contentParams = new LayoutParams(
        LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    mContentView.setLayoutParams(contentParams);
    if (mContentView.getId() < 1) {
      //noinspection ResourceType
      mContentView.setId(CONTENT_VIEW_ID);
    }
    //noinspection ResourceType
    mMenuView.setId(MENU_VIEW_ID);
    mMenuView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
        LayoutParams.WRAP_CONTENT));

    addView(mContentView);
    addView(mMenuView);

  }

 从上边的init方法中可以看出SwipeMenuLayout由两部分组成,分别是用户的 item View 和 menu View 。手指的时候滑动的操作是通过 SimpleOnGestureListener 来完成的。

/**
   * 滑动事件,用于外边调用的接口
   * 这是一个对外暴露的API,而调用这个API的是SwipeMenuListView,那么MotionEvent是SwipeMenuListView的MotionEvent
   * @param event
   * @return
   */
  public boolean onSwipe(MotionEvent event) {
    mGestureDetector.onTouchEvent(event);
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
      mDownX = (int) event.getX();//记下点击的x坐标
      isFling = false;
      break;
    case MotionEvent.ACTION_MOVE:
      // Log.i("byz", "downX = " + mDownX + ", moveX = " + event.getX());
      int dis = (int) (mDownX - event.getX());
      if (state == STATE_OPEN) {//当状态是open时,dis就是0
        Log.i("tag", "dis = " + dis);//这个值一直是0
        //DIRECTION_LEFT = 1 || DIRECTION_RIGHT = -1
        dis += mMenuView.getWidth()*mSwipeDirection;//mSwipeDirection=1
        Log.i("tag", "dis = " + dis + ", mSwipeDirection = " + mSwipeDirection);
      }
      Log.i("tag", "ACTION_MOVE downX = " + mDownX + ", moveX = " + event.getX()+", dis="+dis);
      swipe(dis);
      break;
    case MotionEvent.ACTION_UP:
      //判断滑动距离,是打开还是关闭
      //在这里,如果已经有一个item打开了,此时滑动另外的一个item,还是执行这个方法,怎么改进?
      if ((isFling || Math.abs(mDownX - event.getX()) > (mMenuView.getWidth() / 2)) &&
          Math.signum(mDownX - event.getX()) == mSwipeDirection) {
        Log.i("tag", "ACTION_UP downX = " + mDownX + ", moveX = " + event.getX());
        // open
        smoothOpenMenu();
      } else {
        // close
        smoothCloseMenu();
        return false;
      }
      break;
    }
    return true;
  }

  public boolean isOpen() {
    return state == STATE_OPEN;
  }
  /**
   * 滑动dis的距离,把mContentView和mMenuView都滑动dis距离
   * @param dis
   */
  private void swipe(int dis) {
    if(!mSwipEnable){
      return ;
    }
    //left is positive;right is negative
    if (Math.signum(dis) != mSwipeDirection) {//left=1;right =-1
      dis = 0; //不滑动
    } else if (Math.abs(dis) > mMenuView.getWidth()) {//大于它的宽度,dis就是mMenuView.getWidth()
      dis = mMenuView.getWidth()*mSwipeDirection;
    }
    //重新设置布局,不断左移(或者右移),
    mContentView.layout(-dis, mContentView.getTop(),
        mContentView.getWidth() -dis, getMeasuredHeight());

    if (mSwipeDirection == SwipeMenuListView.DIRECTION_LEFT) {//1
      //同上重新设置menuview的布局,画图很清晰
      mMenuView.layout(mContentView.getWidth() - dis, mMenuView.getTop(),
          mContentView.getWidth() + mMenuView.getWidth() - dis,
          mMenuView.getBottom());
    } else {
      mMenuView.layout(-mMenuView.getWidth() - dis, mMenuView.getTop(),
          - dis, mMenuView.getBottom());
    }
  }
  /**
   * 更新状态state = STATE_CLOSE;
   * 关闭menu
   */
  public void smoothCloseMenu() {
    state = STATE_CLOSE;
    if (mSwipeDirection == SwipeMenuListView.DIRECTION_LEFT) {
      mBaseX = -mContentView.getLeft();
      //滑动mMenuView.getWidth()的距离,正好隐藏掉
      mCloseScroller.startScroll(0, 0, mMenuView.getWidth(), 0, 350);
    } else {
      mBaseX = mMenuView.getRight();
      mCloseScroller.startScroll(0, 0, mMenuView.getWidth(), 0, 350);
    }
    postInvalidate();
  }

  public void smoothOpenMenu() {
    if(!mSwipEnable){
      return ;
    }
    state = STATE_OPEN;
    if (mSwipeDirection == SwipeMenuListView.DIRECTION_LEFT) {
      mOpenScroller.startScroll(-mContentView.getLeft(), 0, mMenuView.getWidth(), 0, 350);
      Log.i("tag","mContentView.getLeft()="+mContentView.getLeft()+", mMenuView="+mMenuView.getWidth());//-451,就是移动的距离dis,-(downX-moveX)
      //mContentView.getLeft()=-540, mMenuView=540 ,这俩的绝对值是相等的,完全正确!哈哈·
    } else {
      mOpenScroller.startScroll(mContentView.getLeft(), 0, mMenuView.getWidth(), 0, 350);
    }
    //在非ui thread中调用这个方法,使视图重绘
    postInvalidate();
  }
  。。。
}

上面主要的方法是onSwipe和swipe这两个方法,主要逻辑是:onSwipe是暴漏给外面调用的API,

在SwipeMenuListView的onTouchEvent事件处理方法中调用了onSwipe;而swipe就是把mContentView和mMenuView都滑动dis距离​。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //宽度是无限扩展的,高度是指定的
    mMenuView.measure(MeasureSpec.makeMeasureSpec(0,
        MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(
        getMeasuredHeight(), MeasureSpec.EXACTLY));
  }
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    mContentView.layout(0, 0, getMeasuredWidth(),
        mContentView.getMeasuredHeight());
    if (mSwipeDirection == SwipeMenuListView.DIRECTION_LEFT) {//左滑
      //相对于父view,以左边和上边为基准,隐藏在右边
      mMenuView.layout(getMeasuredWidth(), 0,
          getMeasuredWidth() + mMenuView.getMeasuredWidth(),
          mContentView.getMeasuredHeight());
    } else {  //右滑,隐藏在左边
      mMenuView.layout(-mMenuView.getMeasuredWidth(), 0,
          0, mContentView.getMeasuredHeight());
    }
  }

 上面的onMeasure、onLayout方法就是自定义View中经常重写的方法,在onMeasure是测量view的大小,这里把宽度类型设置为UNSPECIFIED,可以无限扩展。 onLayout是在view的大小测量之后,把view放到父布局的什么位置,代码里可以看出根据滑动方向吧menuView隐藏在左边(或右边)。

2.3 SwipeMenuAdapter

public class SwipeMenuAdapter implements WrapperListAdapter,
    OnSwipeItemClickListener {

  private ListAdapter mAdapter;
  private Context mContext;
  private SwipeMenuListView.OnMenuItemClickListener onMenuItemClickListener;

  public SwipeMenuAdapter(Context context, ListAdapter adapter) {
    mAdapter = adapter;
    mContext = context;
  }
  。。。。
  /**
   * 添加滑动时的显示的菜单
   * 在这里可以看出每一个Item都是一个SwipeMenuLayout
   */
  public View getView(int position, View convertView, ViewGroup parent) {
    SwipeMenuLayout layout = null;
    if (convertView == null) {
      View contentView = mAdapter.getView(position, convertView, parent);//item的view
      SwipeMenu menu = new SwipeMenu(mContext); //创建SwipeMenu
      menu.setViewType(getItemViewType(position));
      createMenu(menu); //测试的,可以先不管
      SwipeMenuView menuView = new SwipeMenuView(menu,
          (SwipeMenuListView) parent);
      menuView.setOnSwipeItemClickListener(this);
      SwipeMenuListView listView = (SwipeMenuListView) parent;
      layout = new SwipeMenuLayout(contentView, menuView,
          listView.getCloseInterpolator(),
          listView.getOpenInterpolator());
      layout.setPosition(position);
    } else {
      layout = (SwipeMenuLayout) convertView;
      layout.closeMenu();
      layout.setPosition(position);
      View view = mAdapter.getView(position, layout.getContentView(),
          parent);
    }
    if (mAdapter instanceof BaseSwipListAdapter) {
      boolean swipEnable = (((BaseSwipListAdapter) mAdapter).getSwipEnableByPosition(position));
      layout.setSwipEnable(swipEnable);
    }
    return layout;
  }
  //这个方法在创建时,重写啦,在这里是测试的,可以不管。
  public void createMenu(SwipeMenu menu) {
    // Test Code
   。。。。。。
  }
  /**
   * OnSwipeItemClickListener的回掉方法
   * 这个方法在该类创建时,重写啦。
   */
  public void onItemClick(SwipeMenuView view, SwipeMenu menu, int index) {
    if (onMenuItemClickListener != null) {
      onMenuItemClickListener.onMenuItemClick(view.getPosition(), menu,
          index);
    }
  }
  。。。。//省略了不重要的
}

2.4 核心类:SwipeMenuListview,

这个代码很长,看的时候需要耐心。

public class SwipeMenuListView extends ListView {

  private static final int TOUCH_STATE_NONE = 0;
  private static final int TOUCH_STATE_X = 1;
  private static final int TOUCH_STATE_Y = 2;

  public static final int DIRECTION_LEFT = 1; //方向
  public static final int DIRECTION_RIGHT = -1;
  private int mDirection = 1;//swipe from right to left by default

  private int MAX_Y = 5;
  private int MAX_X = 3;
  private float mDownX;
  private float mDownY;
  private int mTouchState;
  private int mTouchPosition;

  private SwipeMenuLayout mTouchView;
  private OnSwipeListener mOnSwipeListener;
  //创建menuItem的
  private SwipeMenuCreator mMenuCreator;
  //menuItem的item点击事件
  private OnMenuItemClickListener mOnMenuItemClickListener;
  private OnMenuStateChangeListener mOnMenuStateChangeListener;
  private Interpolator mCloseInterpolator; //动画变化率
  private Interpolator mOpenInterpolator;

  //----added in myself--下面这两行是我自己加的,
  //你如果下下来代码demo运行下你会发现,当一个item已经滑开时,滑动另外的item,此时原来打开的item没有关闭,可以看下QQ的侧滑,它是关闭的,我这里就稍微修改了下。
  private int mOldTouchPosition = -1;
  private boolean shouldCloseMenu;
  //--------

  public SwipeMenuListView(Context context) {
    super(context);
    init();
  }

  public SwipeMenuListView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init();
  }

  public SwipeMenuListView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
  }
  //初始化变量
  private void init() {
    MAX_X = dp2px(MAX_X);
    MAX_Y = dp2px(MAX_Y);
    mTouchState = TOUCH_STATE_NONE;
  }

  @Override
  /**
   * 对参数adapter进行了一次包装,包装成SwipeMenuAdapter
   */
  public void setAdapter(ListAdapter adapter) {
    super.setAdapter(new SwipeMenuAdapter(getContext(), adapter) {
      @Override
      public void createMenu(SwipeMenu menu) {
        if (mMenuCreator != null) {
          mMenuCreator.create(menu);
        }
      }

      @Override
      public void onItemClick(SwipeMenuView view, SwipeMenu menu,
                  int index) {
        boolean flag = false;
        if (mOnMenuItemClickListener != null) {
          flag = mOnMenuItemClickListener.onMenuItemClick(
              view.getPosition(), menu, index);
        }
        //再次点击list中的item关闭menu
        if (mTouchView != null && !flag) {
          mTouchView.smoothCloseMenu();
        }
      }
    });
  }
  。。。。。
  @Override
  //拦截事件,判断事件是点击事件还是滑动事件
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    //在拦截处处理,在滑动设置了点击事件的地方也能swip,点击时又不能影响原来的点击事件
    int action = ev.getAction();
    switch (action) {
      case MotionEvent.ACTION_DOWN:
        mDownX = ev.getX();
        mDownY = ev.getY();
        boolean handled = super.onInterceptTouchEvent(ev);
        mTouchState = TOUCH_STATE_NONE; //每次Down都把状态变为无状态
        //返回item的position
        mTouchPosition = pointToPosition((int) ev.getX(), (int) ev.getY());

        //得到那个点击的item对应的view,就是SwipeMenuLayout
        View view = getChildAt(mTouchPosition - getFirstVisiblePosition());
        //只在空的时候赋值 以免每次触摸都赋值,会有多个open状态
        if (view instanceof SwipeMenuLayout) {
          //如果有打开了 就拦截.mTouchView是SwipeMenuLayout
          //如果两次是一个mTouchView,更新mTouchView;如果不是一个view,就拦截返回true
          if (mTouchView != null && mTouchView.isOpen() && !inRangeOfView(mTouchView.getMenuView(), ev)) {
            Log.i("tag","Listview中的onInterceptTouchEvent ACTION_DOWN。");
            return true;
          }
          mTouchView = (SwipeMenuLayout) view;
          mTouchView.setSwipeDirection(mDirection);//默认是left=1
        }
        //如果摸在另外一个view,拦截此事件
        if (mTouchView != null && mTouchView.isOpen() && view != mTouchView) {
          handled = true;
        }

        if (mTouchView != null) {
          mTouchView.onSwipe(ev);
        }
        return handled;
      case MotionEvent.ACTION_MOVE: //MOVE时拦截事件,在onTouch中进行处理
        float dy = Math.abs((ev.getY() - mDownY));
        float dx = Math.abs((ev.getX() - mDownX));
        if (Math.abs(dy) > MAX_Y || Math.abs(dx) > MAX_X) {
          //每次拦截的down都把触摸状态设置成了TOUCH_STATE_NONE 只有返回true才会走onTouchEvent 所以写在这里就够了
          if (mTouchState == TOUCH_STATE_NONE) {
            if (Math.abs(dy) > MAX_Y) {
              mTouchState = TOUCH_STATE_Y;
            } else if (dx > MAX_X) {
              mTouchState = TOUCH_STATE_X;
              if (mOnSwipeListener != null) {
                mOnSwipeListener.onSwipeStart(mTouchPosition);
              }
            }
          }
          return true;
        }
    }
    return super.onInterceptTouchEvent(ev);
  }

  @Override
  public boolean onTouchEvent(MotionEvent ev) {
    if (ev.getAction() != MotionEvent.ACTION_DOWN && mTouchView == null)
      return super.onTouchEvent(ev);
    int action = ev.getAction();
    switch (action) {
      case MotionEvent.ACTION_DOWN: //这个DOWN事件的前提是已经拦截事件啦,所以可能的情况时:1.该menu已经滑出来,再点击左边的item区域
                      //2.menu已经滑出来,点击了其他的item
                      //3.滑动item时,先DOWN在MOVE
        Log.i("tag","Listview中的onTouchEvent ACTION_DOWN。是否点击了另一个item");
        int oldPos = mTouchPosition; //这里设计不合理,onInterceptTouchEvent之后直接调用的这个事件,mTouchPosition是一样的
        if(mOldTouchPosition == -1){//-1 is the original value
          mOldTouchPosition = mTouchPosition;
        }
        mDownX = ev.getX();
        mDownY = ev.getY();
        mTouchState = TOUCH_STATE_NONE;

        mTouchPosition = pointToPosition((int) ev.getX(), (int) ev.getY());//list中
        //这里改了,pldPos没有用,改为mOldTouchPosition
        if (mTouchPosition == mOldTouchPosition && mTouchView != null
            && mTouchView.isOpen()) {
          mTouchState = TOUCH_STATE_X; //x方向(横着)滑开
          //调用SwipeMenuLayout的onSwipe()事件接口
          mTouchView.onSwipe(ev);
          Log.i("tag","Listview中的onTouchEvent ACTION_DOWN。滑动了或点击了另一个item");
          return true;
        }
      if(mOldTouchPosition != mTouchPosition){ //when the DOWN position is different
          //shouldCloseMenu = true;
          mOldTouchPosition = mTouchPosition;
        }
        View view = getChildAt(mTouchPosition - getFirstVisiblePosition());
        //已经有一个menu滑开了,此时如果点击了另一个item
        //这个方法永远执行不到!
        if (mTouchView != null && mTouchView.isOpen()) {
          //关闭swipeMenu
          mTouchView.smoothCloseMenu();
          mTouchView = null;
          // return super.onTouchEvent(ev);
          // try to cancel the touch event
          MotionEvent cancelEvent = MotionEvent.obtain(ev);
          cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
          onTouchEvent(cancelEvent); //取消事件,时间结束

          //进行menu close的回掉
          if (mOnMenuStateChangeListener != null) {
            mOnMenuStateChangeListener.onMenuClose(oldPos);
          }
          return true;
        }

        if (view instanceof SwipeMenuLayout) {
          mTouchView = (SwipeMenuLayout) view;
          mTouchView.setSwipeDirection(mDirection);
        }
        if (mTouchView != null) {
          mTouchView.onSwipe(ev);
        }
        break;
      case MotionEvent.ACTION_MOVE:
        //有些可能有header,要减去header再判断
        mTouchPosition = pointToPosition((int) ev.getX(), (int) ev.getY()) - getHeaderViewsCount();
        //如果滑动了一下没完全展现,就收回去,这时候mTouchView已经赋值,再滑动另外一个不可以swip的view
        //会导致mTouchView swip 。 所以要用位置判断是否滑动的是一个view
        if (!mTouchView.getSwipEnable() || mTouchPosition != mTouchView.getPosition()) {
          break;
        }
        float dy = Math.abs((ev.getY() - mDownY));
        float dx = Math.abs((ev.getX() - mDownX));
        if (mTouchState == TOUCH_STATE_X) { //X方向的话
          if (mTouchView != null) {
            mTouchView.onSwipe(ev); //调用滑动事件
          }
          getSelector().setState(new int[]{0});
          ev.setAction(MotionEvent.ACTION_CANCEL);
          super.onTouchEvent(ev);//事件结束
          return true;
        } else if (mTouchState == TOUCH_STATE_NONE) {//DOWN事件后的Move
          if (Math.abs(dy) > MAX_Y) {
            mTouchState = TOUCH_STATE_Y;
          } else if (dx > MAX_X) {
            mTouchState = TOUCH_STATE_X;
            if (mOnSwipeListener != null) {
              mOnSwipeListener.onSwipeStart(mTouchPosition);
            }
          }
        }
        break;
      case MotionEvent.ACTION_UP: //关闭了menu
        Log.i("tag","onTouchEvent事件的ACTION_UP");
        if (mTouchState == TOUCH_STATE_X) {
          if (mTouchView != null) {
            Log.i("tag","onTouchEvent事件的ACTION_UP 为什么没有关闭");
            boolean isBeforeOpen = mTouchView.isOpen();
            //调用滑动事件
            mTouchView.onSwipe(ev);
            boolean isAfterOpen = mTouchView.isOpen();
            if (isBeforeOpen != isAfterOpen && mOnMenuStateChangeListener != null) {
              if (isAfterOpen) {
                mOnMenuStateChangeListener.onMenuOpen(mTouchPosition);
              } else {
                mOnMenuStateChangeListener.onMenuClose(mTouchPosition);
              }
            }
            if (!isAfterOpen) {
              mTouchPosition = -1;
              mTouchView = null;
            }
          }
          if (mOnSwipeListener != null) {
            //进行滑动结束的回掉
            mOnSwipeListener.onSwipeEnd(mTouchPosition);
          }
          ev.setAction(MotionEvent.ACTION_CANCEL);
          super.onTouchEvent(ev);
          return true;
        }
        break;
    }
    return super.onTouchEvent(ev);
  }

  public void smoothOpenMenu(int position) {
    if (position >= getFirstVisiblePosition()
        && position <= getLastVisiblePosition()) {
      View view = getChildAt(position - getFirstVisiblePosition());
      if (view instanceof SwipeMenuLayout) {
        mTouchPosition = position;
        if (mTouchView != null && mTouchView.isOpen()) {
          mTouchView.smoothCloseMenu();
        }
        mTouchView = (SwipeMenuLayout) view;
        mTouchView.setSwipeDirection(mDirection);
        mTouchView.smoothOpenMenu();
      }
    }
  }
  /**
   * 可以进去看源代码,就是将不同的单位统一转换成像素px,这里是dp->px
   * @param dp
   * @return
   */
  private int dp2px(int dp) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
        getContext().getResources().getDisplayMetrics());
  }
  public static interface OnMenuItemClickListener {
    boolean onMenuItemClick(int position, SwipeMenu menu, int index);
  }

  public static interface OnSwipeListener {
    void onSwipeStart(int position);

    void onSwipeEnd(int position);
  }

  public static interface OnMenuStateChangeListener {
    void onMenuOpen(int position);

    void onMenuClose(int position);
  }
  。。。。
}

这个类中最重要的逻辑就是关于事件的判断和分发,什么时候拦截事件,不同的事件对应什么操作。如果对事件分发不清楚的同学,可以在网上找找相关的博客,也可以看我的后续博客,应该这两天的事。

在这里分析SwipeMenuListView的事件分发逻辑:核心就是SwipeMenuListView中item的点击事件和滑动事件的处理。当滑动时SwipeMenuListView拦截事件,自己处理,记住这个逻辑看代码就一目了然了。下面是我画的一个事件分发流程图:

触摸事件是一个事件序列:ACTION_DOWN->ACTION_MOVE->....ACTION_MOVE->ACTION_UP. 以ACTION_DOWN开始,以ACTION_UP结束。

下边是我的一个打印的流程:(自己在代码中加log)

I/tag: Listview中的onInterceptTouchEvent ACTION_DOWN。view=class com.baoyz.swipemenulistview.SwipeMenuLayout
I/tag: onInterceptTouchEvent ACTION_DOWN handled=false
I/tag: SwipeMenuLayout onTouchEvent
I/tag: Listview中的onTouchEvent ACTION_DOWN。是否点击了另一个item
I/tag: oldPos=1 mTouchPosition=1
I/tag: ACTION_MOVE downX = 987, moveX = 906.69666, dis=80
I/tag: ACTION_MOVE downX = 987, moveX = 855.5785, dis=131
I/tag: ACTION_MOVE downX = 987, moveX = 797.6258, dis=189
I/tag: ACTION_MOVE downX = 987, moveX = 735.9639, dis=251
I/tag: ACTION_MOVE downX = 987, moveX = 666.5104, dis=320
I/tag: ACTION_MOVE downX = 987, moveX = 589.0626, dis=397
I/tag: ACTION_MOVE downX = 987, moveX = 509.15567, dis=477
I/tag: ACTION_MOVE downX = 987, moveX = 431.7224, dis=555
I/tag: ACTION_MOVE downX = 987, moveX = 361.2613, dis=625
I/tag: ACTION_MOVE downX = 987, moveX = 319.70398, dis=667
I/tag: onTouchEvent事件的ACTION_UP
I/tag: onTouchEvent事件的ACTION_UP 为什么没有关闭
I/tag: isFling=true e1.getX()=987.08606 e2.getX()=319.70398 velocityX=-4122.911 MAX_VELOCITYX=-1500
I/tag: ACTION_UP downX = 987, moveX = 319.70398
I/tag: mContentView.getLeft()=-540, mMenuView=540

 三、存在的问题

1.如果你下下来框架运行了,你会发现一个问题:

当ListView的一个item已经滑开,假设为item1;此时滑动另外一个的item,叫它item2;

这种情况下item1不会关闭,item2当然也不会打开。

这种效果并不好,我在代码中已经修改了这个问题。具体代码,我已经标明。

2.就是下面的这段代码:在SwipeMenuListView的onTouchEvent(MotionEvent ev)的ACTION_DOWN中,这段代码永远不会执行到,因为​onTouchEvent和onInterceptTouchEvent​对应的一个MotionEvent。

mTouchPosition ==oldPos​永远相等。

//这个方法永远执行不到!作者的愿意是当mTouchPosition != oldPos时CloseMenu,但是按照这个代码这两个值是永远相等的,
        //因为对应的是一个MotionEvent当然就相等啦
        if (mTouchView != null && mTouchView.isOpen()) {
          //关闭swipeMenu
          mTouchView.smoothCloseMenu();
          //mTouchView = null;
          // return super.onTouchEvent(ev);
          // try to cancel the touch event
          MotionEvent cancelEvent = MotionEvent.obtain(ev);
          cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
          onTouchEvent(cancelEvent); //取消事件,时间结束

          //进行menu close的回掉
          if (mOnMenuStateChangeListener != null) {
            mOnMenuStateChangeListener.onMenuClose(oldPos);
          }
          return true;
        }

在代码中我已经修改了这个问题。目前已经在github上提交给原作者啦。

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

(0)

相关推荐

  • Android编程内存溢出与防范方法浅析

    本文实例讲述了Android编程内存溢出与防范方法.分享给大家供大家参考,具体如下: Android的虚拟机是基于寄存器的Dalvik,它的最大堆大小一般是16M.但是Android采用的是Java语言编写,所以在很大程度上,Android的内存机制等同于Java的内存机制,在刚开始开发的时候,内存的限制问题会给我们带来内存溢出等严重问题.在我们不使用一些内存的时候,我们要尽量在Android或者其他平台上避免在运行其他程序时,保存必要的状态,使得一些死进程所带来的内存问题,应该尽量在关闭程序或

  • Android编程实现大图滚动显示的方法

    本文实例讲述了Android编程实现大图滚动显示的方法.分享给大家供大家参考,具体如下: 问题: 我有一张比较大的图片,比如长宽都是屏幕的两倍大小,我想实现的功能是首先将图片居中显示,由于图片太大显然只能显示一部分,然后可以通过拖动,实现图片的平滑滚动(既看不出来滚动刷新痕迹). 就像google地图一样,如果用mapView这个控件,那么可以在屏幕上拖动整个地图,但是由于地图信息量太大,如果一次拖动过快,那么屏幕会暂时显示出一些刷新痕迹(灰白的格子). 想使用mapView来加载已有图片,但是

  • android仿爱奇艺加载动画实例

    本篇文章介绍了android仿爱奇艺加载动画实例,具体代码如下: 效果图: 用到的知识点: Path ValueAnimator 如果对Path和ValueAnimator还不熟悉推荐去看这几个大神的Blog自定义view的目前讲的最适合我的文章 ,自定义view的详细教程和实践,这个也是教程和实践,感谢他们的付出!(希望大家可以认真看完,可以得到很多启发). 拆解动画 一个圆先顺时针的慢慢画出来(圆不是一个闭合的圆) 这一步是一个组合动画,圆慢慢的消失,同时三角形顺时针旋转 这里的难点主要就是

  • Android中activity跳转按钮事件的四种写法

    具体实现代码: public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 方法1. 采用实现OnClickListener接口的类 ((Button) findViewById(R.i

  • Android开发中记一个SwipeMenuListView侧滑删除错乱的Bug

    做侧滑删除网上有很多方案,比如重写Listview实现滑动的监听,今天说下一个SwipeListView,这个是之前一个朋友在网上开源的一个封装组件,能够适用于多种情况,项目地址:https://github.com/baoyongzhang/SwipeMenuListView,我也采用了拿来主义直接拿来用了. 但是在调试运行的滑动删除数据的时候,却出现了一个问题,删除位置错乱,删除的第一个数据,却删除了最后一个,于是找问题呗,我首先用listview试了下,数据是没有问题的,那么说明是删除的时

  • Android 仿苹果IOS6开关按钮

    先给大家展示下效果图: 不知道大家对效果图感觉怎么样,个人觉还不错,感兴趣的朋友可以参考下实现代码哦. public class ToggleButton extends View { private SpringSystem springSystem; private Spring spring ; /** */ private float radius; /** 开启颜色*/ private int onColor = Color.parseColor("#4ebb7f"); /*

  • Android编程闹钟设置方法详解

    本文实例讲述了Android编程闹钟设置方法.分享给大家供大家参考,具体如下: 闹钟在生活中最常见了,在Android中可以通过AlarmManager来实现闹钟,AlarmManager类专门用来设置在某个指定的时间去完成指定的时间.AlarmManager就会通过onReceive()方法去执行这些事件,就算系统处于待机状态,同样不会影响运行.可以通过Context.getSystemService方法来获得该服务.AlarmManager中的方法不少,如下: 方法 说明 Cancel 取消

  • Android SwipeMenuListView框架详解分析

    周末 特地把Android SwipeMenuListView(滑动菜单)的知识资料整理一番,以下是整理内容: SwipeMenuListView(滑动菜单) A swipe menu for ListView.--一个非常好的滑动菜单开源项目. Demo  一.简介 看了挺长时间的自定义View和事件分发,想找一个项目练习下..正好印证自己所学. 在github上找到了这个项目:SwipeMenuListView这的真不错,对事件分发和自定义View都很有启发性,虽然还有点小瑕疵,后面说明.想

  • Python卷积神经网络图片分类框架详解分析

    [人工智能项目]卷积神经网络图片分类框架 本次硬核分享当时做图片分类的工作,主要是整理了一个图片分类的框架,如果想换模型,引入新模型,在config中修改即可.那么走起来瓷!!! 整体结构 config 在config文件夹下的config.py中主要定义数据集的位置,训练轮数,batch_size以及本次选用的模型. # 定义训练集和测试集的路径 train_data_path = "./data/train/" train_anno_path = "./data/trai

  • 低门槛开发iOS、Android、小程序应用的前端框架详解

    现如今跨平台开发技术已不是什么新鲜话题了,在市面上也有一些开源的框架可供选择,然而技术成熟.产品服务健全的平台并不多,其中也不乏推陈出新的框架值得关注. 比如最近使用的AVM,由APICloud迭代推出的多端开发框架,基于JavaScript,兼容多语法,如果是Vue.React的用户,可直接上手,没什么学习成本,具备虚拟DOM,可一次编写多端渲染:主要是APICloud上线已有7年,相对已经成熟,所以我把自己的一些认知和实践结合AVM官方文档的内容做了一下整理,希望能对需要使用跨平台开发技术的

  • Java Mybatis框架多表操作与注解开发详解分析

    目录 一对一查询 多对多查询 Mybatis的注解开发 Mybatis的增删查改 MyBatis的注解实现复杂映射开发 一对一查询 一对一查询的模型 用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户. 一对一查询的需求:查询一个订单,与此同时查询出该订单所属的用户 一对一查询的语句 对应的sql语句: select * from orders o,user u where o.uid=u.id;查询的结果如下: 创建Order和User实体 创建OrderMapper接口 p

  • Java Mybatis框架Dao层的实现与映射文件以及核心配置文件详解分析

    目录 Mybatis的Dao层实现 传统开发方式 代理开发方式 MyBatis映射文件深入 动态sql语句 动态SQL之<if> 动态SQL之<foreach> SQL片段抽取 总结 Mybatis核心配置文件深入 typeHandlers标签 plugins标签 总结 Mybatis的Dao层实现 传统开发方式 1.编写UserDao接口 public interface UserMapper { public List<User> findAll() throws

  • Android LayoutInflater.inflate()详解及分析

    Android  LayoutInflater.inflate()详解 深入理解LayoutInflater.inflate() 由于我们很容易习惯公式化的预置代码,有时我们会忽略很优雅的细节.LayoutInflater以及它在Fragment的onCreateView()中填充View的方式带给我的就是这样的感受.这个类用于将XML文件转换成相对应的ViewGroup和控件Widget.我尝试在Google官方文档与网络上其他讨论中寻找有关的说明,而后发现许多人不但不清楚LayoutInfl

  • Android  LayoutInflater.inflate()详解及分析

    Android  LayoutInflater.inflate()详解 深入理解LayoutInflater.inflate() 由于我们很容易习惯公式化的预置代码,有时我们会忽略很优雅的细节.LayoutInflater以及它在Fragment的onCreateView()中填充View的方式带给我的就是这样的感受.这个类用于将XML文件转换成相对应的ViewGroup和控件Widget.我尝试在Google官方文档与网络上其他讨论中寻找有关的说明,而后发现许多人不但不清楚LayoutInfl

  • Android 文件操作详解及简单实例

     Android 文件操作详解 Android 的文件操作说白了就是Java的文件操作的处理.所以如果对Java的io文件操作比较熟悉的话,android的文件操作就是小菜一碟了.好了,话不多说,开始今天的正题吧. 先从一个小项目入门吧 首先是一个布局文件,这一点比较的简单,那就直接上代码吧. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="htt

  • Android 资源 id详解及的动态获取

    Android 资源 id详解 我们平时获取资源是通过 findViewById 方法进行的,比如我们常在onCreate方法中使用这样的语句: btnChecked=(ImageView)findViewById(R.id.imgCheck); findViewById是我们获取layout中各种View 对象比如按钮.标签.ListView和ImageView的便利方法.顾名思义,它需要一个int参数:资源id. 资源id非常有用.Android回自动为每个位于res目录下的资源分配id,包

  • Java Spring Boot消息服务万字详解分析

    目录 消息服务概述 为什么要使用消息服务 异步处理 应用解耦 流量削峰 分布式事务管理 常用消息中间件介绍 ActiveMQ RabbitMQ RocketMQ RabbitMQ消息中间件 RabbitMQ简介 RabbitMQ工作模式介绍 Work queues(工作队列模式) Public/Subscribe(发布订阅模式) Routing(路由模式) Topics(通配符模式) RPC Headers RabbitMQ安装以及整合环境搭建 安装RabbitMQ 下载RabbitMQ 安装R

随机推荐