จากรูปด้านบนจะเห็นว่าข้อความ สามารถมี layout อยู่ทั้งหมด 3 แบบด้วยกัน คือ
- ข้อความของเราเอง (กล่องสีน้ำเงิน)
- ข้อความของคนอื่น (กล่องสีเทา)
- รูป Sticker
พอจะเห็นภาพมากขึ้นละเนาะ โดยปกติเวลาสร้าง ListView/RecyclerView ก็จะมี layout อยู่แบบเดียว แต่พอต้องมาทำ ListView/RecyclerView ที่มี layout หลายๆ แบบ ผมถึงกับต้องอึ้ง!! เพราะไม่รู้ว่าต้องทำยังไง #จบข่าว แต่ด้วยหน้าที่อันยิ่งใหญ่ มาพร้อมกับความรับผิดชอบอันใหญ่ยิ่ง ต้องฝ่าฟันอุปสรรคออกไปให้ได้ ดังนั้น เรามาดูวิธีกันเลยดีกว่าครับ
สิ่งที่ควรรู้มาก่อน
- Custom ListView โดยคุณเอก - http://www.akexorcist.com
- ViewHolder Pattern โดยคุณเฟริ์ส - http://www.artit-k.com
#1 เตรียม Layout XML
Layout ที่จะใช้ในบทความนี้ เพื่อให้เห็นความแตกต่างจะมีอยู่ 3 แบบ ดังนี้
edittext_list_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"
android:padding="@dimen/activity_horizontal_margin">
<EditText
android:id="@+id/edittext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"/>
</LinearLayout>
textview_list_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"
android:padding="@dimen/activity_horizontal_margin">
<TextView
android:id="@+id/textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
imageview_list_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:gravity="center"
android:orientation="horizontal"
android:padding="@dimen/activity_horizontal_margin">
<ImageView
android:id="@+id/imageview"
android:layout_width="36dp"
android:layout_height="36dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher"/>
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
#2 สร้าง Class สำหรับเก็บข้อมูล
ก่อนอื่นสร้าง Class สำหรับเก็บข้อมูลในแต่ละแถวก่อน ซึ่งมีหน้าที่เก็บข้อความที่จะแสดงและประเภทของแถวนั้นๆ
Item.java
Item.java
public class Item {
public static final int TYPE_EDITTEXT = 0;
public static final int TYPE_TEXTVIEW = 1;
public static final int TYPE_IMAGEVIEW = 2;
private String data;
private int type;
public Item(String data, int type) {
this.data = data;
this.type = type;
}
public String getData() {
return data;
}
public int getType() {
return type;
}
}
#3 สร้าง Activity
ในส่วนของ Activity สมมุติข้อมูลของแต่ละแถวขึ้นมา แล้วก็ set adapter ให้ ListView/RecyclerView ก็เป็นอันเรียบร้อย (ตรงนี้ใครงง ให้กลับไปอ่านบทความที่แนะนำด้านบนใหม่นะครับ)
ListViewActivity.java
public class ListViewActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list_view);
ListView listView = (ListView) findViewById(R.id.listview);
List- items = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
items.add(new Item("I am EditText #" + i, Item.TYPE_EDITTEXT));
items.add(new Item("I am TextView #" + i, Item.TYPE_TEXTVIEW));
items.add(new Item("I am ImageView #" + i, Item.TYPE_IMAGEVIEW));
}
MultipleLayoutAdapter adapter = new MultipleLayoutAdapter(this, items);
listView.setAdapter(adapter);
}
}
RecyclerViewActivity.java
public class RecyclerViewActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recycler_view);
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
List- items = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
items.add(new Item("I am EditText #" + i, Item.TYPE_EDITTEXT));
items.add(new Item("I am TextView #" + i, Item.TYPE_TEXTVIEW));
items.add(new Item("I am ImageView #" + i, Item.TYPE_IMAGEVIEW));
}
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(new MultipleLayoutRecyclerAdapter(items));
}
}
#4 สร้าง Adapter
พระเอกของงานนี้ คือ เจ้า Adapter นี่แหละครับ แต่เนื่องจาก Adapter ของ ListView และ RecyclerView นั้นต่างกันนะครับ เลยขอแยกออกเป็น 2 ส่วนนะครับ4.1 Adpater สำหรับ ListView4.1 Adapter ของ ListView
4.2 Adpater สำหรับ RecyclerView
MultipleLayoutAdapter.java
public class MultipleLayoutAdapter extends BaseAdapter {
.
.
.
@Override
public int getViewTypeCount() {
// จำนวนประเภท layout ที่จะแสดง ในที่นี้ คือ 3
return 3;
}
@Override
public int getItemViewType(int position) {
// return ประเภทของแถว จากข้อมูลที่เรากำหนดไว้ตอนสร้าง Activity ในข้อที่ 3
return getItem(position).getType();
}
}
ในส่วนของ method getView ก็จะทำ inflate layout แต่ละประเภท ดังนี้
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Item item = getItem(position);
int viewType = getItemViewType(position);
switch (viewType) {
case Item.TYPE_EDITTEXT:
convertView = inflateEditTextView(convertView, parent, item);
break;
case Item.TYPE_TEXTVIEW:
convertView = inflateTextView(convertView, parent, item);
break;
case Item.TYPE_IMAGEVIEW:
convertView = inflateImageView(convertView, parent, item);
break;
}
return convertView;
}
จากโค้ดด้านบนจะเห็นว่าผมแตก method ออกมาเพื่อ inflate layout มาดูกันว่าข้างใน method ทำอะไรบ้าง
inflateEditTextView
private View inflateEditTextView(View convertView, ViewGroup parent, Item item) {
EditTextViewHolder viewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.edittext_list_row, parent, false);
viewHolder = new EditTextViewHolder(convertView);
convertView.setTag(viewHolder);
}
else {
viewHolder = (EditTextViewHolder) convertView.getTag();
}
viewHolder.editText.setText(item.getData());
return convertView;
}
inflateTextView
private View inflateTextView(View convertView, ViewGroup parent, Item item) {
TextViewHolder viewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.textview_list_row, parent, false);
viewHolder = new TextViewHolder(convertView);
convertView.setTag(viewHolder);
}
else {
viewHolder = (TextViewHolder) convertView.getTag();
}
viewHolder.textView.setText(item.getData());
return convertView;
}
inflateImageView
private View inflateImageView(View convertView, ViewGroup parent, Item item) {
ImageViewHolder viewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.imageview_list_row, parent, false);
viewHolder = new ImageViewHolder(convertView);
convertView.setTag(viewHolder);
}
else {
viewHolder = (ImageViewHolder) convertView.getTag();
}
viewHolder.textView.setText(item.getData());
return convertView;
}
เอ .... รู้สึกว่าจะลืมอะไรไปซักอย่าง ติ๊กต๋อก ติ๊กต๋อก ติ๊กต๋อก ปิ๊ง!! ลืม ViewHolder นั่นเอง
static class EditTextViewHolder {
private EditText editText;
public EditTextViewHolder(View convertView) {
this.editText = (EditText) convertView.findViewById(R.id.edittext);
}
}
static class TextViewHolder {
private TextView textView;
public TextViewHolder(View convertView) {
this.textView = (TextView) convertView.findViewById(R.id.textview);
}
}
static class ImageViewHolder {
private ImageView imageView;
private TextView textView;
public ImageViewHolder(View convertView) {
this.imageView = (ImageView) convertView.findViewById(R.id.imageview);
this.textView = (TextView) convertView.findViewById(R.id.textview);
}
}
4.2 Adapter ของ RecyclerView
ส่วนของ RecyclerView จะใช้ RecyclerView.Adapter แทน BaseAdapter ซึ่งจะมีเพียง method getItemViewType เท่านั้น
MultipleLayoutAdapter.java
public class MultipleLayoutRecyclerAdapter extends RecyclerView.Adapter {
private List- items;
.
.
@Override
public int getItemViewType(int position) {
return items.get(position).getType();
}
}
อ่าว!! เหมือนของ BaseAdapter เลยนี่นา ... ตรงส่วนนี้ทำงานเหมือนกันเลยทั้งของ BaseAdapter และ RecyclerView.Adapter เพียงแต่ว่า RecyclerView.Adapter จะไม่มี method getViewTypeCount แล้วนั่นเอง แล้วมันจะทำงานได้ยังไง? มาลองดูกันครับ
onCreateViewHolder
ต้องบอกว่า RecyclerView.Adapter นี่มัน cool จริงๆ เพราะว่าใน argument ของ method onCreateViewHolder นั้นมี viewType มาให้ด้วย ทำให้ทำ Multiple layout ของเราสะดวกขึ้นเยอะเลย
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (viewType) {
case Item.TYPE_EDITTEXT:
View vEditText = inflater.inflate(R.layout.edittext_list_row, parent, false);
return new EditTextViewHolder(vEditText);
case Item.TYPE_TEXTVIEW:
View vTextView = inflater.inflate(R.layout.textview_list_row, parent, false);
return new TextViewHolder(vTextView);
case Item.TYPE_IMAGEVIEW:
default:
View vImageView = inflater.inflate(R.layout.imageview_list_row, parent, false);
return new ImageViewHolder(vImageView);
}
}
onBindViewHolder
มาถึงส่วนของการแสดงผล RecyclerView.Adapter จะวิ่งมาที่ method onBindViewHolder ซึ่งตรงนี้เราอาจจะต้อง casting ViewHolder กันหน่อย เผื่อให้ทำงานได้ถูกประเภท
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
Item item = items.get(position);
switch (viewHolder.getItemViewType()) {
case Item.TYPE_EDITTEXT:
EditTextViewHolder editTextViewHolder = (EditTextViewHolder) viewHolder;
editTextViewHolder.editText.setText(item.getData());
break;
case Item.TYPE_TEXTVIEW:
TextViewHolder textViewHolder = (TextViewHolder) viewHolder;
textViewHolder.textView.setText(item.getData());
break;
case Item.TYPE_IMAGEVIEW:
default:
ImageViewHolder imageViewHolder = (ImageViewHolder) viewHolder;
imageViewHolder.textView.setText(item.getData());
break;
}
}
ส่วนสุดท้ายก็ คือ ViewHolder ซึ่งของ RecyclerView ก็จะแตกต่างจาก ListView นิดนึง เพราะต้อง extends class RecyclerView.ViewHolder
static class EditTextViewHolder extends RecyclerView.ViewHolder {
private EditText editText;
public EditTextViewHolder(View itemView) {
super(itemView);
this.editText = (EditText) itemView.findViewById(R.id.edittext);
}
}
static class TextViewHolder extends RecyclerView.ViewHolder {
private TextView textView;
public TextViewHolder(View itemView) {
super(itemView);
this.textView = (TextView) itemView.findViewById(R.id.textview);
}
}
static class ImageViewHolder extends RecyclerView.ViewHolder {
private ImageView imageView;
private TextView textView;
public ImageViewHolder(View itemView) {
super(itemView);
this.imageView = (ImageView) itemView.findViewById(R.id.imageview);
this.textView = (TextView) itemView.findViewById(R.id.textview);
}
}
Running...
หลังจากเหน็ดเหนื่อยกับการตรากตรำทำโค้ดมาอย่างยากลำบาก เรามาดูผลลัพธ์กันดีกว่า
ผลลัพธ์ออกมาก็สวยงามพอใช้ได้ ท่านไหนอยากได้แบบงามๆ ผมอับโค้ดไว้ที่ Github ลองเอาไปทำเล่นต่อกันดูนะครับ ทำแล้วก็อย่าลืมเอามาอวดกันบ้างเด้อ
สำหรับบทความนี้ก็คงจบลงเพียงเท่านี้ครับ ผิดพลาดประการใด ก็สามารถติชม แนะนำเข้ามาได้เลยนะครับ
ขอบคุณครับ
ลิงค์อ้างอิง
Google reference : BaseAdapter
akexorcist.com : [Android Code] Custom List View เบื้องต้น
artit-k.com : [Dev] รู้จักกับ ViewHolder Pattern สำหรับ ListView, GridView...
stackoverflow.com : Making sense of LayoutInflater
github.com : RockerFlower/GroupViewHolder
artit-k.com : [Dev] รู้จักกับ ViewHolder Pattern สำหรับ ListView, GridView...
stackoverflow.com : Making sense of LayoutInflater
github.com : RockerFlower/GroupViewHolder