I'm trying to create a simple chat/message layout. My problem is with the Adapter. I want to use two different layouts. One for outgoing and one incoming messages. Furthermore I would like to use the ViewHolder approach. But my implementation causes the same layout to be inflated/reused everytime getView runs.
Can anyone please explain to me why this is happening? Do I need to use two viewholders?
The relevant code is shown below
#Override
public View getView(int position, View convertView, ViewGroup parent) {
int direction = getItemViewType(position);
ViewHolder holder;
if (convertView == null) {
int res = 0;
int resid = 0;
holder = new ViewHolder();
if (direction == DIRECTION_INCOMING) {
holder = new ViewHolder();
res = R.layout.message_l_box;
resid = R.id.left_bubble_text;
}
else {
res = R.layout.message_r_box;
resid = R.id.right_bubble_text;
}
convertView = LayoutInflater.from(mContext).inflate(res, parent, false);
holder.text = (TextView) convertView.findViewById(resid);
convertView.setTag(holder);
}
else {
holder = (ViewHolder) convertView.getTag();
}
holder.text.setText(getItem(position).first);
return convertView;
}
// Ensure that find by id is not called every time -> could cause slow scrolling
private class ViewHolder {
TextView text;
}
ListViews reuse Views when possible for performance. That is what the convertView View in your code is for. If Android already had another row that you scrolled away from it can be reused so it will be passed into your getView() method. Your code is not creating a new layout if convertView is not null.
You could do something like this, basically store the current layouts direction in the ViewHolder so you know if you can reuse the layout or have to re-inflate it.
private class ViewHolder {
TextView text;
int direction;
}
if (convertView == null || ((ViewHolder) convertView.getTag()).direction != direction) {
// same code you have now to layout a new row
holder.direction = direction
} else { // we know the existing view has same direction so it has the correct layout already inflated and we can reuse it
holder = (ViewHolder) convertView.getTag();
}
Related
When I create a class extends BaseAdapter, I get a warning message
Unconditional layout inflation from view adapter: Should use View Holder pattern (use recycled view passed into this method as the second parameter) for smoother scrolling
Do I need to change my code like this suggestion? All my code is running smoothly, it's just whether it is necessary to change the code to do updata code with the latest styles? Or just need to add #SuppressLint({ "ViewHolder", "InflateParams" }) ?
My adapter
public View getView(int position, View convertView, ViewGroup parent) {
View v = inflater.inflate(R.layout.list_item, null);
TextView merchant_type = (TextView) v.findViewById(R.id.merchant_type);
TextView merchant_name = (TextView) v.findViewById(R.id.merchant_name);
TextView merchant_location = (TextView) v.findViewById(R.id.merchant_location);
VoucherBean obj = (VoucherBean) getItem(position);
merchant_type.setText(obj.getMerchantType());
merchant_name.setText(obj.getMerchantName());
merchant_location.setText(obj.getMerchantLocation());
return v;
}
If you want to change my code as recommended above warning, like what my code later? Sorry if my question is too basic for a beginner
if you have more data in your list view then you should use recycling, because it improves scrolling and performance also. here is code look like if you use view holder in your adapter.
your ViewHolder looks like
public class ViewHolder {
public TextView merchant_type;
public TextView merchant_name;
public TextView merchant_location;
}
and your getView method
View vi = convertView;
ViewHolder holder;
if (convertView == null) {
vi = inflater.inflate(R.layout.list_item, null);
holder = new ViewHolder();
holder.merchant_type = (TextView) vi.findViewById(R.id.merchant_type);
holder.merchant_name = (TextView) vi.findViewById(R.id.merchant_name);
holder.merchant_location = (TextView) vi.findViewById(R.id.merchant_location);
vi.setTag(holder);
} else
holder = (ViewHolder) vi.getTag();
// now set your text view here like
holder.merchant_name.setText("Bla Bla Bla");
// return your view
return vi;
The View Holder pattern is meant to make your code easier to read and to maintain. If you look at the standard ViewHolder you'll see that its a class that basically does what you do in your getView:
private static class ExampleViewHolder{
TextView merchant_type;
TextView merchant_name;
TextView merchant_location;
public ExampleViewHolder(View view){
merchant_type = (TextView) v.findViewById(R.id.merchant_type);
merchant_name = (TextView) v.findViewById(R.id.merchant_name);
merchant_location = (TextView) v.findViewById(R.id.merchant_location);
}
}
Then in your getView method you'd get your view like this:
ExampleViewHolder holder = new ExampleViewHolder(view);
view.setTag(holder);
It's good to have them and the newer apis kind of force the use of this pattern, however as you can see by the code it doesn't change much besides readability and ease of maintenance.
However! There is an important part of the getView method that you are missing, and is most likely the reason for the warning.
The listview recycles the views, and gives them back to the adapter so that the same view doesnt have to be inflated everytime. Saves resources and a lot of memory, and you're not making use of this very important aspect of the listView.
You see, the way it recycles views is passing the old view, that you inflated before, back to the adapter through the convertView, so if the convertView is not null you can be sure its the inflated layout that you are using. So use that instead of inflating a new one:
View view = convertView;
ExampleViewHolder holder;
if(view == null){//means convertView is also null
view = inflater.inflate(yourlayout, parent, false);
holder = new ExampleViewHolder(view);
view.setTag(holder);
}else{
holder = (ExampleViewHolder) view.getTag();
}
I am developing an app using a ListView with a simple custom adapter, each row containing a CheckBox object. However, due to ListView's recycling feature (that I don't plan on turning off), when any of the boxes are checked, others below or above in the ListView are also checked.
The following is my getView() in the adapter, along with the custom ViewHolder class:
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
LayoutInflater mInflater = (LayoutInflater) c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = mInflater.inflate(R.layout.populate_friends_row, null);
holder = new ViewHolder();
holder.nameCheckBox = (CheckBox) convertView.findViewById(R.id.isFriend);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.nameCheckBox.setText(data.get(position).contactLabel);
holder.nameCheckBox.setChecked(checked.get(position));
holder.nameCheckBox.setTag(String.valueOf(position)); // to properly track the actual position
holder.nameCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener(){
#Override
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked) {
int pos = Integer.parseInt(buttonView.getTag().toString());
checked.set(pos, isChecked);
}
});
return convertView;
}
public static class ViewHolder {
public CheckBox nameCheckBox;
}
I am already holding the checked boxes in the ArrayList of booleans: checked.
Any help would be appreciated, thank you.
When you're calling holder.nameCheckBox.setChecked(checked.get(position)); to configure the view to be displayed for this view, the listener is called while the tag still has the position of the previous checkbox.
Try removing the listener (setting it to null) before calling setChecked
I know that many similar questions have been posted on stackoverflow, so please don't think I haven't searched high and low. I think my problems simply comes from now completely understanding listViews and the lifecycles of list items. I have a list view that can contain two types of messages, outbound or inbound. Originally, my listView would use a different background color depending on the type of message (outbound vs inbound), and it worked flawlessly. Now my application doesn't require a different background for list items, but it actually requires different layouts for different list items.
This is a clip of my Adapter.
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
SoapBoxMessage thisMessage = messages.get(position);
if (v == null) {
LayoutInflater inflater = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (thisMessage.isOutbound()) {
v = inflater.inflate(R.layout.outbound_row, null);
} else {
v = inflater.inflate(R.layout.inbound_row, null);
}
}
Adapters can support different ViewItemTypes that will solve your recycling problems.
static public enum LAYOUT_TYPE {
INBOUND,
OUTBOUND
}
#Override
public int getViewTypeCount () {
return LAYOUT_TYPE.values().length;
}
#Override
public int getItemViewType (int position) {
if ( messages.get(position).isOutbound())
return LAYOUT_TYPE.OUTBOUND.ordinal();
else
return LAYOUT_TYPE.INBOUND.ordinal();
}
#Override
public View getView (int position, View convertView, ViewGroup parent) {
LAYOUT_TYPE itemType = LAYOUT_TYPE.values()[getItemViewType(position)];
... (code until inflater )
switch (itemType){
case INBOUND:
convertview = /inflate & configure inbound layout
break;
case OUTBOUND:
convertview = /inflate & configure outbound layout
break;
}
you don't need to worry about recycling views because the listview will respect the ViewItemTypes for each position and it will only provide a convertview of the correct viewtype for that position
The problem is that listview is recycling the view so when you check if the view is null it wont pass that because the view is not null when it is recycled
you would need to inflate the view each time getView is called, basically removing if(v == null)
Try to use a ViewHolder like this:
ViewHolder holder;
public View getView(int position, View convertView, ViewGroup parent) {
convertView = null;
SoapBoxMessage thisMessage = messages.get(position);
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (thisMessage.isOutbound()) {
convertView = inflater.inflate(R.layout.outbound, null, false);
//specific to your outbound layout
holder = new ViewHolder();
holder.text= (TextView)convertView.findViewById(R.id.textview);
holder.group = (RadioGroup)convertView.findViewById(R.id.toggleGroup);
holder.toggle = (ToggleButton)convertView.findViewById(R.id.toggleButton);
} else {
convertView = inflater.inflate(R.layout.inbound, null, false);
//specific to your inbound layout
holder = new ViewHolder();
holder.text= (TextView)convertView.findViewById(R.id.textview);
holder.group = (RadioGroup)convertView.findViewById(R.id.toggleGroup);
holder.toggle = (ToggleButton)convertView.findViewById(R.id.toggleButton);
}
convertView.setTag(holder);
}
else{
holder = (ViewHolder) convertView.getTag();
}
//Here you can set the text or other code you want to implement
holder.text.setText("Whatever!");
return convertView;
}
static class ViewHolder {
//TODO put components you use like:
TextView text;
RadioGroup group;
ToggleButton toggle;
}
It's because of the recycling that is happening. You would need something along these lines:
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
SoapBoxMessage thisMessage = messages.get(position);
if (convertView == null) {
LayoutInflater mInflater = (LayoutInflater) context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
convertView = mInflater.inflate(R.layout.listview_feedlog_item, null);
holder = new ViewHolder();
holder.txtTime = (TextView) convertView.findViewById(R.id.textTime);
holder.txtDate = (TextView) convertView.findViewById(R.id.textDate);
convertView.setTag(holder);
} else
holder = (ViewHolder) convertView.getTag();
// I don't know how your SoapBoxMessage is made up so here are two sample methods
holder.txtTime.setText(thisMessage.getTime());
holder.txtDate.setText(thisMessage.getDate());
return convertView;
}
/* private view holder class */
private class ViewHolder {
TextView txtTime;
TextView txtDate;
}
Also, remember to always reset or initiate a value in the getView method. Since a View can be recycled it might carry with it properties of its former life.
Whether this is a good practice or not, Removing if (v == null) will solve the problem.
Anyway, you will have to re-inflate the view.
I have developed an app in which I display data in Gridview. All data comes from local storage. I am able to display each data correctly and there are no issues with it. But when I have scroll the Gridview and goto the bottom and getback to Top,it changes position. And sometimes when I scroll down and getback to Top, a blank screen appears on screen;no data found at all!
So I thought that there is issue with getView(). I am unable to figure out the problem
Code of getView():
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
ViewHolder viewholder;
// LayoutInflater inflator = activit.getLayoutInflater();
if(convertView == null) {
// recycled view is null so create it.
viewholder = new ViewHolder();
convertView = inflator.inflate(R.layout.gridviewrow, null);
viewholder.imgvGridicon = (ImageView) convertView.findViewById(R.id.imgvGridicon);
viewholder.txtGridItemlabel = (TextView) convertView.findViewById(R.id.txtGridItemlabel);
convertView.setTag(viewholder);
} else {
viewholder = (ViewHolder) convertView.getTag();
}
if ((lstpinfo.get(position).appname.toString()) != null) {
viewholder.imgvGridicon.setImageDrawable((lstpinfo.get(position).icon));
viewholder.txtGridItemlabel.setText(lstpinfo.get(position).appname.toString());
}
return convertView;
}
Update::
Intitalize of inflater::
private LayoutInflater inflator;
private ArrayList<PInfo> lstpinfo = new ArrayList<PInfo>();
public GridViewAdapter(Context cntx, ArrayList<PInfo> lstpinfo) {
activit = cntx;
inflator = LayoutInflater.from(cntx);
this.lstpinfo = lstpinfo;
}
You have to set your view height with fixed value. There's a scrolling bug in android gridview with different heights. Please see this for reference.
I have a listview that uses a customadapter based on the baseadapter. The listview populates ok using external data and I can pick up the click events and know which item has been selected.
I'm having a problem updating the clicked item's views, like TextView and ViewFlipper.
Do I have to update something via the listview or is it via the adapter. I've tried things like the following;
View test = (View)adapter.getView(pos, null, myListView);
ViewFlipper temp = (ViewFlipper)test.findViewById(R.id.flipholder);
temp.showNext();
TextView temp2 = (TextView)test.findViewById(R.id.filmtitle);
temp2.setText("Hello World");
Which results in the viewfliper flipping the first and third item or second and fourth and the text not updating at all.
Any ideas?
Cheers
Try this pattern in getView:
public View getView(int position, View convertView, ViewGroup parent) {
// A ViewHolder keeps references to children views to avoid unneccessary calls
// to findViewById() on each row.
ViewHolder holder;
// When convertView is not null, we can reuse it directly, there is no need
// to reinflate it. We only inflate a new View when the convertView supplied
// by ListView is null.
if (convertView == null) {
convertView = mInflater.inflate(R.layout.list_item_icon_text, null);
// Creates a ViewHolder and store references to the two children views
// we want to bind data to.
holder = new ViewHolder();
holder.text = (TextView) convertView.findViewById(R.id.text);
holder.icon = (ImageView) convertView.findViewById(R.id.icon);
convertView.setTag(holder);
} else {
// Get the ViewHolder back to get fast access to the TextView
// and the ImageView.
holder = (ViewHolder) convertView.getTag();
}
// Bind the data efficiently with the holder.
holder.text.setText(DATA[position]);
holder.icon.setImageBitmap((position & 1) == 1 ? mIcon1 : mIcon2);
return convertView;
}
static class ViewHolder {
TextView text;
ImageView icon;
}
}