[ANDROID] มาลองทำ Multiple layout บน ListView/RecyclerView กัน

          หากนึกภาพไม่ออกว่า ListView/RecyclerView ที่มี layout หลายๆ แบบ คืออะไร ให้ลองนึกภาพ App Chat หน้าตาประมาณนั้นเลย มันหลายแบบยังไง? ลองมาดูตัวอย่างจาก App Facebook Messenger กัน

จากรูปด้านบนจะเห็นว่าข้อความ สามารถมี layout อยู่ทั้งหมด 3 แบบด้วยกัน คือ
  1. ข้อความของเราเอง (กล่องสีน้ำเงิน)
  2. ข้อความของคนอื่น (กล่องสีเทา)
  3. รูป Sticker
          พอจะเห็นภาพมากขึ้นละเนาะ โดยปกติเวลาสร้าง ListView/RecyclerView ก็จะมี layout อยู่แบบเดียว แต่พอต้องมาทำ ListView/RecyclerView ที่มี layout หลายๆ แบบ ผมถึงกับต้องอึ้ง!! เพราะไม่รู้ว่าต้องทำยังไง #จบข่าว แต่ด้วยหน้าที่อันยิ่งใหญ่ มาพร้อมกับความรับผิดชอบอันใหญ่ยิ่ง ต้องฝ่าฟันอุปสรรคออกไปให้ได้ ดังนั้น เรามาดูวิธีกันเลยดีกว่าครับ

สิ่งที่ควรรู้มาก่อน

  1. Custom ListView โดยคุณเอก - http://www.akexorcist.com
  2. 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
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 สำหรับ ListView
4.2 Adpater สำหรับ RecyclerView
4.1 Adapter ของ ListView
สำหรับ ListView เราจะ Override getViewTypeCount และ getItemViewType ของ BaseAdapter กันครับ

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

Teeranai P

Developer ตัวน้อยๆ ที่หลงใหลในโลกของการพัฒนา Software. รักการเขียนโปรแกรมเป็นอันดับ 2 รองลงมาจากการนอน