阅读 120

RecyclerView高级特性

ItemDecoration

RecyclerView.ItemDecoration最常见的作用就是用来绘制RecyclerView Item之间的分割线。但如果你认为它只能用来绘制分割线,那就大错特错了。这里结合一个具体项目的需求来谈谈RecyclerView.ItemDecoration高级一点的用法。

image.png

具体需求如上所示,首先整体上可以肯定是一个RecyclerView,然后看看item如何实现。Item可以分为左右两边,右边的好实现,关键是左边的时间轴有点麻烦。 从图中可以注意到,正在进行的item icon比较大,刚好和title上下对齐;其他状态的item icon都相对于title居中显示。那么对于这种状态下的item,时间轴要怎么画呢?

思路:

  1. 以icon为划分,上面一个短线条、下面一个长线条。这样的画,上面一个短线条的怎么显示取决于上一个item,下面一个长线条则取决于当前item。这就是说,绘制当前item时还需要知道上一个item的信息。

    实现有点麻烦了,而且也不优雅。

  2. 左边的轴交给RecyclerView.ItemDecoration实现,RecyclerView只需要按照常规的写法显示右边的一些信息即可。这样的实现很优雅。

综合考虑,采用了第二种方案。在贴实现代码之前,先了解一下RecyclerView.ItemDecoration类的一些方法。

RecyclerView.ItemDecoration

RecyclerView.ItemDecoration类是一个抽象类,所有可以override的方法只有3组:

RecyclerView.ItemDecoration

/**
  * An ItemDecoration allows the application to add a special drawing and layout offset
  * to specific item views from the adapter's data set. This can be useful for drawing dividers
  * between items, highlights, visual grouping boundaries and more.
  *
  * <p>All ItemDecorations are drawn in the order they were added, before the item
  * views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()}
  * and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView,
  * RecyclerView.State)}.</p>
  */
public abstract static class ItemDecoration {
    /**
      * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
      * Any content drawn by this method will be drawn before the item views are drawn,
      * and will thus appear underneath the views.
      *
      * @param c Canvas to draw into
      * @param parent RecyclerView this ItemDecoration is drawing into
      * @param state The current state of RecyclerView
      */
    public void onDraw(Canvas c, RecyclerView parent, State state) {
        onDraw(c, parent);
    }

    /**
      * @deprecated
      * Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)}
      */
    @Deprecated
    public void onDraw(Canvas c, RecyclerView parent) {
    }

    /**
      * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
      * Any content drawn by this method will be drawn after the item views are drawn
      * and will thus appear over the views.
      *
      * @param c Canvas to draw into
      * @param parent RecyclerView this ItemDecoration is drawing into
      * @param state The current state of RecyclerView.
      */
    public void onDrawOver(Canvas c, RecyclerView parent, State state) {
        onDrawOver(c, parent);
    }

    /**
      * @deprecated
      * Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
      */
    @Deprecated
    public void onDrawOver(Canvas c, RecyclerView parent) {
    }


    /**
      * @deprecated
      * Use {@link #getItemOffsets(Rect, View, RecyclerView, State)}
      */
    @Deprecated
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
        outRect.set(0, 0, 0, 0);
    }

    /**
      * Retrieve any offsets for the given item. Each field of <code>outRect</code> specifies
      * the number of pixels that the item view should be inset by, similar to padding or margin.
      * The default implementation sets the bounds of outRect to 0 and returns.
      *
      * <p>
      * If this ItemDecoration does not affect the positioning of item views, it should set
      * all four fields of <code>outRect</code> (left, top, right, bottom) to zero
      * before returning.
      *
      * <p>
      * If you need to access Adapter for additional data, you can call
      * {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the
      * View.
      *
      * @param outRect Rect to receive the output.
      * @param view    The child view to decorate
      * @param parent  RecyclerView this ItemDecoration is decorating
      * @param state   The current state of RecyclerView.
      */
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
        getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
                parent);
    }
}
复制代码

上面这段代码就是ItemDecoration类的声明,可以看到只有三个方法需要override——onDraw、onDrawOver、getItemOffsets。

在注释中仍然有一些非常实用的解释:

  1. 所有的ItemDecoration按照被添加的顺序进行绘制

  2. temDecoration.onDraw可以通过Canvas绘制任意装饰到RecyclerView上

    该方法会在item views绘制之前调用,所以绘制的内容会显示到views的下面

  3. ItemDecoration.onDrawOver可以通过Canvas绘制任意装饰到RecyclerView上

    该方法会在item views绘制之后调用,所以绘制的内容会显示到views的上面

  4. ItemDecoration.getItemOffsets可以通过设置outRect的值来设置item view的inset(和padding或margin类似)

    outRect的默认实现以会设置且返回为0

  5. 如果需要访问Adapter获取更多的信息,可以调用RecyclerView.getChildAdapterPosition(View)来获取该View的adapter位置

实现UI效果

在了解了ItemDecoration相关的知识后,我们看一下最开始的时间轴的写法:

class CertProgressItemDecoration : RecyclerView.ItemDecoration() {

    /** doing的下标,后台传递的数值从1开始 */
    var activeStepIndex = -1
    /** 除了虚线之外的Paint */
    private val paint = Paint()
    /** 虚线相关 */
    private val dashPath = Path()
    private val dashPaint = Paint()

    init {
        paint.strokeWidth = SizeUtils.dp2px(1F).toFloat()
        paint.color = Color.WHITE
        paint.isAntiAlias = true

        dashPaint.strokeWidth = SizeUtils.dp2px(1F).toFloat()
        dashPaint.style = Paint.Style.STROKE
        dashPaint.isAntiAlias = true
        dashPaint.pathEffect = DashPathEffect(floatArrayOf(SizeUtils.dp2px(6F).toFloat(), SizeUtils.dp2px(3F).toFloat()), 0F)
    }

    override fun getItemOffsets(
        outRect: Rect?,
        view: View?,
        parent: RecyclerView?,
        state: RecyclerView.State?
    ) {
        // item左边的部分距离左边68dp,所以左边的时间轴部分设置为68dp
        // 其他三个方面都不需要额外处理
        outRect?.set(SizeUtils.dp2px(68F), 0, 0, 0)
    }

    override fun onDrawOver(c: Canvas?, parent: RecyclerView?, state: RecyclerView.State?) {}

    override fun onDraw(c: Canvas?, parent: RecyclerView?, state: RecyclerView.State?) {
        parent ?: return
        c ?: return

        for (i in 0 until parent.childCount) {
            val child = parent.getChildAt(i)

            // 需要绘制的矩形坐标
            val left = 0F
            val top = child.top.toFloat()
            val right = SizeUtils.dp2px(68F).toFloat()
            val bottom = child.bottom.toFloat()

            // 绘制背景
            // 由于整个RecyclerView是没有设置颜色的,颜色都由item进行绘制
            // 但是我们设置了item view左边的inset,所以这里需要绘制上背景颜色
            paint.color = Color.WHITE
            c.drawRect(left, top, right, bottom, paint)

            val recyclerAdapter = parent.adapter
            // 当前要绘制的data的下标
            val dataIndex = i
            // data的总数
            val dataCount = parent.childCount

            // 时间轴的中心
            val centerX = (right - left) / 2
            // 时间轴icon所在的centerY值
            // doing状态icon size为22dp
            val bPartTop = SizeUtils.dp2px(11F) + top
            // 当前item的上半根线条
            val lastDataIndex = dataIndex - 1
            if (dataIndex != 0) {
                if (lastDataIndex < activeStepIndex) {
                    paint.color = parent.context.getColorCompact(R.color.colorAccent)
                    c.drawLine(centerX, top, centerX, bPartTop, paint)
                } else if (lastDataIndex != dataCount - 1) {
                    dashPaint.color = parent.context.getColorCompact(R.color.colorTimeLineUndo)
                    dashPath.reset()
                    dashPath.moveTo(centerX, top)
                    dashPath.lineTo(centerX, bPartTop)
                    c.drawPath(dashPath, dashPaint)
                }
            }
            // 当前item的下半根线条
            if (dataIndex < activeStepIndex) {
                paint.color = parent.context.getColorCompact(R.color.colorAccent)
                c.drawLine(centerX, bPartTop, centerX, bottom, paint)
            } else if (dataIndex != dataCount - 1) {
                dashPaint.color = parent.context.getColorCompact(R.color.colorTimeLineUndo)
                dashPath.reset()
                dashPath.moveTo(centerX, bPartTop)
                dashPath.lineTo(centerX, bottom)
                c.drawPath(dashPath, dashPaint)
            }
            // 画icon
            val bitmap = when {
                dataIndex < activeStepIndex -> BitmapFactory.decodeResource(parent.context.resources, R.drawable.ic_timeline_tick_n)
                dataIndex == activeStepIndex -> BitmapFactory.decodeResource(parent.context.resources, R.drawable.ic_timeline_ing_n)
                else -> BitmapFactory.decodeResource(parent.context.resources, R.drawable.ic_timeline_unfinished_n)
            }
            val bitmapX = centerX - (bitmap.width shr 1)
            val bitmapY = bPartTop - (bitmap.height shr 1)
            c.drawBitmap(bitmap, bitmapX, bitmapY, paint)
        }
    }
}
复制代码

注意,如果RecyclerView设置了header或者footer,需要注意一下下标的正确换算。

时间轴的代码写完之后,只需要调用RecyclerView.addItemDecoration(ItemDecoration)方法添加上去即可。

拖拽排序以及滑动删除

效果视频

主要流程如下:

  1. 定义拖拽操作、滑动删除接口
  2. Adapter中实现第一步的接口,这里主要是体现对数据的操作
  3. 自定义Callback实现android.support.v7.widget.helper.ItemTouchHelper.Callback,此Callback就是RecyclerView实现拖拽、滑动删除的关键
  4. 创建ItemTouchHelper并attachToRecyclerView

1. 定义拖拽、滑动操作的接口

public interface IDragSwipe {
    /**
     * 两个Item交换位置
     * @param fromPosition 第一个Item的位置
     * @param toPosition 第二个Item的位置
     */
    void onItemSwapped(int fromPosition, int toPosition);

    /**
     * 删除Item
     * @param position 待删除Item的位置
     */
    void onItemDeleted(int position);

    /**
     * Item标记完成
     * @param position Item的位置
     */
    void onItemDone(int position);
}
复制代码

这里定义了三个方法,交换、删除以及标记完成

2. Adapter实现该接口

public class TodoTaskAdapter extends BaseQuickAdapter<TodoTask, BaseViewHolder> implements IDragSwipe {
    ...

    @Override
    public void onItemSwapped(int fromPosition, int toPosition) {
        Collections.swap(getData(), fromPosition, toPosition);
        notifyItemMoved(fromPosition, toPosition);
    }

    @Override
    public void onItemDeleted(int position) {
        Toast.makeText(mContext, "onItemDeleted", Toast.LENGTH_SHORT).show();
        mData.remove(position);
        notifyItemRemoved(position);
    }

    @Override
    public void onItemDone(int position) {
        Toast.makeText(mContext, "onItemDone", Toast.LENGTH_SHORT).show();
        mData.remove(position);
        notifyItemRemoved(position);
    }
}
复制代码

如上所示

  • 交换时使用Collections.swap将对应位置的数据进行交换,然后通知数据有更改(notifyItemMoved以及notifyItemRemoved)会有默认的动画效果。
  • 删除以及标记完成都是简单的移除了数据,然后通知更新。

3. 自定义Callback类,调用Adapter的交换、删除等操作方法

public class DragSwipeCallback extends android.support.v7.widget.helper.ItemTouchHelper.Callback {

    /** 通过此变量通知外界发生了排序、删除等操作 */
    private IDragSwipe mAdapter;

    public DragSwipeCallback(IDragSwipe adapter){
        // 注入IDragSwipe
        mAdapter = adapter;
    }

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        // 确定拖拽、滑动支持的方向
        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
        return makeMovementFlags(dragFlags, swipeFlags);
    }

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

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

    /**
     * 拖拽、交换事件
     */
    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        mAdapter.onItemSwapped(viewHolder.getAdapterPosition(), target.getAdapterPosition());
        return true;
    }

    /**
     * 滑动成功的事件
     */
    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        switch (direction) {
            case ItemTouchHelper.END: // START->END 标记完成事件
                mAdapter.onItemDone(viewHolder.getAdapterPosition());
                break;
            case ItemTouchHelper.START: // END->START 删除事件
                mAdapter.onItemDeleted(viewHolder.getAdapterPosition());
                break;
            default:
        }
    }

    /**
     * 拖拽、滑动时如何绘制列表
     * actionState只会为ACTION_STATE_DRAG或者ACTION_STATE_SWIPE
     */
     @Override
     public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
         switch (actionState) {
             case ItemTouchHelper.ACTION_STATE_DRAG:
                 // 拖拽时,如果是isCurrentlyActive,则设置translationZ,否则复位
                 viewHolder.itemView.setTranslationZ(SizeUtils.dp2px(isCurrentlyActive ? 4 : 0));
                 super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
                 break;
             case ItemTouchHelper.ACTION_STATE_SWIPE:
                 // 滑动时,对view的绘制
                 View rootView = viewHolder.itemView;
                 View contentView = rootView.findViewById(R.id.ll_content_root);
                 View actionView = rootView.findViewById(R.id.ll_action_root);
                 ImageView doneImageView = actionView.findViewById(R.id.iv_task_done);
                 View actionSpaceView = actionView.findViewById(R.id.view_action_space);
                 ImageView deleteImageView = actionView.findViewById(R.id.iv_task_delete);
                 if (dX < 0) {
                     deleteImageView.setImageResource(R.drawable.ic_delete_white_24dp);
                     deleteImageView.setBackgroundResource(R.color.ffff4081);
                     doneImageView.setImageDrawable(null);
                     doneImageView.setBackgroundResource(R.color.ffff4081);
                     actionSpaceView.setBackgroundResource(R.color.ffff4081);
                 } else {
                     doneImageView.setImageResource(R.drawable.ic_done_white_24dp);
                     doneImageView.setBackgroundResource(R.color.ff53c4ac);
                     deleteImageView.setImageDrawable(null);
                     deleteImageView.setBackgroundResource(R.color.ff53c4ac);
                     actionSpaceView.setBackgroundResource(R.color.ff53c4ac);
                 }
                 contentView.setTranslationX(dX);
                 break;
             default:
         }
     }

    /**
     * 在onSelectedChanged、onChildDraw、onChildDrawOver操作完成后可以在此进行清楚操作
     */
    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        super.clearView(recyclerView, viewHolder);
        View rootView = viewHolder.itemView;
        View contentView = rootView.findViewById(R.id.ll_content_root);
        contentView.setTranslationX(0);
    }
}
复制代码

4. 创建ItemTouchHelper并attachToRecyclerView

创建TodoTaskAdapter、将其注入DragSwipeCallback类,然后注入ItemTouchHelper,最后attachToRecyclerView

mTodoTaskAdapter = new TodoTaskAdapter(queryTaskFromDB());
ItemTouchHelper.Callback callback = new DragSwipeCallback(mTodoTaskAdapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setAdapter(mTodoTaskAdapter);
touchHelper.attachToRecyclerView(mRecyclerView);
复制代码
文章分类
Android
文章标签