RecyclerView核心要点

RecyclerView核心要点

RecyclerView是什么?

A flexible view for providing
a limited window into
a large data set

image-20221219135334818

ListView的局限

  • 只有纵向列表一种布局
  • 没有支持动画的 API
  • 接口设计和系统不一致
    • setOnItemClickListener()
    • setOnItemLongClickListener()
    • setSelection()
  • 没有强制实现 ViewHodler
  • 性能不如 RecyclerView

RecyclerView的优势

  • 默认支持LinearGridStaggered Grid三种布局
  • 友好的ItemAnimator动画API
  • 强制实现ViewHolder
  • 解耦的架构设计
  • 相比ListView更好的性能

LayoutManager支持的布局

image-20221219135803610

RecyclerView的重要组件

image-20221219135856099

RecyclerView Demo

ViewHolder究竟是什么?

  • View holder 和 item view 是什么关系?一对一?一对多?多对多?

  • View holder 解决的是什么问题?

  • View hodler 的 ListView item view 的复用有什么关系

没有实现view hodlergetView()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SimpleListViewAdapter extends BaseAdapter{
@Override
public View getView(int position, View convertView, ViewGroup parent){
if(convertView == null){
convertView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_view, parent, false);
}
ImageView avatar = convertView.findViewById(R.id.user_avatar);
TextView name = convertView.findViewById(R.id.user_name);
TextView title = convertView.findViewById(R.id.user_title);

User user = getItem(position);
Glide.with(parent.getContext()).load(user.avatarUrl()).into(avatar);
name.setText(user.name);
title.setText(user.title);
return convertView;
}
}

实现了view holdergetView()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SimpleListViewAdapter extends BaseAdapter{
@Override
public View getView(int position, View convertView, ViewGroup parent){
UserViewHolder holder;
if(convertView == null){
convertView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_view, parent, false);
holder = new UserViewHolder(convertView);
holder.avatar = convertView.findViewById(R.id.user_avatar);
holder.name = convertView.findViewById(R.id.user_name);
holder.title = convertView.findViewById(R.id.user_title);
convertView.setTag(holder);
} else {
holder = (UserViewHolder)convertView.getTag();
}

User user = getItem(position);
Glide.with(parent.getContext()).load(user.avatarUrl()).into(holder.avatar);
holder.name.setText(user.name);
holder.title.setText(user.title);
return convertView;
}
}

Item viewview holder一一对应

image-20221219160157885

View holder最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static class UserViewHolder extends RecyclerView.ViewHolder{
ImageView avatar;
TextView name;
UserVIewHolder(@Nonull View itemView){
super(itemView);
avatar = itemView.findViewById(R.id.avatar);
name = itemView.findViewById(R.id.name);
}

void bindTo(User user){
//bind data to UI
}
}

//void onBindViewHolder(UserViewHolder holder, int postion){
// holder.bindTo(userList.get(position));
//}

RecyclerView缓存机制

ListView缓存图示一

image-20221219164349788

ListView缓存图示二

image-20221219164435276

RecyclerView缓存图示一

image-20221219164639192

RecyclerView缓存图示二

image-20221219164726509

ViewCacheExtension Example

  • 广告卡片
    • 每一页一共有4个广告
    • 这些广告短期内不会发生变化
  • 每次滑入一个广告卡片,一般情况下都需要重新绑定
  • Cache 只关心 position,不关心 view type
  • RecycledViewPool 只关心 view type,都需要重新绑定
  • 在 ViewCacheExtension 里保持4个广告Card缓存

注意:列表中 item/广告的 impression 统计

  • ListView 通过 getView() 统计
  • RecyclerView 通过 onBindViewHolder() 统计?可能错误!
  • 通过 onViewAttachedToWindow() 统计

RecyclerView性能优化策略

在onBindViewHolder里设置点击监听?

onBindViewHodler里设置点击监听器会导致重复创建对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SimpleAdapter extends RecyclerView.Adapter{
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType){

}
@Override
public void onBindViewHolder(ViewHolder holder, int position){
holder.itemView.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
//...
}
});
}
}

改成在onCreateViewHolder里设置点击监听!

View-ViewHolder-View.OnClickListener 三者一一对应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SimpleAdapter extends RecyclerView.Adapter{
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType){
final SimpleViewHolder holder = new SimpleViewHolder();
holder.itemView.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
//...
}
});
}
@Override
public void onBindViewHolder(ViewHolder holder, int position){

}
}

使用LinearLayoutManager.setInitialPrefetchItemCount()

image-20221219171601392 image-20221219171628111

  • 用户滑动到横向滑动的 item RecyclerView 的时候,由于需要创建更复杂的 RecyclerView 以及多个子 view,可能会导致页面卡顿
  • 由于 RenderThread 的存在,RecyclerView 会进行 prefetch
  • LinearLayoutManager.setInitialPrefetchItemCount(横向列表初次显示时可见的item个数)
    • 只有 LinearLayoutManager 有这个API
    • 只有嵌套在内部的 RecyclerView 才会生效

RecyclerView.setHasFixedSize()

1
2
3
4
5
6
7
8
//伪代码
void onContentsChanged(){
if (mHasFixedSize){
layoutChildren();
} else {
requestLayout();
}
}

如果 Adapter 的数据变化不会导致 RecyclerView 的大小变化 –》 RecyclerView.setHasFixedSize(true)

多个 RecyclerView 共用 RecycledViewPool

image-20221219172830681

共用 RecycledViewPool 代码

1
2
3
4
RecyclerView.RecycledViewPool recycledViewPool = new RecyclerView.RecycledViewPool();
recyclerView1.setRecyclerViewPool(recycledViewPool);
recyclerView2.setRecyclerViewPool(recycledViewPool);
recyclerView3.setRecyclerViewPool(recycledViewPool);

DiffUtil

  • DiffUtil is a utility class that can calculate the difference between two lists and output a list of update operations that converts the first list into the second one.

  • 局部更新方法 notifyItemXXX() 不适用于所有情况

  • notifyDataSetChange() 会导致整个布局重绘,重新绑定所有 ViewHolder,而且会失去可能的动画效果

  • DiffUtil 适用于整个页面需要刷新,但是有部分数据可能相同的情况

1
2
3
4
5
6
7
8
public abstract static class Callback{
public abstract int getOldListSize();
public abstract int getNewListSize();
public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition); public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
public Object getChangePayload(int oldItemPosition, int newItemPosition){
return null;
}
}

DiffUtil.Callback逻辑

image-20221220093215675

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
public class UserDiffCallback extends DiffUtil.Callback{
private List<User> oldList;
private List<User> newList;
public UserDiffCallback(List<User> oldList, List<User> newList){
this.oldList = oldList;
this.newList = newList;
}
@Override
public int getOldListSize(){
return oldList.size();
}
@Override
public int getNewListSize(){
return newList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){
return oldList.get(oldItemPosition).id == newList.get(newItemPosition).id;
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){
User oldUser = oldList.get(oldItemPosition);
User newUser = newList.get(newItemPosition);
return oldUser.id == newUser.id && oldUser.name.equals(newUser.name)
&& oldUser.profession.equals(newUser.profession);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class UserDiffCallback extends DiffUtil.Callback{
private List<User> oldList;
private List<User> newList;
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition){
User oldUser = oldList.get(oldItemPosition);
User newUser = newList.get(newItemPosition);
Bundle payload = new Bundle();
if(oldUser.id != newUser.id){
payload.putLong(User.KEY_ID, newUser.id);
}
if(!oldUser.name.equals(newUser.name)){
payload.putString(User.KEY_NAME, newUser.name);
}
if(!oldUser.profession.equals(newUser.profession)){
payload.putString(User.KEY_PROF, newUser.profession);
}
if(payload.size() == 0) return null;
return payload;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ShowcaseRVAdapter extends RecyclerView.Adapter<ShowcaseRvAdapter.UserViewHolder>{
private List<User> userList;
public ShowcaseRVAdapter(){
userList = new ArrayList<>(UserRepo.USER_LIST);
}

public void swapData(List<User> newList, boolean diff){
if(diff){
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new UserDiffCallback(userList, newList), false);
userList = newList;
diffResult.dispatchUpdatesTo(this);
} else {
userList = newList;
notifyDataSetChanged();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ShowcaseRVAdapter extends RecyclerView.Adapter<ShowcaseRvAdapter.UserViewHolder>{
@Override
public void onBindViewHolder(@NonNull ShowcaseRVAdapter.UserViewHolder holder, int position){
User user = userList.get(position);
holder.name.setText(user.name);
}
@Override
public void onBindVIewHolder(@NonNull UserViewHolder holder, int position, @NonNull List<Object> payloads){
if(payloads.isEmpty()){
onBindViewHolder(holder, position);
} else {
Bundle payload = (Bundle) payloads.get(0);
for(String key: payload.keySet()){
switch(key){
case User.KEY_NAME:
holder.name.setText(payload.getString(key));
break;
}
}
}
}
}

在列表很大的时候异步计算diff

  • 使用 Thread/Handler 将 DiffResult 发送到主线程
  • 使用 RxJava 将 calculateDiff 操作放到后台线程
  • 使用 Google 提供的 AsyncListDiffer(Executor)/ListAdapter

AsyncListDiffer / ListAdapter 代码示例

为什么ItemDecoration可以绘制分割线?

image-20221220103857479

image-20221220103913261

Overlay Divider 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OverlayDivider extends RecyclerView.ItemDecoration{
@Overrid
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state){
if (mOrientation == VERTICAL_LSIT) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}

protected void drawVertical(Canvas c, RecyclerView parent){}
protected void drawHorizontal(Canvas c, RecyclerView parent){}
//不需要覆写 getItemOffsets()
}

image-20221220104340595

ItemDecoration 还可以做什么?

  • Drawing dividers between items

  • Highlights

    image-20221220104447264

  • Visual grouping boundaries

    image-20221220104510540

RecyclerView 更多知识