I have a LinearLayout view that already contains several elements. I want to add a lot more Views to it, programmatically. And because this is inside a ScrollView, everything will be scrolled.
So what I do is go through my list, and add new instances of my custom View to it. That custom view inflates a XML layout and adds a few methods.
This approach works well. The problem is that it's super slow, even without any crazy code... a list with 10 items takes around 500ms to instantiate. As an user experience standpoint, this is hard to swallow.
My question is, is this the correct/best approach? Android seems to take a lot of time inflating the layout, even though "R.layout.my_list_item" is super simple. I wonder if there's a way to maybe to reuse "inflated" layouts for additional views, kinda caching the more complex parsing?
I've tried doing this with a ListView (and adapter and a wrapper) and it seems to be much faster. The problem is that I can't use a simple ListView; my layout is more complex than a simple list (the LinearLayout itself contains additional custom icons, and it has another parent with even more Views before it's wrapped by the ScrollView).
But is there a way to use an adapter for a LinearLayout? Would that be faster than trying to add the views myself?
Any help is appreciated. I'd love to make this faster.
Code follows.
Main Activity:
// The LinearLayout that will contain everything
lineList = (LinearLayout) findViewById(R.id.lineList);
// Add a lot of items for testing
for (int i = 0; i < 10; i++) {
addListItem("Item number " + i);
}
protected void addListItem(String __title) {
MyListItem li;
li = new MyListItem(this);
li.setTitle(__title);
lineList.addView(li);
}
MyListItem:
public class MyListItem extends RelativeLayout {
protected TextView textTitle;
public MyListItem(Context __context) {
super(__context);
init();
}
public MyListItem(Context __context, AttributeSet __attrs) {
super(__context, __attrs);
init();
}
public MyListItem(Context __context, AttributeSet __attrs, int __attrsdefStyle) {
super(__context, __attrs, __attrsdefStyle);
init();
}
protected void init() {
// Inflate the XML layout
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.my_list_item, this);
// Create references
textTitle = (TextView)findViewById(R.id.textTitle);
}
public void setTitle(String __text) {
textTitle.setText(__text);
}
}
What I'm trying to accomplish is this. Consider this layout:
This layout is a FrameLayout (outer box) containing a ImageView (in gray), a TextView (inner rectangle, on top) and a LinearLayout (inner rectangle, on bottom). This LinearLayout rectangle is the one I'm dynamically populating with a few items.
After I populate it, I want the final result to be this (where every new rectangle is a new MyListItem instance):
That is, everything is scrollable (the background image, for example, is aligned on top). The LinearLayout isn't scrollable by itself (everything else follows) hence why a ListView, from what I know, wouldn't work very well
in my case.
3 Options:
Replace everything with a ListView, with the other parent and custom icons as a header view for the ListView. ListView is faster, because it only creates Views as it needs them.
Programatically create the contents of my_list_item instead of inflating, might be quicker
Use of ViewStubs may allow you to load views on-demand.
Maybe it isn't loading the views but the data? in which case prepare the data in a background thread.
A ListView is the way to go.
You say that your layout is too complex. But it is completely okay to inflate a complex layout as a child. For example a layout that has text and then an icon:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
Could be inflated in your adapter as so:
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
LinearLayout root = null;
ImageView editImageView;
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
root = (LinearLayout)inflater.inflate(R.layout.item, null);
} else {
root = (LinearLayout)convertView;
}
}
You can also be a little more clever in order to support a header. Just add a check if the index is the root and inflate a different view. Since the header is the only one that is different you will still take advantage of all the other rows being reusable. You can even pre-inflate and store the header and reuse it to completely get rid of inflation.
Just use a ListView!
It's the easiest to set up and easiest to maintain. You define an XML layout for the List-Row, and an XML layout for the View which holds the entire List. The ListAdapter does the rest for you.
Just create a:
List<HashMap<String, String>> services = new ArrayList<HashMap<String, String>>();
...and loop through your data to add as many items as you like to the Map. Then set this map to your ListAdapter. Whether 10 items or 100 items the ListAdapter will create a List with that many items.
Example:
public void updateProductList(String searchTerm) {
createOrOpenDB(this);
Cursor cursor = (searchTerm!=null)? dbAdapter.fetchWhere(TBL_NAME, KEY_NAME+" LIKE '%"+searchTerm+"%'")
: dbAdapter.fetchAll(TBL_NAME);
int rows = cursor.getCount();
if (rows<=0) return;
services.clear(); //clear current list
for (int i=0; i<rows; i++) {
HashMap<String, String> map = new HashMap<String, String>();
cursor.moveToPosition(i);
map.put(KEY_NAME, "" + cursor.getString(cursor.getColumnIndex(KEY_NAME)));
map.put(KEY_DESC, "" + cursor.getString(cursor.getColumnIndex(KEY_DESC)));
map.put(KEY_COST, "" + cursor.getDouble(cursor.getColumnIndex(KEY_COST)));
services.add(map);
}
cursor.close();
closeDB();
ListAdapter adapter = new SimpleAdapter(this, services, R.layout.products_view_row,
new String[] {KEY_NAME, KEY_DESC, KEY_COST},
new int[] {R.id.listViewText1, R.id.listViewText2, R.id.listViewText3});
setListAdapter(adapter);
}
Related
As a little eperiment, I'm trying to do the following.
I have an AXML describing a vertical linear layout which contains a listview (only filling 200dp of the vertical linear layout ). The AXML is inflated when the activity starts with SetContentView. Then the listview is correctly populated with values using its Adapter.
In the GetView method of the listview Adapter, I am trying to also dynamically create a button and add it to the linear layout, but for some reason the button is not added.
If I try to add the button in the constructor method of the Adapter instead, it is correctly added.
Can you tell me what could be possibly going wrong?
Let me add some code:
class TracksAdapter : BaseAdapter<string> {
Activity context;
List<Dictionary<string,string>> trackList;
// constructor
public TracksAdapter (Activity context, List<Dictionary<string,string>> trackList) {
this.context = context;
this.trackList = trackList;
// Just as a little test, if I create the button from here it will be correctly added to linear layout:
var ll = context.FindViewById<LinearLayout>(Resource.Id.linLayForResultsActivity);
Button b1 = new Button(context);
b1.Text = "Btn";
ll.AddView(b1);
}
public override View GetView(int position, View oldView, ViewGroup parent) {
// if I create the button from here it will not be added to the layout
var ll = context.FindViewById<LinearLayout>(Resource.Id.linLayForResultsActivity);
Button b1 = new Button(context);
b1.Text = "Btn";
ll.AddView(b1);
// this other code is working
View view = context.LayoutInflater.Inflate(Resource.Layout.ResultItem, null);
var artistLabel = view.FindViewById<TextView>(Resource.Id.resultArtistNameTextView);
artistLabel.Text = trackList[position]["trackArtistName"];
return view;
}
}
Update: adding some more context information because I know this can be a bit weird to understand without it:
In GetView, I don't need to return the new button I am trying to create there. GetView only need to return a listview view item, but, along its execution, GetView also has to create and add a button to the linear layout containing the listview.
The real code is much more complex than that. I have simplified it in the question. In the real code, the listview items are made of text and a button. The GetView also attaches event handlers to the buttons. Then what I need is, when a user clicks a button in any of the listview items, another button is added below the listview. So I need the code for adding another button to be in GetView, and the button needs to be added outside of the listview, ie. to the linear layout containing the listview.
Use the LayoutInflator to create a view based on your layout template, and then inject it into the view where you need it.
LayoutInflater vi = (LayoutInflater) getApplicationContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = vi.inflate(R.layout.your_layout, null);
// fill in any details dynamically here
TextView textView = (TextView) v.findViewById(R.id.a_text_view);
textView.setText("your text");
// insert into main view
ViewGroup insertPoint = (ViewGroup) findViewById(R.id.insert_point);
insertPoint.addView(v, 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT));
I looked in you code, you are returning view, while you add the button to ll, you should return ll
what you return in getView() is what you see in the list item layout, since you're adding the button to ll and returning view, the button won't appear.
you can add the button to view as you implementation
Also check this:
Try using boolean addViewInLayout (View child, int index, ViewGroup.LayoutParams params)
http://developer.android.com/reference/android/view/ViewGroup.html#addViewInLayout(android.view.View, int, android.view.ViewGroup.LayoutParams)
It's working... Without making any changes now it's working as it should... ! Ugh!
I really don't know what I was doing wrong here... probably it was because of some sort of caching of older version of the installed APK.. ? I know this sort of stuff can happen, and that's why I've always been uninstalling the app before deplyoing the new version to the device... but still...!
I am having a RecyclerView with a Custom Adapter which extends RecyclerView.Adapter
I am Creating LinearLayout with Textview at Runtime and inflating it in each row of RecyclerView
For Example, 1st Row of RecyclerView will have 2 or 3 Textview created at runtime, 2nd Row will have 2 or 3 Textviews created at runtime, 3rd Row will have some Textviews...and so on...
Its working almost Prefect if I check my Log... But when I scroll it down, it just places some textview in wrong places, means I get previous Textviews again when I scroll down in wrong places
#Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
//Movie movie = mItems.get(i);
hm2 = new HashMap<String, ArrayList<PD_Data>>();
sub_rows2 = new ArrayList<PD_Data>();
hm2=categories.get(i);
String key=hm2.keySet().toArray()[0].toString();
sub_rows2=hm2.get(key);
Log.i(LOG_TKT,key);
viewHolder.textview_category.setText(key);
LayoutInflater inflater;
View new_sub_row;
for(int x=0;x<sub_rows2.size();x++){
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
new_sub_row = inflater.inflate(R.layout.recyclerview_pd, null);
TextView heading2 = (TextView)new_sub_row.findViewById(R.id.heading2);
heading2.setText(sub_rows2.get(x).sub_heading);
Log.i(LOG_TKT,sub_rows2.get(x).sub_heading);
TextView detail2 = (TextView)new_sub_row.findViewById(R.id.detail2);
detail2.setText(sub_rows2.get(x).value);
Log.i(LOG_TKT, sub_rows2.get(x).value);
viewHolder.linearlayout_recyclerview_pd.addView(new_sub_row);
}
//viewHolder.imgThumbnail.setImageResource(movie.getThumbnail());
hm2 = new HashMap<String, ArrayList<PD_Data>>();
sub_rows2 = new ArrayList<PD_Data>();
}
What am I doing wrong ?
It is obvious from your question that you aren't familier with how to use RecyclerViews. You need to read up on the subject. Here is a good start.
Basically, onBindViewHolder() is only responsible for binding your data to your viewHolders which hold your item layouts provided by you through onCreateViewHolder(). The reason for that is that RecyclerView recycles your views so it doesn't have to create new views every time you scroll.
In your case, it appears that you would need to use some technique that will tell the RecyclerView to use different viewHolders for different items. See how you can do it here.
I have given answer for better practice of binding data with ViewHolder in another question. You can check it here RecyclerView causes issue when recycling I hope this will help you. This will solve you problem well.
EDIT
Have you solved your problem yet? If not , consider my advice. You know the problem right? suppose you create a ViewHOlder for item 0 and add some text in it. While scrolling suppose this ViewHolder recycled for item number 10, then according to your code it will add new text rows along with some text rows you added for item number 0. You can solve it like
LayoutInflater inflater;
View new_sub_row;
//check here if the linear layout already has previusely added child, if yes remove them
if(viewHolder.linearlayout_recyclerview_pd.getChildCount()>0){
viewHolder.linearlayout_recyclerview_pd.removeAllViews();
}
for(int x=0;x<sub_rows2.size();x++){
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
new_sub_row = inflater.inflate(R.layout.recyclerview_pd, null);
TextView heading2 = (TextView)new_sub_row.findViewById(R.id.heading2);
heading2.setText(sub_rows2.get(x).sub_heading);
Log.i(LOG_TKT,sub_rows2.get(x).sub_heading);
TextView detail2 = (TextView)new_sub_row.findViewById(R.id.detail2);
detail2.setText(sub_rows2.get(x).value);
Log.i(LOG_TKT, sub_rows2.get(x).value);
viewHolder.linearlayout_recyclerview_pd.addView(new_sub_row);
}
I want to write an activity that is similar to the about screen of android phones. I want it to display some information in the style of the about screen of android phones.
Like this
title1
info
-----------------
title2
info
-----------------
etc.
Is there a special view that I can use or is it just a result of multiple views placed in a specific way? Or is there an activity template in android studio that I can use?
Use ListView. You can create a custom layout for cells and then use an array or a cursor to fill the data.
ListView: A view that shows items in a vertically scrolling list. The
items come from the ListAdapter associated with this view.
ListAdapter can receive data as input. The adapter would inflate the layout for each cell in its getView() method and assign the data to the individual views in the cell.
Read more about ListView here: http://developer.android.com/reference/android/widget/ListView.html
See PreferenceActivity or PreferenceFragment. They are special list views populated either from code or from a xml file. There are many different preference types to choose from (checkbox, switch, list etc)
An example preference fragment:
You can use ListView and a custom ArrayAdapter to create a screen like that. If you need any help about how to create a custom ArrayAdapter check this useful tutorial here.
If you want to create a simple list, then ListView is probably the simplest option. You may also want to look into ListActivity and/or ListFragment as well to further simplify the process.
If you intend to use complex animations, or have the list update dynamically with animations, you may be better served with RecyclerView, although using it is more complex.
An straightforward implementation of ListActivity could look something like this:
public class MainActivity extends ListActivity {
String[] titles = { "title one", "title two" };
String[] descriptions = { "desc 1", "desc 2" };
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setListAdapter(new ListAdapter() {
leave everything the same, except for getCount() and getView()
#Override
public int getCount() {
return titles.length;
}
This will ensure you list is always the correct length as your array.
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) parent.getContext().
getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.row, parent, false);
} else {
view = convertView;
}
TextView title = (TextView) view.findViewById(R.id.title);
TextView description = (TextView) view.findViewById(R.id.description);
title.setText(titles[position]);
description.setText(descriptions[position]);
return view;
}
And row.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/title"
android:textSize="24sp"
android:textStyle="bold"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/description"
android:textSize="20sp"/>
</LinearLayout>
I have a ListView in my application. The adapter for this listview contains multiple item view types (around 5 till now), via which I can inflate different types of row views inside the listview.
All row views inflated inside the adapter are custom subclassed view/view group.
public class CustomView1 extends RelativeLayout {
Bundle bundle;
public CustomView1(Bundle bundle) {
super(context);
this.bundle = bundle;
addSubViews(bundle.getBundleList("list"));
}
private void addSubViews(ArrayList<Bundle> list) {
for(Bundle element : list) {
//add sub views via reflection
View view = (View) Class.forName(packageName + type).getConstructor(Bundle.class).newInstance(element);
addView(view);
}
}
//called from getView() in adapter when convertView != null
public void onRecycle(Bundle bundle) {
if(bundle != this.bundle) {
this.bundle = bundle;
removeAllViews();
addSubViews(bundle.getBundleList("list"));
}
}
}
Bundle passed to each custom view contains layout info for that view. In this way, I can create and add any view/viewgroup inside any viewgroup. All well till now.
Now the problem comes when this code runs inside ListView. Since all the view types are created by the adapter initially, scrolling jerks a lot because the adapter keeps on creating new custom views of different itemViewType. How to reduce those jerks in listview ? Any ideas? In the listview, all viewTypes are different at the top 5 positions, so the adapter has to create these views and that makes the experience sluggish.
Even when the adapter recycles similar view type convertViews after 5th index, I clear the container using removeAllViews() and run this loop again because the subView bundle list of the incoming bundle from 6th position onwards might be different. So in the end, adapter is only recycling empty ViewGroups. Since the subView list can possibly contain anything (maybe one more bundle list inside any element bundle), I have to do removeAllViews() to accommodate new subview tree in the recycled convertView.
I thought of using vertical ScrollView but that would take too much memory upfront, and the number of custom views inflated is dynamic, can increase to 20.
The app is running but the scroll is so bad there is hardly any usability left, so its looking like till now I have achieved nothing by adding so much dynamic behavior also. Please suggest me ways to counter this problem.
I am suspecting that the use of setLayoutParams inside CustomView classes may be stopping the scroll because I set the width/height of all views after they are created.
Update #1 getView() code using ViewHolder pattern
ViewHolder holder;
if(convertView == null) {
holder = new ViewHolder();
holder.customView1 = new CustomView1(bundle);
convertView = holder.customView1;
convertView.setTag(holder);
} else {
holder = (ViewHolder)convertView.getTag();
}
holder.customView1.onRecycle(bundle);
ListView has excellent support for different View types. Just make sure to use view holder pattern to avoid jerky scrolling and then override getViewTypeCount() and getItemViewType().
More detail http://android.amberfog.com/?p=296
I have an application that will have 5-15 buttons depending on what is available from a backend. How do I define the proper GridView layout files to include an array of buttons that will each have different text and other attributes? Each button will essentially add an item to a cart, so the onClick code will be the same except for the item it adds to the cart.
How can I define an array so I can add a variable number of buttons, but still reference each of them by a unique ID? I've seen examples of the arrays.xml, but they have created an array of strings that are pre-set. I need a way to create an object and not have the text defined in the layout or arrays xml file.
Update - Added info about adding to a GridView
I want to add this to a GridView, so calling the [addView method](http://developer.android.com/reference/android/widget/AdapterView.html#addView(android.view.View,%20int) results in an UnsupportedOperationException. I can do the following:
ImageButton b2 = new ImageButton(getApplicationContext());
b2.setBackgroundResource(R.drawable.img_3);
android.widget.LinearLayout container = (android.widget.LinearLayout) findViewById(R.id.lay);
container.addView(b2);
but that doesn't layout the buttons in a grid like I would like. Can this be done in a GridView?
In the following code, you should change the upper limits of the for to a variable.
public class MainActivity
extends Activity
implements View.OnClickListener {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TableLayout layout = new TableLayout (this);
layout.setLayoutParams( new TableLayout.LayoutParams(4,5) );
layout.setPadding(1,1,1,1);
for (int f=0; f<=13; f++) {
TableRow tr = new TableRow(this);
for (int c=0; c<=9; c++) {
Button b = new Button (this);
b.setText(""+f+c);
b.setTextSize(10.0f);
b.setTextColor(Color.rgb( 100, 200, 200));
b.setOnClickListener(this);
tr.addView(b, 30,30);
} // for
layout.addView(tr);
} // for
super.setContentView(layout);
} // ()
public void onClick(View view) {
((Button) view).setText("*");
((Button) view).setEnabled(false);
}
} // class
Here's a nice sample for you:
https://developer.android.com/guide/topics/ui/layout/gridview.html
You should just create buttons instead of imageviews in getView adapter method.
If you are using a GridView, or a ListView (etc), and are producing Views to populate them via the adapter getView(pos, convertView, viewGroup), you might encounter confusion (i did once).
If you decide to re-use the convertView parameter, you must reset everything inside of it. It is an old view being passed to you by the framework, in order to save the cost of inflating the layout. It is almost never associated with the position it was in the layout before.
class GridAdapter extends BaseAdapter // assigned to your GridView
{
public View getView(int position, View convertView, ViewGroup arg2) {
View view;
if (convertView==null)
{
view = getLayoutInflater().inflate(R.layout.gd_grid_cell, null);
}
else
{
// reusing this view saves inflate cost
// but you really have to restore everything within it to the state you want
view = convertView;
}
return view;
}
// other methods omitted (e.g. getCount, etc)
}
I think this represents one of those Android things where the concept is a little difficult to grasp at first, until you realize there's a significant optimization available within it (have to be nice to CPU on a little mobile device)