Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

Android控件--RecyclerView

$
0
0

1、简介

谷歌在Android5.0之后推出了RecyclerView,它是ListView还有GridView的升级的一个View。它之所以叫RecyclerView,是与它的设计思想有关。

RecyclerView与ListView和GridView的设计思想不同,我们看看:

  • 不关心Item如何显示,是否显示在正确的位置。
  • 不关心Item间如何分隔(例如ListView可以去设置它的divider,或者dividerHeight,设置它的分隔线高度和样式)
  • 不关心Item增加与删除的动画效果
  • 只关注如何回收与复用View

既然RecycleView不关心它的Item在内部的显示,那么我们如何做才能控制RecyclerView显示的风格呢?

Android给我们提供了一个类,叫做LayoutManager,我们可以通过LayoutManager来控制RecyclerView的显示。比如说我想要一个ListView的显示风格我们就可以去调用mRecyclerView.stateLayoutManager()传入一个LinearLayoutManager,如果是想要GridView的显示风格,就传入一个GridLayoutManager。

我们可以通过传入不同的LayoutManager的实例,让RecyclerView去显示不同的风格。

解决了显示的问题,我们看RecyclerView又是如何实现分隔的。与显示同样,我们也需要引入一个类,就是ItemDecoration。如果我们需要去定义Item间分隔的效果,那么可以通过去实现ItemDecoration的子类,用它提供的onDraw()或者onDrawOver()方法去绘制Item间的分隔的情况。如果需要的分隔仅仅是背景色的间距,那么可以去设置Margin和Padding去实现分隔。

至于动画效果我们可以通过ItemAnimator来实现,我们可以通过不同的ItemAnimator的子类去去实现不同的动画效果。

综上所述,RecyclerView其实是一个插件式的架构,它可以通过上述的类组合来实现我们的需求。

RecyclerView涉及到的类有:

  • Adapter
  • ViewHolder
  • LayoutManager
  • ItemDecoration
  • ItemAnimator

关于ViewHolder相信大家都已经很熟悉了,我们经常把它用于ListView和GridView的Adapter中去。在RecyclerView中强制我们使用ViewHolder的开发模式,它没有使用传统的BaseAdapter,而是自己提供了一个RecyclerView到Adapter的类。

2、用途

RecyclerView的用途要比ListView和GridView都广泛:

  • Just like ListView
  • Just like GridView
  • 横向ListView
  • 横向GridView
  • 瀑布流
  • 定制Item增加与删除动画

可以看到,RecyclerView在可以实现ListView和GridView的风格之外,还有各种的功能。可以看到前面五种都是对风格的定义,所以都和LayoutManager相关,而最后一项关于动画的设置则是ItemAnimator。我们的例子也都是围绕这些功能的实现展开的。

3、实现ListView

首先要在项目中使用RecyclerView,就要导入它的包,在Android Studio中我们只要去添加它的依赖即可。

当然版本要对应,这是需要注意的。我们在xml中使用RecyclerView控件需要导入它的完整包名,这个如果不知道我们可以通过Ctrl + n快捷键可以打开一个类,输入RecyclerView就可以进入这个类的代码里,把包名复制下来即可。

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

我们先来实现RecyclerView的adapter,我们前面说过RecyclerView的adapter已经不用继承BaseAdapter了,它实现了自己的Adapter。

public class SimpleAdapter extends RecyclerView.Adapter<SimpleAdapter.MyViewHolder> {

    private Context context;
    private LayoutInflater mInflater;
    private List<String> mDatas;

    public SimpleAdapter(Context context, List<String> datas) {
        this.context = context;
        this.mDatas = datas;
        mInflater = LayoutInflater.from(context);
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        View view = mInflater.inflate(R.layout.item, parent, false);
        MyViewHolder viewHolder = new MyViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {

        holder.tv.setText(mDatas.get(position));
    }

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

    class MyViewHolder extends ViewHolder {

        TextView tv;

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

            tv = (TextView) itemView.findViewById(R.id.id_tv);
        }
    }
}

我们可以看到RecyclerView的adapter的方法里有 onCreateViewHolder() 和 onBindViewHolder() 两个对ViewHolder的方法,在ListView或GridView中我们都是在getView()中自己定义ViewHolder,而RecyclerView则强制我们必须使用它。我们都知道Adapter是对item数据的加载,item的布局如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="72dp"
    android:background="#44f1de14">

    <TextView
        android:id="@+id/id_tv"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

我这里仅是为了介绍RecyclerView的用法,所以这个数据源就用字符串来代替。

public class MainActivity extends AppCompatActivity {

    private RecyclerView mRecyclerView;
    private SimpleAdapter mAdapter;
    private List<String> mDatas;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initData();
        mRecyclerView = (RecyclerView) findViewById(R.id.id_recyclerview);
        mAdapter = new SimpleAdapter(this, mDatas);
        mRecyclerView.setAdapter(mAdapter);

        //设置RecyclerView的布局管理
        LinearLayoutManager mLayoutManager = new LinearLayoutManager(this,
                LinearLayoutManager.VERTICAL, false);
        mRecyclerView.setLayoutManager(mLayoutManager);
    }

    private void initData() {
        mDatas = new ArrayList<String>();
        for (int i = 'A'; i <= 'Z'; i++) {
            mDatas.add((char)i + "");
        }
    }
}

这些都是初始化设置的,我们来看下LinearLayoutManager的构造方法,第一个参数是上下文,第二个就是设置ListView是垂直的还是水平的,最后一个参数表示的是是否逆向布局(意思是将数据反向显示,原先从左向右,从上至下。设为true之后全部逆转)。

我们可以看到是实现了我们的ListView,要想实现横向ListView只要设置为Horizontal即可。虽然很简单,但是是因为我们只在布局中写了一个TextView而已,如果加上了其它的控件那我们的RecyclerView就会变得更绚丽丰富。

4、实现分隔线

正如我们前面介绍RecyclerView的一样,RecyclerView是不关注分隔线,所以如果我们想要为RecyclerView添加分隔线,就要自己设置。ItemDecoration是一个抽象类,但系统并没有提供一个可以使用的类,所以这个我们需要自己实现。

这里我们用GitHub上的别人写好的ItemDecoration,这个是代码的链接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 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) {
        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);
            RecyclerView v = new 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);
        }
    }
}

我们来讲讲这个类的代码原理,首先由三个常量,都是系统常量的标志,listDivider是我们系统主题的一个分隔线,并且支持横向和纵向。

mDivider是我们item间隔的图片资源。mOrientation是确定RecyclerView的显示方向。

构造方法就是对各个属性的获取,我们学过自定义属性的朋友应该知道TypeArray的使用含义,不了解的可以看看我的博客Android–自定义控件解析(一),里面有对属性的获取相关的知识介绍。

setOrientation()方法自不必说,相信大家都能看懂。

在onDraw()里是根据我们的方向来绘制不同的显示。如果是垂直显示,使用drawVertical()方法。因为是垂直,所以左右两边的位置是不变的,我们可以先确定这两个的值,然后我们根据RecyclerView中item的数量一个个的绘制Divider。top,bottom的值的获取大家都能理解,getIntrinsicHeight()方法就是获取固有的高度,不过这个方法返回的是dp的值,而getBottom()和getPadding这样的方法返回的值是以像素为单位的,所以我们应该为其做单位转换。如果有人看过我的博客 Android自定义视频播放器就知道我写过一个PixelUtil的像素转换工具类,我们可以使用一下。

public class PixelUtil {

    private static Context mContext;

    public static void initContext(Context context) {
        mContext = context;
    }

    public static int dp2px(float value) {

        final float scale = mContext.getResources().getDisplayMetrics().densityDpi;

        return (int) (value * (scale / 160) + 0.5f);
    }
}

在MainActivity的onCreate()里调用一下做个初始化就可以使用了。

在DividerItemDecoration中用到getIntrinsicHeight()方法的地方修改一下:

PixelUtil.dp2px(mDivider.getIntrinsicHeight())

drawHorizontal()方法的逻辑和垂直时是差不多的,大家在看代码的时候就应该能想象的到绘制的区域是在RecyclerView的哪里。

最后我们还需要注意getItemOffset()方法,虽然谷歌标记它已经过时了,不过对于我们初学者来说用法是差不多的。它就是用来提供Item的offset,然后用来绘制divider,因为默认下RecyclerView不知道要去绘制我们的item间距,所以我们要通过这个方法去预留一定的空间。这个区域是由我们的outRect提供,我们用set()方法,如果是垂直就设置bottom,水平设置right,这样就没问题啦。当然也要进行单位转换。

我们有了ItemDecoration的类,就可以为RecyclerView添加分隔线了:

mRecyclerView.addItemDecoration(new DividerItemDecoration(this,
         DividerItemDecoration.VERTICAL_LIST));

addItemDecoration()从add上可以知道RecyclerView可以设置多个ItemDecoration,所以在这个方法可以多次使用,传入不同的Divider样式。

我们可以看到在每个Item之间已经有了一条分隔线,但这个分隔线是调用了系统有的样式,颜色等等的属性都不是我们设置的,所以如果我们想要一个颜色渐变的Divider就应该去自定义Divider。

首先当然是要绘制我们的Drawable了,相信学过Android动画的朋友对这个设置渐变都是小菜一碟的,不了解的也没事,可以看看我的博客Android–Drawable标签介绍,就直接上代码了:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <size android:height="4dp"/>

    <gradient
        android:startColor="#ffff0000"
        android:centerColor="#ff00ff00"
        android:endColor="#ff0000ff"
        android:type="linear"/>
</shape>

前面说过我们是从主题里获取那个样式,所以我们也要把这个Drawable设置到主题中去,才能够使用它。

在values下的styles.xml文件中style标签里添加个item即可:

<item name="android:listDivider">@drawable/divider</item>

5、实现GridView

在我们实现GridView之前,我们先来为我们的app添加菜单栏,然后在溢出列表里实现切换布局。

第一步如果使用的是Android Studio,那IDE生成的项目已经默认不加Menu了,所以我们要自己在res下创建menu文件夹,然后在创建main.xml:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context="com.ht.recycleview.MainActivity">

    <item
        android:id="@+id/action_add"
        android:orderInCategory="100"
        app:showAsAction="always"
        android:icon="@mipmap/ic_menu_add"
        android:title="Add"/>

    <item
        android:id="@+id/action_delete"
        android:icon="@mipmap/ic_menu_delete"
        android:orderInCategory="100"
        app:showAsAction="always"
        android:title="Delete"/>

    <item
        android:id="@+id/action_listView"
        android:orderInCategory="100"
        app:showAsAction="never"
        android:title="ListView"/>

    <item
        android:id="@+id/action_gridView"
        android:orderInCategory="100"
        app:showAsAction="never"
        android:title="GridView"/>

    <item
        android:id="@+id/action_hor_girdView"
        android:orderInCategory="100"
        app:showAsAction="never"
        android:title="HorizontalGridView"/>

    <item
        android:id="@+id/action_staggered"
        android:orderInCategory="100"
        app:showAsAction="never"
        android:title="StaggerdGridView"/>
</menu>

这里要注意的就是showAsAction这个属性,它有五个值:

  • ifRoom:会显示在Item中,但是如果已经有4个或者4个以上的Item时会隐藏在溢出列表中。当然个数并不仅仅局限于4个,依据屏幕的宽窄而定。
  • never:永远不会显示。只会在溢出列表中显示,而且只显示标题,所以在定义item的时候,最好把标题都带上。
  • always:无论是否溢出,总会显示。
  • withText:withText值示意ActionBar要显示文本标题。ActionBar会尽可能的显示这个标题,但是,如果图标有效并且受到ActionBar空间的限制,文本标题有可能显示不全。
  • collapseActionView:声明了这个操作视窗应该被折叠到一个按钮中,当用户选择这个按钮时,这个操作视窗展开。否则,这个操作视窗在默认的情况下是可见的,并且即便在用于不适用的时候,也要占据操作栏的有效空间。一般要配合ifRoom一起使用才会有效果。

因为在Android Studio生成的Activity里已经不会自动为我们添加onCreateOptionMenu(),onOptionsItemSeclected()两个方法了,所以我们还要重写它。

@Override
public boolean onCreateOptionsMenu(Menu menu) {

    getMenuInflater().inflate(R.menu.main, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {

    int id = item.getItemId();

    switch (id) {
        case R.id.action_listView:

            mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
            break;

        case R.id.action_gridView:

            mRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));
            break;

        case R.id.action_hor_girdView:

            mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager
                                           (5, StaggeredGridLayoutManager.HORIZONTAL));
            break;

        case R.id.action_staggered:
            break;
    }
    return super.onOptionsItemSelected(item);
}

修改成GridView只需要调用setLayoutManager()方法即可,Android已经给我们设置好了。我们也来设置下水平的GridView,这就要使用StaggeredGridLayoutManager类,其实我们要实现瀑布流也是使用这个类,与这里不同的就是瀑布流的Item的宽高是不一样的。后面再介绍。

这里还要注意我们设置成水平,就要为它的高度修改一下,像之前垂直时我们设置了它的宽度一样。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:background="#4444ddff"
    android:layout_margin="3dp"
    android:layout_width="match_parent"
    android:layout_height="72dp" >

    <TextView
        android:gravity="center"
        android:layout_width="72dp"
        android:layout_height="match_parent"
        android:id="@+id/id_tv"/>
</FrameLayout>

如果RecyclerView导入的版本比较低我们这样设置就好了,会自动帮我们适配,像我就把版本设为了22.2.1,如果版本比较高就没有这个功能了,我们就要自己为高度设置值了。

因为要演示多种布局,所以先前的DividerDecoration就并不是很适用了,比如我们分隔线的宽等于屏宽减去padding,那在这个例子的水平GridView中因为一行有三个Item所以分隔线会绘制三次,所以我就把分隔线去掉了,这里就用margin来分隔Item,在实际项目中,大家可以根据自己的需求自定义想要的ItemDecoration。

6、实现瀑布流

要实现瀑布流在前面已经说过关键是设置Item的高度,所以我们先要将所有Item的高度确定好,这个设置Item就要在我们的Adapter做啦。

public class SimpleAdapter extends RecyclerView.Adapter<SimpleAdapter.MyViewHolder> {

    private LayoutInflater mInflater;
    private List<String> mDatas;

    private List<Integer> mHeights;

    public SimpleAdapter(Context context, List<String> datas) {
        this.mDatas = datas;
        mInflater = LayoutInflater.from(context);

        mHeights = new ArrayList<Integer>();
        for (int i = 0; i < mDatas.size(); i++) {
            mHeights.add((int) (100 + Math.random() * 300)); //100~400px
        }
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        View view = mInflater.inflate(R.layout.item, parent, false);
        MyViewHolder viewHolder = new MyViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {

        ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
        lp.height = mHeights.get(position);
        holder.itemView.setLayoutParams(lp);
        holder.tv.setText(mDatas.get(position));
    }

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

    class MyViewHolder extends ViewHolder {

        TextView tv;

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

            tv = (TextView) itemView.findViewById(R.id.id_tv);
        }
    }
}

LayoutParams度量单位是px,这点我们要注意啦,itemView就是我们在创建ViewHolder的时候为MyViewHolder初始化传入的View。

7、动画添加

动画的添加在前面也说过,只要使用setAnimator()方法即可设置动画,我们先来使用系统的默认动画DefaultItemAnimator看看添加和删除的效果。

mRecyclerView.setItemAnimator(new DefaultItemAnimator());
case R.id.action_add:
    mAdapter.addItem(1);
    break;

case R.id.action_delete:
    mAdapter.removeItem(1);
    break;

为我们的RecyclerView添加动画,然后在Menu中设置点击后的逻辑。

public void addItem(int pos) {
    mDatas.add(pos, "insert one");
    notifyItemInserted(pos);
}

public void removeItem(int pos) {
    mDatas.add("insert one");
    notifyItemRemoved(pos);
}

在adapter中添加这两个方法,注意这里不能使用notifyDataSetChange()方法。如果动画在进行,用户要删除这个Item,就会导致bug,因为没有进行判断,所以需要在点击之前判断,这里我没有做,大家可以将RecyclerView传入Adapter,然后通过position获得RecyclerView的item,再判断是否在播放动画即可。

系统就给我们提供了这么一种ItemAnimator,不过在GitHub上是有许多受欢迎的自定义类,我这里提供一个链接让大家学习自定义ItemAnimator

8、点击事件

RecyclerView并没有为它的Item添加click和longClick的对外回调事件,但是这是很常用的功能,所以这也是必须学习的地方,我们在Adapter中将其实现。

大家都知道我们常用的OnClickListener就是一个回调的接口,所以我们也来定义这样的接口。

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

private OnItemClickListener mListener;

public void setOnItemClickListener(OnItemClickListener listener) {
    this.mListener = listener;
}
class MyViewHolder extends ViewHolder {

    TextView tv;

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

        tv = (TextView) itemView.findViewById(R.id.id_tv);

        if (mListener != null) {

            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {

                    mListener.onItemClick(v, getAdapterPosition());
                }
            });

            itemView.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {

                    mListener.onItemLongClick(v, getAdapterPosition());
                    return true;
                }
            });
        }
    }
}

这里有几点需要注意:

  • 官方不推荐在onBindViewHolder中添加点击事件。
  • 添加的position是在绑定中用到的,如果对item进行删除的话,position就不对了,应该用getAdapterPotision。

longclick返回true就不会触发onClick()方法了,然后就可以在我们的Activity中为Adapter设置点击事件:

mAdapter.setOnItemClickListener(new SimpleAdapter.OnItemClickListener() {
    @Override
    public void onItemClick(View view, int position) {
        Toast.makeText(MainActivity.this, "click " + position, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onItemLongClick(View View, int position) {
        Toast.makeText(MainActivity.this, "long click " + position, Toast.LENGTH_SHORT).show();
    }
});

RecyclerView的基础学习结束就到此为止了,这里借鉴了鸿洋大神的代码例子,大家可以看看他的博客学习更深入的内容Android 自定义RecyclerView 实现真正的Gallery效果

结束语:本文仅用来学习记录,参考查阅。

作者:HardWorkingAnt 发表于2017/6/3 0:18:31 原文链接
阅读:35 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>