07 พฤศจิกายน 2556

[Android Code] จัดการกับ Custom List View ที่มีภาพอย่างไรให้มีประสิทธิภาพ


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


        ถ้าใครหลายๆคนที่เคยทำตามบทความเจ้าของบล็อกในบทความเรื่อง [Android Code] Custom List View เบื้องต้น ถ้าลองทำตามตัวอย่างเป๊ะๆก็จะเห็นว่าทำงานได้ปกติ

        แต่ทีนี้ "ถ้าแสดงภาพหลายๆภาพและมีขนาดใหญ่ล่ะ?"


        จะเห็นว่าเมื่อเลื่อนไปได้ซักพักจะเกิด Out of memory ขึ้น ทั้งนี้ก็มาจากการโหลดภาพจำนวนเยอะๆนั่นแหละ

        ซึ่งในบทความนี้ เจ้าของบล็อกก็จะมาอธิบายต่อ ถึงการจัดการกับภาพให้แสดงบน Custom List View ได้ดียิ่งขึ้น


        จากเดิมคือนำภาพใน Drawable มาแสดงบน Image View ตรงๆ (ให้ย้อนกลับไปดูบทความเก่า) ซึ่งต้องประกาศ int[] ของภาพ Drawable ในกรณีที่ใช้ภาพจำนวนเยอะๆ ก็จะทำให้ต้องกำหนดตัวแปรเยอะตาม จึงเปลี่ยนใหม่จากการประกาศไว้ใน int[] ก็ลองมาใช้ Resource ดูหน่อย [Android Code] การเก็บค่าไว้ใน res/values [Values Resource]

        ถ้าได้ลองอ่านตามลิ้งบทความที่ให้ไปนี้แล้ว จะเห็นว่าผู้ที่หลงเข้ามาอ่านเก็บค่าตัวแปรไว้ใน values.xml ได้ ข้อดีก็คือทำให้เก็บข้อมูลแบบแยกให้เป็นระเบียบได้ ดังนั้น String Array และ Integer Array ที่ใช้กำหนดข้อมูล เจ้าของบล็อกก็จะย้ายไปเก็บไว้ใน values แทน แต่ไม่ใช่ไฟล์ values.xml นะ สร้างขึ้นใหม่เลย ชื่อ listview_data.xml เพราะการสร้าง Values Resource ไม่จำเป็นต้องเก็บไว้ใน string.xml การสร้างไฟล์แยกออกมาแต่เก็บข้อมูลเหมือนกันก็ทำได้เช่นกัน ข้อดีคือตัวอย่างของเจ้าของบล็อกจะได้ไม่ต้องไปยุ่ง string.xml



listview_data.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string-array name="my_image_array">
        <item>@drawable/aerithgainsborough</item>
        <item>@drawable/barretwallace</item>
        <item>@drawable/caitsith</item>
        <item>@drawable/cidhighwind</item>
        <item>@drawable/cloudstrife</item>
        <item>@drawable/elena</item>
        <item>@drawable/elfe</item>
        <item>@drawable/fuhito</item>
        <item>@drawable/gun</item>
        <item>@drawable/heidegger</item>
        <item>@drawable/katana</item>
        <item>@drawable/knife</item>
        <item>@drawable/martialartsf</item>
        <item>@drawable/martialartsm</item>
        <item>@drawable/nunchaku</item>
        <item>@drawable/presidentshinra</item>
        <item>@drawable/professorhojo</item>
        <item>@drawable/redxiii</item>
        <item>@drawable/reevetuesti</item>
        <item>@drawable/renosinclair</item>
        <item>@drawable/rod</item>
        <item>@drawable/rude</item>
        <item>@drawable/rufusshinra</item>
        <item>@drawable/scarlet</item>
        <item>@drawable/sears</item>
        <item>@drawable/sephiroth</item>
        <item>@drawable/shalva</item>
        <item>@drawable/shotgun</item>
        <item>@drawable/shuriken</item>
        <item>@drawable/tifalockhart</item>
        <item>@drawable/tseng</item>
        <item>@drawable/twoguns</item>
        <item>@drawable/verdot</item>
        <item>@drawable/vincentvalentine</item>
        <item>@drawable/yuffiekisaragi</item>
        <item>@drawable/zackfair</item>
    </string-array>

    <string-array name="my_string_array">
        <item>Aerith Gainsborough</item>
        <item>Barret Wallace</item>
        <item>Cait Sith</item>
        <item>Cid Highwind</item>
        <item>Cloud Strife</item>
        <item>Elena</item>
        <item>Elfe</item>
        <item>Fuhito</item>
        <item>Gun</item>
        <item>Heidegger</item>
        <item>Katana</item>
        <item>Knife</item>
        <item>Martial Arts F</item>
        <item>Martial Arts M</item>
        <item>Nunchaku</item>
        <item>President Shinra</item>
        <item>Professor Hojo</item>
        <item>Red XIII</item>
        <item>Reeve Tuesti</item>
        <item>Reno Sinclair</item>
        <item>Rod</item>
        <item>Rude</item>
        <item>Rufus Shinra</item>
        <item>Scarlet</item>
        <item>Sears</item>
        <item>Sephiroth</item>
        <item>Shalva</item>
        <item>Shotgun</item>
        <item>Shuriken</item>
        <item>Tifa Lockhart</item>
        <item>Tseng</item>
        <item>Two Guns</item>
        <item>Verdot</item>
        <item>Vincent Valentine</item>
        <item>Yuffie Kisaragi</item>
        <item>Zack Fair</item>
    </string-array>
    
    <color name="blue">#32E0FF</color>
    <color name="yellow">#FFE432</color>

</resources>

        จะเห็นว่าเจ้าของบล็อกเตรียมข้อมูลไว้ทั้งหมดสามส่วน

        • Integer Array ที่เป็น ID ของ Drawable โดยกำหนดชื่อของอาร์เรย์ชุดนี้ว่า my_image_array

        • String Array ที่จะแสดงใน Text View โดยกำหนดชื่อของอาร์เรย์ชุดนี้ว่า my_string_array

        • Color อันนี้กำหนดเพิ่มเล่นๆ เป็นค่าสีที่เอาไว้เรียกใช้


        ในการเรียกใช้งานค่าทั้งสองก็จะเรียกผ่านคำสั่งดังนี้

TypedArray image_array = getResources().obtainTypedArray(R.array.my_image_array);
int[] array_res = new int[my_image_array.length()];
for(int i = 0 ; i < array_res.length ; i++) 
    array_res[i] = my_image_array.getResourceId(i, R.drawable.ic_launcher);
my_image_array.recycle();

        โดยจะกำหนด TypedArray ที่จะดึงมาก่อน จากนั้นสร้าง Integer Array รอรับค่า จากนั้นก็วนรับค่าให้ครบ กรณีที่ดึงค่าที่ตำแหน่งใดๆไม่ได้จะแทนด้วยภาพไอคอน จากนั้นก็เคลียร์ข้อมูลในตัวแปรของคลาส TypedArray ทิ้งซะ


        สำหรับกรณีข้อมูล String Array ก็ไม่ต่างกันซักเท่าไร ต่างกันแค่ตรงที่ไม่ต้องกำหนด Default Values ในกรณีที่ดึงค่าไม่ได้

TypedArray string_array = getResources().obtainTypedArray(R.array.my_string_array);
String[] array_string = new String[my_string_array.length()];
for(int i = 0 ; i < array_string.length ; i++) 
    array_string[i] = my_string_array.getString(i);
my_string_array.recycle();

        ดังนั้นเจ้าของบล็อกจะได้ String Array ของข้อความที่จะแสดงเก็บไว้ในตัวแปร String Array ที่ชื่อ array_string และ Integer Array ของ ID ภาพใน Drawable ที่จะแสดงเก็บไว้ในตัวแปร Integer Array ที่ชื่อ array_res จากนั้นก็นำไปกำหนดใน Custom List View ได้เลย ดังนั้นโค๊ดในหน้าหลักของเจ้าของบล็อกจะมีดังนี้


Main.java
package app.akexorcist.customlistviewperformance;

import android.os.Bundle;
import android.widget.ListView;
import android.app.Activity;
import android.content.res.TypedArray;

public class Main extends Activity {
    
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        int[] array_res = getImageArray(R.array.my_image_array, R.drawable.ic_launcher);
        String[] array_string = getStringArray(R.array.my_string_array);
        
        ListView listView = (ListView)findViewById(R.id.listView1);
        listView.setAdapter(new CustomListViewAdapter(getApplicationContext()
                , android.R.id.text1, array_string, array_res));
    }
    
    public int[] getImageArray(int resId, int defResId) {
        TypedArray my_image_array = getResources().obtainTypedArray(resId);
        int[] array_res = new int[my_image_array.length()];
        for(int i = 0 ; i < array_res.length ; i++) 
            array_res[i] = my_image_array.getResourceId(i, defResId);
        my_image_array.recycle();
        return array_res;
    }
    
    public String[] getStringArray(int resId) {
        TypedArray my_string_array = getResources().obtainTypedArray(resId);
        String[] array_string = new String[my_string_array.length()];
        for(int i = 0 ; i < array_string.length ; i++) 
            array_string[i] = my_string_array.getString(i);
        my_string_array.recycle();
        return array_string;
    }
}

        อธิบายคร่าวๆละกัน เจ้าของบล็อกทำฟังก์ชันไว้สองอัน คือ getImageArray เพื่อดึงข้อมูลภาพใน Values Resource และ getStringArray เพื่อดึงข้อมูล String ใน Values Resource จากนั้นก็นำค่ามากำหนดใน Custom List View

        ทีนี้สิ่งที่เปลี่ยนไปนั้นไม่ใช่โค๊ดหลักอยู่แล้ว เพราะโค๊ดหลักควรจะเน้นเรียกใช้งานง่าย สั้นๆ ดังนั้นการทำงานจริงๆจะอยู่เบื้องหลังเกือบทั้งหมด


        สำหรับการดึงภาพมาใช้จะมีการเช็คขนาดภาพก่อนว่าใหญ่แค่ไห ถ้าใหญ่เกินที่กำหนดก็จะลดคุณภาพภาพที่จะแสดงลง เพื่อป้องกันการล้นของบัฟเฟอร์ โดยจะใช้ AsyncTask ซึ่งเป็น Background Tread ดังนั้น Thread หลักจึงไม่ค้างเวลาโหลดรูป (เพราะมันจะไปค้างที่ Thread ของ AsyncTask แทนนั่นเอง) เมื่อ AsyncTask โหลดภาพเสร็จจึงนำไปแสดงใน Image View


DecodeTask.java
package app.akexorcist.customlistviewperformance;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView;

public class DecodeTask extends AsyncTask<String, Void, Bitmap> {
    Context mContext;
    ImageView v;
    int resId;
    
    public DecodeTask(Context context, ImageView iv, int res_id) {
        mContext = context;
        v = iv;
        resId = res_id;
    }

    protected Bitmap doInBackground(String... params) {
        return decodeBitmapFromResource(resId, 300, 300);
    }

    protected void onPostExecute(Bitmap result) {
        v.setImageBitmap(result);
    }
    
    private Bitmap decodeBitmapFromResource(int resId, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(mContext.getResources(), resId, options);
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        options.inJustDecodeBounds = false;
        Bitmap bmp = BitmapFactory.decodeResource(mContext.getResources(), resId, options);
        return bmp;
    }

    private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if(height > reqHeight || width > reqWidth) {
            if(width > height) 
                inSampleSize = Math.round((float) height / (float) reqHeight);
            else 
                inSampleSize = Math.round((float) width / (float) reqWidth);
        }
        return inSampleSize;
     }
}    

        โดยฟังก์ชันหลักๆใน DecodeTask จะมีดังนี้

        doInBackground เป็นเมธอดของ AsyncTask ที่มีไว้ทำคำสั่งเบื้องหลัง เจ้าของบล็อกก็จะให้ทำการลดคุณภาพภาพให้เหมาะสมกับ Image View

        onPostExecute เป็นเมธอดของ AsyncTask ที่ทำงานต่อจาก doInBackground ก็คือเมื่อ doInBackground ทำงานเสร็จแล้ว ก็จะทำงานที่เมธอดนี้ต่อเลย โดยจะให้นำภาพที่ปรับใน doInBackground แล้ว มาแสดงใน Image View

        decodeBitmapFromResource เป็นฟังก์ชันเพื่อปรับภาพให้ไม่ใหญ่เกินจำเป็น

        calculateInSampleSize เป็นฟังก์ชันคำนวณเพื่อปรับภาพให้เหมาะสม

        ซึ่งในการโหลดภาพเพื่อแสดงบน Custom List View แต่ละครั้ง ก็จะเรียกใช้ DecodeTask เพื่อให้โหลดภาพบน Background Thread เมื่อได้ภาพมาแล้วก็แสดงบน Image View ในแถวนั้นๆ


CustomListViewAdapter.java
package app.akexorcist.customlistviewperformance;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

public class CustomListViewAdapter extends ArrayAdapter<String> {
    String[] STR; 
    int[] RESOURCE_ID;
    LayoutInflater INFLATER;
    
    public CustomListViewAdapter(Context context, int tvResId, String[] objects, int[] resId) {
        super(context, tvResId, objects);
        STR = objects;
        RESOURCE_ID = resId;
        INFLATER = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }
 
    public View getView(int position, View convertView, ViewGroup parent) {
        ImageView imageView = null;
        if(convertView == null) {
            convertView = INFLATER.inflate(R.layout.listview_row, parent, false);
            imageView = (ImageView)convertView.findViewById(R.id.imageView1);
        } else {
            imageView = (ImageView)convertView.findViewById(R.id.imageView1);
            DecodeTask dt1 = (DecodeTask)imageView.getTag(R.id.imageView1);
            if(dt1 != null) 
                dt1.cancel(true);
        }        
        
        imageView.setImageBitmap(null);
        DecodeTask dt2 = new DecodeTask(getContext(), imageView, RESOURCE_ID[position]);
        dt2.execute();
        imageView.setTag(R.id.imageView1, dt2);
        
        TextView textView = (TextView)convertView.findViewById(R.id.textView1);
        textView.setText(STR[position]);
        
        return convertView;
    }
}

        คำสั่งของ CustomListViewAdapter จะเหมือนกับบทความเก่าเกือบหมด ต่างกันตรงที่เมธอด getView ที่จะเปลี่ยนแปลงจากเดิมไป โดยจะให้โหลดภาพโดยใช้ DecodeTask นั่นเอง เป็นอันเสร็จ


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

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

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="app.akexorcist.customlistviewperformance.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>


        พอลองทดสอบดูก็จะเห็นว่าเวลาแสดงแถวใดๆ แถวนั้นๆก็จะทำการโหลดภาพด้วย AsyncTask เมื่อเสร็จแล้วจึงแสดงให้เห็นนั่นเอง



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

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


        สำหรับผู้ที่หลงเข้ามาอ่านคนใดต้องการไฟล์ตัวอย่างสามารถดาวน์โหลดได้ที่

                Custom List View Image Performance [Google Drive]

                Custom List View Image Performance [GitHub]

                Custom List View Image Performance [Sleeping For Less]




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

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