澳门新葡萄京官网注册 14

澳门新葡萄京官网注册【转载】为什么要使用ItemDecoration

在Android应用开发中会经常碰到一个叫divider的东西,就是两个View之间的分割线。最近工作中注意到这个divider并分析了一下,竟然发现内有乾坤,惊为天人…

为什么要使用ItemDecoration
泡在网上的日子 / 文 发表于2017-06-22 17:20 第277次阅读
ItemDecoration,RecyclerView
0
编辑推荐:稀土掘金,这是一个针对技术开发者的一个应用,你可以在掘金上获取最新最优质的技术干货,不仅仅是Android知识、前端、后端以至于产品和设计都有涉猎,想成为全栈工程师的朋友不要错过!
原文:ItemDecoration in Android Part 1 : Avoid adding dividers to the
view
layout
Part 1:不要用view做分割线

RecylerView介绍

RecylerView是support-v7包中的新组件,是一个强大的滑动组件,与经典的ListView相比,同样拥有item回收复用的功能,这一点从它的名字recylerview即回收view也可以看出。官方对于它的介绍则是:RecyclerView
是 ListView
的升级版本,更加先进和灵活。RecyclerView通过设置LayoutManager,ItemDecoration,ItemAnimator实现你想要的效果。

  • 使用LayoutManager来确定每一个item的排列方式。
  • 使用ItemDecoration自己绘制分割线,更灵活
  • 使用ItemAnimator为增加或删除一行设置动画效果。

ListView的divider

澳门新葡萄京官网注册 1

注意

新建完项目,需要在app/build.gradle增加RecylerView依赖,不然找不到RecyclerView类

compile 'com.android.support:recyclerview-v7:23.1.0'

1. 定制divider的边距

ListView的divider默认是左右两头到底的,如何简单的设置一个边距呢?

利用inset或者layer-list都可以简单的实现,代码如下:

<!-- 方法一 -->
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
 android:insetLeft="16dp" >
    <shape android:shape="rectangle" >
        <solid android:color="#f00" />
    </shape>
</inset>
<!-- 方法二 -->
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:left="16dp">
        <shape android:shape="rectangle">
            <solid android:color="#f00" />
        </shape>
    </item>
</layer-list>

其中inset除了左边距insetLeft, 还有insetTop、insetRight、insetBottom,
效果图:

澳门新葡萄京官网注册 2

1498123313800821.jpeg

RecylerView简单的Demo

我们来看activity代码,跟ListView写法差不多,只是这边多设置了布局管理器。

public class LinearLayoutActivity extends AppCompatActivity {
    private RecyclerView recyclerView;
    private RecyclerViewAdapter adapter;
    private List<String> datas;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.recycler_main);

        initData();

        recyclerView= (RecyclerView) findViewById(R.id.recyclerview);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));//设置布局管理器
        recyclerView.addItemDecoration(new DividerItemDecoration(this));
        recyclerView.setAdapter(adapter=new RecyclerViewAdapter(this,datas));
    }

    private void initData(){
        datas=new ArrayList<>();
        for(int i=0;i<100;i++){
            datas.add("item:"+i);
        }
    }
}

activity对应的布局文件:recycler_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>

adapter相对ListView来说变化比较大的。把ViewHolder逻辑封装起来了,代码相对简单一些。

  • 需要继承RecyclerView.Adapter,重写三个方法
  • MyViewHolder需要继承RecyclerView.ViewHolder

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.MyViewHolder>{
    private List<String> datas;
    private LayoutInflater inflater;

    public  RecyclerViewAdapter(Context context,List<String> datas){
        inflater=LayoutInflater.from(context);
        this.datas=datas;
    }

    //创建每一行的View 用RecyclerView.ViewHolder包装
    @Override
    public RecyclerViewAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView=inflater.inflate(R.layout.recycler_item,null);
        return new MyViewHolder(itemView);
    }

    //给每一行View填充数据
    @Override
    public void onBindViewHolder(RecyclerViewAdapter.MyViewHolder holder, int position) {
        holder.textview.setText(datas.get(position));
    }

    //数据源的数量
    @Override
    public int getItemCount() {
        return datas.size();
    }

    class MyViewHolder extends RecyclerView.ViewHolder{
        private TextView textview;

        public MyViewHolder(View itemView) {
            super(itemView);
            textview= (TextView) itemView.findViewById(R.id.textview);
        }
    }
}

我们来看看效果图:

澳门新葡萄京官网注册 3

RecylerView基本使用

2. 最后一项的divider

很多同学可能发现了,ListView最后一项的divider有时候有,有时候又没有。

我画个图大家就都能理解了:

澳门新葡萄京官网注册 4

上面是数据不足的显示效果,如果数据满屏的话,都是看不多最后的divider的。

真相是,当ListView高度是不算最后一项divider的,所以只有在match_parent的情况下,ListView的高度是有余的,才能画出最后的那个divider。

ps:网上很多资料,把最后一项的divider和footerDividersEnabled混在一起了,这个是不对的,两个从逻辑上是独立的,类似的还有一个headerDividersEnabled,headerDividersEnabled和footerDividersEnabled不会影响到默认情况下最后的divider的绘制,他们是给header和footer专用的,特此说明。

首先,什么是ItemDecoration?来看看官网是如何解释的。
ItemDecoration允许从adapter的数据集合中为特定的item视图添加特性的绘制以及布局间隔。它可以用来实现item之间的分割线,高亮,分组边界等。
我们不能简单的把ItemDecoration看成一个名字响亮的分割线。它比divider要多很多内容。一个divider只能绘制在item之间,但是ItemDecoration可以绘制在item的四边。ItemDecoration为decoration的测量和绘制提供了全方位的控制。一个decoration可以是一条分割线,也可以仅仅是一个间隔(inset)。
但不幸的是,绝大多数android开发者都没有使用item
decoration。在这个分为三部分的系列文章中,我们将了解ItemDecoration的强大之处。
第一部分: 不要添加view来做分割线— 使用 ItemDecoration
第二部分: 不要使用padding来做间隔 —使用 ItemDecoration
第三部分: 在GridLayoutManager中高效的绘制decorations
本文是第一部分。
不要用view做分割线 —会影响性能
我曾看到一些开发者在为RecyclerView添加divider的时候采用了一些捷径。原因很简单,ListView原生支持divider,可以直接在xml中设置divider。
<ListView
android:id=”@+id/activity_home_list_view”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:divider=”@android:color/black”
android:dividerHeight=”8dp”/>

RecyclerView增加分隔线

RecyclerView是没有android:divider跟android:dividerHeight属性的,如果我们需要分割线,就只能自己动手去实现了。

  • 需要继承ItemDecoration类,实现onDraw跟getItemOffsets方法。
  • 调用RecyclerView的addItemDecoration方法。

我们先写一个DividerItemDecoration类,继承RecyclerView.ItemDecoration,在getItemOffsets留出item之间的间隔,然后就会调用onDraw方法绘制(onDraw的绘制优先于每一行的绘制)

public class DividerItemDecoration extends RecyclerView.ItemDecoration{
    /*
    * RecyclerView的布局方向,默认先赋值 为纵向布局
    * RecyclerView 布局可横向,也可纵向
    * 横向和纵向对应的分割线画法不一样
    * */
    private int mOrientation = LinearLayoutManager.VERTICAL;

    private int mItemSize = 1;//item之间分割线的size,默认为1

    private Paint mPaint;//绘制item分割线的画笔,和设置其属性

    public DividerItemDecoration(Context context) {
        this(context,LinearLayoutManager.VERTICAL,R.color.colorAccent);
    }

    public DividerItemDecoration(Context context, int orientation) {
        this(context,orientation, R.color.colorAccent);
    }

    public DividerItemDecoration(Context context, int orientation, int dividerColor){
        this(context,orientation,dividerColor,1);
    }

    /**
     * @param context
     * @param orientation 绘制方向
     * @param dividerColor 分割线颜色 颜色资源id
     * @param mItemSize 分割线宽度 传入dp值就行
     */
    public DividerItemDecoration(Context context, int orientation, int dividerColor, int mItemSize){
        this.mOrientation = orientation;
        if(orientation != LinearLayoutManager.VERTICAL && orientation != LinearLayoutManager.HORIZONTAL){
            throw new IllegalArgumentException("请传入正确的参数") ;
        }
        //把dp值换算成px
        this.mItemSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,mItemSize,context.getResources().getDisplayMetrics());
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        mPaint.setColor(context.getResources().getColor(dividerColor));
    }


    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if(mOrientation == LinearLayoutManager.VERTICAL){
            drawVertical(c,parent) ;
        }else {
            drawHorizontal(c,parent) ;
        }
    }

    /**
     * 绘制纵向 item 分割线
     * @param canvas
     * @param parent
     */
    private void drawVertical(Canvas canvas,RecyclerView parent){
        final int left = parent.getPaddingLeft() ;
        final int right = parent.getMeasuredWidth() - parent.getPaddingRight();
        final int childSize = parent.getChildCount() ;
        for(int i = 0 ; i < childSize ; i ++){
            final View child = parent.getChildAt( i ) ;
            RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
            final int top = child.getBottom() + layoutParams.bottomMargin ;
            final int bottom = top + mItemSize ;
            canvas.drawRect(left,top,right,bottom,mPaint);
        }
    }

    /**
     * 绘制横向 item 分割线
     * @param canvas
     * @param parent
     */
    private void drawHorizontal(Canvas canvas,RecyclerView parent){
        final int top = parent.getPaddingTop() ;
        final int bottom = parent.getMeasuredHeight() - parent.getPaddingBottom() ;
        final int childSize = parent.getChildCount() ;
        for(int i = 0 ; i < childSize ; i ++){
            final View child = parent.getChildAt( i ) ;
            RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
            final int left = child.getRight() + layoutParams.rightMargin ;
            final int right = left + mItemSize ;
            canvas.drawRect(left,top,right,bottom,mPaint);
        }
    }

    /**
     * 设置item分割线的size
     * @param outRect
     * @param view
     * @param parent
     * @param state
     */
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if(mOrientation == LinearLayoutManager.VERTICAL){
            outRect.set(0,0,0,mItemSize);//垂直排列 底部偏移
        }else {
            outRect.set(0,0,mItemSize,0);//水平排列 右边偏移
        }
    }
}

不要忘记调用addItemDecoration方法哦

recyclerView.addItemDecoration(new DividerItemDecoration(this));//添加分割线

重新运行,效果图:

澳门新葡萄京官网注册 5

添加分割线

大家读到这里肯定会有一个疑问,这货比ListView麻烦多了啊,但是google官方为什么要说是ListView的升级版呢?接下来开始放大招。。。

RecyclerView的Divider

RecyclerView的Divider叫做ItemDecoration,RecyclerView.ItemDecoration本身是一个抽象类,官方没有提供默认实现。

官方的Support7Demos例子中有个DividerItemDecoration,
我们可以直接参考一下,位置在sdk的这里:

extras/android/support/samples/Support7Demos/src/…/…/decorator/DividerItemDecoration.java

但是这个DividerItemDecoration有三个问题:

  1. 只支持系统默认样式,不支持自定义Drawable类型的divider
  2. 里面的算法对于无高宽的Drawable(比如上面用到的InsetDrawable)是画不出东西的
  3. 水平列表的Divider绘制方法drawHorizontal()的right计算有误,导致垂直Divider会绘制不出来,应该改为:final
    int right = left + mDivider.getIntrinsicWidth();;

针对这几个问题,我修复并增强了一下:

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;

/**
 * RecyclerView的ItemDecoration的默认实现
 * 1. 默认使用系统的分割线
 * 2. 支持自定义Drawable类型
 * 3. 支持水平和垂直方向
 * 4. 修复了官方垂直Divider显示的bug
 * 扩展自官方android sdk下的Support7Demos下的DividerItemDecoration
 */
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 mWidth;
    private int mHeight;

    private int mOrientation;

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

    /**
 * 新增:支持自定义dividerDrawable
 *
 * @param context
 * @param orientation
 * @param dividerDrawable
 */
    public DividerItemDecoration(Context context, int orientation, Drawable dividerDrawable) {
        mDivider = dividerDrawable;
        setOrientation(orientation);
    }

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

    /**
 * 新增:支持手动为无高宽的drawable制定宽度
 * @param width
 */
    public void setWidth(int width) {
        this.mWidth = width;
    }

    /**
 * 新增:支持手动为无高宽的drawable制定高度
 * @param height
 */
    public void setHeight(int height) {
        this.mHeight = height;
    }

    @Override
        public void onDraw(Canvas c, RecyclerView parent) {
            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);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin +
                Math.round(ViewCompat.getTranslationY(child));
            final int bottom = top + getDividerHeight();
            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 +
                Math.round(ViewCompat.getTranslationX(child));
            final int right = left + getDividerWidth();
            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, getDividerHeight());
            } else {
                outRect.set(0, 0, getDividerWidth(), 0);
            }
        }

    private int getDividerWidth() {
        return mWidth > 0 ? mWidth : mDivider.getIntrinsicWidth();
    }

    private int getDividerHeight() {
        return mHeight > 0 ? mHeight : mDivider.getIntrinsicHeight();
    }

}

使用如下:

// 默认系统的divider
dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST);
// 自定义图片drawable分的divider
dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST, getResources().getDrawable(R.drawable.ic_launcher));
// 自定义无高宽的drawable的divider - 垂直列表
dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST, new ColorDrawable(Color.parseColor("#ff00ff")));
dividerItemDecoration.setHeight(1);
// 自定义无高宽的drawable的divider - 水平列表
dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.HORIZONTAL_LIST, new ColorDrawable(Color.parseColor("#ff00ff")));
dividerItemDecoration.setWidth(1);
// 自定义带边距且无高宽的drawable的divider(以上面InsetDrawable为例子)
// 这个地方也可以在drawable的xml文件设置size指定宽高,效果一样
dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.HORIZONTAL_LIST, getResources().getDrawable(R.drawable.list_divider));
dividerItemDecoration.setWidth(DisplayLess.$dp2px(16) + 1);

但是到了RecyclerView,就再也不能直接添加divider了。需要添加一个绘制divider的ItemDecoration。但是开发者发现它很麻烦,于是直接把divider添加到(item的)view上,而不是使用ItemDecoration。
<LinearLayout android:orientation=”vertical”>
<LinearLayout android:orientation=”horizontal”>
<ImageView />
<TextView />
</LinearLayout>
<View
android:width=”match_parent”
android:height=”1dp”
android:background=”#333″ />
</LinearLayout>

GridLayoutManager

在RecyclerView中实现不同的列表,只需要切换不同的LayoutManager即可。RecyclerView.LayoutManager跟RecyclerView.ItemDecoration一样,都是RecyclerView静态抽象内部类,但是LayoutManager有三个官方写好的实现类。

  • LinearLayoutManager 线性布局管理器 跟ListView功能相似
  • GridLayoutManager 网格布局管理器 跟GridView功能相似
  • StaggeredGridLayoutManager 瀑布流布局管理器

刚刚我们用的是LinearLayoutManager,现在我们切换到GridLayoutManager,看到下面这句代码,有没有感觉分分钟切换不同列表显示。

recyclerView.setLayoutManager(new GridLayoutManager(this,2));

如果要显示多列或者要纵向显示就new不同的构造方法,以下代码纵向显示4列。当前如果你还需要反方向显示,把false改成true就可以。

recyclerView.setLayoutManager(new GridLayoutManager(this,4,GridLayoutManager.HORIZONTAL,false));

因为用的是网格布局,所以呢绘制分割线的代码需要重新修改一下。网格布局一行可以有多列,并且最后一列跟最后一行不需要绘制,所以我们得重新创建一个类。
DividerGridItemDecoration.java

public class DividerGridItemDecoration extends RecyclerView.ItemDecoration {
    /*
    * RecyclerView的布局方向,默认先赋值 为纵向布局
    * RecyclerView 布局可横向,也可纵向
    * 横向和纵向对应的分割线画法不一样
    * */
    private int mOrientation = LinearLayoutManager.VERTICAL;

    private int mItemSize = 1;//item之间分割线的size,默认为1

    private Paint mPaint;//绘制item分割线的画笔,和设置其属性

    public DividerGridItemDecoration(Context context) {
        this(context,LinearLayoutManager.VERTICAL,R.color.colorAccent);
    }

    public DividerGridItemDecoration(Context context, int orientation) {
        this(context,orientation, R.color.colorAccent);
    }

    public DividerGridItemDecoration(Context context, int orientation, int dividerColor){
        this(context,orientation,dividerColor,1);
    }

    /**
     * @param context
     * @param orientation 绘制方向
     * @param dividerColor 分割线颜色 颜色资源id
     * @param mItemSize 分割线宽度 传入dp值就行
     */
    public DividerGridItemDecoration(Context context, int orientation, int dividerColor, int mItemSize){
        this.mOrientation = orientation;
        if(orientation != LinearLayoutManager.VERTICAL && orientation != LinearLayoutManager.HORIZONTAL){
            throw new IllegalArgumentException("请传入正确的参数") ;
        }
        //把dp值换算成px
        this.mItemSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,mItemSize,context.getResources().getDisplayMetrics());
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        mPaint.setColor(context.getResources().getColor(dividerColor));
    }

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

    private int getSpanCount(RecyclerView parent) {
        // 列数
        int spanCount = -1;
        RecyclerView.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 canvas, 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 + mItemSize;
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mItemSize;
            canvas.drawRect(left,top,right,bottom,mPaint);
        }
    }

    public void drawVertical(Canvas canvas, 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 + mItemSize;
            canvas.drawRect(left,top,right,bottom,mPaint);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, int itemPosition,RecyclerView parent) {
        int spanCount = getSpanCount(parent);
        int childCount = parent.getAdapter().getItemCount();

        if (isLastRow(parent, itemPosition, spanCount, childCount)){//如果是最后一行,不需要绘制底部
            outRect.set(0, 0, mItemSize, 0);
        } else if (isLastColum(parent, itemPosition, spanCount, childCount)){// 如果是最后一列,不需要绘制右边
            outRect.set(0, 0, 0, mItemSize);
        } else {
            outRect.set(0, 0, mItemSize,mItemSize);
        }
    }

    private boolean isLastColum(RecyclerView parent, int pos, int spanCount, int childCount) {
        RecyclerView.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 isLastRow(RecyclerView parent, int pos, int spanCount, int childCount) {
        RecyclerView.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();
            if (orientation == StaggeredGridLayoutManager.VERTICAL){//纵向
                childCount = childCount - childCount % spanCount;
                if (pos >= childCount)//最后一行
                    return true;
            } else{ //横向
                if ((pos + 1) % spanCount == 0) {//是最后一行
                    return true;
                }
            }
        }
        return false;
    }
}

写了这两个画分割线的类,主流的布局:线性列表跟网格列表都能展示了。。。赶紧运行代码看看结果:

澳门新葡萄京官网注册 6

网格列表

手动的Divider

有的时候没有系统控件的原生支持,只能手动在两个view加一个divider,比如,设置界面每项之间的divider,水平平均分隔的几个view之间加一个竖的divider等等。

无论横的竖的,都非常简单,定一个View,设置一个background就可以了,正常情况下没什么好说的。

下面我们来考虑一种常见设置界面,这种设置界面的分割线是有左边距的,比如微信的设置界面,我相信绝大部分人的布局代码都是这样实现的:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent">

    <!--这个group_container的background一定要设置,
 而且要和list_item_bg的list_item_normal一致,
 否则效果会不正确。 -->
    <LinearLayout
 android:id="@+id/group_container"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_alignParentTop="true"
 android:layout_marginTop="48dp"
 android:background="#fff"
 android:orientation="vertical">

        <RelativeLayout
 android:id="@+id/account_container"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:background="@drawable/list_item_bg"
 android:clickable="true">

            <TextView
 android:id="@+id/account_title"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_alignParentLeft="true"
 android:layout_centerVertical="true"
 android:layout_margin="16dp"
 android:text="First Item"
 android:textColor="#f00"
 android:textSize="16sp" />
        </RelativeLayout>

        <View
 android:layout_width="match_parent"
 android:layout_height="1px"
 android:layout_marginLeft="16dp"
 android:background="#f00" />

        <RelativeLayout
 android:id="@+id/phone_container"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:background="@drawable/list_item_bg"
 android:clickable="true">

            <TextView
 android:id="@+id/phone_title"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_alignParentLeft="true"
 android:layout_centerVertical="true"
 android:layout_margin="16dp"
 android:text="Second Item"
 android:textColor="#f00"
 android:textSize="16sp" />

        </RelativeLayout>
    </LinearLayout>

</RelativeLayout>

效果图如下,顺便我们也看看它的Overdraw状态:

澳门新葡萄京官网注册 7

通过分析Overdraw的层次,我们发现为了一个小小的边距,设置了整个groud_container的背景,从而导致了一次Overdraw。

能不能优化掉这个Overdraw?答案是肯定的。

背景肯定要去掉,但是这个左边距的View就不能这么简单的写了,需要自定义一个View,它要支持能把左边距的空出的16dp的线用list_item_normal的颜色值绘制一遍,这样才能看的出左边距。

这个View具体代码如下:

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;

import com.jayfeng.lesscode.core.R;

public class SpaceDividerView extends View {

    private int mSpaceLeft = 0;
    private int mSpaceTop = 0;
    private int mSpaceRight = 0;
    private int mSpaceBottom = 0;
    private int mSpaceColor = Color.TRANSPARENT;

    private Paint mPaint = new Paint();

    public SpaceDividerView(Context context) {
        this(context, null);
    }

    public SpaceDividerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SpaceDividerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SpaceDividerView, defStyleAttr, 0);
        mSpaceLeft = a.getDimensionPixelSize(R.styleable.SpaceDividerView_spaceLeft,
                (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, getResources().getDisplayMetrics()));
        mSpaceTop = a.getDimensionPixelSize(R.styleable.SpaceDividerView_spaceTop,
                (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, getResources().getDisplayMetrics()));
        mSpaceRight = a.getDimensionPixelSize(R.styleable.SpaceDividerView_spaceRight,
                (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, getResources().getDisplayMetrics()));
        mSpaceBottom = a.getDimensionPixelSize(R.styleable.SpaceDividerView_spaceBottom,
                (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, getResources().getDisplayMetrics()));
        mSpaceColor = a.getColor(R.styleable.SpaceDividerView_spaceColor, Color.TRANSPARENT);
        a.recycle();

        mPaint.setColor(mSpaceColor);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mSpaceLeft > 0) {
            canvas.drawRect(0, 0, mSpaceLeft, getMeasuredHeight(), mPaint);
        }
        if (mSpaceTop > 0) {
            canvas.drawRect(0, 0, getMeasuredWidth(), mSpaceTop, mPaint);
        }
        if (mSpaceRight > 0) {
            canvas.drawRect(getMeasuredWidth() - mSpaceRight, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
        }
        if (mSpaceBottom > 0) {
            canvas.drawRect(0, getMeasuredHeight() - mSpaceBottom, getMeasuredWidth(), getMeasuredHeight(), mPaint);
        }
    }
}

用这个SpaceDividerView我们重写一下上面的布局代码:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 xmlns:app="http://schemas.android.com/apk/res-auto">

    <LinearLayout
 android:id="@+id/group_container"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_alignParentTop="true"
 android:layout_marginTop="48dp"
 android:orientation="vertical">

        <RelativeLayout
 android:id="@+id/account_container"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:background="@drawable/list_item_bg"
 android:clickable="true">

            <TextView
 android:id="@+id/account_title"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_alignParentLeft="true"
 android:layout_centerVertical="true"
 android:layout_margin="16dp"
 android:text="First Item"
 android:textColor="#f00"
 android:textSize="16sp" />
        </RelativeLayout>

        <com.jayfeng.lesscode.core.other.SpaceDividerView
 android:layout_width="match_parent"
 android:layout_height="1px"
 android:background="#f00"
 app:spaceLeft="16dp"
 app:spaceColor="@color/list_item_normal"/>

        <RelativeLayout
 android:id="@+id/phone_container"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:background="@drawable/list_item_bg"
 android:clickable="true">

            <TextView
 android:id="@+id/phone_title"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_alignParentLeft="true"
 android:layout_centerVertical="true"
 android:layout_margin="16dp"
 android:text="Second Item"
 android:textColor="#f00"
 android:textSize="16sp" />

        </RelativeLayout>
    </LinearLayout>

</RelativeLayout>

效果图和Overdraw状态如下:

澳门新葡萄京官网注册 8

界面中group_container那块由之前的绿色变成了蓝色,说明减少了一次Overdraw。

上述情况下,SpaceDividerView解耦了背景色,优化了Overdraw,而且这个SpaceDividerView也是支持4个方向的,使用起来特别方便。

每当我们走捷径的时候,都有可能会产生副作用。而这里的副作用是可能影响性能。
当在布局中添加了一个divider的时候,我们增加了view的个数。我们都知道view的数目越少会得到越好的性能。有时候增加一个view来实现divider还会增加布局的层级。比如上面的例子中,我们不仅仅增加了一个view,还增加了一个包含它们的
linear layout。为了一个divider而创建了额外的布局。
不要用view做分割线 —会带来副作用
因为divider是view的一部分,所以在item
动画期间,divider也会一起跟着动画。如下图:

StaggeredGridLayoutManager

actviity中修改下布局管理器,大家应该感觉很熟悉了吧~~~

recyclerView.setLayoutManager(new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL));

瀑布流列表一般列的高度是不一致的,为了模拟不同的宽高,数据源我把String类型改成了对象.然后初始化的时候随机了一个高度.

public class ItemData {
    private String content;//item内容
    private int height;//item高度

    public ItemData() {
    }

    public ItemData(String content, int height) {
        this.content = content;
        this.height = height;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }
}

瀑布流列表没有添加分割线,给item布局设置了android:padding属性。recycler_staggered_item.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:padding="5dp"
    android:layout_width="wrap_content"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textview"
        android:background="@color/colorAccent"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="122"
        android:textSize="20sp"/>
</FrameLayout>

最后我们在适配器的onBindViewHolder方法中给itemd中的TextView设置一个高度

@Override
public void onBindViewHolder(StaggeredGridAdapter.MyViewHolder holder, int position) {
    ItemData itemData=datas.get(position);
    holder.textview.setText(itemData.getContent());
    //手动更改高度,不同位置的高度有所不同
    holder.textview.setHeight(itemData.getHeight());
}

是不是感觉so easy,赶紧运行看看效果:

澳门新葡萄京官网注册 9

瀑布流效果

阴影divider

阴影分割线的特点是重叠在下面的view之上的,它的目的是一种分割线的立体效果。

澳门新葡萄京官网注册 10

使用RelativeLayout并控制上边距离可以实现:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent">

    <!-- layout_marginTop的值应该就是不包括阴影高度的header高度-->
    <LinearLayout
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:layout_alignParentTop="true"
 android:layout_marginTop="@dimen/header_height"
 android:orientation="vertical">
    </LinearLayout>

    <!-- 这个要放在最后,才能显示在最上层,这个header里面包括一个阴影View-->
    <include
 android:id="@+id/header"
 layout="@layout/include_header" />
</RelativeLayout>

虽然再简单不过了,还是稍微分析一下,header包括内容48dp和阴影8dp,那么marginTop就是48dp了。

澳门新葡萄京官网注册 11

添加header跟footer

RecyclerView添加头部跟底部是没有对应的api的,但是我们很多的需求都会用到,于是只能自己想办法实现了。我们可以通过适配器的getItemViewType方法来实现这个功能。

修改后的适配器代码:RecyclerHeadFootViewAdapter.java

public class RecyclerHeadFootViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
    private List<String> datas;
    private LayoutInflater inflater;

    public static final int TYPE_HEADER=1;//header类型
    public static final int TYPE_FOOTER=2;//footer类型
    private View header=null;//头View
    private View footer=null;//脚View

    public  RecyclerHeadFootViewAdapter(Context context, List<String> datas){
        inflater=LayoutInflater.from(context);
        this.datas=datas;
    }

    //创建每一行的View 用RecyclerView.ViewHolder包装
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if(viewType==TYPE_HEADER){
            return new RecyclerView.ViewHolder(header){};
        }else if(viewType==TYPE_FOOTER){
            return new RecyclerView.ViewHolder(footer){};
        }
        View itemView=inflater.inflate(R.layout.recycler_item,null);
        return new MyViewHolder(itemView);
    }

    //给每一行View填充数据
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position){
        if(getItemViewType(position)==TYPE_HEADER||getItemViewType(position)==TYPE_FOOTER){
            return;
        }
        MyViewHolder myholder= (MyViewHolder) holder;
        myholder.textview.setText(datas.get(getRealPosition(position)));
    }

    //如果有头部 position的位置是从1开始的  所以需要-1
    public int getRealPosition(int position){
        return header==null?position:position-1;
    }

    //数据源的数量
    @Override
    public int getItemCount() {
        if(header == null && footer == null){//没有head跟foot
            return datas.size();
        }else if(header == null && footer != null){//head为空&&foot不为空
            return datas.size() + 1;
        }else if (header != null && footer == null){//head不为空&&foot为空
            return datas.size() + 1;
        }else {
            return datas.size() + 2;//head不为空&&foot不为空
        }
    }

    @Override
    public int getItemViewType(int position){
        //如果头布局不为空&&位置是第一个那就是head类型
        if(header!=null&&position==0){
            return TYPE_HEADER;
        }else if(footer!=null&&position==getItemCount()-1){//如果footer不为空&&最后一个
            return TYPE_FOOTER;
        }
        return super.getItemViewType(position);
    }

    public void setHeader(View header) {
        this.header = header;

        notifyItemInserted(0);//在位置0插入一条数据,然后刷新
    }

    public void setFooter(View footer) {
        this.footer = footer;
        notifyItemInserted(datas.size()-1);//在尾部插入一条数据,然后刷新
    }

    class MyViewHolder extends RecyclerView.ViewHolder{
        private TextView textview;

        public MyViewHolder(View itemView) {
            super(itemView);
            textview= (TextView) itemView.findViewById(R.id.textview);
        }
    }
}
  • getItemCount

    有header跟footer的时候需要在源数据长度基础上进行增加。

  • getItemViewType

    通过getItemViewType判断不同的类型

  • onCreateViewHolder

    通过不同的类型创建item的View

  • onBindViewHolder

如果是header跟footer类型是不需要绑定数据的,header跟footer的View一般在actvity中创建,不需要这边做处理,所以这两种类型我们就不往下执行,如果有头布局,position==0的位置被header占用了,但是我们的数据源也就是集合的下标是从0开始的,所以这里需要-1。

  • setHeader

    设置头布局,在第一行插入一条数据,然后刷新。注意这个方法调用后会有插入的动画,这个动画可以使用默认的,也可以自己定义

  • setFooter

    设置尾部布局,在尾部插入一条数据,然后刷新。

添加header跟footer的方法终于封装好了,在activity中只需要两行代码就能添加header,跟ListView调用addHeader方法一样简单,又可以happy的玩耍了。这里需要注意的是我们初始化View的时候,inflate方法需要三个参数。

  • resource 资源id
  • root 父View
  • attachToRoot true:返回父View false:返回资源id生成的View

//添加header
View header=LayoutInflater.from(this).inflate(R.layout.recycler_header,recyclerView,false);
adapter.setHeader(header);

//添加footer
View footer=LayoutInflater.from(this).inflate(R.layout.recycler_footer,recyclerView,false);
adapter.setFooter(footer);

recycler_header跟recycler_footer布局文件我就不贴出来了,就一个TextView,我们直接看效果图:

澳门新葡萄京官网注册 12

RecyclerView添加头跟尾

1498117406107936.gif

item点击事件&&增加或删除带动画效果

当我们调用RecyclerView的setOnItemClickListener方法的时候,发现居然没有,用了RecyclerView你要习惯什么东西都自己封装。。。

首先我们从adapter开刀,内部写一个接口,一个实例变量,提供一个公共方法,设置监听。

private RecyclerViewItemClick recyclerViewItemClick;

public void setRecyclerViewItemClick(RecyclerViewItemClick recyclerViewItemClick) {
    this.recyclerViewItemClick = recyclerViewItemClick;
}

public interface RecyclerViewItemClick{
    /**
     * item点击
     * @param realPosition 数据源position
     * @param position view position
     */
    void onItemClick(int realPosition,int position);
}

在onBindViewHolder方法中给item监听点击事件

if(recyclerViewItemClick!=null) {
    myholder.itemView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            recyclerViewItemClick.onItemClick(getRealPosition(position),position);
        }
    });
}

在activity的onCreate方法中进行监听,顺便设置item增加删除动画。我用的是sdk自带的默认动画。

adapter.setRecyclerViewItemClick(recyclerViewItemClick);
recyclerView.setItemAnimator(new DefaultItemAnimator());

private RecyclerHeadFootViewAdapter.RecyclerViewItemClick recyclerViewItemClick=new RecyclerHeadFootViewAdapter.RecyclerViewItemClick() {
    @Override
    public void onItemClick(int realPosition, int position) {
        Log.i("ansen","删除数据:"+realPosition+" view位置:"+position);
        Log.i("ansen","当前位置:"+position+" 更新item数量:"+(adapter.getItemCount()-position-1));

        datas.remove(realPosition);//删除数据源
        adapter.notifyItemRemoved(position);//item移除动画
        //更新position至adapter.getItemCount()-1的数据
        adapter.notifyItemRangeChanged(position,adapter.getItemCount()-position-1);
    }
};

显然divider不应该随着item一起做动画。而是和item分开,像这样才是对的:

源码下载

RecyclerView

澳门新葡萄京官网注册 13

1498120170236717.gif

不要用view做分割线— 缺乏灵活性
如果divider是(item的)view的一部分,那么你就无法控制它。你唯一能控制的就是根据item的position改变divider的可见状态。
而item decoration就灵活多了。

澳门新葡萄京官网注册 14

1498120202925785.png

In the above image for the last item in the group divider fills the
entire width. Other dividers have a margin of 56dp to their left side.
Here is the ItemDecorator’s onDraw code.
在上图中,group最后一个item的divider充满了整个宽度。其它的divider都有一个56dp的左边距。这是这个ItemDecorator的onDraw代码:

@Override
public void onDraw(Canvas canvas, RecyclerView parent,
RecyclerView.State state) {
canvas.save();
final int leftWithMargin = convertDpToPixel(56);
final int right = parent.getWidth();

final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
int adapterPosition = parent.getChildAdapterPosition(child);
left = (adapterPosition == lastPosition) ? 0 : leftWithMargin;
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = mBounds.bottom +
Math.round(ViewCompat.getTranslationY(child));
final int top = bottom – mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}

不要用view做分割线—使用 ItemDecoration
写一个自己的ItemDecoration其实非常简单。你只需要创建一个继承了ItemDecoration的类就可以了。重写
getItemOffsets() 和 onDraw() 方法。具体实现可以参考
这个
示例。
而 25.0.0版本的支持库中,我们有一个新的类
“DividerItemDecoration”。这个类直接实现了divider。
DividerItemDecoration decoration = new
DividerItemDecoration(getApplicationContext(), VERTICAL);
recyclerView.addItemDecoration(decoration);

提示
一个RecyclerView可以添加多个ItemDecoration。发挥头脑风暴的时候到了。

所有decoration都在item绘制之前绘制。如果你想让decoration在view之后绘制,重写onDrawOver()
而不是onDraw() 。

所以下次想为RecyclerView添加分割线的时候,别使用在item布局添加view这种方式了,使用ItemDecoration。

发表评论

电子邮件地址不会被公开。 必填项已用*标注