SwipeMenuListView框架完全解析

来源:http://www.cnblogs.com/jycboy/archive/2016/10/29/SwipeMenuListView.html
-Advertisement-
Play Games

SwipeMenuListView(滑動菜單) A swipe menu for ListView.--一個非常好的滑動菜單開源項目。 Demo 一、簡介 看了挺長時間的自定義View和事件分發,想找一個項目練習下。。正好印證自己所學。 在github上找到了這個項目:SwipeMenuListVi ...


SwipeMenuListView(滑動菜單)

A swipe menu for ListView.--一個非常好的滑動菜單開源項目。

Demo

Screenshot

 

一、簡介

看了挺長時間的自定義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上提交給原作者啦。

轉載請註明出處:http://www.cnblogs.com/jycboy/p/SwipeMenuListView.html

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 啦啦啦~博主又來騷擾大家啦~大家是不是感覺上次的Android開發博文有點長呢~主要是因為博主也是小白,在做實驗的過程中查詢了很多很多概念,努力去理解每一個知識點,才完成了最終的實驗。還有就是隨著我們的實驗的進行,代碼量也會越來越多,所以在接下來的博文中會對源碼進行取捨,而不會把全部的實驗代碼 ...
  • 1.三級緩存設計步驟: * 從記憶體中取圖片 * 從本地文件中取圖片 向記憶體中保持一份 * 請求網路圖片,獲取圖片,顯示到控制項上 * 向記憶體存一份 * 向本地文件中存一份 2.網路緩存 線程池類Executors的使用 public static ExecutorService newCachedTh ...
  • 1.關聯庫 compile 'com.android.support:design:23.3.0' 2.佈局寫上TabLayout 3.TabLayout和ViewPager關聯 4.解決TabLayout和ViewPagerIndicator的相容問題 4.1 <activity android: ...
  • 學習完第一行代碼後,繼續學習Android群英傳,希望能堅持下去,好好學習完這本書,並及時做好相關筆記,鞏固相關知識; 一. View的介紹: 1`.我們先來看下view樹結構: 2.樹結構內容: a.viewparent是整棵樹的核心,它統一調度和分配所有的交互管理事件。 b.控制項分為viewgr ...
  • ...
  • 前言 啦啦啦~又要和大家一起學習Android開發啦,博主心裡好激動噠~ 在上篇博文中,我們通過線性佈局和基礎組件的使用,完成了一個簡單的學生課外體育積分電子認證系統的界面,本篇博文,將和大家一起熟悉Button、RadioButton、EditText等基本控制項,探討能夠處理這些控制項的基本事件,學 ...
  • 先說下基本動畫部分 基本動畫部分比較簡單, 但能實現的動畫效果也很局限 使用方法大致為: #1. 創建原始UI或者畫面 #2. 創建CABasicAnimation實例, 並設置keypart/duration/fromValue/toValue #3. 設置動畫最終停留的位置 #4. 將配置好的動 ...
  • 前言 學習本系列內容需要具備一定 HTML 開發基礎,沒有基礎的朋友可以先轉至 "HTML快速入門(一)" 學習 本人接觸 React Native 時間並不是特別長,所以對其中的內容和性質瞭解可能會有所偏差,在學習中如果有錯會及時修改內容,也歡迎萬能的朋友們批評指出,謝謝 文章第一版出自簡書,如果 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...