在開中難免遇到RecyclerView中帶有倒計時的場景,有時候不止一個屏镊。那么如何實現(xiàn)一個帶有倒計時的RecyclerView便是本文討論的課題
1.在RecyclerView中實現(xiàn)高質(zhì)量倒計時需要解決哪些問題。
- 當列表中有多個倒計時如何處理符隙,開啟多個還是統(tǒng)一管理跳纳?如果每個item都開啟倒計時是否造成了資源浪費?不在展示區(qū)的倒計時又該如何取消卡儒?不在屏幕上的倒計時如何保證時間的準確性?只使用一個倒計時如何通知所有需要更新的item俐巴?如何保證不在屏幕上而不被通知的item的時間的準確性骨望?能否在退出后臺或者跳轉(zhuǎn)到其他頁面的時候暫停減少資源的浪費?
2.帶著這些問題著手設(shè)計我們的倒計時方案欣舵。
首先從大的邏輯上我們優(yōu)選單任務(wù)倒計時方案擎鸠。
計時器的選擇,考慮到可能頻繁啟停缘圈,我們選擇Rxjava提供的interval方法劣光。內(nèi)部基于線程池管理,避免使用Timer類會造成線程開關(guān)的成本過高糟把。
敲黑板啦重點來了 計時器只負責定時通知相關(guān)item更新UI绢涡,不記錄item剩余時間。由于前端獲取的時間可能有時差或者被用戶修改過是不可信的糊饱,網(wǎng)關(guān)下發(fā)的bean類中是剩余時間如2000秒垂寥。需要在bean類中手動增加一個變量 倒計時結(jié)束時間點 我們命名為endCountTime,在json映射為bean類的時候我們獲取當前系統(tǒng)時間 讓它加上 倒計時剩余時間 就是所需要的 endCountTime另锋。 同時呢由于系統(tǒng)時間的不可信,也就是System.getCurrentMillions是不可信的狭归,所以我們選擇系統(tǒng)的開機時鐘 SystemClock.elapsedRealtime()夭坪。這是我們能夠得以實現(xiàn)隨時暫停再開啟倒計時 倒計時時間依然能夠保證正確的關(guān)鍵因素。
使用一個List來管理需要通知的Item位置pos过椎,我們命名為countDownPositions室梅。在bindViewHolder的時候我們將需要更新的itemPosition添加到countDownPositions中,當計時器任務(wù)通知時來遍歷countDownPositions疚宇,然后進行notityitemchanged亡鼠。在這我們會遇到兩個選擇,不在屏幕上的item敷待,notifyitemchange的時候會不會造成浪費间涵。另一種選擇的是在notifyitemchange 的時候判斷是否在屏幕上。 判斷是否在屏幕上和直接notify成本哪個更高榜揖,雖然有查閱相關(guān)資料勾哩,也做過一些測試抗蠢。并沒有分辨出來哪個更好,如有了解的還請指教思劳,在本次實踐中 我是判斷是否在屏幕上來決定是否notify迅矛。
由于我們記錄的item位置pos,當RecyclerView發(fā)生潜叛,增刪的時候秽褒,我們記錄的位置有可能會錯位,所以我們給adaptert注冊一個數(shù)據(jù)觀察器威兜,這樣在數(shù)據(jù)發(fā)生變動的時候销斟,可以保證需要更新的item不會產(chǎn)生錯位
3. 整體思路整理完畢,接下來代碼實現(xiàn)
- Helper類代碼實現(xiàn)
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", "移除所有標記位置");
}
/**
* 手動調(diào)用開始定時更新
*/
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()));
}
}
/**
* 手動調(diào)用停止定時更新
*/
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 已經(jīng)處理了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);
}
}
}
}
結(jié)尾奉上demo源碼 歡迎圍觀多提意見