话不多说先上图
目前功能如下
- 支持启用或禁用侧滑菜单
- 支持菜单在条目的左边或者右边
- 支持滑动阻塞或非阻塞
- 支持点击了menu后是否自动关闭menu
- 支持menu打开和关闭的回调监听
- 可快速打开和关闭menu
写之前已经有自己的思路,后面看了几个博客的思路想碰撞下,看看会不会有新的启发,果不其然还是有的!!毕竟一个人的思维容易固定化;
我想要的结果就是,功能都在的情况下,不跟列表产生耦合!
先大致讲下思路:
自然是继承ViewGroup,重写它的onMeasure,onLayout;
xml中SwipeMenuLayout为最外层的布局,嵌套的view,第一个view是列表的itemView,之后的都是menuItem;
onMeasure中需要让第一个itemView的宽度为Match_parent;
onLayout中需要根据菜单在左在右的boolean值来控制menu的布局位置;
剩下的就是在dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent中来处理手指的事件分发逻辑了。
下面贴下主要代码;
onMeasure中第一个view必须是Match_parent,高度的话如果itemView是wrap_content,就是子view中最高的一个作为SwipeMenuLayout的高度
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取测量模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//内容view的宽度
int contentWidth = 0;
int contentMaxHeight = 0;
mMenuWidth = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childAt = getChildAt(i);
if (childAt.getVisibility() == View.GONE) {
continue;
}
LayoutParams layoutParams = childAt.getLayoutParams();
if (i == 0) {
//让itemView的宽度为parentView的宽度
layoutParams.width = getMeasuredWidth();
mContentView = childAt;
}
//测量子view的宽高
measureChild(childAt, widthMeasureSpec, heightMeasureSpec);
//如果parentView测量模式不是精准的
if (heightMode != MeasureSpec.EXACTLY) {
contentMaxHeight = Math.max(contentMaxHeight, childAt.getMeasuredHeight());
}
//child测量结束后才能获取宽高
if (i == 0) {
contentWidth = childAt.getMeasuredWidth();
} else {
mMenuWidth += childAt.getMeasuredWidth();
}
}
//取最大值 重新测量
int height = Math.max(getMeasuredHeight(), contentMaxHeight);
setMeasuredDimension(contentWidth, height);
}
onLayout中根据isEnableLeftMenu来确定menu是从左边开始布局还是从右边开始布局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int pLeft = getPaddingLeft();
int pTop = getPaddingTop();
int left = 0;
int right = 0;
for (int i = 0; i < childCount; i++) {
View childAt = getChildAt(i);
if (childAt.getVisibility() == View.GONE) {
continue;
}
if (i == 0) {
childAt.layout(pLeft, pTop, pLeft + childAt.getMeasuredWidth(), pTop + childAt.getMeasuredHeight());
left += pLeft + childAt.getMeasuredWidth();
} else {
//放置左侧
if (isEnableLeftMenu) {
childAt.layout(right - childAt.getMeasuredWidth(), pTop, right, pTop + childAt.getMeasuredHeight());
right -= childAt.getMeasuredWidth();
} else {
//放置右侧
childAt.layout(left, pTop, left + childAt.getMeasuredWidth(), pTop + childAt.getMeasuredHeight());
left += childAt.getMeasuredWidth();
}
}
}
}
下面是主要的一个事件分发的逻辑,如果有对这个分发走向不太清楚的可以参考下这边文章:图解 Android 事件分发机制
贴一下图吧,先大致看下
这个流程图走向很清晰了,我们在onInterceptTouchEvent的move中判断滑动距离,如果滑动距离大于一个值,我们认为它是滑动了,不是点击,这个值可以通过如下方式获取
//获取滑动的最小值,大于这个值就认为他是滑动 默认是8
mScaledTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
再贴下事件分发的逻辑代码,基本都有注释在,看起来也很清晰;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mFirstRawX = ev.getRawX();
getParent().requestDisallowInterceptTouchEvent(false);
//关闭上一个打开的SwipeMenuLayout
chokeIntercept = false;
if (null != mCacheView) {
if (mCacheView != this) {
mCacheView.closeMenuAnim();
chokeIntercept = isOpenChoke;
}
//屏蔽父类的事件,只要有一个侧滑菜单处于打开状态, 就不给外层布局上下滑动了
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//多指触摸状态改变
isFingerTouch = false;
//如果已经侧滑出菜单,菜单范围内的点击事件不拦截
if (Math.abs(getScrollX()) == Math.abs(mMenuWidth)) {
//菜单范围的判断
if ((isEnableLeftMenu && ev.getX() < mMenuWidth)
|| (!isEnableLeftMenu && ev.getX() > getMeasuredWidth() - mMenuWidth)) {
//点击菜单关闭侧滑
if (isClickMenuAndClose) {
closeMenuAnim();
}
break;
}
//否则点击了item, 直接动画关闭
closeMenuAnim();
return true;
}
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (!this.isEnableSwipe) {
return super.onInterceptTouchEvent(ev);
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//多跟手指的触摸处理 isFingerTouch为true的话 表示之前已经有一个down事件了,
if (isFingerTouch) {
return true;
} else {
isFingerTouch = true;
}
//第一个触点的id, 此时可能有多个触点,但至少一个,计算滑动速率用
mPointerId = ev.getPointerId(0);
mLastRawX = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
//大于系统给出的这个数值,就认为是滑动了 事件进行拦截,在onTouch中进行逻辑操作
if (Math.abs(ev.getRawX() - mFirstRawX) >= mScaledTouchSlop) {
longClickable(false);
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
//如果关闭了侧滑 直接super
if (!this.isEnableSwipe) {
return super.onTouchEvent(ev);
}
acquireVelocityTracker(ev);
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
//有阻塞
if (chokeIntercept) {
break;
}
//计算移动的距离
float gap = mLastRawX - ev.getRawX();
//view滑动
scrollBy((int) (gap), 0);
if (Math.abs(gap) > mScaledTouchSlop || Math.abs(getScrollX()) > mScaledTouchSlop) {
getParent().requestDisallowInterceptTouchEvent(true);
}
//超过范围的话--->归位
//目前是右滑的话 (菜单在左边)
if (isEnableLeftMenu) {
if (getScrollX() < -mMenuWidth) {
scrollTo(-mMenuWidth, 0);
} else if (getScrollX() > 0) {
scrollTo(0, 0);
}
} else {
if (getScrollX() < 0) {
scrollTo(0, 0);
} else if (getScrollX() > mMenuWidth) {
scrollTo(mMenuWidth, 0);
}
}
//重新赋值
mLastRawX = ev.getRawX();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//unitis值为1000(毫秒)时间单位内运动了多少个像素 正负最多为mScaledMaximumFlingVelocity
mVelocityTracker.computeCurrentVelocity(1000, mScaledMaximumFlingVelocity);
float velocityX = mVelocityTracker.getXVelocity(mPointerId);
//释放VelocityTracker
recycleVelocityTracker();
if (!chokeIntercept && Math.abs(ev.getRawX() - mFirstRawX) >= mScaledTouchSlop) {
//获取x方向的运动速度
Log.d(TAG, "onTouchEvent: " + velocityX);
//滑动速度超过1000 认为是快速滑动了
if (Math.abs(velocityX) > 1000) {
if (velocityX < -1000) {//左滑了
if (!isEnableLeftMenu) {
//展开Menu
expandMenuAnim();
} else {
//关闭Menu
closeMenuAnim();
}
} else {//右滑了
if (!isEnableLeftMenu) {
//关闭Menu
closeMenuAnim();
} else {
//展开Menu
expandMenuAnim();
}
}
} else {
//超过菜单布局的40% 就展开 反之关闭
if (Math.abs(getScrollX()) > mMenuWidth * 0.4) {//否则就判断滑动距离
//展开Menu
expandMenuAnim();
} else {
//关闭Menu
closeMenuAnim();
}
}
return true;
}
break;
}
return super.onTouchEvent(ev);
}
剩下的部分就是一些不是很关键的了,需要看的话请移步到GitHub中Android侧滑菜单-SwipeMenuLayout