RecyclerView 侧滑按钮实现方法
启发来源于此博客!
上月末由于家中有事耽搁没有更新博客,在此补上。
原理
在按下 RecyclerView 时记录手指按下的位置并记录,在滑动手指的同时滚动控件,在手指松开时判断是否打开侧滑页面还是回弹起始位置。原理就是这么简单,下面根据这个原理一步步实现。
实现步骤
首先定义基本的 Item 布局 layout_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:background="@drawable/shape_white"
android:orientation="horizontal"
android:paddingLeft="10dp">
<TextView
android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:background="@drawable/shape_bg_logout_select"
android:gravity="center"
android:paddingBottom="5dp"
android:paddingLeft="13dp"
android:paddingRight="13dp"
android:paddingTop="5dp"
android:text="上传"
android:textColor="@color/talk_text_white"
android:textSize="14sp" />
<TextView
android:id="@+id/delete"
android:layout_width="100dp"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:background="@color/talk_text_red"
android:gravity="center"
android:text="删除"
android:textColor="@color/talk_text_white"
android:textSize="14sp" />
</LinearLayout>
在layout_item中,定义了 TextView content 占满横屏,定义 TextView delete 宽度为100,这样 delete 在默认的状态下是看不见的。只有当 Item 向左滑动时才能看到这个超出屏幕的 delete。
在 Item 的布局定义完成后,接下来编写基本的 XRecycleView逻辑
public class XRecyclerView extends RecyclerView {
// 本次点击的 Item
private View mRootView;
// 上次点击的 Item
private View mPrevRootView;
// 上次onTouchEvent 的 X,y坐标
private int mPrevX = 0;
private int mPrevY = 0;
// 允许滑动的最大距离
final int MAX_WIDTH = 100;
// MAX_WIDTH 折算为 px的值
private int MAX_WIDTH_DIMEN;
public XRecyclerView(Context context) {
super(context);
init(context);
}
public XRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public XRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
MAX_WIDTH_DIMEN = (int) (context.getResources().getDisplayMetrics().density * MAX_WIDTH + 0.5);
// RecyclerView 默认是没有 divider 的
addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL));
}
}
上述代码中我们继承 RecyclerView 定义了一个简单的 XRecyclerView,它除了满足 RecyclerView的功能以外,其他什么都不能做。
当手指触摸到 RecyclerView 的时候,我们需要知道触摸的是哪个 Item,于是重写 onTouchEvent方法。在 onTouchEvent 触发 Action_Down 的时候,通过findChildViewUnder(x, y)找出点击的坐标属于哪个 Item,然后根据getChildViewHolder(view) 找出与该 Item 绑定的 XRecyclerViewHolder,在 XRecyclerViewHolder 中我们就能找到该 Item 的 ItemView,以及给该 Item 的 delete 按钮添加点击事件了。
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mScrollTotalX = 0;
mScrollTotalY = 0;
//根据点击的坐标获取哪个 Item 被点击了
View view = findChildViewUnder(x, y);
if (view == null) {
// 该事件不进行消费,返回上层控件进行处理
return false;
}
//获取布局 Item 视图
final XRecyclerViewHolder viewHolder = (XRecyclerViewHolder) getChildViewHolder(view);
View itemView = viewHolder.itemView;
// 保存本次点击视图的对象
mRootView = itemView;
if (mOnItemClickListener != null && viewHolder.mDeleteView != null) {
viewHolder.mDeleteView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mOnItemClickListener.onItemDelete(viewHolder.getAdapterPosition());
}
});
}
}
break;
case MotionEvent.ACTION_MOVE:
***
break;
case MotionEvent.ACTION_UP:
***
break;
}
mPrevX = x;
mPrevY = y;
return super.onTouchEvent(event);
}
在触发Action_Move 时,ItemView根据手指滑动距离滚动。
@Override
public boolean onTouchEvent(MotionEvent event) {
***
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
***
break;
case MotionEvent.ACTION_MOVE: {
if (mRootView == null)
return false;
if (Math.abs(mPrevX - x) > 0 && Math.abs(mPrevX - x) > Math.abs(mPrevY - y)) {
int scrollX = mRootView.getScrollX();
int newScrollX = scrollX + mPrevX - x;
if (newScrollX < 0)
newScrollX = 0;
else if (newScrollX > MAX_WIDTH_DIMEN)
newScrollX = MAX_WIDTH_DIMEN;
mRootView.scrollTo(newScrollX, 0);
}
}
break;
case MotionEvent.ACTION_UP:
***
break;
}
mPrevX = x;
mPrevY = y;
return super.onTouchEvent(event);
}
在触发 Action_Up 时,根据 ItemView滑动的距离判断是否打开侧滑部分,还是恢复原始位置。
@Override
public boolean onTouchEvent(MotionEvent event) {
***
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
***
break;
case MotionEvent.ACTION_MOVE:
***
break;
case MotionEvent.ACTION_UP: {
if (mRootView == null)
return false;
int newScrollX;
if (scrollX > MAX_WIDTH_DIMEN / 2) {
newScrollX = MAX_WIDTH_DIMEN;
mPrevRootView = mRootView;
} else
newScrollX = 0;
mRootView.scrollTo(newScrollX, 0);
invalidate();
}
break;
}
mPrevX = x;
mPrevY = y;
return super.onTouchEvent(event);
}
优化
上述通过代码阐述了侧滑实现的基本原理,但是在使用的时候会比较尴尬,比如说,恢复上次滑出的菜单、自然的滑出或回弹菜单、触发 ItemClick点击事件等。这些部分我们都可以在 onTouchEvent 中进行优化。
private void init(Context context) {
mOpenScroller = new OverScroller(context, new LinearInterpolator());
mCloseScroller = new OverScroller(context, new AccelerateInterpolator());
MAX_WIDTH_DIMEN = (int) (context.getResources().getDisplayMetrics().density * MAX_WIDTH + 0.5);
addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL));
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
LogMgr.e("", "x = " + x);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mScrollTotalX = 0;
mScrollTotalY = 0;
//根据点击的坐标获取哪个 Item 被点击了
View view = findChildViewUnder(x, y);
if (view == null) {
// 该事件不进行消费,返回上层控件进行处理
return false;
}
//获取布局 Item 视图
final XRecyclerViewHolder viewHolder = (XRecyclerViewHolder) getChildViewHolder(view);
View itemView = viewHolder.itemView;
// 判断本次点击视图是否为上次点击视图,如果是,则重置位置,并清空本次及上次的点击视图
if (itemView.equals(mPrevRootView)) {
mScrolledView = mPrevRootView;
mPrevRootView = mRootView = null;
mCloseScroller.startScroll(mScrolledView.getScrollX(), 0, -mScrolledView.getScrollX(), 0);
invalidate();
return true;
}
// 如果本次点击的视图,不是上次点击的视图,则恢复上一次点击的视图
if (mPrevRootView != null) {
mScrolledView = mPrevRootView;
mPrevRootView = null;
mCloseScroller.startScroll(mScrolledView.getScrollX(), 0, -mScrolledView.getScrollX(), 0);
invalidate();
}
// 保存本次点击视图的对象
mRootView = itemView;
if (mOnItemClickListener != null && viewHolder.mDeleteView != null) {
viewHolder.mDeleteView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mOnItemClickListener.onItemDelete(viewHolder.getAdapterPosition());
}
});
}
}
break;
case MotionEvent.ACTION_MOVE: {
if (mRootView == null)
return false;
mScrollTotalX += Math.abs(mPrevX - x);
mScrollTotalY += Math.abs(mPrevY - y);
if (Math.abs(mPrevX - x) > 0 && Math.abs(mPrevX - x) > Math.abs(mPrevY - y)) {
int scrollX = mRootView.getScrollX();
int newScrollX = scrollX + mPrevX - x;
if (newScrollX < 0)
newScrollX = 0;
else if (newScrollX > MAX_WIDTH_DIMEN)
newScrollX = MAX_WIDTH_DIMEN;
mRootView.scrollTo(newScrollX, 0);
}
}
break;
case MotionEvent.ACTION_UP: {
if (mRootView == null)
return false;
// 判断是否有滑动,如果没有滑动则触发点击事件
int scrollX = mRootView.getScrollX();
if (mScrollTotalX < 1 && mScrollTotalY < 1) {
// 触发点击事件
int position = getChildAdapterPosition(mRootView);
if (mOnItemClickListener != null && position >= 0) {
mOnItemClickListener.onItemClickLister(mRootView, position);
}
performClick();
return true;
}
/*if (scrollX - 1 <= 0 && Math.abs(mPrevY - y) < 1) {
// 触发点击事件
int position = getChildAdapterPosition(mRootView);
LogMgr.e("", "点了" + position + " 在 scrollX = " + scrollX + " 触发");
if (mOnItemClickListener != null && position >= 0) {
mOnItemClickListener.onItemClickLister(mRootView, position);
}
performClick();
return true;
}*/
int newScrollX;
if (scrollX > MAX_WIDTH_DIMEN / 2) {
newScrollX = MAX_WIDTH_DIMEN;
mPrevRootView = mRootView;
} else
newScrollX = 0;
mOpenScroller.startScroll(scrollX, 0, newScrollX - scrollX, 0);
invalidate();
}
break;
}
mPrevX = x;
mPrevY = y;
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
if (mOpenScroller.computeScrollOffset()) {
if (mRootView != null)
mRootView.scrollTo(mOpenScroller.getCurrX(), mOpenScroller.getCurrY());
}
if (mCloseScroller.computeScrollOffset()) {
if (mScrolledView != null)
mScrolledView.scrollTo(mCloseScroller.getCurrX(), mCloseScroller.getCurrY());
}
invalidate();
}
@Override
public boolean performClick() {
return super.performClick();
}
private OnItemClickListener mOnItemClickListener;
public interface OnItemClickListener {
void onItemClickLister(View itemView, int position);
void onItemDelete(int position);
}
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.mOnItemClickListener = onItemClickListener;
}
原理和代码实现就介绍到这里。
源码请见提交记录– f2dacabb560d6e95c3e8a7656ce6da5f6c5cf124
由于该 XRecyclerView 与布局文件耦合性太强,所以后续还需继续优化,优化方法在下篇博客中给出( 因为我也要想想该怎么弄O(∩_∩)O哈哈~ )。