This is my first question in Stack Overflow, I have been building a simple chat application on Android which loads the chat History from a online database and it will be displayed in a list view using a customised message adapter.
Here is the current state of the program:
Demo
However, the layout of the list items is not correct after the 6th chat message down the array list, and all the following chat messages are repeating the layout of the first 6 messages.
Here is the code for my adapter:
public class messageAdapter extends ArrayAdapter<chatMessage> {
private Activity activity;
private List<chatMessage> messages;
public messageAdapter(Activity context, int resource, List<chatMessage> objects) {
super(context, resource, objects);
this.activity = context;
this.messages = objects;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
LayoutInflater inflater = (LayoutInflater) activity.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
int layoutResource = 0; // determined by view type
chatMessage chatMessage = getItem(position);
int viewType = getItemViewType(position);
if (chatMessage.isMine()) {
layoutResource = R.layout.chat_bubble_right;
} else {
layoutResource = R.layout.chat_bubble_left;
}
if (convertView != null) {
holder = (ViewHolder) convertView.getTag();
} else {
convertView = inflater.inflate(layoutResource, parent, false);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
Log.d("ViewID", "generated");
}
//set message content
holder.message.setText(chatMessage.getContent());
return convertView;
}
#Override
public int getViewTypeCount() {
// return the total number of view types. this value should never change
// at runtime
return 2;
}
#Override
public int getItemViewType(int position) {
// return a value between 0 and (getViewTypeCount - 1)
return position % 2;
}
private class ViewHolder {
private TextView message;
public ViewHolder(View v) {
message = (TextView) v.findViewById(R.id.txt_msg);
}
}
And this is the method that I load chat messages into the Array List:
private class getChatHistory extends AsyncTask<DBConnection,Long,JSONArray> {
#Override
protected JSONArray doInBackground(DBConnection... params) {
return params[0].getChatHistory(userID);
}
#Override
protected void onPostExecute(JSONArray jsonArray) {
chatData = jsonArray;
if (chatData != null)
{
for (int i = 0; i < chatData.length(); i++)
{
JSONObject currentItem = null;
try
{
currentItem = chatData.getJSONObject(i);
int msgID = currentItem.getInt("MessageID");
String currentText = currentItem.getString("MessageContent");
int senderID = currentItem.getInt("SenderID");
int receiverID = currentItem.getInt("ReceiverID");
chatMessage currentMessage = new chatMessage(currentText, senderID, userID);
Log.d("Is Mine", Boolean.toString(currentMessage.isMine()));
messageHistory.add(currentMessage);
DBAdapter.notifyDataSetChanged();
} catch (JSONException e) {
e.printStackTrace();
}
}
}
}
}
And here is the JSONArray I obtained from the PHP file that I ran:
[{"MessageID":"1","SenderID":"1","ReceiverID":"8","Duration":"2.4","MessageContent":"agnblean liajiaj vliwv fla","MessageLength":"26","Status":"received","Date":"2016-04-04 14:00:00"},
{"MessageID":"2","SenderID":"8","ReceiverID":"1","Duration":"3.1","MessageContent":"akwuehrgeubwfcofawve","MessageLength":"20","Status":"received","Date":"2016-04-04 17:00:00"},
{"MessageID":"3","SenderID":"8","ReceiverID":"1","Duration":"3.1","MessageContent":"akwuehrgeubwfjurawve","MessageLength":"20","Status":"received","Date":"2016-04-04 17:00:05"},
{"MessageID":"4","SenderID":"8","ReceiverID":"1","Duration":"3.1","MessageContent":"akwuehrgeubwalwrawve","MessageLength":"20","Status":"received","Date":"2016-04-04 17:00:10"},
{"MessageID":"5","SenderID":"1","ReceiverID":"8","Duration":"3.1","MessageContent":"akwuehrgeubwalwrawve","MessageLength":"20","Status":"received","Date":"2016-04-04 17:01:10"},
{"MessageID":"8","SenderID":"1","ReceiverID":"8","Duration":"4.6","MessageContent":"vsjkgkgredjegwhkaga","MessageLength":"23","Status":"received","Date":"2016-04-05 05:00:00"},
{"MessageID":"9","SenderID":"8","ReceiverID":"1","Duration":"5.2","MessageContent":"agrlanwligna","MessageLength":"21","Status":"received","Date":"2016-04-06 00:00:00"},
{"MessageID":"10","SenderID":"8","ReceiverID":"1","Duration":"7.2","MessageContent":"akewgaughurawaarg","MessageLength":"12","Status":"received","Date":"2016-04-12 00:00:00"},
{"MessageID":"11","SenderID":"1","ReceiverID":"8","Duration":"7.2","MessageContent":"wgkakjrgnjange","MessageLength":"41","Status":"received","Date":"2016-04-15 00:00:00"},
{"MessageID":"12","SenderID":"1","ReceiverID":"8","Duration":"4.67","MessageContent":"yikes","MessageLength":"5","Status":"received","Date":"2016-04-21 00:00:00"},
{"MessageID":"13","SenderID":"8","ReceiverID":"1","Duration":"8.2","MessageContent":"iobanoine","MessageLength":"4","Status":"received","Date":"2016-04-30 00:00:00"}]
So I thought that this would produce the correct layout for the chat history, which the active user being user ID = 1, and all the message with sender ID = 1 should be on the right hand side of the list view, but instead I got this:
screenshot
This is the screenshot of the 5-8th element in the list view, but the 7th element is on the right hand side instead of being on the left hand side, and the later element keep on repeating the previous 6 element's pattern. I have checked the log for the convert view and it only shows up 6 times, is that in anyway related to this error? And how do I solve this problem of the adapter not locating the list item resource correctly?
EDIT : I have changed the override of the getItemViewType() into this
#Override
public int getItemViewType(int position) {
chatMessage chatMessage = getItem(position);
if (chatMessage.isMine()) {
return 0;
} else {
return 1;
}
}
And I have also removed the override method for getViewTypeCount, and changed the condition a bit in the getView() method:
int viewType = getItemViewType(position);
if (viewType==0) {
layoutResource = R.layout.chat_bubble_right;
} else {
layoutResource = R.layout.chat_bubble_left;
}
Now the chat message list is in normal order up to the 8th element, and then the order become incorrect again.
EDIT 2
I have trace the log for the number of list item generated (i.e. new items) and this is the result I get after scrolling down to the bottom of the list view:
04-06 19:23:54.894 11202-11202/com.example.user.normalinterface D/ViewID: generated
04-06 19:23:54.907 11202-11202/com.example.user.normalinterface D/ViewID: generated
04-06 19:23:54.912 11202-11202/com.example.user.normalinterface D/ViewID: generated
04-06 19:23:54.914 11202-11202/com.example.user.normalinterface D/ViewID: generated
04-06 19:23:56.850 11202-11202/com.example.user.normalinterface D/ViewID: generated
is this in anyway related to my problem? It seeems that all the subsequent record in the list view are repeating the pattern from the first 5 item in the list.
I think it come from the getViewTypeCount() method. Here you say that you have 2 different types of layout. And with the method getItemViewType() you say that the different types are one out of 2 messages. But you don't event use the type in the getView method.
Apparently you don't need those 2 types, so I would recommend that you just remove the 2 methods :
getViewTypeCount() and
getItemViewType()
I don't think you need them in you case.
EDIT :
You actually need those method, but you overrided getItemViewType badly.
#Override
public int getItemViewType(int position) {
chatMessage chatMessage = getItem(position);
if (chatMessage.isMine()) {
return 0;
} else {
return 1;
}
}
In my experience, but may not be relevant to this, you need to call notifyDataSetChanged() in the adapter, so set up a method called refreshData in the adapter like this:
public void refreshData(ArrayList<yourList> data) {
mValues.clear();
mValues.addAll(data);
notifyDataSetChanged();
}
and call from your activity:
mAdapter.refreshData(newData)
with the new data as an argument.
Also, make sure you reference the adapter class properly, so try adding to the beginning of your class:
private final DBAdapter mAdapter;
Then in getChaHistory add:
mAdapter = DBAdapter
The idea being that you have a correctly referenced handle to your adapter. Note the change to previous answer where you now call mAdapter.refreshData(newData) instead of DBAdapter.refreshData(newdata). Really I think you should pass the adapter to the class, which is what I do, but you can try this way and see how you get on?
This will work correctly, but won't be as fast. It decides which layout to instantiate depending on the value if "isMine()" If "isMine() is true then the layout which contains the right aligned TextView is instantiated. Otherwise, the other layout is instantiated, (where the TextView is left aligned).
#Override
public View getView(int position, View convertView, ViewGroup parent){
if(messages.get(position).isMine()){
item = ((Activity)context).getLayoutInflater().inflate(R.layout.chat_bubble_right,null);
}
else{
item = ((Activity)context).getLayoutInflater().inflate(R.layout.chat_bubble_left,null);
}
// set the text in the view.
((TextView) item.findViewById(R.id.chat_text)).setText(data.get(position).getMessage());
return item;
}
The chat_text id refers to the TextView within the instantiated layout. (Both layouts contain a TextView with the id chat_text).
I used this Google IO event as a resource:
https://www.youtube.com/watch?v=wDBM6wVEO70
I'm trying to add images in a ListView which has an ArrayAdapter. Fyi, the toList() is a conversion from iterator to a list of the given DBObject.
I override the View getView() and set a textview and an image.
private static class EventAdapter extends ArrayAdapter<DBObject> {
public EventAdapter(Context context, int resource, Iterable<DBObject> events) {
super(context, resource, toList(events));
}
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
LayoutInflater vi = LayoutInflater.from(getContext());
v = vi.inflate(R.layout.adapter_event_list, null);
DBObject event = getItem(position);
if (event != null) {
//Get the logo if any
if( ((DBObject)event.get("events")).containsField("logo") ){
String logoURL = ((DBObject)((DBObject)event.get("events")).get("logo")).get("0").toString();
ImageView eventLogo = (ImageView) v.findViewById(R.id.eventLogoList);
new setLogo().execute(logoURL, eventLogo);
}
TextView title= (TextView) v.findViewById(R.id.eventTitleList);
title.setText( ((DBObject)event.get("events")).get("title").toString() );
}
return v;
}
protected static <T> List<T> toList( Iterable<T> objects ) {
final ArrayList<T> list = new ArrayList<T>();
for( T t : objects ) list.add(t);
return list;
}
//setLogo() method here. See below
}
The text in the textview is fine. However the images are getting messed up. They seem to load in wrong places in the list. The route of the code is: 1)Get from the DB (async) 2)populate the ListView 3) while populating load each image(second async).
Here is the setLogo() AsyncTask which is inside the EventAdapter above:
private class setLogo extends AsyncTask<Object,Void,Bitmap>{
ImageView eventLogo = null;
#Override
protected Bitmap doInBackground(Object...params) {
try{
Bitmap eventImage = downloadBitmap((String) params[0]);
eventLogo = (ImageView) params[1];
return eventImage;
}
catch(Exception e){
e.printStackTrace();
return null;
}
}
#Override
protected void onPostExecute(Bitmap eventImage) {
if(eventImage!=null && eventLogo!=null){
eventLogo.setImageBitmap(eventImage);
}
}
}
I did so (using an Async) which I believe is the correct way to load images from urls. I saw this post on multithreading and from which I borrowed the downloadBitmap() method.
As explained above the images are loaded in wrong places of the ListView. What can be a robust way to load them?
Also the idea to pass the v.findViewById(R.id.eventLogoList) inside the AsyncTask is that the program will distinguish each adapter's ImageView but it seems it doesn't.
Update
After following the problem that is causing this mix I found this SO question.
I altered my code in order to check if the if is causing the problem.
//Get the logo if any
if( ((DBObject)event.get("events")).containsField("logo") ){
String logoURL = ((DBObject)((DBObject)event.get("events")).get("logo")).get("0").toString();
ImageView eventLogo = (ImageView) row.findViewById(R.id.eventLogoList);
//new setLogo().execute(logoURL, eventLogo);
TextView title= (TextView) row.findViewById(R.id.eventTitleList);
title.setText( "Shit happens" );
}
Let's say I have 40 items. The Shit happens is set on the fields that a logo field exists. If I scroll down/up the order changes and the text gets messed up. It is because the stack created inside the loop is small than the maximum of the list..I guess... I am still struggling.
PS: I found this easy library to load images asynchronously instead of DYI stuff.
Update 2
I added an else with a static url. Because of the time it take to the image to load they are still misplaced.
I would really go for a good library like Picasso.
It will handle all the hard part for you and it's very well written.
http://square.github.io/picasso/
I have a GridView showing some TextViews, the views are drawn so that ONE is marked as selected. The problem is that when the user interaction changes the selected item in my logic I want to update the views involved, namely the view showing previous selected item and the view showing the current selected item. (Of course in my real problem the child views in the grid are more complex as well as the update process which could involve some calculations, loading of resources, accessing databases, etc.)
Till now I'm using BaseAdapter.notifyDataSetChanged or GridView.invalidateViews and both do the work but also updates ALL the visible child views of the GridView but what I want is to update just TWO views among them.
How can I do that? How to get just the two views for the updating process?
Note: I'm facing this problem with ListViews also but I think maybe it will have the same solution.
Example code:
public class MainActivity extends Activity {
private int selectedPos = 36;
private class MyAdapter extends BaseAdapter {
#Override
public int getCount() {
return 100;
}
#Override
public Object getItem(int position) {
return position;
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView tv;
if(convertView==null) {
tv = new TextView(MainActivity.this);
} else {
tv = (TextView) convertView;
}
Log.d("Updating -----", String.valueOf(position));
if(position==selectedPos)
tv.setText("Item ".toUpperCase() + String.valueOf(position));
else
tv.setText("Item " + String.valueOf(position));
return tv;
}
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final GridView gv = (GridView) findViewById(R.id.gridView1);
gv.setAdapter(new MyAdapter());
gv.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
selectedPos = position;
//gv.invalidateViews();
((MyAdapter)gv.getAdapter()).notifyDataSetChanged();
}
});
}
}
What I'm looking for is something like (some kind of pseudo code):
updateThisViewAsNotSelected(selectedPos);
gv.redrawThisView(selectedPos);
selectedPos = position;
updateThisViewAsSelected(selectedPos);
gv.redrawThisView(selectedPos);
Recreating views all the time:
What exactly is the problem of updating the views when getView is called? If you do nothing but change the text of a TextView I can see no problem.
If you are planning to do something more you need to change your view on Adapters. See below.
Long running operations and adapters
You should never do any long running operations in the adapter itself. And not in getView at all.
You should begin thinking about Adapters as... well adapters. They adapt the data you have to the current UI context.
So it's not the job of the adapter to fetch data from the database or anything like that. The adapter should be fed data and simply adapt it into views.
For feeding the adapter with data consider using AsyncTask. It performs operations away from the UI threat and and can update the adapter when it is done.
You can for example let the Adapter have a List of objects that represent each view. Then to update the adapter you just need to provide it a new list of changed objects and notify it that the data has changed.
You need to traverse through the visible children of the list or grid view to achieve to that. For example, if you'd like to achieve that with a grid view:
for(int k = gridView.getFirstVisiblePosition(); k <= gridView.getLastVisiblePosition(); k++) {
View view = gridView.getChildAt(k);
// you can update the view here
}
If you'd like to get a child from a specific position:
int firstVisiblePosition = gridView.getFirstVisiblePosition();
for (int k = 0; k < gridView.getChildCount(); k++ ) {
int current = firstVisiblePosition + k;
if (current == updateThisPosition) {
View child = gridView.getChildAt(i);
// Update the view
TextView anything = (TextView) child.findViewById(R.id.anything_text);
anything.setText("updated!");
}
}
Hi I have an array adapter that is populated from an array like so:
private class PlacesDetailAdapter extends ArrayAdapter<PlaceDetail> {
private ArrayList<PlaceDetail> items;
public PlacesDetailAdapter(Context context, int textViewResourceId, ArrayList<PlaceDetail> items) {
super(context, textViewResourceId, items);
this.items = items;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(R.layout.placedetail_list_row, null);
}
PlaceDetail o = items.get(position);
if (!o.getType.equals("place") {
TextView tt = (TextView) v.findViewById(R.id.placeDetailTitle_Txt);
tt.setText(o.getName());
ImageView iv = (ImageView) v.findViewById(R.id.placeDetailIcon_Img);
iv.setImageBitmap(o.getImage);
}
return v;
}
}
The problem is that objects with type equal to place are still output as rows.
In addition I try to remove the objects from the array above the IF like so:
Iterator iter = m_orders.iterator();
while(iter.hasNext()){
PlaceDetail vs = (PlaceDetail)iter.next();
if(vs.getType().equals("place")) {
m_orders.remove(vs);
}
}
m_adapter.notifyDataSetChanged();
When I do this, I get a concurrent editing exception.
How I simply limit rows of a certain type from being displayed?
Thanks!
Answering your second question, you're not allowed to modify a list directly while an iterator is traversing it, as doing so could theoretically cause the iterator to fail in a number of ways (skip elements, get stuck in an infinite loop). There is, however, a remove() method on Iterator that will remove the last item returned from next(), which should do what you want here:
Iterator iter = m_orders.iterator();
while(iter.hasNext()){
PlaceDetail vs = (PlaceDetail)iter.next();
if(vs.getType().equals("place")) {
iter.remove();
}
}
m_adapter.notifyDataSetChanged();
Another way of approaching your problem might be to define a Cursor that traverses your list, only stopping at the items you want, and then using a CursorAdapter rather than ArrayAdapter. It's a bit more work, but has the advantage that it doesn't require any up-front processing, so might be a bit more responsive.
I'm having problems with some BaseAdapter code that I adapted from a book. I've been using variations of this code all over the place in my application, but only just realized when scrolling a long list the items in the ListView become jumbled and not all of the elements are displayed.
It's very hard to describe the exact behavior, but it's easy to see if you take a sorted list of 50 items and start scrolling up and down.
class ContactAdapter extends BaseAdapter {
ArrayList<Contact> mContacts;
public ContactAdapter(ArrayList<Contact> contacts) {
mContacts = contacts;
}
#Override
public int getCount() {
return mContacts.size();
}
#Override
public Object getItem(int position) {
return mContacts.get(position);
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
if(convertView == null){
LayoutInflater li = getLayoutInflater();
view = li.inflate(R.layout.groups_item, null);
TextView label = (TextView)view.findViewById(R.id.groups_item_title);
label.setText(mContacts.get(position).getName());
label = (TextView)view.findViewById(R.id.groups_item_subtitle);
label.setText(mContacts.get(position).getNumber());
}
else
{
view = convertView;
}
return view;
}
}
You are only putting data in the TextView widgets when they are first created. You need to move these four lines:
TextView label = (TextView)view.findViewById(R.id.groups_item_title);
label.setText(mContacts.get(position).getName());
label = (TextView)view.findViewById(R.id.groups_item_subtitle);
label.setText(mContacts.get(position).getNumber());
to be after the if/else block and before the method return, so you update the TextView widgets whether you are recycling the row or creating a fresh one.
To further clarify the answer of CommonsWare, here is some more info:
The li.inflate operation (needed here for parsing of the layout of a row from XML and creating the appropriate View object) is wrapped by an if (convertView == null) statement for efficiency, so the inflation of the same object will not happen again and again every time it pops into view.
HOWEVER, the other parts of the getView method are used to set other parameters and therefore should NOT be included within the if (convertView == null){ }... else{ } statement.
In many common implementation of this method, some textView label, ImageView or ImageButton elements need to be populated by values from the list[position], using findViewById and after that .setText or .setImageBitmap operations.
These operations must come after both creating a view from scratch by inflation and getting an existing view if not null (e.g. on a refresh).
Another good example where this solution is applied for a ListView ArrayAdapter appears in https://stackoverflow.com/a/3874639/978329