Simple VIewHolder pattern implementation bug using convertView - android

I'm trying to implement the ViewHolder pattern with convertView. Two questions:
1) When I comment lines #1 and #2 (which are required for the pattern) everything works fine. When the if is in place everything gets scrambled, the first element of the list gets shown twice (in the beginning and in the end of the list) and after some orientation changes and list scrolling everything gets jumbled. Why is this happenning?
2) I'm using a ListActivity and providing the TextView and array of Strings for the ArrayAdapter (#3) but for some reason I still need line (#4) otherwise the list items are blank. Is this because i'm not using super.getView()?
class SushiAdapter extends ArrayAdapter<String> {
private final Activity context;
SushiAdapter(Activity context) {
super(context, R.layout.row, R.id.label, MenuItems); // #3
this.context = context;
}
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
if (row == null) { //#1
LayoutInflater inflater = context.getLayoutInflater();
row = inflater.inflate(R.layout.row, parent, false);
ViewHolder holder = new ViewHolder();
holder.icon = (ImageView) row.findViewById(R.id.icon);
holder.position = position;
holder.item = (TextView) row.findViewById(R.id.label);
row.setTag(holder);
} // #2
ViewHolder newHolder = (ViewHolder) row.getTag();
newHolder.item.setText(MenuItems[newHolder.position]); // #4
newHolder.icon.setImageResource(R.drawable.sushi);
return row;
}
}

If you are creating a new instance of array adapter, for example:
ArrayAdapter<T> sushiAdapter = new ArrayAdapter<T>(context, R.layout.row, R.id.label, MenuItems)
this instance already has it's own implementation of getView method. So it's obvious that in your case when override getView, you provide the complete implementation by yourself and take all responsibilities for filling views with content.
So if you want to add add something new, please call super.getView() before.
But in your case, when you have Image + text view you need to extend BaseAdapter and provide all implementation by yourself. ArrayAdapter provides simple functionality and extending it is not a common practice.

Related

how to make android listView redrawing only newly inserted rows

I use listView with BaseAdapter
I initial put 50 items in adapter
Then when the list is scrolled up and reach row 2, I load 50 more
I execute adpater.notifyDatasetChanged() when I add more items.
Then I do listView.setSelection(50)
However, my issue is android is re-creating view for all 100 rows.
But I only want the new 50 rows to be drawn from 0 - 50
If I will have 1000 rows, UI will be very slow.
Is it possible to draw partial rows?
You should implement a view holder pattern in your adapter. Here's an example of the Adapter.getView method from one of my apps that implements this pattern:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if (convertView == null) {
final LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(R.layout.list_item_feeling, parent, false);
viewHolder = new ViewHolder();
viewHolder.imageViewFeeling = (ImageView) convertView.findViewById(R.id.imageview_feeling);
viewHolder.textViewFeeling = (TextView) convertView.findViewById(R.id.textview_feeling);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
final Feeling feeling = getItem(position);
viewHolder.imageViewFeeling.setImageResource(feeling.getDrawable());
viewHolder.textViewFeeling.setText(feeling.getLabel());
return convertView;
}
The ViewHolder class is simple and just holds references to the views:
private static class ViewHolder {
ImageView imageViewFeeling;
TextView textViewFeeling;
}
The idea is that the inflate and findViewById methods are heavy and if you already have a View there is no need to recreate it.
You might also consider replacing the ListView with a RecyclerView which forces the view holder pattern.

What are the conditions under which an adapter doesn't update attached view

I am using this library to create swipe-able cards : https://github.com/Diolor/Swipecards
The view which makes the swipe-able cards control, gets attached to an adapter and sources it's drawing from it.
In my implementation, every card has a button, and when it is clicked, something in the source array changes, for which I want to refresh the whole card list. I call notifyDataSetChanged() on the associated adapter, but the getView() never gets called in the adapter to see any updates.
What's strange is that the same adapter works perfectly with a ListView
Is there any specific requirement either in the adapter's side or in the view itself which is required for the proper functioning of notifyDataSetChanged?
My Code:
(Please ignore the absence of ViewHolder pattern and the presence of click receivers inside the adapter. Code quality is the least thing I can be concerned about right now when a crucial functionality isn't working)
Adapter (using Array Adapter)
public class TourCardAdapter extends ArrayAdapter<TourCardBean> implements View.OnClickListener {
Context context;
ToursFragment.ToursControlsClickListener clickListener;
public TourCardAdapter(Context context, ArrayList<TourCardBean> tourCardsArr, ToursFragment.ToursControlsClickListener clickListener) {
super(context, 0, tourCardsArr);
this.context = context;
this.clickListener = clickListener;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View rowView = convertView;
ViewHolder viewHolder;
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
TourCardBean tourCard = getItem(position);
rowView = inflater.inflate(R.layout.card_tour, parent, false);
viewHolder = new ViewHolder();
viewHolder.title = (TextView) rowView
.findViewById(R.id.title);
viewHolder.detail = (TextView) rowView
.findViewById(R.id.detail);
viewHolder.likeCount = (TextView) rowView
.findViewById(R.id.likeCount);
viewHolder.image = (ImageView) rowView
.findViewById(R.id.cardLocationImage);
viewHolder.likeButton = (ImageView) rowView
.findViewById(R.id.cardLikeImage);
viewHolder.shareButton = (ImageView) rowView
.findViewById(R.id.cardShareImage);
viewHolder.likeButton.setOnClickListener(this);
viewHolder.shareButton.setOnClickListener(this);
viewHolder.title.setText(tourCard.getTitle());
viewHolder.detail.setText(tourCard.getDetails());
viewHolder.likeCount.setText("" + tourCard.getLikeCount());
viewHolder.likeButton.setTag(tourCard.getId());
viewHolder.shareButton.setTag(tourCard.getId());
return rowView;
}
#Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.cardLikeImage:
clickListener.onLikeClick((int) v.getTag());
break;
case R.id.cardShareImage:
clickListener.onShareClick((int) v.getTag());
break;
}
}
/**
* View Holder for ListView
*
* #author Aman Alam
*/
class ViewHolder {
public ImageView image;
public ImageView likeButton;
public ImageView shareButton;
public TextView title;
public TextView detail;
public TextView likeCount;
}
}
If it works with the ListView, your Adapter shouldn't be the problem.
The SwipeFlingAdapterView is (more or less) directly based on AdapterView which doesn't call getView() at all. So it's its responsibility to make a call to getView() when the dataset was changed. The relevant portion of the code seems to be, which might be blocked by your click event:
if (this.flingCardListener.isTouching()) {
PointF lastPoint = this.flingCardListener.getLastPoint();
if (this.mLastTouchPoint == null || !this.mLastTouchPoint.equals(lastPoint)) {
this.mLastTouchPoint = lastPoint;
removeViewsInLayout(0, LAST_OBJECT_IN_STACK);
layoutChildren(1, adapterCount);
}
}
Overall the SwipeFlingAdapterView doesn't seem to be prepared for a dataset change event at all.
Based on some of the issues logged against the Swipecards library, it appears that it may have bugs that prevent it from updating the views on notifyDataSetChanged(). This one has a couple of workarounds that might work for you. Specifically, flingContainer.removeAllViewsInLayout().
Libraries can be very useful, but I think it is wise you use the support library offered by google.
You get the benefits of getting the latest updates in material design as soon they are released.
You can declare in your gradle file. this;
compile 'com.android.support:design:22.2.0'

Recycling views in custom array adapter: how exactly is it handled?

I am having an unclear issue concerning the recycling of views in a getView method of a custom array adapter.
I understand that elements are reused, but how do I know exact what to implement in the first part of the if statement, and what in the second?
Right now I am having following code. I came to this question due to dropping the code in the second part of the statement which results in a list of the first 9 elements, which are repeated numberous times instead of all elements. I didn't really know what is causing this exactly...
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
if (row == null) {
LayoutInflater inflater = ((Activity) context).getLayoutInflater();
row = inflater.inflate(layoutResourceId, parent, false);
title = getItem(position).getTitle();
size = calculateFileSize(position);
txtTitle = (TextView) row.findViewById(R.id.txtTitle);
tvFileSize = (TextView) row.findViewById(R.id.tvFileSize);
txtTitle.setText(title);
tvFileSize.setText(size);
} else {
title = getItem(position).getTitle();
size = calculateFileSize(position);
txtTitle = (TextView) row.findViewById(R.id.txtTitle);
tvFileSize = (TextView) row.findViewById(R.id.tvFileSize);
txtTitle.setText(title);
tvFileSize.setText(size);
}
return row;
}
It's easy. The first time no row is created, so you have to inflate them. Afterwards, the Android os may decide to recycle the views that you already inflated and that are not visible anymore. Those are already inflated and passed into the convertView parameter, so all you have to do is to arrange it to show the new current item, for example placing the right values into the various text fields.
In short, in the first part you should perform the inflation AND fill the values, in the second if (if convertView != null) you should only overwrite the field because, given the view has been recycled, the textviews contain the values of the old item.
This post and this are good starting points
I understand that elements are reused, but how do I know exact what to implement in the first part of the if statement, and what in the second?
The organization is quite simple once you get the hang of it:
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
/* This is where you initialize new rows, by:
* - Inflating the layout,
* - Instantiating the ViewHolder,
* - And defining any characteristics that are consistent for every row */
} else {
/* Fetch data already in the row layout,
* primarily you only use this to get a copy of the ViewHolder */
}
/* Set the data that changes in each row, like `title` and `size`
* This is where you give rows there unique values. */
return convertView;
}
For detailed explanations of how ListView's RecycleBin works and why ViewHolders are important watch Turbo Charge your UI, a Google I/O presentation by Android's lead ListView programmers.
You want to create a ViewHolder class in your MainActivity. Something like
static class ViewHolder
{
TextView tv1;
TextView tv2;
}
then in your getView, the first time you get your Views from your xml in the if and reuse them after that in the else
View rowView = convertView;
if (rowView == null)
{
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
rowView = inflater.inflate(R.layout.layout_name_to_inflate, parent, false);
holder = new ViewHolder();
holder.tv1= (TextView) rowView.findViewById(R.id.textView1);
holder.tv2 = (RadioGroup) rowView.findViewById(R.id.textView2);
rowView.setTag(holder);
}
else
{
holder = (ViewHolder) rowView.getTag();
}
I would recommend that you use the View holder and convertview pattern to create your listView as it will be more efficient.Here is a good explanation of how it works with a re-use strategy. This will answer your question on how re-cycling works. If you want to refer to a code sample, I have it on GitHub.
Hope this helps.
The last part of the question I really couldn't grasp without a picture of the effect but for the first part "what to implement in the first part of the if statement, and what in the second" I think I've found the this implementation very common.
You would find the view references first and store them to a static class ViewHolder which then you attach to the tag of the new inflated view. As the listview recycles the views and a convertView is passed getView you get the ViewHolder from the convertView's tag so you don't have to find the references again (which greatly improves performance) and update the view data with that of your object at the position given.
Technically you don't care what position the view was since all you care for is the references to the views you need to update which are held within it's ViewHolder.
#Override
public View getView(int position, View convertView, ViewGroup container) {
ViewHolder holder;
Store store = getItem(position);
if (convertView == null) {
convertView = mLayoutInflater.inflate(R.layout.item_store, null);
// create a holder to store references
holder = new ViewHolder();
// find references and store in holder
ViewGroup logoPhoneLayout = (ViewGroup) convertView
.findViewById(R.id.logophonelayout);
ViewGroup addressLayout = (ViewGroup) convertView
.findViewById(R.id.addresslayout);
holder.image = (ImageView) logoPhoneLayout
.findViewById(R.id.image1);
holder.phone = (TextView) logoPhoneLayout
.findViewById(R.id.textview1);
holder.address = (TextView) addressLayout
.findViewById(R.id.textview1);
// store holder in views tag
convertView.setTag(holder);
} else {
// Retrieve holder from view
holder = (ViewHolder) convertView.getTag();
}
// fill in view with our store (at this position)
holder.phone.setText(store.phone);
holder.address.setText(store.getFullAddress());
UrlImageViewHelper.setUrlDrawable(holder.image, store.storeLogoURL,
R.drawable.no_image);
return convertView;
}
private static class ViewHolder {
ImageView image;
TextView phone;
TextView address;
}

How to keep list items in memory?

I have custom listview. When I scroll my listview, android keeps in memory (as far as I understand) items which is displaying on screen and doesn't keep items which is hidden (not scrolled to).
In my case (I think) keeping all list items would be better than generating hidden items.
So, how to "tell" android to keep all items in memory? (15-20 items). PS: if it's wasting of resources, I'd like just to try.
My adapter (some funcs):
private View newView(Context context, ViewGroup parent) {
LayoutInflater layoutInflater = LayoutInflater.from(context);
return layoutInflater.inflate(R.layout.myl,parent,false);
}
public View getView(int position,View convertView,ViewGroup parent) {
View view=null;
if(convertView!=null) view=convertView; else view=newView(context,parent);
HashMap<String,String> d=new HashMap<String,String>();
d=data.get(position);
String qweqwe=d.get("qweqwe"); //9 more lines like this.
TextView txt=(TextView)view.findViewById(R.id.mfmf); //
txt.setText(qweqwe); //
txt.setTypeface(mlf); //5 more blocks of 3 lines like this.
if (smth.equals("0")){
view.setBackgroundDrawable(context.getResources().getDrawable(R.drawable.mvmv));
} else {
view.setBackgroundDrawable(context.getResources().getDrawable(R.drawable.mvmv2));
}
return view;
}
Ok.. there are some things that can be optimized here, instead of trying to fix lag with workarounds :)
You should implement a static class, where you can store references to the Views in your myl.xml. For each View you want to manipulate in myl.xml, you create a View in this static class. So if you have 10 TextViews, you fill this class with 10 TextViews.
static class AdapterViewsHolder {
TextView txt1;
TextView txt2;
TextView txt3;
...
ImageView img1;
... etc etc.
}
In the adapter, you now only do the findViewById() calls if the convertView is null. findViewById() is not cheap, so limiting the amount of calls increases performance.
private HashMap<String, String> mData;
private LayoutInflater mInflater;
private TypeFace mCustomTypeFace;
// Some contructor for passing data into the Adapter.
public BaseAdapter(HashMap<String, String> data, Context ctx) {
mData = data;
mInflater = LayoutInflater.from(ctx);
mCustomTypeFace = Typeface.createFromAsset(ctx.getAssets(), "yourTypeFace.ttf");
}
public View getView(int position, View convertView, ViewGroup parent) {
// This AdapterViewsHolder will hold references to your views, so you don't have to
// call findViewById() all the time :)
AdapterViewsHolder holder;
// Check if convertView is null
if(convertView == null) {
// If it is, we have to inflate a new view. You can probably use the newView() call here if you want.
convertView = mInflater.inflate(R.layout.myl, null);
// Initialize the holder
holder = new AdapterViewsHolder();
// Now we do the smart thing: We store references to the views we need, in the holder. Just find all the views you need, by id.
holder.txt1 = (TextView) convertView.findViewById(R.id.textview1);
holder.txt2 = (TextView) convertView.findViewById(R.id.textview2);
...
holder.img1 = (ImageView) convertView.findViewById(R.id.imageview1);
// Store the holder in the convertViews tag
convertView.setTag(holder);
}
else {
// If convertView is not null, we can get get the holder we stored in the tag.
// This holder now contains references to all the views we need :)
holder = (AdapterViewsHolder) convertView.getTag();
}
// Now we can start assigning values to the textviews and the imageviews etc etc
holder.txt1.setText(mData.get(position));
...
holder.txt1.setTypeface(mCustomTypeFace);
...
holder.img1.setImageResource("IMAGE RESOURCE HERE");
if(someThing.equals("sometext") {
convertView.setBackgroundDrawable(somedrawable);
}
else {
convertView.setBackgroundDrawable(someotherdrawable);
}
// Finally, we return the convertView
return convertView;
}
I do not know how your data is organized, so you have to change this code a bit.
One more thing that can cause lag is the android:cacheColorHint xml attribute. Usually you set this to either the same color as you application background, or transparent. Setting it transparent have been known to cause rapid Garbage collections on some occasions.
You could override the Adapter and have it inflate all of the views in the constructor, then just return the proper one with getView(). Might make it easy if you store the Views in some data object (array, list etc..)
But really you should let the system use the convertView like it was designed to. Overall you'd get better performance doing it that way I think.

Custom adapter getting wrong position on getView metod in ListView

I have list view with custom array adapter. I want to get items click position from getView method. I am getting a few list view items position but when i add more then 7 items into my list i get wrong position from getView method. I mean when i click 9th list item it returns 1.
Here is my code
#Override
public View getView(int pos, View convertView, ViewGroup parent) {
this.position = pos;
Log.v("View position", Integer.toString(pos));
lineView = convertView;
if(lineView==null)
{
adapterLine=new AdapterLine();
layoutInflater=context.getLayoutInflater();
lineView=layoutInflater.inflate(com.inomera.sanalmarket.main.R.layout.adapter, null,true);
adapterLine.sListText = (TextView) lineView.findViewById(R.id.sListText);
adapterLine.sListCheckbox = (CheckBox) lineView.findViewById(R.id.sListCheckbox);
adapterLine.sListImageView = (ImageView) lineView.findViewById(R.id.imageView1);
adapterLine.gestureOverlayView = (GestureOverlayView) lineView.findViewById(com.inomera.sanalmarket.main.R.id.gestureOverlayView1);
adapterLine.gestureOverlayView.setGestureVisible(false);
// To remember whitch tab is selected
adapterLine.sListImageView.setTag(pos);
adapterLine.sListCheckbox.setTag(pos);
adapterLine.sListText.setTag(pos);
Log.v("adapter", "position of adapter is " + Integer.toString(pos));
Thanks for any help!
The array adapter will only create one object per visible row in your list view. After this, the adapter will recycle that already created view. This is the purpose of the:
if(lineView==null)
line in your adapter.
You will want to put in an else section that sets up the row using the recycled view. This other article may be helpful:
How can I make my ArrayAdapter follow the ViewHolder pattern?
This implementation is incomplete add else section which sets value from data. here is implementation which worked for me
public View getView(final int position, View convertView, ViewGroup parent) {
final ViewHolder viewHolder;
if (convertView == null) {
viewHolder = new ViewHolder();
LayoutInflater inflator = reportViewContext.getLayoutInflater();
convertView = inflator.inflate(R.layout.tabitems, null);
viewHolder.name= (TextView) convertView
.findViewById(R.id.user_reportTab_userName);
//set other values needed in viewHolder if any
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder)convertView.getTag();
}
//data is list of Data model
if(data.size()<=0) {
//if data unavailable }
else {
/***** Get each Model object from Array list ********/
System.out.println("Position:"+position);
/************ Set Model values in Holder elements ***********/
viewHolder.name.setText(data.get(position).getName());
}
return convertView;
}
I know the question is very old, answered in case anyone stumbled on similar problem

Categories

Resources