自定义View:侧滑RecycleView
控件功能详解:
1.侧滑RecycleView,侧滑弹出隐藏菜单(删除)
2.可在ViewPage中使用,解决了横向滑动冲突的问题
3.SideRecycleView 并没有暴露出侧滑菜单的点击事件,可在自己的ViewHolder中实现点击事件,优点就是可以添加多个隐藏菜单,只要保证这些隐藏菜单有一个共同的父布局即可。
使用注意事项:
1、使用SideRecycleView item样式必须为 水平父布局【1+1】模式。即显示的布局是一个共同的大布局,隐藏的菜单布局是一个共同的大布局。都可以包含若干个子布局。
2.使用SideRecycleView必须使用水平的LinearLayoutManager。
3.为解决RecycleView侧滑菜单和ViewPage嵌套带来的冲突, 添加速度判断,缓慢向左滑,显示隐藏菜单,快速滑动,切换ViewPage
4.SideRecycleView 并没有暴露出侧滑菜单的点击事件方法,可在自己的viewHolder中实现点击事件,所以在点击侧滑菜单部分时并不会恢复正常的显示,需要调用public方法closeMenu()。
效果图如下
好了,话不多说,看代码,注释也写的比较详细了
import android.content.Context;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.LinearInterpolator;
import android.widget.Scroller;
/**
* 侧滑RecycleView,侧滑弹出隐藏菜单(删除)
* 注意事项:
* 1、使用SideRecycleView item样式必须为 水平父布局【1+1】模式
* 即显示的布局是一个共同的大布局,隐藏的菜单布局是一个共同的大布局。都可以包含若干个子布局
* 2、使用SideRecycleView必须使用水平的LinearLayoutManager
* 3、为解决RecycleView侧滑菜单和ViewPage嵌套带来的冲突,
* 添加速度判断,缓慢向左滑,显示隐藏菜单,快速滑动,切换ViewPage
*/
public class SideRecycleView extends RecyclerView {
/**
* 上一次移动的X点
*/
private int lastMoveX;
/**
* 上一次移动的Y点
*/
private int lastMoveY;
/**
* 滚动条
*/
private Scroller mScroller;
/**
* 当前选中的item的位置
*/
private int selectItemPosition;
/**
* 是否是第一次onTouch
*/
private boolean isFirst = true;
/**
* 本次选中的item
*/
private View item;
/**
* 上一次选择的item布局
*/
private View mLastItem;
/**
* 记录连续移动的长度
*/
private int mMoveWidth = 0;
/**
* 隐藏布局的宽度
*/
private int mHiddenWidth = 0;
/**
* 速度追踪器
*/
private VelocityTracker mVelocityTracker;
/**
* 设置最大速度(超过这个速度,不显示隐藏菜单)
*/
private int MaxVelocity = 2000;
public SideRecycleView(@NonNull Context context) {
this(context, null);
}
public SideRecycleView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SideRecycleView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mScroller = new Scroller(context, new LinearInterpolator(context, null));
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int eventX = (int) event.getX();
int eventY = (int) event.getY();
obtainVelocity(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录当前按下点的坐标(以自身view左上角为原点)
lastMoveX = eventX;
lastMoveY = eventY;
//计算当前选中的item的位置
selectItemPosition = pointToPosition(eventX, eventY);
if (isFirst) {
//第一次时,不用重置上一次的Item
isFirst = false;
} else {
//屏幕再次接收到点击时,恢复上一次item状态
if (mLastItem != null && mMoveWidth > 0) {
//将Item右移,恢复原位
if (mLastItem == item) {
//点击同一个item
if (((ViewGroup) item).getChildCount() == 2) {
View view = ((ViewGroup) item).getChildAt(1);
Rect rect = new Rect();
view.getGlobalVisibleRect(rect);//以view在屏幕坐标系中的左上为原点,计算当前view在屏幕坐标中的可视区域,当前view的实际大小减去超出屏幕或被挡部分
if (!rect.contains((int) event.getRawX(), (int) event.getRawY())) {
//event.getRawX()当前触摸事件距离整个屏幕左边的距离
// event.getRawY()当前触摸事件距离整个屏幕顶边的距离
//说明不在显示出来的菜单布局上
scrollRight(mLastItem, (0 - mMoveWidth));
//清空变量
mMoveWidth = 0;
mHiddenWidth = 0;
}
}
} else {
scrollRight(mLastItem, (0 - mMoveWidth));
//清空变量
mMoveWidth = 0;
mHiddenWidth = 0;
}
}
}
//取到当前选中的Item,赋给mCurItemLayout,以便对其进行左移
int firstVisibleItemPosition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
item = getChildAt(selectItemPosition - firstVisibleItemPosition);//获取当前选中的item
if (item != null) {
if (((ViewGroup) item).getChildCount() == 2) {
//有两个子布局(可见布局+隐藏的菜单布局,菜单布局内可多个菜单item)
// getChildAt( 0 )是可见布局,getChildAt( 1 )是隐藏布局 菜单布局
//这里将删除按钮的宽度设为可以移动的距离
mHiddenWidth = ((ViewGroup) item).getChildAt(1).getWidth();
getParent().requestDisallowInterceptTouchEvent(true);
}
}
break;
case MotionEvent.ACTION_MOVE:
mVelocityTracker.computeCurrentVelocity(1000);设置units的值为1000,意思为一秒时间内运动了多少个像素
int xVelocity = (int) mVelocityTracker.getXVelocity();//获取X方向上的滑动速度
int yVelocity = (int) mVelocityTracker.getYVelocity();//获取Y方向上的滑动速度
if (Math.abs(xVelocity) > Math.abs(yVelocity) && Math.abs(xVelocity) < MaxVelocity) {
// X方向的速度大于Y方向的速度,并且X方向上的速度小于最大速度,让RecycleView执行侧滑
getParent().requestDisallowInterceptTouchEvent(true);
return true;
} else {
closeMenu();
releaseVelocity();//重置速度追踪器
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
releaseVelocity();
getParent().requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_CANCEL:
//以外移动中断
releaseVelocity();
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int eventX = (int) event.getX();
int eventY = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//没有拦截,调用不到
break;
case MotionEvent.ACTION_MOVE:
int xMove = eventX;//移动后的X点坐标
int yMove = eventY;//移动后的Y点坐标
//值为负值时,手指向左滑动;值为正时,手指向右滑动。这与Android的屏幕坐标定义有关
int x = xMove - lastMoveX;
int y = yMove - lastMoveY;
if (x < 0 && Math.abs(y) < Math.abs(x)) {
//x < 0 并且x轴滑动距离>y轴滑动距离时认为是左滑
int newScrollX = Math.abs(x);
if (mMoveWidth > mHiddenWidth) {
//本次移动的宽度>隐藏布局的宽度就不能继续移动了
newScrollX = 0;
} else if ((mMoveWidth + newScrollX) > mHiddenWidth) {
//本次移动的宽度+已经移动的距离 > 隐藏布局的宽度 就不能移动折磨多距离了,否则会显示出白边
//应该移动剩余隐藏布局的宽度
newScrollX = mHiddenWidth - mMoveWidth;
} else if ((mMoveWidth + newScrollX) < mHiddenWidth) {
newScrollX = Math.abs(x);
}
//左滑,每次滑动手指移动的距离
scrollLeft(item, newScrollX);
//计算已经移动的距离
mMoveWidth = mMoveWidth + newScrollX;
lastMoveX = eventX;
} else if (x > 0 && Math.abs(y) < Math.abs(x)) {
//x > 0 并且 x轴滑动距离>y轴滑动距离时认为是右滑
scrollRight(item, 0 - mMoveWidth);//执行右滑,这里没有做跟随,瞬间恢复
mMoveWidth = 0;//清空数据
}
break;
case MotionEvent.ACTION_UP:
if (item != null) {
int scrollX = item.getScrollX();//已经滑动的距离
if (mHiddenWidth > mMoveWidth) {
int toX = mHiddenWidth - mMoveWidth;//剩余隐藏的长度
if (scrollX > mHiddenWidth / 2) {
//超过一半长度时松开,则自动滑到左侧
scrollLeft(item, toX);
mMoveWidth = mHiddenWidth;
} else {
//不到一半时松开,则恢复原状
scrollRight(item, 0 - mMoveWidth);
mMoveWidth = 0;
}
}
invalidate();//刷新视图
mLastItem = item;
releaseVelocity();
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_CANCEL:
closeMenu();
releaseVelocity();
invalidate();//刷新视图
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
item.scrollBy(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
super.computeScroll();
}
/**
* 计算当前选中的item的位置
*
* @param x
* @param y
* @return
*/
private int pointToPosition(int x, int y) {
if (getLayoutManager() == null) {
return -1;
}
int firstVisibleItemPosition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
Rect itemRect = new Rect();
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child.getVisibility() == VISIBLE) {
child.getHitRect(itemRect);//以父类左上角为原点
if (itemRect.contains(x, y)) {
return firstVisibleItemPosition + i;
}
}
}
return -1;
}
/**
* 向左滑动
*/
private void scrollLeft(View item, int scrollX) {
if (item != null) {
item.scrollBy(scrollX, 0);
}
}
/**
* 向右滑动
*/
private void scrollRight(View item, int scrollX) {
if (item != null) {
item.scrollBy(scrollX, 0);
}
}
/**
* 恢复原来的状态
*/
public void closeMenu() {
if (item != null && item.getScaleX() != 0) {
item.scrollTo(0, 0);
mHiddenWidth = 0;
mMoveWidth = 0;
}
}
/**
* 清空速度追踪器
*/
private void releaseVelocity() {
if (mVelocityTracker != null) {
mVelocityTracker.clear();
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
/**
* 给速度追踪器绑定移动事件
*
* @param event
*/
private void obtainVelocity(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
}
}
上述代码就是整个SideRecycleView的全部逻辑,可以看出,主要逻辑就是在处理事件分发和重写onTouchEvent。
在代码中有这么一段,这句话的意思就是点击的这个item有两个子布局,即证明这个item有隐藏的菜单布局,可以侧滑显示菜单
((ViewGroup) item).getChildCount() == 2
- 下面这段代码的意思就是判断上次选中的item有没有恢复原状
- 如果上次选中的item侧滑菜单显示 && 上次选中的item和这次选中的item是同一个item
- 获取到隐藏布局的矩形区域,判断触摸的点的坐标是否在这个区域内,
- 如果是在这个区域内,证明点击了侧滑菜单,就不处理,等待点击事件的响应,在点击事件中关闭菜单。
- 如过直接在代码里关闭侧滑菜单,就会出现还没有执行到点击事件,侧滑菜单就关闭了,点击事件无响应。
- 原因就是onInterceptTouchEvent和onTouchEvent在事件分发处理机制中执行的优先级高于点击事件。
- 如果不是,就关闭侧滑菜单
if (mLastItem != null && mMoveWidth > 0) {
//将Item右移,恢复原位
if (mLastItem == item) {
//点击同一个item
if (((ViewGroup) item).getChildCount() == 2) {
View view = ((ViewGroup) item).getChildAt(1);
Rect rect = new Rect();
view.getGlobalVisibleRect(rect);//以view在屏幕坐标系中的左上为原点,计算当前view在屏幕坐标中的可视区域,当前view的实际大小减去超出屏幕或被挡部分
if (!rect.contains((int) event.getRawX(), (int) event.getRawY())) {
//event.getRawX()当前触摸事件距离整个屏幕左边的距离
// event.getRawY()当前触摸事件距离整个屏幕顶边的距离
//说明不在显示出来的菜单布局上
scrollRight(mLastItem, (0 - mMoveWidth));
//清空变量
mMoveWidth = 0;
mHiddenWidth = 0;
}
}
}
在onInterceptTouchEvent方法的MotionEvent.ACTION_DOWN:中有这么一段代码,在MotionEvent.ACTION_DOWN:中添加如下代码,是请求父布局不要拦截这个触摸事件。这样才能保证会走onInterceptTouchEvent:中的MotionEvent.ACTION_MOVE:方法,否则,父布局以上如果存在可滑动的控件,就会被消费掉。这里的move方法中的逻辑就不会走 (删除线部分我是这么理解的,可能不一定对,毕竟我还是个小白。滑稽保命!!!)
getParent().requestDisallowInterceptTouchEvent(true);
onInterceptTouchEvent方法中的MotionEvent.ACTION_MOVE:中判断,如果水平方向上的滑动速度大于垂直方向的滑动速度,并且水平方向速度小于自己设置的最大速度,就认为是水平滑动,可以显示出隐藏菜单,就该执行onTouchEvent中的逻辑,所以要return true;,并且告诉父类请求父类不要拦截。
注意一定要在up事件中添加如下代码允许父类可以拦截,否则父类中的滚动逻辑等就不会被执行
getParent().requestDisallowInterceptTouchEvent(false);
在布局文件中直接使用即可
<com.xxx.view.SideRecycleView
android:id="@+id/recycle"
android:layout_width="match_parent"
android:layout_height="match_parent" />
item的布局只要满足水平父布局【1+1】的效果,并且隐藏菜单布局宽度需要具体值,不能使用自适应或充满。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_item_fragment_content_sign_replenish_orders"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">必须是水平布局
<FrameLayout
android:id="@+id/frame_layout_item_fragment_content_sign_replenish_orders"
android:layout_width="match_parent"这块必须是充满
android:layout_height="wrap_content"
android:background="@color/colorPrimary">
<RelativeLayout
android:id="@+id/rl_item_fragment_content_sign_replenish_orders"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<RelativeLayout
android:id="@+id/rl_item_fragment_content_sign_replenish_orders_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:paddingRight="6dp"
android:paddingBottom="6dp">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="20dp"
android:clickable="false"
android:gravity="center"
android:paddingHorizontal="6dp">
<TextView
android:id="@+id/tv_item_fragment_content_sign_replenish_orders_recycle_item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:text="内容" />
<TextView
android:id="@+id/tv_item_fragment_content_sign_replenish_orders_recycle_item_title_content"
android:textSize="16sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_toRightOf="@id/tv_item_fragment_content_sign_replenish_orders_recycle_item_title"
android:text="给药方式:" />
<TextView
android:id="@+id/tv_item_fragment_content_sign_replenish_orders_recycle_item_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/tv_item_fragment_content_sign_replenish_orders_recycle_item_title_content"
android:textSize="16sp"
android:text="40.6"/>
</RelativeLayout>
</RelativeLayout>
<TextView
android:id="@+id/tv_item_fragment_content_sign_replenish_orders_recycle_item_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/rl_item_fragment_content_sign_replenish_orders_item"
android:layout_alignParentRight="true"
android:layout_marginTop="18dp"
android:text="今天 2020.4.2 12:12"/>
</RelativeLayout>
</FrameLayout>
<LinearLayout
android:id="@+id/ll_delete"
android:layout_width="72dp" 这块必须是具体值
android:layout_height="match_parent"
android:gravity="center">
<ImageView
android:id="@+id/img_add_content_sign_replenish_orders_right_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="40dp"
android:src="@color/colorPrimary"/>
</LinearLayout>
</LinearLayout>