2.3.2 (上)一篇文章完全掌握 RecycleView 的六大用法

一、RecycleView 简介

(1)RecycleView是什么

RecyclerView 出现已经有一段时间了,相信大家肯定不陌生了,不过这里还是简单介绍下。

RecylerView是Android L版本中新添加的一个用来取代ListView的SDK,是support-v7包中的新组件,是一个强大的滑动组件,与经典的ListView相比,同样拥有item回收复用的功能,这一点从它的名字recylerview即“回收view”也可以看得出来,毫无疑问它的灵活性与可替代性比listview更好。

看到这也许有人会问:除此之外还有什么优点呢?我们为什么一定要用RecylerView呢?下面我们详细介绍下RecylerView的诸多优点。

(2)RecyclerView的优点是什么?

根据官方的介绍RecylerView是ListView的升级版,既然如此那么RecylerView必然有它的优点,现就RecylerView相对于ListView的优点罗列如下:

1、RecylerView封装了viewholder的回收复用。

也就是说 RecylerView标准化了ViewHolder,编写Adapter面向的是ViewHolder而不再是View了,复用的逻辑被封装了,写起来更加简单

2、提供了一种插拔式的体验,高度的解耦,异常的灵活。

针对一个Item的显示,RecylerView专门抽取出了相应的类来控制Item的显示,使其的扩展性非常强。

比如:你想控制横向或者纵向滑动列表效果可以通过LinearLayoutManager这个类来进行控制,与GridView效果对应的是GridLayoutManager,与瀑布流对应的还有StaggeredGridLayoutManager等,也就是说RecylerView不再拘泥于ListView的线性展示方式,它也可以实现ListView、GridView等多种效果。再比如你想控制Item的分隔线,可以通过继承RecylerView的ItemDecoration这个类,然后针对自己的业务需求去写代码。

总之,通过简单改变下LayoutManager,就可以产生很多不同的效果,那么我们可以根据手机屏幕的宽度去动态设置LayoutManager,屏幕宽度一般的,显示为ListView,宽度稍大的显示两列的GridView或者瀑布流,或者横纵屏幕切换时变化,显示的列数和宽度成正比。甚至某些特殊屏幕,让其横向滑动,再选择一个好的动画效果,可以达到前所未有的用户体验。

3、可以控制Item增删的动画。

可以通过ItemAnimator这个类进行控制,当然针对增删的动画,RecylerView也有其自己默认的实现。

(3)RecyclerView项目结构

  • Adapter:使用RecyclerView之前,你需要一个继承自RecyclerView.Adapter的适配器,作用是将数据与每一个item的界面进行绑定。
  • LayoutManager:用来控制其显示的方式,比如:确定每一个item如何进行排列摆放,何时展示和隐藏。回收或重用一个View的时候,LayoutManager会向适配器请求新的数据来替换旧的数据,这种机制避免了创建过多的View和频繁的调用findViewById方法(与ListView原理类似)。
    目前SDK中提供了三种自带的LayoutManager:
    • LinearLayoutManager(线性布局管理器)
    • GridLayoutManager(网格布局管理器)
    • StaggeredGridLayoutManager(瀑布流布局管理器)

二、RecycleView 的简单使用

说了这么多,可能大家最关心的就是RecylerView应该怎么用,我们先来讨论讨论RecylerView的用法的理论知识,然后首先结合一个实例来展示是一个最简单的使用方法,然后介绍更多RecyclerView的用法。

1、添加依赖

在AS的build.gradle中添加依赖,然后同步一下就可以引入依赖包:

dependencies {
    ...
    compile 'com.android.support:appcompat-v7:24.0.0'
}

2、编写代码

添加完依赖之后,就开始写代码了,与ListView用法类似,也是先在xml布局文件中创建一个RecyclerView的布局:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <android.support.v7.widget.RecyclerView
        android:id="@+id/my_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"/>
</RelativeLayout>
创建完布局之后在MainActivity中获取这个RecyclerView,并声明LayoutManager与Adapter,代码如下:
mRecyclerView = (RecyclerView)findViewById(R.id.my_recycler_view);
// 创建默认的线性LayoutManager
mLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(mLayoutManager);
// 如果可以确定每个item的高度是固定的,设置这个选项可以提高性能
mRecyclerView.setHasFixedSize(true);
// 创建并设置Adapter
mAdapter = newMyAdapter(getDummyDatas());
mRecyclerView.setAdapter(mAdapter);

可以看到对RecylerView的设置过程,比ListView要复杂一些,因为ListView可能只需要去设置一个adapter就能正常使用了,这也是RecylerView高度解耦的表现,虽然代码抒写上有点复杂,但它的扩展性是极高的。也就是说RecyclerView只管回收与复用View,其他的你可以自己去设置。这就给予了我们充分定制的自由,所以我们才可以轻松的通过这个控件实现ListView、GirdView、瀑布流等效果。

接下来就是Adapter的创建:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
    public String[] datas = null;
    public MyAdapter(String[] datas) {
        this.datas = datas;
    }
    // 创建新View,被LayoutManager所调用
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item,viewGroup,false);
        ViewHolder vh = new ViewHolder(view);
        return vh;
    }
    // 将数据与界面进行绑定的操作
    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int position) {
        viewHolder.mTextView.setText(datas[position]);
    }
    // 获取数据的数量
    @Override
    public int getItemCount() {
        return datas.length;
    }
    // 自定义的ViewHolder,持有每个Item的的所有界面元素
    public static class ViewHolder extends RecyclerView.ViewHolder {
        public TextView mTextView;
        public ViewHolder(View view){
        super(view);
            mTextView = (TextView) view.findViewById(R.id.text);
        }
    }
}

在了解了RecyclerView的一些控制之后,我们来看看它的Adapter的写法,RecyclerView的Adapter与ListView的Adapter还是有点区别的,RecyclerView.Adapter需要实现3个方法:

  • onCreateViewHolder()
    这个方法主要生成为每个Item inflater出一个View,但是该方法返回的是一个ViewHolder。该方法把View直接封装在ViewHolder中,然后我们面向的是ViewHolder这个实例,当然这个ViewHolder需要我们自己去编写。直接省去了当初的convertView.setTag(holder)和convertView.getTag()这些繁琐的步骤。
  • onBindViewHolder()
    这个方法主要用于适配渲染数据到View中。方法提供给你了一个viewHolder,而不是原来的convertView。
  • getItemCount()
    这个方法就类似于BaseAdapter的getCount方法了,即总共有多少个条目。

可以看到RecyclerView标准化了ViewHolder,编写 Adapter面向的是ViewHoder而不在是View了,复用的逻辑被封装了,写起来更加简单。其实它的写法与BaseAdapter的写法是差不多的,大家可以对比下它与getView方法写法的区别,在onCreateViewHolder方法中初始化了一个View,然后返回一个ViewHolder,这个返回的ViewHolder类似于之前在getView中的convertView.getTag(),然后在onBindViewHolder方法中去给这个ViewHolder中的控件填充值。其实它的原理跟getView是差不多的,只是做了封装,我们写起来比较简洁。

编译运行效果如下:

三、RecycleView 添加点击和长击事件

上面我们讲了如何使用RecyclerView的Adpater,接下来我们为添加点击监听的功能。

熟悉ListView的小伙伴都知道ListView在使用的时候是可以直接添加点击事件的,就是说ListView给我们提供了一个onItemClickListener监听器,这样当我们点击Item的时候,会回调相关的方法,以便我们方便处理Item点击事件。但是对于RecyclerView来讲,非常可惜的是,该控件没有给我们提供ClickListener和LongClickListener这样的内置监听器方法,所以我们需要自己动手进行改造实现,只不过是会多了些代码而已。

实现的方式比较多,我们可以通过mRecyclerView.addOnItemTouchListener去监听然后去判断手势,
也可以通过adapter中自己去提供回调,这里我们选择最常用的后者来讲解,前者的方式大家有时间有兴趣自己去实现。

(1)首先我们在Adapter中创建一个实现点击接口:

其中view是点击的Item,data是我们的数据,因为我们想知道我点击的区域部分的数据是什么,以便我下一步进行操作:

public static interface OnRecyclerViewItemClickListener {
    void onItemClick(View view , DataModel data);
}
(2)定义完接口,添加接口和设置Adapter接口的方法:
private OnRecyclerViewItemClickListener mOnItemClickListener = null;
public void setOnItemClickListener(OnRecyclerViewItemClickListener listener) {
    this.mOnItemClickListener = listener;
}
(3)然后为Adapter实现OnClickListener方法:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> implements View.OnClickListener{
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, final int i) {
        View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item, viewGroup, false);
        ViewHolder vh = new ViewHolder(view);
        // 将创建的View注册点击事件
        view.setOnClickListener(this);
        return vh;
    }
    @Override
    public void onBindViewHolder(ViewHolder viewHolder, final int i) {
        viewHolder.mTextView.setText(datas.get(i).title);
        // 将数据保存在itemView的Tag中,以便点击时进行获取
        viewHolder.itemView.setTag(datas.get(i));
    }
    ...
    @Override
    public void onClick(View v) {
        if (mOnItemClickListener != null) {
            // 注意这里使用getTag方法获取数据
            mOnItemClickListener.onItemClick(v,(DataModel)v.getTag());
        }
    }
    ...
}
当然我们也可以通过Recyclerview直接获取item位置信息:
        @Override
        public void onClick(View v) {
            int position = mRecyclerview.getChildAdapterPosition(v);
            Log.i(TAG, "position : " + position);
        }

这样获取位置信息的方法依赖于Recyclerview,所以如果MyAdapter和当前界面不是在同一个界面的话就需要传过来一个Recyclerview实例,这样是比较麻烦的,一般来讲我们在mAdapter设置监听器的地方来获取即可,如下。

(4)最后在Activity或其他地方应用:
mAdapter = new MyAdapter(getDummyDatas());
mRecyclerView.setAdapter(mAdapter);
mAdapter.setOnItemClickListener(new MyAdapter.OnRecyclerViewItemClickListener() {
    @Override
    public void onItemClick(View view, DataModel data) {
        // Do something.
    }
});

这样我们的点击事件监听器就设置完毕了,同理我们可以为RecyclerView设置长击事件,而且把两个接口融合到一起也是可以的,比如:

    public interface OnItemClickLitener {
        void onItemClick(View view, int position);
        void onItemLongClick(View view, int position);
    }

后续的步骤同点击事件是一样的,但是我们却可以在长击事件里面做更多的事情,比如我们熟悉的长按删除,可谓一举两得。

编译运行看效果:

四、RecycleView 添加和删除数据

以前在ListView当中,我们只要修改后数据用Adapter的notifyDatasetChange一下就可以更新界面。然而在RecyclerView中还有一些更高级的用法:

  • 添加数据:
public void addItem(DataModel content, int position) {
    datas.add(position, content);
    notifyItemInserted(position); //Attention!
}
  • 删除数据:
public void removeItem(DataModel model) {
    int position = datas.indexOf(model);
    datas.remove(position);
    notifyItemRemoved(position);//Attention!
}

改变数据后我们可以设置RecycleView滚动到相应位置:

mRecyclerview.scrollToPosition(position);

五、RecycleView 增加多样式分隔线

我们可以通过RecyclerView.addItemDecoration(ItemDecoration decoration)这个方法进行设置RecycleView的分割线,但是RecycleView的分隔线是不是强制地需要被设置呢?当然不是,因为我们上面的例子没有设置分隔线也没有报错,但是通过设置分割线可以让我们每一个Item从视觉上面相互分开来,例如ListView中divider哪种非常相似的效果。

可以看见,RecyclerView.addItemDecoration(ItemDecoration decoration)这个方法是一个ItemDecoration,而这个可以是我们自己定义的继承自ItemDecoration的一个对象,所以接下来我们创建一个继承RecyclerView.ItemDecoration类来绘制分隔线。

系统提供的ItemDecoration是一个抽象类,我们主要实现以下几个方法:
public static abstract class ItemDecoration {

public void onDraw(Canvas c, RecyclerView parent, State state) {
            onDraw(c, parent);
}

public void onDrawOver(Canvas c, RecyclerView parent, State state) {
            onDrawOver(c, parent);
}

public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
            getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(), parent);
}

@Deprecated
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
            outRect.set(0, 0, 0, 0);
}

(1)增加分隔线

既然我们为RecycleView设置了分隔线,那么 当我们的RecyclerView在进行绘制的时候一定会进行Decoration的绘制,那么就会去调用onDraw和onDrawOver方法,所以我们其实只要去重写onDraw(onDrawOver在drawChildren之后,一般我们选择复写其中一个即可)和getItemOffsets这两个方法就可以实现相应的分隔线绘制了。而在绘制好了之后,LayoutManager会进行Item的布局,这个时候就会去调用getItemOffset方法来计算每个Item的Decoration合适的尺寸,这就是分隔线在绘制的过程中各个方法的调用步骤 ,下面我们来实现一个:

public class DividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };

    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;

    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

    private Drawable mDivider;

    private int mOrientation;

    public DividerItemDecoration(Context context, int orientation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }

    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent) {
        Log.v("recyclerview - itemdecoration", "onDraw()");

        if (mOrientation == VERTICAL_LIST) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }

    public void drawVertical(Canvas c, RecyclerView parent) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            android.support.v7.widget.RecyclerView v = new android.support.v7.widget.RecyclerView(parent.getContext());
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    public void drawHorizontal(Canvas c, RecyclerView parent) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int left = child.getRight() + params.rightMargin;
            final int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
        if (mOrientation == VERTICAL_LIST) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
}

在这里我们是采用了系统主题(android.R.attr.listDivider)的方式来设置分隔线的(使用系统listDivider的好处是方便我们去随意的改变分隔线样式),然后来获取尺寸和位置进行setBound()绘制。接着通过outRect.set()来设置绘制整个区域范围,当然了它是有两种情况的,一种LinearLayoutManager.HORIZONTAL另外一种LinearLayoutManager.VERTICAL,我们需要分别对其进行处理。最后在RecyclerView中设置我们自定义的分割线,即在MainActivity中加上一句recyclerView .addItemDecoration(new DividerItemDecoration(MainActivity.this,LinearLayoutManager.VERTICAL)。

(2)改变样式

正如我们上面提到的,使用系统listDivider的好处是方便我们去随意的改变,因为该属性我们可以直接声明在这里:
 <!-- Application theme. -->
    <style name="AppTheme" parent="AppBaseTheme">
      <item name="android:listDivider">@drawable/divider_bg</item>  
    </style>
然后我们可以自己写个drawable,以此实现分隔符的更换:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
    <gradient
        android:centerColor="#ff00ff00"
        android:endColor="#ff0000ff"
        android:startColor="#ffff0000"
        android:type="linear" />
    <size android:height="4dp"/>
</shape>
或者这样:
<?xml version="1.0" encoding= "utf-8"?>  
<shape xmlns:android="http://schemas.android.com/apk/res/android"  
    android:shape="rectangle" >  
     
        <!-- 填充的颜色 -->  
       <solid android:color ="@color/color_red"/>  
       
      <!--  线条大小 -->  
      <size android:height ="1dp" android:width ="1dp"/>  
</shape>  

六、 两种方式为 RecyclerView 添加动画

(1)添加系统动画和自定义动画

控制RecyclerView增加和删除的动画是通过ItemAnimator这个类来实现的,ItemAnimator这类也是个抽象的类,系统默认给我们提供了一种增加和删除的动画,下面我们就来看看这种动画的效果,我们需要做的修改如下:

LinearLayoutManager layoutManager = new LinearLayoutManager(this);  
// 设置布局管理器  
recyclerView.setLayoutManager(layoutManager);  
// 设置增加或删除条目的动画  
recyclerView.setItemAnimator( new DefaultItemAnimator()); 

值得注意的是:我们这里更新数据集不是用 adapter.notifyDataSetChanged() 而是 notifyItemInserted(position) 与 notifyItemRemoved(position),否则是没有动画效果的,所以在Adapter中增加的代码如下:

public void addData(int position) {
    mDatas.add(position, "Insert One");
    notifyItemInserted(position);
}

public void removeData(int position) {
    mDatas.remove(position);
    notifyItemRemoved(position);
}

然后在合适的地方调用addData和removeData就能看到相应的动画效果了。

我们这里只提供了一种动画,如果你想要去自定义各种更好玩的动画效果的话,可以自定义实现,也可以到github的许多类似的项目中借鉴经验,比如这里:RecyclerViewItemAnimators,有兴趣的同学可以自行查阅。

(2)使用ItemTouchHelper实现Item的拖拽和滑动删除以及动画效果

简介:

Google官方文档上是这么介绍的:
This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView. 这是一个支持RecyclerView滑动删除和拖拽的实用工具类。

我们自己的理解:
ItemTouchHelper是一个强大的工具,它是RecyclerView.ItemDecoration的子类,它处理好了关于在RecyclerView上添加拖动排序与滑动删除的所有事情,也就是说它可以轻易的添加到几乎所有的LayoutManager和Adapter中。除此之外,它还可以和现有的item动画一起工作,提供受类型限制的拖放动画等。

首先我们给出itemTouchHelper的整体实现,然后对其中各个方法做出解释。

itemTouchHelper的整体实现:
    itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
        @Override
        public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
            int dragFlags = 0, swipeFlags = 0;
            if (recyclerView.getLayoutManager() instanceof StaggeredGridLayoutManager) {
                dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
            } else if (recyclerView.getLayoutManager() instanceof LinearLayoutManager) {
                dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
                // 设置侧滑方向为从左到右和从右到左都可以
                swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
            }
            return makeMovementFlags(dragFlags, swipeFlags);
        }

        @Override
        public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
            int from = viewHolder.getAdapterPosition();
            int to = target.getAdapterPosition();
            Collections.swap(meizis, from, to);
            mAdapter.notifyItemMoved(from, to);
            return true;
        }

        @Override
        public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
            mAdapter.removeItem(viewHolder.getAdapterPosition());
        }

        @Override
        public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
            super.onSelectedChanged(viewHolder, actionState);
            if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
                viewHolder.itemView.setBackgroundColor(Color.LTGRAY);
            }
        }

        @Override
        public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
            super.clearView(recyclerView, viewHolder);
            viewHolder.itemView.setBackgroundColor(Color.WHITE);
        }

        @Override
        public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
            viewHolder.itemView.setAlpha(1 - Math.abs(dX) / screenwidth);
            super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
        }

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

        @Override
        public boolean isItemViewSwipeEnabled() {
            return true;
        }
    });
  • getMovementFlags()方法:
    ItemTouchHelper让我们可以轻易得到一个事件的方向,我们需要重写getMovementFlags()方法来指定可以支持的拖放和滑动的方向。使用helperItemTouchHelper.makeMovementFlags(int, int)来构造返回的flag,这里我们启用了上下左右两种方向。
    注:上下为拖动(drag),左右为滑动(swipe)。
  • isLongPressDragEnabled()方法:
    ItemTouchHelper可以用于没有滑动的拖动操作(或者反过来),你必须指明你到底要支持哪一种。要支持长按RecyclerView item进入拖动操作,你必须在isLongPressDragEnabled()方法中返回true。或者,也可以调用ItemTouchHelper.startDrag(RecyclerView.ViewHolder) 方法来开始一个拖动。
  • isItemViewSwipeEnabled()方法:
    要在view任意位置触摸事件发生时启用滑动操作,则直接在sItemViewSwipeEnabled()中返回true就可以了。或者,你也主动调用ItemTouchHelper.startSwipe(RecyclerView.ViewHolder) 来开始滑动操作。
  • onMove()和onSwiped()方法:
    用于通知底层数据的更新。
  • onSelectedChanged()、clearView()和onChildDraw()方法:
    设置拖拽和滑动时的响应动画效果。

七、几种不同的布局管理器

上面我们编写的类似ListView样子的Demo都是通过LinearLayoutManager来实现的,接下来我们介绍几种其它类型的LayoutManager。

系统提供了3个实现类:
  • LinearLayoutManager:线性管理器,支持横向、纵向。
  • GridLayoutManager:网格布局管理器。
  • StaggeredGridLayoutManager:瀑布流式布局管理器。

(1)实现网格布局

如果想把上面的线性布局改成网格布局,可以这样改写:

//mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));

但是改为GridLayoutManager以后,对于分割线前面的DividerItemDecoration就不再适用了,主要是因为它在绘制的时候,比如水平线针对每个child的取值为:

final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();

这样写对于每个Item一行的情况是没问题的,而GridLayoutManager一行有多个childItem,这样就会多次绘制,并且GridLayoutManager的Item如果为最后一列或者为最后一行时,也需要对边界条件进行处理,所以我们编写了DividerGridItemDecoration:

    public class DividerGridItemDecoration extends RecyclerView.ItemDecoration {

        private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
        private Drawable mDivider;

        public DividerGridItemDecoration(Context context) {
            final TypedArray a = context.obtainStyledAttributes(ATTRS);
            mDivider = a.getDrawable(0);
            a.recycle();
        }

        @Override
        public void onDraw(Canvas c, RecyclerView parent, State state) {

            drawHorizontal(c, parent);
            drawVertical(c, parent);

        }

        private int getSpanCount(RecyclerView parent) {
            // 列数
            int spanCount = -1;
            LayoutManager layoutManager = parent.getLayoutManager();
            if (layoutManager instanceof GridLayoutManager) {

                spanCount = ((GridLayoutManager) layoutManager).getSpanCount();
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                spanCount = ((StaggeredGridLayoutManager) layoutManager)
                        .getSpanCount();
            }
            return spanCount;
        }

        public void drawHorizontal(Canvas c, RecyclerView parent) {
            int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = parent.getChildAt(i);
                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                        .getLayoutParams();
                final int left = child.getLeft() - params.leftMargin;
                final int right = child.getRight() + params.rightMargin
                        + mDivider.getIntrinsicWidth();
                final int top = child.getBottom() + params.bottomMargin;
                final int bottom = top + mDivider.getIntrinsicHeight();
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(c);
            }
        }

        public void drawVertical(Canvas c, RecyclerView parent) {
            final int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = parent.getChildAt(i);

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

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

        private boolean isLastColum(RecyclerView parent, int pos, int spanCount, int childCount) {
            LayoutManager layoutManager = parent.getLayoutManager();
            if (layoutManager instanceof GridLayoutManager) {
                if ((pos + 1) % spanCount == 0)// 如果是最后一列,则不需要绘制右边
                {
                    return true;
                }
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                int orientation = ((StaggeredGridLayoutManager) layoutManager).getOrientation();
                if (orientation == StaggeredGridLayoutManager.VERTICAL) {
                    if ((pos + 1) % spanCount == 0)// 如果是最后一列,则不需要绘制右边
                    {
                        return true;
                    }
                } else {
                    childCount = childCount - childCount % spanCount;
                    if (pos >= childCount)// 如果是最后一列,则不需要绘制右边
                        return true;
                }
            }
            return false;
        }

        private boolean isLastRaw(RecyclerView parent, int pos, int spanCount, int childCount) {
            LayoutManager layoutManager = parent.getLayoutManager();
            if (layoutManager instanceof GridLayoutManager) {
                childCount = childCount - childCount % spanCount;
                if (pos >= childCount)// 如果是最后一行,则不需要绘制底部
                    return true;
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                int orientation = ((StaggeredGridLayoutManager) layoutManager)
                        .getOrientation();
                // StaggeredGridLayoutManager 且纵向滚动
                if (orientation == StaggeredGridLayoutManager.VERTICAL) {
                    childCount = childCount - childCount % spanCount;
                    // 如果是最后一行,则不需要绘制底部
                    if (pos >= childCount)
                        return true;
                } else
                // StaggeredGridLayoutManager 且横向滚动
                {
                    // 如果是最后一行,则不需要绘制底部
                    if ((pos + 1) % spanCount == 0) {
                        return true;
                    }
                }
            }
            return false;
        }

        @Override
        public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
            int spanCount = getSpanCount(parent);
            int childCount = parent.getAdapter().getItemCount();
            if (isLastRaw(parent, itemPosition, spanCount, childCount))// 如果是最后一行,则不需要绘制底部
            {
                outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
            } else if (isLastColum(parent, itemPosition, spanCount, childCount))// 如果是最后一列,则不需要绘制右边
            {
                outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
            } else {
                outRect.set(0, 0, mDivider.getIntrinsicWidth(),
                        mDivider.getIntrinsicHeight());
            }
        }
    }

也就是说我们需要在getItemOffsets方法中去判断是否是最后一行,如果是的话则不需要绘制底部;如果是最后一列的话则不需要绘制右边,并且整个判断也考虑到了StaggeredGridLayoutManager 的横向和纵向,所以稍稍有些复杂,一般如果仅仅是希望有空隙,还是去设置item的margin更方便。

(2)实现瀑布流布局

瀑布流布局其实也可以实现GridLayoutManager一样的功能,我们按照下列代码改写:

// mRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));
mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL));
可以看到效果为:

我们可以发现:通过RecyclerView去实现ListView、GridView、瀑布流的效果基本上没有什么区别,仅仅通过设置不同的LayoutManager即可实现不同的效果,这就是RecyclerView结合LayoutManager的强大之处。

感谢优秀的你跋山涉水看到了这里,不如关注下让我们永远在一起!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容