I'm developing an Android app in which I've got some lists. It's more complex than I'll explain here, but the scenario I'm posting behaves unexpectedly as well.
I've been browsing several questions in Stack Overflow but none that I found solves this issue.
So, for the sake of the question I've built this scenario. I've got an Activity with an array of Strings, like so:
public class TestViewlazyloadActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
String[] items = { "lorem", "ipsum", "dolor", "sit", "amet" };
ListView list = (ListView) findViewById(R.id.testListView);
TestAdapter adapter = new TestAdapter(this, R.layout.list_item, items);
list.setAdapter(adapter);
}
}
Being my two layouts this way:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<ListView
android:id="#+id/testListView"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
</ListView>
</LinearLayout>
And this way:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<TextView
android:id="#+id/testTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout>
Pretty simple. Then I've implemented an ArrayAdapter class with the ViewHolder idea, which getView() method is as follows:
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
String item = this.getItem(position);
if(convertView == null) {
LayoutInflater inflater = ((Activity) this.getContext()).getLayoutInflater();
convertView = inflater.inflate(R.layout.list_item, null);
holder = new ViewHolder();
holder.textView = (TextView) convertView.findViewById(R.id.testTextView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
final ViewHolder auxHolder = holder;
LoadingService.capitalizeItemWithDelay(item, new LoadedCallback() {
public void onItemLoaded(String item) {
auxHolder.textView.setText(item);
}
});
return convertView;
}
Being LoadedCallback an interface with just onItemLoaded. And LoadingService.capitalizeItemWithDelay() just starts a Thread which sleeps for a random amount of time (to simulate a lazy load) and calls this callback with the upper case String.
Normal behavior should be that user is viewing the list with 5 blank rows. After some time items begin to spawn in their respective rows, even in a random order given the random sleep timer.
What really happens is that items get loaded in the first row, one by one, and eventually some of them get set in their real place. It's like the auxHolder keeps the reference in its TextView to the first element, which is weird. I understand it's either because of the view being recycled or because of the auxHolder being final.
Any ideas? Thanks in advance.
Get rid of final on your ViewHolder variable and move ViewHolder auxHolder into the inner class.
Try this:
...
LoadingService.capitalizeItemWithDelay(item, new LoadedCallback() {
ViewHolder auxHolder = holder;
public void onItemLoaded(String item) {
auxHolder.textView.setText(item);
When you use the final key word with a local variable, like auxHolder, you are saying that the value you give it, is the last value it will have. This is why you are having problems. Here is a link that will explain a little better: http://www.javapractices.com/topic/TopicAction.do?Id=23
You may not be updating on the UI Thread.
I would try performing your loading actions in a post and see that fixes everything.
Try doing something like this
convertView.post(new Runnable() {
#Override
public void run() {
LoadingService.capitalizeItemWithDelay(item, new LoadedCallback() {
ViewHolder auxHolder = holder;
public void onItemLoaded(String item) {
auxHolder.textView.setText(item);
}
}
}
});
Related
I created a custom listview layout with images which are loaded from web like this:
http://i.stack.imgur.com/l8ZOc.png
It works fine when scrolling down. However, when you scroll down, the previous items go out of screen then destroyed. When you try to scroll up again, it gets loaded again (from cache, faster but not instant) which causes a delay and it is not fluent as it should be.
1.Is there an example of how to do this properly?
2.Is there a way to prevent listview items being destroyed when they are out of screen?
3.If so, will it cause problems to keep too many items?
Bellow is my code:
MenuAdapter:
public class MenuAdapter extends BaseAdapter{
Context context;
List<MyMenuItem> menuItems;
MenuAdapter(Context context, List<MyMenuItem> menuItems) {
this.context = context;
this.menuItems = menuItems;
}
#Override
public int getCount() {
return menuItems.size();
}
#Override
public Object getItem(int position) {
return menuItems.get(position);
}
#Override
public long getItemId(int position) {
return menuItems.indexOf(getItem(position));
}
private class ViewHolder {
ImageView ivMenu;
TextView tvMenuHeader;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
LayoutInflater mInflater = (LayoutInflater) context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
if (convertView == null) {
convertView = mInflater.inflate(R.layout.menu_item, null);
holder = new ViewHolder();
holder.tvMenuHeader = (TextView) convertView.findViewById(R.id.tvMenuHeader);
holder.ivMenu = (ImageView) convertView.findViewById(R.id.ivMenuItem);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
MyMenuItem row_pos = menuItems.get(position);
Picasso.with(context)
.load(row_pos.getItem_image_url())
.into(holder.ivMenu);
holder.tvMenuHeader.setText(row_pos.getItem_header());
Log.e("Test", "headers:" + row_pos.getItem_header());
return convertView;
}
}
MyMenuItem:
public class MyMenuItem {
private String item_header;
private String item_image_url;
public MyMenuItem(String item_header, String item_image_url){
this.item_header=item_header;
this.item_image_url=item_image_url;
}
public String getItem_header(){
return item_header;
}
public void setItem_header(String item_header){
this.item_header=item_header;
}
public String getItem_image_url(){
return item_image_url;
}
public void setItem_image_url(String item_image_url){
this.item_image_url=item_image_url;
}
}
MainActivity:
public class MyActivity extends Activity implements AdapterView.OnItemClickListener {
List<MyMenuItem> menuItems;
ListView myListView;
JSONArray jsonArray;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
Bundle extras = getIntent().getExtras();
if(extras!=null){
try{
jsonArray = new JSONArray(extras.getString("Data"));
}catch (Exception e){
e.printStackTrace();
}
}
menuItems = new ArrayList<MyMenuItem>();
for (int i = 0; i < jsonArray.length(); i++) {
try {
MyMenuItem item = new MyMenuItem(jsonArray.getJSONObject(i).getString("title"), jsonArray.getJSONObject(i).getString("imageURL"));
menuItems.add(item);
}catch (Exception e){
e.printStackTrace();
}
}
myListView = (ListView) findViewById(R.id.list);
MenuAdapter adapter = new MenuAdapter(this, menuItems);
myListView.setAdapter(adapter);
myListView.setOnItemClickListener(this);
}
}
MenuItem.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/ivMenuItem"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="center"
android:src="#drawable/em" />
<TextView
android:id="#+id/tvMenuHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#55000000"
android:paddingBottom="15dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="15dp"
android:textColor="#android:color/white"
android:layout_gravity="left|top"
android:layout_alignBottom="#+id/ivMenuItem"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" />
</RelativeLayout>
1. Is there an example of how to do this properly?
Your code looks pretty close to perfect. The Adapter's getView method is usually the critical path to optimize. Compare for example Picasso's own example SampleListDetailAdapter.java. The important points it (as well as your code) does
check for & re-use already inflated views, inflation is expensive.
use ViewHolder so you don't have to call findViewById every time. Not terribly expensive on simple views. Also cached afaik.
Picasso.with(context).load(url)... each time you need to display an image. This should finish instantly but still use caches and other magic.
There are some minor optimizations you can add, but I doubt that there are noticeable or even measurable changes:
pure style change: use BaseAdapter#getItem(position). This method
exists for you only. The framework doesn't use it.
#Override
public MyMenuItem getItem(int position) { // << subclasses can use subtypes in overridden methods!
return menuItems.get(position);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
...
MyMenuItem row_pos = getItem(position);
Use a sane id method
#Override
public long getItemId(int position) {
return menuItems.indexOf(getItem(position));
}
is equivalent to
#Override
public long getItemId(int position) {
return position;
}
but now infinitely faster. indexOf(Object) scales really badly with the number of objects.
Cache objects that don't change:
MenuAdapter(Context context, List<MyMenuItem> menuItems) {
this.mLayoutInflater = LayoutInflater.from(content);
this.mPicasso = Picasso.with(context);
}
..
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.menu_item, null);
...
mPicasso
.load(row_pos.getItem_image_url())
.into(holder.ivMenu);
2. Is there a way to prevent listview items being destroyed when they are out of screen?
No(*).
..(*) well you can essentially cache the result of getView e.g. in LruCache(position, View) or LruCache(MyMenuItem, View), then don't touch the convertView - they need to remain unconverted or you would kill those views in your cache. Also
#Override
public int getItemViewType(int position) {
return Adapter.IGNORE_ITEM_VIEW_TYPE;
}
seemed to be required because the standard adapter using code assumes that views it removes from visibility are gone. They are not and messing with them messes with your cache and caused weird display problems for me.
3. If so, will it cause problems to keep too many items?
Yes. This behavior is not intendend / expected. There is also more or less nothing you gain. You might be able to save you the call to holder.tvMenuHeader.setText(). Likewise the one to Picasso but both of them should complete instantly. Picasso should have your image cached already. By caching all Views you essentially add another cache that also contains all the images. I would rather check that the picasso cache works as intended and holds most items. The only reason you may want to do it with view caching is for cases that require complicated setup of the view, so it becomes worth caching the completely constructed view rather than just some content parts.
Profile
Profiling can actually tell you where you can / need / should improve. The first to look at IMO is traceview. You'll see if code blocks the main thread which results in choppy list scrolling. If you're doing complicated views and you see that the draw methods are executed most of the time, profile them as well.
http://www.curious-creature.org/docs/android-performance-case-study-1.html
http://blog.venmo.com/hf2t3h4x98p5e13z82pl8j66ngcmry/performance-tuning-on-android
http://www.vogella.com/tutorials/AndroidTools/article.html
I'm creating a simple chess clock -type timer app. I'm trying to show the players and the time they have left as rows in a ListView. I'm using a custom view that extends RelativeLayout for these rows, so that I can give it methods that highlight the player in turn, for example.
Row layout class:
public class GameTimerView extends RelativeLayout {
private TextView nameView;
private TextView timerView;
public GameTimerView(Context context) {
super(context);
LayoutInflater inflater = LayoutInflater.from(context);
inflater.inflate(R.layout.timer_view, this);
loadViews();
}
...
private void loadViews() {
nameView = (TextView)findViewById(R.id.nameView);
timerView = (TextView)findViewById(R.id.timerView);
}
public void setName(String name) {
nameView.setText(name);
}
public void setTime(long timeInMillis) {
timerView.setText(String.format("%02d:%02d",
TimeUnit.MILLISECONDS.toMinutes(timeInMillis),
TimeUnit.MILLISECONDS.toSeconds(timeInMillis) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(timeInMillis))
));
}
public void setActive() {
this.nameView.setTextColor(Color.GREEN);
}
public void setInactive() {
nameView.setTextColor(Color.BLACK);
}
}
Row layout XML (timer_view.xml):
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="#+id/nameView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="#+id/timerView"
android:text="#string/player_default_name" />
<TextView
android:id="#+id/timerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="#string/zero_time" />
</RelativeLayout>
Adapter:
public class playerArrayAdapter extends ArrayAdapter<Player> {
private final Context context;
private final ArrayList<Player> players;
public playerArrayAdapter(Context context, ArrayList<Player> values) {
super(context, R.layout.timer_view, values);
this.context = context;
this.players = values;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
GameTimerView playerView = new GameTimerView(context);
players.get(position).setTimerView(playerView);
return playerView;
}
}
Player class setTimerView function:
public void setTimerView(GameTimerView timer) {
this.timerView = timer;
this.timerView.setName(this.name);
this.timerView.setTime(this.totalCountDown);
this.timerView.setInactive();
}
In the activity's onCreate method:
playerArrayAdapter playersAdapter = new playerArrayAdapter(
getApplicationContext(),
game.getPlayers()
);
ListView playersView = (ListView) findViewById(R.id.playerList);
playersView.setAdapter(playersAdapter);
At first this seems to work, and the desired player names and times are rendered to the list properly. However, if I later programmatically call for example Player.timerView.setActive(), nothing happens.
Having looked at dozens of examples of custom adapters for ListViews none of them seems to be using it this way - the view is always inflated directly in Apdater.getView(). I want the flexibility of an extended view class however, but apparently I'm doing something wrong.
So, what's the correct way to use custom view class for ListView rows?
First, when inflating your GameTimerView, you've got a RelativeLayoutinside another.
Second, to answer the question : it might be good not to re-create a view on each call to ArrayAdapter.getView(), but instead modify playerArrayAdapter by adding a cache like this :
private List<View> views;
public playerArrayAdapter(Context context, ArrayList<Player> values) {
super(context, R.layout.timer_view, values);
views = new ArrayList<View>(values.length);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View playerView;
if (position < views.size()) {
playerView = views.get(position);
if (playerView != null)
return playerView;
} else {
while (views.size() < position)
views.add(null);
}
playerView = new GameTimerView(context);
views.add(position, playerView);
players.get(position).setTimerView(playerView);
return playerView;
}
If this successfully corrects your problem, it means that previously, when setting a player as active the ListView was getting all views again for rendering, recreating them, and doing so, erasing any previous state.
As per your getView() method of Adapter, it will create a new view always that might have causing you issue.
If you really want to implement a CustomViewGroup then please refer this good implementation of the custom view here. Hope this will help you to start.
I have an ArrayAdapter for a list view that has multiple buttons in it. For one toggle button, I want to have a default state based on a condition, and let users toggle the button as well.
However, when users click button on row 1, the button for row 3 actually gets selected. I'm not sure why this is happening. Below is snippet of relevant code from my getView method with comments.
layout of my toggle button
<ToggleButton android:id="#+id/color_toggle"
android:layout_width="50px"
android:layout_height="50px"
android:focusable="false"
android:textOn="" android:textOff="" android:layout_alignParentLeft="true"
android:layout_marginRight="10dp"
/>
class Color {
int id;
int something;
}
List<Color> colorsList;
class ColorHolder {
TextView colorNameText;
ToggleButton toggleButton;
}
public View getView(final int position, final View convertView, final ViewGroup parent) {
View rowView = convertView;
Color c = colorsList.get(position);
if (null == rowView) {
rowView = this.inflater.inflate(R.layout.list_item_color, parent, false);
holder = new ColorHolder();
holder.colorNameText = (TextView) rowView.findViewById(R.id.color_name);
holder.toggleButton = (ToggleButton) rowView.findViewById(R.id.color_toggle);
rowView.setTag(holder);
}
else {
holder = (ColorHolder)rowView.getTag();
}
holder.toggleButton.setTag(c.getId());
final ColorHolder thisRowHolder = holder;
holder.toggleButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (thisRowHolder.toggleButton.isChecked()) {
thisRowHolder.toggleButton.setBackgroundDrawable(//normal button);
thisRowHolder.toggleButton.setChecked(false);
for (int i = 0; i < colorList.size(); i++) {
if (colorList.get(i) == (Integer)v.getTag()) {
colorList.get(i).setSomething(0);
break;
}
}
adapter.notifyDataSetChanged();
}
else {
thisRowHolder.toggleButton.setBackgroundDrawable(//enabled button);
thisRowHolder.toggleButton.setChecked(true);
for (int i = 0; i < colorList.size(); i++) {
if (colorList.get(i) == (Integer)v.getTag()) {
colorList.get(i).setSomething(1);
break;
}
}
adapter.notifyDataSetChanged();
}
}
});
if (c.getSomething()>0) {
holder.toggleButton.setBackgroundDrawable(//enabled button);
holder.toggleButton.setChecked(true);
}
else {
holder.toggleButton.setBackgroundDrawable(//normal button);
holder.toggleButton.setChecked(false);
}
return rowView;
}
Question
What am I doing wrong? why are other buttons in third row toggling even though i'm toggling buttons in row one.
I read that this happens because the listView recycles, is there no way to fix it? Some strategies i've tried, to no avail, based on similar questions: 1) put onClickListener in the if clause. 2) instead of setting int in setTag instead set the holder and use that holder in onClickListener
update
I've updated all the code in the question with suggestions I received.
Hope This Helps.
Activity Code
public class DemoActivity extends Activity {
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
ColorInfo[] clr= new ColorInfo[20];
for(int i=0;i<20;i++){
clr[i] = new ColorInfo();
}
((ListView)findViewById(R.id.list)).setAdapter(new MyAdapter(this, 0, clr));
}
private static class MyAdapter extends ArrayAdapter<ColorInfo> implements OnClickListener{
LayoutInflater inflater;
public MyAdapter(Context context, int textViewResourceId,
ColorInfo[] objects) {
super(context, textViewResourceId, objects);
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if(convertView == null){
convertView = inflater.inflate(R.layout.row, null);
holder = new ViewHolder();
holder.tgl = (ToggleButton) convertView.findViewById(R.id.toggle);
convertView.setTag(holder);
}
holder = (ViewHolder) convertView.getTag();
holder.tgl.setTag(position);
holder.tgl.setOnClickListener(this);
holder.tgl.setChecked(getItem(position).isChecked);
return convertView;
}
private static class ViewHolder{
ToggleButton tgl;
}
public void onClick(View v) {
int pos = (Integer) v.getTag();
ColorInfo cinfo = getItem(pos);
cinfo.isChecked = !cinfo.isChecked;
}
}
private static class ColorInfo{
boolean isChecked=false;
}
}
main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<ListView
android:id="#+id/list"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
</ListView>
</LinearLayout>
row.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ToggleButton android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/toggle"
/>
</LinearLayout>
Your problem is listview recycling views
You have to store state of toggle button for each row of listview.
Eg.Create class which stores information about each row,suppose ColorInfo which contains color and isChecked boolean.
so instead of
Color c = colorsList.get(position);
it will be
ColorInfo colorInfo = colorsList.get(position);
and in getview
togglebutton.setCheck(colorInfo.isCheck)
and in onClick listener of toggle buttons you change state of object of ColorInfo for that position to toggleChecked true or false and notifyDatasetChanged,this will solve your problem.
You are using a member variable for your ViewHolder, rather than a final local variable. So your OnClickListener is referencing whatever the latest holder instance is, which will correspond with the most recently created or recycled list item.
Do this instead:
//Lock in this reference for the OnClickListener
final ColorHolder thisRowHolder = holder;
holder.favButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (thisRowHolder.toggleButton.isChecked()) {
thisRowHolder.toggleButton.setBackgroundDrawable(getResources().getDrawable(...);
thisRowHolder.toggleButton.setChecked(false);
}
else {
thisRowHolder.toggleButton.setBackgroundDrawable(getResources().getDrawable(...));
thisRowHolder.toggleButton.setChecked(true);
}
}
});
...
Edit:
Also noticed this. In these two lines:
holder.colorNameText = (TextView) itemView.findViewById(R.id.color_name);
holder.toggleButton = (ToggleButton) itemView.findViewById(R.id.color_toggle);
You are finding the views in some member variable itemView, but you need to be finding them in rowView so you are getting the instances for this specific row. All your view holders are looking at the same ToggleButton instance, which may not even be on screen.
Edit 2:
One more thing you're missing. You need to store the state of the toggle buttons and reapply them. So in your OnClickListener, when you call setChecked() you must also update the backing data in colorsList. Looks like you already cached a reference to the proper list element in your ToggleButton's ID, so should be easy. Then move this block of code out of your if/else block and put it afterwards, so the toggle button is always updated to the latest data:
if (c.getSomething()>0) {
holder.toggleButton.setBackgroundDrawable(getResource().getDrawable(...)));
holder.setChecked(false);
}
else {
holder.toggleButton.setBackgroundDrawable(getResource().getDrawable(...)));
holder.setChecked(true);
}
I want my ListView to contain buttons, but setting the button's xml property, onClick="myFunction" and then placing a public void myFunction(android.view.View view) method in the activity causes an NoSuchMethodException (the stack trace is null) to be thrown, as although the onclick listener is there, it doesn't fire myFunction(...) and cause the activity to close.
How do I create a custom Adapter that connects a View.OnClickListener to a button on each row of a ListView?
My ListView is created as follows...
[activity.java content..]
public void myFunction(android.view.View view)
{
//Do stuff
}
[activity.xml content..]
<LinearLayout xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".FrmCustomerDetails" >
<ListView android:id="#+id/LstCustomerDetailsList" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_weight="1" android:clickable="true" android:clipChildren="true" android:divider="#null" android:dividerHeight="0dp" android:fastScrollEnabled="true" android:footerDividersEnabled="false" android:headerDividersEnabled="false" android:requiresFadingEdge="vertical" android:smoothScrollbar="true" />
</LinearLayout>
[activity_row_item.xml content..]
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="#+id/Llt" android:layout_width="match_parent" android:layout_height="match_parent" >
<Button android:id="#+id/Btn" android:text="Click me" android:onClick="myFunction" />
</LinearLayout>
Here is how to create the custom Adapter, connecting View.OnClickListener to a ListView with a button per row...
1. Create a layout for a typical row
In this case, the row is composed of three view components:
name (EditText)
value (EditText:inputType="numberDecimal")
delete (Button)
Xml
pay_list_item.xml layout is as follows:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<EditText
android:id="#+id/pay_name"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="2"
android:hint="Name" />
<EditText
android:id="#+id/pay_value"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="1"
android:inputType="numberDecimal"
android:text="0.0" />
<Button
android:id="#+id/pay_removePay"
android:layout_width="100dp"
android:layout_height="fill_parent"
android:text="Remove Pay"
android:onClick="removePayOnClickHandler" />
</LinearLayout>
Note: the button has onClick handler defined in xml layout file, because we want to refer its action to a specific list item.
Doing this means that the handler will be implemented in Activity file and each button will know which list item it belongs to.
2. Create list item adapter
This is the java class that is the controller for pay_list_item.xml.
It keeps references for all of its views, and it also puts these references in tags, extending the ArrayAdapter interface.
The Adapter:
public class PayListAdapter extends ArrayAdapter<Payment> {
private List<Payment> items;
private int layoutResourceId;
private Context context;
public PayListAdapter(Context context, int layoutResourceId, List<Payment> items) {
super(context, layoutResourceId, items);
this.layoutResourceId = layoutResourceId;
this.context = context;
this.items = items;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
PaymentHolder holder = null;
LayoutInflater inflater = ((Activity) context).getLayoutInflater();
row = inflater.inflate(layoutResourceId, parent, false);
holder = new PaymentHolder();
holder.Payment = items.get(position);
holder.removePaymentButton = (ImageButton)row.findViewById(R.id.pay_removePay);
holder.removePaymentButton.setTag(holder.Payment);
holder.name = (TextView)row.findViewById(R.id.pay_name);
holder.value = (TextView)row.findViewById(R.id.pay_value);
row.setTag(holder);
setupItem(holder);
return row;
}
private void setupItem(PaymentHolder holder) {
holder.name.setText(holder.Payment.getName());
holder.value.setText(String.valueOf(holder.Payment.getValue()));
}
public static class PaymentHolder {
Payment Payment;
TextView name;
TextView value;
ImageButton removePaymentButton;
}
}
Here we list the Payment class items.
There are three most important elements here:
PayListAdapter constructor: sets some private fields and calls superclass constructor. It also gets the List of Payment objects. Its implementation is obligatory.
PaymentHolder: static class that holds references to all views that I have to set in this list item. I also keep the Payment object that references to this particular item in list. I set it as tag for ImageButton, that will help me to find the Payment item on list, that user wanted to remove
Overriden getView method: called by superclass. Its goal is to return the single List row. We create its fields and setup their values and store them in static holder. Holder then is put in row’s tag element. Note that there is a performance issue, as the row is being recreated each time it is displayed. I used to add some flag in holder like isCreated, and set it to true after row was already created. then you can add if statement and read tag’s holder instead of creating it from scratch.
Payment.java is quite simple as for now and it looks a bit like BasicNameValuePair:
public class Payment implements Serializable {
private String name = "";
private double value = 0;
public Payment(String name, double value) {
this.setName(name);
this.setValue(value);
}
...
}
There are additional gets and sets for each private field not shown.
3. Add ListView to the activity layout xml file
In its simpliest form, it will be enough to add this view to activity layout:
<ListView
android:id="#+id/EnterPays_PaysList"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
</ListView>
4. Set up adapter to this list view in Activity Java code
In order to display items in ListView you need to set up its adapter and map it to some other ArrayList of Payment objects (as I am extending an Array adapter here). Here is code that is responsible for binding adapter to editPersonData.getPayments() ArrayList:
PayListAdapter adapter = new PayListAdapter(AddNewPerson.this, R.layout.pay_list_item, editPersonData.getPayments());
ListView PaysListView = (ListView)findViewById(R.id.EnterPays_PaysList);
PaysListView.setAdapter(adapter);
5. Adding / removing items to ListView (and its adapter)
Adapter is handled just like any other ArrayList, so adding new element to it is as simple as:
Payment testPayment = new Payment("Test", 13);
adapter.add(testPayment);
adapter.remove(testPayment);
6. Handle Remove Payment button click event
In an activity’s code, where ListView is displayed, add public method that will handle remove button click action. The method name has to be exactly the same as it was in pay_list_item.xml:
android:onClick="removePayOnClickHandler"
The method body is as follows:
public void removePayOnClickHandler(View v) {
Payment itemToRemove = (Payment)v.getTag();
adapter.remove(itemToRemove);
}
The Payment object was stored in ImageButton’s Tag element. Now it is enough to read it from Tag, and remove this item from the adapter.
7. Incorporate remove confirmation dialog window
Probably you need also make sure that user intentionally pressed the remove button by asking him additional question in confirmation dialog.
Dialogue
a) Create dialog’s id constant
This is simply dialog’s ID. it should be unique among any other dialog window that is handled by current activity. I set it like that:
protected static final int DIALOG_REMOVE_CALC = 1;
protected static final int DIALOG_REMOVE_PERSON = 2;
b) Build dialog
I use this method to build dialog window:
private Dialog createDialogRemoveConfirm(final int dialogRemove) {
return new AlertDialog.Builder(getApplicationContext())
.setIcon(R.drawable.trashbin_icon)
.setTitle(R.string.calculation_dialog_remove_text)
.setPositiveButton(R.string.calculation_dialog_button_ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
handleRemoveConfirm(dialogRemove);
}
})
.setNegativeButton(R.string.calculation_dialog_button_cancel, null)
.create();
}
AlertDialog builder pattern is utilized here. I do not handle NegativeButton click action – by default the dialog is just being hidden. If dialog’s confirm button is clicked, my handleRemoveConfirm callback is called and action is performed based on dialog’s ID:
protected void handleRemoveConfirm(int dialogType) {
if(dialogType == DIALOG_REMOVE_PERSON){
calc.removePerson();
}else if(dialogType == DIALOG_REMOVE_CALC){
removeCalc();
}
}
c) Show Dialog
I show dialog after my remove button click. The showDialog(int) is Android’s Activity’s method:
OnClickListener removeCalcButtonClickListener = new OnClickListener() {
public void onClick(View v) {
showDialog(DIALOG_REMOVE_CALC);
}
};
the showDialog(int) method calls onCreateDialog (also defined in Activity’s class). Override it and tell your app what to do if the showDialog was requested:
#Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_REMOVE_CALC:
return createDialogRemoveConfirm(DIALOG_REMOVE_CALC);
case DIALOG_REMOVE_PERSON:
return createDialogRemoveConfirm(DIALOG_REMOVE_PERSON);
}
}
Take a look at this blog post I wrote on exactly this matter:
Create custom ArrayAdapter
There are comments that explain every action I make in the adapter.
Here is the explanation in short:
So lets for example take a row where you want to place a CheckBox, ImageView
and a TextView while all of them are clickable. Meaning that you can click the
row it self for going to another Actvity for more details on the row, check its
CheckBox or press the ImageView to perform another operation.
So what you should do is:
1. First create an XML layout file for your ListView row:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<CheckBox
android:id="#+id/cbCheckListItem"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp" />
<TextView
android:id="#+id/tvItemTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="item string" />
<ImageView
android:id="#+id/iStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="#drawable/ic_launcher"
android:src="#drawable/ic_launcher" />
</LinearLayout>
2. Second in your java code define a ViewHolder, a ViewHolder
is designed to hold the row views and that way operating more quickly:
static class ViewHolder
{
TextView title;
CheckBox checked;
ImageView changeRowStatus;
}
3. Now we have to define CustomArrayAdapter, using the array adapter
we can define precisely what is the desired output for each row based on the content of this
row or it’s position. We can do so by overriding the getView method:
private class CustomArrayAdapter extends ArrayAdapter<RowData>
{
private ArrayList<RowData> list;
//this custom adapter receives an ArrayList of RowData objects.
//RowData is my class that represents the data for a single row and could be anything.
public CustomArrayAdapter(Context context, int textViewResourceId, ArrayList<RowData> rowDataList)
{
//populate the local list with data.
super(context, textViewResourceId, rowDataList);
this.list = new ArrayList<RowData>();
this.list.addAll(rowDataList);
}
public View getView(final int position, View convertView, ViewGroup parent)
{
//creating the ViewHolder we defined earlier.
ViewHolder holder = new ViewHolder();)
//creating LayoutInflator for inflating the row layout.
LayoutInflater inflator = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//inflating the row layout we defined earlier.
convertView = inflator.inflate(R.layout.row_item_layout, null);
//setting the views into the ViewHolder.
holder.title = (TextView) convertView.findViewById(R.id.tvItemTitle);
holder.changeRowStatus = (ImageView) convertView.findViewById(R.id.iStatus);
holder.changeRowStatus.setTag(position);
//define an onClickListener for the ImageView.
holder.changeRowStatus.setOnClickListener(new OnClickListener()
{
#Override
public void onClick(View v)
{
Toast.makeText(activity, "Image from row " + position + " was pressed", Toast.LENGTH_LONG).show();
}
});
holder.checked = (CheckBox) convertView.findViewById(R.id.cbCheckListItem);
holder.checked.setTag(position);
//define an onClickListener for the CheckBox.
holder.checked.setOnClickListener(new OnClickListener()
{
#Override
public void onClick(View v)
{
//assign check-box state to the corresponding object in list.
CheckBox checkbox = (CheckBox) v;
rowDataList.get(position).setChecked(checkbox.isChecked());
Toast.makeText(activity, "CheckBox from row " + position + " was checked", Toast.LENGTH_LONG).show();
}
});
//setting data into the the ViewHolder.
holder.title.setText(RowData.getName());
holder.checked.setChecked(RowData.isChecked());
//return the row view.
return convertView;
}
}
4. Now you need to set this adapter, as the adapter of your ListView.
this ListView can be created in java or using an XML file, in this case I’m using a list that was
defined in the XML file using the “list” id:
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_layout);
ListView list = (ListView)findViewById(R.id.list);
CustomArrayAdapter dataAdapter = new CustomArrayAdapter(this, R.id.tvItemTitle, rowDataList);
list.setAdapter(dataAdapter);
}
5. Finally if we want to be able to press the row it self and not only a certain view in it
we should assign an onItemClickListener to the ListView:
list.setOnItemClickListener(new OnItemClickListener()
{
public void onItemClick(AdapterView<?> parent, View view,int position, long id)
{
Toast.makeText(activity, "row " + position + " was pressed", Toast.LENGTH_LONG).show();
}
});
First, the way of adding listeners in xml using onClick="function" is deprecated. You need a ViewHolder class to link the button in the xml to your java code. Then you can implement onClickListener for that.
Inside your getView() implementation of CustomAdapter, you can try like below.
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = inflater.inflate(R.layout.xxxxx, null);
holder = new ViewHolder();
holder.invite = (Button) convertView.findViewById(R.id.button);
} else {
holder = (ViewHolder) convertView.getTag();
}
final int pos = position;
holder.button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
handleClick(pos);
}
});
}
class ViewHolder {
Button button;
}
Is it possible to add views to a different layout than the one called in setContentView() in the OnCreate()?
Im trying to use Zylincs ViewPager (found at http://www.zylinc.com/blog-reader/items/viewpager-page-indicator.html) and I have that part setup and working great.
What this leaves me with is 4 layouts. 1 is main.xml, and the other three are main_pg0.xml, main_pg1.xml, and main_pg3.xml
The three pages are where the content of each "page" is where main.xml contains the page viewer.
In the onCreate im using setContentView(main.xml).
What im trying to do now is be able to add textViews programically to some of the pages.
What I have right now is:
LayoutInflater factory = getLayoutInflater();
View layout = factory.inflate(R.layout.main_pg0, null);
View linearLayout = layout.findViewById(R.id.linearLayout);
TextView valueTV = new TextView(this);
valueTV.setText("hallo hallo");
valueTV.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT));
((LinearLayout) linearLayout).addView(valueTV);
This is as far as I've been able to get, which does not add the textview at all.
----EDIT----
Hoping to clarify a little more what im trying to do
Heres my code
Main.java
public class Main extends FragmentActivity implements PageInfoProvider {
public GlobalVars globalVars;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
globalVars = (GlobalVars) getApplicationContext();
MyPagerAdapter adapter = new MyPagerAdapter();
ViewPager myPager = (ViewPager) findViewById(R.id.viewpager);
myPager.setAdapter(adapter);
myPager.setCurrentItem(0);
ViewPagerIndicator indicator = (ViewPagerIndicator) findViewById(R.id.indicator);
myPager.setOnPageChangeListener(indicator);
indicator.init(0, adapter.getCount(), this);
Resources res = getResources();
Drawable prev = res.getDrawable(R.drawable.indicator_prev_arrow);
Drawable next = res.getDrawable(R.drawable.indicator_next_arrow);
indicator.setArrows(prev, next);
}
private class MyPagerAdapter extends PagerAdapter {
public int getCount() {
return 3;
}
public Object instantiateItem(View collection, int position) {
LayoutInflater inflater = (LayoutInflater) collection.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
int resId = 0;
switch (position) {
case 0:
resId = R.layout.main_pg0;
LoadVehicles();
break;
case 1:
resId = R.layout.main_pg1;
break;
case 2:
resId = R.layout.main_pg2;
break;
}
View view = inflater.inflate(resId, null);
((ViewPager) collection).addView(view, 0);
return view;
}
#Override
public void destroyItem(View arg0, int arg1, Object arg2) {
((ViewPager) arg0).removeView((View) arg2);
}
#Override
public void finishUpdate(View arg0) {
// TODO Auto-generated method stub
}
#Override
public boolean isViewFromObject(View arg0, Object arg1) {
return arg0 == ((View) arg1);
}
#Override
public void restoreState(Parcelable arg0, ClassLoader arg1) {
// TODO Auto-generated method stub
}
#Override
public Parcelable saveState() {
// TODO Auto-generated method stub
return null;
}
#Override
public void startUpdate(View arg0) {
// TODO Auto-generated method stub
}
}
public String getTitle(int position) {
String title = "--Untitled--";
switch (position) {
case 0:
title = "Page 0";
break;
case 1:
title = "Page 1";
break;
case 2:
title = "Page 2";
break;
}
return title;
}
}
main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<com.zylinc.view.ViewPagerIndicator
android:id="#+id/indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#EAEAEA"
android:paddingBottom="8dp"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:paddingTop="8dp" />
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#ffeaeaea" >
<View
android:layout_width="fill_parent"
android:layout_height="1dp"
android:layout_alignBottom="#+id/current"
android:background="#C5C5C5" />
<ImageView
android:id="#+id/current"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerInParent="true"
android:src="#drawable/indicator_current" />
</RelativeLayout>
<android.support.v4.view.ViewPager
android:id="#+android:id/viewpager"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</LinearLayout>
main_pg0.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#FFFFFF"
android:orientation="vertical"
android:id="#+id/linearLayout" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:text="main_pg0.xml"
android:textColor="#0C0C0C"
android:textSize="18sp" />
</LinearLayout>
What im trying to do is add a text view programmatically to main_pg0.xml ONLY, while leaving the other 2 pages alone
You haven't explicitly mentioned whether you're feeding Fragments to the ViewPager, but since you're working from the Zylinc implementation, I'm going to assume you are. As a matter of fact, I'll just use the provided example code to show you the idea.
The key to understanding how to realise what you're after is to get that the pages in the ViewPager are self-contained units in the form of Fragments. They are reusable, have their own life cycle, and can handle their own layouts. Hence, if you want to make runtime modifications to the layout of the pages, you will want to do this inside the Fragment(s) that make up these pages.
The following method can be found in the example code's ItemFragment class. I've simply added the TextView from your own code snippet to it. For demonstration purposes, I did hide the ListView that used to fill up all the space in the date_fragment.xml layout file.
#Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.date_fragment, container, false);
View tv = v.findViewById(R.id.text);
((TextView)tv).setText(readableDateFormat.format(date));
// changes below
TextView valueTV = new TextView(getActivity());
valueTV.setText("hallo hallo");
valueTV.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,LayoutParams.WRAP_CONTENT));
((ViewGroup)v).addView(valueTV);
return v;
}
That will give you the "hallo hallo" text (you're a Dutchie? ;)) directly underneath the date in the page:
By the way, you might want to consider switching to Jake's ViewPagerIndicator. Although I haven't closely looked at the one you're currently using, Jake's seems more flexible to me and cleaner to work with. Up to you of course.
Edit: Okay, so you're not actually using fragments in the ViewPager, but are rather feeding it the layouts. That's fine, although it'll probably become harder to manage when the pages get more complex. Anyways, key to changing the page's layouts at runtime is still to do it whenever you build/inflate it. When using fragments, that would be in the overridden method, as shown above. In your case, however, you'll need to do it directly in instantiateItem(...).
I didn't actually type this out in an IDE, so please mind and minor syntactical errors.
public Object instantiateItem(View collection, int position) {
LayoutInflater inflater = (LayoutInflater) collection.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = null;
switch (position) {
case 0:
view = inflater.inflate(R.layout.main_pg0, null);
TextView valueTV = new TextView(getActivity());
valueTV.setText("hallo hallo");
valueTV.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,LayoutParams.WRAP_CONTENT));
((ViewGroup)view).addView(valueTV);
break;
case 1:
view = inflater.inflate(R.layout.main_pg1, null);
// or perhaps better for readability: build the layout in a different method, passing in the root
buildSecondPageLayout(view);
break;
case 2:
view = inflater.inflate(R.layout.main_pg2, null);
buildThirdPageLayout(view);
break;
}
return view;
}
You have to add view in main view which is inflated layout.So just add one line.
LayoutInflater factory = getLayoutInflater();
View layout = factory.inflate(R.layout.main_pg0, null);
View linearLayout = layout.findViewById(R.id.linearLayout);
TextView valueTV = new TextView(this);
valueTV.setText("hallo hallo");
valueTV.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT));
((LinearLayout) linearLayout).addView(valueTV);
layout.addView(linearLayout);