RecycleView实现QQ侧滑效果

一、    侧滑效果描述

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来改变位置



  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值