在开中难免遇到RecyclerView中带有倒计时的场景,有时候不止一个。那么如何实现一个带有倒计时的RecyclerView便是本文讨论的课题

1.在RecyclerView中实现高质量倒计时需要解决哪些问题。

  • 当列表中有多个倒计时如何处理,开启多个还是统一管理?如果每个item都开启倒计时是否造成了资源浪费?不在展示区的倒计时又该如何取消?不在屏幕上的倒计时如何保证时间的准确性?只使用一个倒计时如何通知所有需要更新的item?如何保证不在屏幕上而不被通知的item的时间的准确性?能否在退出后台或者跳转到其他页面的时候暂停减少资源的浪费?

2.带着这些问题着手设计我们的倒计时方案。

  • 首先从大的逻辑上我们优选单任务倒计时方案。
  • 计时器的选择,考虑到可能频繁启停,我们选择Rxjava提供的interval方法。内部基于线程池管理,避免使用Timer类会造成线程开关的成本过高。
  • 敲黑板啦重点来了 计时器只负责定时通知相关item更新UI,不记录item剩余时间。由于前端获取的时间可能有时差或者被用户修改过是不可信的,网关下发的bean类中是剩余时间如2000秒。需要在bean类中手动增加一个变量 倒计时结束时间点 我们命名为endCountTime,在json映射为bean类的时候我们获取当前系统时间 让它加上 倒计时剩余时间 就是所需要的 endCountTime。 同时呢由于系统时间的不可信,也就是System.getCurrentMillions是不可信的,所以我们选择系统的开机时钟 SystemClock.elapsedRealtime()。这是我们能够得以实现随时暂停再开启倒计时 倒计时时间依然能够保证正确的关键因素。
  • 使用一个List来管理需要通知的Item位置pos,我们命名为countDownPositions。在bindViewHolder的时候我们将需要更新的itemPosition添加到countDownPositions中,当计时器任务通知时来遍历countDownPositions,然后进行notityitemchanged。在这我们会遇到两个选择,不在屏幕上的item,notifyitemchange的时候会不会造成浪费。另一种选择的是在notifyitemchange 的时候判断是否在屏幕上。 判断是否在屏幕上和直接notify成本哪个更高,虽然有查阅相关资料,也做过一些测试。并没有分辨出来哪个更好,如有了解的还请指教,在本次实践中 我是判断是否在屏幕上来决定是否notify。
  • 由于我们记录的item位置pos,当RecyclerView发生,增删的时候,我们记录的位置有可能会错位,所以我们给adaptert注册一个数据观察器,这样在数据发生变动的时候,可以保证需要更新的item不会产生错位

3. 整体思路整理完毕,接下来代码实现

  • Helper类代码实现
public class RvCountDownHelper {

private List<Index> countDownPositions = new ArrayList<>();
private RecyclerView.Adapter rvAdapter;
private RecyclerView recyclerView;

private Disposable countDownTask;

private OnTimeCollectListener mListener;

public RvCountDownHelper(RecyclerView.Adapter rvAdapter, RecyclerView recyclerView) {
this.recyclerView = recyclerView;
this.rvAdapter = rvAdapter;
rvAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
removeAllPosition();
super.onChanged();
Log.d("AdapterDataObserver", "onChanged");
}

@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
super.onItemRangeChanged(positionStart, itemCount);
Log.d("AdapterDataObserver", "onItemRangeChanged");
}

@Override
public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
super.onItemRangeChanged(positionStart, itemCount, payload);
Log.d("AdapterDataObserver", "onItemRangeChanged");
}

@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
for (Index countDownPosition : countDownPositions) {
if (countDownPosition.index >= positionStart) {
countDownPosition.index += itemCount;
}
}
super.onItemRangeInserted(positionStart, itemCount);
Log.d("AdapterDataObserver", "onItemRangeInserted");
}

@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
for (int i = countDownPositions.size() - 1; i >= 0; i--) {
Index temp = countDownPositions.get(i);
if (temp.index >= positionStart + itemCount) {
temp.index = temp.index - itemCount;
} else if (temp.index >= positionStart) {
removeCountDownPosition(temp.index);
}
}
super.onItemRangeRemoved(positionStart, itemCount);
Log.d("AdapterDataObserver", "onItemRangeRemoved");
}

@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {

Log.d("ItemMove", "frompos =" + fromPosition + " toPos =" + toPosition + " itemCount= " + itemCount);

for (Index countDownPosition : countDownPositions) {
if (countDownPosition.index == fromPosition) {
countDownPosition.index = toPosition;
}else if (countDownPosition.index == toPosition) {
countDownPosition.index = fromPosition;
}
}

super.onItemRangeMoved(fromPosition, toPosition, itemCount);
Log.d("AdapterDataObserver", "onItemRangeMoved");
}
});
}

public void setOnTimeCollectListener(OnTimeCollectListener listener) {
this.mListener = listener;
}

/**
* 新增一个需要倒计时的item位置
* @param pos
*/
public void addPosition2CountDown(int pos) {
Index addPos = new Index(pos);
if (!countDownPositions.contains(addPos)) {
Log.d("CountDown", "新增pos-" + pos);
countDownPositions.add(addPos);
startCountDown();
}
}

/**
* 移除一个需要定时更新的item
* @param pos
*/
public void removeCountDownPosition(int pos) {
boolean remove = countDownPositions.remove(new Index(pos));
Log.d("CountDown", "移除pos-" + pos + "result = " + remove);

}

/**
* 移除所有需要定时更新的item
*/
public void removeAllPosition() {
countDownPositions.clear();
Log.d("CountDown", "移除所有标记位置");
}

/**
* 手动调用开始定时更新
*/
public void startCountDown() {
if (countDownTask == null || countDownTask.isDisposed()) {
countDownTask = Observable.interval(0, 1000, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(aLong -> {
Log.d("倒计时--", "cur aLong= " + aLong);

if (countDownTask.isDisposed()) {
return;
}

if (countDownPositions.isEmpty()) {
countDownTask.dispose();
return;
}

for (Index countDownPosition : countDownPositions) {
RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();
if (lm != null) {
View itemView = recyclerView.getLayoutManager().findViewByPosition(countDownPosition.index);
if (itemView != null) {
if (mListener != null) {
RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForPosition(countDownPosition.index);
mListener.onTimeCollect(viewHolder, countDownPosition.index);
} else {
rvAdapter.notifyItemChanged(countDownPosition.index);
}
}
}

}

}, throwable -> Log.e("倒计时异常", throwable.getMessage()));

}
}

/**
* 手动调用停止定时更新
*/
public void stopCountDown() {
if (countDownTask != null && !countDownTask.isDisposed()) {
countDownTask.dispose();
}
}

/**
* 获取所有的item位置记录
*/
public List<Index> getAllRecordPos() {
return countDownPositions;
}

/**
* 销毁
*/
public void destroy() {
stopCountDown();
mListener = null;
countDownTask = null;
recyclerView = null;
rvAdapter = null;
}

interface OnTimeCollectListener {
void onTimeCollect(RecyclerView.ViewHolder vh,int pos);
}

static class Index {
int index;

public Index(int index) {
this.index = index;
}

@Override
public boolean equals(@Nullable Object obj) {
if(!(obj instanceof Index)) {
// instanceof 已经处理了obj = null的情况
return false;
}
Index indObj = (Index) obj;
// 地址相等
if (this == indObj) {
return true;
}
// 如果两个对象index相等
return indObj.index == this.index;
}

@Override
public int hashCode() {
return 128 * index;
}
}
}
  • 使用代码样例
public class MainActivity extends AppCompatActivity {
MyRvAdapter myRvAdapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

RecyclerView rvMyRv = findViewById(R.id.rvMyRv);
// rvMyRv.setItemAnimator(null);
((SimpleItemAnimator)rvMyRv.getItemAnimator()).setSupportsChangeAnimations(false);
rvMyRv.setLayoutManager(new LinearLayoutManager(this));
myRvAdapter = new MyRvAdapter(rvMyRv);
rvMyRv.setAdapter(myRvAdapter);

}

@Override
protected void onPause() {
super.onPause();
myRvAdapter.stopCountDown();
}

@Override
protected void onResume() {
super.onResume();
myRvAdapter.startCountDown();
}

public void addClick(View view) {
myRvAdapter.addItem();
}

public void removeClick(View view) {
myRvAdapter.deleteItem();
}

public void exchangeClick(View view) {
myRvAdapter.exchangeItem(4, 2);
}

static class MyRvAdapter extends RecyclerView.Adapter<MyViewHolder> {

List<TestData> times;
RvCountDownHelper countDownHelper;
RecyclerView mRecyclerView;

public MyRvAdapter(RecyclerView recyclerView) {
this.mRecyclerView = recyclerView;
times = new ArrayList<>();
countDownHelper = new RvCountDownHelper(this, mRecyclerView);
// countDownHelper.setOnTimeCollectListener((viewHolder,pos) -> {
// if (viewHolder instanceof MyViewHolder) {
// long curMillions = SystemClock.elapsedRealtime();
// long endMillions = times.get(pos).countDownEndTime;
//
// long tmp = endMillions - curMillions;
//
// if (tmp > 1000) {
// ((MyViewHolder) viewHolder).tvShowTime.setText("倒计时 " + getShowStr(tmp));
// }
// }
// });

long curMillions = SystemClock.elapsedRealtime();
for (int i = 0; i < 50; i++) {
if (i % 2 == 0) {
times.add(TestData.createRandomData(curMillions + (long) new Random().nextInt(30 * 60 * 1000)));
} else {
times.add(TestData.createRandomData(-1));
}
}
}

public void addItem() {
long curMillions = SystemClock.elapsedRealtime();
times.add(0, TestData.createRandomData(curMillions + (long) new Random().nextInt(30 * 60 * 1000)));
notifyItemInserted(0);
}

public void deleteItem() {
times.remove(0);
notifyItemRemoved(0);
}

public void exchangeItem(int fromPos, int toPos) {
Collections.swap(times,fromPos,toPos);
notifyItemRangeChanged(fromPos, toPos + 1 - fromPos);
}

@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
View contentView = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_layout, viewGroup, false);
return new MyViewHolder(contentView);
}

@Override
public void onBindViewHolder(@NonNull MyViewHolder viewHolder, int i) {
TestData data = times.get(i);

if (data.isCountDownItem) {
long curMillions = SystemClock.elapsedRealtime();

long tmp = data.countDownEndTime - curMillions;

if (tmp > 1000) {
viewHolder.tvShowTime.setText("倒计时 " + getShowStr(tmp));
countDownHelper.addPosition2CountDown(i);

} else {
viewHolder.tvShowTime.setText("倒计时 00:00:00");
countDownHelper.removeCountDownPosition(i);
}
}else {
viewHolder.tvShowTime.setText("无倒计时");
}

}

@Override
public int getItemCount() {
return times.size();
}

private String getShowStr(long mis) {
mis = mis / 1000; //
long h = mis / 3600;
long m = mis % 3600 / 60;
long d = mis % 3600 % 60;
return h + ":" + m + ":" + d;
}

public void destroy() {
countDownHelper.destroy();
}

public void stopCountDown() {
countDownHelper.stopCountDown();
}

public void startCountDown() {
countDownHelper.startCountDown();
}
}

@Override
protected void onDestroy() {
myRvAdapter.destroy();
super.onDestroy();
}

static class MyViewHolder extends RecyclerView.ViewHolder {
TextView tvShowTime;

public MyViewHolder(@NonNull View itemView) {
super(itemView);
tvShowTime = itemView.findViewById(R.id.tvShowTime);
}
}

static class TestData {

public TestData(boolean isCountDownItem, long countDownEndTime) {
this.isCountDownItem = isCountDownItem;
this.countDownEndTime = countDownEndTime;
}

boolean isCountDownItem;

long countDownEndTime;

static TestData createRandomData(long endTime) {
if (endTime < 0) {
return new TestData(false, endTime);
} else {
return new TestData(true, endTime);
}
}
}


}