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: 代码中注释的很清楚
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | /** * 横向的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:
这个类代码有点长,我们分成三部分看,只粘贴核心代码,剩下的看一下应该就懂啦。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | 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
来完成的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | /** * 滑动事件,用于外边调用的接口 * 这是一个对外暴露的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距离。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | 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,
这个代码很长,看的时候需要耐心。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 | 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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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永远相等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | //这个方法永远执行不到!作者的愿意是当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
如果您觉得阅读本文对您有帮助,请点一下�?推荐”按钮,您的“推荐�?将是我最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利�?
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步