22 กันยายน 2555

[Android Code] Custom List View เบื้องต้น

        ** อัพเดทใหม่เมื่อ 22/05/2014 **

        บทความนี้ก็จะขอพูดถึงเรื่องของการ Custom List View กันบ้าง เพราะบ่อยครั้งที่มีผู้ที่หลงเข้ามาอ่านที่ใช้งาน List View แต่ทว่า List View แบบเดิมๆนั้นไม่สามารถตอบโจทย์ให้ได้มากนัก เพราะเป็นแค่ Text View ธรรมดาๆเท่านั้น


        ดังนั้นเจ้าของบล็อกจึงขอประเดิมบทความนี้ให้เป็นการทำ List View ในแบบที่ต้องการ

        ก่อนจะมาทำ Custom List View ก็ควรรู้พื้นฐานของ List View กันเสียหน่อยจะดีกว่า ว่าเดิมทีนั้น List View มันทำงานยังไง เพื่อที่ว่าเวลาทำ Custom แล้วจะได้มองภาพได้ง่ายขึ้น

        ในการใช้งาน List View โดยปกติแล้วจะใช้เวลาที่ต้องการแสดงข้อความในรูปแบบของ List ที่เป็นข้อมูลจำนวนเยอะเรียงต่อๆกัน

        ผู้ที่หลงเข้ามาอ่านหลายๆท่านมักจะเข้าใจผิดว่าการใช้งาน List View คือการเอาข้อมูลยัดลงใน List View แล้วมันจะแสดงข้อมูลให้ทันที แต่แท้จริงแล้วหัวใจสำคัญที่สุดจะอยู่ที่ Adapter ต่างหาก โดยที่ข้อมูลจะต้องกำหนดลงไปใน Adapter ก่อน แล้วนำ Adapter ไปกำหนดให้ List View อีกที ถึงจะทำให้ List View แสดงข้อมูลได้ ดังนั้น List View ก็เป็นแค่ View ที่ใช้สำหรับแสดงข้อมูลเท่านั้น

        สำหรับ Adapter ก็เปรียบเสมือนแหล่งกำหนดข้อมูลที่จะให้แสดง ในการใช้งาน List View แบบพื้นฐาน ข้อมูลที่จะแสดงจะอยู่ในรูปของ String Array โดยกำหนดให้แสดงข้อมูลแต่ละตัวบน Text View ที่ระบบได้เตรียมไว้แล้ว (android.R.layout.simple_list_item_1) แล้วจึงนำไปแสดงใน List View

        ถ้าจะให้สรุปการทำงานคร่าวๆก็จะได้ออกมาเป็นแบบนี้


        จะเห็นว่าใน Adapter จะมีการกำหนด Layout ที่จะใช้ด้วย ถ้าเจ้าของบล็อกลองเปลี่ยนไปเป็น Layout ที่สร้างขึ้นมาแทนล่ะ? ก็ย่อมได้ แต่โดยปกติ Adapter ที่ใช้งานกันมักจะเป็น ArrayAdapter ซึ่งคลาสนี้ทำขึ้นมาสำหรับ Text View ธรรมดาๆเท่านั้น ดังนั้นถ้าอยากจะกำหนดให้แสดงข้อมูลอย่างอื่นนอกเหนือไปจาก Text View ธรรมดาๆ ก็จะต้องทำการ Custom เอาเอง

        เพิ่มเติม - จะบอกว่าเป็นการทำ Custom List View ก็ไม่ถูกต้องซักเท่าไร เพราะแท้จริงแล้วสิ่งที่ทำคือ Custom Adapter ต่างหาก ส่วน List View จะไม่ได้ไป Custom อะไรเลย แต่ทว่าส่วนใหญ่จะนิยมเรียกกันว่า Custom List View เพราะว่ามันใช้กับ List View นั่นเอง จึงขอให้เข้าใจไว้ ณ ที่นี้ด้วย ว่าจริงๆแล้วมันคือการ Custom Adapter ต่างหากล่ะ XD

        สมมติว่าเจ้าของบล็อกอยากให้ในแต่ละแถวของ List View มี Image View กับ Text View อย่างละอัน โดยที่เจ้าของบล็อกสร้าง Layout ขึ้นมาใหม่ (จะตั้งไฟล์ว่าอะไรก็ได้) แล้วจัด Layout ออกมาดังนี้

listview_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="wrap_content"
    android:background="#FFFFFF"
    android:gravity="center_vertical"
    android:orientation="horizontal" >

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:background="@drawable/ic_launcher" />
    
    <TextView
        android:id="@+id/textView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="5dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="20dp"
        android:layout_marginTop="5dp"
        android:text="Text"
        android:textSize="20sp"
        android:textColor="#000000" />
    
</LinearLayout>

        สำหรับการสร้าง Layout เพื่อใช้ใน List View จะไม่มีกำหนดตายตัว ขอแค่อย่าลืมกำหนดชื่อ ID ให้กับ View ที่ต้องการแสดงข้อมูลด้วยก็เป็นพอ จากตัวอย่างเจ้าของบล็อกกำหนดเป็น imageView1 กับ textView1

        ต่อมาที่ต้องทำก็ตือสร้างคลาส Adapter ขึ้นมาเอง โดยที่สืบทอดจากคลาสเดิมที่มีอยู่ โดยเจ้าของบล็อกจะสืบทอดมาจาก BaseAdapter ซึ่งเป็นคลาสแม่ของเหล่าคลาส Adapter ต่างๆ โดยกำหนดชื่อขึ้นมาว่า CustomAdapter (หากินง่ายดี)
package app.akexorcist.customlistview;

import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

public class CustomAdapter extends BaseAdapter {

    public int getCount() {
        return 0;
    }

    public Object getItem(int position) {
        return null;
    }

    public long getItemId(int position) {
        return 0;
    }

    public View getView(int position, View view, ViewGroup paernt) {
        return null;
    }
}

        อันนี้คือฟอร์มเริ่มต้นสำหรับ BaseAdapter การสร้างคลาสใดๆที่สืบทอดจากคลาสนี้จะต้องมีฟอร์มเริ่มต้นเช่นนี้ทุกครั้ง โดยแต่ละฟังก์ชันจะมีหน้าที่ต่างกันไปดังนี้

        getCount - เอาไว้บอกว่ามีข้อมูลอยู่ทั้งหมดกี่ชุด (จำนวน Array ที่ใช้กำหนดเป็นข้อมูล)

        getItem -  เอาไว้ดึง Object ใดๆแล้วแต่จะกำหนด อย่างเช่น List View ที่มี Text View เป็นสมาชิกหลัก ผู้ที่หลงเข้ามาอ่านสามารถกำหนดในฟังก์ชันนี้ได้ว่าจะให้ Return เป็น Text View ของข้อมูลแถวที่ต้องการได้

        getItemId - คล้ายๆกับ getItem แต่ว่าจะเป็น ID ของ Object นั้นๆ

        getView - เอาไว้กำหนดค่าให้กับ Layout ที่ใช้แสดงในแต่ละแถวของ List View


        ในการทำ Custom List View หัวใจสำคัญหลักๆก็คงจะอยู่ที่ getView เพราะฟังก์ชันนี้จะทำงานก็ต่อเมื่อแต่ละแถวของ List View ถูกแสดงให้เห็นบนหน้าจอ getView นี้ก็จะทำงานทันทีพร้อมกับบอกว่าเป็นของข้อมูลชุดที่เท่าไร ดังนั้นในนี้จึงเหมาะกับการกำหนดคำสั่งต่างๆเพื่อใช้แสดงข้อมูลใน List View

        ก่อนจะไปยุ่งกับ getView ต้องเตรียมข้อมูลให้พร้อมเสียก่อน เพราะคลาสตัวนี้ยังไม่ได้มีการรับข้อมูลมาจากที่ใดเลย แล้วจะเอาข้อมูลจากไหนไปกำหนดใน List View ได้ล่ะ?  ดังนั้นเจ้าของบล็อกจึงประกาศ Constructor ให้กับคลาสนี้ซะก่อนดังนี้
package app.akexorcist.customlistview;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

public class CustomAdapter extends BaseAdapter {
    Context mContext;
    String[] strName; 
    int[] resId;
       
    public CustomAdapter(Context context, String[] strName, int[] resId) {
        this.mContext= context;
        this.strName = strName;
        this.resId = resId;
    }

    public int getCount() {
        return 0;
    }

    public Object getItem(int position) {
        return null;
    }

    public long getItemId(int position) {
        return 0;
    }

    public View getView(int position, View view, ViewGroup parent) {
        return null;
    }
}

        สำหรับ Constructor จะเห็นว่าเจ้าของบล็อกกำหนดพารามิเตอร์เพิ่มเข้ามาสามตัวด้วยกัน คือ Context, String Array และ Integer Array โดยที่ String Array คือข้อความที่ต้องการกำหนดลงใน List View ส่วน Integer Array คือ Resource ID ของภาพที่เก็บไว้ในโฟลเดอร์ Drawable (เจ้าของบล็อกจะไม่ใช้วิธีกำหนด Bitmap Array เพราะมันเปลือง Memory แต่จะใช้วิธีกำหนด Resource ID แล้วค่อยนำมากำหนดให้กับ Image View ใน getView อีกทีหนึ่ง) ส่วน Context จะต้องใช้ตอนที่ getView ทำงาน ไว้รออธิบายตอนนั้นแล้วก็จะเข้าใจเอง

        ดังนั้นเวลาประกาศใช้งานคลาสนี้ก็จะเป็นดังนี้
CustomAdapter adapter = new CustomAdapter(context, strName, resId);

        เมื่อกำหนดพารามิเตอร์แล้วเจ้าของบล็อกก็โยนค่าจากพารามิเตอร์เหล่านี้ขึ้นไปเก็บไว้เป็น Global Variable แทน (เพราะจะนำไปใช้ใน getView อีกทีหนึ่ง)

        และก่อนที่จะไปกำหนดคำสั่งใน getView เพื่อป้องกันการลืม ให้ไปกำหนดใน getCount เสียก่อนเลย จากเดิมที่บอกไว้ว่า getCount คือคำสั่งเอาไว้อ่านว่าข้อมูลที่กำหนดใน Adapter ตัวนี้มีทั้งหมดเท่าไร แต่ทว่าข้อมูลที่จะใช้ใน Adapter นี้มีสองชุดด้วยกัน ซึ่งจะเลือกตัวไหนก็ได้ เพราะถือว่าข้อมูลทั้งสองอย่างมีจำนวนสมาชิกที่เท่ากันอยู่แล้ว
package app.akexorcist.customlistview;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

public class CustomAdapter extends BaseAdapter {
    Context mContext;
    String[] strName; 
    int[] resId;
       
    public CustomAdapter(Context context, String[] strName, int[] resId) {
        this.mContext= context;
        this.strName = strName;
        this.resId = resId;
    }

    public int getCount() {
        return strName.length;
    }

    public Object getItem(int position) {
        return null;
    }

    public long getItemId(int position) {
        return 0;
    }

    public View getView(int position, View view, ViewGroup parent) {
        return null;
    }
}
        ส่วน getItem กับ getItemId จะไม่ค่อยได้ใช้กันอยู่แล้ว เจ้าของบล็อกจึงขอถือวิสาสะข้ามไปเลยละกันเนอะ

        ทีนี้มาเข้าสู่ส่วนสำคัญของคลาสนี้กันต่อเลย นั่นก็คือ getView ซึ่งอย่างที่บอกว่าในฟังก์ชันนี้จะทำงานเมื่อข้อมูลในแถวใดๆถูกแสดงให้เห็นบนหน้าจอ ดังนั้นในหน้านี้จะต้องกำหนด Layout ที่จะให้แสดงข้อมูล ทำการกำหนดข้อมูลซะ
package app.akexorcist.customlistview;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

public class CustomAdapter extends BaseAdapter {
    Context mContext;
    String[] strName; 
    int[] resId;
       
    public CustomAdapter(Context context, String[] strName, int[] resId) {
        this.mContext= context;
        this.strName = strName;
        this.resId = resId;
    }

    public int getCount() {
        return strName.length;
    }

    public Object getItem(int position) {
        return null;
    }

    public long getItemId(int position) {
        return 0;
    }

    public View getView(int position, View view, ViewGroup parent) {
        LayoutInflater mInflater = 
                (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
     
        View view = mInflater.inflate(R.layout.listview_row, parent, false);
        return view;
    }
}
        จากคำสั่งเป็นคำสั่งกำหนด Layout ที่จะใช้แสดงใน List View ซึ่งเจ้าของบล็อกกำหนดให้ใช้เป็นไฟล์ listview_row.xml ที่สร้างไว้ในตอนแรกนั่นเอง จากนั้นก็ Return กลับขึ้นไป โดยจะเห็นว่า mContext ถูกเรียกใช้ตอนประกาศ LayoutInflater เพราะว่าคำสั่ง getSystemService จะต้องเรียกผ่าน Context ของ Actvityi หลักเท่านั้น เจ้าของบล็อกจึงกำหนดไว้ใน Constructor แล้วเก็บไว้ใน Global เพื่อนำมาใช้ในนี้นั่นเอง

        แต่ทว่ายังเป็นแค่การกำหนด Layout เฉยๆ ยังไม่ได้ทำการกำหนดข้อมูลใดๆเข้าไป ดังนั้นถ้าลองทดสอบการทำงานดูก็จะพบว่า Layout มาแสดงอยู่ใน List View ก็จริง แต่ว่ายังไม่ขึ้นข้อมูลตามที่กำหนดไว้

        ให้สังเกตตรง view ให้ดีๆ โดยที่ view ตัวนี้ก็เป็น View ของ Layout ที่กำหนดไว้ ดังนั้นถ้าต้องการประกาศ View ใดๆที่อยู่ใน Layout จะต้องเรียกผ่าน view ดังนี้
package app.akexorcist.customlistview;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

public class CustomAdapter extends BaseAdapter {
    Context mContext;
    String[] strName; 
    int[] resId;
       
    public CustomAdapter(Context context, String[] strName, int[] resId) {
        this.mContext= context;
        this.strName = strName;
        this.resId = resId;
    }

    public int getCount() {
        return strName.length;
    }

    public Object getItem(int position) {
        return null;
    }

    public long getItemId(int position) {
        return 0;
    }

    public View getView(int position, View view, ViewGroup parent) {
        LayoutInflater mInflater = 
                (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
     
        if(view == null) 
            view = mInflater.inflate(R.layout.listview_row, parent, false);

        TextView textView = (TextView)view.findViewById(R.id.textView1);
        textView.setText(strName[position]);

        ImageView imageView = (ImageView)view.findViewById(R.id.imageView1);
        imageView.setBackgroundResource(resId[position]);

        return view;
    }
}
        จะเห็นว่าสามารถใช้ findViewById ได้เลย แต่ทว่าต้องเป็น findViewById ที่มาจาก view เพื่อให้รู้ว่า View ที่กำลังประกาศเป็น View ที่อยู่ภายใน Layout ที่ได้สร้างไว้ (แล้วกำหนดค่าไว้ใน view) จากนั้นก็เอาค่าจาก Array ที่เตรียมไว้ทั้งสองตัวมากำหนดในนี้ได้เลย

        เพิ่มเติม - เนื่องจากเป็นการ Custom ขึ้นมาเอง ดังนั้นข้อมูลที่ต้องการแสดงจึงไม่ต้องห่วงเลย จากเดิมเจ้าของบล็อกใช้ข้อมูลแค่สองชุดคือข้อความกับภาพ สมมติว่าผู้ที่หลงเข้ามาอ่านต้องทำเป็นข้อมูลซักห้าชุด ก็เพียงแค่กำหนดพารามิเตอร์ใน Constructor เพิ่ม แล้วเก็บค่าไว้ใน Global เหมือนเดิม แล้วนำมากำหนดค่าใน getView นั่นเอง


        แล้วเวลาใช้งานล่ะ?

        สำหรับ List View ก็ประกาศเหมือนปกติเลย แต่ทว่า Adapter ที่ใช้กำหนดใน List View ก็ให้สร้างมาจาก Adapter ที่ได้สร้างขึ้นมาเอง ในตัวอย่างนี้เจ้าของบล็อกได้ตั้งชื่อเป็น CustomAdapter ไว้นั่นเอง


Main.java
package app.akexorcist.customlistview;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;

public class Main extends Activity {

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        int[] resId = { R.drawable.aerithgainsborough
                    , R.drawable.barretwallace, R.drawable.caitsith
                    , R.drawable.cidhighwind, R.drawable.cloudstrife
                    , R.drawable.redxiii, R.drawable.sephiroth
                    , R.drawable.tifalockhart, R.drawable.vincentvalentine
                    , R.drawable.yuffiekisaragi, R.drawable.zackfair };
        
        String[] list = { "Aerith Gainsborough", "Barret Wallace", "Cait Sith"
                    , "Cid Highwind", "Cloud Strife", "RedXIII", "Sephiroth"
                    , "Tifa Lockhart", "Vincent Valentine", "Yuffie Kisaragi"
                    , "ZackFair" };
        
        CustomAdapter adapter = new CustomAdapter(getApplicationContext(), list, resId);
        
        ListView listView = (ListView)findViewById(R.id.listView1);
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(new OnItemClickListener() {
            public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {

            }
        });
    }
}

        สำหรับ Layout หลักก็กำหนดหรือออกแบบได้ตามปกติเลย โดยมี List View อยู่ในนั้นด้วย (ถ้าไม่มี List View แล้วจะสร้าง Custom List View ขึ้นมาทำไมกันล่ะ?)

main.xml
<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"
    android:background="#FFFFFF" >
    
    <ListView
        android:id="@+id/listView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:layout_margin="20dp" />
    
</RelativeLayout>

CustomAdapter.java
package app.akexorcist.customlistview;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

public class CustomAdapter extends BaseAdapter {
    Context mContext;
    String[] strName; 
    int[] resId;
       
    public CustomAdapter(Context context, String[] strName, int[] resId) {
        this.mContext= context;
        this.strName = strName;
        this.resId = resId;
    }

    public int getCount() {
        return strName.length;
    }

    public Object getItem(int position) {
        return null;
    }

    public long getItemId(int position) {
        return 0;
    }

    public View getView(int position, View view, ViewGroup parent) {
        LayoutInflater mInflater = 
                (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
     
        if(view == null) 
            view = mInflater.inflate(R.layout.listview_row, parent, false);

        TextView textView = (TextView)view.findViewById(R.id.textView1);
        textView.setText(strName[position]);

        ImageView imageView = (ImageView)view.findViewById(R.id.imageView1);
        imageView.setBackgroundResource(resId[position]);

        return view;
    }
}


listview_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="wrap_content"
    android:background="#FFFFFF"
    android:gravity="center_vertical"
    android:orientation="horizontal" >

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:background="@drawable/ic_launcher" />
    
    <TextView
        android:id="@+id/textView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="5dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="20dp"
        android:layout_marginTop="5dp"
        android:text="Text"
        android:textSize="20sp"
        android:textColor="#000000" />
    
</LinearLayout>


AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="app.akexorcist.customlistview"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="19" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="app.akexorcist.customlistview.Main"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
     
        เมื่อลองทดสอบก็จะเห็นว่า Layout ใน List View เปลี่ยนเป็น Layout ที่ได้สร้างไว้ และข้อมูลที่แสดงอยู่ข้างในก็เป็นไปตามที่กำหนดไว้แล้ว



        จะเห็นได้ว่าการทำ Custom List View (หรือจริงๆก็คือ Custom Adapter) ไม่ได้มีอะไรยากเลย ติดแค่ว่าต้องทำคลาสเพิ่มและสร้าง Layout เพิ่ม เพื่อให้ได้ List View ในแบบที่ต้องการ สำหรับผู้ที่หลงเข้ามาอ่านคนใดต้องการไฟล์ตัวอย่างสามารถดาวน์โหลดได้ที่ Custom List View [Google Drive] หรือ Custom List View [GitHub]

        และนอกจากนี้ยังมีบทความการทำแบบขั้นสูงขึ้นมาอีกหน่อยเพื่อความมันส์ในการลองฝีมือ [Android Code] Custom List View แบบจัดเต็ม




เหล่าพันธมิตรแอนดรอยด์

Devahoy Layer Net NuuNeoI The Cheese Factory Somkiat CC Mart Routine Artit-K Arnondora Kamonway Try to be android developer Oatrice Benz Nest Studios Kotchaphan@Medium Jirawatee@Medium Travispea@Medium