现在很多需求上面都是列表允许编辑,比如删除、排序等操作,基于RecyclerView的方式也有很多种,但目前还没找到一款比较满意的,Android开发我相信很多人都听过需求这样和你说过,“IOS上面这个效果很棒,我想要你实现它”,每次听到这样的话,头都是大的,很想将我42码的板拖拍在他50码的大脸上,你TM不知道Android也有很多特性、也有很多特别棒的体验,为什么一定要去模仿IOS呢?
哈哈,话虽然这么说,但有一些效果,IOS上面做的确实比Android的效果强很多,所以我们也就被迫去模仿IOS了。额,跑题了,这里不讲解两款系统的差异性,先来说说我做的这个包含删除按钮的列表控件吧。
诚然,我这款也是模仿IOS端做的,IOS在这个地方做的很棒,所以我也仿造它的效果,来自己动手做一下了,话不多说,直接看效果图吧!
效果还行吧,主要是删除按钮出现的方式和列表滑动时的表现和IOS的很像,看了效果,接下来就看源码吧!
首先,我们的控件是基于RecyclerView的,所以应该继承它,其次,既然有拖动事件,那么必须重写onTouchEvent方法,下面我将代码分段讲解,最后贴出完整代码。
控件有手势判断,我使用了Android提供的包装类来帮我判断手势GestureDetectorCompat,它可以处理多点触控、轻触、点击、滑动等事件,省去我们不少工作量。
/**
* 手势包装类,它帮助我们处理多点手势等复杂问题
*/
private final GestureDetectorCompat mDetector;
我们在手指离开时,需要对滑动的速率进行判断,所以需要使用VelocityTracker类,但在GestureDetectorCompat方法中,它在接口里面给出的并不是直接的VelocityTracker,而是速率值,这样我就自定义一个类来将速率值保存下来,在手指抬起时进行相关逻辑判断,并且还需要在判断完之后清除它的值,所以就有:
/**
* 用来记录当前滑动时的速率,只在拖动item时有效
*/
private class VelocityTrackerClass {
private float velocityX = 0.0f;
private float velocityY = 0.0f;
private void setVelocity(float x, float y) {
velocityX = x;
velocityY = y;
}
/**
* 清空速率
*/
private void clear() {
velocityX = 0.0f;
velocityY = 0.0f;
}
/**
* 判断是否有速率
*/
private boolean isEmpty() {
return velocityX == 0.0f && velocityY == 0.0f;
}
}
/**
* 记录当前滑动的item对象
*/
private RecyclerView.ViewHolder mTouchVieHolder;
全局变量定义完来,我们来看重点。当一个item被加载时,我如何在这个item的下面添加一个删除按钮从而达到那种滑动出现的效果呢?我首先想到的是从RecyclerView.ViewHolder入手,进入RecyclerView.ViewHolder源码,看到每一个item的View都是一个变量itemView,所以我就想将这个itemView变为我自己的View,然后在自己的View的最下层添加一个删除按钮,看来一下源码,发现itemView是final的,我们无法改变,所以我就使用反射将它强制改为我自定义的一个RelativeLayout布局文件:
public DeleteViewHolder(@NonNull View originalView) {
super(originalView);
mContentView = originalView;
mContentView.setClickable(false); //如果这里不设置item的外层布局为不可点击,则下面的删除按钮布局就接受不到点击事件
mRootLayout = new RelativeLayout(mContentView.getContext().getApplicationContext());
Field[] fields = RecyclerView.ViewHolder.class.getDeclaredFields();
for (Field field : fields) {
if (field.getName().equals("itemView")) {
field.setAccessible(true);
try {
field.set(this, mRootLayout);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
break;
}
}
initDefaultViews();
}
在初始化initDefaultViews方法中,我添加一个TextView删除控件,这样就实现了我们的需求了。我又增加来一个方法addDeleteLayout用来可以自定义下层的布局,也就是说我们可以将删除按钮换掉,换成我们自己想要的就可以了。
以上说的都是从布局方面来讲的,现在进入最主要的手势阶段:onTouchEvent
在onTouchEvent方法中,我们需要自己判断手指Down和Up,当Down时,我们需要将之前已经是滑动状态的item还原,当是Up时,我们需要将item滑动到指定的位置。上面说到了全局变量mTouchVieHolder就可以判断当前是否有滑动的item,当它为null时,则表示无滑动的item,所以这里可以在Down事件中进行判断;手指抬起时,我们需要将已经随手指滑动的item滑动到删除按钮(以下简称删除布局)左侧或者完全遮盖删除布局,所以主要的判断条件就在这里了:
if (e.getAction() == MotionEvent.ACTION_DOWN) {
mVelocityTrackerClass.clear();
RecyclerView.ViewHolder tmp = getTouchViewHolder(e);
if (mTouchVieHolder != null && !mTouchVieHolder.equals(tmp)) {
if (mTouchVieHolder instanceof DeleteViewHolder) {
((DeleteViewHolder) mTouchVieHolder).scrollReplyHolder(1, mReplyItemCallback);
return true;
}
}
} else if (e.getAction() == MotionEvent.ACTION_UP) { // 手指抬起时
if (mTouchVieHolder != null && mTouchVieHolder instanceof DeleteViewHolder) { // 如果之前已经滑动过item了,那么将该item还原到最初位置或者完全显示删除按钮
if (!mVelocityTrackerClass.isEmpty()) { // 有滑动速率,则对滑动速率进行判断,自动滑动,只判断横向速率
if (mVelocityTrackerClass.velocityX < -mVelocityX) { // 滑动到左边
((DeleteViewHolder) mTouchVieHolder).scrollReplyHolder(2, mReplyItemCallback);
} else if (mVelocityTrackerClass.velocityX > mVelocityX) { // 滑动到右边
((DeleteViewHolder) mTouchVieHolder).scrollReplyHolder(1, mReplyItemCallback);
} else {
((DeleteViewHolder) mTouchVieHolder).scrollReplyHolder(3, mReplyItemCallback);
}
} else {
((DeleteViewHolder) mTouchVieHolder).scrollReplyHolder(3, mReplyItemCallback);
}
return true;
} else { // 如果之前没有滑动过item,则调用父类方法,自由滑动整个列表
return super.onTouchEvent(e);
}
}
大家要注意一下super.onTouchEvent(e)方法的调用,调用这个方法,是告诉控件,也就是RecyclerView执行自己的手势判断,你可以看到RecyclerView里面的这个方法,也作了大量的判断哦。
在手指滑动的时候,我们进入的方法是在之前的全局变量mDetector中的,它就相当与是 一个代理,将任务交给它,让它帮你处理一系列复杂的手势。在GestureDetectorCompat的onScroll方法中,我们对控件进行滑动,这里我们可以得到滑动的x、y距离,这里的x、y距离是MOVE中前后两个MotionEvent事件的距离差,不是从DOWN开始到当前事件为止时距离只差,这个可以自己测试打印一下值就明白了。这里我们需要记录当前滑动的item,赋值给全局变量。在onFling方法中,我们取得手指滑动的速率,然后在RecycleView的Up事件中使用此值来进行逻辑判断。最后item的点击事件我放在了onSingleTapConfirmed,这个有待考证,可以自己修改:
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (mTouchVieHolder != null && mTouchVieHolder instanceof DeleteViewHolder) {
((DeleteViewHolder) mTouchVieHolder).smoothScrollBy((int) distanceX);
return true;
} else {
if (Math.abs(distanceX) > mViewConfiguration.getScaledTouchSlop() && Math.abs(distanceY) < mViewConfiguration.getScaledTouchSlop()) {
mTouchVieHolder = getTouchViewHolder(e2);
if (mTouchVieHolder != null && mTouchVieHolder instanceof DeleteViewHolder) {
((DeleteViewHolder) mTouchVieHolder).smoothScrollBy((int) distanceX);
}
return true;
} else {
return false;
}
}
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
mVelocityTrackerClass.setVelocity(velocityX, velocityY);
return super.onFling(e1, e2, velocityX, velocityY);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (mOnItemClickListener != null) {
RecyclerView.ViewHolder holder = getTouchViewHolder(e);
if (holder != null) {
mOnItemClickListener.onItemHolderClick(DeleteRecyclerView.this, holder, holder.getAdapterPosition());
}
}
return super.onSingleTapConfirmed(e);
}
主要的流程逻辑就是上面写的这些了,可以将这个控件DeleteRecyclerView当作一个普通的RecyclerView使用,如果你使用的ViewHolder是RecyclerView.ViewHolder,那么它将不会有删除按钮这个功能,如果你使用的是DeleteRecyclerView.DeleteViewHolder这个,那么它默认会自带一个删除按钮。因为删除布局是可以自定义的,所以我没有将删除事件以接口的形式公开出去,大家可以直接拷贝代码,然后修改这个地方就可以了。
好了,贴上全部的代码吧,有问题或者错误联系我,大家一起进步!
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Color;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.GestureDetectorCompat;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.lang.reflect.Field;
/**
* Author: Yuri.zheng<br>
* Date: 2017/4/10<br>
* Email: 497393102@qq.com<br>
*/
public class DeleteRecyclerView extends RecyclerView {
/**
* 左右滑动时的速率大小,只要绝对值大于这个值,那么进行滑动还原
*/
private final int mVelocityX = 1500;
/**
* 手势包装类,它帮助我们处理多点手势等复杂问题
*/
private final GestureDetectorCompat mDetector;
/**
* 记录手势滑动item时的速率
*/
private VelocityTrackerClass mVelocityTrackerClass;
/**
* 记录当前滑动的item对象
*/
private RecyclerView.ViewHolder mTouchVieHolder;
/**
* item的点击事件
*/
private OnItemClickListener mOnItemClickListener;
private ReplyItemCallback mReplyItemCallback = new ReplyItemCallback() {
public void callback(boolean isShowDeleteLayout) {
if (!isShowDeleteLayout) {
mTouchVieHolder = null;
}
}
};
public DeleteRecyclerView(Context context) {
this(context, null);
}
public DeleteRecyclerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, -1);
}
public DeleteRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mDetector = new GestureDetectorCompat(getContext().getApplicationContext(), new MyGestureCompat());
mVelocityTrackerClass = new VelocityTrackerClass();
// 将item的长按事件禁掉,如果不禁掉,则长按item不抬起手指进行拖动时,手势是接受不到事件的
setLongClickable(false);
}
/**
* 设置列表点击事件
*/
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.mOnItemClickListener = onItemClickListener;
}
@Override
public void setLongClickable(boolean longClickable) {
super.setLongClickable(longClickable);
mDetector.setIsLongpressEnabled(longClickable);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
mDetector.onTouchEvent(e);
if (e.getAction() == MotionEvent.ACTION_DOWN) {
mVelocityTrackerClass.clear();
RecyclerView.ViewHolder tmp = getTouchViewHolder(e);
if (mTouchVieHolder != null && !mTouchVieHolder.equals(tmp)) {
if (mTouchVieHolder instanceof DeleteViewHolder) {
((DeleteViewHolder) mTouchVieHolder).scrollReplyHolder(1, mReplyItemCallback);
return true;
}
}
} else if (e.getAction() == MotionEvent.ACTION_UP) { // 手指抬起时
if (mTouchVieHolder != null && mTouchVieHolder instanceof DeleteViewHolder) { // 如果之前已经滑动过item了,那么将该item还原到最初位置或者完全显示删除按钮
if (!mVelocityTrackerClass.isEmpty()) { // 有滑动速率,则对滑动速率进行判断,自动滑动,只判断横向速率
if (mVelocityTrackerClass.velocityX < -mVelocityX) { // 滑动到左边
((DeleteViewHolder) mTouchVieHolder).scrollReplyHolder(2, mReplyItemCallback);
} else if (mVelocityTrackerClass.velocityX > mVelocityX) { // 滑动到右边
((DeleteViewHolder) mTouchVieHolder).scrollReplyHolder(1, mReplyItemCallback);
} else {
((DeleteViewHolder) mTouchVieHolder).scrollReplyHolder(3, mReplyItemCallback);
}
} else {
((DeleteViewHolder) mTouchVieHolder).scrollReplyHolder(3, mReplyItemCallback);
}
return true;
} else { // 如果之前没有滑动过item,则调用父类方法,自由滑动整个列表
return super.onTouchEvent(e);
}
}
// 如果已经有记录的item了,则不再滑动列表了,单独处理该item
if (mTouchVieHolder != null) {
return true;
}
return super.onTouchEvent(e);
}
// 根据手势获取当前点击的item对象
private ViewHolder getTouchViewHolder(MotionEvent e) {
View view = findChildViewUnder(e.getX(), e.getY());
if (view != null) {
return getChildViewHolder(view);
}
return null;
}
private interface ReplyItemCallback {
/**
* 当手指抬起时,还原item时的回调方法,内部监听,不对外开放
*
* @param isShowDeleteLayout 是否完全显示了item下面的删除控件
*/
void callback(boolean isShowDeleteLayout);
}
/**
* 用来记录当前滑动时的速率,只在拖动item时有效
*/
private class VelocityTrackerClass {
private float velocityX = 0.0f;
private float velocityY = 0.0f;
private void setVelocity(float x, float y) {
velocityX = x;
velocityY = y;
}
/**
* 清空速率
*/
private void clear() {
velocityX = 0.0f;
velocityY = 0.0f;
}
/**
* 判断是否有速率
*/
private boolean isEmpty() {
return velocityX == 0.0f && velocityY == 0.0f;
}
}
private class MyGestureCompat extends GestureDetector.SimpleOnGestureListener {
/**
* 获取手机设置的认为抖动像素点,每个手机可能得到的值不一样
*/
private ViewConfiguration mViewConfiguration;
public MyGestureCompat() {
super();
mViewConfiguration = ViewConfiguration.get(getContext().getApplicationContext());
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (mTouchVieHolder != null && mTouchVieHolder instanceof DeleteViewHolder) {
((DeleteViewHolder) mTouchVieHolder).smoothScrollBy((int) distanceX);
return true;
} else {
if (Math.abs(distanceX) > mViewConfiguration.getScaledTouchSlop() && Math.abs(distanceY) < mViewConfiguration.getScaledTouchSlop()) {
mTouchVieHolder = getTouchViewHolder(e2);
if (mTouchVieHolder != null && mTouchVieHolder instanceof DeleteViewHolder) {
((DeleteViewHolder) mTouchVieHolder).smoothScrollBy((int) distanceX);
}
return true;
} else {
return false;
}
}
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
mVelocityTrackerClass.setVelocity(velocityX, velocityY);
return super.onFling(e1, e2, velocityX, velocityY);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (mOnItemClickListener != null) {
RecyclerView.ViewHolder holder = getTouchViewHolder(e);
if (holder != null) {
mOnItemClickListener.onItemHolderClick(DeleteRecyclerView.this, holder, holder.getAdapterPosition());
}
}
return super.onSingleTapConfirmed(e);
}
}
public static class DeleteViewHolder extends RecyclerView.ViewHolder {
private final int mDuration = 300;
private final RelativeLayout mRootLayout;
private final View mContentView;
private View mDeleteLayout;
private boolean isShowDeleteLayout = false;
private int mMaxOffest = -1;
/**
* 阻尼系数
*/
private float mDampingCoefficient = 1;
private ObjectAnimator mReplyAnimator = null;
public DeleteViewHolder(@NonNull View originalView) {
super(originalView);
mContentView = originalView;
mContentView.setClickable(false); //如果这里不设置item的外层布局为不可点击,则下面的删除按钮布局就接受不到点击事件
mRootLayout = new RelativeLayout(mContentView.getContext().getApplicationContext());
Field[] fields = RecyclerView.ViewHolder.class.getDeclaredFields();
for (Field field : fields) {
if (field.getName().equals("itemView")) {
field.setAccessible(true);
try {
field.set(this, mRootLayout);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
break;
}
}
initDefaultViews();
}
private void initDefaultViews() {
TextView deleteLayout = new TextView(mContentView.getContext().getApplicationContext());
deleteLayout.setBackgroundColor(Color.RED);
deleteLayout.setGravity(Gravity.CENTER);
deleteLayout.setText("Delete");
deleteLayout.setTextColor(Color.WHITE);
deleteLayout.setTextSize(20); // 设置字体大小
int left_right_padd = 150;
deleteLayout.setPadding(
left_right_padd,
deleteLayout.getPaddingTop(),
left_right_padd,
deleteLayout.getPaddingBottom()
); // 设置字体间距
addDeleteLayout(deleteLayout);
deleteLayout.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// 这里会和item的点击事件有冲突,解决办法是判断当前的deletelayout是否显示,如果显示了,则执行点击事件,没显示则不执行点击事件
if (isShowDeleteLayout) {
Toast.makeText(mRootLayout.getContext(), "Delete click position: " + getAdapterPosition(), Toast.LENGTH_SHORT).show();
}
}
});
}
/**
* 在每一个item布局的下面添加一个布局,此布局可自定义。默认情况下只包含一个删除按钮
*/
public void addDeleteLayout(View view) {
mDeleteLayout = view;
mContentView.measure(0, 0);
RelativeLayout.LayoutParams deleteLayoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, mContentView.getMeasuredHeight());
deleteLayoutParams.rightMargin = 5; // 右边设置一点边距,防止滑出的时候会出现一点点红色
deleteLayoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
deleteLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
mRootLayout.addView(mDeleteLayout, deleteLayoutParams);
RelativeLayout.LayoutParams itemLayoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
mRootLayout.addView(mContentView, itemLayoutParams);
mDeleteLayout.measure(0, 0);
mMaxOffest = mDeleteLayout.getMeasuredWidth();
}
private void smoothScrollBy(int dx) {
float distance = mContentView.getTranslationX() - dx / mDampingCoefficient;
if (distance < 0) {
mContentView.setTranslationX(distance);
float tX = Math.abs(mContentView.getTranslationX());
if (tX > mMaxOffest) {
mDampingCoefficient = 1 + Math.abs(mContentView.getTranslationX() / mMaxOffest * 4);
} else {
mDampingCoefficient = 1;
}
}
}
/**
* 对滑动的item进行还原
*
* @param way 还原的方式:一共有三种方式
* <li>1、强制还原到最右边,这个一般是第二次touch屏幕时,将上次的item强制还原,值传1
* <li>2、强制还原到最左边,值传2
* <li>3、手指离开屏幕时,无速率,则根据滑动的宽度是否大于删除布局宽度的一半来进行判断滑向左边还是右边,值传3<br>
* @param callback 在还原动画结束之后的回调方法
*/
private void scrollReplyHolder(final int way, final ReplyItemCallback callback) {
if (mReplyAnimator != null && mReplyAnimator.isRunning()) {
return;
}
if (way == 1) {
mReplyAnimator = ObjectAnimator.ofFloat(mContentView, "translationX", mContentView.getTranslationX(), 0).setDuration(mDuration);
} else if (way == 2) {
mReplyAnimator = ObjectAnimator.ofFloat(mContentView, "translationX", mContentView.getTranslationX(), -mMaxOffest).setDuration(mDuration);
} else if (way == 3) {
if (Math.abs(mContentView.getTranslationX()) < mMaxOffest / 2) {
mReplyAnimator = ObjectAnimator.ofFloat(mContentView, "translationX", mContentView.getTranslationX(), 0).setDuration(mDuration);
} else {
mReplyAnimator = ObjectAnimator.ofFloat(mContentView, "translationX", mContentView.getTranslationX(), -mMaxOffest).setDuration(mDuration);
}
} else {
throw new RuntimeException("Error param ...");
}
mReplyAnimator.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
if (callback != null) {
if (mContentView.getTranslationX() == 0) {
mDampingCoefficient = 1;
}
isShowDeleteLayout = (mContentView.getTranslationX() != 0);
callback.callback(isShowDeleteLayout);
}
}
});
mReplyAnimator.setInterpolator(new DecelerateInterpolator());
mReplyAnimator.start();
}
/**
* 外部获取item下面的删除控件的布局
*/
public View getDeleteLayout() {
return mDeleteLayout;
}
}
public interface OnItemClickListener {
/**
* item点击事件
*
* @param currentListView 当前RecyclerView
* @param currentHolderItem 当前点击的item对象
* @param position 当前点击的位置
*/
void onItemHolderClick(RecyclerView currentListView, RecyclerView.ViewHolder currentHolderItem, int position);
}
}
mDeleteRecyclerView = (DeleteRecyclerView) findViewById(R.id.list);
mDeleteRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mDeleteRecyclerView.setItemAnimator(new DefaultItemAnimator());
mDeleteRecyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
mDeleteRecyclerView.setAdapter(new MyAdapter());
mDeleteRecyclerView.setOnItemClickListener(new DeleteRecyclerView.OnItemClickListener() {
@Override
public void onItemHolderClick(RecyclerView currentListView, RecyclerView.ViewHolder currentHolderItem, int position) {
Toast.makeText(MainActivity.this, "Item click position: " + position, Toast.LENGTH_SHORT).show();
}
});
自定义Adapter:
private class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyHolder> {
private List<String> list = new ArrayList<>();
public MyAdapter() {
for (int i = 0; i < 100; i++) {
list.add(i + "");
}
}
@Override
public MyHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new MyHolder(View.inflate(MainActivity.this, R.layout.list_item, null));
}
@Override
public void onBindViewHolder(MyHolder holder, int position) {
holder.mText.setText(list.get(position));
}
@Override
public int getItemCount() {
return list.size();
}
class MyHolder extends DeleteRecyclerView.DeleteViewHolder {
private TextView mText;
public MyHolder(View itemView) {
super(itemView);
mText = (TextView) itemView.findViewById(R.id.text);
}
}
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ff00ff00"
android:padding="16dp"/>
</LinearLayout>
TKS!