I'm looking into writing a custom adapter to populate a listview with 3 textviews per line. I've found quite a bit of example code to do this, but the one that seemed the best was at: http://www.anddev.org/custom_widget_adapters-t1796.html
After a few minor tweaks to fix some compiler issues with the latest Android SDK, I got it running, only to get the exception:
ERROR/AndroidRuntime(281): java.lang.UnsupportedOperationException: addView(View, LayoutParams) is not supported in AdapterView
So I did a lot of research and found lots of possible reasons and fixes for this. None of which changed a thing. My adapter code is currently:
public class WeatherAdapter extends BaseAdapter {
private Context context;
private List<Weather> weatherList;
public WeatherAdapter(Context context, int rowResID,
List<Weather> weatherList ) {
this.context = context;
this.weatherList = weatherList;
}
public int getCount() {
return weatherList.size();
}
public Object getItem(int position) {
return weatherList.get(position);
}
public long getItemId(int position) {
return position;
}
public View getView(int position, View convertView, ViewGroup parent) {
Weather weather = weatherList.get(position);
//LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LayoutInflater inflater = LayoutInflater.from(context);
View v = inflater.inflate(R.layout.weather_row, null, true);
TextView cityControl = (TextView)v.findViewById( R.id.city );
TextView temperatureControl = (TextView)v.findViewById( R.id.temperature );
ImageView skyControl = (ImageView)v.findViewById( R.id.sky );
return v;
}
}
So I have tried the commented out way of getting the inflater, and the currently uncommented out. I have tried passing "parent" to inflate as well as null, and passing "true", "false" and omitting completely the last parameter. None of them have worked, and all examples I've found so far have been from 2008 which I get the feeling are a bit outdated.
If anyone could help with this then I would love to resolve the issue.
I believe this line is at fault:
View v = inflater.inflate(R.layout.weather_row, null, true);
You need instead:
View v = inflater.inflate(R.layout.weather_row, parent, false);
The false makes the inflated view independent of the parent, not attached to it, which very oddly seems to be the accepted design for custom views within AdapterViews. Why this is so, I find utterly baffling, but the pattern above worked for me.
I'm a beginner also so take this answer with a pinch of salt - if it doesn't work, move on and Google some more. Not familiar with AdapterView since I traditionally have a ListView or GridView and a custom Adapter extended off a BaseAdapter and then listView.setAdapter(myCustomAdapter).
You could try making something like this inside the WeatherAdapter class:
public void addToList(Weather mWeather) {
weatherList.add(mWeather);
}
Then in the class that calls WeatherAdapter:
weatherAdapter.addToList(weatherToAdd);
weatherAdapter.notifyDataSetChanged();
Also you need to optimize it more in the getView method:
http://www.youtube.com/watch?v=wDBM6wVEO70
For the AdaptertView addView method:
void addView(View child)
This method is not supported and
throws an
UnsupportedOperationException when
called." (From Android documentation)
Probably the inflating procedure calls the addView method and this is not possible from an AdapterView, or its AdapterView.
From the documentation:
"An Adapter object acts as a bridge
between an AdapterView and the
underlying data for that view. The
Adapter provides access to the data
items. The Adapter is also responsible
for making a View for each item in the
data set".
I think that the inflating operation could be done from a simple Activity that models your view and all other operations, for example, retrieving data and showing data in other classes.
Hope it will be helpful!
#Axel22's answer is key, but there are a few other things missing from your code. First, you should be extending either BaseAdapter or ArrayAdapter, depending on your preference. Second, you want to get in the practice of using a ViewHolder to avoid making excessive calls to findViewById, and (most importantly) recycling your View.
private Class ViewHolder {
public TextView cityControl;
public TextView temperatureControl;
public ImageView skyControl;
public ViewHolder(TextView cityControl, TextView temperatureControl, ImageView skyControl) {
this.cityControl = cityControl;
this.temperatureControl = temperatureControl;
this.skyControl = skyControl;
}
Your getView function can recycle views and utilize the ViewHolder class as follows:
public View getView(int position, View convertView, ViewGroup parent) {
Weather weather = weatherList.get(position);
// This is how you attempt to recycle the convertView, so you aren't
// needlessly inflating layouts.
View v = convertView;
ViewHolder holder;
if (null == v) {
v = LayoutInflater.from(getContext()).inflate(R.layout.weather_row, parent, false);
TextView cityControl = (TextView)v.findViewById( R.id.city );
TextView temperatureControl = (TextView)v.findViewById( R.id.temperature );
ImageView skyControl = (ImageView)v.findViewById( R.id.sky );
holder = new ViewHolder(cityControl, temperatureControl, skyControl);
v.setTag(holder);
} else {
holder = (ViewHolder) v.getTag();
}
holder.cityControl.setText("Metropolis");
holder.temperatureControl.setText("78");
holder.skyControl.setImageResource(R.drawable.daily_planet);
return v;
}
For tons more examples (and other optimization tips), see this blog post (or just google for ViewHolder).
Related
I'm trying to create a UI similar to Google Keep. I know how to create a staggered View using a Recycler View. If i click a specific Card. Then it has to open a activity.
I can achieve this using onclick method.
This same scenario happens in atleast 5 different Activities in my App.
My question is that
Can I use this single Adapter in all those 5 places ?
If yes, where should i place the onclick actions ?
If no, How can I Create a staggered layout like Keep?
Thanks in Advance!
(See application for RecyclerView below in edits)
Like I mentioned in my comment, it's certainly fine to have separate adapters for all your Activities which use different data and views. As your app data and layouts get more complex, so does your code...that's just the way it is.
But if some of your Activities used similar data in their ListViews -- maybe, for example, two TextViews and an ImageButton -- you could save some effort by defining a single adapter that can be used for multiple Activities. You would then instantiate separate objects for each Activity, similar to the way you would create several ArrayAdapter<String> objects to populate multiple ListViews.
The BaseAdapter is a great class to extend when writing a custom adapter. It's flexible and allows you complete control over the data that's getting shown in your ListView. Here's a minimal example:
public class CustomBaseAdapter extends BaseAdapter {
private Context context;
private ArrayList<String> listData;
public CustomBaseAdapter(Context context, ArrayList<String> listData) {
this.context = context;
this.listData = listData;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.your_list_item_layout, parent, false);
//populate the view with your data -- some examples...
TextView textData = (TextView) convertView.findViewById(R.id.yourTextView);
textData.setText(listData.get(position));
ImageButton button = (ImageButton) convertView.findViewById(R.id.yourImageButton);
button.setOnClickListener(new View.OnClickListener() {
//...
//...
});
}
return convertView;
}
#Override
public Object getItem(int position) {
return 0;
}
#Override
public long getItemId(int position) {
return 0;
}
#Override
public int getCount() {
return listData.size();
}
}
So the key part of this code is obviously the getView() method, which is called every time the ListView needs some data to display. For efficiency, views are stored in something called a convertView so they may be re-used and not have to be inflated every time a view appears on the screen.
So what we do in getView() is first find out if the convertView exists. If it does, we just pass that back to the calling ListView because everything should already be instantiated and ready to go. If the convertView is null, it means the data hasn't been instantiated (or needs to be re-instantiated for whatever reason), and so we inflate a brand new view and populate it with our data.
So in the case of this example adapter above, if several of your Activities all displayed a single list of Strings, you could reuse this adapter for each one, passing in a different ArrayList<String> through the constructor each time you created a new object. But obviously you could pass in more than just Strings if you had more data to show. The level of complexity is up to you. And if the difference among your Activities was too great, I would just create custom versions of this class for all of them and just instantiate them and populate them however you'd like. It will keep all your data very organized and encapsulated.
Hope this helps! Feel free to ask questions for more clarification if you need it.
EDIT IN RESPONSE TO COMMENTS
Since you are using a RecyclerView instead of just plain ListViews (which I, for some reason, totally forgot) you could still do something very similar using a RecyclerView.Adapter<YourViewHolder> instead. The difference would be that instead of inflating the views in a getView() method, they are inflated inside your custom ViewHolder, which I assume you already have. The code might look something like this:
public class CustomRecyclerViewAdapter extends RecyclerView.Adapter<StringViewHolder> {
private final List<String> items;
public CustomRecyclerViewAdapter(ArrayList<String> items) {
this.items = items;
}
#Override
public StringViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//So instead of inflating the views here or in a getView() like in
//in the BaseAdapter, you would instead inflate them in your custom
//ViewHolder.
return new StringViewHolder(parent);
}
#Override
public void onBindViewHolder(StringViewHolder holder, int position) {
holder.setModel(items.get(position));
}
#Override
public long getItemId(int position) {
return items.get(position).hashCode();
}
#Override
public int getItemCount() {
return items.size();
}
}
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'
So, I am making this application. The application parses a website, or more specifically a vbulletin-board. When I'm parsing a thread in the forum, I have divided it up so that when I parse each post in that thread, I get the actual content of the post in sections such as this, and I store the sections in the correct order in an array:
[Plain text]
[Quote from somebody]
[Plain text]
[Another quote]
[Another quote again]
[Some more plain text]
However, a post can be arranged in any order as you might know, and can consist of more or fewer sections than in the example, and it doesn't have to have quotes in it either, or it might just be one or several quotes. Anything is possible.
When I list the posts in my application, I am using a ListView. Each row of this listview will then always consist of a header, and any combination of the previously mentioned sections.
The way I was thinking of doing it after googling a bit about it is to have one "Base-layout" with just a layout-tag in one XML-file, and a separate layout for each section, stored in separate XML-files, and at each call to getView() in my adapter, look at the post at that position in my "Post-list", and then loop through the sections in that particular post, and inflate a new "Quote-layout" for each quote-section stored in the post, and inflate a "Plain-text-layout" for each plain-text-section in the post. And for each of those I fill in all the content belonging to that post.
I think this would work, but there might be a performance problem? As I understand it layout inflation is quite expensive, and I won't be able to recycle the View passed in to getView() either, since it might have a bunch of sections added to it that I might not need in another call to getView().. That is, if I understand getView() and the recycling somewhat.
This is a basic example of what I mean with the getView() method of the adapter:
#Override
public View getView(int i, View view, ViewGroup viewGroup) {
// Inflate the base-layout, which the others are added to.
view = mInflater.inflate(R.layout.forum_post,null);
View header = mInflater.inflate(R.layout.post_header_layout, null);
View message = mInflater.inflate(R.layout.post_text_layout, null);
View quote = mInflater.inflate(R.layout.post_quote_layout, null);
((ViewGroup)view).addView(header);
((ViewGroup)view).addView(message);
((ViewGroup)view).addView(quote);
return view;
}
And then inflate more quote-views/message-views as needed when I extract the data from my list of saved posts.
The base-layout is just a LinearLayout-tag
The layouts I inflate are just RelativeLayouts with some TextViews and an ImageView added.
This code produces this result, where I have a Header with
username, picture, etc.., One section of Plain text, and one Quote-section.
This doesn't seem to work properly all the time though, because when I tried it out just now a copy of the list seemed to get stuck on the background and another one scrolled on top of it..
http://s14.postimg.org/rizid8q69/view.png
Is there a better way to do this? Because I imagine this isn't very efficient
You need to override getViewItemType and getViewTypeCount.
getItemViewType(int position) - returns information which layout type you should use based on position
Then you inflate layout only if it's null and determine type using getItemViewType.
Example :
private static final int TYPE_ITEM1 = 0;
private static final int TYPE_ITEM2 = 1;
private static final int TYPE_ITEM3 = 2;
#Override;
public int getItemViewType(int position)
{
int type;
if (position== 0){ // your condition
type = TYPE_ITEM1; //type 0 for header
} else if(position == 1){
type = TYPE_ITEM2; //type 1 for message
}else {
type = TYPE_ITEM3; //type 2 for Quote
}
return type;
}
#Override
public int getViewTypeCount() {
return 3; //three different layouts to be inflated
}
In getView
int type= getItemViewType(i); // determine type using position.
switch (type) {
case TYPE_ITEM1:
view= mInflater.inflate(R.layout.post_header_layout, null); // inflate layout for header
break;
case TYPE_ITEM2:
view = mInflater.inflate(R.layout.post_text_layout, null); // inflate layout for quote
break;
case TYPE_ITEM3:
quote = mInflater.inflate(R.layout.post_quote_layout, null); // inflate layout for message
break;
....
You need to use a View Holder for smooth scrolling and performance.
http://developer.android.com/training/improving-layouts/smooth-scrolling.html
You can check the tutorial below
http://android.amberfog.com/?p=296
First of all you want to reuse convertView that has been passed as one of the argument. This way you can avoid inflating the item View.
Secondly, you could use something as ViewHolder to store references to your inner Views. Using ViewHolder will increase performance whether you are inflating view or finding them by id as both methods are very expensive.
Set the ViewHolder as a Tag on item View.
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
ViewHolder viewHolder;
// if possible reuse view
if (convertView == null) {
final LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(resource, parent, false);
viewHolder = new ViewHolder(mInflater.inflate(R.layout.post_header_layout, null));
view.setTag(viewHolder);
} else {
// reuse view
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
//set text, listeners, icon, etc.
return view;
}
The ViewHolder is just private inner class storing referenced to view.
private static class ViewHolder {
private final View view;
private ViewHolder(View view) {
this.view = view;
}
}
Talk about ListView usage was given at Google IO 2010.
The inflater needs to know the real type of the futur parent ViewGroup, therefore the following code is erroneous:
view = mInflater.inflate(R.layout.forum_post,null);
and instead, you should use this one:
view = mInflater.inflate(R.layout.forum_post,viewGroup,false);
Same thing for the other inflate: use the real parent (view in this case) or another viewGroup which is of the same type as the (futur) parent; otherwise the LayoutParameters will not be set to the right type and the values that you have specified in your XML code will be lost (never used).
I'm attempting to use CommonsWare's MergeAdapter class and having limited success. In particular, I am not sure if 1) my ArrayAdapter is suitable for use, 2) if I am adding it correctly, and 3) if I am doing all that is necessary to wire everything up.
Here is my subclass of ArrayAdapter:
class PDLAdapter extends ArrayAdapter<PartnerDisease> {
public PDLAdapter(final Context context) {
super(context, 0);
}
#Override
public View getView(final int position, View convertView, final ViewGroup parent) {
ViewHolder viewHolder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.partnerdisease_list_item, null);
viewHolder = new ViewHolder(convertView);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.populateViews(getItem(position));
return convertView;
}
}
Here is my object StructuredSubDisease (the name makes no sense if you actually consider it's a top-level object containing sub diseases, but whatever):
class StructuredSubDisease {
public String headingText;
public ArrayList<PartnerDisease> subDiseases;
public View headingView() {
View returnView = mInflater.inflate(R.layout.partnerdisease_list_item, null);
TextView t = (TextView) returnView.findViewById(R.id.tv_displayname);
t.setText(headingText);
return returnView;
}
}
...and here is where the "magic" is supposed to be happening.
for (StructuredSubDisease s : subDiseaseList) {
mMergeAdapter.addView(s.headingView()); // #Alex, <--- thing 1
PartnerDiseaseListAdapter adapter = new PartnerDiseaseListAdapter(this);
for (PartnerDisease p : s.subDiseases) {
adapter.add(p);
}
mMergeAdapter.addAdapter(adapter); // <--- and thing 2
}
I have Logged the count:
Log.i("mergecount", "" + mMergeAdapter.getCount());
This returns 1, where I would expect 2.
EDIT: I forgot to mention, the result of this is that the headingView() is displayed with the proper heading text, but there is no list beneath it.
Where am I going wrong?
my ArrayAdapter is suitable for use
It seems OK.
if I am adding it correctly
It seems OK.
if I am doing all that is necessary to wire everything up
You don't have any diseases, apparently.
because I added two things - the headingView() (which is rendered) and the adapter (which silently fails)
getCount() returns the number of total rows that should be in your ListView, not the number of things added to the MergeAdapter. In your case, it would appear that you have no diseases.
Start by putting your PartnerDiseaseListAdapter directly into your ListView, ignoring the MergeAdapter. Get that working. Then, switch back to the MergeAdapter.
I'm having problems with some BaseAdapter code that I adapted from a book. I've been using variations of this code all over the place in my application, but only just realized when scrolling a long list the items in the ListView become jumbled and not all of the elements are displayed.
It's very hard to describe the exact behavior, but it's easy to see if you take a sorted list of 50 items and start scrolling up and down.
class ContactAdapter extends BaseAdapter {
ArrayList<Contact> mContacts;
public ContactAdapter(ArrayList<Contact> contacts) {
mContacts = contacts;
}
#Override
public int getCount() {
return mContacts.size();
}
#Override
public Object getItem(int position) {
return mContacts.get(position);
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
if(convertView == null){
LayoutInflater li = getLayoutInflater();
view = li.inflate(R.layout.groups_item, null);
TextView label = (TextView)view.findViewById(R.id.groups_item_title);
label.setText(mContacts.get(position).getName());
label = (TextView)view.findViewById(R.id.groups_item_subtitle);
label.setText(mContacts.get(position).getNumber());
}
else
{
view = convertView;
}
return view;
}
}
You are only putting data in the TextView widgets when they are first created. You need to move these four lines:
TextView label = (TextView)view.findViewById(R.id.groups_item_title);
label.setText(mContacts.get(position).getName());
label = (TextView)view.findViewById(R.id.groups_item_subtitle);
label.setText(mContacts.get(position).getNumber());
to be after the if/else block and before the method return, so you update the TextView widgets whether you are recycling the row or creating a fresh one.
To further clarify the answer of CommonsWare, here is some more info:
The li.inflate operation (needed here for parsing of the layout of a row from XML and creating the appropriate View object) is wrapped by an if (convertView == null) statement for efficiency, so the inflation of the same object will not happen again and again every time it pops into view.
HOWEVER, the other parts of the getView method are used to set other parameters and therefore should NOT be included within the if (convertView == null){ }... else{ } statement.
In many common implementation of this method, some textView label, ImageView or ImageButton elements need to be populated by values from the list[position], using findViewById and after that .setText or .setImageBitmap operations.
These operations must come after both creating a view from scratch by inflation and getting an existing view if not null (e.g. on a refresh).
Another good example where this solution is applied for a ListView ArrayAdapter appears in https://stackoverflow.com/a/3874639/978329