Custom Adapter
Nếu sử dụng ArrayAdapter
thì kiểu dữ liệu truyền vào là 1 list String
và layout định nghĩa từng dòng cho ListView
chỉ cần TextView
.
Làm sao biết được cách mà Adapter
sẽ lấy dữ liệu và gắn vào từng dòng như thế nào? Bài viết sẽ chỉ rõ cách mà Adapter
làm việc như thế nào và cách tùy chỉnh ListView
với những nhu cầu phức tạp hơn.
Tạo 1 project có tên là ListViewAnvanced.
File activity_main.xml được định nghĩa như sau:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.nguyennghia.listviewadvanced.MainActivity"> <ListView android:id="@+id/lv_songs" android:layout_width="match_parent" android:layout_height="match_parent" /> </RelativeLayout>
Tạo file row_song.xml trong thư mục layout và design để hiển thị thông tin của 1 bài hát như hình dưới đây:
Nội dung của file row_song.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" android:paddingBottom="3dp" android:paddingLeft="6dp" android:paddingRight="6dp" android:paddingTop="3dp"> <FrameLayout android:id="@+id/fl_code" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_centerVertical="true"> <TextView android:id="@+id/tv_code" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#19b395" android:textSize="18dp" /> </FrameLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginLeft="8dp" android:layout_toRightOf="@+id/fl_code" android:orientation="vertical"> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="end" android:textColor="#2c3e50" android:textSize="16dp" android:textStyle="bold" /> <TextView android:id="@+id/tv_lyric" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="end" android:textColor="#34495e" android:textSize="14dp" /> <TextView android:id="@+id/tv_artist" android:textColor="#7f8c8d" android:textSize="14dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> </RelativeLayout>
class Song
đại diện cho 1 bài hát gồm các thông tin:
mCode
: mã số bài hátmTitle
: tên bài hátmLyric
: lời bài hát.mAstist
: tên ca sĩ.
File Song.java
package com.example.nguyennghia.listviewadvanced; /** * Created by nguyennghia on 24/08/2016. */ public class Song { private String mCode; private String mTitle; private String mLyric; private String mArtist; public Song() { } public Song(String code, String title, String lyric, String artist) { this.mCode = code; this.mTitle = title; this.mLyric = lyric; this.mArtist = artist; } public String getCode() { return mCode; } public String getTitle() { return mTitle; } public String getLyric() { return mLyric; } public String getArtist() { return mArtist; } public void setCode(String code) { this.mCode = code; } public void setTitle(String title) { this.mTitle = title; } public void setLyric(String lyric) { this.mLyric = lyric; } public void setArtist(String artist) { this.mArtist = artist; } }
Phần quan trọng nhất là viết custom Adapter.
package com.example.nguyennghia.listviewadvanced; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; import java.util.List; /** * Created by nguyennghia on 24/08/2016. */ public class SongAdapter extends ArrayAdapter<Song> { private Context mContext; private LayoutInflater mLayoutInflater; private List<Song> mSongs; public SongAdapter(Context context, List<Song> objects) { super(context, 0, objects); mContext = context; mSongs = objects; mLayoutInflater = LayoutInflater.from(context); } @Override public View getView(int position, View convertView, ViewGroup parent) { // Get song object at position Song song = mSongs.get(position); // Inflat view from row_song.xml convertView = mLayoutInflater.inflate(R.layout.row_song, parent, false); // findViewById in convertView TextView tvCode = (TextView) convertView.findViewById(R.id.tv_code); TextView tvTitle = (TextView) convertView.findViewById(R.id.tv_title); TextView tvLyric = (TextView) convertView.findViewById(R.id.tv_lyric); TextView tvArtist = (TextView) convertView.findViewById(R.id.tv_artist); // Set attributes tvCode.setText(song.getCode()); tvTitle.setText(song.getTitle()); tvLyric.setText(song.getLyric()); tvArtist.setText(song.getArtist()); return convertView; } }
File MainActivity.java
package com.example.nguyennghia.listviewadvanced; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.widget.ListView; import java.util.ArrayList; import java.util.List; public class MainActivity extends AppCompatActivity { private ListView lvSongs; private List<Song> mSongs; private SongAdapter mSongAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); lvSongs = (ListView) findViewById(R.id.lv_songs); // Create song data mSongs = new ArrayList<>(); mSongs.add(new Song("60696", "NẾU EM CÒN TỒN TẠI", "Khi anh bắt đầu 1 tình yêu Là lúc anh tự thay", "Trịnh Đình Quang")); mSongs.add(new Song("60701", "NGỐC", "Có rất nhiều những câu chuyện Em dấu riêng mình em biết", "Khắc Việt")); mSongs.add(new Song("60650", "HÃY TIN ANH LẦN NỮA", "Dẫu cho ta đã sai khi ở bên nhau Cô yêu thương", "Thiên Dũng")); mSongs.add(new Song("60610", "CHUỖI NGÀY VẮNG EM", "Từ khi em bước ra đi cõi lòng anh ngập tràng bao", "Duy Cường")); mSongs.add(new Song("60656", "KHI NGƯỜI MÌNH YÊU KHÓC", "Nước mắt em đang rơi trên những ngón tay Nước mắt em", "Phạm Mạnh Quỳnh")); mSongs.add(new Song("60685", "MỞ", "Anh mơ gặp em anh mơ được ôm anh mơ được gần", "Trịnh Thăng Bình")); mSongs.add(new Song("60752", "TÌNH YÊU CHẮP VÁ", "Muốn đi xa nơi yêu thương mình từng có Để không nghe", "Mr. Siro")); mSongs.add(new Song("60608", "CHỜ NGÀY MƯA TAN", "1 ngày mưa và em khuất xa nơi anh bóng dáng cứ", "Trung Đức")); mSongs.add(new Song("60603", "CÂU HỎI EM CHƯA TRẢ LỜI", "Cần nơi em 1 lời giải thích thật lòng Đừng lặng im", "Yuki Huy Nam")); mSongs.add(new Song("60720", "QUA ĐI LẶNG LẼ", "Đôi khi đến với nhau yêu thương chẳng được lâu nhưng khi", "Phan Mạnh Quỳnh")); mSongs.add(new Song("60856", "QUÊN ANH LÀ ĐIỀU EM KHÔNG THỂ - REMIX", "Cần thêm bao lâu để em quên đi niềm đâu Cần thêm", "Thiện Ngôn")); // Create adapter mSongAdapter = new SongAdapter(this, mSongs); // Set adapter for ListView lvSongs.setAdapter(mSongAdapter); } }
Chạy ứng dụng sẽ thấy kết quả như sau:
Đến đây có thể thấy rằng việc custom ListView
chỉ là tạo 1 Adapter mới, dòng mới và xử lý trong phương thức getView()
của Adapter. Các thao tác khác đều giống như ListView cơ bản.
Phương thức getView()
dùng để tạo và gắn dữ liệu vào cho View
trước khi thêm vào ListView
.
- Khi
ListView
hiện lên thìgetView()
gọi đúng n lần (với n là sốView
hiển thị trên màn hình, và cũng chính là số con củaListView
).- Ví dụ
ListView
có 100 item thì số con là số item nhìn thấy trên màn hình.
- Ví dụ
- Khi cuộn
ListView
thì phương thứcgetView()
được gọi (kể cả việc cuộn lên hay cuộn xuống). - Khi cuộn các item bị mất đi, sẽ bị xóa khỏi
ListView
và gọigetView()
để tạoView
mới rồi thêm vàoListView
.
Các tham số trong phương thức getView()
:
position
: vị trí của của item trong listview.convertView
: đối tượng cache view, đối tượng này rất quan trọng sẽ được đề cập thêm ngay phần dưới.parent
: đối tượngListView
.
Cơ chế tái sử dụng View của ListView - View Recycling
Khi ArrayAdapter
có 1000 item, thì thực sự ArrayAdapter
binding lên ListView
1 số item sao cho lấp đủ ListView
, còn những item khác chưa được binding lên.
Khi cuộn ListView
thì những View
mất đi sẽ được lưu trữ lại ở đối tượng convertView
, sau đó sẽ xóa View
đó khỏi ListView
và gọi getView()
để thêm View
mới vào ListView
.
Xem lại phương thức getView()
:
@Override public View getView(int position, View convertView, ViewGroup parent) { Song song = mSongs.get(position); convertView = mLayoutInflater.inflate(R.layout.row_song, parent, false); TextView tvCode = (TextView) convertView.findViewById(R.id.tv_code); TextView tvTitle = (TextView) convertView.findViewById(R.id.tv_title); TextView tvLyric = (TextView) convertView.findViewById(R.id.tv_lyric); TextView tvArtist = (TextView) convertView.findViewById(R.id.tv_artist); tvCode.setText(song.getCode()); tvTitle.setText(song.getTitle()); tvLyric.setText(song.getLyric()); tvArtist.setText(song.getArtist()); return convertView; }
Nhận thấy khi getView()
được gọi thì luôn được tạo View mới từ row_song.xml, sau đó tiếp tục findViewById
để lấy các View khác. Như vậy sẽ mất rất nhiều thời gian và chi phí. Giả sử dòng quá phức tạp như dòng News Feed của Facebook thì khi cuộn sẽ có cảm giác bị giật.
Để khắc phục nhược điểm này, sử dụng lại View
đã lưu trữ dữ liệu ở bộ nhớ đệm như sau:
@Override public View getView(int position, View convertView, ViewGroup parent) { Song song = mSongs.get(position); if(convertView == null) { Log.e(TAG, "getView: " + convertView); convertView = mLayoutInflater.inflate(R.layout.row_song, parent, false); } TextView tvCode = (TextView) convertView.findViewById(R.id.tv_code); TextView tvTitle = (TextView) convertView.findViewById(R.id.tv_title); TextView tvLyric = (TextView) convertView.findViewById(R.id.tv_lyric); TextView tvArtist = (TextView) convertView.findViewById(R.id.tv_artist); tvCode.setText(song.getCode()); tvTitle.setText(song.getTitle()); tvLyric.setText(song.getLyric()); tvArtist.setText(song.getArtist()); return convertView; }
Như vậy chỉ tạo View
cho những item đầu tiên để lấp đầy ListView
, khi cuộn View
sẽ được cache lại và sử dụng đối tượng này để đặt dữ liệu chứ không cần tạo inflate
để tạo View
mới từ row_song.xml.
Nhưng có 1 nhược điểm là phải sử dụng findViewById
lặp lại trong getView()
, để khắc phục nhược điểm này cần sử dụng 1 pattern gọi là ViewHolder
.
Sử dụng ViewHolder
Cập nhật lại class SongAdapter
như sau:
package com.example.nguyennghia.listviewadvanced; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; import java.util.List; /** * Created by nguyennghia on 24/08/2016. */ public class SongAdapter extends ArrayAdapter<Song> { private static final String TAG = "SongAdapter"; private Context mContext; private LayoutInflater mLayoutInflater; private List<Song> mSongs; public SongAdapter(Context context, List<Song> objects) { super(context, 0, objects); mContext = context; mSongs = objects; mLayoutInflater = LayoutInflater.from(context); } @Override public View getView(int position, View convertView, ViewGroup parent) { Song song = mSongs.get(position); ViewHolder viewHolder; if (convertView == null) { convertView = mLayoutInflater.inflate(R.layout.row_song, parent, false); viewHolder = new ViewHolder(); viewHolder.tvCode = (TextView) convertView.findViewById(R.id.tv_code); viewHolder.tvTitle = (TextView) convertView.findViewById(R.id.tv_title); viewHolder.tvLyric = (TextView) convertView.findViewById(R.id.tv_lyric); viewHolder.tvArtist = (TextView) convertView.findViewById(R.id.tv_artist); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.tvCode.setText(song.getCode()); viewHolder.tvTitle.setText(song.getTitle()); viewHolder.tvLyric.setText(song.getLyric()); viewHolder.tvArtist.setText(song.getArtist()); return convertView; } class ViewHolder { private TextView tvCode; private TextView tvTitle; private TextView tvLyric; private TextView tvArtist; } }
Tải source code
- Tải trực tiếp: ListViewAdvanced-master.zip.
- Tải từ Github.