Thursday, October 11, 2012

Android ListView : Custom Adapter and Layout

Topics covered

Android ListView

Array Adapter

Custom layout

Optimization: View holder pattern

In the last post, we talked about how we can interact with the user handling the item click events and the long click events. We saw also how to  add and remove items dynamically from the ListView using the adapter. Now we want to go a bit further and start analyzing how the ListView can be customized. In this post we will talk about two different customization aspects:
  • Custom Layout
  • Custom Adapter

Android ListView Custom Adapter


The first thing we want to discover is how we can implement a custom layout. In the last post we used a prebuilt layout shipped with Android SDK and we saw how simple is adding and removing items. To keep things simple, let’s suppose we want a custom layout built by a Planet name and under it a Number, that is the distance. Let’s reuse the Planet tutorial and assume we want to have something like that:
custom_layout_row_android_listview
where each item row is like the one shown above.
The first step is to define the row layout. So we go under res directory in our Android project and we create a new layout called row_layout.xml. Considering we want to have two line for each row we will use a LinearLayout with vertical orientation. We have then:
<?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" >

<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:textStyle="bold"
/>

<TextView
android:id="@+id/dist"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize="8dp"
android:textStyle="italic"/>
</LinearLayout>

The next step is how can we use this layout for our ListView rows? We could reuse the example shown in the last post and match the new view id in the row layout with the new values. We want to go further and introduce a custom adapter. This adapter will manage our layout. To not re-invent the wheel, we can create a new class called PlanetAdapter and extend the ArrayAdapter, in this way:
public class PlanetAdapter extends ArrayAdapter<Planet> {

private List<Planet> planetList;
private Context context;

public PlanetAdapter(List<Planet> planetList, Context ctx) {
super(ctx, R.layout.row_layout, planetList);
this.planetList = planetList;
this.context = ctx;
}

public View getView(int position, View convertView, ViewGroup parent) {

// First let's verify the convertView is not null
if (convertView == null) {
// This a new view we inflate the new layout
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.row_layout, parent, false);
}
// Now we can fill the layout with the right values
TextView tv = (TextView) convertView.findViewById(R.id.name);
TextView distView = (TextView) convertView.findViewById(R.id.dist);
Planet p = planetList.get(position);

tv.setText(p.getName());
distView.setText("" + p.getDistance());


return convertView;
}

Let’s analyzie this class. In the constructor we reference the new layout calling super constructor. What’s now? Well we need to override some methods in our class in order to customize the standard ArrayLayout. We have to consider that each time a row is created the ListView calls getView method in the custom adapter. So if we want to modify the layout of each row, we have just to override the getView. An important aspect we have to consider is that the Android OS is very smart and tries to reuse the view we created to show each row. In other word, Android OS doesn’t create for each row in the ListView a corresponding view but only for the row that are visible, reusing the same views if the user scrolls up and down. So the first thing we have to do in this method is verify if the view we get as parameter is null (so it is the first time) or is not null so we are reusing an existing view. If the view is null, we have simply inflate our layout, otherwise we can directly use findViewById to find out components. You can see this in the code:
if (convertView == null) {
// This a new view we inflate the new layout
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.row_layout, parent, false);
}

The next step is to update the view with the right information. Let’s suppose we have only the Planet name and the sun distance (in our case is just a fake value). So we use findViewById in the layout, we inflated before, to get the reference to the view we want to update and then simply set the new value.
// Now we can fill the layout with the right values
TextView tv = (TextView) convertView.findViewById(R.id.name);
TextView distView = (TextView) convertView.findViewById(R.id.dist);
Planet p = planetList.get(position);
tv.setText(p.getName());
distView.setText("" + p.getDistance());

Running the example we have:

android_listview_custom_layout

Another interesting example is using an image for each row. To keep things simple and clear we supposed that the image is always the same but we could change it as the planet changes. To do so we have simply to modify our row layout to include an ImageView component that will be update in the getView. So the layout will be:
<?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" >

<ImageView
android:id="@+id/img"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="left"
android:paddingLeft="0dip"
android:src="@drawable/planet" />

<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/img"
android:layout_toRightOf="@+id/img"
android:textStyle="bold" />

<TextView
android:id="@+id/dist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/name"
android:layout_gravity="center"
android:layout_marginTop="2dip"
android:layout_toRightOf="@id/img"
android:gravity="right"
android:textSize="8dp"
android:textStyle="italic" />

</RelativeLayout>

In this case we have to modify the constructor and the layout inflater in this way:
public PlanetAdapter(List<Planet> planetList, Context ctx) {
super(ctx, R.layout.img_row_layout, planetList);
this.planetList = planetList;
this.context = ctx;
}
...

public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
// First let's verify the convertView is not null
if (convertView == null) {
// This a new view we inflate the new layout
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = inflater.inflate(R.layout.img_row_layout, null);
}
...
return v;
}
Running the example above we have:

android list view custom layout with image


 


Optimization


By now for each new row the system inflates the new layout and then find the view to update. This process is very expensive and consumes a lot of resources, so we could find something smarter to avoid to look again and again at the same components. To optimize this view look up we can use a common pattern called View Holder pattern. This pattern is very simple and implies using a static inner class that holds the reference to the view inside the layout. So if we have a new row and the view is null we will inflate the new layout and use the View Holder pattern to keep track of the components we inflated, otherwise we don’t need anymore to look for them because we store the references in the static class. The static holder class is:
private static class PlanetHolder {
public TextView planetNameView;
public TextView distView;
}



Now the problem is: How we can bind the static class to the ListView row. Very Simple. For each view  (remember getView method) we simply use a setTag method and we store our static class. In this way we can handle the ListView more efficiently. We need to modify the getView method in this way:
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;

PlanetHolder holder = new PlanetHolder();

// First let's verify the convertView is not null
if (convertView == null) {
// This a new view we inflate the new layout
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = inflater.inflate(R.layout.img_row_layout, null);
// Now we can fill the layout with the right values
TextView tv = (TextView) v.findViewById(R.id.name);
TextView distView = (TextView) v.findViewById(R.id.dist);


holder.planetNameView = tv;
holder.distView = distView;

v.setTag(holder);
}
else
holder = (PlanetHolder) v.getTag();

Planet p = planetList.get(position);
holder.planetNameView.setText(p.getName());
holder.distView.setText("" + p.getDistance());


return v;
}
Source code @ github



Android ListView : Custom Adapter and Layout Rating: 4.5 Diposkan Oleh: Unknown

0 comments:

Post a Comment