突发奇想,想自己实现RecyclerView中的滑动菜单控件。看了几篇大神的文章,有继承自ViewGroup实现的,有继承RecyclerView实现的,等等...但是都不是太符合我的预期,我希望的是一个简单的,并且能很快使用的旧项目中的。因此,在看了几篇文章后,决定自己来尝试着写一个可以很快使旧项目也能新增滑动菜单的控件。老规矩,先上效果图。
效果图:
效果很常见,没有炫酷的动画,因为要考虑能快速兼容老项目的原因。只是常规的展开和收起,根据拖动的距离来自动判断是该收起还是该展开。
思路分析:
其实刚开始我是针对RecyclerView的触摸事件进行拦截,然后自己去写逻辑来实现滑动效果的。但是技术不佳,实现的效果太差了,而且始终不知道该如何将菜单布局加入进去还不破坏原有的item布局。后来看到一篇文章,使用了HorizontalScrollView,采用了他的思路,发现真的非常方便。链接:RecyclerView 侧滑删除菜单 最简版 没有之一。
因此,我的这个控件也是采用继承HorizontalScrollView来实现,主要分为一个用于包裹Item布局的FrameLayout和一个用于包裹侧滑菜单布局的FrameLayou,它们都被一个水平的LinearLayout包裹。这个LinearLayout最终被HorizontalScrollView所包裹着。
代码实现:
在/res/values下新建一个attr.xml文件,用于自定义属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ScrollMenuLayout">
<attr name="itemLayout" format="reference"/>
<attr name="rightMenuLayout" format="reference"/>
</declare-styleable>
</resources>
然后新建一个Java类,取名ScrollMenuLayout并继承HorizontalScrollView,实现逻辑我将在代码片段中用注释的形式进行说明,这样子更容易理解:
public class ScrollMenuLayout extends HorizontalScrollView {
private static final String TAG = "MenuItem";
private FrameLayout container_item;//用于包裹Item布局的容器
private FrameLayout container_menu_right;//用于包裹侧滑菜单布局的容器
private float lastX;//记录上一次触摸的x轴的值,也就是横向的位置
private float moveX;//累计自手指按下→移动→抬起,这个过程中移动的距离
public ScrollMenuLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setHorizontalScrollBarEnabled(false);//隐藏滚动条
//获取xml文件中的属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ScrollMenuLayout);
//new一个水平的线性布局容器,用来水平放置两个FrameLayout
LinearLayout linearLayout = new LinearLayout(context);
linearLayout.setOrientation(LinearLayout.HORIZONTAL);
//将两个容器实例化
container_item = new FrameLayout(context);
container_menu_right = new FrameLayout(context);
//如果在xml文件中设置了布局id,则直接加载
int item_layout_id = array.getResourceId(R.styleable.ScrollMenuLayout_itemLayout, -1);
if (item_layout_id != -1) {
//将Item布局文件加载到FrameLayout中,注意使用LayoutInflater时,第一个参数为布局文件id,第二个参数为布局容器
//由于我使用了addView的方式,所以第二个参数要填Null,否则会出现Item已经有父容器的错误
container_item.addView(LayoutInflater.from(context).inflate(item_layout_id, null));
}
int menu_layout_right_id = array.getResourceId(R.styleable.ScrollMenuLayout_rightMenuLayout, -1);
if (menu_layout_right_id != -1) {
//同上的逻辑
container_menu_right.addView(LayoutInflater.from(context).inflate(menu_layout_right_id, null));
}
//组装
linearLayout.addView(container_item);
linearLayout.addView(container_menu_right);
addView(linearLayout);
array.recycle();//回收属性数组
}
@Override
protected void onDraw(Canvas canvas) {
//将Item的宽度设为父容器的宽度,用于将侧滑菜单顶出视野
//放在onDraw执行是为了保证能获取到父容器的宽度,这里的父容器指的就是在Adapter中
//onCreateViewHolder方法的第二个参数ViewGroup
ViewGroup.LayoutParams layoutParams = container_item.getLayoutParams();
layoutParams.width = ((ViewGroup) getParent()).getWidth();
container_item.setLayoutParams(layoutParams);
super.onDraw(canvas);
}
/**
* 设置item布局
*
* @param v item布局view
*/
public void setItemView(View v) {
container_item.removeAllViews();
container_item.addView(v);
}
/**
* 获取item布局view
* 方便去做各种监听等等
*
* @return Null or View
*/
public View getItemView() {
if (container_item.getChildCount() > 0) {
return container_item.getChildAt(0);
}
return null;
}
/**
* 设置右边的菜单
*
* @param v 右边菜单布局View
*/
public void setRightMenuView(View v) {
container_menu_right.removeAllViews();
container_menu_right.addView(v);
}
/**
* 获取右边的菜单布局View
*
* @return Null or View
*/
public View getRightMenuView() {
if (container_menu_right.getChildCount() > 0) {
return container_menu_right.getChildAt(0);
}
return null;
}
/**
* 展开右边菜单
*/
public void expandRightMenu() {
arrowScroll(FOCUS_RIGHT);
}
/**
* 收起右边菜单
*/
public void closeRightMenu() {
arrowScroll(FOCUS_LEFT);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = ev.getX();
break;
case MotionEvent.ACTION_MOVE:
//记录移动距离
moveX += ev.getX() - lastX;
lastX = ev.getX();
Log.d(TAG, "menu width = " + container_menu_right.getWidth() + " moveX = " + moveX);
break;
case MotionEvent.ACTION_UP:
if (moveX > 0) {
//意图:收起,手指从左向右滑动
if (Math.abs(moveX) >= container_menu_right.getWidth() / 2) {
//滑动距离大于一半,收起
closeRightMenu();
} else {
//展开
expandRightMenu();
}
} else if (moveX < 0) {
//意图展开,手指从右向左滑动
if (Math.abs(moveX) >= container_menu_right.getWidth() / 2) {
//展开
expandRightMenu();
} else {
//收起
closeRightMenu();
}
}
moveX = 0;//重置
return true;//消费该次事件,不再传递,解决滑动冲突
}
return super.onTouchEvent(ev);
}
}
可以看到,其实主要的实现就是将Item布局的宽度设置为整个HorizontalScrollView父容器的宽度,然后就可以刚好将侧滑菜单的布局给顶出视野范围,我们再重写HorizontalScrollView的滑动逻辑, 当滑动的距离大于侧滑菜单布局的宽度的一半的时候,就自动的将侧滑菜单弹出或者收起。
其实整个控件的逻辑及其简单,只要注意下在onTouch中,event.getX()的值为负数的时候表示手指正在从右向左滑动,反之则为从左向右滑动。
结束语:
到此,整个控件的实现就完成了,由于采用了将Item布局和侧滑菜单布局分开的方式,所以整个控件能很快的替换老项目的布局,并且不需要修改太多的东西就能完成侧滑的功能。当然,如果需要一些比较炫酷的动画,这个就需要大家自己去实现了,我后期也会考虑试试再新增动画的功能。
项目我已经开源到了GitHub上,并且附带了demo,欢迎大家查阅。GitHub链接