recyclerView的优势就不多说了,这里总结一下在项目实际使用中发现recyclerView的一些比较好用的地方:
1. gridLayoutManager可以动态设置列数 比如a类一行2个,b类一行3个
2.通过给LayoutManager设置reverse(反向), 轻松实现下拉加载(查看历史消息)
3.通过notifyItemChanged(int,obj)局部刷新
-
gridLayoutManager可以动态设置列数
这个我之前的文章提到过:当时需求按照产品经理的说法是图片显示成grid(一行2个);字幕文本显示成list(一个占一整行);
当时是有点懵逼的,recyclerView貌似不支持两个layoutManager吧。
然后就是考虑把一行2个当成一个item,图片和字幕当成2个type来区分,然后用list来装;不过这样比较麻烦就是要考虑一些边界情况。而且如果再加几种type,一行3个,一行4个,那这样搞岂不是GG - -;
然后查了下,发现可以通过改变spanSize来根据viewType来改变spanCount实现grid不同TYPE显示不同个数的效果
//设置grid的宽度为2
GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), 2);
//gridLayoutManager这里设置的spanCount=2
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
if (manager instanceof GridLayoutManager) {
final GridLayoutManager gridManager = ((GridLayoutManager) manager);
gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
int type = getItemViewType(position);
switch (type) {
case TYPE_IMAGE:
//占用1/2 表示2列
Log.d("tag", "列数2");
return 1;
case TYPE_TEXT:
//占用2/2 表示1列
Log.d("tag", "列数1");
return 2;
default:
Log.d("tag", "default列数2");
return 2;
}
}
});
}
其实就是把所有type的最小公倍数算出来,然后通过getSpanSize去分配每种type所占的大小。
如 a类一行2个,b类一行3个,c类一行5个;
那么grid宽就设为30单位,然后getSpanSize,a一个就占15的单位,所以一行显示2个;b就是一个10单位,所以一行显示3个,c类似;
这样无论多少种type都可以很轻松的区分了。
-
通过给LayoutManager设置reverse(反向), 轻松实现下拉加载(类似微信查看历史消息)
需求类似qq 微信公众号的下拉加载查看历史消息(和常见的下拉刷线,上拉加载不同)。
比较常用的下拉刷新,上拉加载:用sweapRefresh包一个recyclerView可以下拉刷新。上拉到底时,分页请求下一页数据,返回数据后,将数据加在原数据后面,notify加载下一页(底部的加载item过度一下)
但是现在这个需求不同的是,一开始进去看到的是最近的消息,也就是从recyclerView的最底部开始,往下拉到顶时,去请求上一页(历史消息)
第一次做时想法也是类似通用的方法,到顶时请求,请求回来再notify。不过这时就出现问题了。
往下加载时,比如list的postion是19(有20条数据了),然后请求了10条加到list尾,在notify刷新。这样没问题,还是从position=19往下bind了新的holder。
但是往上加载时,到顶了说明当前pistion=0了,然后请求数据回来时,这个数据因为比之前的数据时间要老一些,就要放在list头。此时如果直接notify,因为postion=0就会直接跑到新加数据的第一个item那里。就会形成跳跃的现象。
这里我做了一些尝试比如记住请求数据前的位置,请求完数据后再scrollTo原来的位置但是这样效果不理想,还是会抖一下。和微信查看历史消息那么平滑完全不能比。
然后发现从上往下加载数据,与请求回来的数据放在list头就是两个方向相反的操作,这个并不好兼容。在老司机的提示下,发现recyclerView支持reverse.
/**
* @param context Current context, will be used to access resources.
@param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link
* #VERTICAL}.
* @param reverseLayout When set to true, layouts from end to start.
*/
public LinearLayoutManager(Context context, @RecyclerView.Orientation int orientation,
boolean reverseLayout) {
setOrientation(orientation);
setReverseLayout(reverseLayout);
}
emmmm.... 这样请求数据和加载数据方向就是一致的了。
其实,把手机倒过来你就会发现微信的查看历史消息和普通的下拉加载一模一样。我们要做的仅仅需要设置reverse,然后数据源的顺序注意一下就OK了(底部postion=0,这点要注意)
这里说个题外话:微信的未读消息显示的逻辑。
比如未读消息比较多,屏幕显示不下了,就要在右上角显示一个“10条未读消息”这样子,点击后smoothScrollTo “以下为未读消息”的item;这个一个屏幕显示多少消息才装不下就没有具体去计算。由于需求里面的消息类型高度比较固定,大概估算了只能放3条,再多就显示不下了。我看微信公招也是差不多的,有时3条未读,手机屏幕显示不下,它也没提示新消息,应该也是估的吧,自己动态算有些麻烦。
-
通过notifyItemChanged(int,obj)局部刷新
以前初学的时候,就喜欢用notifyDataSetChanged();而且不管bindHoder用到的数据源变没变,感觉来了就给它notifyDataSetChanged();现在想想,当时也是萌蠢萌蠢的,哈哈。
先看一下recyclerView支持的刷新。我们常用的是notifyDataSetChanged()全刷新一遍,notifyItemRangeChanged范围刷新,notifyItemChanged某个item刷新,当然其他的我们要知道有这个东西,以后做到特殊需求才有方向,就像上面说的reverse一样,如果不知道,要自己搞,简直费时费力。
notifyItemRangeChanged(int ,int,obj(可选)我用的也不多。这个是听老司机说过。在分页加载的时候,如果数据回来了,直接notifyDataSetChanged。如果是普通的item那就没问题,但是如果item是播放器(抖音播放列表那种),就会影响到播放器的状态。通过notifyItemRangeChanged可以避免影响到之前的播放器。
这里主要讲notifyItemChanged(int,obj)局部刷新
其实使用的场景很普遍,类似点击一个item他要改变一些背景,代表自己是foucus状态,同时上一个focus的item要取消状态。
当我们处理时,只需要notifyItemChanged(position)只让这两个item重新走一下bindViewholder就可以刷新状态,不用全部item都跟着刷新,因为它们bind所要用的数据没变化,刷新了也和以前的状态一样的。
然后notifyItemChanged(int,obj)还有第二个参数,看一下注释。
/*
* Client can optionally pass a payload for partial change. These payloads will be merged
* and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the
* item is already represented by a ViewHolder and it will be rebound to the same
* ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing
* payloads on that item and prevent future payload until
* {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume
* that the payload will always be passed to onBindViewHolder(), e.g. when the view is not
* attached, the payload will be simply dropped.
*
* @param position Position of the item that has changed
* @param payload Optional parameter, use null to identify a "full" update
*
* @see #notifyItemRangeChanged(int, int)
*/
public final void notifyItemChanged(int position, @Nullable Object payload) {
mObservable.notifyItemRangeChanged(position, 1, payload);
}
大概意思就是我们可以选择传一个payload进去,就是第二个参数,代表这次刷新是这个item部分改变。就是我们只希望改变这个postionItem的部分,比如只改变文字或者背景,不重新加载图片;
其实就是之前说的,数据源没变,重新bind没有意义。但是有部分变了,你就是要notity才可以刷新,此时又不想刷新该item没有变化的部分(比如img的url没变),就可以通过这个方法,告诉adapter,我这次只需要局部刷新(变化的数据可以传给payload参数,也可以用全局变量)
为什么要局部刷新?
刷新一些大图的时候,会有闪烁效果。刷新webp(一种类似gif,但是更高效的动图)时动图会有停顿。而且,加载图片肯定是要耗时的嘛,在一些低端机器上引起本可以避免的卡顿。
notyfi带有payLoad的刷新后,bindViewHolder也需要对这个进行处理。
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position,
@NonNull List
@NonNull List
由于全局维护了一个focusIndex的,所以我不需要payload传什么有效数据,只需要不为null即可,所以这里我是notifyItemChanged(postion,"");
题外话:这里说一下notify的异步
private int mFocusIndex = -1;
public void setmFocusIndex(int focusIndex) {
//取消上一个focus的状态
notifyItemChanged(mFocusIndex,"");
this.mFocusIndex = focusIndex;
//设置新的focus的状态
notifyItemChanged(mFocusIndex,"");
}
上面代码,从上往下执行看起来是不是有问题的?
//取消上一个focus的状态
notifyItemChanged(mFocusIndex,"");时,因为当前focusIndex还没有重置,bindholder时,认为它还是被focus的item,所以取消不了focus状态?
实际上是可以的,因为notify是异步的!
通常我们认为重置focus状态要用两个变量记录:一个是当前的加上foucus背景,一个是上一个focus来取消背景。
其实如果知道notify是异步的,只需要向上面代码那样写。
因为notify到后面会去调用requestLayout刷新view,类似于发送消息到队尾。虽然发消息时,是按照串行的先第一个notify,然后设置新的foucusIndex,然后再第二个notity。但是因为message在队尾,等message被拿出来回调到bindViewHolder的时候,this.mFocusIndex = focusIndex;已经执行完了,所以就没有上面提到的,看起来好像不行的问题。
不过我还是选择使用了2个index变量来写,这样可读性更高一些吧。
ps:有没有懂web前端的android老哥,一些交流下。最近想试试webApp 和 flutter。