I have a String[][] of data and I am trying to make a custom listView from it.
Here is the data
String[][] myDataArray = {{"cat1","cat2","cat3","cat4"},
{"dog1","dog2","dog3"},
{"lion1"},
{"monkey1","monkey2"}};
And now here is how I am trying to display this data in my listView. I want each array within the array to have its own row. So all the cats will be in one row (cell), all the dogs will be in another row and so on. Here is a picture to make it clear each item in the row, is a textView.
I have made cell_4.xml, cell_3.xml, cell_2.xml, cell_1.xml layout file for each of the rows.
And then in the activity that I am trying to show this, I just have a plain old listView.
Now I am not quite sure how to edit/ adapt the data. I have to display it in this way. So that it uses the correct cell layout for each array within the String[]. I was thinking about using a switch statement to get the number of items in each inner array. But having some trouble with the ArrayAdapter. To get it set up.
I have looked at a couple of examples on stackoverflow like this one Custom ListView Android to try and figure this out but can't get it.
EDIT
Here is trying to set up adapter and call MyListViewAdapter, but I don't know what to set as context.
here is the code:
private void handleData(String[][] data){
BaseAdapter adapter = MyListAdapter(context, data);
ListView list = (ListView) findViewById(R.id.mealsListView);
list.setAdapter(adapter);
}
Some thoughts:
1) If you are determined to use ListView, skip this point. Else, you might be interested in GRIDVIEW that natively support a table structure.
2) Your idea is consistent. ListView only knows about ROWS, so your adapter will be called for you to display a ROW, and it's up to you to transform the array in that row into an element with multiple cells. You'll do that in getView()
3) You'll make use of the Item Types (getViewTypeCount and getItemViewType) to declare you have different item types. Each type will be a row with a given number of cells: 1,2,3,4...
you will override getViewTypeCount() to return the maximum number of cells in a row
you will either inflate a static layout for the number of cells a row has, or generate it dynamically
Let's get started ... First of all in the adapter we override the Type methods to declare
our rows will be of different types:
#Override
public int getViewTypeCount() {
return 4;
// you have 4 types of rows.
// SUPER IMPORTANT: No row in the array can have more cells than this number
// or getView will crash (you'd have to define additional layouts)
}
#Override
public int getItemViewType(int position) {
// for a given position, you need to return what type is it. This number ranges
// from 0 to itemtypecount-1. We return the length of the array (number of cells)
// this function is called by the View Recycler to appropriately pass you the
// correct view to reuse in convertView
return myDataArray[position].length - 1;
}
And then we need to implement getView(). The typical implementation will be the first one, where you create different XMLs, and the second one is a more advanced implementation where we dynamically create the layouts without any xml.
First Case: Static Layouts
Ideal if you limit the Row Array Length to say 3 or 4, to avoid creating dozens of layouts. So you define 4 xmls (ie. row_1_childs, row_2_childs, row_3_childs, row_4_childs) that will be the templates of rows with that number of children. Then,
and then in GetView:
// we define an array of layout ids to quickly select the layout to inflate depending on
// the number of rows:
private final static int[] sLayouts=new int[] {
R.layout.row_1_childs,
R.layout.row_2_childs,
R.layout.row_3_childs,
R.layout.row_4_childs
};
public View getView (int position, View convertView, ViewGroup parent) {
int maxcells=myDataArray[position].length;
if (convertView == null) {
// generate the appropriate type
if (maxcells<=sLayout.length) {
// just check we are in bounds
convertView=LayoutInflater.from(parent.getContext()).inflate(sLayout[maxcells-1], null);
} else {
// you have a row with too many elements, need to define additional layouts
throw new RuntimeException ("Need to define more layouts!!");
}
}
// At this point, convertView is a row of the correct type, either just created,
// or ready to recycle. Just fill in the cells
// for example something like this
ViewGroup container=(ViewGroup)convertView;
for (int i=0; i<maxcells; i++) {
// We assume each row is a (linear)layout whose only children are textviews,
// one for each cell
TextView cell=(TextView)container.getChildAt(i); // get textview for cell i
cell.setText(myDataArray[position][i]);
cell.setTag( new PositionInfo(position, i)); // we store the cell number and row inside the TextView
cell.setOnClickListener(mCellClickListener);
}
return convertView;
}
Second Case: Dynamic Layouts
Another solution would be to dynamically generate the rows, and dynamically generate as many text views as you might need. To do so, keep overriding getViewTypeCount() to return the Maximum number of children, and define getView like this:
public View getView (int position, View convertView, ViewGroup parent) {
String rowData=myDataArray[position];
if (convertView==null) {
// generate a LinearLayout for number of children:
LinearLayout row=new LinearLayout(context);
for (int i=0, len=rowData.length(); i<len; i++) {
// generate a textview for each cell
TextView cell=new TextView(parent.getContext());
// we will use the same clicklistener (very efficient)
cell.setOnClickListener(mCellClickListener);
row.addView(cell, new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1)); // same width for each cell
}
convertView=row;
}
// here convertView has the correct number of children, same as before:
ViewGroup container=(ViewGroup)convertView;
for (int i=0, len=rowData.length(); i<len; i++) {
TextView cell=(TextView)container.getChildAt(i);
cell.setText(rowData[i]);
cell.setTag( new PositionInfo(position, i)); // we store the cell number and row inside the TextView
}
return convertView;
}
// auxiliar class to store row and col in each textview for the clicklistener
private class PositionInfo {
public int row, col;
public PositionInfo(int row, int col) { this.row=row; this.col=col; }
}
// trick: only one clicklistener for millions of cells
private View.OnClickListener mCellClickListener=new View.OnClickListener() {
#Override
public void onClick(View v) {
PositionInfo position=(PositionInfo)v.getTag(); // we stored this previously
// you pressed position.row and position.col
}
}
Solution (1) is cool to manually create the layouts and configure them a lot.
Solution (2) is cool to programmatically support any number of cells, in case they are very different
Both solutions are pretty efficient, because they play nice with the View recycler: If you fail to use View Types and you constantly inflate layouts, your ListView will be laggy and waste a lot of memory and resources.
You might run into problems if the size of each string in the row varies and you might then have to push data onto the next line.
Try using an alternate view, if your aim is categorization of similar data, expandable listview is an option to consider.
You will need to make your own adapter by extending BaseAdapter. You can check the data's size the getView() method, and inflate the correct layout.
UPDATE:
public class MyListAdapter extends BaseAdapter{
String[][] mData;
LayoutInflater mLayoutInflater;
public MyListAdapter(Context context, String[][] data) {
mData = data;
mLayoutInflater = LayoutInflater.from(context);
}
#Override
public int getCount() {
return mData.length;
}
#Override
public Object getItem(int position) {
return null;
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
String data[] = mData.get(position);
switch(data.length){
case 4:
convertView = mLayoutInflater.inflate(R.layout.cell_4, parent, false);
TextView t1 = (TextView) convertView.findViewById(R.id.one);
t1.setText(data[0]);
break;
case 3:
convertView = mLayoutInflater.inflate(R.layout.cell_3, parent, false);
break;
case 2:
convertView = mLayoutInflater.inflate(R.layout.cell_2, parent, false);
break;
case 1:
convertView = mLayoutInflater.inflate(R.layout.cell_1, parent, false);
break;
default:
convertView = mLayoutInflater.inflate(R.layout.blank, parent, false);
}
return convertView;
}
}
Related
I am trying to determine the best way to have a single listview contains different rows styles. I know how to create a custom row + custom array adapter to support a custom row for the entire list view. But how can one listview support many different row styles?
Since you know how many types of layout you would have - it's possible to use those methods.
getViewTypeCount() - this methods returns information how many types of rows do you have in your list
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.
Look at this tutorial for further information.
To achieve some optimizations in structure that you've described in comment I would suggest:
Storing views in object called ViewHolder. It would increase speed because you won't have to call findViewById() every time in getView method. See List14 in API demos.
Create one generic layout that will conform all combinations of properties and hide some elements if current position doesn't have it.
I hope that will help you. If you could provide some XML stub with your data structure and information how exactly you want to map it into row, I would be able to give you more precise advise. By pixel.
You have to write you custom adapter which extends BasaAdapter. NOTE: viewTypeCount(). Then you can use condition to check and assign different views
private class MyAdapter extends BaseAdapter{
public static final int ITEM_TYPE_ONE = 1;
public static final int ITEM_TYPE_TWO = 2;
#Override
public int getViewTypeCount() {
return 2;
}
#Override
public int getItemViewType(int position) {
return (position % 2 == 0) ? ITEM_TYPE_ONE : ITEM_TYPE_TWO;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
int itemTypeId = getItemViewType(position);
if (convertView == null) {
if (itemTypeId == ITEM_TYPE_ONE) {
convertView = inflater.inflate(R.layout.layout_one, parent, false);
} else {
convertView = inflater.inflate(R.layout.layout_two, parent, false);
}
}
}
}
Just create an attribute and myListView.addView('your inflate view')
LayoutInflater inflater = LayoutInflater.from(this);
RelativeView myCustomView =(RelativeView)inflater.inflate(R.layout.task_since_user);
myListView.addView(myCustomView);
The scenario
I'm trying the create something akin to the featured page on Google Play Store. But instead of showing three items for a category I'm allowing it show any number of items in a two column staggered grid view fashion.
So each list item has a header that has a title and a description followed by a custom view (lets call this SVG, as in Staggered View Group) that shows some number of children views in a staggered grid view fashion.
I have a class called FeaturedItems that hold the data for a row in the featured list. Here is an extract:
public class FeaturedItems {
private String mName;
private String mDescription;
private ArrayList<Object> mList;
public FeaturedItems(String name, String description, Object... items) {
mName = name;
mDescription = description;
mList = new ArrayList<Object>();
for (int i = 0; i < items.length; i++) {
mList.add(items[i]);
}
}
public int getItemCount() {
return mList.size();
}
public Object getItem(int position) {
return mList.get(position);
}
public String getFeatureName() {
return mName;
}
public String getFeatureDescription() {
return mDescription;
}
}
The FeaturedListAdapter binds the data with the views in the getView() method of the adapter. The getView() method is as follows:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
FeaturedItems items = getItem(position);
if (convertView == null) {
LayoutInflater infalInflater = (LayoutInflater) this.mContext
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = infalInflater.inflate(mResource, null);
holder = new ViewHolder();
holder.title = (TextView) convertView.findViewById(R.id.list_item_shop_featured_title);
holder.description = (TextView) convertView.findViewById(R.id.list_item_shop_featured_description);
holder.svg = (StaggeredViewGroup) convertView.findViewById(R.id.list_item_shop_featured_staggered_view);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.title.setText(items.getFeatureName());
holder.description.setText(items.getFeatureDescription());
// HELP NEEDED HERE
// THE FOLLOWING PART IS VERY INEFFICIENT
holder.svg.removeAllViews();
for (int i = 0; i < items.getItemCount(); i++) {
FeaturedItem item = new FeaturedItem(mContext, items.getItem(i));
item.setOnShopActionsListener((ShopActions) mContext);
holder.svg.addView(item);
}
return convertView;
}
The problem
In the getView() method, each time a view is returned, it removes all the child views in the SVG and instantiates new views called FeaturedItem that are then added to the SVG. Even if the SVG in a particular row, say first row, was populated, when the user scrolls back to it from the bottom, the getView() method will remove all the children views in the SVG and instantiates new views to be populated with.
The inefficiency here is very obvious, and the list view animation skips frames when scrolled quite often.
I can't just reuse the convertView here because it shows the wrong featured items in the StaggeredViewGroup. Therefore I have to remove all children from the StaggeredViewGroup and instantiate and add the views that are relevant to the current position.
The question
Is there a way around this problem? Or are there some alternative approaches to creating a page similar to the Google Play Store featured page, but with each row having different number of featured items thus having its unique height?
There should be an easy way to improve this solution. Just reuse the svg children that are already present, add new ones if they are not enough, and then remove any surplus ones.
For example (in semi-pseudocode, method names may not be exact):
for (int i = 0; i < items.getItemCount(); i++)
{
if (i < svg.getChildCount())
{
FeaturedItem item = i.getChildAt(i);
// This item might've been set to invisible the previous time
// (see below). Ensure it's visible.
item.setVisibility(View.VISIBLE);
// reuse the featuredItem view here, e.g.
item.setItem(items.getItem(i));
}
else
{
// Add one more item
FeaturedItem item = new FeaturedItem(mContext, items.getItem(i));
...
holder.svg.addView(item);
}
}
// hide surplus item views.
for (int i = items.getItemCount(); i < svg.getChildCount(); i++)
svg.getChildAt(i).setVisibility(View.GONE);
/**
as an alternative to this last part, you could delete these surplus views
instead of hiding them -- but it's probably wasteful, since they may need
to be recreated later
while (svg.getChildCount() > items.getItemCount())
svg.removeChildView(svg.getChildCount() - 1);
**/
I have a ListView that's being populated by an ArrayAdapter:
someListView.setAdapter(adapter);
Each element in the adapter is inflated using the same layout.xml. Now I want to add an element of a different type (inflated using a different layout file) to the beginning of the ListView.
What I want to achieve is, to have a special element on top of all other elements in the list view, but also scrolls with the list (exits the screen from top if the user scrolls down).
I've tried to add the new element to the array but it's a different type so that won't work.
I've tried to insert a dummy element to the array at position 0, and modify the adapter's getView() so that if (position == 0) return myUniqueView, but that screwed up the entire list view somehow: items not showing, stuff jumping all over the place, huge gaps between elements, etc.
I start to think the best practice of achieving what I want, is not through editing the array adapter. But I don't know how to do it properly.
You don't need anything special to do what you ask. Android already provides that behavior built in to every ListView. Just call:
mListView.addHeaderView(viewToAdd);
That's it.
ListView Headers API
Tutorial
Do't know exactly but it might usefull
https://github.com/chrisjenx/ParallaxScrollView
In your adapter add a check on the position
private static final int LAYOUT_CONFIG_HEADER = 0;
private static final int LAYOUT_CONFIG_ITEMS = 1;
int layoutType;
#Override
public int getItemViewType(int position) {
if (position== 0){
layoutType = LAYOUT_CONFIG_HEADER;
} else {
layoutType = LAYOUT_CONFIG_ITEMS;
}
return layoutType;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
LayoutInflater inflater = null;
int layoutType = getItemViewType(position);
if (row == null) {
if (layoutType == LAYOUT_CONFIG_HEADER) {
//inflate layout header
}
} else {
//inflate layout of others rows
}
}
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 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