一、背景
前面已经实现了 RecyclerView 的下拉刷新和上拉加载更多,给 RecyclerView 添加 header,这两个用的比较多,这次实现的是滑动菜单,实现这个是因为产品经理通常会告诉你,我们要做一个和某某应用一样的效果。有人就说了:“这产品总是模仿,总是让做和别人一样的效果(小声嘀咕:关键这还真不那么容易实现)”。这就不能忍,那还有让你更不能忍的,产品告诉你做一个跟 QQ 的滑动菜单一毛一样的效果,然后你去网上一搜“仿 QQ 滑动菜单”,嚯,这么多写好的裤子,直接拿来用;然后产品说,效果要一样,但是某个地方要小改一下,例如,四面八方都加上图标,就像下面这样:
你有点郁闷了,做成一样不就好了,这现成的裤子都是彷 QQ 的,怎么加图标呀。然后你只好去 Read the fucking code,看看是否支持设置图标,或者能不能改一改库源码来实现,不过你惊喜的发现这裤子居然支持 setMenuBtnImage(),通过这个就能直接设置滑动菜单的图标了,但是当你测试了下之后发现,这玩意是写死的,图标只能定在左边,这就不好实现在四面八方加图标了呀,然后你决定改一下库源码来实现一下这个效果,可是你刚准备开改时,扫地大妈来了,大妈看了一眼后很感兴趣的说了:“你们这个效果我见过,我给你们提个建议啊,这个滑出来的菜单要让人一看就感觉热血沸腾,让用户有一种冲冠一怒为红颜的冲动,要让用户觉得他只要点一下就能上天了…”,产品一听,行,你这个建议好啊,我们就这样干。这样吧,我也不为难你们研发的,我给你个动画,你给我放到这个图标的位置,我要放一匹骏马儿在那奔跑,用户一滑出来就能看见,嘚儿驾~。
你仿佛听见了心中羊驼跑过的声音,大妈你是干啥的?你是来公司体验生活的吧?这时刚走开的大妈接起了电话:“房子?都租出去了,对,六套都租出去了”。
你看了看这灯红酒绿的城市,天空中飘满的雾霾,心中想起了家乡的小薇的你无语凝噎。得,自己写一个吧,你想怎么改就怎么改。
下面是最终效果:
二、思路分析
1.要搞一个能跟着手指左右滑动的 Layout,在这个 Layout 里有正文部分和菜单部分,正文部分占满整个控件的宽,菜单部分在正文的右边,手指往左滑动时滑出菜单部分;
2.控制滑动边界,往左滑动时最多滑动到右边菜单完全显示,往右滑动时最多滑动到正文完全显示,也就是初始状态;
3.手指滑完抬起时,判断是否应该是打开状态还是关闭状态,自动滑动到相应位置;
4.Layout 要做滑动冲突处理,以防 Layout 的滑动操作影响菜单的点击事件;
5.把这个写好的 Layout 放到 RecyclerView 的 item 里,并处理这个滑动 Layout 和 RecyclerView 的滑动冲突;
6.仿 QQ 交互优化,用户滑动 RecyclerView 时,开启的滑动菜单会立即关闭;
三、具体实现
1.首先了解一下 Scroller 的用法,不了解的可以看看郭大佬的一篇文章,本文最后贴出了连接,这个类是用来处理 View 滑动的相关操作,主要是为了实现抬起手指后控制 ScrollerLayout平滑滚动到相应的位置的,具体代码如下:
public class ScrollerLayout extends ViewGroup {
/**
* 用于完成滚动操作的实例
*/
private Scroller mScroller;
/**
* 判定为拖动的最小移动像素数
*/
private int mTouchSlop;
/**
* 手机按下时的屏幕坐标
*/
private float mXDown;
/**
* 手机当时所处的屏幕坐标
*/
private float mXMove;
/**
* 上次触发ACTION_MOVE事件时的屏幕坐标
*/
private float mXLastMove;
/**
* 界面可滚动的左边界
*/
private int leftBorder;
/**
* 界面可滚动的右边界
*/
private int rightBorder;
private int mSwipViewWidth;
private int scrolledX;
private float xDown;
private float yDown;
public ScrollerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// 第一步,创建Scroller的实例
mScroller = new Scroller(context);
ViewConfiguration configuration = ViewConfiguration.get(context);
// 获取TouchSlop值
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
setClickable(true);
setFocusable(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredHeight = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 为ScrollerLayout中的每一个子控件测量大小
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
measuredHeight = Math.max(measuredHeight, childView.getMeasuredHeight());
}
setMeasuredDimension(getMeasuredWidth(), measuredHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
int childCount = getChildCount();
if (childCount > 2) {
throw new IllegalStateException("you can have at most two child views!");
}
if (childCount <= 0) {
return;
}
int width = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 为ScrollerLayout中的每一个子控件在水平方向上进行布局
int measuredWidth = childView.getMeasuredWidth();
childView.layout(width, 0, width + measuredWidth, childView.getMeasuredHeight());
width += measuredWidth;
if (i == childCount - 1) {
mSwipViewWidth = measuredWidth;
}
}
// 初始化左右边界值
leftBorder = getChildAt(0).getLeft();
rightBorder = getChildAt(getChildCount() - 1).getRight();
}
}
@Override
public boolean dispatchTouchEvent