前段时间项目有个需求是需要在listview的item中做个60s倒计时功能,并且倒计时的时间由本地记录,无关服务端。网上找了一些demo,有倒计时的功能,但总有些问题,也无法满足需求,最后自己改进后满足了需求,然后就想着记录下开发过程中遇到的一些问题以及最后的成品。
项目需求背景:app是关于视频会议,视频会议列表由listview来展示。本次需求是需要在会议列表的item中加一个再次通知的功能,点击后有个60s倒计时,等倒计时结束才可以再次点击。
试了几种后,感觉使用CountDownTimer来实现是比较好的。
demo:
import android.content.Context;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import com.sfyc.countdownlist.R;
import com.sfyc.countdownlist.entity.TimerItem;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import static com.sfyc.countdownlist.R.id.toolbar;
public class CountDownListActivity extends AppCompatActivity {
private Context mContext;
@BindView(toolbar)
Toolbar mToolbar;
@BindView(R.id.list_view)
ListView mListView;
MyAdapter mAdapter;
private ArrayList<TimerItem> lstTimerItems;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list_view);
ButterKnife.bind(this);
mContext = this;
mToolbar.setTitle(R.string.title_list_view_countdown);
initDadas();
mAdapter = new MyAdapter(mContext, lstTimerItems);
mListView.setAdapter(mAdapter);
}
private void initDadas() {
lstTimerItems = new ArrayList<>();
lstTimerItems.add(new TimerItem("A", 0));
lstTimerItems.add(new TimerItem("B", 0));
lstTimerItems.add(new TimerItem("C", 0));
lstTimerItems.add(new TimerItem("D", 0));
lstTimerItems.add(new TimerItem("E", 0));
lstTimerItems.add(new TimerItem("F", 0));
lstTimerItems.add(new TimerItem("G", 0));
lstTimerItems.add(new TimerItem("H", 0));
lstTimerItems.add(new TimerItem("I", 0));
lstTimerItems.add(new TimerItem("J", 0));
lstTimerItems.add(new TimerItem("K", 0));
lstTimerItems.add(new TimerItem("L", 0));
lstTimerItems.add(new TimerItem("M", 0));
lstTimerItems.add(new TimerItem("N", 0));
lstTimerItems.add(new TimerItem("O", 0));
}
public static class MyAdapter extends BaseAdapter {
private List<TimerItem> mDatas;
private Context mContext;
//用于退出activity,避免countdown,造成资源浪费。
private SparseArray<CountDownTimer> countDownCounters;
public MyAdapter(Context mContext, List<TimerItem> mDatas) {
this.mContext = mContext;
this.mDatas = mDatas;
this.countDownCounters = new SparseArray<>();
}
/**
* 清空资源
*/
public void cancelAllTimers() {
if (countDownCounters == null) {
return;
}
Log.e("TAG", "size : " + countDownCounters.size());
for (int i = 0, length = countDownCounters.size(); i < length; i++) {
CountDownTimer cdt = countDownCounters.get(countDownCounters.keyAt(i));
if (cdt != null) {
cdt.cancel();
}
}
}
@Override
public int getCount() {
if (mDatas != null && !mDatas.isEmpty()) {
return mDatas.size();
}
return 0;
}
@Override
public Object getItem(int position) {
if (mDatas != null && !mDatas.isEmpty()) {
return mDatas.get(position);
}
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final ViewHolder viewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item_common2, parent, false);
viewHolder = new ViewHolder();
viewHolder.btn = (TextView) convertView.findViewById(R.id.btn);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
final TimerItem data = mDatas.get(position);
viewHolder.btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
data.setExpirationTime(60*1000);
notifyDataSetChanged();
}
});
CountDownTimer countDownTimer = countDownCounters.get(viewHolder.btn.hashCode());
//将前一个缓存清除
if (countDownTimer != null) {
countDownTimer.cancel();
}
long timer = data.getExpirationTime();
// timer = timer - System.currentTimeMillis();
if (timer > 0) {
countDownTimer = new CountDownTimer(timer, 1000) {
public void onTick(long millisUntilFinished) {
viewHolder.btn.setText(millisUntilFinished/1000 +"");
Log.e("TAG", data.name + " : " + millisUntilFinished);
}
public void onFinish() {
viewHolder.btn.setText("开始倒计时");
}
}.start();
countDownCounters.put(viewHolder.btn.hashCode(), countDownTimer);
} else {
viewHolder.btn.setText("开始倒计时");
}
return convertView;
}
public class ViewHolder {
public TextView btn;
}
}
}
效果:
但是这里有几个问题:
1.当item不可见时,CountDownTimer会停止运行,直至item可见时继续运行,这样倒计时的时间就会不准确。比如在55s的时候item不可见,过了20s后item可见了,你会发现倒计时的时间还在55s。
2.刷新问题,由于本项目做的是会议列表,因此有每隔5s自动刷新会议列表,并且用户也可以手动下拉刷新列表。这样不管之前的倒计时状态是怎样,刷新后都会变为未倒计时状态。
针对上述问题的解决方案:
1.因为之前按钮点击后我是给了个60s的时间,然后利用这个时间去做递减才会出现的这个情况。后来改成点击的时候赋予当前系统+60s的时间。然后去判断这个时间与当前系统时间的差值,如果差值大于0的话就执行倒计时。这样不管item不可见多久,时间的差值都不会有问题。
2.刷新问题一直都没有很好的解决,所以后面我用了一个笨办法,就是另外建立一个数组,每次点击按钮赋值的时候,在新建的数组中保存该对象。然后在每次刷新数据的时候,将数据与保存数据的数组一一对比,如果数据一致就将时间赋值过去。因为会议系统中会议id是唯一的,因此我这里是根据id去判断的。
改进后代码:
import android.content.Context;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import com.sfyc.countdownlist.R;
import com.sfyc.countdownlist.entity.TimerItem;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import static com.sfyc.countdownlist.R.id.toolbar;
public class CountDownListActivity extends AppCompatActivity {
private Context mContext;
@BindView(toolbar)
Toolbar mToolbar;
@BindView(R.id.list_view)
ListView mListView;
MyAdapter mAdapter;
private ArrayList<TimerItem> lstTimerItems;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list_view);
ButterKnife.bind(this);
mContext = this;
mToolbar.setTitle(R.string.title_list_view_countdown);
initDadas();
mAdapter = new MyAdapter(mContext, lstTimerItems);
mListView.setAdapter(mAdapter);
}
private void initDadas() {
lstTimerItems = new ArrayList<>();
lstTimerItems.add(new TimerItem("A", 0));
lstTimerItems.add(new TimerItem("B", 0));
lstTimerItems.add(new TimerItem("C", 0));
lstTimerItems.add(new TimerItem("D", 0));
lstTimerItems.add(new TimerItem("E", 0));
lstTimerItems.add(new TimerItem("F", 0));
lstTimerItems.add(new TimerItem("G", 0));
lstTimerItems.add(new TimerItem("H", 0));
lstTimerItems.add(new TimerItem("I", 0));
lstTimerItems.add(new TimerItem("J", 0));
lstTimerItems.add(new TimerItem("K", 0));
lstTimerItems.add(new TimerItem("L", 0));
lstTimerItems.add(new TimerItem("M", 0));
lstTimerItems.add(new TimerItem("N", 0));
lstTimerItems.add(new TimerItem("O", 0));
}
public static class MyAdapter extends BaseAdapter {
private ArrayList<TimerItem> timerItemsSave = new ArrayList<>();
private List<TimerItem> mDatas;
private Context mContext;
//用于退出activity,避免countdown,造成资源浪费。
private SparseArray<CountDownTimer> countDownCounters;
public MyAdapter(Context mContext, List<TimerItem> mDatas) {
this.mContext = mContext;
this.mDatas = mDatas;
this.countDownCounters = new SparseArray<>();
}
/**
* 清空资源
*/
public void cancelAllTimers() {
if (countDownCounters == null) {
return;
}
Log.e("TAG", "size : " + countDownCounters.size());
for (int i = 0, length = countDownCounters.size(); i < length; i++) {
CountDownTimer cdt = countDownCounters.get(countDownCounters.keyAt(i));
if (cdt != null) {
cdt.cancel();
}
}
}
@Override
public int getCount() {
if (mDatas != null && !mDatas.isEmpty()) {
return mDatas.size();
}
return 0;
}
@Override
public Object getItem(int position) {
if (mDatas != null && !mDatas.isEmpty()) {
return mDatas.get(position);
}
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final ViewHolder viewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item_common2, parent, false);
viewHolder = new ViewHolder();
viewHolder.btn = (TextView) convertView.findViewById(R.id.btn);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
final TimerItem data = mDatas.get(position);
viewHolder.btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
data.setExpirationTime(System.currentTimeMillis() + 60 * 1000);
notifyDataSetChanged();
timerItemsSave.add(data);
}
});
//将当前item与保存起来的会议实体一一对比,如果会议id相同,则认为是同一个会议,那么去判断
//如果按钮还需要倒计时,那么将时间赋值过去,如果不需要就从保存的列表中移除
for (int i = 0; i < timerItemsSave.size(); i++) {
if (timerItemsSave.get(i).getName().equals(data.getName())) {
if (timerItemsSave.get(i).getExpirationTime() - System.currentTimeMillis() > 0) {
//如果时间差大于0,按钮还需要倒计时,赋值
data.setExpirationTime(timerItemsSave.get(i).getExpirationTime());
} else {
//如果时间差小于等于0,不需要倒计时,将保存的对象移除(避免一次次的保存对象导致校验耗费时间)
timerItemsSave.remove(timerItemsSave.get(i));
}
}
}
CountDownTimer countDownTimer = countDownCounters.get(viewHolder.btn.hashCode());
//将前一个缓存清除
if (countDownTimer != null) {
countDownTimer.cancel();
}
long timer = data.getExpirationTime();
// timer = timer - System.currentTimeMillis();
if (timer > 0) {
countDownTimer = new CountDownTimer(timer, 1000) {
public void onTick(long millisUntilFinished) {
viewHolder.btn.setText(millisUntilFinished/1000 +"");
Log.e("TAG", data.name + " : " + millisUntilFinished);
}
public void onFinish() {
viewHolder.btn.setText("开始倒计时");
}
}.start();
countDownCounters.put(viewHolder.btn.hashCode(), countDownTimer);
} else {
viewHolder.btn.setText("开始倒计时");
}
return convertView;
}
public class ViewHolder {
public TextView btn;
}
}
}
由于本身项目东西太多,因此就不贴出来而是写了demo。思路就是:点击按钮赋值同时保存对象,刷新数据的时候与保存的数据对比,需要倒计时的话就赋值。时间必须要用系统时间,可以避免倒计时数字错乱的问题。