继上一篇 Android View的事件分发机制和滑动冲突解决的理论知识铺垫,我们也来撸起袖子仿QQ侧滑造个轮子。
欢迎到Github star
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies {
compile 'com.github.qdxxxx:SwipeMenuContainer:v1.0.3'
}
Layout
"match_parent"
android:layout_height="70dp"
qdx:isLeftMenu="false" //false(右侧是菜单),true(左侧是菜单)
...
/>
演示apk下载地址 : https://fir.im/6q2m
name | format | 中文解释 |
---|---|---|
isLeftMenu | boolean | 菜单是否在内容左边(如果在左边,则右滑) |
enableParentLongClick | boolean | 允许父类长按(此时内容就会被拦截down事件) |
expandRatio | float | 菜单能够自动打开的阈值 |
expandDuration | integer | 菜单展开动画时间 |
collapseRatio | float | 菜单能够自动关闭的阈值 |
collapseDuration | integer | 菜单关闭动画时间 |
collapseInstant | void | 不显示动画,关闭菜单 |
collapseSmooth | void | 平滑关闭菜单 |
expandSmooth | void | 平滑打开菜单 |
借鉴SwipeDelMenuLayout
超强的BaseRecyclerViewAdapterHelper
首先我们来分析一下侧滑菜单的整个流程
首先onLayout有两个操作思路,第一是代码家的SwipeLayout ,如下图所示
我们采用SwipeDelMenuLayout的设计方式
通过布局中isLeftMenu来设置当前菜单布局的摆放位置(只能为左或者是右),然后菜单的每一项便会以LinearLayout
的horizontal
方式排列。此方法的有点是上手快,逻辑判断少,缺点就是只能允许一侧为菜单布局。
下面我们来layout菜单布局,如上图所示,如果是左侧为菜单,我们只需依次layout出“置顶”,“点赞”,“收藏”的位置。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = l;
int right = r;
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
if (childView.getVisibility() == GONE)
continue;
if (i == 0) {
childView.layout(left, getPaddingTop(), left + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight());
} else {
if (isLeftMenu) {//菜单在左边
childView.layout(left - childView.getMeasuredWidth(), getPaddingTop(), left, getPaddingTop() + childView.getMeasuredHeight());
left = left - childView.getMeasuredWidth();
} else {//菜单在右边
childView.layout(right, getPaddingTop(), right + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight());
right = right + childView.getMeasuredWidth();
}
}
}
}
我们只需Measure布局上所有的子View即可
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
childView.setClickable(true);//设置子View可以点击
if (childView.getVisibility() == GONE)
continue;
measureChild(childView, widthMeasureSpec, heightMeasureSpec);//measure所有的子View
}
}
只负责记录手指触摸的坐标,并且将所有的开关标记(是否打开菜单,是否为单击等)重置。
mPointGapF.x = ev.getX();////用来设置滑动的x轴方向距离,即展开/关闭menu
mPointDownF.x = ev.getX();//记录手指第一次点击down的x轴位置
mPointDownF.y = ev.getY();//记录手指第一次点击down的y轴位置
move事件也只用来滑动展开/关闭菜单布局,另外还需处理父类的拦截事件。
处理父类拦截事件
mScaleTouchSlop是触发移动事件的最小距离,通过ViewConfiguration.get(context).getScaledTouchSlop()
获取。
这里的拦截分两种情况
float gapX = mPointDownF.x - ev.getX();
float gapY = mPointDownF.y - ev.getY();
if (Math.abs(gapX) < mScaleTouchSlop && Math.abs(gapX) > Math.abs(gapY) * 2f) {
isInterceptParent = true;
} else if (Math.abs(gapX) > mScaleTouchSlop || Math.abs(getScrollX()) > mScaleTouchSlop) {
isInterceptParent = true;
}
if (!isInterceptParent) {
break;
}
getParent().requestDisallowInterceptTouchEvent(true);
处理完了事件拦截,我们就可以随心所欲的展开/关闭菜单布局。
首先介绍一下ScrollTo和ScrollBy :
scrollTo():表示的是移动到哪个坐标点,坐标点的位置就会移动到屏幕原点的位置
scrollBy():表示的是移动的增量dx和dy
getScrollX() : 表示scroll移动x轴的距离
从上图演示可以看出,我们使用的scrollTo()或者是scrollBy()都是基于手机屏幕幕布移动的,也就是说如果菜单布局在内容的右侧,想要滑出“删除”布局,那么scrollBy x轴的值 dx>0。所以我们scroll的对象并不是我们的真正布局对象!而是手机用来展示这个布局的“幕布”。
下面代码我们来展开菜单布局,并且处理滑动越界情况
scrollBy((int) (mPointGapF.x - ev.getX()), 0);//滑动布局
mPointGapF.x = ev.getX();
if (isLeftMenu) {//如果左边是菜单,允许向右滑动
if (-getScrollX() < 0) {//菜单布局向右滑动getScrollX()是<0的
scrollTo(0, 0);
}
if (getScrollX() <= -mWidthofMenu) {//mWidthofMenu菜单布局的总宽度
scrollTo(-mWidthofMenu, 0);//处理越界情况
}
} else {//右边是菜单,向左滑动
if (getScrollX() < 0) {//向左滑动getScrollX()是>0的
scrollTo(0, 0);
}
if (getScrollX() >= mWidthofMenu) {
scrollTo(mWidthofMenu, 0);
}
}
为了完善侧滑效果,我们在ACTION_UP事件处理展开/闭合动画效果,我们依据两个点进行判断
我们引入VelocityTracker
来计算x轴方向用户手指的滑动即时速度
VelocityTracker mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final float velocityX = mVelocityTracker.getXVelocity(mPointerId);//如果有多指触碰,计算第一根手指触碰的速度
我们先分析右侧是菜单(手指向左滑动展开菜单)的情况,mExpandLimit为展开的阈值,mCollapseLimit 为闭合菜单的阈值。(都为菜单宽度的30%)
if (!isExpand) { //如果还没展开
if (getScrollX() > mExpandLimit || (velocityX < -1000 && isInterceptParent)) {
expandSmooth();
} else {
collapseSmooth();
}
} else {//已经展开的
if (getScrollX() < mCollapseLimit || (velocityX > 1000 && isInterceptParent)) {
collapseSmooth();
} else if (!isClickEvent) {//如果是滑动的
expandSmooth();
} else if (ev.getX() < getWidth() - getScrollX()) {
collapseSmooth();//点击内容部分区域View(点击区域分为内容区域及菜单区域)
}
}
最后我们只需要处理是否拦截子view(包括内容布局,菜单布局),须重写onInterceptTouchEvent
方法,拦截的判定条件如下
所谓工欲善其事,必先利其器,总体分析下来仿QQ侧滑菜单的难度并不是很大,关键在于是否掌握了 Android View的事件分发机制和滑动冲突解决。
学习成长的道路亦如此,在前行的道路上披荆斩棘的同时,也应该让“斧子”更加锋利的助我们前行。
欢迎到Github star