views:

124

answers:

1

Long Story Short: a method of my activity updates and scrolls the ListView through an ArrayAdapter like it should, but a method of an internal TimerTask for polling messages (which are displayed in the ListView) updates the ListView, but don't scroll it. Why?

Long Story:

I have a chat activity with this layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="#fff"
    >
    <ListView android:id="@+id/messageList"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:stackFromBottom="true"
        android:transcriptMode="alwaysScroll"
        android:layout_weight="1"
        android:fadeScrollbars="true"
    />
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        >
        <EditText android:id="@+id/message"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
        />
        <Button android:id="@+id/button_send"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Send"
            android:onClick="sendMessage"
        />
    </LinearLayout>
</LinearLayout>

The internal listView (with id messageList) is populated by an ArrayAdapter which inflates the XML below and replaces strings in it.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:clickable="false"
    android:background="#fff"
    android:paddingLeft="2dp"
    android:paddingRight="2dp"
    >
    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/date"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:textSize="16sp"
     android:textColor="#00F"
     android:typeface="monospace"
     android:text="2010-10-12 12:12:03"
     android:gravity="left"
 />
 <TextView xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/sender"
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
     android:textSize="16sp"
     android:textColor="#f84"
     android:text="spidey"
     android:gravity="right"
     android:textStyle="bold"
 />
 <TextView xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/body"
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
     android:textSize="14sp"
     android:padding="1dp"
     android:gravity="left"
     android:layout_below="@id/date"
     android:text="Mensagem muito legal 123 quatro cinco seis."
     android:textColor="#000"
 />
</RelativeLayout>

The problem is: in the main layout, I have a EditText for the chat message, and a Button to send the message. I have declared the adapter in the activity scope:

public class ChatManager extends Activity{

 private EditText et;
 private ListView lv;
 private Timestamp lastDate = null;
 private long campaignId;
 private ChatAdapter ca;
 private List<ChatMessage> vetMsg = new ArrayList<ChatMessage>();
 private Timer chatPollingTimer;
 private static final int CHAT_POLLING_PERIOD = 10000;
...
}

So, inside sendMessage(View v), the notifyDataSetChanged() scrolls the ListView acordingly, so I can see the latest chat messages automatically:

 public void sendMessage(View v) {
  String msg = et.getText().toString();

  if(msg.length() == 0){
   return;
  }

  et.setText("");

  String xml = ServerCom.sendAndGetChatMessages(campaignId, lastDate, msg);
  Vector<ChatMessage> vetNew = Chat.parse(new InputSource(new StringReader(xml)));
  //Pegando a última data
  if(!vetNew.isEmpty()){
   lastDate = vetNew.lastElement().getDateSent();
   //Atualizando a tela
   vetMsg.addAll(vetNew);
   ca.notifyDataSetChanged();
  }
    }

But inside my TimerTask, I can't. The ListView IS UPDATED, but it just don't scroll automatically. What am I doing wrong?

 private class chatPollingTask extends TimerTask {
  @Override
  public void run() {
   String xml;

   if(lastDate != null){
    //Chama o Updater
    xml = ServerCom.getChatMessages(campaignId, lastDate);
   }else{
    //Chama o init denovo
    xml = ServerCom.getChatMessages(campaignId);
   }

   Vector<ChatMessage> vetNew = Chat.parse(new InputSource(new StringReader(xml)));
   if(!(vetNew.isEmpty())){
    //TODO: descobrir porque o chat não está rolando quando chegam novas mensagens
    //Descobrir também como forçar o rolamento, enquanto o bug não for corrigido.
    Log.d("CHAT", "New message(s) acquired!");
    lastDate =  vetNew.lastElement().getDateSent();
    vetMsg.addAll(vetNew);
    ca.notifyDataSetChanged();
   }

  }
 }

How can I force the scroll to the bottom? I've tried using scrollTo using lv.getBottom()-lv.getHeight(), but didn't work. Is this a bug in the Android SDK?

Sorry for the MASSIVE amount of code, but I guess this way the question gets pretty clear.

A: 

You need to invoke changes to the dataset (notifyDataSetChanged()) on the UI thread. The TimerTask gets invoked on a different thread. There are a number of ways to accomplish this, see this blog post for a list of ways.

Regarding scrollTo, that is not used to scroll in the since of scrolling a list. It scrolls the entire View element..not exactly what you'd expect. Instead, use one of the ListView.setSelection methods.

Mayra
I've tried creating a method in my activity before posting the question here just to try solving this "outside of main UI thread" problem, but it didn't work either.
Spidey
setSelection(vetMsgs.size()-1) didn't work either.
Spidey
Actually, now that I think about it I'm not sure why notifyDatasetChanged should necessarily cause the list to scroll to the bottom. It seems like you should just call ListView.setSelection(vetNew.size() -1) right after you call notifyDatasetChanged.
Mayra
That's EXACTLY what I'm doing, and not working right now. ... vetMsg.addAll(vetNew); ca.notifyDataSetChanged(); lv.setSelection(vetMsg.size()-1); ...I thought notifyDataSetChanged() would scroll it automatically because my ListView has stackFromBottom property set to true, and the sendMessage button, which does almost the same thing (post to a webservice and then retrieve the new state in XML format), works with just notifyDataSetChanged().
Spidey
Looking at the documentation from stackFromBottom...I think it just flips where item 1 is, doesn't affect the scrolling of the list. Have you tried setting TranscriptMode to be true? That claims to always scroll to show new items. I'm not sure why setSelection wouldn't work though, what behavior do you see when you call it? Nothing happens?
Mayra
Nothing happens. I'll try setSelection(0) since stackFromBottom reverses the items order.
Spidey
Mayra, since you help me out with the out of UI thread problem (which you were right in bringing it to the table), I'll just comment the solution. Please, update your answer so I can accept it, otherwise I'll post the answer myself after a couple days.Now to the solution, I had to implement a Handler and send a message, from the TimerTask instance, to the main activity, and then ca.notifyDataSetChanged() worked flawlessly.
Spidey