Trong hướng dẫn này, chúng ta sẽ sử dụng một CustomAdapter để hiển thị các dòng tùy chỉnh (custom rows) của ListView trong Android bằng một ArrayList. Ngoài ra, để nâng cao trải nghiệm người dùng, ListView sẽ được thêm hiệu ứng khi cuộn.

Tổng quan về Custom Adapter cho ListView trong Android
ArrayAdapter là adapter (thành phần kết nối dữ liệu với giao diện) đơn giản nhất để đổ dữ liệu từ một ArrayList vào view. Đây cũng chính là adapter mà chúng ta sẽ triển khai trong bài hướng dẫn này.
Ngoài ra, cũng có những adapter khác, ví dụ như CursorAdapter. Nó liên kết trực tiếp với một tập kết quả (result set) từ database SQLite cục bộ và sử dụng Cursor làm nguồn dữ liệu.
Tái sử dụng các hàng (Recycling Rows)
Khi một ListView được khởi tạo, các hàng sẽ được tạo ra và hiển thị cho đến khi lấp đầy toàn bộ chiều cao của danh sách. Sau thời điểm đó, sẽ không có ihàng mới nào được tạo ra trong bộ nhớ.
Khi người dùng cuộn theo list, những item đã khuất khỏi màn hình sẽ được giữ lại trong bộ nhớ để dùng sau. Mỗi khi có một hàng mới sắp hiển thị, nó sẽ tái sử dụng một trong những hàng cũ đã được lưu lại này.
Tạo một View template
Bây giờ, ta sẽ tạo một file layout (row_item.xml) để định nghĩa giao diện tùy chỉnh cho các item hiển thị trên mỗi hàng.
<RelativeLayout xmlns:android="<https://schemas.android.com/apk/res/android>"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="Marshmallow"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@android:color/black" />
<TextView
android:id="@+id/type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/name"
android:layout_marginTop="5dp"
android:text="Android 6.0"
android:textColor="@android:color/black" />
<ImageView
android:id="@+id/item_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@android:drawable/ic_dialog_info" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<TextView
android:id="@+id/version_heading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="API: "
android:textColor="@android:color/black"
android:textStyle="bold" />
<TextView
android:id="@+id/version_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="23"
android:textAppearance="?android:attr/textAppearanceButton"
android:textColor="@android:color/black"
android:textStyle="bold" />
</LinearLayout>
</RelativeLayout>
Trong bài hướng dẫn này, ta sẽ xây dựng một ứng dụng có một danh sách, trong đó mỗi hàng hiển thị một đoạn mô tả bằng text và một icon thông tin.
Khi người dùng nhấn vào một hàng, một SnackBar (thông báo nhỏ hiện lên ở cuối màn hình) sẽ hiện ra để hiển thị nội dung text của hàng đó. Nếu nhấn vào icon thông tin, SnackBar sẽ hiển thị thông tin chi tiết dành riêng cho hàng tương ứng.
Cấu trúc project

Code
Chúng ta sẽ tạo một ListView tùy chỉnh bằng cách tạo một lớp con (subclass) của ArrayAdapter với DataModel là đối tượng dữ liệu. getView() là phương thức trả về view thực tế được dùng làm một hàng trong ListView tại một vị trí nhất định.
Dưới đây là file layout content_main.xml chứa ListView:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="<https://schemas.android.com/apk/res/android>"
xmlns:tools="<https://schemas.android.com/tools>"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="<https://schemas.android.com/apk/res-auto>"
tools:context="com.journaldev.customlistview.MainActivity"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:showIn="@layout/activity_main">
<ListView
android:id="@+id/list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</RelativeLayout>
Còn đây là file DataModel.java chứa lớp định nghĩa cấu trúc dữ liệu cho các đối tượng chứa trong ArrayList.
public class DataModel {
String name;
String type;
String version_number;
String feature;
public DataModel(String name, String type, String version_number, String feature ) {
this.name=name;
this.type=type;
this.version_number=version_number;
this.feature=feature;
}
public String getName() {
return name;
}
public String getType() {
return type;
}
public String getVersion_number() {
return version_number;
}
public String getFeature() {
return feature;
}
}
File CustomAdapter.java định nghĩa CustomAdapter, lớp có nhiệm vụ đổ dữ liệu từ DataModel vào ListView.
public class CustomAdapter extends ArrayAdapter<DataModel> implements View.OnClickListener{
private ArrayList<DataModel> dataSet;
Context mContext;
// View lookup cache
private static class ViewHolder {
TextView txtName;
TextView txtType;
TextView txtVersion;
ImageView info;
}
public CustomAdapter(ArrayList<DataModel> data, Context context) {
super(context, R.layout.row_item, data);
this.dataSet = data;
this.mContext=context;
}
@Override
public void onClick(View v) {
int position=(Integer) v.getTag();
Object object= getItem(position);
DataModel dataModel=(DataModel)object;
switch (v.getId())
{
case R.id.item_info:
Snackbar.make(v, "Release date " +dataModel.getFeature(), Snackbar.LENGTH_LONG)
.setAction("No action", null).show();
break;
}
}
private int lastPosition = -1;
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// Get the data item for this position
DataModel dataModel = getItem(position);
// Check if an existing view is being reused, otherwise inflate the view
ViewHolder viewHolder; // view lookup cache stored in tag
final View result;
if (convertView == null) {
viewHolder = new ViewHolder();
LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(R.layout.row_item, parent, false);
viewHolder.txtName = (TextView) convertView.findViewById(R.id.name);
viewHolder.txtType = (TextView) convertView.findViewById(R.id.type);
viewHolder.txtVersion = (TextView) convertView.findViewById(R.id.version_number);
viewHolder.info = (ImageView) convertView.findViewById(R.id.item_info);
result=convertView;
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
result=convertView;
}
Animation animation = AnimationUtils.loadAnimation(mContext, (position > lastPosition) ? R.anim.up_from_bottom : R.anim.down_from_top);
result.startAnimation(animation);
lastPosition = position;
viewHolder.txtName.setText(dataModel.getName());
viewHolder.txtType.setText(dataModel.getType());
viewHolder.txtVersion.setText(dataModel.getVersion_number());
viewHolder.info.setOnClickListener(this);
viewHolder.info.setTag(position);
// Return the completed view to render on screen
return convertView;
}
}
Trong đoạn code trên, ta đã thêm một OnClickListener vào ImageView. Khi người dùng nhấn vào nó, một SnackBar sẽ hiển thị kèm theo mô tả cho hàng tương ứng. Các hàng trong danh sách cũng được thêm hiệu ứng animation khi cuộn. Dưới đây là nội dung hai file XML định nghĩa animation.
down_from_top.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="<https://schemas.android.com/apk/res/android>"
android:shareInterpolator="@android:anim/decelerate_interpolator">
<translate
android:fromXDelta="0%" android:toXDelta="0%"
android:fromYDelta="-100%" android:toYDelta="0%"
android:duration="400" />
</set>
up_from_bottom.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="<https://schemas.android.com/apk/res/android>"
android:shareInterpolator="@android:anim/decelerate_interpolator">
<translate
android:fromXDelta="0%" android:toXDelta="0%"
android:fromYDelta="100%" android:toYDelta="0%"
android:duration="400" />
</set>
MainActivity.java là nơi ta gán CustomAdapter cho ListView. Cùng với đó, ta cũng khởi tạo một ArrayList chứa các đối tượng DataModel.
public class MainActivity extends AppCompatActivity {
ArrayList<DataModel> dataModels;
ListView listView;
private static CustomAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
listView=(ListView)findViewById(R.id.list);
dataModels= new ArrayList<>();
dataModels.add(new DataModel("Apple Pie", "Android 1.0", "1","September 23, 2008"));
dataModels.add(new DataModel("Banana Bread", "Android 1.1", "2","February 9, 2009"));
dataModels.add(new DataModel("Cupcake", "Android 1.5", "3","April 27, 2009"));
dataModels.add(new DataModel("Donut","Android 1.6","4","September 15, 2009"));
dataModels.add(new DataModel("Eclair", "Android 2.0", "5","October 26, 2009"));
dataModels.add(new DataModel("Froyo", "Android 2.2", "8","May 20, 2010"));
dataModels.add(new DataModel("Gingerbread", "Android 2.3", "9","December 6, 2010"));
dataModels.add(new DataModel("Honeycomb","Android 3.0","11","February 22, 2011"));
dataModels.add(new DataModel("Ice Cream Sandwich", "Android 4.0", "14","October 18, 2011"));
dataModels.add(new DataModel("Jelly Bean", "Android 4.2", "16","July 9, 2012"));
dataModels.add(new DataModel("Kitkat", "Android 4.4", "19","October 31, 2013"));
dataModels.add(new DataModel("Lollipop","Android 5.0","21","November 12, 2014"));
dataModels.add(new DataModel("Marshmallow", "Android 6.0", "23","October 5, 2015"));
adapter= new CustomAdapter(dataModels,getApplicationContext());
listView.setAdapter(adapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
DataModel dataModel= dataModels.get(position);
Snackbar.make(view, dataModel.getName()+"\\n"+dataModel.getType()+" API: "+dataModel.getVersion_number(), Snackbar.LENGTH_LONG)
.setAction("No action", null).show();
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
Giao diện của ứng dụng khi hoạt động sẽ như hình bên dưới.

Bài hướng dẫn về Custom Adapter cho ListView trong Android đến đây là hết rồi. Hy vọng rằng bạn đã có thêm một giải pháp để biến các danh sách dữ liệu khô khan thành những giao diện sống động, tương tác, giúp ứng dụng của bạn nổi bật hơn trong mắt người dùng. Bạn có thể tải về project ví dụ hoàn chỉnh ở trên từ đây.