欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

RecycleView实现QQ侧滑效果

程序员文章站 2022-03-09 22:41:39
...

一、    侧滑效果描述

1、 item向左滑动不是马上删除Item,而是展示删除按钮

2、 这边利用RecycleView提供的ItemTouchHelper可以较轻松实现这个效果

3、 效果图:

RecycleView实现QQ侧滑效果

  

二、    代码实现

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