Android RecyclerView Demo

想法

RecyclerView 出来已经有好长时间了, 在自己的项目中却一直没有使用 … 这几天查阅了相关资料, 一步步地 check 着这个熟悉而又陌生的家伙, 自此爱不释手。

官方是这样介绍的:

RecyclerView is a more advanced and flexible version of ListView. This widget is a container for large sets of views that can be recycled and scrolled very efficiently. Use the RecyclerView widget when you have lists with elements that change dynamically.

写了一个 Demo, 包涵了如下功能, 下文也将依次以此展开 (适合新手, 大神绕道):

  1. RecyclerView 控件的基本用法 ;
  2. item 的 Click, LongClick 事件处理 ;
  3. 同一样式的 Item 不同布局的展示 ;
  4. Item 控件的状态变化处理, 如阅读痕迹等 …

写着写着, 大致有了如此模样:

**Demo 代码放在了这里:** https://github.com/absentm/Demo **Apk 下载地址:** https://github.com/absentm/Demo/blob/master/apk/RecyclerViewDemo.apk

基本使用

添加包引用

在 Gradle 文件中添加 recyclerview 的包引用, cardview 也顺便加上吧:

1
2
compile 'com.android.support:recyclerview-v7:24.+'
compile 'com.android.support:cardview-v7:24.+'

XML 中定义

这个很基本了, 长这样就 ok :

1
2
3
4
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent" />

Activity 中初始化

在 Activity 中需要查找 id , 初始化布局管理器(LayoutManager), 设置Adapter等, 大致如下:

1
2
3
4
5
6
7
8
mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview);
mMainPagesAdapter = new MainPagesAdapter(MainActivity.this, mDatas);
mLayoutManager = new LinearLayoutManager(MainActivity.this, LinearLayoutManager.VERTICAL, false);

mRecyclerView.setAdapter(mMainPagesAdapter);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setItemAnimator(new DefaultItemAnimator());//默认动画
mRecyclerView.setHasFixedSize(true);//效率最高

Adapter 构造适配器

到这里, 当和 ListView 做对比时, 可以发现 RecyclerView 似乎是在强制开发者使用 ListView 的 ViewHolder 模式, Adapter 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import android.content.Context;
import android.graphics.Color;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.dm.recyclerviewdemo.R;
import com.dm.recyclerviewdemo.beans.NewsBean;

import java.util.List;

/**
* MainPagesAdapter
* Created by dm on 16-11-17.
*/

public class MainPagesAdapter
extends RecyclerView.Adapter<MainPagesAdapter.ItemContentHolder>
implements View.OnClickListener, View.OnLongClickListener {

private Context context;
private List<NewsBean> mDatas;

public MainPagesAdapter(Context context, List<NewsBean> datas) {
this.context = context;
mDatas = datas;
}

@Override
public ItemContentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.main_news_item, parent, false);

MainPagesAdapter.ItemContentHolder viewHolder = new MainPagesAdapter.ItemContentHolder(view);
return viewHolder;
}

@Override
public void onBindViewHolder(ItemContentHolder holder, int position) {
// 使用Glide图片缓存框架加载图
Glide.with(context)
.load(mDatas.get(position).getPicUrl())
.error(R.drawable.icon)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.crossFade()
.centerCrop()
.into(holder.newsItemImv);
holder.newsItemTitleTv.setText(mDatas.get(position).getTitle());
holder.newsItemTimeTv.setText(mDatas.get(position).getCtime());

// 将数据保存在itemView的Tag中,以便点击时进行获取
holder.itemView.setTag(mDatas.get(position));
}

@Override
public int getItemCount() {
return mDatas == null ? 0 : mDatas.size();
}

public static class ItemContentHolder extends RecyclerView.ViewHolder {
ImageView newsItemImv;
TextView newsItemTitleTv;
TextView newsItemTimeTv;

public ItemContentHolder(View itemView) {
super(itemView);
newsItemImv = (ImageView) itemView.findViewById(R.id.main_item_image_imv);
newsItemTitleTv = (TextView) itemView.findViewById(R.id.main_item_title_tv);
newsItemTimeTv = (TextView) itemView.findViewById(R.id.main_item_time_tv);
}
}
}

数据源 mDatas

完成了以上操作, 还需要加载我们的数据。 Demo 的数据来自 ApiStore 的娱乐新闻 , 请求网络后返回 Json 格式数据。所以, 需要进行Json数据解析。

本文使用 Thread + Handler + Gson 技术进行网络数据加载和解析, 该技术的使用方法可参看我的这些文章:

Java中Gson的使用
Java中使用Gson解析json数据
Android 使用Thread+Handler实现非UI线程更新UI界面

数据加载和填充的核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 发送网络请求, 获取数据
isConnect = SystemUtils.checkNetworkConnection(MainActivity.this);
if (isConnect) {
new Thread(new Runnable() {
@Override
public void run() {
String newsDatas = SystemUtils.getNewsJsonStr();
Log.i(TAG, "newsDatas >>> " + newsDatas);
if (!newsDatas.equals("")) {
Message message = mHandler.obtainMessage();
message.obj = newsDatas;
mHandler.sendMessage(message);
} else {
mProgressBar.setVisibility(View.GONE);
Snackbar.make(mCoordinatorLayout, "数据加载出错 !!!",
Snackbar.LENGTH_LONG).setAction("Action", null).show();
}
}
}).start();
} else {
Snackbar.make(mCoordinatorLayout, "No NetWork !!!",
Snackbar.LENGTH_LONG).setAction("Action", null).show();
mProgressBar.setVisibility(View.GONE);
}

// 解析数据, 填充到控件上
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
String newsJsonStr = msg.obj.toString();
Log.d("FindVideoAty", " Json: " + newsJsonStr);

Gson gson = new Gson();
NewsInfosBean newsInfosBean = gson.fromJson(newsJsonStr, NewsInfosBean.class);
if (newsInfosBean.getCode() == 200) {
mDatas = newsInfosBean.getNewslist();

MainPagesAdapter.layoutFlag = 0;
mMainPagesAdapter = new MainPagesAdapter(MainActivity.this, mDatas);
mLayoutManager = new LinearLayoutManager(MainActivity.this,
LinearLayoutManager.VERTICAL, false);
mRecyclerView.setAdapter(mMainPagesAdapter);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setItemAnimator(new DefaultItemAnimator());//默认动画
mRecyclerView.setHasFixedSize(true);//效率最高

mMainPagesAdapter.setOnItemClickListener(MainActivity.this);
mMainPagesAdapter.setOnItemLongClickListener(MainActivity.this);
} else {
Snackbar.make(mCoordinatorLayout, "数据加载出错 !!!",
Snackbar.LENGTH_LONG).setAction("Action", null).show();
}

mProgressBar.setVisibility(View.GONE);
}
};

Click 事件监听

以上基本上是 RecyclerView 的用法过程了, 但是 Google 却没有没给我们提供相应 Item 的点击事件处理, 我们需要自己改写自己的Adapter实现:

修改 Adapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 使 Adapter implements View.OnClickListener, View.OnLongClickListener事件监听
public class MainPagesAdapter
extends RecyclerView.Adapter<MainPagesAdapter.ItemContentHolder>
implements View.OnClickListener, View.OnLongClickListener

// 设置item点击事件的回调函数
private OnRecyclerViewItemClickListener mOnItemClickListener = null;
// 设置item点击事件的回调函数
private OnRecyclerViewItemLongClickListener mOnItemLongClickListener = null;

// define interface
public interface OnRecyclerViewItemClickListener {
void onItemClick(View view, NewsBean data);
}

// define interface
public interface OnRecyclerViewItemLongClickListener {
void onItemLongClick(View view, NewsBean data);
}

// 提供外部 Activity 的实现方法
public void setOnItemClickListener(MainPagesAdapter.OnRecyclerViewItemClickListener listener) {
this.mOnItemClickListener = listener;
}

public void setOnItemLongClickListener(MainPagesAdapter.OnRecyclerViewItemLongClickListener listener) {
this.mOnItemLongClickListener = listener;
}

// 在 Adapter 中的 onCreateViewHolder 方法中创建的View注册点击事件
view.setOnClickListener(this);
view.setOnLongClickListener(this);

// 在 Adapter 中重写 View 的 onClick 和 onLongClick 方法
@Override
public void onClick(View view) {
if (mOnItemClickListener != null) {
//注意这里使用getTag方法获取数据
mOnItemClickListener.onItemClick(view, (NewsBean) view.getTag());
}
}

@Override
public boolean onLongClick(View view) {
if (mOnItemLongClickListener != null) {
mOnItemLongClickListener.onItemLongClick(view, (NewsBean) view.getTag());
}

return false;
}

注释较详细, 详见上文 Demo 的 Github 地址。


在 Activity 中处理 Item 点击事件的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使 Activity 实现上述 Adapter 中定义的 Click 事件接口
public class MainActivity extends BaseActivity implements
MainPagesAdapter.OnRecyclerViewItemClickListener,
MainPagesAdapter.OnRecyclerViewItemLongClickListener

// 重写 Adapter 的 onItemClick, onItemLongClick 方法

@Override
public void onItemClick(View view, NewsBean data) {
// 可以跳转到 Item 的详情页面
Toast.make(MainActivity.this, "data: "
+ data.getTitle, Toast.LENGTH_LONG).show();
}

@Override
public void onItemLongClick(View view, final NewsBean data) {
// 可以设置状态标记
Toast.make(MainActivity.this, "data: "
+ data.getCtime, Toast.LENGTH_LONG).show();
}

这样点击事件的处理逻辑就 OK 了!


Item 状态变换处理

有时候, 我们还有这样的需求:

在列表中点击一个 Item 之后, 被点击的 Item 控件的字体颜色, 背景颜色或者图片是否显示等发生改变, 表现出和其他 Item 不一样的呈现状态。(如新闻点击阅读后, 当前新闻 Item 颜色变浅等)

我们的处理思路是: 在数据 Bean 中添加标记状态, 当 Item 的点击事件发生时改变当前数据 Bean 的标记状态, 之后使用 notifyDataSetChanged() 通知 Adapter 数据改变, 更新 UI 界面。因此, 需要如下代码处理。

数据 Bean 中添加标记属性

添加一个 isSelected 属性, 用于数据标记。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import java.io.Serializable;

public class NewsBean implements Serializable {
private String ctime; // 新闻发布时间
private String title; // 新闻标题
private String description; // 新闻描述
private String picUrl; // 新闻配图
private String url; // 新闻链接地址

private boolean isSelected = false; // 是否已读

public NewsBean() {
}

public String getCtime() {
return ctime;
}

public void setCtime(String ctime) {
this.ctime = ctime;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

public String getPicUrl() {
return picUrl;
}

public void setPicUrl(String picUrl) {
this.picUrl = picUrl;
}

public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

public boolean isSelected() {
return isSelected;
}

public void setSelected(boolean selected) {
isSelected = selected;
}
}

Adapter 中根据 Bean 的标记状态设置不同显示效果

在 Adapter 中的 onBindViewHolder()方法中设置 Item 的不同加载状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
public void onBindViewHolder(ItemContentHolder holder, int position) {
// 使用Glide图片缓存框架加载图
Glide.with(context)
.load(mDatas.get(position).getPicUrl())
.error(R.drawable.icon)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.crossFade()
.centerCrop()
.into(holder.newsItemImv);
holder.newsItemTitleTv.setText(mDatas.get(position).getTitle());
holder.newsItemTimeTv.setText(mDatas.get(position).getCtime());

int textColor = Color.parseColor("#CC000000");
int textColorChange = Color.parseColor("#40000000");
if (mDatas.get(position).isSelected()) { // 判断是否标记
holder.newsItemTitleTv.setTextColor(textColorChange);
holder.newsItemTimeTv.setTextColor(textColorChange);
} else {
holder.newsItemTitleTv.setTextColor(textColor);
holder.newsItemTimeTv.setTextColor(textColor);
}

// 将数据保存在itemView的Tag中,以便点击时进行获取
holder.itemView.setTag(mDatas.get(position));
}
这样根据标记, 数据更新时加载数据, 显示不同的状态。

Activtiy 的 Click() 方法中更改数据 Bean 标记状态

这样在相应的 Activity 中, 当点击事件发生时, 修改相关 Item 数据并通知 Adapter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Override
public void onItemClick(View view, NewsBean data) {
// 标记已读,设置标记位
data.setSelected(true);
mMainPagesAdapter.notifyDataSetChanged();

// 跳转至详情页
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putSerializable("newsInfos", data);
intent.setClass(MainActivity.this, ItemDetailAty.class);
intent.putExtras(bundle);
startActivity(intent);
}

@Override
public void onItemLongClick(View view, final NewsBean data) {
new MaterialDialog.Builder(MainActivity.this)
.title("标记")
.items(R.array.tag_values)
.itemsCallback(new MaterialDialog.ListCallback() {
@Override
public void onSelection(MaterialDialog dialog,
View itemView,
int position,
CharSequence text) {

String listItemStr = (String) text;
Log.d(TAG, "listItemStr >>> " + listItemStr);
switch (listItemStr) {
case "已读":
data.setSelected(true);
mMainPagesAdapter.notifyDataSetChanged();
break;
case "未读":
data.setSelected(false);
mMainPagesAdapter.notifyDataSetChanged();
break;
}

}
}).show();
}

这样效果就出来了, 如展示图3所示。


样式切换

RecycleView 的布局管理器为我们提供了不同的布局样式, 如默认的线性列表布局, 网格布局, 瀑布流式布局等。Demo 中也提供了相关不同布局的切换, 如上图4, 5, 6所示。

思路是这样的: 在Adapter设置一个布局加载标志, 根据传回来的不同布局切换标志, 在 Adapter的 onCreateViewHolder() 方法中 inflate不同的布局, 具体做法如下:

Adapter 中添加加载布局标志

1
public static int layoutFlag = 0;

0: 默认卡片布局;
1: 无间隔卡片布局;
2: 瀑布流式布局;
3: 网格布局


改写 Adapter 中 onCreateViewHolder()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public ItemContentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
switch (layoutFlag) {
case 0:
view = LayoutInflater.from(context).inflate(R.layout.main_news_item, parent, false);
break;
case 1:
view = LayoutInflater.from(context).inflate(R.layout.main_news_item_nogap, parent, false);
break;
case 2:
case 3:
view = LayoutInflater.from(context).inflate(R.layout.main_news_item_flu, parent, false);
break;
default:
view = LayoutInflater.from(context).inflate(R.layout.main_news_item, parent, false);
break;
}

MainPagesAdapter.ItemContentHolder viewHolder = new MainPagesAdapter.ItemContentHolder(view);
//将创建的View注册点击事件
view.setOnClickListener(this);
view.setOnLongClickListener(this);
return viewHolder;
}

Activity 中 Menu 菜单触发布局切换事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// Menu 菜单
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.dm.recyclerviewdemo.activitys.MainActivity">

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

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

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

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

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

// Activity Menu 事件处理
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_cardview:
mLayoutManager = new LinearLayoutManager(MainActivity.this,
LinearLayoutManager.VERTICAL, false);
initRecycleView(0, mDatas, mLayoutManager);
break;
case R.id.action_no_gapview:
mLayoutManager = new LinearLayoutManager(MainActivity.this,
LinearLayoutManager.VERTICAL, false);
initRecycleView(1, mDatas, mLayoutManager);
break;
case R.id.action_fluview:
mLayoutManager = new StaggeredGridLayoutManager(2,
StaggeredGridLayoutManager.VERTICAL);
initRecycleView(2, mDatas, mLayoutManager);
break;
case R.id.action_gridview:
mLayoutManager = new GridLayoutManager(MainActivity.this, 3);
initRecycleView(3, mDatas, mLayoutManager);
break;
case R.id.action_about:
startActivity(new Intent(MainActivity.this, AboutAty.class));
break;
}

return super.onOptionsItemSelected(item);
}

private void initRecycleView(int flag,
List<NewsBean> datas,
RecyclerView.LayoutManager layoutManager) {
MainPagesAdapter.layoutFlag = flag;

mMainPagesAdapter = new MainPagesAdapter(MainActivity.this, datas);
mRecyclerView.setAdapter(mMainPagesAdapter);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setItemAnimator(new DefaultItemAnimator());//默认动画
mRecyclerView.setHasFixedSize(true);//效率最高

mMainPagesAdapter.setOnItemClickListener(MainActivity.this);
mMainPagesAdapter.setOnItemLongClickListener(MainActivity.this);
}

这样实现了基本效果, 但仍存在一个问题: Item 的标记状态存在内存中,随着 Activity 的消亡而消失; 可以考虑将数据 Bean 的标记状态存放在外部(数据库, SP, 网络等)


用到的开源库

特别感谢这些作者提供的开源库, 丰富了 Demo 功能。

compile ‘com.daimajia.numberprogressbar:library:1.2@aar’
compile ‘com.github.bumptech.glide:glide:3.7.0’
compile ‘com.google.code.gson:gson:2.8.0’
compile ‘com.afollestad.material-dialogs:commons:0.9.0.1’
compile ‘com.jaeger.statusbaruitl:library:1.3.0’
compile ‘de.hdodenhof:circleimageview:2.1.0’


参考资料

[1] http://blog.csdn.net/lmj623565791/article/details/45059587
[2] http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/1118/2004.html
[3] http://www.loongwind.com/archives/189.html
[4] http://frank-zhu.github.io/android/2015/01/16/android-recyclerview-part-1/
[5] https://github.com/Tikitoo/blog/issues/29


0%