Android开发之路

RecyclerView详解

2018-11-19  本文已影响88人  kim_liu
本文目录

RecyclerView是谷歌推出的代替ListView的列表控件。关于它的基本使用,它的封装,添加头脚布局等,比较简单就不再描述,这里主要是记录一些知识点。

一 . RecyclerView间隔线

RecyclerView并没有现成的间隔线,我们需要自己去绘制间隔线,创建一个类,继承RecyclerView.ItemDecoration,重写其中的两个方法,就可以绘制出自己的间隔线了。

1 . LinearLayoutManager的垂直和水平状态下的间隔线。

第一个方法:getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)。
第二个方法:onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)

1 . RecyclerView首先会调用这个方法,获取条目之间的间隙,即第一个参数Rect所指的矩形区域。即下图的矩形区域: image.png
2 . 获得该矩形区域之后,在getItemOffsets中设置该矩形区域的左上右下的偏移量。
3 . 在onDraw(Canvas c, RecyclerView parent,RecyclerView.State state)中,将事先准备好的分割线(使用系统的或者使用shape文件自己绘制)画到参数canvas上,在画之前需要计算出左上右下的坐标。

代码如下:绘制LinearLayoutManager中水平和垂直的分割线,注释已经写好

public class MyItemDecoration extends RecyclerView.ItemDecoration {


    private final int[] attrs = new int[]{
            android.R.attr.listDivider
    };
    private int orientation;
    private Drawable mDivider;//分割线

    public MyItemDecoration(Context context, int orientation) {
        //1. 获取系统的分割线
        TypedArray ta = context.obtainStyledAttributes(attrs);//获取系统的分割线
        mDivider = ta.getDrawable(0);
        ta.recycle();

        //2. 获取自定义的分割线
        mDivider = context.getResources().getDrawable(R.drawable.item_divier);

      setOrientation(orientation);

    }

    //设置垂直或者是水平方向
    private void setOrientation(int orientation){
        if(orientation != LinearLayoutManager.HORIZONTAL &&
                orientation != LinearLayoutManager.VERTICAL){
            throw new IllegalArgumentException(
                    "请传入LinearLayoutManager.HORIZONTAL或者LinearLayoutManager.VERTICAL");
        }
        this.orientation = orientation;
    }

    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);
        //2。调用该方法,在这里用户自己绘制剑阁线,RecyclerView会在获取条目间隙之后调用该方法
        if(orientation == LinearLayoutManager.VERTICAL){
            //垂直
            drawVertical(c,parent);

        }else if(orientation == LinearLayoutManager.HORIZONTAL){
            //水平
            drawHorizontal(c,parent);
        }
    }

    /**
     * 当RecyclerView为水平方向时,画分割线
     * 方法和drawVertical很像,也是先算出上下左右,再把图片画到画布上
     * @param c
     * @param parent
     */
    private void drawHorizontal(Canvas c, RecyclerView parent) {
        //左侧,就是RecyclerView左侧值,如果有paddingLeft,那么需要减去
        int top = parent.getPaddingTop();
        //右侧,RecyclerView右侧值,如果有paddingRight值,那么需要减去
        int bottom = parent.getHeight() - parent.getPaddingBottom();
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++){
            View child = parent.getChildAt(i);
            //计算top和bottom,
            RecyclerView.LayoutParams params
                    = (RecyclerView.LayoutParams) child.getLayoutParams();
            //top  child.getBottom()获取到该条目,距离顶部的位置, + 该条目的margin值, + 可能使用动画效果,产生的偏移量
            int left = child.getRight() + params.rightMargin + Math.round(ViewCompat.getTranslationX(child));
            //bottom 算好了top bottom只需要在top的基础上添加一个分割线的宽度即可。
            int right = left + mDivider.getIntrinsicWidth();
            mDivider.setBounds(left,top,right,bottom);
            mDivider.draw(c);
        }
    }

    /**
     * 当RecyclerView为垂直的时,画分割线
     * 方法很简单: 算好分割线的左上右下坐标,使用canvas,把图片画到canvas上
     * @param c
     * @param parent
     */
    private void drawVertical(Canvas c, RecyclerView parent) {
        //左侧,就是RecyclerView左侧值,如果有paddingLeft,那么需要减去
        int left = parent.getPaddingLeft();
        //右侧,RecyclerView右侧值,如果有paddingRight值,那么需要减去
        int right = parent.getWidth() - parent.getPaddingRight();
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++){
            View child = parent.getChildAt(i);
            //计算top和bottom,
            RecyclerView.LayoutParams params
                    = (RecyclerView.LayoutParams) child.getLayoutParams();
            //top  child.getBottom()获取到该条目,距离顶部的位置, + 该条目的margin值, + 可能使用动画效果,产生的偏移量
            int top = child.getBottom() + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child));
            //bottom 算好了top bottom只需要在top的基础上添加一个分割线的宽度即可。
            int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left,top,right,bottom);
            mDivider.draw(c);
        }
    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        //1。首先调用该方法,会获取条目之间的间隙宽度,即Rect矩形区域,
        //每一个item都会调用一次该方法
        if(orientation == LinearLayoutManager.VERTICAL){
            //垂直
            //这个上下左右分割线的偏移值
            outRect.set(0,0,0,mDivider.getIntrinsicHeight());
        }else{
            //水平
            outRect.set(0,0,mDivider.getIntrinsicWidth(),0);
        }
    }
}

这里需要注意一点:

child.getBottom(),这个方法获取的值,是该item的底部到RecyclerView顶部的值,不算分割线的高度。如图: image.png

2.GridLayoutManager绘制分割线。

GridLayoutManager是网格样式的列表,因此,item不可能绘制4边的分割线,如果绘制了的话,会出现重叠的情况。 只需要绘制右下的网格线即可。方法和LinearLayoutManager一样,直接贴出代码,关键点可以看代码上的注释。

 public class GridViewItemDecoration extends RecyclerView.ItemDecoration {


    private final Drawable mDivider;

    public GridViewItemDecoration(Context context) {
        //获取到自定义的分割线
        mDivider = context.getResources().getDrawable(R.drawable.item_divier);
    }

    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);
        drawVertical(c,parent);
        drawHorizontal(c,parent);

    }

    /**
     * 绘制水平分割线
     * @param c
     * @param parent
     */
    private void drawHorizontal(Canvas c, RecyclerView parent) {
        int childCount = parent.getChildCount();
        int spanCount = ((GridLayoutManager) parent.getLayoutManager()).getSpanCount();

        for(int i = 0; i < childCount ; i++){

            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params
                    = (RecyclerView.LayoutParams) child.getLayoutParams();
            if(i < spanCount){
                //第一行 
                int top = child.getTop() + params.topMargin;
                int bottom = top + mDivider.getIntrinsicHeight();
                int left = child.getLeft() + params.leftMargin;
                int right = child.getRight() + params.rightMargin;
                mDivider.setBounds(left,top,right,bottom);
                mDivider.draw(c);
            }

            int top = child.getBottom() + params.bottomMargin;
            int bottom = top + mDivider.getIntrinsicHeight();
            int left = child.getLeft() + params.leftMargin;
            int right = child.getRight() + params.rightMargin;

            mDivider.setBounds(left,top,right,bottom);
            mDivider.draw(c);
        }

    }

    /**
     * 绘制垂直分割线
     * @param c
     * @param parent
     */
    private void drawVertical(Canvas c, RecyclerView parent) {
        int childCount = parent.getChildCount();
        int spanCount = ((GridLayoutManager) parent.getLayoutManager()).getSpanCount();
        int cloum = childCount / spanCount;//行数
        Log.i("行数", "drawVertical: "+cloum);

        for(int i = 0; i < childCount ; i++){
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params
                    = (RecyclerView.LayoutParams) child.getLayoutParams();
            for(int j = 0; j <= cloum ; j++){
                if(i == spanCount*j){
                    /*
                    算出每一行的第一列: spanCount是用户输入的列数,如果spanCount=4 
                    那么每一行的第一列就是0 4 8 。。。。。即spanCount的整数倍,
                    如果cloum=5 5行,那么最后一行的第一列就是spanCount*cloum 
                     */
                    //top
                    int top = child.getTop() - params.topMargin;
                    //bottom不以child为标准,用整个recyclerView的bottom为标准
                    int bottom = child.getBottom() + params.bottomMargin;
                    int left = child.getLeft() + params.leftMargin;
                    int right = left + mDivider.getIntrinsicWidth();

                    mDivider.setBounds(left,top,right,bottom);
                    mDivider.draw(c);
                }
                }

            int top = child.getTop() - params.topMargin;
            int bottom = child.getBottom() + params.bottomMargin;
            int left = child.getRight() + params.rightMargin;
            int right = left + mDivider.getIntrinsicWidth();

            mDivider.setBounds(left,top,right,bottom);
            mDivider.draw(c);
        }

    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        //在这里,获取矩形区域的左上右下的偏移量
        int right = mDivider.getIntrinsicWidth();
        int bottom = mDivider.getIntrinsicHeight();

        outRect.set(0,0,right,bottom);
    }
}

这里要注意的就是,算出第一行的item和每一行第一列的item,具体怎么算已经在注释中给出。
除了第一行的上部和第一列的左侧添加上分割线之外,还有一种设计是最后一行的bottom和最后一列的右侧都不要添加上分割线。如果要实现这样的效果,只需要在getItemOffsets()中做文章就行了,如果是最后一行,下侧偏移量设置为0,如果是最后一列,右侧偏移量设置为0,代码如下所示,注释在代码中写清楚了。

 @Override
    public void getItemOffsets(@NonNull Rect outRect, int itemPosition, @NonNull RecyclerView parent) {
        super.getItemOffsets(outRect, itemPosition, parent);
        int right = mDivider.getIntrinsicWidth();
        int bottom = mDivider.getIntrinsicHeight();
        if(isLastColum(itemPosition,parent)){
            //如果是最后一列,右侧偏移量设置为0
            right = 0;
        }
        if (isLastRow(itemPosition,parent)){
            //如果是最后一行,下侧偏移量设置为0
            bottom = 0;
        }
        outRect.set(0,0,right,bottom);
    }

    /**
     * 是否是最后一行
     * @param itemPosition
     * @param parent
     * @return
     */
    private boolean isLastRow(int itemPosition, RecyclerView parent) {

        int spanCount = getSpanCount(parent);
        if(spanCount!=-1){
            //最后一行
            int childCount = parent.getChildCount();
            int lastRowCount = childCount % spanCount;
            if( lastRowCount == 0 || lastRowCount < spanCount){
                return true;
            }
        }else{
            Snackbar.make(parent,"请传入GridLayoutManager",Snackbar.LENGTH_SHORT).show();
        }
        return false;
    }

    private int getSpanCount(RecyclerView parent) {
        RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
        if(layoutManager instanceof GridLayoutManager){
            GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
            int spanCount = gridLayoutManager.getSpanCount();
            return spanCount;
        }
        return -1;
    }

    /**
     * 是否是最后一列
     * @param itemPosition
     * @param parent
     * @return
     */
    private boolean isLastColum(int itemPosition, RecyclerView parent) {
        int spanCount = getSpanCount(parent);
        if(spanCount!=-1){
            if((itemPosition+1)%spanCount == 0){
                return true;
            }
        }else{
            Snackbar.make(parent,"请传入GridLayoutManager",Snackbar.LENGTH_SHORT).show();
        }
        return false;
    }

另外可以通过修改Theme.Appcompat主题样式里面的android:listSelector或者 android:listDivider属性达到改变间隔线的大小和颜色。

<style name="AppTheme" parent="AppBaseTheme">
<item name="android:listDivider">@drawable/item_divider</item>
</style>

二.RecyclerView条目拖拽动画。

这里主要是使用ItemTouchHelper进行条目的拖拽。先来看下效果。


可拖拽RecyclerView效果图

看看效果,是不是非常的炫酷,其实实现这个效果非常的简单,只需要使用Android提供的ItemTouchHelper即可,那么什么是ItemTouchHelper呢,官方文档是这么解释的:

This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView.It works with a RecyclerView and a Callback class, which configures what type of interactions are enabled and also receives events when user performs these actions.Depending on which functionality you support, you should override onMove(RecyclerView, ViewHolder, ViewHolder) and / or onSwiped(ViewHolder, int).

ItemTouchHelper是一个工具类,可实现侧滑删除和拖拽移动,使用这个工具类需要RecyclerView和Callback。同时根据需要重写onMove和onSwiped方法。

根据官方文档的指导,我们来写一个类继承ItemTouchHelper.Callback,并重写其中的方法,我们看到源码中,ItemTouchHelper.Callback中有几个必须要重写的方法

public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)

public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)

public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction)

另外还有4个方法:

//开启长按拖拽功能,默认为true
//如果设置为false,手动开启,调用startDrag()
@Override
public boolean isLongPressDragEnabled() {
    return true;
}

//开始滑动功能,默认为true
//如果设置为false,手动开启,调用startSwipe()
@Override
public boolean isItemViewSwipeEnabled() {
    return true;
}

public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState)

public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)

学习完了理论知识,我们来写第二部分开头的效果。

1. 编写一个类继承ItemTouchHelper.Callback,重写其中的方法。
public class MyItemTouchHelperCallBack extends ItemTouchHelper.Callback {

    private boolean isMove;

    //在CallBack回调时首先调用,用来判断当前是什么动作(比如方向)
    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {

        int up = ItemTouchHelper.UP;//向上
        int down = ItemTouchHelper.DOWN;//向下
        int right = ItemTouchHelper.RIGHT;//向右
        int left = ItemTouchHelper.LEFT;//向左
        int dragFlags = up | down;
        int swipeFlags = right | left;//如果设置为0 ,那么禁止侧滑
        int flag = makeMovementFlags(dragFlags, swipeFlags);
        return flag ;
    }


    //上下拖拽的时候调用
    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder srcHolder, @NonNull RecyclerView.ViewHolder targetHolder) {
       
        return true;
    }

   // 侧滑时调用
    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
        
    }

    //返回true 长按可以拖拽 返回false 长按不可以拖拽
    @Override
    public boolean isLongPressDragEnabled() {
        return true;
    }


    //拖拽或者侧滑时调用
    @Override
    public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
        if(actionState != ItemTouchHelper.ACTION_STATE_IDLE){
        //当不为空闲时,更改条目颜色   
         viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext()
                    .getResources().getColor(R.color.lightgray));
        }
        super.onSelectedChanged(viewHolder, actionState);
    }

    /**
     * 拖拽完成时调用
     * @param recyclerView
     * @param viewHolder
     */
    @Override
    public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
// 拖拽完成时,设置条目颜色为白色     viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext()
                .getResources().getColor(R.color.white));
        super.clearView(recyclerView, viewHolder);
    }
}

2. 在Activity中,将ItemTouchHelper设置给RecyclerView。

        ItemTouchHelper.Callback callback = new MyItemTouchHelperCallBack(adapter);
        itemTouchHelper = new ItemTouchHelper(callback);
        itemTouchHelper.attachToRecyclerView(recyclerView);

到这里,可以实现RecyclerView条目的拖拽和侧滑。但是要实现条目的交换和删除,需要与Adapter进行操作,那么我们就需要用到接口回调,在拖拽时Adapter中需要进行条目的交换,在侧滑时在Adapter中进行条目的删除。

3. 接口的编写

public interface ItemChangeListener {

    /**
     * 上下拖拽时调用 item交换
     * @param fromPosition
     * @param toPosition
     * @return 是否交换
     */
    boolean ItemChange(int fromPosition,int toPosition);

    /**
     * 条目删除时调用
     * @param position
     */
    void ItemRemoved(int position);

}

编写了接口,就得在该调用的时候调用其中的方法, 分析一下,在什么时候需要交换条目呢?当然是在拖拽的时候。什么时候需要删除条目呢?当然是在侧滑的时候,有了这个思路,我们就需要在onMove()和onSwipe()中调用接口中的两个方法,那么就需要在构造方法中传入ItemChangeListener。
改写后的MyItemTouchHelperCallBack:

public class MyItemTouchHelperCallBack extends ItemTouchHelper.Callback {


    private ItemChangeListener listener;
    private boolean isMove;


     //1.在构造方法中传入ItemChangeListener
    public MyItemTouchHelperCallBack(ItemChangeListener listener){
        this.listener = listener;
    }

    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {


        int up = ItemTouchHelper.UP;
        int down = ItemTouchHelper.DOWN;

        int right = ItemTouchHelper.RIGHT;
        int left = ItemTouchHelper.LEFT;

        int dragFlags = up | down;

        int swipeFlags = right | left;

        int i = makeMovementFlags(dragFlags, swipeFlags);

        return I;
    }

    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder srcHolder, @NonNull RecyclerView.ViewHolder targetHolder) {
        if(srcHolder.getItemViewType() != targetHolder.getItemViewType()){
         //如果条目是不同类型,那么就不让拖拽
            return false;
        }
        if(listener != null){
           //在move的时候,调用ItemChange
            isMove = listener.ItemChange(srcHolder.getAdapterPosition(), targetHolder.getAdapterPosition());
        }
        return isMove;
    }

    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
        if(listener != null){
            //在swipe时,调用ItemRemoved方法
            listener.ItemRemoved(viewHolder.getAdapterPosition());
        }
    }

    @Override
    public boolean isLongPressDragEnabled() {
        return true;
    }

    @Override
    public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
        if(actionState != ItemTouchHelper.ACTION_STATE_IDLE){
            viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext()
                    .getResources().getColor(R.color.lightgray));

        }
        super.onSelectedChanged(viewHolder, actionState);
    }

    @Override
    public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
       viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext()
                .getResources().getColor(R.color.white));
        super.clearView(recyclerView, viewHolder);
    }
}

4. adapter需要实现ItemChangeListener,重写其中的方法,在方法中实现条目的交换与删除。

//1.实现ItemChangeListener 并重写其中的方法
public class QQAdapter extends RecyclerView.Adapter<QQAdapter.MyViewHolder> implements ItemChangeListener{


    private StartDragListener listener;
    private List<QQMessage> list;

    public QQAdapter(List<QQMessage> list,StartDragListener listener) {
        this.list = list;
        this.listener = listener;

    }

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        View view = KimLiuUtils.RvInflate(viewGroup, R.layout.listitem);
        MyViewHolder myViewHolder = new MyViewHolder(view);
        return myViewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull final MyViewHolder myViewHolder, int i) {
        QQMessage qqMessage = list.get(i);
        myViewHolder.iv_logo.setImageResource(qqMessage.getLogo());
        myViewHolder.tv_lastMsg.setText(qqMessage.getLastMsg()+"");
        myViewHolder.tv_name.setText(qqMessage.getName()+"");
        myViewHolder.tv_time.setText(qqMessage.getTime()+"");


        myViewHolder.iv_logo.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                if(listener != null){
                    listener.startDrag(myViewHolder);
                }
                return false;
            }
        });



    }

    @Override
    public int getItemCount() {
        return list.size();
    }

   //2.在这里实现条目的交换
    @Override
    public boolean ItemChange(int fromPosition, int toPosition) {
        //1. 交换数据 2. 交换位置 刷新
        Collections.swap(list,fromPosition,toPosition);
        notifyItemMoved(fromPosition,toPosition);
        return true;
    }


   //3.在这里实现条目的删除
    @Override
    public void ItemRemoved(int position) {
        //1.删除数据 2.刷新
        list.remove(position);
        notifyItemRemoved(position);
    }


    class MyViewHolder extends RecyclerView.ViewHolder{

        private final TextView tv_lastMsg;
        private final TextView tv_name;
        private final ImageView iv_pop;
        private final TextView tv_time;
        private ImageView iv_logo;

        public MyViewHolder(@NonNull View itemView) {
            super(itemView);

            iv_logo = itemView.findViewById(R.id.iv_logo);
            tv_lastMsg = itemView.findViewById(R.id.tv_lastMsg);
            tv_name = itemView.findViewById(R.id.tv_name);
            iv_pop = itemView.findViewById(R.id.iv_pop);
            tv_time = itemView.findViewById(R.id.tv_time);
        }
    }
}

至此,我们已经实现了上面的效果,但是要在触摸头像时实现条目的拖拽,可以提高用户的体验,当然也可以不实现,如果要实现,还是使用方法回调,在touch头像的时候调用接口中的方法,在方法的实现中,开始拖拽(调用itemTouchHelper.startDrag(viewHolder))。

实现这样一个功能是不是很简单呢,只要能用好ItemTouchHelper就行,通常我们在App中总是可以看到网格状的RecyclerView要进行拖拽,其实原理和这个相同,这里就不再赘述。

如果对我的内容感兴趣,可以关注我的公众号:平头哥写代码。内容不多,但都是精选。


平头哥写代码
上一篇下一篇

猜你喜欢

热点阅读