Search…

ListView trong Android - Phần 2

16/11/20207 min read
Hướng dẫn tạo ra các ListView trong Android với giao diện phức tạp và linh hoạt.

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:

ListView trong Android

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át
  • mTitle: tên bài hát
  • mLyric: 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:

ListView trong Android

Đế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ủa ListView).
    • Ví dụ ListView có 100 item thì số con là số item nhìn thấy trên màn hình.
  • Khi cuộn ListView thì phương thức getView() đượ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ọi getView() để tạo View mới rồi thêm vào ListView.

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ượng ListView.

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

Bài chung series

IO Stream

IO Stream Co., Ltd

30 Trinh Dinh Thao, Hoa Thanh ward, Tan Phu district, Ho Chi Minh city, Vietnam
+84 28 22 00 11 12
developer@iostream.co

383/1 Quang Trung, ward 10, Go Vap district, Ho Chi Minh city
Business license number: 0311563559 issued by the Department of Planning and Investment of Ho Chi Minh City on February 23, 2012

©IO Stream, 2013 - 2024