I have a ListView which displays news items. They contain an image, a title and some text. The image is loaded in a separate thread (with a queue and all) and when the image is downloaded, I now call notifyDataSetChanged() on the list adapter to update the image. This works, but getView() is getting called too frequently, since notifyDataSetChanged() calls getView() for all visible items. I want to update just the single item in the list. How would I do this?
Problems I have with my current approach are:
Scrolling is slow
I have a fade-in animation on the image which happens every time a single new image in the list is loaded.
I found the answer, thanks to your information Michelle.
You can indeed get the right view using View#getChildAt(int index). The catch is that it starts counting from the first visible item. In fact, you can only get the visible items. You solve this with ListView#getFirstVisiblePosition().
Example:
private void updateView(int index){
View v = yourListView.getChildAt(index -
yourListView.getFirstVisiblePosition());
if(v == null)
return;
TextView someText = (TextView) v.findViewById(R.id.sometextview);
someText.setText("Hi! I updated you manually!");
}
This question has been asked at the Google I/O 2010, you can watch it here:
The world of ListView, time 52:30
Basically what Romain Guy explains is to call getChildAt(int) on the ListView to get the view and (I think) call getFirstVisiblePosition() to find out the correlation between position and index.
Romain also points to the project called Shelves as an example, I think he might mean the method ShelvesActivity.updateBookCovers(), but I can't find the call of getFirstVisiblePosition().
AWESOME UPDATES COMING:
The RecyclerView will fix this in the near future. As pointed out on http://www.grokkingandroid.com/first-glance-androids-recyclerview/, you will be able to call methods to exactly specify the change, such as:
void notifyItemInserted(int position)
void notifyItemRemoved(int position)
void notifyItemChanged(int position)
Also, everyone will want to use the new views based on RecyclerView because they will be rewarded with nicely-looking animations! The future looks awesome! :-)
This is how I did it:
Your items (rows) must have unique ids so you can update them later. Set the tag of every view when the list is getting the view from adapter. (You can also use key tag if the default tag is used somewhere else)
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
View view = super.getView(position, convertView, parent);
view.setTag(getItemId(position));
return view;
}
For the update check every element of list, if a view with given id is there it's visible so we perform the update.
private void update(long id)
{
int c = list.getChildCount();
for (int i = 0; i < c; i++)
{
View view = list.getChildAt(i);
if ((Long)view.getTag() == id)
{
// update view
}
}
}
It's actually easier than other methods and better when you dealing with ids not positions! Also you must call update for items which get visible.
get the model class first as global like this model class object
SampleModel golbalmodel=new SchedulerModel();
and initialise it to global
get the current row of the view by the model by initialising the it to global model
SampleModel data = (SchedulerModel) sampleList.get(position);
golbalmodel=data;
set the changed value to global model object method to be set and add the notifyDataSetChanged its works for me
golbalmodel.setStartandenddate(changedate);
notifyDataSetChanged();
Here is a related question on this with good answers.
The answers are clear and correct, I'll add an idea for CursorAdapter case here.
If youre subclassing CursorAdapter (or ResourceCursorAdapter, or SimpleCursorAdapter), then you get to either implement ViewBinder or override bindView() and newView() methods, these don't receive current list item index in arguments. Therefore, when some data arrives and you want to update relevant visible list items, how do you know their indices?
My workaround was to:
keep a list of all created list item views, add items to this list from newView()
when data arrives, iterate them and see which one needs updating--better than doing notifyDatasetChanged() and refreshing all of them
Due to view recycling the number of view references I'll need to store and iterate will be roughly equal the number of list items visible on screen.
int wantedPosition = 25; // Whatever position you're looking for
int firstPosition = linearLayoutManager.findFirstVisibleItemPosition(); // This is the same as child #0
int wantedChild = wantedPosition - firstPosition;
if (wantedChild < 0 || wantedChild >= linearLayoutManager.getChildCount()) {
Log.w(TAG, "Unable to get view for desired position, because it's not being displayed on screen.");
return;
}
View wantedView = linearLayoutManager.getChildAt(wantedChild);
mlayoutOver =(LinearLayout)wantedView.findViewById(R.id.layout_over);
mlayoutPopup = (LinearLayout)wantedView.findViewById(R.id.layout_popup);
mlayoutOver.setVisibility(View.INVISIBLE);
mlayoutPopup.setVisibility(View.VISIBLE);
For RecycleView please use this code
I used the code that provided Erik, works great, but i have a complex custom adapter for my listview and i was confronted with twice implementation of the code that updates the UI. I've tried to get the new view from my adapters getView method(the arraylist that holds the listview data has allready been updated/changed):
View cell = lvOptim.getChildAt(index - lvOptim.getFirstVisiblePosition());
if(cell!=null){
cell = adapter.getView(index, cell, lvOptim); //public View getView(final int position, View convertView, ViewGroup parent)
cell.startAnimation(animationLeftIn());
}
It's working well, but i dont know if this is a good practice.
So i don't need to implement the code that updates the list item two times.
exactly I used this
private void updateSetTopState(int index) {
View v = listview.getChildAt(index -
listview.getFirstVisiblePosition()+listview.getHeaderViewsCount());
if(v == null)
return;
TextView aa = (TextView) v.findViewById(R.id.aa);
aa.setVisibility(View.VISIBLE);
}
I made up another solution, like RecyclyerView method void notifyItemChanged(int position), create CustomBaseAdapter class just like this:
public abstract class CustomBaseAdapter implements ListAdapter, SpinnerAdapter {
private final CustomDataSetObservable mDataSetObservable = new CustomDataSetObservable();
public boolean hasStableIds() {
return false;
}
public void registerDataSetObserver(DataSetObserver observer) {
mDataSetObservable.registerObserver(observer);
}
public void unregisterDataSetObserver(DataSetObserver observer) {
mDataSetObservable.unregisterObserver(observer);
}
public void notifyDataSetChanged() {
mDataSetObservable.notifyChanged();
}
public void notifyItemChanged(int position) {
mDataSetObservable.notifyItemChanged(position);
}
public void notifyDataSetInvalidated() {
mDataSetObservable.notifyInvalidated();
}
public boolean areAllItemsEnabled() {
return true;
}
public boolean isEnabled(int position) {
return true;
}
public View getDropDownView(int position, View convertView, ViewGroup parent) {
return getView(position, convertView, parent);
}
public int getItemViewType(int position) {
return 0;
}
public int getViewTypeCount() {
return 1;
}
public boolean isEmpty() {
return getCount() == 0;
} {
}
}
Don't forget to create a CustomDataSetObservable class too for mDataSetObservable variable in CustomAdapterClass, like this:
public class CustomDataSetObservable extends Observable<DataSetObserver> {
public void notifyChanged() {
synchronized(mObservers) {
// since onChanged() is implemented by the app, it could do anything, including
// removing itself from {#link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {#link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
}
public void notifyInvalidated() {
synchronized (mObservers) {
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onInvalidated();
}
}
}
public void notifyItemChanged(int position) {
synchronized(mObservers) {
// since onChanged() is implemented by the app, it could do anything, including
// removing itself from {#link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {#link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
mObservers.get(position).onChanged();
}
}
}
on class CustomBaseAdapter there is a method notifyItemChanged(int position), and you can call that method when you want update a row wherever you want (from button click or anywhere you want call that method). And voila!, your single row will update instantly..
My solution:
If it is correct*, update the data and viewable items without re-drawing the whole list. Else notifyDataSetChanged.
Correct - oldData size == new data size, and old data IDs and their order == new data IDs and order
How:
/**
* A View can only be used (visible) once. This class creates a map from int (position) to view, where the mapping
* is one-to-one and on.
*
*/
private static class UniqueValueSparseArray extends SparseArray<View> {
private final HashMap<View,Integer> m_valueToKey = new HashMap<View,Integer>();
#Override
public void put(int key, View value) {
final Integer previousKey = m_valueToKey.put(value,key);
if(null != previousKey) {
remove(previousKey);//re-mapping
}
super.put(key, value);
}
}
#Override
public void setData(final List<? extends DBObject> data) {
// TODO Implement 'smarter' logic, for replacing just part of the data?
if (data == m_data) return;
List<? extends DBObject> oldData = m_data;
m_data = null == data ? Collections.EMPTY_LIST : data;
if (!updateExistingViews(oldData, data)) notifyDataSetChanged();
else if (DEBUG) Log.d(TAG, "Updated without notifyDataSetChanged");
}
/**
* See if we can update the data within existing layout, without re-drawing the list.
* #param oldData
* #param newData
* #return
*/
private boolean updateExistingViews(List<? extends DBObject> oldData, List<? extends DBObject> newData) {
/**
* Iterate over new data, compare to old. If IDs out of sync, stop and return false. Else - update visible
* items.
*/
final int oldDataSize = oldData.size();
if (oldDataSize != newData.size()) return false;
DBObject newObj;
int nVisibleViews = m_visibleViews.size();
if(nVisibleViews == 0) return false;
for (int position = 0; nVisibleViews > 0 && position < oldDataSize; position++) {
newObj = newData.get(position);
if (oldData.get(position).getId() != newObj.getId()) return false;
// iterate over visible objects and see if this ID is there.
final View view = m_visibleViews.get(position);
if (null != view) {
// this position has a visible view, let's update it!
bindView(position, view, false);
nVisibleViews--;
}
}
return true;
}
and of course:
#Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
final View result = createViewFromResource(position, convertView, parent);
m_visibleViews.put(position, result);
return result;
}
Ignore the last param to bindView (I use it to determine whether or not I need to recycle bitmaps for ImageDrawable).
As mentioned above, the total number of 'visible' views is roughly the amount that fits on the screen (ignoring orientation changes etc), so no biggie memory-wise.
In addition to this solution (https://stackoverflow.com/a/3727813/5218712) just want to add that it should work only if listView.getChildCount() == yourDataList.size();
There could be additional view inside ListView.
Example of how the child elements are populated:
Related
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 have a configuration activity for an AppWidget. I want to let the user choose an existing string for the widget, or create one of their own by selecting the first option and editing the text within the list.
How do I change the element type of just the first list item to a different type of view that will allow users to insert custom text?
The ListAdapter interface has two methods getViewTypeCount() and getItemViewType(). So in your case, they might be implemented like this:
#Override
public int getViewTypeCount() {
return 2;
}
#Override
public int getItemViewType(int position) {
// first position has view type == 0; all others have view type == 1
return position == 0 ? 0 : 1;
}
What this does is make sure that the correct views are recycled for the correct positions. Once you have these methods, then you can do:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (position == 0) {
// create a custom edit view and return it
} else {
// create a selection view and return it
}
}
I have the code below which works except it always hides atleast one real item in the listview because the ad displays at that position.
Example of the problem: I have a list of 4 times, and the adView is displaying at position 3. on the listview I can only see 3 times and the AdView, the 4th item does not get displayed
I played around with increasing the size of the adapter everytime I return an ad but it didn't work very well.
Any ideas?
public View getView(final int position, View row, ViewGroup parent) {
MyHolder holder = null;
boolean showAd = proVersion == false && (position % 8 == k);
if (showAd) {
AdView adView = adList.get(position);
if (adView == null) {
AdView adViewNew = new AdView((Activity) context, AdSize.BANNER, context.getResources().getString(
R.string.adId));
adViewNew.loadAd(Utils.getAdRequest("gps", lat, lng, keywords));
adList.add(position, adViewNew);
return adViewNew;
} else {
return adView;
}
} else if (row == null || row instanceof AdView) {
LayoutInflater inflater = ((SherlockActivity) context).getLayoutInflater();
row = inflater.inflate(viewResourceId, parent, false);
holder = new MyHolder();
holder.textName = (TextView) row.findViewById(R.id.name);
row.setTag(holder);
} else {
holder = (MyHolder) row.getTag();
}
holder.textName.setText(items.get(position).getName());
// more code
return row;
}
#Override
public int getCount() {
if (items != null) {
return items.size();
} else {
return 0;
}
}
There is more than one way of achieving this. The best way is relying upon the ability of the listView to recycle multiple item types.
In your adapter,
getViewTypeCount() - returns information how many types of rows you have in the listView
getItemViewType(int position) returns information on which layout type you should use based on the position. That's where your logic of determining the listView objects should go.
There is a good tutorial on how to use different item types in a list here:
Specifically, look at "Different list items’ layouts".
After that, you need minor arithmetic conversions for your indices so that you map the position to the right place in your data structure (or, you can merge your data structure to include both ad data and item data).
Basically, instead of using items.size() you need to use items.size() + Math.floor(items.size()/NTH_ITEM) and when you get the position, if it's an ad position (position % NTH_ITEM == 0) use a simple conversion like Math.floor(position/NTH_ITEM) to extract from the ad data structure and in similar fashion for your item's structure.
You should rely on the holder pattern of the ListView to reuse your different item view types, like in the tutorial above.
Different approaches, just for the notion, include using something like item wrappers that merge the data source into one and enables differentiating between items by using a type property, like an enum, or a "merging adapter" that merges two specific adapters (this might have a better modularity if you are to include these view types in different lists).
Let me know if you need any help implementing specific parts of this in your use case.
Ben Max's answer is cleaner.
It is recommended to use view types when dealing with different types of view.
Here's a good way to do it.
Create a wrapper for your different data types:
private class ItemWrapper {
public static final int TYPE_NORMAL = 0;
public static final int TYPE_AD = 1;
public static final int TYPE_COUNT = 2;
public ListObject item;
public AdObject adItem;
public int type;
public ItemWrapper(ListObject item) {
this.type = TYPE_NORMAL;
this.item = item
}
public ItemWrapper(AdObject adItem) {
this.type = TYPE_AD;
this.adItem = adItem;
}
}
Now come some of the changes to your adapter:
Let's assume you get you initialize your adapter in the constructor
public class MyAdapter extends BaseAdapter{
ArrayList<ItemWrapper> mWrappedItems = new ArrayList<ItemWrapper>();
public void MyAdapter(List<ListObject> items,AdItem adItem){
for(ListObject item:items){
mWrappedItems.add(new ItemWrapper(item));
}
//you can insert your ad item wherever you want!
mWrappedItems.add(2,new ItemWrapper(adItem));
}
}
Then a few more changes to your adapter:
#Override
public ItemWrapper getItem(int position) {
return mWrappedItems == null ? null : mWrappedItems.get(position);
}
#Override
public int getItemViewType(int position) {
return getItem(position).type;
}
#Override
public int getViewTypeCount() {
//tells the adapter how many different view types it will need to recycle
return ItemWrapper.TYPE_COUNT;
}
Now for your getView....
You'll see this is nicer because you can control how your views are inflated
public View getView(final int position, View row, ViewGroup parent) {
final ItemWrapper item = getItem(position);
final int type = item.type;
if (row == null) {
if (type == ItemWrapper.TYPE_NORMAL) {
//inflate your normal view layouts
} else if (type == ItemWrapper.TYPE_AD) {
//inflate your ad layout
}
}
//now you can easily populate your views based on the type
if (type == ItemWrapper.TYPE_NORMAL) {
//get your item data
final ListObject data = item.item;
//populate as you would normally
} else if (type == ItemWrapper.TYPE_AD) {
final AdItem adItem = item.adItem;
//populate your ad as needed
}
return row;
}
You'll notice that wrapping your data like this gives you great control on how/where your item is display by just manipulating the list instead of doing any weird logic inside your getView(); it also makes it simple to let you inflate layouts according to type and populate them easily.
I hope this helps!
P.S i wrote a blog entry about this topic before as well:
http://qtcstation.com/2012/04/a-limitation-with-the-viewholder-pattern-on-listview-adapters/
I'd go in a different way, instead of adding the ad layout dynamically. Assuming your layout (which is accessed via the View row parameter of your getView() method) is customized, simply add a LinearLayout in it which will be used for the AdView.
As you don't want it to be visible on all the rows, mark its visibility to gone by default, but defining it that way will allow you to control your layout and define it how it should be shown.
Assuming this is your layout file:
<LinearLayout
...>
<!-- Put here the AdView layout -->
<LinearLayout
android:id="#+id/adViewLL"
...
android:visibility="gone" />
<!-- Add whatever else you need in your default layout -->
...
</LinearLayout>
Now simply change your getView() method to set the visibility to VISIBLE whenever you need and put there the View for your AdView.
public View getView(final int position, View row, ViewGroup parent) {
MyHolder holder = null;
boolean showAd = proVersion == false && (position % 8 == k);
if (row == null) {
LayoutInflater inflater = ((SherlockActivity) context).getLayoutInflater();
row = inflater.inflate(viewResourceId, parent, false);
holder = new MyHolder();
holder.textName = (TextView) row.findViewById(R.id.name);
row.setTag(holder);
if (showAd) {
LinearLayout myAd = (LinearLayout) row.findViewByid(R.id.adViewLL);
myAd.setVisibility(View.VISIBLE);
AdView adViewNew = new AdView((Activity) context, AdSize.BANNER, context.getResources().getString(R.string.adId));
...
myAd.addView(adViewNew);
}
} else {
holder = (MyHolder) row.getTag();
}
holder.textName.setText(items.get(position).getName());
// more code
return row;
}
I would add to nKn's answer my following experience:
Encapsulate the Ad view with a ViewSwitcher
Show a message where the Ad goes, something like: "a message from our lovely sponsors…"
When the Ad callback hits you (onAdLoaded), do a holder.switcher.setDisplayedChild(1); // if 1 is where your AdView is.
Have a Flag where you can disable Ads so always do if (showAd && adsEnabled) {}, you never know when you might need to disable ads, if you have an API try to retrieve that value from there.
#4 can also be used for an else clause: else {holder.switcher.setDisplayedChild(0);//your generic message}
Have an "AdController" where you determine how often (every what ## of items you want to insert an ad). and have your list ask if (YourAdController.isAd(position) && adsEnabled) { do the add thing }
This is slightly a logical error.
Here's an explanation on whats happening.
Going by your description, every 8x position is an ad
Why go on to mess with views or maintaining multiple layouts
In your dataset(i.e the list that you are passing to the adapter) just add an item that indicates its an ad item at every 8x position
So, if its a pro version there's no ad item entry in the list
Finally, in your getview
if(ad item)
populate adView
else
populate regularItemView
Hope it simplifies
I have the same problem on one of the applications that i worked on. here's the approach that worked with me.
First,you need to create a class that contains your ad details.
public class AdUnitDetails {
private String mUnitId;
private int mIndex;
public AdUnitDetails(String mUnitId,int mIndex) {
super();
this.mUnitId = mUnitId;
this.mIndex = mIndex;
}
public String getUnitId() {
return mUnitId;
}
public void setUnitId(String mUnitId) {
this.mUnitId = mUnitId;
}
public int getIndex() {
return mIndex;
}
public void setIndex(int mIndex) {
this.mIndex = mIndex;
}
}
Then, you create a Sparsearray containing adviews with their positions
SparseArray<AdView> mAdViewsMap = new SparseArray<AdView>();
Then you need to modify your adapter to receive an arraylist of objects and then you add the ads in their corresponding positions when you fill this list. example(item, item , item , addetails, item, item, etc).
Then, in your adapter. add the following methods:
private static final int TYPE_ITEM= 0;
private static final int TYPE_AD = 1;
private static final int TYPES_COUNT = TYPE_AD + 1;
#Override
public int getViewTypeCount() {
return TYPES_COUNT;
}
#Override
public int getItemViewType(int position) {
if (mItems.get(position) instanceof YourItemClass)
return TYPE_ITEM;
else if (mItems.get(position) instanceof AdUnitDetails)
return TYPE_AD;
return super.getItemViewType(position);
}
Then, in your getView() method,add the following
View view = convertView;
if (mItems.get(position) instanceof YourItemClass) {
// Handle your listview item view
} else if (mItems.get(position) instanceof AdUnitDetails) {
return getAdView(position);
}
return view;
your getAdView() method
private AdView getAdView(final int position) {
if (mAdViewsMap.get(position) == null) {
AdSize adSize = getAdSize(AdSize.BANNER);
AdView mAdView = new AdView(mContext, adSize, ((AdUnitDetails) mItems.get(position)).getUnitId());
mAdView.loadAd(new AdRequest());
mAdViewsMap.put(position, mAdView);
return mAdView;
} else {
return mAdViewsMap.get(position);
}
}
It might be too late but here is how i solved mine.
Converted the current position from int to string ( optional)
and then every time view is inflated check if position contains (3 || 6 || 9) for that every 3rd visible layout shows an advertisement banner. hope it was helpful will post code if needed :)
oh and || means "or" if anyone is wondering
here is the code Ashish Kumar Gupta
RelativeLayout rl = (RelativeLayout) convertView.findViewById(R.id.adviewlayout); //putting the advertisement banner inside a relativelayout/linearlayout if you want. so that you can make it visibile whenever you want.
String pos = String.valueOf(position); // converting listview item position from int to string//
if (pos.endsWith("3")||pos.endsWith("6")||pos.endsWith("9")) {
// every fourth position will show an advertisement banner inside a listview. here is the proof (0,1,2,advertisement,4,5,advertisement,7,8,advertisement,10,11,12,advertisement,14,15,advertisement,17,18,advertisement,20). did not put advertisements in 0 position cuz its rude to put advertisements in like the first layout in my POV//
rl.setVisibility(View.VISIBLE);
}
else
{
rl.setVisibility(View.GONE);
//if its not a 3rd position advertisements will not be shown//
}
you are welcome :)
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!");
}
}
In arrayAdptor we use following code:
final LayoutInflater inflater = activity.getLayoutInflater();
row = (LinearLayoutCustom) inflater.inflate(R.layout.row, null);
final TextView label = (TextView) row.findViewById(R.id.title);
label.setText(position + "" + items[position]);
return row;
Now suppose some value are null (for example at position 2 , items[2] = null ) so i dont want to show it in row. i want to hide it. if i use
row.setVisibility(View.GONE)
it leaves a blank space at this row which i dont want. so what should i do?
AFAIK you can't return a null view from getView, but you could just make the view invisible and height 1. Although manipulating using the getCount is probably the preferred way.
view.setVisibility(View.INVISIBLE);
view.getLayoutParams().height = 1;
You'll need to have the adapter return the total number of non-null items with getCount and then keep a mapping of position to your internal data structure.
For example.
You have a list
1 - John
2 - null
3 - Bill
4 - Susan
5 - null
When getCount is called it returns 3.
Then when getView is called on position 1 you return the item at list[1].
getView on position 2 returns list[3] (as it's the 2nd non-null),
and so forth.
This is the only way I've found to do this.
You can use a View that has no height for the "hidden" items so that you don't have to do all the model housekeeping and mapping. For example, suppose you had a "filter" EditText field that when data is entered it only keeps matching items:
public View getView(int position, View convertView, ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) MyActivity.this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
RelativeLayout view = (RelativeLayout) inflater.inflate(R.id.myListLayout, null, false);
...
// if we didn't match filter be GONE and leave
if (filterText.length() > 0 && myModelValueAtPosition.toLowerCase().indexOf(filterText) < 0){
view = (RelativeLayout) inflater.inflate(R.layout.myListLayoutWithZeroHeight, null, false);
view.setVisibility(View.GONE); // this doesn't really do anything useful; I'd hoped it would work by itself, but turns out the zero height layout is the key
return view;
}
view.setVisibility(View.VISIBLE);
...
}
Here you need to write the logic in your getCount(),getItemId() and getItem(),
It will create the no of rows what the getCount return
//How many items are in the data set represented by this Adapter
public int getCount() {
return //Should return the count of rows you need to display (here the count excluding null values)
}
And
//This need to return data item associated with the specified position in the data set.
public Object getItem(int position) {
return //Return the object need to display at position, need the logic to skip null value
}
Edit:So in your getview
public View getView(int position, View convertView, ViewGroup parent) {
----
getItem(position);//Object corresponding to position ,In your case it will not be null since you need to write the logic to skip null object at getItem
----
}
This is the solution I implemented, here is a code example for everyone that is looking it:
class Shipment_Adapter extends ArrayAdapter<Shipment>{
....
ArrayList<Integer> emptyPositions = new ArrayList<>();
public Shipment_Adapter(Context context, int shipment_row, Shipment[] myShipments){
super(context, R.layout.shipment_row,myShipments);
//populate emptyPositions list
for(int i = 0; i< myShipments.length; i++){
if(myShipments[i]==null){
emptyPositions.add(i);
}
}
this.mShipment = myShipments;
this.mContext = context;
}
//corrects size of List
#Override
public int getCount() {
return (mShipment.length - emptyPositions.size());
}
//recursive function that checks if position is not empty until it isn't
public int isEmpty(int positiontocheck){
int newposition;
if(emptyPositions.contains(positiontocheck)){
//true? check that next one is free
return isEmpty(positiontocheck+1);
}else{
newposition = positiontocheck;
}
return newposition;
}
}
public View getView(int position, View convertView, ViewGroup parent) {
//now just need to use isEmpty to get the next not empty position in
//case our real position is empty
position= isEmpty(position);
Shipment shipment = mShipment[position];
...//and so on
}
hope this helps!