i've built a chat application.
inside my chat messages Activity, i have a ListView which shows all text message.
inside each text messages i have a TextView which will by default write "Sending..." and i want it to be updated after the message is sent.
inside each ChatMessage object i have a sent and time property. if sent is true i will show in the TextView the time, and if it's false i will write "Sending" as written above.
when i'm sending a new message, i'm adding a new view to the ListView adapter, and from some reason it shows that message was sent although it didn't... can't really understand why.
this is my ArrayAdapter's getView() and holder class:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
ChatMessage chatMessage = chatMessagesArrayList.get(position);
ChatMessageHolder chatMessageHolder = null;
if (row == null)
{
if (chatMessage.getSenderId() == app.getFacebookCurrentUser().getId())
row = LayoutInflater.from(context).inflate(layoutResourceId, null);
else
row = LayoutInflater.from(context).inflate(R.layout.chat_green, null);
chatMessageHolder = new ChatMessageHolder();
chatMessageHolder.message = (TextView) row.findViewById(R.activity_chat.message);
chatMessageHolder.sentTime = (TextView) row.findViewById(R.activity_chat.sent_time);
chatMessageHolder.isSent = chatMessage.isSent();
row.setTag(chatMessageHolder);
}
else
{
chatMessageHolder = (ChatMessageHolder) row.getTag();
}
chatMessageHolder.message.setText(chatMessage.getMessage());
if (chatMessageHolder.isSent)
chatMessageHolder.sentTime.setText(app.getDateTime(chatMessage.getTime()));
return row;
}
private static class ChatMessageHolder
{
TextView message, sentTime;
boolean isSent = false;
}
i can't really understand why doesn't it write the time or "Sending..." according to the isSent boolean flag...
It's because the ListView recycles previous views (that's what the convertView variable is). Likely it's recycling another view that was already set to "Sent", and you're never handling the alternate case where chatMessagerHolder.isSent is false. You should avoid setting chatMessageHolder.isSent = chatMessage.isSent(); until after that first if-else block. That first if-else should just be to initialize the view or recycle the object. Also, when you check if(chatMessageHolder.isSent) you should also handle what the TextView should say if that is false (i.e. in a followup else statement) since the views are recycled.
Related
I just started learning android and I'm at a point where I want to do the question described below but I'm not sure how to start.
I have an array of data with the following data,
1, text1, image1.png
2, text2, image2.png
3, text3, null
4, null, image3.png
I know how to create a ListView with ArrayAdapter along with their xml layout following some tutorial.
As you see in the array above sometimes it doesn't contain an image, sometimes it doesn't contain text and sometimes it has both.
My question is how to make that work with layout so that it dynamically changes based on the array values?
In other words how can I start thinking about building a listview+ArrayAdapter+layout where I can view an imageiew only where the array record has an image only, viewing a textview when there is a text only and viewing both of them when both are available.
A link to a tutorial will be extremely helpful
You could create a type MyCustomType that represents one array element (In your case it holds a number, a text and an image). Furthermore you need to implement your custom array adapter. This adapter uses an ArrayList of your MyCustomType.
public class CustomAdapter extends ArrayAdapter<MyCustomType> {
//...
private ArrayList<MyCustomType> foo;
public CustomAdapter(Context context, Activity bar, ArrayList<MyCustomType> foo) {
super(bar, R.layout.row, foo);
this.foo = foo;
mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
Override getViewTypeCount() to determine how many different kinds of rows you have. getItemViewType returns the kind of row that has to be displayed.
Your getView method could be similiar to his one:
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
int type = getItemViewType(position); // decide what row type has to be displayed
if (convertView == null) {
convertView = mInflater.inflate(R.layout.row, parent, false);
viewHolder = new ViewHolder();
viewHolder.number = //...
viewHolder.text = //...
viewHolder.image = //...
convertView.setTag(viewHolder);
}
else {
viewHolder = (ViewHolder) convertView.getTag(); // avoided to call findViewById
}
switch(type) {
case TYPE1:
//... you could just change the visibility of the corresponding view
break;
case TYPE 2:
// ...
break;
}
return convertView;
}
My advice is to use a custom array adapter. There is a good tutorial here.
The official documentation can be found here.
Essentially you will create a class that extends the ArrayAdapter class. Implement the overrides and put your handling to show or not show particular views in the getView method. This method will fire for each item in the list passed it.
After looking for some answers here, I find myself in a disturbing situation where my Listview is really getting on my nerve.
Here are the questions I looked for :
Maintain ListView Item State
How to save state of CheckBox while scrolling in ListView?
I'm using a custom adapter with a custom row as below.
My Listview is simple as it is displaying a custom row made of three elements :
1) an ImageView displaying contact picture cropped in a circle ;
2) a TextViewdisplaying the contact full name as plain text ;
3) and finally an ImageView that holds the purpouse of a CheckBox.
Please focus on the last element. The ImageView CheckBox-like will have its src changed upon click.
When the user click, the ImageView will switch between a check sign and an unchecked sign according to it's previous status. Possible status are : {checked | unchecked}
So far so good.
But as soon as I scroll the ListView, any aforementioned change will disappear as Android recycle unused view.
Here comes the so-called ViewHolder pattern. Unfortunately, this pattern is failling me on two issues :
First, when scrolling, my organized-in-an-alphabetical-order listview gets disorganized.
e.g. somehow, whitout any reason, the first displayed contact name gets displayed again later on the ListView as I scrolled. That can happen with any row ! So it would seem unused view are being wrongly re-used.
Second, and in accordance to the first issue, the checked status do seem to stay, but not always and if it does stay, it may very well stay on the wrong row ... and that can happen randomly, of course. Therefore ViewHoder is not a viable solution.
Before discouvering the ViewHolder pattern, I have been using a HashMap to store the item position upon click as followed :
ContactsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public final void onItemClick(final AdapterView<?> adapterView, final View view,
final int position, final long id) {
final ImageView check = (ImageView) view.findViewById(R.id.checkImage);
final TextView name = (TextView) view.findViewById(R.id.contactName);
final Boolean isChecked = Boolean.valueOf(checkedContactsList.isChecked(position));
if (isChecked != null && isChecked.booleanValue() == true) {
check.setImageDrawable(getActivity().getResources().getDrawable(R.drawable.unchecked_sign));
checkedContactsList.(position);
} else {
check.setImageDrawable(getActivity().getResources().getDrawable(R.drawable.checked_sign));
checkedContactsList.add(position, true);
}
}
});
I tried adding a different value instead of position.
I tried with ContactsListView.getPositionForView(view)
And I also tried with the View's ID, but still it doesn't work.
I wish I could use ContactsListView.getSelectedItemPosition() but it returns -1 as there is no selection event because I'm handling a touch/click event.
And this is how my Custom Adapter looks like :
public final View getView(final int position,
final View convertView, final ViewGroup parent) {
final LayoutInflater inflater = (LayoutInflater) this.context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final View contactRowView = inflater.inflate(R.layout.contact_row, parent, false);
final ImageView contactPic = (ImageView) contactRowView.findViewById(R.id.contactPic);
final TextView contactName = (TextView) contactRowView.findViewById(R.id.contactName);
final ImageView checkImage = (ImageView) contactRowView.findViewById(R.id.checkImage);
// the list is the same as above and therefore contains the exact same entries
if (this.checkedContactsList.isChecked(position))
checkImage.setImageDrawable(this.context.getResources().getDrawable(R.drawable.checked_sign));
contactPic.setImageBitmap(cropePictureInCircle(this.contacts.get(position).getPicture()));
contactName.setText(this.contacts.get(position).getName());
return contactRowView;
}
Is there a good way to keep the checked row checked and the unchecked row unchecked in the given alphabetical order ?
Thanks !
For the list position change I know the solution but for the second problem I am still searching for a solution, anyway first make a viewHolder class;
public class ViewHolder{
//put all of your textviews and image views and
//all views here like this
TextView contactName;
ImageView checkImage;
ImageView contactImage;
}
Then edit your adapter:
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
final View contactRowView = convertView;
ViewHolder holder;
if (contactRowView == null) {
final LayoutInflater inflater = (LayoutInflater)
this.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE
);
contactRowView =
inflater.inflate(R.layout.contact_row, parent,
false);
holder = new ViewHolder():
holder.contactPic = (ImageView)
contactRowView.findViewById(R.id.contactPic);
holder.contactName = (TextView)
contactRowView.findViewById(R.id.contactName);
holder.checkImage = (ImageView)
contactRowView.findViewById(R.id.checkImage);
contactRowView.setTag(holder);
} else {
holder = contactRowView.getTag();
// the list is the same as above and therefore contains the exact same entries
if (this.checkedContactsList.isChecked(position))
holder.checkImage.setImageDrawable(this.context.get.
Resources().getDrawable(R.drawable.checked_sign));
holder.contactPic.setImageBitmap(cropePictureInCircle(this.contacts.get(position).getPicture()));
holder.contactName.setText(this.contacts.get(position).getName());
return contactRowView;
}
}
Hope this helps and sorry because writing code from my phone is totally awkward.
I am working on android chat application. I am facing a problem in send and receive function using xmpp. I am able to send message from emulator to xmpp and receive message from xmpp. But i am facing issue in displaying the incoming and outgoing message in a list view. I am confused how to give the condition to set view layout.
if(message from xmpp) {
TextView textLabel = (TextView) row.findViewById(R.id.textb); // if message received dislay in left side textview
textLabel.setText(receiveddata); //receiveddata contains arraylist of incoming message
} else (message from me) {
TextView textLabel = (TextView) row.findViewById(R.id.texts); // if message sent by me dislay in right side textview
textLabel.setText(sentdata); //sentdata contains arraylist of outgoing message
}
Kindly tell me how can i do this.
Thanks
You can create an adapter class having two layout having same field. use if condition for incoming and outgoing messages. and inflate accordingly.
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
entry = list.get(position);
if (convertView == null) {
if (getItemViewType(position) == 0) {
convertView = inflator.inflate(
R.layout.messages_even_list_layout, null);
} else {
convertView = inflator.inflate(
R.layout.messages_odd_list_layout, null);
}
I have a ListView in a custom ArrayAdapter that displays an icon ImageView and a TextView in each row. When I make the list long enough to let you scroll through it, the order starts out right, but when I start to scroll down, some of the earlier entries start re-appearing. If I scroll back up, the old order changes. Doing this repeatedly eventually causes the entire list order to be seemingly random. So scrolling the list is either causing the child order to change, or the drawing is not refreshing correctly.
What could cause something like this to happen? I need the order the items are displayed to the user to be the same order they are added to the ArrayList, or at LEAST to remain in one static order. If I need to provide more detailed information, please let me know. Any help is appreciated. Thanks.
I was having similar issues, but when clicking an item in the custom list, the items on the screen would reverse in sequence. If I clicked again, they'd reverse back to where they were originally.
After reading this, I checked my code where I overload the getView method. I was getting the view from the convertedView, and if it was null, that's when I'd build my stuff. However, after placing a breakpoint, I found that it was calling this method on every click and on subsequent clicks, the convertedView was not null therefore the items weren't being set.
Here is an example of what it was:
public View getView(int position, View convertView, ViewGroup parent)
{
View view = convertView;
if (view == null)
{
LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = vi.inflate(R.layout.listitemrow, null);
RssItem rssItem = (RssItem) super.getItem(position);
if (rssItem != null)
{
TextView title = (TextView) view.findViewById(R.id.rowtitle);
if (title != null)
{
title.setText(rssItem.getTitle());
}
}
}
return view;
}
The subtle change is moving the close brace for the null check on the view to just after inflating:
public View getView(int position, View convertView, ViewGroup parent)
{
View view = convertView;
if (view == null)
{
LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = vi.inflate(R.layout.listitemrow, null);
}
RssItem rssItem = (RssItem) super.getItem(position);
if (rssItem != null)
{
TextView title = (TextView) view.findViewById(R.id.rowtitle);
if (title != null)
{
title.setText(rssItem.getTitle());
}
}
return view;
}
I hope this helps others who experience this same problem.
To further clarify the answer of farcats below in more general way, here is my explanation:
The vi.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 (view == 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 (view == null) statement.
Similarily, in other common implementation of this method, some textView, 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.
Another good example where this solution is applied for BaseAdapter appears in BaseAdapter causing ListView to go out of order when scrolled
The ListView reuses view objects when you scroll. Are you overriding the getView method? You need to make sure you set each property for every view, don't assume that it will remember what you had before. If you post that method, someone can probably point you at the part that is incorrect.
I have a ListView, AdapterView and a View (search_options) that contains EditText and 3 Spinners. ListView items are multiple copies of (search_options) layout, where user can add more options in ListView then click search to send sql query built according to users options.
I found that convertView mixing indecies so I added a global list (myViews) in activity and passed it to ArrayAdapter. Then in ArrayAdapter (getView) I add every newly added view to it (myViews).
Also on getView instead of checking if convertView is null, I check if the global list (myViews) has a view on the selected (position).. It totally solved problems after losing 3 days reading the internet!!
1- on Activity add this:
Map<Integer, View> myViews = new HashMap<>();
and then pass it to ArrayAdapter using adapter constructor.
mSOAdapter = new SearchOptionsAdapter(getActivity(), resultStrs, myViews);
2- on getView:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
ViewHolder viewHolder;
if (!myViews.containsKey(position)) {
viewHolder = new ViewHolder();
LayoutInflater inflater = LayoutInflater.from(getContext());
view = inflater.inflate(R.layout.search_options, parent, false);
/// ...... YOUR CODE
myViews.put(position, view);
FontUtils.setCustomFontsIn(view, getContext().getAssets());
}else {
view = myViews.get(position);
}
return view;
}
Finally no more mixing items...
I load data from Cursor to listview, but my Listview not really display "smooth". The data change when I drag up and down on the scollbar in my ListView. And some items look like duplicate display in my list.
I hava a "complex ListView" (two textview, one imageview) So I used newView(), bindView() to display data. Can someone help me?
I will describe you how to get such issue that you have. Possibly this will help you.
So, in list adapter you have such code:
public View getView(int position, View contentView, ViewGroup arg2)
{
ViewHolder holder;
if (contentView == null) {
holder = new ViewHolder();
contentView = inflater.inflate(R.layout.my_magic_list,null);
holder.label = (TextView) contentView.findViewById(R.id.label);
contentView.setTag(holder);
} else {
holder = (ViewHolder) contentView.getTag();
}
holder.label.setText(getLabel());
return contentView;
}
As you can see, we set list item value only after we have retrieved holder.
But if you move code into above if statement:
holder.label.setText(getLabel());
so it will look after like below:
if (contentView == null) {
holder = new ViewHolder();
contentView = inflater.inflate(R.layout.my_magic_list,null);
holder.label = (TextView) contentView.findViewById(R.id.label);
holder.label.setText(getLabel());
contentView.setTag(holder);
}
you will have your current application behavior with list item duplication.
Possibly it will help.
ListView is a tricky beast.
Your second question first: you're seeing duplicates because ListView re-uses Views via convertView, but you're not making sure to reset all aspects of the converted view. Make sure that the code path for convertView!=null properly sets all of the data for the view, and everything should work properly.
You'll want your getView() method to look roughly like the following if you're using custom views:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
final MyCustomView v = convertView!=null ? (MyCustomView)convertView : new MyCustomView();
v.setMyData( listAdapter.get(position) );
return v;
}
If you're not using your own custom view, just replace the call to new MyCustomView() with a call to inflater.inflate(R.layout.my_layout,null)
As to your first question, you'll want to watch Romain's techtalk on ListView performance here: http://code.google.com/events/io/sessions/TurboChargeUiAndroidFast.html
From his talk and in order of importance from my own experience,
Use convertView
If you have images, don't scale your images on the fly. Use Bitmap.createScaledBitmap to create a scaled bitmap and put that into your views
Use a ViewHolder so you don't have to call a bunch of findViewByIds() every time
Decrease the complexity of the views in your listview. The fewer subviews, the better. RelativeLayout is much better at this than, say, LinearLayout. And make sure to use if you're implementing custom views.
I'm facing this problem as well, but in my case I used threads to fetch the external images. It is important that the current executing thread do not change the imageView if it is reused!
public View getView(int position, View vi, ViewGroup parent) {
ViewHolder holder;
String imageUrl = ...;
if (vi == null) {
vi = inflater.inflate(R.layout.tweet, null);
holder = new ViewHolder();
holder.image = (ImageView) vi.findViewById(R.id.row_img);
...
vi.setTag(holder);
} else {
holder = (ViewHolder) vi.getTag();
}
holder.image.setTag(imageUrl);
...
DRAW_MANAGER.fetchDrawableOnThread(imageUrl, holder.image);
}
And then on the fetching thread I'm doing the important check:
final Handler handler = new Handler() {
#Override
public void handleMessage(Message message) {
// VERY IMPORTANT CHECK
if (urlString.equals(url))
imageView.setImageDrawable((Drawable) message.obj);
};
Thread thread = new Thread() {
#Override
public void run() {
Drawable drawable = fetchDrawable(urlString);
if (drawable != null) {
Message message = handler.obtainMessage(1, drawable);
handler.sendMessage(message);
}
}};
thread.start();
One could also cancel the current thread if their view is reused (like it is described here), but I decided against this because I want to fill my cache for later reuse.
Just one tip: NEVER use transparent background of item layout - it slows performance greatly
You can see the reocurring text in multiple rows if you handle it in the wrong way. I've blogged a bit about it recently - see here. Other than that you might want to take a look at ListView performance optimization. Generally it's because of the view reuse and I've seen it few times already.