RecyclerView的应用
RecyclerView是Android 5.0以后提出的新UI控件,可以用来代替传统的ListView。但是RecyclerView并不会完全替代ListView,因为两者的使用场景不一样。但是RecyclerView的出现会让很多开源项目被废弃,例如横向滚动的ListView, 横向滚动的GridView, 瀑布流控件,因为RecyclerView能够实现所有这些功能,这是由于RecyclerView对各个功能进行解耦,从而相对于ListView有更好的拓展性。本篇文章着重讲述RecyclerView的使用方式方式上,以及和ListView的对比。
使用RecyclerView
具体使用是需要在代码中看对应的功能实现就好了。对于AndroidX项目来说,直接使用即可,无需引入依赖。
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
tools:context=".MainActivity">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:text="添加数据"
android:onClick="onClickAddData"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"/>
<Button
android:text="横向排列"
android:onClick="onClickHorizontal"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_marginStart="3dp"
android:layout_height="wrap_content"/>
<Button
android:text="反向展示"
android:onClick="onClickReverse"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_marginStart="3dp"
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/btn_linear_layout"
android:text="线性布局"
android:onClick="onChangeLayout"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/btn_grid_layout"
android:text="网格布局"
android:onClick="onChangeLayout"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_marginStart="3dp"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/btn_staggered_grid_layout"
android:text="瀑布流布局"
android:onClick="onChangeLayout"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_marginStart="3dp"
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="插入一条数据"
android:onClick="onInsertDataClick"/>
<Button
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="删除一条数据"
android:layout_marginStart="3dp"
android:onClick="onRemoveDataClick"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:background="#CDDC39"
android:layout_margin="4dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:padding="5dp"
android:id="@+id/iv"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="fitXY"/>
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textColor="@color/black"
android:gravity="center_vertical"
android:layout_marginStart="8dp"/>
</LinearLayout>
MyRecycleViewAdapter.java
/**
* 1、继承RecycleView.Adapter
* 2、绑定ViewHolder
* 3、实现Adapter的相关方法
*/
public class MyRecycleViewAdapter extends RecyclerView.Adapter<MyRecycleViewAdapter.MyViewHolder> {
private final Context context;
private final RecyclerView recyclerView;
private List<String> dataSource;
private OnItemClickListener listener;
public MyRecycleViewAdapter(Context context, RecyclerView recyclerView){
this.context = context;
this.recyclerView = recyclerView;
this.dataSource = new ArrayList<>();
}
public void setDataSource(List<String> dataSource) {
this.dataSource = dataSource;
notifyDataSetChanged();
}
public void setListener(OnItemClickListener listener) {
this.listener = listener;
}
// 创建并返回ViewHolder
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new MyViewHolder(LayoutInflater.from(context).inflate(R.layout.item, parent, false));
}
// 通过ViewHolder绑定数据
@Override
public void onBindViewHolder(@NonNull MyRecycleViewAdapter.MyViewHolder holder, int position) {
holder.imageView.setImageResource(getIcon(position));
holder.textView.setText(dataSource.get(position));
LinearLayout.LayoutParams params;
if(StaggeredGridLayoutManager.class.equals(recyclerView.getLayoutManager().getClass())){
int randomHeight = getRandomHeight();
// 只在瀑布流布局中使用随机高度
params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
randomHeight < 50 ? dp2px(context, 50f): randomHeight
);
}else{
params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
params.gravity = Gravity.CENTER;
holder.textView.setLayoutParams(params);
holder.itemView.setOnClickListener(v -> listener.onItemClick(position));
}
private int dp2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
// 返回数据数量
@Override
public int getItemCount() {
return dataSource.size();
}
// 返回不同的随机ItemView高度
private int getRandomHeight(){
return (int)(Math.random() * 500);
}
// 根据不同的position选择一个图片
private int getIcon(int position){
switch (position % 5){
case 0:
return R.drawable.ic_4k;
case 1:
return R.drawable.ic_5g;
case 2:
return R.drawable.ic_360;
case 3:
return R.drawable.ic_adb;
case 4:
return R.drawable.ic_alarm;
default:
return 0;
}
}
// 添加一条数据
public void addData (int position) {
dataSource.add(position, "插入的数据");
notifyItemInserted(position);
// 刷新ItemView
notifyItemRangeChanged(position, dataSource.size() - position);
}
// 删除一条数据
public void removeData (int position) {
dataSource.remove(position);
notifyItemRemoved(position);
// 刷新ItemView
notifyItemRangeChanged(position, dataSource.size() - position);
}
static class MyViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;
TextView textView;
public MyViewHolder(@NonNull View itemView) {
super(itemView);
imageView = itemView.findViewById(R.id.iv);
textView = itemView.findViewById(R.id.tv);
}
}
interface OnItemClickListener {
void onItemClick(int position);
}
}
MainActivity.java
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private MyRecycleViewAdapter adapter;
private LinearLayoutManager linearLayoutManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.recycler_view);
// 设置线性布局
linearLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(linearLayoutManager);
adapter = new MyRecycleViewAdapter(this, recyclerView);
adapter.setListener(position -> Toast.makeText(MainActivity.this, "第" + position + "数据被点击", Toast.LENGTH_SHORT).show());
recyclerView.setAdapter(adapter);
}
public void onClickAddData(View view) {
List<String> data = new ArrayList<>();
for (int i = 0; i < 30; i++) {
data.add("第" + i + "条数据");
}
adapter.setDataSource(data);
}
public void onClickHorizontal(View view) {
linearLayoutManager.setReverseLayout(false);
// 横向排列ItemView
linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(linearLayoutManager);
}
public void onClickReverse(View view) {
// 数据反向展示
linearLayoutManager.setReverseLayout(true);
// 数据纵向排列
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(linearLayoutManager);
}
public void onChangeLayout(View view) {
switch (view.getId()){
case R.id.btn_linear_layout:
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(linearLayoutManager);
break;
case R.id.btn_grid_layout:
GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 2);
recyclerView.setLayoutManager(gridLayoutManager);
break;
case R.id.btn_staggered_grid_layout:
StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(staggeredGridLayoutManager);
break;
}
}
// 插入一条数据
public void onInsertDataClick (View v) {
adapter.addData(1);
}
// 删除一条数据
public void onRemoveDataClick (View v) {
adapter.removeData(1);
}
}
布局类 | 效果 |
---|---|
LinearLayoutManager | 以垂直或水平滚动列表方式显示项目 |
GridLayoutManager | 在网格中显示项目 |
StaggeredGridLayoutManager | 在分散对齐网格中显示项目 |
上述代码解析
RecyclerView使用步骤
1、创建Adapter:创建一个继承RecyclerView.Adapter<VH>
的Adapter类(VH是ViewHolder的类名),记为MyRecycleViewAdapter。
2、创建ViewHolder:在MyRecycleViewAdapter中创建一个继承RecyclerView.ViewHolder的静态内部类,记为VH。ViewHolder的实现和ListView的ViewHolder实现几乎一样。
3、在MyRecycleViewAdapter中实现三个方法:
// 映射ItemLayoutId,创建VH并返回
onCreateViewHolder(ViewGroup parent, int viewType)
// 为Holder设置指定数据
onBindViewHolder(VH holder, int position)
// 返回Item的个数
getItemCount()
RecyclerView局部刷新
ListView通过adapter.notifyDataSetChanged()实现ListView的更新,这种更新方法的缺点是全局更新,即对每个Item View都进行重绘。但事实上很多时候,我们只是更新了其中一个Item的数据,其他Item其实可以不需要重绘。所以在上面的代码中:adapter.addData(1)
与adapter.removeData(1)
都是使用的局部刷新:
// 添加一条数据
public void addData (int position) {
dataSource.add(position, "插入的数据");
notifyItemInserted(position);
// 刷新ItemView
notifyItemRangeChanged(position, dataSource.size() - position);
}
// 删除一条数据
public void removeData (int position) {
dataSource.remove(position);
notifyItemRemoved(position);
// 刷新ItemView
notifyItemRangeChanged(position, dataSource.size() - position);
}
如果是ListView要完成局部刷新就稍微复杂一点:
public void updateItemView(ListView listview, int position, Data data){
int firstPos = listview.getFirstVisiblePosition();
int lastPos = listview.getLastVisiblePosition();
// 可见才更新,不可见则在getView()时更新
if(position >= firstPos && position <= lastPos){
//listview.getChildAt(i)获得的是当前可见的第i个item的view
View view = listview.getChildAt(position - firstPos);
VH vh = (VH)view.getTag();
vh.text.setText(data.text);
}
}
Item的点击/长按事件
interface OnItemClickListener {
void onItemClick(int position);
}
private OnItemClickListener listener;
public void setListener(OnItemClickListener listener) {
this.listener = listener;
}
......
public void onBindViewHolder(@NonNull MyRecycleViewAdapter.MyViewHolder holder, int position) {
......
holder.itemView.setOnClickListener(v -> listener.onItemClick(position));
holder.itemView.setOnLongClickListener(v -> {
listener.onItemLongClick(position);
return false;
});
}
其他说明
1、dp单位转px单位:
private int dp2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
2、ImageView的scaleType的属性
android:scaleType="center"
: 保持原图的大小,显示在ImageView的中心。当原图的size大于ImageView的size时,多出来的部分被截掉。
android:scaleType="center_inside"
: 以原图正常显示为目的,如果原图大小大于ImageView的size,就按照比例缩小原图的宽高,居中显示在ImageView中。如果原图size小于ImageView的size,则不做处理居中显示图片。
android:scaleType="center_crop"
: 以原图填满ImageView为目的,如果原图size大于ImageView的size,则与center_inside一样,按比例缩小,居中显示在ImageView上。如果原图size小于ImageView的size,则按比例拉升原图的宽和高,填充ImageView居中显示。
android:scaleType="matrix"
: 不改变原图的大小,从ImageView的左上角开始绘制,超出部分做剪切处理。
androd:scaleType="fit_xy"
: 把图片按照指定的大小在ImageView中显示,拉伸显示图片,不保持原比例,填满ImageView。
android:scaleType="fit_start"
: 把原图按照比例放大缩小到ImageView的高度,显示在ImageView的start(前部/上部)。
android:sacleType="fit_center"
: 把原图按照比例放大缩小到ImageView的高度,显示在ImageView的center(中部/居中显示)。
android:scaleType="fit_end"
: 把原图按照比例放大缩小到ImageView的高度,显示在ImageVIew的end(后部/尾部/底部)。
ListView和RecyclerView对比
ListView的一些优点: 1、可以通过addHeaderView(), addFooterView()添加头视图和尾视图。
2、可以通过"android:divider"
设置自定义分割线。
3、通过setOnItemClickListener()和setOnItemLongClickListener()可以很方便的设置点击事件和长按事件。
这些功能在RecyclerView中都没有直接的接口,虽然实现起来很简单但还是要自己实现,所以ListView用来实现简单的显示功能更简单。
RecyclerView的优点: 1、默认已经实现了View的复用,回收机制更加完善。
2、默认支持局部刷新。
3、容易实现添加item、删除item的动画效果。
4、容易实现拖拽、侧滑删除等功能。
5、RecyclerView是一个插件式的实现,对各个功能进行解耦,从而扩展性比较好。
回收机制分析
ListView回收机制
ListView为了保证Item View的复用,实现了一套回收机制,该回收机制的实现类是RecycleBin,他实现了两级缓存:
View[] mActiveViews: 缓存屏幕上的View,在该缓存里的View不需要调用getView()。
ArrayList
void layoutChildren(){
// 1. 如果数据被改变了,则将所有ItemView回收至scrapView
// 而RecyclerView会根据情况放入Scrap Heap或RecyclePool,否则回收至mActiveViews
if(dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
}else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
// 2. 填充
switch(){
case LAYOUT_XXX:
fillXxx();
break;
case LAYOUT_XXX:
fillXxx();
break;
}
// 3. 回收多余的activeView
mRecycler.scrapActiveViews();
}
其中fillXxx()实现了对Item View进行填充,该方法内部调用了makeAndAddView(),实现如下:
View makeAndAddView(){
if(!mDataChanged) {
child = mRecycler.getActiveView(position);
if (child != null) {
return child;
}
}
child = obtainView(position, mIsScrap);
return child;
}
其中,getActiveView()是从mActiveViews中获取合适的View,如果获取到了,则直接返回,而不调用obtainView(),这也印证了如果从mActiveViews获取到了可复用的View,则不需要调用getView()。
obtainView()是从mScrapViews中获取合适的View,然后以参数形式传给了getView(),实现如下:
View obtainView(int position){
final View scrapView = mRecycler.getScrapView(position); // 从RecycleBin中获取复用的View
final View child = mAdapter.getView(position, scrapView, this);
}
接下去我们介绍getScrapView(position)的实现,该方法通过position得到Item Type,然后根据Item Type从mScrapViews获取可复用的View,如果获取不到,则返回null,具体实现如下:
class RecycleBin{
private View[] mActiveViews; // 存储屏幕上的View
private ArrayList<View>[] mScrapViews; // 每个item type对应一个ArrayList
private int mViewTypeCount; // item type的个数
private ArrayList<View> mCurrentScrap; // mScrapViews[0]
View getScrapView(int position) {
final int whichScrap = mAdapter.getItemViewType(position);
if(whichScrap < 0) {
return null;
}
if(mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
}else if (whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}
private View retrieveFromScrap(ArrayList<View> scrapViews, int position){
int size = scrapViews.size();
if(size > 0){
return scrapView.remove(scrapViews.size() - 1); // 从回收列表中取出最后一个元素复用
}else{
return null;
}
}
}
RecyclerView回收机制
RecyclerView和ListView的回收机制非常相似,但是ListView是以View作为单位进行回收,RecyclerView是以ViewHolder作为单位进行回收。Recycler是RecyclerView回收机制的实现类,他实现了四级缓存:
mAttachedScrap: 缓存在屏幕上的ViewHolder。 mCachedViews: 缓存屏幕外的ViewHolder,默认为2个。ListView对于屏幕外的缓存都会调用getView()。 mViewCacheExtensions: 需要用户定制,默认不实现。 mRecyclerPool: 缓存池,多个RecyclerView共用。
主要需要关注的是 getViewForPosition()方法,因此此处介绍该方法的实现:
View getViewForPosition(int position, boolean dryRun){
if(holder == null){
// 从mAttachedScrap,mCachedViews获取ViewHolder
holder = getScrapViewForPosition(position,INVALID,dryRun); // 此处获得的View不需要bind
}
final int type = mAdapter.getItemViewType(offsetPosition);
if (mAdapter.hasStableIds()) { // 默认为false
holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
}
if(holder == null && mViewCacheExtension != null){
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
if(view != null){
holder = getChildViewHolder(view);
}
}
if(holder == null){
holder = getRecycledViewPool().getRecycledView(type);
}
if(holder == null){ // 没有缓存,则创建
holder = mAdapter.createViewHolder(RecyclerView.this, type); // 调用onCreateViewHolder()
}
if(!holder.isBound() || holder.needsUpdate() || holder.isInvalid()){
mAdapter.bindViewHolder(holder, offsetPosition);
}
return holder.itemView;
}
从上述实现可以看出,依次从mAttachedScrap, mCachedViews, mViewCacheExtension, mRecyclerPool寻找可复用的ViewHolder,如果是从mAttachedScrap或mCachedViews中获取的ViewHolder,则不会调用onBindViewHolder(),mAttachedScrap和mCachedViews也就是我们所说的Scrap Heap;而如果从mViewCacheExtension或mRecyclerPool中获取的ViewHolder,则会调用onBindViewHolder()。
RecyclerView局部刷新的实现原理也是基于RecyclerView的回收机制,即能直接复用的ViewHolder就不调用onBindViewHolder()。
参考资料
1、强大而灵活的RecyclerView Adapter: https://github.com/CymChad/BaseRecyclerViewAdapterHelper