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效果。
结束语:本文仅用来学习记录,参考查阅。