本文微信公眾號「AndroidTraveler」首發。
背景
在 Android 列表開發過程中,有時候我們的 Item 會有一些元件,比如倒數計時。這類元件要求不斷重新整理,這個時候由於列表複用的機制,因此會有一些坑。那麼我們本篇文章就給大家講兩個主題。
第一個是列表複用是否一定有問題。 第二個是出現問題有哪些解決方案可供我們選擇。
小 Demo
由於我們的主題重點是為了解決不斷重新整理問題,因此關於 RecyclerView 的基本使用就不再贅述,不清楚的小夥伴可以看下我之前的文章:
RecyclerView基本使用
首先我們看下效果圖:
很簡單,就是一個 RecyclerView 列表,列表項有兩個元件。分別代表第幾項和剩餘秒數。
這裡就是通過倒數計時來演示重新整理可能存在的問題。
重點程式碼是 Adapter 裡面的顯示邏輯,初始為:
@Override
public void onBindViewHolder(RecyclerViewViewHolder holder, int position) {
holder.mTvNum.setText(String.valueOf(position + 1));
updateTime(holder, itemList.get(position));
}
private void updateTime(final RecyclerViewViewHolder holder, final long time) {
String content;
long remainTime = time - System.currentTimeMillis();
remainTime /= 1000;
if (remainTime <= 0) {
content = "Time up";
holder.mTxtTitle.setText(content);
return;
}
content = "剩下"+remainTime+"秒";
holder.mTxtTitle.setText(content);
}
複製程式碼
全部程式碼見:github.com/nesger/Recy…
接下來我們增加重新整理方法,有很多種,我們一一說明。
1. 使用 handler 來實現倒數計時重新整理
修改顯示程式碼,如下:
@Override
public void onBindViewHolder(RecyclerViewViewHolder holder, int position) {
holder.mTvNum.setText(String.valueOf(position + 1));
updateTime(holder, itemList.get(position));
}
private void updateTime(final RecyclerViewViewHolder holder, final long time) {
String content;
long remainTime = time - System.currentTimeMillis();
remainTime /= 1000;
if (remainTime <= 0) {
content = "Time up";
holder.mTxtTitle.setText(content);
return;
}
content = "剩下"+remainTime+"秒";
holder.mTxtTitle.setText(content);
holder.mTxtTitle.postDelayed(new Runnable() {
@Override
public void run() {
updateTime(holder, time);
}
}, 1000);
}
複製程式碼
可以看到通過 handler 延時一秒,然後每次更新時間也是減少一秒。
我們看下效果圖:
可以看到沒滾動之前還好,滾動之後會發現,倒數計時都亂了。
當然有時候可能不會暴露出來,比如滾動數目少,或者只有部分元件有倒數計時,不像我們這個例子,所有專案都有倒數計時,但是這也間接留下了可能的坑。
出現這個問題的原因在於元件的複用,如果你用 ListView 演示,並且不用複用,那麼是不會錯亂的。
當然列表不復用這個肯定是不推薦的。
因此,該方式不推薦。
全部程式碼見:github.com/nesger/Recy…
2. 使用 Timer 來實現倒數計時重新整理
@Override
public void onBindViewHolder(RecyclerViewViewHolder holder, int position) {
holder.mTvNum.setText(String.valueOf(position + 1));
updateTime(holder, itemList.get(position));
}
private void updateTime(final RecyclerViewViewHolder holder, final long time) {
String content;
long remainTime = time - System.currentTimeMillis();
remainTime /= 1000;
if (remainTime <= 0) {
content = "Time up";
holder.mTxtTitle.setText(content);
return;
}
content = "剩下"+remainTime+"秒";
holder.mTxtTitle.setText(content);
}
複製程式碼
一樣不行,不推薦。
全部程式碼見:github.com/nesger/Recy…
3. 使用 Timer + View 集合
其實我們簡單分析一下就知道,出現上面錯亂情況的原因大致是兩個:一個是複用,一個是程式碼多次呼叫。
所以如果能夠解決這兩個問題,那麼這個問題就解決了。
因為我們這裡的業務是倒數計時監聽,所有 View 都是一樣的,就是一秒更新一次。
所以我們的定時器不需要 N 個,只需要一個,在建構函式初始化即可。
另外為了避免複用和程式碼多次呼叫問題,我們將 View 通過一個集合儲存起來。
最後修改的程式碼如下:
private Timer mTimer;
private Set<RecyclerViewViewHolder> mHolders;
public RecyclerViewAdapter(Activity activity, List<Long> itemList) {
if (activity == null || itemList == null) {
throw new IllegalArgumentException("params can't be null");
}
this.activity = activity;
this.itemList = itemList;
mHolders = new HashSet<>();
mTimer = new Timer();
mTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
for (RecyclerViewViewHolder holder : mHolders) {
updateTime(holder, holder.getTime());
}
}
}, 0, 1000);
}
@Override
public void onBindViewHolder(RecyclerViewViewHolder holder, int position) {
holder.setTime(itemList.get(position));
mHolders.add(holder);
holder.mTvNum.setText(String.valueOf(position + 1));
updateTime(holder, itemList.get(position));
}
複製程式碼
效果圖如下:
可以看到沒問題了。
當然這裡有些優化還沒處理,因為本篇主要是思路分析,這裡就不新增了。
待優化點:定時器的啟動和關閉跟生命週期關聯,無資料來源不啟用定時器等。
全部程式碼見:github.com/nesger/Recy…
該方法來自與一名朋友的分享。
4. 使用 ScheduledExecutorService + View 集合
這邊 AndroidStudio 有安裝阿里巴巴提供的一個程式碼檢測外掛,連結為:plugins.jetbrains.com/plugin/1004…
在 AndroidStudio 輸入外掛名字 Alibaba Java Coding Guidelines 查詢安裝即可。
在方法 3 使用 Timer 時提示下面資訊:
Run multiple TimeTask by using ScheduledExecutorService rather than Timer because Timer will kill all running threads in case of failing to catch exceptions.
//org.apache.commons.lang3.concurrent.BasicThreadFactory
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
//do something
}
},initialDelay,period, TimeUnit.HOURS);
複製程式碼
所以我們這裡修改 Timer 為 ScheduledExecutorService:
private ScheduledExecutorService mExecutorService;
public RecyclerViewAdapter(Activity activity, List<Long> itemList) {
if (activity == null || itemList == null) {
throw new IllegalArgumentException("params can't be null");
}
this.activity = activity;
this.itemList = itemList;
mHolders = new HashSet<>();
mExecutorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
@Override
public Thread newThread(@NonNull Runnable r) {
Thread thread = new Thread(r);
thread.setName("countdown");
return thread;
}
});
mExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
for (RecyclerViewViewHolder holder : mHolders) {
updateTime(holder, holder.getTime());
}
}
}, 0, 1000, TimeUnit.MILLISECONDS);
}
複製程式碼
全部程式碼見:github.com/nesger/Recy…
有更多方法歡迎到上面的 GitHub 連結提 PR,可以基於 feature/refresh 分支新建分支。
有另外一位朋友提出了自定義 View 的處理方式,將倒數計時的功能放到 View 裡面去處理,這個感興趣的小夥伴可以實現然後提 PR 哈,這裡提供額外一種思路。