动手打造史上最简单的 Recycleview 侧滑菜单
来源:互联网 发布:淘宝开店后怎么卖东西 编辑:程序博客网 时间:2024/06/05 03:20
我的开源库:一句代码搞定 RecycleView 侧滑菜单、添加头部底部、加载更多
本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发。
在实现 Recycleview 侧滑菜单时起初使用了开源库 SwipeRecyclerView ,此库功能广泛,但无法满足个人需求,这是因为此库中存在以下局限性:
- 菜单文字一旦确定将无法修改
- 侧滑时整个 item 都会滑动
- 无法自定义菜单样式
只能自己实现了,查阅资料后发现,较多通过 DragHelper 实现的,它是一个手势滑动辅助工具,使 item 可以滑动,然后…… , 等等!既然目的是让 item 可以滑动,那为什么不直接在 item 布局中使用 HorizontalScrollView 呢?参考鸿神的Android 自定义控件打造史上最简单的侧滑菜单,标题致敬一波~
首先是自定义 SlidingMenu :
public class SlidingMenu extends HorizontalScrollView { private static final float radio = 0.3f;//菜单占屏幕宽度比 private final int mScreenWidth; private final int mMenuWidth; private boolean once = true; public SlidingMenu(Context context, AttributeSet attrs) { super(context, attrs); mScreenWidth = ScreenUtil.getScreenWidth(context); mMenuWidth = (int) (mScreenWidth * radio); setOverScrollMode(View.OVER_SCROLL_NEVER); setHorizontalScrollBarEnabled(false); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (once) { LinearLayout wrapper = (LinearLayout) getChildAt(0); wrapper.getChildAt(0).getLayoutParams().width = mScreenWidth; wrapper.getChildAt(1).getLayoutParams().width = mMenuWidth; once = false; } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override public boolean onTouchEvent(MotionEvent ev) { case MotionEvent.ACTION_UP: int scrollX = getScrollX(); if (Math.abs(scrollX) > mMenuWidth / 2) { this.smoothScrollTo(mMenuWidth, 0); } else { this.smoothScrollTo(0, 0); } return true; } return super.onTouchEvent(ev); }}
在 item 布局中应用 SlidingMenu,注意 SlidingMenu 中是按照顺序区分 content 和 menu 的,所以布局文件中顺序要对应一致 :
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="2dp" android:orientation="vertical" > <com.xmwj.slidingmenu.SlidingMenu android:layout_width="wrap_content" android:layout_height="wrap_content"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" > <LinearLayout android:id="@+id/content" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <ImageView android:id="@+id/imageView" android:layout_width="match_parent" android:layout_height="70dp" android:background="#af6fe1" /> </LinearLayout> <LinearLayout android:id="@+id/menu" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:text="置顶" android:textColor="#fff" android:gravity="center" android:background="@color/colorAccent" /> </LinearLayout> </LinearLayout> </com.xmwj.slidingmenu.SlidingMenu></LinearLayout>
OK~ ,这就实现了具有侧滑菜单的 Recycleview 了!不需要 DragHelper !不需要自定义 Recycleview !不需要处理事件分发!是不是超级简单?与其自定义 Recycleview 然后关联 item 实现,为什么不直接改变 item 布局实现!
简单使用一下这个侧滑菜单,首先是适配器代码:
class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private List<String> mData; private Context mContext; MyAdapter(List<String> data, Context context) { mData = data; mContext = context; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new MyViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item, parent, false)); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { } @Override public int getItemCount() { return mData.size(); } private class MyViewHolder extends RecyclerView.ViewHolder { MyViewHolder(View itemView) { super(itemView); } }}
MainActivity 如下:
public class MainActivity extends AppCompatActivity { private RecyclerView mRecyclerView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView); init(); } private void init() { List<String> data = new ArrayList<>(); for(int i=0;i<20;i++) { data.add(null); } mRecyclerView.setAdapter(new MyAdapter(data, this)); }}
效果如下:
至此已经实现了最简单的 Recycleview 侧滑菜单,但 item 的菜单可以同时打开,一般情况下,当触摸其他 item 时应该关闭已打开的菜单,我们可以将打开的 item 引用记录下来,方便及时关闭。改造 SlidingMenu,添加以下方法 :
/** * 关闭菜单 */ public void closeMenu() { this.smoothScrollTo(0, 0); isOpen = false; } /** * 菜单是否打开 */ public boolean isOpen() { return isOpen; } /** * 当打开菜单时记录此 view ,方便下次关闭 */ private void onOpenMenu() { View view = this; while (true) { view = (View) view.getParent(); if (view instanceof RecyclerView) { break; } } ((MyAdapter) ((RecyclerView) view).getAdapter()).holdOpenMenu(this); isOpen = true; } /** * 当触摸此 item 时,关闭上一次打开的 item */ private void closeOpenMenu() { if (!isOpen) { View view = this; while (true) { view = (View) view.getParent(); if (view instanceof RecyclerView) { break; } } ((MyAdapter) ((RecyclerView) view).getAdapter()).closeOpenMenu(); } }
修改 onTouchEvent 方法:
@Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: closeOpenMenu(); break; case MotionEvent.ACTION_UP: int scrollX = getScrollX(); if (Math.abs(scrollX) > mMenuWidth / 2) { this.smoothScrollTo(mMenuWidth, 0); onOpenMenu(); } else { this.smoothScrollTo(0, 0); } return true; } return super.onTouchEvent(ev); }
显然我们将已打开的 item 记录在适配器中,在 Adapter 中添加记录与关闭方法:
private SlidingMenu mOpenMenu; public void holdOpenMenu(SlidingMenu slidingMenu) { mOpenMenu= slidingMenu; } public void closeOpenMenu() { if (mOpenMenu!= null && mOpenMenu.isOpen()) { mOpenMenu.closeMenu(); } }
OK~ 非常简单,看下效果,当触摸其他 item 时已打开的 item 就会自动关闭啦:
在实际使用时,一般需要监听侧滑菜单的点击事件,此方法中的侧滑菜单是 item 的一部分,对其监听也就非常简单,同样,修改菜单文字也非常简单。下面通过改变菜单文字实现置顶/取消置顶功能。
改造适配器,直接附上 MyAdapter 全部代码:
class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> { private List<String> mData; private Context mContext; private SlidingMenu mOpenMenu; public void holdOpenMenu(SlidingMenu slidingMenu) { mOpenMenu = slidingMenu; } public void closeOpenMenu() { if (mOpenMenu != null && mOpenMenu.isOpen()) { mOpenMenu.closeMenu(); } } MyAdapter(List<String> data, Context context) { mData = data; mContext = context; } @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new MyViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item, parent, false)); } @Override public void onBindViewHolder(final MyViewHolder holder, final int position) { holder.imageView.setBackgroundColor(ContextCompat.getColor(mContext, R.color.colorItem)); holder.menuText.setText(mData.get(position)); holder.menuText.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { closeOpenMenu(); boolean top; if (holder.menuText.getText().toString().equals("置顶")) { holder.menuText.setText("取消置顶"); holder.imageView.setBackgroundColor(ContextCompat.getColor(mContext, R.color.colorTopItem)); top = true; }else{ holder.menuText.setText("置顶"); holder.imageView.setBackgroundColor(ContextCompat.getColor(mContext, R.color.colorItem)); top = false; } if (mOnMenuClickListener != null) { mOnMenuClickListener.onClick(position, top); } } }); } public interface OnMenuClickListener { void onClick(int position,boolean top); } private OnMenuClickListener mOnMenuClickListener; public void setOnMenuClickListener(OnMenuClickListener onMenuClickListener) { this.mOnMenuClickListener = onMenuClickListener; } @Override public int getItemCount() { return mData.size(); } class MyViewHolder extends RecyclerView.ViewHolder { TextView menuText; ImageView imageView; MyViewHolder(View itemView) { super(itemView); menuText = (TextView) itemView.findViewById(R.id.menuText); imageView = (ImageView) itemView.findViewById(R.id.imageView); } }}
其中 imageView 为 item 布局中的 item 内容,menuText 为菜单文字控件,当置顶时 item 颜色变为黄色,菜单文字变为“取消置顶”。菜单点击事件以接口形式公开,代码十分简单,下面是 MainActivity :
public class MainActivity extends AppCompatActivity { private RecyclerView mRecyclerView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView); init(); } private void init() { final List<String> data = new ArrayList<>(); for (int i = 0; i < 20; i++) { data.add("置顶"); } MyAdapter myAdapter = new MyAdapter(data, this); myAdapter.setOnMenuClickListener(new MyAdapter.OnMenuClickListener() { @Override public void onClick(int position, boolean top) { data.set(position, top ? "取消置顶" : "置顶"); } }); mRecyclerView.setAdapter(myAdapter); }}
实现效果:
OK~ 成功弥补第三方库“菜单文字一旦确定将无法修改”这个致命缺陷了。有的场景下,我们不希望整个 item 侧滑,比如:
使用第三库时很难实现这样的需求,而此方法就非常容易了,只需要把底部控件置于 SlidingMenu 外部就可以了,以下是 item 布局:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="2dp" android:orientation="vertical" > <com.xmwj.slidingmenu.SlidingMenu android:layout_width="wrap_content" android:layout_height="wrap_content"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" > <LinearLayout android:id="@+id/content" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <ImageView android:id="@+id/imageView" android:layout_width="match_parent" android:layout_height="70dp" /> </LinearLayout> <LinearLayout android:id="@+id/menu" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > <TextView android:id="@+id/menuText" android:layout_width="match_parent" android:layout_height="match_parent" android:text="置顶" android:textColor="#fff" android:gravity="center" android:background="@color/colorAccent" /> </LinearLayout> </LinearLayout> </com.xmwj.slidingmenu.SlidingMenu> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="item 底部备注 ..." android:background="@color/colorAccent" android:textColor="#fff" android:gravity="center" /></LinearLayout>
轻松解决了 “菜单文字一旦确定将无法修改“,“侧滑时整个 item 都会滑动”这两个缺陷,最后一个“无法自定义菜单样式”,第三方库中高度集成了菜单的生成方式,只能设置单一的文字和图标,有时无法满足需求,而此方法实现自定义菜单样式十分简单,在 item 中直接编写 menu 布局即可。
更新(2017.09.07)
案例写的十分简陋,主要是想分享一下思路,另外这种方案跟 item 布局完全耦合,不适合抽离成共用组件,所以并没有去完善功能,比如评论提到的是否允许侧滑、复用机制导致的视图混乱等等。
而现在,我意识到当初的想法是多么的狭隘,这种方案或许可以抽离成共用组件;有些功能是必需的,比如解决复用机制导致的视图混乱,这不叫“完善”,而是“完成”,下面开始添加这些功能。
添加item点击事件
你可能会想到直接给 content 添加 setOnClickListener 方法,但这是不行的,content 作为 SlidingMenu 的子布局,一旦为 content 设置点击监听,SlidingMenu 将无法响应 ACTION_DOWN 事件,而在 ACTION_DOWN 中做了一个很重要的工作:关闭上次打开菜单的 item 。也就是说如果为 content 添加 setOnClickListener 方法,将导致无法自动关闭已打开的菜单。
既然 setOnClickListener 会拦截 ACTION_DOWN 事件,那我们可以在 onTouchEvent 中产生点击事件,改造 SlidingMenu 的 onTouchEvent 方法,使其产生点击事件并以接口形式公开,代码如下:
long downTime = 0;@Overridepublic boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: downTime = System.currentTimeMillis(); closeOpenMenu(); break; case MotionEvent.ACTION_UP: int scrollX = getScrollX(); if (System.currentTimeMillis() - downTime <= 100 && scrollX ==0) { if (mCustomOnClickListener != null) { mCustomOnClickListener.onClick(); } return false; } if (Math.abs(scrollX) > mMenuWidth / 2) { this.smoothScrollTo(mMenuWidth, 0); onOpenMenu(); } else { this.smoothScrollTo(0, 0); } return true; } return super.onTouchEvent(ev)}interface CustomOnClickListener { void onClick();}private CustomOnClickListener mCustomOnClickListener;void setCustomOnClickListener(CustomOnClickListener listener) { this.mCustomOnClickListener = listener;}
改造 MyAdapter :
// 将原来的菜单监听器:public interface OnMenuClickListener { void onClick(int position,boolean top);}private OnMenuClickListener mOnMenuClickListener;public void setOnMenuClickListener(OnMenuClickListener onMenuClickListener) { this.mOnMenuClickListener = onMenuClickListener;}// 修改为内容&菜单监听器:interface OnClickListener { void onMenuClick(int position, boolean top); void onContentClick(int position);}private OnClickListener mOnClickListener;void setOnClickListener(OnClickListener onClickListener) { this.mOnClickListener = onClickListener;}
然后在 onBindViewHolder 中添加 item 点击事件监听即可:
holder.slidingMenu.setCustomOnClickListener(new SlidingMenu.CustomOnClickListener() { @Override public void onClick() { if (mOnClickListener != null) { mOnClickListener.onContentClick(position); } }});
多指触摸将同时打开多个item
感谢评论区的小伙伴提出的这个问题,首先展示一下”事故现场”:
怎么禁止同时打开多个 item 呢?首先要明白所谓的“同时”并不可能真正同时,肯定有先后之分,我们可以像记录已打开的 item 一样,在 Adapter 中记录第一个开始滑动的 item ,而之后的 item 滑动前先判断一下 Adapter 中是否有记录 ,如果有正在滑动的 item 就禁止滑动,正在滑动 item 的记录周期从 ACTION_DOWN 开始,至 ACTION_UP 结束。首先是 SlidingMenu 的 onTouchEvent 方法:
@Override public boolean onTouchEvent(MotionEvent ev) { // 当有正在滑动的item且不是自身则禁止滑动 if (getScrollingMenu() != null && getScrollingMenu() != this) { return false; } switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: downTime = System.currentTimeMillis(); closeOpenMenu(); setScrollingMenu(this);//记录正在滑动item break; case MotionEvent.ACTION_UP: setScrollingMenu(null);//清空记录 int scrollX = getScrollX(); if (System.currentTimeMillis() - downTime <= 100 && scrollX == 0) { if (mCustomOnClickListener != null) { mCustomOnClickListener.onClick(); } return false; } if (Math.abs(scrollX) > mMenuWidth / 2) { this.smoothScrollTo(mMenuWidth, 0); onOpenMenu(); } else { this.smoothScrollTo(0, 0); } return false; } return super.onTouchEvent(ev); }
setScrollingMenu 和 getScrollingMenu 方法如下:
public SlidingMenu getScrollingMenu() { return getAdapter().getScrollingMenu(); } public void setScrollingMenu(SlidingMenu scrollingMenu) { getAdapter().setScrollingMenu(scrollingMenu); }
与上文中已打开 item 一样,需要记录到 Adapter 中,由于在 SlidingMenu 中多次获取 Adapter ,所以将其封装成一个方法:
private MyAdapter getAdapter() { View view = this; while (true) { view = (View) view.getParent(); if (view instanceof RecyclerView) { break; } } return (MyAdapter) ((RecyclerView) view).getAdapter(); }
然后在 MyAdapter 中添加对应字段及其 set/get 方法即可,这基本上就解决了这个问题,在调试过程中还发现一个问题:当上下滚动 Recycleview 时,有时无法响应 SlidingMenu 的 ACTION_UP 事件,从而无法及时清空正在滑动 item 记录,暂且在 MainActivity 中解决这个问题:
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); myAdapter.setScrollingMenu(null); } });
至此成功解决 。
另外在 QQ、微信上也有这种现象:
这算是 BUG 吗?对于 QQ、微信来说不算是 BUG,但对于本案例来说是。因为同时打开多个菜单后,再触摸未打开 item ,QQ 、微信会自动关闭所有打开的菜单,而本案例无法实现,因为本案例中只记录了上次打开的 item ,所以只能禁止同时打开多个 item 。
这是两种不同的实现方案,一个是允许同时打开多个侧滑菜单 ,并记录所有已打开的菜单;一个是不允许同时打开多个菜单 ,只需记录上次打开的。而我认为后者较好,首先同时打开在交互上是不符合用户习惯的,另外后者不需要遍历操作,性能上更优。
代码已同步更新到 github。
总结
Recycleview 侧滑菜单大多的实现思路是:通过自定义 Recycleview 或 Adapter 提供创建菜单方法,然后内部再关联到各个 item 改变其布局,从而使 item 具有侧滑功能,优点是使用简单,但是不够灵活,比如开始提到的三个局限性。本文实现方法直接在 item 布局中进行设置,使 item 具有侧滑功能,实现过程及其简单,易于理解,应该是最简单的 Recycleview 侧滑菜单了,希望能给你带来帮助。
源码 : https://github.com/yhaolpz/SlidingMenu
强力推荐我的开源项目:一款纯 Kotlin 编写的开源安卓应用 “Smile”
强力推荐我的开源项目:一款纯 Kotlin 编写的开源安卓应用 “Smile”
强力推荐我的开源项目:一款纯 Kotlin 编写的开源安卓应用 “Smile”
嗯~重要的事情说三遍。
参考文章
http://blog.csdn.net/lmj623565791/article/details/39185641
https://github.com/yanzhenjie/SwipeRecyclerView
http://blog.csdn.net/u010386612/article/details/52957095
http://www.jianshu.com/p/94e56cf2cd3c
- 动手打造史上最简单的 Recycleview 侧滑菜单
- 动手打造史上最简单的 Recycleview 侧滑菜单
- Android 自定义控件打造史上最简单的侧滑菜单
- Android 自定义控件打造史上最简单的侧滑菜单
- Android 自定义控件打造史上最简单的侧滑菜单
- Android 自定义控件打造史上最简单的侧滑菜单
- Android 自定义控件打造史上最简单的侧滑菜单
- Android 自定义控件打造史上最简单的侧滑菜单
- Android 自定义控件打造史上最简单的侧滑菜单
- Android 自定义控件打造史上最简单的侧滑菜单
- Android 自定义控件打造史上最简单的侧滑菜单
- Android 自定义控件打造史上最简单的侧滑菜单
- Android 打造史上最简单的侧滑菜单
- Android 自定义控件打造史上最简单的侧滑菜单
- Android 自定义控件打造史上最简单的侧滑菜单
- Android--自定义控件打造史上最简单的侧滑菜单
- Android 自定义控件打造史上最简单的侧滑菜单
- Android 自定义控件打造史上最简单的侧滑菜单
- [BZOJ2440] 完全平方数 莫比乌斯函数+容斥+二分
- 如何实现两个文本框同时输入
- 《PHP MySQL和WEB开发》第八章笔记
- Elasticsearch.5.4.1+kibana5.4.1+IK分词集群搭建
- SpringBoot学习-(七)SpringBoot分页插件PageHelper
- 动手打造史上最简单的 Recycleview 侧滑菜单
- Java之旅--跨域(CORS)
- 【数据库】count(*),count(1)和count(列)
- leetcode 221. Maximal Square 最大正方形面积 + DP
- css3 transition 动画
- 超好用的 Linux 文件管理器推荐
- 《代码整洁之道》读书笔记
- Yii2文件上传
- 时钟源关系初解