当前位置:网站首页>使用RecyclerView,实现列表左滑菜单
使用RecyclerView,实现列表左滑菜单
2022-07-27 12:52:00 【Beacon0423】
代码编辑版本环境
AndroidStudio: 版本2021.2.1,Gradle: gradle-7.3.3-bin.zip gradle plugins版本: 7.2.1
语言:Java
本来想用kotlin写的,但是公司要求使用Java进行编写,所以我又很 开心 地拿起了Java。
虽然Kotlin很优秀,我很喜欢,但是谁会和钱过不去呢?
继承RecycylerView的工具类
package com.example.sliderecycylerview;
import android.content.Context;
import android.graphics.Rect;
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.widget.Scroller;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
/** * 支持侧滑的RecyclerView */
public class SlideRecyclerView extends RecyclerView {
private static final int INVALID_POSITION = -1; // 触摸到的点不在子View范围内
private static final int INVALID_CHILD_WIDTH = -1; // 子ItemView不含两个子View
private static final int SNAP_VELOCITY = 600; // 最小滑动速度
private VelocityTracker mVelocityTracker; // 速度追踪器
private int mTouchSlop; // 认为是滑动的最小距离(一般由系统提供)
private Rect mTouchFrame; // 子View所在的矩形范围
private Scroller mScroller;
private float mLastX; // 滑动过程中记录上次触碰点X
private float mFirstX, mFirstY; // 首次触碰范围
private boolean mIsSlide; // 是否滑动子View
private ViewGroup mFlingView; // 触碰的子View
private int mPosition; // 触碰的view的位置
private int mMenuViewWidth; // 菜单按钮宽度
private boolean canSlide = true; // 是否可以滑动子View
public SlideRecyclerView(@NonNull Context context) {
this(context, null);
}
public SlideRecyclerView(@NonNull Context context, @NonNull AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mScroller = new Scroller(context);
}
public SlideRecyclerView(@NonNull Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
int x = (int) e.getX();
int y = (int) e.getY();
obtainVelocity(e);
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
// 如果动画还没停止,则立即终止动画
mScroller.abortAnimation();
}
mFirstX = mLastX = x;
mFirstY = y;
mPosition = pointToPosition(x, y); // 获取触碰点所在的position
if (mPosition != INVALID_POSITION) {
View view = mFlingView;
// 获取触碰点所在的view
mFlingView = (ViewGroup) getChildAt(mPosition - ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition());
// 这里判断一下如果之前触碰的view已经打开,而当前碰到的view不是那个view则立即关闭之前的view,此处并不需要担动画没完成冲突,因为之前已经abortAnimation
if (view != null && mFlingView != view && view.getScrollX() != 0) {
view.scrollTo(0, 0);
}
// 这里进行了强制的要求,RecyclerView的子ViewGroup必须要有2个子view,这样菜单按钮才会有值,
// 需要注意的是:如果不定制RecyclerView的子View,则要求子View必须要有固定的width。
// 比如使用LinearLayout作为根布局,而content部分width已经是match_parent,此时如果菜单view用的是wrap_content,menu的宽度就会为0。
if (mFlingView.getChildCount() == 2) {
mMenuViewWidth = mFlingView.getChildAt(1).getWidth();
} else {
mMenuViewWidth = INVALID_CHILD_WIDTH;
}
}
break;
case MotionEvent.ACTION_MOVE:
mVelocityTracker.computeCurrentVelocity(1000);
// 此处有俩判断,满足其一则认为是侧滑:
// 1.如果x方向速度大于y方向速度,且大于最小速度限制;
// 2.如果x方向的侧滑距离大于y方向滑动距离,且x方向达到最小滑动距离;
float xVelocity = mVelocityTracker.getXVelocity();
float yVelocity = mVelocityTracker.getYVelocity();
if (Math.abs(xVelocity) > SNAP_VELOCITY && Math.abs(xVelocity) > Math.abs(yVelocity) || Math.abs(x - mFirstX) >= mTouchSlop && Math.abs(x - mFirstX) > Math.abs(y - mFirstY)) {
if (canSlide) {
mIsSlide = true;
} else {
mIsSlide = false;
}
return true;
}
break;
case MotionEvent.ACTION_UP:
releaseVelocity();
break;
}
return super.onInterceptTouchEvent(e);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
if (mIsSlide && mPosition != INVALID_POSITION) {
float x = e.getX();
obtainVelocity(e);
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN: // 因为没有拦截,所以不会被调用到
break;
case MotionEvent.ACTION_MOVE:
// 随手指滑动
if (mMenuViewWidth != INVALID_CHILD_WIDTH) {
float dx = mLastX - x;
if (mFlingView.getScrollX() + dx <= mMenuViewWidth
&& mFlingView.getScrollX() + dx > 0) {
mFlingView.scrollBy((int) dx, 0);
}
mLastX = x;
}
break;
case MotionEvent.ACTION_UP:
if (mMenuViewWidth != INVALID_CHILD_WIDTH) {
int scrollX = mFlingView.getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
// 此处有两个原因决定是否打开菜单:
// 1.菜单被拉出宽度大于菜单宽度一半;
// 2.横向滑动速度大于最小滑动速度;
// 注意:之所以要小于负值,是因为向左滑则速度为负值
if (mVelocityTracker.getXVelocity() < -SNAP_VELOCITY) {
// 向左侧滑达到侧滑最低速度,则打开
mScroller.startScroll(scrollX, 0, mMenuViewWidth - scrollX, 0, Math.abs(mMenuViewWidth - scrollX));
} else if (mVelocityTracker.getXVelocity() >= SNAP_VELOCITY) {
// 向右侧滑达到侧滑最低速度,则关闭
mScroller.startScroll(scrollX, 0, -scrollX, 0, Math.abs(scrollX));
} else if (scrollX >= mMenuViewWidth / 2) {
// 如果超过删除按钮一半,则打开
mScroller.startScroll(scrollX, 0, mMenuViewWidth - scrollX, 0, Math.abs(mMenuViewWidth - scrollX));
} else {
// 其他情况则关闭
mScroller.startScroll(scrollX, 0, -scrollX, 0, Math.abs(scrollX));
}
invalidate();
}
mMenuViewWidth = INVALID_CHILD_WIDTH;
mIsSlide = false;
mPosition = INVALID_POSITION;
releaseVelocity(); // 这里之所以会调用,是因为如果前面拦截了,就不会执行ACTION_UP,需要在这里释放追踪
break;
}
return true;
} else {
// 此处防止RecyclerView正常滑动时,还有菜单未关闭
closeMenu();
// Velocity,这里的释放是防止RecyclerView正常拦截了,但是在onTouchEvent中却没有被释放;
// 有三种情况:
// 1.onInterceptTouchEvent并未拦截,在onInterceptTouchEvent方法中,DOWN和UP一对获取和释放;
// 2.onInterceptTouchEvent拦截,DOWN获取,但事件不是被侧滑处理,需要在这里进行释放;
// 3.onInterceptTouchEvent拦截,DOWN获取,事件被侧滑处理,则在onTouchEvent的UP中释放。
releaseVelocity();
}
return super.onTouchEvent(e);
}
private void releaseVelocity() {
if (mVelocityTracker != null) {
mVelocityTracker.clear();
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
private void obtainVelocity(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
}
public int pointToPosition(int x, int y) {
if (null == getLayoutManager()) return INVALID_POSITION;
int firstPosition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
Rect frame = mTouchFrame;
if (frame == null) {
mTouchFrame = new Rect();
frame = mTouchFrame;
}
final int count = getChildCount();
for (int i = count - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getVisibility() == View.VISIBLE) {
child.getHitRect(frame);
if (frame.contains(x, y)) {
return firstPosition + i;
}
}
}
return INVALID_POSITION;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
mFlingView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
/** * 将显示子菜单的子view关闭 * 这里本身是要自己来实现的,但是由于不定制item,因此不好监听器点击事件,因此需要调用者手动的关闭 */
public void closeMenu() {
if (mFlingView != null && mFlingView.getScrollX() != 0) {
mFlingView.scrollTo(0, 0);
}
}
/** * 是否可以滑动,默认为可以 */
public void setSlide(Boolean slide) {
canSlide = slide;
}
}
可以直接添加依赖使用

主界面布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">
<com.example.sliderecycylerview.SlideRecyclerView android:id="@+id/slide" android:layout_width="match_parent" android:layout_height="match_parent"/>
</LinearLayout>
主界面代码
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import android.os.Bundle;
import android.util.Log;
import com.example.swiptest.adapter.RecyclerAdapter;
import com.example.swiptest.adapter.SwipeAdapter;
import com.example.swiptest.databinding.ActivityMainBinding;
import com.example.swiptest.javabean.Test;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
/* private SwipeAdapter adapter; private SwipeAdapter.ClickListener listener;*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setAdapter();
}
private void setAdapter(){
List<Object> list = new ArrayList<>();
/* list.add(new Test(1, "第一个")); list.add(new Test(2, "第二个"));*/
LinearLayoutManager manager = new LinearLayoutManager(this);
manager.setOrientation(LinearLayoutManager.VERTICAL);
binding.slide.setLayoutManager(manager);
RecyclerAdapter adapter = new RecyclerAdapter(this);
adapter.setList(list);
binding.slide.setAdapter(adapter);
/* listener = new SwipeAdapter.ClickListener() { @Override public void itemClick(int pos) { Log.e("TAG", "itemClick: "+pos); } @Override public void itemSwipe(int pos) { Log.e("TAG", "itemSwipe: "+pos); } }; adapter = new SwipeAdapter(this, list); adapter.setListener(listener); binding.listview.setAdapter(adapter);*/
}
Item项的布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/item" android:orientation="horizontal" android:layout_width="match_parent" android:clickable="true" android:layout_height="80dp">
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
<TextView android:id="@+id/tv_id" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="textview" android:textSize="18sp" android:layout_gravity="center"/>
<TextView android:id="@+id/tv_content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="20dp" android:text="123" android:textSize="16sp" />
</LinearLayout>
<LinearLayout android:layout_width="80dp" android:layout_height="80dp" android:background="@color/purple_200" android:orientation="horizontal">
<TextView android:id="@+id/tv_show" android:layout_width="match_parent" android:layout_height="match_parent" android:text="你好" android:textSize="20sp" android:gravity="center"/>
</LinearLayout>
</LinearLayout>
Adapter
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.swiptest.R;
import java.util.List;
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.ItemHold> {
private Context context;
private List<Object> list;
public RecyclerAdapter(Context context) {
this.context = context;
}
public void setList(List<Object> list) {
this.list = list;
}
@NonNull
@Override
public ItemHold onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.listview_item, parent, false);
return new ItemHold(view);
}
@Override
public void onBindViewHolder(@NonNull ItemHold holder, int position) {
holder.hide.setOnClickListener(v -> {
Log.e("TAG", "Click Hide" );
});
}
@Override
public int getItemCount() {
return list.size();
}
public class ItemHold extends RecyclerView.ViewHolder {
private LinearLayout item;
private TextView id;
private TextView content;
private TextView hide;
public ItemHold(@NonNull View itemView) {
super(itemView);
item = itemView.findViewById(R.id.item);
id = itemView.findViewById(R.id.tv_id);
content = itemView.findViewById(R.id.tv_content);
hide = itemView.findViewById(R.id.tv_show);
}
}
}
效果图

参考文章 Android Recyclerview 左滑删除
作者声明:这是篇半原创blog
demo地址 GitHubGitee
边栏推荐
- Deliver temperature with science and technology, vivo appears at the digital China Construction Summit
- 51:第五章:开发admin管理服务:4:开发【新增admin账号,接口】;(只开发了【用户名+密码的,方式】;【@T…】注解控制事务;设置cookie时,是否需要使用URLEncoder去编码;)
- 滑环的分类以及用途
- 工具及方法 - 在线流程图描画
- JNI程序如何进行参数传递
- Verilog的系统任务----$fopen、$fclose和$fdisplay, $fwrite,$fstrobe,$fmonitor
- A brief analysis of the four process pools
- Image features and extraction
- evutil_ make_ internal_ pipe_: pipe: Too many open files
- 汇量科技app出海好地:火了十几年,美国凭什么还是出海首选淘金地
猜你喜欢

v-text

Common distributed theories (cap, base) and consistency protocols (gosssip, raft)

JS divides the array into two-dimensional arrays according to the specified attribute values

Have you understood these 30 questions of enabling financial risk control plus points

面试官常问:如何手撸一个“消息队列”和“延迟消息队列”?

力扣 1480. 一维数组的动态和 383. 赎金信412. Fizz Buzz

面试考点:三种图的问题

Seata's landing practice in ant International Banking

电滑环的常用类型

赋能金融风控加分项的这30个问题,您都搞懂了吗
随机推荐
JNI程序如何进行参数传递
Can I only use tidb binlog tool to synchronize tidb to MySQL in real time?
How about the strength of database HTAP
7.26 simulation summary
Arrays and functions of knowledge in every corner of C language
Li Hang, director of ByteDance AI Lab: past, present and future of language model
SQL group by statement
Redis summary: cache avalanche, cache breakdown, cache penetration and cache preheating, cache degradation
[300 + selected interview questions from big companies continued to share] big data operation and maintenance sharp knife interview question column (IX)
libevent 之 evconnlistener_new_bind
Absolute positioning
Come and watch, 17 practical skills of operation and maintenance~
Feign's overall process
js回调函数(callback)
Vertical and horizontal shooting range - the mystery of the picture
Rotation chart
四大线程池简析
Final solution for high collapse (no side effects)
Training in the second week of summer vacation on July 24, 2022
Differences between shell environment variables and set, env, export