RecyclerView的布局流程设计
RecyclerView的ViewHolder复用流程
从一个神奇的bug开始的RecyclerView复用分析
notifyDataSetChanged()到底做了什么能让UI刷新呢?
notifyDataSetChanged是Adapter的方法,使用了观察者模式,RecyclerViewDataObserver是一个观察者,它是RecyclerView的内部类,实质上,RecyclerView是Adapter的观察者。notifyDataSetChanged就是通知观察者也就是RecyclerView数据有变化了,RecyclerView做了两件事,让所有的ViewHolder无效和调用requestLayout。
Recycler是RecyclerView的final内部类,是专门管理ViewHolder重用的。
我们都知道Adapter的onBindViewHolder方法,是给ViewHolder中的itemView设置数据的,那么,有没有想过为啥在这里设置数据就能最终显示出来呢?是谁调用了这个方法呢?方法中的ViewHolder实例是怎么来的呢?
UI最终能展示出来,肯定是和得和RecyclerView的onMeasure、onLayout和onDraw流程有关系才能显示出来,先说结论:onBindViewHolder是在onMeasure或onLayout中最终调用的。
RecyclerView的测量和布局步骤,都是委托给LayoutManager做的,LayoutManager去获取View,不是直接从Adapter中获取,而且通过Recycler,这样的话Recycler的重用机制就能发挥作用。Recycler简单来说,就是如果有能重用的ViewHolder,就用这个ViewHolder(这里需要注意的是,ViewHolder中itemView上面的数据都还在),在这个时机会调用Adapter的onBindViewHolder方法刷新itemView。
需要说明的是ViewHolder回收到pool中是根据ViewType来回收的,一个ViewType最多回收5个ViewHolder。
RecyclerView怎么实现聊天界面
我们知道正常情况下,RV都是从上往下布局的。但是,在聊天界面,消息从新到旧,是需要从下往上布局的。要实现,也比较简单,可以调用LinearLayoutManager的setReverseLayout方法,将布局反转成从下到上的布局。
可是,有个新问题,对于聊天消息不满一页的情况,消息虽然也是从下往上显示的,这个没问题,但是整体展示在屏幕的下半部分,这个显然是不符合预期的。
为了解决这个问题,可以调用LinearLayoutManager的setStackFromEnd(true)方法,让RV item是从RV的底部向顶部显示。这里需要说的是,这样的话,调用Adapter的onBindViewHolder方法position就是从大到小回调的了,不是从0到大回调的了。
但是,这样又会导致一个新问题,就是RV默认展示到了底部,这样的话,对于有多页消息的情况,首先看到的不是第1页最新消息。
为了解决这个问题,需要让RV滚动到0位置。
public interface OnScrollCallback {
void onScroll(int position);
}
static class MAdapter extends RecyclerView.Adapter<MAdapter.MViewHolder> {
OnScrollCallback callback;
public void setOnScrollCallback(OnScrollCallback callback1) {
callback = callback1;
}
@NonNull
@Override
public MViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.layout_item, viewGroup, false);
return new MViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull MViewHolder mViewHolder, int i) {
Log.e("lzy", "onBindViewHolder " + i);
callback.onScroll(i);
int value = i + 1;
mViewHolder.TV.setText(value + "");
}
@Override
public int getItemCount() {
return 100;
}
static class MViewHolder extends RecyclerView.ViewHolder {
TextView TV;
public MViewHolder(@NonNull View itemView) {
super(itemView);
TV = itemView.findViewById(R.id.tv);
}
}
}
HashMap<Integer, Boolean> mHashMap = new HashMap<>();
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
binding.srf.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
Log.e("lzy", "onRefresh");
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
binding.srf.setRefreshing(false);
}
}, 2000);
}
});
RecyclerView rv = binding.rv;
linearLayoutManager = new CustomLinearLayoutManager(getActivity());
linearLayoutManager.setReverseLayout(true);
linearLayoutManager.setStackFromEnd(true);
rv.setLayoutManager(linearLayoutManager);
rv.scrollToPosition(0);
MAdapter mAdapter = new MAdapter();
rv.setAdapter(mAdapter);
mAdapter.setOnScrollCallback(new OnScrollCallback() {
@Override
public void onScroll(int position) {
int pos = position;
Log.e("lzy", "onScrolled " + pos);
Log.e("lzy", "(pos + 1) % 20 " + (pos + 1) % 20);
if ((pos + 1) % 20 == 0 && mHashMap.get(pos) == null) {
Log.e("lzy", "onScrolled false");
mHashMap.put(pos, true);
linearLayoutManager.setCanScroll(false);
binding.rv.stopScroll();
binding.srf.setRefreshing(true);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Log.e("lzy", "onScrolled true");
linearLayoutManager.setCanScroll(true);
binding.srf.setRefreshing(false);
}
}, 5000);
}
}
});
}