RecycleView实现QQ侧滑效果
程序员文章站
2022-03-09 22:41:39
...
一、 侧滑效果描述
1、 item向左滑动不是马上删除Item,而是展示删除按钮
2、 这边利用RecycleView提供的ItemTouchHelper可以较轻松实现这个效果
3、 效果图:
二、 代码实现
1、创建含有RecycleView的fragment
package com.example.myapplication;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
public class FragmentMainPage extends Fragment {
private RecyclerView mRecyclerView;
private RecyclerView.LayoutManager manager;
private MyAdapter adapter;
private ItemTouchHelper itemTouchHelper;
private List<String> mDatas= new ArrayList<>();
@Override
public ViewonCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view =inflater.inflate(R.layout.fragment_main_page, container, false);
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
mRecyclerView = view.findViewById(R.id.recycler);
manager = new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false);
initData();
initRecycler();
}
private void initData() {
for (int i = 0; i < 30; i++){
mDatas.add("这是第" + i + "item");
}
}
private void initRecycler() {
mRecyclerView.setLayoutManager(manager);
mRecyclerView.setHasFixedSize(true);
adapter = new MyAdapter(getActivity(), mDatas);
mRecyclerView.setAdapter(adapter);
//RecyclerView与ItemTouchHelper关联
ItemTouchHelper.Callback callback = new MyItemTouchHelperCallback(adapter);
itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(mRecyclerView);
}
}
上面的重点就是itemTouchHelper,可以看到它的构造函数需要一个Callback,这个Callback将adapter包裹起来,然后把这个itemTouchHelper依附在recycleview上面,这样它就能监听到RecycleView的各种事件,从而封装它们,为我们实现侧滑等效果提供方便的接口
2、先来看看adapter的具体内容
package com.example.myapplication;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.Intent;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ItemViewHolder> implements MyItemTouchHelperCallback.ItemTouchHelperAdapter {
public Context mContext;
private List<String> mDatas = new ArrayList<>();
public static final int ITEM_TYPE_SINGLE_MATERIAL = 0;
public static final int ITEM_TYPE_ALBUM_TITLE = 1;
public static final int ITEM_TYPE_ALBUM_ITEM = 2;
private int lastSelectPos = -1;
public MyAdapter(Context context, List<String> mdatas) {
this.mContext = context;
this.mDatas = mdatas;
}
@Override
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = null;
if (viewType == ITEM_TYPE_SINGLE_MATERIAL)
view = LayoutInflater.from(mContext).inflate(R.layout.single_material_item, parent, false);
else if (viewType == ITEM_TYPE_ALBUM_TITLE)
view = LayoutInflater.from(mContext).inflate(R.layout.album_title_item, parent, false);
else
view = LayoutInflater.from(mContext).inflate(R.layout.album_item, parent, false);
return new ItemViewHolder(view, viewType);
}
@Override
public void onBindViewHolder(ItemViewHolder holder, int position) {
int viewType = getItemViewType(position);
if (viewType == ITEM_TYPE_ALBUM_ITEM) {
if (position == mDatas.size() - 1)
holder.mDivider.setVisibility(View.GONE);
holder.mAlbumTitle.setText(mDatas.get(position - 2));
if (holder.itemView.getScrollX() != 0) {
holder.itemView.scrollTo(0, 0);
holder.mAlbumDeleteIv.setVisibility(View.VISIBLE);
}
}
}
@Override
public int getItemViewType(int position) {
if (position == 0)
return ITEM_TYPE_SINGLE_MATERIAL;
else if (position == 1)
return ITEM_TYPE_ALBUM_TITLE;
else
return ITEM_TYPE_ALBUM_ITEM;
}
@Override
public int getItemCount() {
if (null == mDatas) {
return 2;
}
return mDatas.size() + 2;
}
@Override
public void onItemMove(int fromPosition, int toPosition) {
Collections.swap(mDatas, fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onItemDelete(int position) {
mDatas.remove(position);
notifyItemRemoved(position);
}
@Override
public void onItemChange(int lastSelectPos) {
notifyItemChanged(lastSelectPos);
}
@Override
public int getLastSelectItem() {
return lastSelectPos;
}
class ItemViewHolder extends RecyclerView.ViewHolder implements MyItemTouchHelperCallback.ItemTouchHelperViewHolder {
ImageView mAlbumIcon;
TextView mAlbumTitle;
ImageView mAlbumDeleteIv;
TextView mAlbumDeleteTv;
TextView mDivider;
ImageView mSingleMaterialGoIv;
int mAnimDistance;
ValueAnimator mAnimator;
int mDirection = -1;
public ItemViewHolder(View itemView, int type) {
super(itemView);
initView(type);
initListener(type);
}
private void initView(int type) {
if (type == ITEM_TYPE_SINGLE_MATERIAL) {
mSingleMaterialGoIv = itemView.findViewById(R.id.go_to_single_material_iv);
} else if (type == ITEM_TYPE_ALBUM_ITEM) {
mAlbumIcon = itemView.findViewById(R.id.album_icon_iv);
mAlbumTitle = itemView.findViewById(R.id.album_title_tv);
mAlbumDeleteIv = itemView.findViewById(R.id.album_delete_iv);
mAlbumDeleteTv = itemView.findViewById(R.id.album_delete_tv);
mDivider = itemView.findViewById(R.id.divider);
}
}
public void startAnimation(int direction) {
mDirection = direction;
mAnimator.start();
}
private void initListener(int type) {
if (type == ITEM_TYPE_ALBUM_ITEM) {
mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
mAnimator.setInterpolator(new DecelerateInterpolator());
mAnimator.setDuration(200);
mAnimator.addUpdateListener(animation -> {
float ratio = animation.getAnimatedFraction();
if (mDirection == 1)
itemView.scrollTo((int) (ratio * mAnimDistance), 0);
else
itemView.scrollTo((int) ((1 - ratio) * mAnimDistance), 0);
});
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (mDirection == -1)
mAlbumDeleteIv.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationStart(Animator animation) {
if (mAnimDistance <= 0)
mAnimDistance = mAlbumDeleteTv.getWidth();
if (mDirection == 1)
mAlbumDeleteIv.setVisibility(View.GONE);
}
}
);
mAlbumDeleteIv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (lastSelectPos >= 0 && lastSelectPos != getAdapterPosition()) {
notifyItemChanged(lastSelectPos);
}
startAnimation(1);
lastSelectPos = getAdapterPosition();
}
});
mAlbumDeleteTv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onItemDelete(getAdapterPosition());
}
});
} else if (type == ITEM_TYPE_SINGLE_MATERIAL) {
mSingleMaterialGoIv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
}
}
@Override
public void onItemSelect() {
//恢复上一次选中的Item
mAlbumDeleteIv.setVisibility(View.GONE);
if (lastSelectPos != getAdapterPosition()) {
onItemChange(lastSelectPos);
}
}
@Override
public void onItemClear() {
lastSelectPos = getAdapterPosition();
if (itemView.getScrollX() != 0)
mAlbumDeleteIv.setVisibility(View.GONE);
else
mAlbumDeleteIv.setVisibility(View.VISIBLE);
}
}
}
album_item.xml布局:
<?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="64dp"
android:background="#ffffff"
android:descendantFocusability="blocksDescendants"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="20dp"
android:layout_marginStart="20dp">
<ImageView
android:id="@+id/album_icon_iv"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
/>
<TextView
android:id="@+id/album_title_tv"
android:layout_width="wrap_content"
android:layout_height="21dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="22dp"
android:layout_toRightOf="@id/album_icon_iv"
android:gravity="left"
android:text="ddd"
android:textColor="#2c2e30"
android:textSize="15sp" />
<ImageView
android:id="@+id/album_delete_iv"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentRight="true"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="20dp"
android:src="@drawable/ic_launcher_background" />
<TextView
android:layout_width="match_parent"
android:layout_height="1dp"
android:id="@+id/divider"
android:layout_alignParentBottom="true"
android:background="#f7f7f7">
</TextView>
</RelativeLayout>
<FrameLayout
android:layout_width="80dp"
android:layout_height="match_parent"
android:background="#fd4965">
<TextView
android:id="@+id/album_delete_tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="删除"
android:textColor="#ffffff"
android:textSize="15sp" />
</FrameLayout>
</LinearLayout>
可以看到这个Adapter就是我们常见的RecycleView的adapter,不同的是利用它的getViewType,我们可以实现一个RecycleView里面有不同的item布局,比如上面的第一个item布局和第二个和剩余的item布局是不一样的,这也是这个adapter的一个优点
3、现在来看一下最重要的MyItemTouchHelperCallback
package com.example.myapplication;
import android.graphics.Canvas;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import java.nio.file.FileAlreadyExistsException;
public class MyItemTouchHelperCallback extends ItemTouchHelper.Callback {
private ItemTouchHelperAdapter mAdapter;
/**
* 当前滑动距离
*/
private float scrollDistance = 0;
private boolean isNeedRecover = true;
private boolean isCanScrollLeft = false;
private boolean isCanScrollRight = false;
/**
* 删除按钮宽度
*/
private int deleteBtnWidth = 0;
public MyItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
this.mAdapter = adapter;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
if (viewHolder.getItemViewType() != target.getItemViewType()) {
return false;
}
mAdapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true;
}
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
@Override
public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
//设置滑动删除最大距离,1.5代表是itemview宽度的1.5倍,目的是不让它删除
return 1.5f;
}
@Override
public float getSwipeEscapeVelocity(float defaultValue) {
//设置滑动速度,目的是不让它进入onSwiped
return defaultValue * 100;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
//mAdapter.onItemDelete(viewHolder.getAdapterPosition());
}
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
if (deleteBtnWidth <= 0)
deleteBtnWidth = getSlideLimitation(viewHolder);
int currentScroll = viewHolder.itemView.getScrollX();
if (dX < 0 && isCanScrollLeft && currentScroll <= deleteBtnWidth) {
dX = Math.abs(dX) <= deleteBtnWidth ? dX : -deleteBtnWidth;
if (!isNeedRecover) {
int newScroll = deleteBtnWidth + (int) dX;
newScroll = newScroll <= currentScroll ? currentScroll : newScroll;
viewHolder.itemView.scrollTo(newScroll, 0);
} else {
viewHolder.itemView.scrollTo(-(int) dX, 0);
scrollDistance = dX;
}
} else if (dX > 0 && isCanScrollLeft) {
//可以左滑的情况下往右滑,恢复item位置
viewHolder.itemView.scrollTo(0, 0);
scrollDistance = 0;
} else if (dX > 0 && isCanScrollRight && currentScroll >= 0) {
if (!isNeedRecover) {
dX = Math.abs(dX) <= Math.abs(currentScroll) ? dX : currentScroll;
viewHolder.itemView.scrollTo((int) dX, 0);
} else {
dX = Math.abs(dX) <= deleteBtnWidth ? dX : deleteBtnWidth;
viewHolder.itemView.scrollTo(deleteBtnWidth - (int) dX, 0);
scrollDistance = dX;
}
} else if (dX < 0 && isCanScrollRight) {
//可以右滑的情况下往左滑,恢复item位置
viewHolder.itemView.scrollTo(deleteBtnWidth, 0);
scrollDistance = deleteBtnWidth;
}
} else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
}
/**
* 获取删除按钮的宽度
*/
public int getSlideLimitation(RecyclerView.ViewHolder viewHolder) {
ViewGroup viewGroup = (ViewGroup) viewHolder.itemView;
return viewGroup.getChildAt(1).getLayoutParams().width;
}
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
//ACTION_DOWN首先会调用这个,然后再调用onChildDraw
if (viewHolder instanceof ItemTouchHelperViewHolder) {
ItemTouchHelperViewHolder itemTouchHelperViewHolder = (ItemTouchHelperViewHolder) viewHolder;
itemTouchHelperViewHolder.onItemSelect();
isNeedRecover = true;
scrollDistance = 0;
isCanScrollLeft = false;
isCanScrollRight = false;
if (viewHolder.itemView.getScrollX() > 0)
isCanScrollRight = true;
else
isCanScrollLeft = true;
}
} else {
//ACTION_UP会首先进入这里,然后再执行recover animation
if (Math.abs(scrollDistance) >= deleteBtnWidth / 2) {
isNeedRecover = false;
}
}
super.onSelectedChanged(viewHolder, actionState);
}
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
//滑动结束后触发
if (viewHolder instanceof ItemTouchHelperViewHolder) {
ItemTouchHelperViewHolder itemTouchHelperViewHolder = (ItemTouchHelperViewHolder) viewHolder;
itemTouchHelperViewHolder.onItemClear();
if (viewHolder.itemView.getScrollX() >= deleteBtnWidth / 2)
viewHolder.itemView.scrollTo(deleteBtnWidth, 0);
else
viewHolder.itemView.scrollTo(0, 0);
}
}
public interface ItemTouchHelperViewHolder {
void onItemSelect();
void onItemClear();
}
public interface ItemTouchHelperAdapter {
void onItemMove(int fromPosition, int toPosition);
void onItemDelete(int position);
void onItemChange(int lastSelectPos);
int getLastSelectItem();
}
}
这个就是我们实现QQ侧滑效果最重要的地方了,我们来具体介绍一下:
1) ItemTouchHelperCallback支持滑动和拖拽,默认这两个都是开启的,如果想关掉,可以覆盖它的方法来禁用
2) 不过ItemTouchHelperCallback支持的水平滑动删除是当你手指滑动距离超过recycleView宽度一半或者你滑动速度超过最大速度都会触发onSwiped,即删除item操作,具体可以看一下ItemTouchHelper这个类的源码:
private int checkHorizontalSwipe(ViewHolder viewHolder, int flags) {
if ((flags & (LEFT | RIGHT)) != 0) {
final int dirFlag = mDx > 0 ? RIGHT : LEFT;
if (mVelocityTracker != null && mActivePointerId > -1) {
mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND,
mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity));
final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId);
final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId);
final int velDirFlag = xVelocity > 0f ? RIGHT : LEFT;
final float absXVelocity = Math.abs(xVelocity);
if ((velDirFlag & flags) != 0 && dirFlag == velDirFlag
&& absXVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity)
&& absXVelocity > Math.abs(yVelocity)) {
return velDirFlag;
}
}
final float threshold = mRecyclerView.getWidth() * mCallback
.getSwipeThreshold(viewHolder);
if ((flags & dirFlag) != 0 && Math.abs(mDx) > threshold) {
return dirFlag;
}
}
return 0;
}
备注:
1)这个函数就是判断水平滑动是否触发删除操作,可以看到有两个依据,一个是mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity),这个是获取触发删除的最小滑动速度,如果滑动速度大于它会触发删除;另一个是mCallback.getSwipeThreshold(viewHolder),这个是获取触发删除的滑动距离,默认是RecycleView宽度的一半。所以一旦这两个条件的任何一个满足就会触发删除操作
2)那触发删除操作会有什么影响呢?问题就来了,触发后ItemTouchHelper会把当前item的移动距离设为RecycleView的宽度大小,这样你下次滑动的时候就会出问题了,因为你得先再触发一次删除操作才会把item的移动距离恢复,然后你才可以继续左右滑动,不然你会看到它一动不动
3)但是我们若想实现QQ侧滑效果,就不能让它触发删除操作,因为我们只是要展示删除按钮而已,所以解决办法就是覆盖触发删除的两个条件:
@Override
public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
//设置滑动删除最大距离,1.5代表是itemview宽度的1.5倍,目的是不让它删除
return 1.5f;
}
@Override
public float getSwipeEscapeVelocity(float defaultValue) {
//设置滑动速度,目的是不让它进入onSwiped
return defaultValue * 100;
}
4)经过上面的操作我们就能在onChildDraw里面实现侧滑展示删除按钮的逻辑了,具体可以看一下上面的代码,因为onChildDraw给我们的距离是绝对距离(当前位置与第一次按下位置的位移向量),所以我们要选择scrollTo来改变位置
上一篇: word文档底纹怎么设置(讲解word给文字添加底纹)
下一篇: c 指针与数组