Android使用RecyclerView和StaggeredGridLayoutManager实现瀑布流效果-装饰

版权声明:本文为博主原创文章,未经博主允许不得转载。https://blog.csdn.net/sinat_25074703/article/details/83216019

8、实现StaggeredItemDecoration定制分割线

在实际的项目开发过程中,为了更好的用户体验,分割线是必不可少的,甚至有异想天开的产品会脑补出千姿百态的分割效果。这里主要通过简单的直线分割来阐述RecyclerView#ItemDecoration的风采。

8.1从定义说起

Google官方对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.
*
*

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)}.


*/

译文:

ItemDecoration允许(既然是允许,不添加也不犯法)应用添加一种特殊的绘制和布局补偿系数(注意这两个词汇,绘制是为了渲染,绘在什么地方由补偿系数说了算)到指定的item views中,这些item views来自于adapter数据源(这里的adapter指的是前面提到的RecyclerView#Adapter,具体到本项目是StaggeredAdapter)。这种被允许的方式一定(注意情态动词can be)是有用的,哪里有用?对于绘制items之间的分割线,高亮,可视化分组分割线等一系列需求都是有用的。
*
所有的ItemDecorations按照被添加的顺序绘制(这一句信息量大啊!他告诉我们ItemDecorations可以随意添加,无论多少个。但是他们要按照添加的顺序排队绘制,先绘制的或许会被覆盖看不到。砌墙的砖头——后来居上嘛!),所有的这些ItemDecorations的添加时间段是在ItemDecoration#onDraw()方法被调用之前(我能理解,不然无法绘制), 在ItemDecoration#onDrawOver()之后(我理解不了啊,因为没有意义啊!在此,我大胆推测Google想表达的是,绘制期间不能有添加逻辑,否则就会报异常)。

看完了定义,我们可以确定RecyclerView#ItemDecoration的一些关键信息:使用前需要先绘制,而绘制使用的onDraw()和onDrawOver()方法。那么我们来看一下这个类吧!

8.2类及其方法

    public abstract static class ItemDecoration{ 

    }

// 这是一个共有的静态抽象内部类,从这点来说跟ViewHolder和Adapter没啥两样,都是待价而沽,静候明主。
既然是抽象类必然有抽象方法,如下图:
Android使用RecyclerView和StaggeredGridLayoutManager实现瀑布流效果-装饰_第1张图片
截图中清晰地展示ItemDecoration的三个抽象方法onDraw()、onDrawOver()、getItemOffsets(),有人说你瞎吗?明明是6个。我不瞎,另外三个是同名过期的方法。

  1. onDraw()
        /**
         * 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.
         *
         * 绘制任何适合的decorations进入到画布中,这张画布是提供给RecyclerView使用的(这个当然,否则如何保证同步呢)
         * 任何被这个方法绘制的内容都是在item views(指的是RecyclerView的每一个ItemView)被绘制之前绘制,并且(该方法绘制的东西)将出现在所有view的最底层。【这一段传递两个信息,ItemDecoration会被首先绘制,它在最底层。换句话说,如果不做布局上的调整,哪辈子也看不到它】
         * @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);
        }
  1. onDrawOver()
       /**
         * 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.
         * 绘制任何适合的decorations进入到画布中,这张画布是提供给RecyclerView使用的(这个当然,否则如何保证同步呢)
         * 任何被这个方法绘制的内容都是在item views(指的是RecyclerView的每一个ItemView)被绘制之后绘制,并且(该方法绘制的东西)将出现在所有view的最顶层。【这一段传递两个信息,ItemDecoration会被最后绘制,它在最顶层。换句话说,如果不做布局上的调整,天天都能看到它】
         * @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);
        }
  1. getItemOffsets()
       /**
         * Retrieve any offsets for the given item. Each field of outRect 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.
         * 译文:
         * 取回指定item的任何补偿系数(这里指的是布局补偿系数)。参数outRect的每个属性是以item应该被插入的像素数为基准的(就是说item上下左右都可以插入,但插入的是一个矩形,就算他在视觉上是一条线,它的本质是一个矩形),类似于margin或padding
         * 默认outRect的边界设置为0,就是没有布局。
         * 

* If this ItemDecoration does not affect the positioning of item views, it should set * all four fields of outRect (left, top, right, bottom) to zero * before returning. * 译文: * 如果该ItemDecoration不影响item views的位置(比如绘制的位置没有交叉重叠),那么outRect的四个属性值都可以设置为0,但是需要在返回之前设置。 *

* If you need to access Adapter for additional data, you can call * {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the View. * 译文:如果你需要访问Adapter里的其他数据的话,可以通过调用RecyclerView#getChildAdapterPosition(View)方法来获取中指定位置的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); }

通过源码追踪不难发现,getItemOffsets()最终会被StaggeredGridLayoutManager的onLayoutChildren()调用【onLayoutChildren()—>fill()—>measureChildWithDecorationsAndMargin()—>calculateItemDecorationsForChild()—>getItemDecorInsetsForChild()—>getItemOffsets()】,StaggeredGridLayoutManager是RecyclerView#LayoutManager的派生类,onLayoutChildren()是其派生方法,定义在RecyclerView中,用于完成布局任务。结合View的绘制流程:测量(onMeasure)——布局(onLayout)——绘制(onDraw),ItemDecoration的执行流程应该是线调用getItemOffsets,接着调用onDraw,最后调用onDrawOver

8.3实现RecyclerView#ItemDecoration绘制分割线

在项目文件夹中找到staggered文件夹右键创建StaggeredItemDecoration并继承自RecyclerView.ItemDecoration,代码如下:

package com.edwin.idea.staggered;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.util.Log;
import android.view.View;

/**
 * Created by Edwin,CHEN on 2018/9/18.
 */

public class StaggeredItemDecoration extends RecyclerView.ItemDecoration {

    private static final String TAG = StaggeredItemDecoration.class.getSimpleName();
    
    @NonNull
    public static StaggeredItemDecoration createDefault(Context context) {
        return new StaggeredItemDecoration(context);
    }

    private StaggeredItemDecoration(Context context) {

    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        Log.d(TAG, "getItemOffsets");
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        Log.d(TAG, "onDraw");
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        Log.d(TAG, "onDrawOver");
    }
}

在StaggeredFragment.java文件中声明并添加ItemDecoration实例到RecyclerView中,代码如下:

 private StaggeredItemDecoration staggeredItemDecoration;
 
 @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        viewGroupRoot = (ViewGroup) inflater.inflate(R.layout.fragment_staggered, null);

        recyclerView = (RecyclerView) viewGroupRoot.findViewById(R.id.recycler_view);
        staggeredGridLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
        
        staggeredItemDecoration = StaggeredItemDecoration.createDefault(getContext().getApplicationContext());
        
        staggeredAdapter = new StaggeredAdapter();
        initData();
        staggeredAdapter.setData(list);
        recyclerView.setLayoutManager(staggeredGridLayoutManager);
        //注意:这里是addXXX()不是setXXX()
        recyclerView.addItemDecoration(staggeredItemDecoration);
        
        recyclerView.setAdapter(staggeredAdapter);

        return viewGroupRoot;
    }
 @Override
     protected void initData() {
         list = new ArrayList<>();
         for (int i = 0; i < 50; i++) {
             StaggeredVO staggeredVO = new StaggeredVO();
             staggeredVO.setPrice("$ " + i);
             if (i % 2 == 1) {
                 staggeredVO.setDescription("Staggered Formal Item " + i);
             }
             list.add(staggeredVO);
         }
    }

执行代码,会得出如下图信息,
Android使用RecyclerView和StaggeredGridLayoutManager实现瀑布流效果-装饰_第2张图片
从图中可以看到三个方法的执行,这证明顺序证明了我们在8.2结尾处的假设。
绘制分割线的本质就是绘制一个Drawable,Google对Drawable的定义是:

A Drawable is a general abstraction for “something that can be drawn.”
Drawable就是对“将要绘制的东西”的提取

关于Drawable既可以自定义xml文件,也可以通过系统获取,
1.这里我们以系统提供的分割线属性为例,如下图是系统提供的自定义属性:(android包的res/values/attrs.xml文件中)
Android使用RecyclerView和StaggeredGridLayoutManager实现瀑布流效果-装饰_第3张图片
在StaggeredItemDecoration的构造方法中,获取系统提供的自定义属性如下:

    //获取系统提供的分割线属性,并初始化单元素数组ATTRS
    private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
    
    //分割线
    private Drawable mDivider;

    //分割线高度
    private int mDividerHeight;

    //分割线宽度
    private int mDividerWidth;
    
    private StaggeredItemDecoration(Context context) {
        TypedArray a = context.obtainStyledAttributes(ATTRS);
        // 此处获取reference不同于Boolean、Integer等的获取方式,类似的还有getText()、getString()
        mDivider = a.getDrawable(0);
        a.recycle();

        //假设分割线的基础单元是1*1,宽高必须定义
        mDividerWidth = 1;
        mDividerHeight = 1;
    }

2.自定义Drawable代码如下:【未验证】


    public StaggeredItemDecoration(Context context, Drawable drawable) {
        ColorDrawable dividerDrawable = new ColorDrawable(context.getResources().getColor(android.R.color.holo_red_dark));
        mDivider = dividerDrawable;
        
        //假设分割线的基础单元是1*1,宽高必须定义
        mDividerWidth = 1;
        mDividerHeight = 1;
    }

接着在getItemOffsets中设置布局参数,由于分割线在右下方,宽度都是1,outRect的参数设置如下:

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        Log.d(TAG, "getItemOffsets");
        outRect.set(0/*left*/, 0/*top*/, 1/*right*/, 1/*bottom*/);
    }

现在运行代码,应该看不到什么效果,因为一像素的白色实在不是人眼所能分辨的,建议改成111像素试试,被切割的感觉如何?
Android使用RecyclerView和StaggeredGridLayoutManager实现瀑布流效果-装饰_第4张图片
接下来可以就是onDraw()方法的调用了,通过它可以完成绘制工作。分为两步:水平分割线和垂直分割线。
画图的本质就是确定颜色搭配,起始位置,中间路径。画直线只考虑起始位置就好了,因为图的颜色是系统默认的。一个矩形图,分为左上右下四个位置,计算好了调用它的draw方法绘制就好。对于每一个itemView,需要获取其当前位置的LayouParams信息,根据其布局信息,才能画出分割线。

  1. 水平分割线
    代码如下:

    /**
     * 绘制水平分割线,分割竖直方向的item
     * @param c
     * @param recyclerView
     */
    private void drawHorizontal(Canvas c, RecyclerView recyclerView) {
        int childCount = recyclerView.getChildCount(); // visible children【看不见,不用绘制,系统也不调用。】
        if (childCount <= 1) { //只有一个不分割
            return;
        }
        for (int i = 0; i < childCount; i++) {
            // 获取当前的itemView,child就是itemView及其布局参数
            View child = recyclerView.getChildAt(i); 
            RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
            int left = child.getLeft() - layoutParams.leftMargin; //左边起点,别忘了去掉margin和padding
            int top = child.getBottom() + layoutParams.bottomMargin; // 水平线矩形在下边
            // 注意,mDividerWidth是竖直分割线矩形宽度,要加上
            int right = child.getRight() + layoutParams.rightMargin + mDividerWidth; 
            // 注意,mDividerHeight是水平分割线矩形宽度,要加上
            int bottom = child.getBottom() + layoutParams.bottomMargin + mDividerHeight;
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }
  1. 垂直分割线
    代码如下:
   /**
     * 绘制垂直分割线,分割水平方法的item
     * @param c
     * @param recyclerView
     */
    private void drawVertical(Canvas c, RecyclerView recyclerView) {
        int childCount = recyclerView.getChildCount(); // visible children
        int itemCount = recyclerView.getAdapter().getItemCount();// total items
        if (childCount <= 1) {
            return;
        }
        for (int i = 0; i < childCount; i++) {
            View child = recyclerView.getChildAt(i); // child就是itemView
            RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
            int left = child.getRight() + layoutParams.rightMargin; // 竖直分割线在右边
            int top = child.getTop() - layoutParams.topMargin; //需要减去顶部布局margin
            // 注意,mDividerWidth是竖直分割线矩形宽度,要加上
            int right = child.getRight() + layoutParams.rightMargin + mDividerWidth;
            // 注意,mDividerHeight是水平分割线矩形宽度,绘制水平线时已经添加,这里不可以再添加
            int bottom = child.getBottom() + layoutParams.bottomMargin /*+ mDividerHeight*/;
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

将这两个方法添加到onDraw()方法中,代码如下:

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        Log.d(TAG, "onDraw");
        drawHorizontal(c, parent);
        drawVertical(c, parent);
    }

运行一下代码,结果如下图:
Android使用RecyclerView和StaggeredGridLayoutManager实现瀑布流效果-装饰_第5张图片
最后一个itemView左边的分割线怎么没了,一首《凉凉》送给在下~

其实,这也是我写这篇文章的初衷,因为我一个朋友遇到这个问题之后果断放弃了,这增加了我解决该问题的决心。

该问题的直接感受是左边的最后一个itemView没有竖直分割线,原因是它右边没有兄弟啊,没人帮他补这条线。解决方案当然是,绘制右边的分割线了,我的方案是:
1.确定最后一个itemView是否在右边边【可以试一下,最后一个在左边不存在这种情况】
2.记录最后一次左边竖直分割线线的结束位置
3.给最后一个itemView绘制三条线,主要是左边这条
更改drawVertical()方法代码如下:


    /**
     * 绘制垂直分割线,分割水平方法的item
     * @param c
     * @param recyclerView
     */
    private void drawVertical(Canvas c, RecyclerView recyclerView) {
        int childCount = recyclerView.getChildCount(); // visible children
        int itemCount = recyclerView.getAdapter().getItemCount();// total items
        if (childCount <= 1) {
            return;
        }
        for (int i = 0; i < childCount; i++) {
            View child = recyclerView.getChildAt(i); // child就是itemView
            RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
            int left = child.getRight() + layoutParams.rightMargin; // 竖直分割线在右边
            int top = child.getTop() - layoutParams.topMargin;
            // 注意,mDividerWidth是竖直分割线矩形宽度,要加上
            int right = child.getRight() + layoutParams.rightMargin + mDividerWidth;
            // 注意,mDividerHeight是水平分割线矩形宽度,绘制水平线时已经添加,这里不可以再添加
            int bottom = child.getBottom() + layoutParams.bottomMargin /*+ mDividerHeight*/;

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


            // 特殊处理最后两个itemView,其实应该是最后三个
            int itemPosition = recyclerView.getChildAdapterPosition(child);
            if (itemPosition == itemCount - 1 || itemPosition == itemCount - 2) {
                // 最后两个特殊处理
                StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) child.getLayoutParams();
                int spanIndex = lp.getSpanIndex();  // leftItemSpanIndex = 0, rightItemSpanIndex = 1
                if (itemPosition == itemCount - 2 && spanIndex == 0) {
                    previousBottom = bottom;
                } else if (itemPosition == itemCount - 1 && spanIndex == 1) {
                    // 注意补充的线在左边
                    mDivider.setBounds(child.getLeft() - layoutParams.leftMargin, previousBottom, child.getLeft() - layoutParams.leftMargin + mDividerWidth, bottom);
                    mDivider.draw(c);
                }
            }
        }
    }

再次使用快捷键Ctrl+R运行一下代码吧~
结果如下图:
Android使用RecyclerView和StaggeredGridLayoutManager实现瀑布流效果-装饰_第6张图片
终于写完了,代码中存在一些重复绘制,onDrawOver也没有验证。对的,我就是不想验证~

诗云:
滚滚长江东逝水,浪花淘尽英雄。是非成败转头空,青山依旧在,几度夕阳红。
白发渔樵江渚上,惯看秋月春风。一壶浊酒喜相逢,古今多少事,都付笑谈中!

你可能感兴趣的:(技术文章)