在电商、秒杀类产品里经常有在列表里展示倒计时的需求,本文来自一位朋友的投稿,介绍了实现这种需求的一种方案,思路还是不错的,但使用 Handler 实现倒计时在主线程卡顿时可能会有延迟,建议结合 Java 定时任务类 ScheduledExecutorService 使用。欢迎有兴趣的朋友一起探讨。
作者:AndroidMsky原文:https://blog.csdn.net/AndroidMsky/article/details/88557445
本文介绍在RecyclerView中使用倒计时楼层,并且每秒刷新显示倒计时。没有纠结于样式,主要介绍代码结构和设计模式。
先看一下效果:
我们采取的是观察者模式的方法,启动一个handler,每隔一秒去刷新所有注册过的item楼层。观察者模式的大概关系如下图:
我们并没有使用JAVA中的Observable,因为在释放Holder的时机比较难处理存在内存泄露的风险,所以我们采用WeakHashMap去保存所有Holder,但是在GC不频繁的情况下,弱引用也可以访问到无效的Holder对象,如果RecyclerView频繁更新,GC又不频繁进行,将会有通知到很多无效的Holder。为此我们采用了均衡的两套方案:
在RecyclerView更新不频繁的场景下我们只在Adapter的OnCreate中注册Holder,并且除非页面销毁否则不去管理WeakHashMap中的内容,一切交给GC处理。
在RecyclerView更新频繁的时候,我们会在onbind方法里判断是否注册了Holder,没有则加入,并且在notifyDataChange的时候解除所有Holder的注册,这样就算Holder没有被GC回收,也收不到我们的更新指令了。
选用哪种方案将在Observable初始化的时候得到确认:
/*
* @param beatTime 更新频率
* @param changeable 易变得,如果为true
* 会在bind方法里访问Map,不存在则加入Map
* 如果存在频繁的下拉刷新,分页加载建议设为true
*
* 如果列表数据不频繁更新请设置为false,
* 只会在onCreate时候注册观察者。
* Holder会在GC发生后自动停止更新。
*
*
* */
private final int BEAT_TIME;
private final boolean CHANGEABLE;
...
public Center(int beatTime, boolean changeable) {
BEAT_TIME = beatTime;
CHANGEABLE = changeable;
}
注册方法:
private WeakHashMap<Observer, Object> weakHashMap = new WeakHashMap();
...
@Override
public void addObserver(Observer observer) {
weakHashMap.put(observer, null);
}
通过bindRecyclerView获取到当前展示的item位置范围:
public void bindRecyclerView(RecyclerView recyclerView){
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int first=-1;
int last=-1;
RecyclerView.LayoutManager layoutManager=recyclerView.getLayoutManager();
if (layoutManager instanceof GridLayoutManager){
first=((GridLayoutManager)layoutManager).findFirstVisibleItemPosition();
last=((GridLayoutManager)layoutManager).findLastVisibleItemPosition();
}
if (layoutManager instanceof LinearLayoutManager){
first=((LinearLayoutManager)layoutManager).findFirstVisibleItemPosition();
last=((LinearLayoutManager)layoutManager).findLastVisibleItemPosition();
}
postionFL.frist=first;
postionFL.last=last;
Log.e("lmtlmt2","frist:"+first+"last:"+last);
}
});
}
通知所有holder更新数据并将位置访问传递给所有观察者,观察者拿到显示范围,并和自己做比较,决定是否更新UI:
private void notifyObservers() {
Iterator iter = weakHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Observer observer = (Observer) entry.getKey();
if (observer != null) {
if (postionFL.frist>-1&&postionFL.last>-1)
observer.update(null, postionFL);
else observer.update(null, null);
}
}
Log.e("lmtlmt", "weakHashMap size" + weakHashMap.size());
}
停止和开始倒计时:
@Override
public void startCountdown() {
if (!isStart) {
handler.sendEmptyMessage(0);
isStart = true;
}
}
@Override
public void stopCountdown() {
isStart = false;
handler.removeCallbacksAndMessages(null);
}
通知更新和判断是否加入Map,仅在易变模式下有效:
@Override
public boolean containHolder(Observer observer) {
//如果不是易变的 直接屏蔽次方法。
if (!CHANGEABLE) return true;
if (weakHashMap.containsKey(observer)) return true;
return false;
}
@Override
public void notifyAdapter() {
if (!CHANGEABLE) return;
deleteObservers();
}
倒计时model代码,这里注意我们使用的是elapsedRealtime(可以理解为开机后cpu的增长时钟),不会因为用户调整系统时间而导致倒计时不准确。
public class TimeBean {
private long elapsedRealtime;
TimeBean(long remainTime) {
elapsedRealtime = remainTime + SystemClock.elapsedRealtime()/1000;
}
public long getRainTime() {
return elapsedRealtime - SystemClock.elapsedRealtime()/1000;
}
下面是Adapter的代码:ViewHolder实现Observer接口等待更新信号,只更新在范围内的ViewHolder
static class ViewHolder extends RecyclerView.ViewHolder implements Observer {
int lastBindPositon = -1;
TextView textView;
TimeBean timeBean;
public ViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.tv1);
}
@Override
public void update(Observable o, Object arg) {
if (arg!=null&&arg instanceof Center.PostionFL){
Center.PostionFL postionFL=(Center.PostionFL)arg;
if (lastBindPositon>=postionFL.frist&&lastBindPositon<=postionFL.last){
Log.e("lmtlmtupdate", "update" + lastBindPositon);
bindCountView(textView, timeBean);
}
}
}
两个关键方法:注意在非易变模式下,bind中的containHolder方法恒为true。
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
countDownCenter.addObserver(viewHolder);
countDownCenter.startCountdown();
return viewHolder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.lastBindPositon = position;
holder.timeBean = list.get(position);
bindCountView(holder.textView, holder.timeBean);
if (!countDownCenter.containHolder(holder)){
countDownCenter.addObserver(holder);
}
}
测试代码片段:
for (int i = 0; i < 100; i++) {
list.add(new TimeBean(i*10));
}
countDownCenter=new Center(1000,false);
listAdapter=new ListAdapter(list,countDownCenter);
recyclerView.setAdapter(listAdapter);
final FrameLayout frameLayout=findViewById(R.id.f1);
frameLayout.addView(recyclerView);
方案总结:
倒计时信号恒定时间准确。
使用弱引用即便操作不当也不会产生内存泄露。
提供两种权衡方法,根据项目自身情况最大程度避免CPU无效开销。
可根据Activity或Fragment生命周期进行暂停和重开。
入侵性小,介入简便。
代码地址:https://github.com/AndroidMsky/RecycleViewFloorCountDown