Android:在ListView中使用handler和runnable实现多个计时器。2个问题

3
我正在创建一个应用程序,其中包含两列的ListView。第一列应该显示倒计时,第二列显示额外的文本,解释倒计时的目的。 以下是我的代码,它运行得...或多或少。 我有一个带有多行的列表视图,计时器在滴答。 其中一个问题是: 我的runnable中的set.Text()似乎覆盖了所有行。例如,第一行的runnable也为第二行和第三行设置了文本,第二行的runnable也为第一行和第三行设置了文本,依此类推。这导致第一列闪烁(具有正确值和其他行的值)。 如何为ListView中特定的行设置文本?
下一个问题是: 即使我从处理程序中删除回调,runnable仍在继续运行。但是当活动处于后台或关闭时,不需要计时器滴答声,我也不想浪费系统资源。
我的Activity:
public class TimerActivity extends ListActivity {

MyTimerAdapter myTimerAdapter = null;
ArrayList<Long> timerList = new ArrayList<Long>();
ArrayList<String> textList = new ArrayList<String>();

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.listactivity);

    myTimerAdapter = new MyTimerAdapter(this, R.layout.row, R.id.tv_timer, R.id.tv_text);
    setListAdapter(myTimerAdapter);
}

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

@Override
protected void onPause() {
    myTimerAdapter.clear();
    myTimerAdapter.notifyDataSetChanged();
    super.onPause();
}

private void refreshView() {

    myTimerAdapter.clear();
    timerList.clear();
    textList.clear();

    // some code to read database and fill 
    // array timerList with a long value (used for displaying a countdown) 
    // and textList with some additional text

    myTimerAdapter.add(timerList, textList);
    myTimerAdapter.notifyDataSetChanged();
}

}

我的适配器:

public class MyTimerAdapter extends ArrayAdapter<String> {
private Activity mContext;
private ArrayList<Long> mTimer;
private ArrayList<String> mText;
private int mViewId;
private int mViewIdFieldTimer;
private int mViewIdFieldText;
private int listSize;
private ArrayList<Handler> handlerList = new ArrayList<Handler>();
private ArrayList<TimerRunnable> runList = new ArrayList<TimerRunnable>();

public MyTimerAdapter(Activity context, int textViewResId, int tv1, int tv2) {
    super(context, textViewResId);
    mContext = context;
    mViewId = textViewResId;
    mViewIdFieldTimer = tv1;
    mViewIdFieldText = tv2;
    listSize = 0;
}

public void add(ArrayList<Long> timer, ArrayList<String> text) {
    mTimer = timer;
    mText = text;
    listSize = mText.size();
    handlerList.clear();
    runList.clear();
}

@Override
public void clear() {
    super.clear();
    int i;
    for (i=0; i<listSize; i++) {
        handlerList.get(i).removeCallbacksAndMessages(runList.get(i));
        runList.get(i).stopHandler();
    }
}

@Override
public int getCount() {
    return listSize;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View v = convertView;
    if (v == null) {
        LayoutInflater vi = mContext.getLayoutInflater();
        v = vi.inflate(mViewId, null);
    }

    long timerLine = mTimer.get(position);
    if (timerLine != 0) {
        TextView tvTimer = (TextView) v.findViewById(mViewIdFieldTimer);

        if (tvTimer != null) {
            tvTimer.setTag(position);
            final Handler mTimerHandler = new Handler();
            TimerRunnable timerTask = new TimerRunnable(tvTimer, tvTimer.getTag().toString(), timerLine);
            mTimerHandler.post(timerTask);

            // save in array to stop later
            handlerList.add(mTimerHandler);
            runList.add(timerTask);
        }
    }

    String textLine = mText.get(position);
    if (textLine != null) {
        TextView tvText = (TextView) v.findViewById(mViewIdFieldText);
        if (tvText != null) {
            tvText.setText(textLine);
        }
    }

    return v;
}
}

我的可运行对象:

public class TimerRunnable implements Runnable {
private TextView tv;
final Handler mTimerHandler = new Handler();
String tag;
long endtime;
long sec;

public TimerRunnable (TextView tv, String tag, long endtime) {
    this.tv = tv;
    this.tag = tag;
    this.endtime = endtime;
}

public void run() {
    if (tv.getTag().toString().equals(tag)) {
        Calendar cal = Calendar.getInstance();
        sec = endtime - (cal.getTimeInMillis() / 1000); //endtime - aktuelle Zeit
        if (sec >= 0) {

            // some code formatting the time in seconds to something like hh:mm:ss (var String txt)

            tv.setText(txt);

            System.out.println(txt);  // only for tests; so I could see that runnable is still running

            mTimerHandler.postDelayed(this, 1000);
        } 
    }
}

public void stopHandler() {
    mTimerHandler.removeCallbacksAndMessages(null);
}
}
2个回答

5
我会按照以下步骤进行:

  1. Use a Timer (http://developer.android.com/reference/java/util/Timer.html) and let it run periodically every second or whatever.
  2. Create the Timer object in Activities onCreate() and cancel() the timer in Activities onDestroy() (or even better start it in onResume and cancel it in onPause()). So thats the simplest way how to avoid memory leaks and that your timer is running even if the activity is closed.
  3. update the ListView from the running timer. There are 2 options:

    • Simply call adapter.notifyDatasetChanged() from your timer. But this could lead to a "blinking" listview. However you could try and checkout if this could work in your application, since is the simplest implementation.
    • Update directly the TextViews that are currently visible in the ListView

      private void updateTime(){
      
      int firstVisibleItemIndex = listView.getFirstVisiblePosition();
      
      for (int i = 0; i < listView.getChildCount(); i++) {
          View v = listView.getChildAt(i);
      
              YourItem item = (YourItem)adapter
                      .getItem(firstVisibleItemIndex + i));
      
              ViewHolder vh = (ViewHolder) v.getTag();
                                  // Calculate the time somehow, i.e. call a methot on your data item
                                  vh.tvTimer.setText(item.getElapsedTime());
      
      
          }
      
      }
      

      }

所以我想第二个选项是最好的。你可能会想知道什么是ViewHolder。ViewHolder是一种提高ListView性能的模式。http://developer.android.com/training/improving-layouts/smooth-scrolling.html#ViewHolder 你的Adapter存在的问题是findViewById()方法会在每次用户滚动时被调用。findViewById()方法是一个昂贵的方法调用,因为它必须遍历所有的视图子元素以找到具有给定id的那个视图。在你的简单的Adapter中,它可能不会有太大的影响(因为你只有一个子视图,即TextView)。但ViewHolder是你应该在Adapter实现中始终使用的东西。


谢谢你,但是我的 ViewHolder 似乎不起作用。当我 setText() 时,通过 ViewHolder 获取的 TextViews 保持为空。我必须说这是我为 Android 编写的第一个应用程序之一。 - yvi
嗨,我现在使用findViewById()而不是ViewHolder,现在计时器和列表显示正常工作。谢谢! - yvi
为了节省资源,我在调用可运行构造函数时将所有TextView保存在一个ArrayList<TextView>中。在run()中,我只调用每个ArrayList项。 - yvi
"直接更新TextView" - 省了我的时间))) 谢谢 - NickUnuchek

1

以下是我的代码更改:

主活动:

public class TimerActivity extends ListActivity {

MyTimerAdapter myTimerAdapter = null;
ArrayList<Long> timerList = new ArrayList<Long>();
ArrayList<String> textList = new ArrayList<String>();
ViewHolder vh = new ViewHolder();
ListView lv = null;
View v = null;
Timer t = null;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.listactivity);
}

@Override
protected void onResume() {
    super.onResume();

    vh.tvTimer = (TextView) findViewById(R.id.tv_timer);
    vh.tvText = (TextView) findViewById(R.id.tv_text);
    lv = (ListView) findViewById(android.R.id.list);
    lv.setTag(vh);
    t = new Timer();

    myTimerAdapter = new MyTimerAdapter(this, R.layout.timer_row, lv, vh);
    setListAdapter(myTimerAdapter);

    refreshView();
}

@Override
protected void onPause() {
    myTimerAdapter.clear();
    myTimerAdapter.notifyDataSetChanged();
    super.onPause();
}

@Override
protected void onDestroy() {
    t.cancel();
    super.onDestroy();
}

private void refreshView() {

    myTimerAdapter.clear();
    timerList.clear();
    textList.clear();

    // some code to read database and fill 
    // array timerList with a long value (used for displaying a countdown) 
    // and textList with some additional text

    myTimerAdapter.add(timerList, textList);
    myTimerAdapter.notifyDataSetChanged();

    updateTime();
}

private void updateTime() {

        t.scheduleAtFixedRate(new TimerTask() {

            @Override
            public void run() {
                runOnUiThread(new Runnable() {
                    long sec;
                    long timerLine;
                    Calendar cal;
                    ViewHolder rvh;
                    View v;

                    @Override
                    public void run() {
                        if (lv.getChildCount() > 0) {
                        for (int i = 0; i < lv.getChildCount(); i++) {
                            v = lv.getChildAt(i);
                            rvh = (ViewHolder) v.getTag();
                            timerLine = timerList.get(i);
                            cal = Calendar.getInstance();
                            sec = timerLine - (cal.getTimeInMillis() / 1000);
                            if (sec >= 0) {

                                // some code formatting the time in seconds to something like hh:mm:ss (var String txt)

                                rvh.tvTimer.setText(txt);
                            }
                        }
                        }
                    }

                });
            }

        }, 0, 1000);
}
}

适配器:
public class MyTimerAdapter extends ArrayAdapter<String> {
private Activity mContext;
private ArrayList<Long> mTimer;
private ArrayList<String> mText;
private int mViewId;
private ViewHolder mViewHolder;
private ListView mListView;
private int listSize;

public MyTimerAdapter(Activity context, int textViewResId, ListView lv, ViewHolder vh) {
    super(context, textViewResId);
    mContext = context;
    mViewId = textViewResId;
    mViewHolder = vh; //(ViewHolder) lv.getTag();
    mListView = lv;
    listSize = 0;
}

public void add(ArrayList<Long> timer, ArrayList<String> text) {
    mTimer = timer;
    mText = text;
    listSize = mText.size();
}

@Override
public void clear() {
    super.clear();
}

@Override
public int getCount() { // wird benötigt, wenn die Daten nicht schon im Konstruktor mitgeliefert werden. Ansonsten wird getView nicht aufgerufen!
    return listSize;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View v = convertView;
    if (v == null) {
        LayoutInflater vi = mContext.getLayoutInflater();//LayoutInflater.from(mContext);
        v = vi.inflate(mViewId, null);
    }

    long timerLine = mTimer.get(position);
    if (timerLine != 0) {
        TextView tvTimer = mViewHolder.tvTimer;
        if (tvTimer != null) {
            tvTimer.setText("00:00:00");
        }
    }

    String textLine = mText.get(position);
    if (textLine != null) {
        TextView tvText = mViewHolder.tvText;
        if (tvText != null) {
            tvText.setText(textLine);
        }
    }

    return v;
}
}

ViewHolder类:

public class ViewHolder {
TextView tvTimer;
TextView tvText;
int position;
}

正如所述,这并不起作用。我的列表中的两行都是空的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接