Android ArrayAdapter not properly converting Views - android

I have a custom ArrayAdapter for a ListView that uses a custom row layout, defined separately in XML. The layout is just three TextViews and an ImageView, put together in a RelativeLayout. To improve performance, the adapter uses a ViewHolder system like the one described here to convert existing Views instead of inflating new ones. In ArrayAdapter.getView(), the adapter is supposed to bold or unbold the first TextView, depending on a boolean.
When I first open the app, all of the TextViews are properly bolded or unbolded. However, if I scroll to the bottom of the ListView, then scroll back to the top, all of the title TextViews are bold, even if they aren't supposed to be. I think it must have something to do with converting existing views that are already bold, but I can't figure out what it is. I've debugged the app with Android Studio, and it runs just like I think it should -- when I scroll back up, the adapter properly bolds/unbolds things in the debug window, but they all seem to be bold on the app.
One thing I have noticed is that if I change the textStyle attribute of the TextView to "bold," all the title TextViews are bold from the beginning, and never change. It's only if I remove textStyle or set it to "normal" that the TextViews are normal at the start.
Here's getView() in the ArrayAdapter:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
PostShell shell = postShellList.get(getCount() - 1 - position); //I stack my ListView backwards, so index 0 is at the bottom
ViewHolder holder;
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.row_layout, parent, false);
holder = new ViewHolder();
holder.firstLine = (TextView) convertView.findViewById(R.id.firstLine);
holder.secondLine = (TextView) convertView.findViewById(R.id.secondLine);
holder.thirdLine = (TextView) convertView.findViewById(R.id.thirdLine);
holder.image = (ImageView) convertView.findViewById(R.id.image);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.firstLine.setText(shell.postTitle);
if (shell.unread) {
holder.firstLine.setTypeface(holder.firstLine.getTypeface(), Typeface.BOLD);
} else {
holder.firstLine.setTypeface(holder.firstLine.getTypeface(), Typeface.NORMAL);
}
//convert other TextViews
}
My ViewHolder class is just a static class with a few TextViews and an ImageView.
And here's the relevant part of the code for the row layout I'm using:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="88dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="84dp"
android:paddingRight="16dp"
android:paddingTop="6dp"
android:id="#+id/firstLine"
android:ellipsize="end"
android:text="First"
android:textStyle="normal"
android:singleLine="true" />
<!-- other views -->
</RelativeLayout>

The problem seems to be that "unboldening" the text does not work with the following statement:
holder.firstLine.setTypeface(holder.firstLine.getTypeface(), Typeface.NORMAL);
The following snippet leaves out the holder.firstLine.getTypeface() and just uses a simpler variety of setTypeface(). Worked for me.
if (shell.unread) {
holder.firstLine.setTypeface(holder.firstLine.getTypeface(), Typeface.BOLD);
} else {
holder.firstLine.setTypeface(Typeface.DEFAULT);
}

PostShell shell = postShellList.get(getCount() - 1 - position); //I stack my ListView backwards, so index 0 is at the bottom
If the ListView asks for the element at a particular position, you'd better give it the element at that position or it's going to be confused. IF you want to alter the order of the items in the list, change the order of the list, don't try to trick ListView into rendering it in a different order.

You should store your views in the normal order and use android:stackFromBottom or setStackFromBottom().

Related

Effective ArrayAdapter

First of all, my problem:
My ListView woun't scroll smoothly.
Now a bunch of details:
I'm currently using an ArrayAdapter<CustomClass> in my App for displaying Text and and Image in each element of the ListView. I've been trying to make the ListView to scroll as smooth as possible. But as soon as the text becomes longer (about 40 characters), the ListView starts to stutter when scrolling.
I am displaying about 9 rows at the same time. If I make the ListView smaller (about 6 rows) it works fine..
I am not implementing onScrollListener and I am not running big background tasks.
This is the code I'm currently using (only getView and Holder):
#Override
public View getView(int position, View convertView, ViewGroup parent) {
//View row = convertView;
Holder holder = null;
if(convertView == null){
//Log.e("adapter", "convertview == null");
LayoutInflater inflater = ((Activity)context).getLayoutInflater();
convertView = inflater.inflate(layoutResourceId, parent, false);
holder = new Holder();
holder.imgIcon = (ImageView)convertView.findViewById(R.id.icon);
holder.txtTitle = (TextView)convertView.findViewById(R.id.folder_name);
holder.txtInfo = (TextView)convertView.findViewById(R.id.info_text);
holder.pBar = (ProgressBar)convertView.findViewById(R.id.pBar);
convertView.setTag(holder);
}else{
holder = (Holder)convertView.getTag();
}
TrackInfo tInfo = data.get(position);
if(tInfo == null){
return convertView;
}
holder.imgIcon.setImageResource(icon);
holder.txtTitle.setText(tInfo.getTitle());
return convertView;
}
static class Holder
{
ImageView imgIcon;
TextView txtTitle;
TextView txtInfo;
ProgressBar pBar;
}
You may notice there are more elements than I actively use. This is due to the reason that I normally use the others, too, but I am currently ignoring them since I was trying to find out why it's not scrolling smoothly.
As already mentioned, it seems to be the length of the string tInfo.getTitle(). I can't change the length of the strings, since those are filenames I can't influence.
Now my QUESTION:
What's the problem? Is it that much data to handle? Or is my code bad?
I'm testing on a Moto G (1.2GHz Quad-Core, more details here).
Thank you for your attention, have a good flight!
I was working with marquee effect in the ListView. I always thought that as long as I don't call TextView.setSelected(true), this wouldn't cause any further processing. Therefore, I had android:ellipsize="marquee" as a parameter for my TextView in the layout-file of the ListView-element, while only one to-be-highlighted element was set selected.
Apparently, I was wrong.
As long as the text wasn't too long for the given space (about 40 characters), there was no problem. But if the size of the text exceeded the given space, the problems started.
I am not sure what exactly the problem is, but after having a look into the source of the TextView, I recognized that there is a lot more to do when marquee is enabled:
The TextView is faded out on the right side (instead of ...)
A Spannable is used as a CharSequence
It needs to be checked if marquee should start
...
So long story short:
I removed marquee and ListView scrolls very smoothly.

Erratic behaviour of listview (Android)

In my listview I have a custom Adapter, which I build using a TreeMap, I defined the getView method which is as follows. I am trying to strike out the text in a certian textview of the listview on click, only those textviews will be striken off which contain a certain pattern of characters (y#y). However on clicking one row in the listview I am getting strike out effect on some other row.
public View getView(int position, View convertView, ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (convertView == null) {
convertView = inflater.inflate(R.layout.chklistlayout, parent, false);
}
TextView textView = ((TextView) convertView.findViewById(R.id.textView1));
TextView imageview = ((TextView) convertView.findViewById(R.id.textView2));
textView.setText(values[position]);
imageview.setText(mValues[position]);
String s = mValues[position];
if (s.contains("y#y")) {
System.out.println("In if of getview");
System.out.println(s);
imageview.setPaintFlags(imageview.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
} else {
}
return convertView;
}
}
I tried using a holder pattern too, using a static holder class, but the problem seems to persist. Any pointers?
this answer is half mine and half from Muhammad Babar that we both commented on the question making together a quite nice answer:
use else block to handle this, imageview.setPaintFlags() not to strike
that happens
Because of the convertView. When you scroll the list the same view
that was used before is give back to getView method. So the same
imageView that you painted with StrikeThrough before is still painted
with it. So you have to revert that action.
more over, because of this recycling, usually dealing with Adapters you must ALWAYS undo/revert/repaint/change text/change image to all the elements to make sure it will be on screen the way you want.
ps.: now you should apply a Holder pattern to get better performance.

It is possible to implement a ListView without XML files?

I'm trying to implement a ListView on my app, but i'm trying to understand and learn how to achieve it without using XML files, all with java code.
I'm stuck on the inflater part, the mInflater.inflate(); function needs a resource xml file, so, i didn't understand how to continue without using XML files
I have a ArrayList of strings, and I simply need a ListView that shows a list with these strings of the Arraylist, and a delete Button on the right of the String. If the user press the Button, the item of the List get's deleted.
Each item of the ListView has two things, a TextView with the String of the ArrayList and a Button to delete it.
If someone can give me code examples i will be grateful.
Thanks
I totally agree with CommonsWare. But the part that you are stuck is the part that you have to create a row "template" for the ListView rows. The inflater is used so as to make a single View out of a complete layout.xml file. So the basic idea is that you create an xml that represents each row and then inflate it through that piece of code.
In your situation, you need to do that through code. Perhaps add a LinearLayout as a parent with orientation=vertical add some width or height properties and then add 2 TextViews so as to be a title and a subtitle with some additional properties. Then you should add them to the LinearLayout and you there you go.
Your LinearLayout is a pile of Views that are dynamically created and have the same effect as inflating all the above code through an xml file.
But I reaaaaally don't see the point in creating such a fuss over a much faster, easier, straight forwarded, better implemented and not to mention best practice...
EDIT: Somewhere inside your adapter you have: mInflater.inflate(); with the resource that you mention. As I previously said the resource determines how the "template" for each row would be. So a normal xml file that will determine a list row would be something like this:
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content" android:orientation="vertical" >
<TextView ... /> <! --some properties you want to set -->
<TextView ... /> <! --some properties you want to set -->
</LinearLayout>
This xml produces a 2 line list row for a ListView. With the layout inflater the above xml file returns a View object that contains all the bundle.
So if you want to create it from code, then the snippet would be:
LinearLayout layout = new LinearLayout(context);
//layout set some properties
TextView title = new TextView(context);
//title set some properties
TextView subtitle = new TextView(context);
//subtitle set some properties
layout.add(title);
layout.add(subtitle);
Now instead of inflating the xml to get the contents into a single View object, you have the layout variable in the code snippet that contains all the logic that was previously inflated through the xml.
If you have created a custom ListView adapter before then you should be familiar with creating a custom list row and how it works.
EDIT: sample code for the adapter of the ListView
this is the standart procedure of getView() method of the adapter by inflating a single layout:
#Override
public View getView(int position, View view, ViewGroup viewgroup) {
ViewHolder holder; //our view holder of the row
if (view == null) {
view = context.getLayoutInflater().inflate(R.layout.static_layout, null);
holder = new ViewHolder();
//set the views of the holder
view.setTag(holder);
} else {
holder = (ViewHolder) view.getTag();
}
//rest of implementation of the View
return view;
}
dynamic implementation:
#Override
public View getView(int position, View view, ViewGroup viewgroup) {
ViewHolder holder; //our view holder of the row
if (view == null) {
LinearLayout layout = new LinearLayout(context);
//layout set some properties
TextView title = new TextView(context);
//title set some properties
TextView subtitle = new TextView(context);
//subtitle set some properties
layout.add(title);
layout.add(subtitle);
//CREATING THE LAYOUT THROUGH CODE
view = layout; //INSTEAD OF INFLATING A LAYOUT FOR THE ROW I JUST BINDED IT TO THE RECENTLY CREATED LAYOUT
holder = new ViewHolder();
//bind the views of the holder to the views of the layout
view.setTag(holder);
} else {
holder = (ViewHolder) view.getTag();
}
//rest of implementation of the View
return view;
}

ListView and rows recycling problem

I have this ListView:
I'm using a custom adapter.
As you see, each row is made of a checkbox, a big TextView, and a little TextView.
All items have defined the little TextView, even the "Item 2" but it's a void string.
The problem comes when I tap the EditText placed in the header of the list:
The keyboard appears, and the rows are recycled, so the getView method of my adapter is called. In that method I have an if clause where I check if the length of the "optional" text (the little TextView) is greater than 0. In that case I make some room (the space that you can see in the screenshot) and I display it.
The problem is that "Item 2" has the "optional" text initialized but it's void (0-sized). I don't understand why the if clause is executed. But more strangely... the else is also executed! In the else I just show a void string in the little TextView.
Why is this happening? The app is really simple. This is my getView method:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = vi.inflate(R.layout.list_row, null);
}
ListItem list_item = items.get(position);
TextView item_title = (TextView) convertView.findViewById(R.id.item_title);
TextView item_optional = (TextView) convertView.findViewById(R.id.item_optional);
item_title.setText(list_item.getTitle());
// If the task has an optional text, make some room and display it
if (list_item.hasOptional()) {
// This portion of code will be executed when you tap the EditText and the keyboard appears, putting the item up in the row
LayoutParams layout_params = (LayoutParams) item_title.getLayoutParams();
layout_params.topMargin = 10;
layout_params.height = -2; // -2: wrap_content
item_title.setLayoutParams(layout_params);
item_optional.setText(list_item.getOptional());
item_optional.setVisibility(0);
} else {
// This portion of code will ALSO be executed when you tap the EditText... why? this should not happen!
item_optional.setText("");
}
return convertView;
}
The source code can be seen here (github).
When you modify a recycled view you have no idea what the state of the view is, with respect to how it might have been customized by previous calls to getView. The view you are recycling is not a fresh-out-the-box inflation of R.layout.list_row. Think of it as a "second hand" or "used" view.
So I can see under if (list_item.hasOptional().. you make some modification to the item_title.getLayoutParams(). As a view created here may later be recycled for a list item that will fail the check if (list_item.hasOptional() under the else code block you must reset the values you modify to the default specified in the layout.

Custom list adapter repeats entries

I am trying to create a ListView that will be populated with the entries from an array.
So this is my item layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="60dip" >
<ImageView android:id="#+id/list_item_image"
android:layout_height="wrap_content"
android:padding="2dip"
android:layout_gravity="center_vertical|center_horizontal"
android:layout_width="50dip"/>
<TextView android:id="#+id/list_item"
android:layout_height="fill_parent"
android:textSize="25sp"
android:layout_width="fill_parent"
android:ellipsize="marquee"
android:gravity="center_vertical"
android:padding="5dip" >
</TextView>
</LinearLayout>
I tried changing the layout_height of the LinearLayout but I ran into some problems. If I keep the height at wrap_content, my list is displayed with the correct entries -- Item 1, Item 2, Item 3, and so on until Item 12. However if I change the height to 60dip, the entries repeat after the sixth entry (I get Item 1, Item 2, Item 3, Item 4, Item 5, Item 6, Item 1, Item 2, Item 3...). If I keep on making it larger, the entries repeat more frequently.
This is a snippet from the ListAdapter where I set the list entries:
public View getView(int position, View convertView, ViewGroup parent) {
LinearLayout layout;
if (convertView == null){
layout = (LinearLayout) LayoutInflater.from(mContext).inflate(R.layout.items_list_item, parent, false);
TextView title = (TextView) layout.findViewById(R.id.list_item);
title.setText(menuItems[position]);
ImageView icon = (ImageView) layout.findViewById(R.id.list_item_image);
int logo = getResources().getIdentifier(menuIcons[position], "drawable", getPackageName());
icon.setImageResource(logo);
} else {
layout = (LinearLayout) convertView;
}
return layout;
}
Anybody else encountered this problem? I do not understand what is going on since I thought it should be straight-forward grabbing from the array.
EDIT: included the whole of my getView() method. Pardon the ugly way of getting the icons, I haven't figured it out yet,
You didn't post enough code, but in your Adapter's getView(...) try to make use of the convertView.
public View getView(int position, View convertView, ViewGroup parent){
if(convertView == null){
convertView = mInflater.inflate(R.layout.my_listitem_row, parent, false);
}
//...fill the TextViews on your layout
return convertView;
}
Fetching the icons should be as easy as
icon.setImageResource(R.drawable.my_icon); //the res/drawable folder has the my_icon.png file
From the little snippet of code I'm going to guess that it's something to do with the views being reused and the text not getting updated. I'm not certain though without seeing all of the code for the ListAdapter.
Take a look at this session from Google I/O 2010 for loads of really helpful information on how to use ListViews (and by extension adapters). It contains lots of tips and advice on the best way of using them. If you have time watch the video, if not the slides are avaliable.
Good Luck :)

Categories

Resources