30 เมษายน 2556

[Android Code] การใช้ AsyncTask ในการกำหนดค่าเริ่มต้นให้กับ Layout


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

และบทความนี้ก็เหมาะกับคนที่จะทำหน้าโหลดของแอปฯด้วย
แต่คนละอันกับ Splash Screen นะ อันนั้นใช้ระยะเวลาที่ตายตัว
แต่ในบทความนี้จะรอจนกว่าเตรียมหน้า Layout จนเสร็จ


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

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

ดังนั้นบทความนี้จะใช้วิธีโหลดข้อมูลหนักๆไปแหละ
แต่ว่าจะแสดง Dialog ขึ้นมาโดยมี Progress แสดง
ว่าตอนนี้กำลังโหลดหน้า Layout ไปถึงเท่าไรแล้ว
เพื่อให้ผู้ใช้รู้ว่าแอปฯไม่ได้ค้าง แต่กำลังโหลดอยู่


ซึ่งการจะทำแบบนี้ไม่แนะนำให้แสดง Dialog ตรงๆนะ
แต่ใช้คลาส AsycnTask แทน ซึ่งคลาส AsyncTask
เป็นคลาสที่ยอมให้ทำงานเบื้องหลังบน Thread หลักได้
โดยทำให้ไม่ต้องใช้คลาสอย่าง Thread หรือ Handler
และ AsyncTask ใช้คำสั่งเฉพาะบน Thread หลักได้
ไม่เหมือนกับ Thread หรือ Handler ที่ทำโดยตรงไม่ได้

สำหรับภาพที่นำมาแสดงจะเอามาแสดงเลยก็ไม่ได้
เพราะว่ามีขนาดใหญ่เกิน ถ้าเอามาแสดงทันที
ก็จะทำให้เกิดการ Out of memory หรือบัฟเฟอร์ไม่พอ
ดังนั้นภาพจะถูกนำมาลดขนาดให้เล็กลงแล้ว Crop
โดยภาพที่จะเอามาแสดงจริงๆก็เหลือ 500 x 500 px
(ซึ่งขั้นตอนนี้ไม่ขออธิบายละเอียด ไม่ใช่ประเด็นนัก)
และโค๊ดในส่วนนี้ก็ทำให้คำสั่งทำงานนานพอสมควร

คำสั่งในการทำภาพให้เหลือขนาด 500 x 500 px
ก็จะใช้คลาส Bitmap แล้วเอามากำหนดใน GridView
ดังนั้นการทำงานก็จะแบ่งออกเป็นสองส่วนด้วยกัน
คือคำสั่งส่วน Bitmap กับส่วนที่กำหนด GridView

ทีนี้ก็มาดูที่การทำงานของ AsyncTask กันก่อน
AsyncTask จะประกอบไปด้วยฟังก์ชันข้างในดังนี้
private class nameTask extends AsyncTask<Void, Integer, Void> { protected void onPreExecute() { } protected Void doInBackground(Void... params) { } protected void onProgressUpdate(Integer... values) { } protected void onPostExecute(Void result) { } }

onPreExecute คือฟังก์ชันที่จะทำงานก่อนทำงานใน doInBackground

doInBackground คือฟังก์ชันที่จะให้ทำงานที่เบื้องหลังของ Thread หลัก
ทำงานที่เบื้องหลัง จึงไม่สามารถใช้คำสั่งเฉพาะบน Thread หลักได้ในนี้
โดยส่วนมากจะใช้สำหรับเตรียมข้อมูลที่โหลดหนักๆให้พร้อมก่อน
แล้วไปใช้คำสั่งเฉพาะบน Thread หลัก ที่ฟังก์ชัน onPostExecute แทน

onProgressUpdate คือฟังก์ชันที่ใช้อัพเดท Progress หรือสถานะการทำงาน

onPostExecute คือฟังก์ชันที่ทำงานเมื่อฟังก์ชัน doInBackground ทำงานเสร็จ

อาจจะฟังดูงง เดี๋ยวขออธิบายการทำงานของบทความนี้เลยดีกว่า
สำหรับ onPreExecute เจ้าของบล็อกได้สร้าง Progress Dialog ที่นี่
เพื่อใช้แสดงเป็น Dialog ให้ผู้ใช้เห็นว่ากำลังโหลดข้อมูลอยู่นั่นเอง

ส่วน doInBackground ก็ให้ทำการดึงไฟล์ภาพมาแปลงเป็น Bitmap
แล้วย่อขนาดภาพให้เหลือแค่ 500 x 500 px ยังไม่ใส่ใน GridView
แล้วก็อัพเดทค่าให้ Progress Dialog ในฟังก์ชัน onProgressUpdate

ส่วน onProgressUpdate เอาไว้อัพเดทค่าบน Progress Dialog
เพื่อให้ผู้ใช้ได้รู้ว่าตอนนั้นกำลังโหลดข้อมูลไปได้เท่าไรแล้ว

และ onPostExecute เนื่องจากฟังก์ชันนี้เป็นฟังก์ชันที่ทำงาน
เมื่อ doInBackground ทำงานเสร็จแล้ว จึงปิด Progress Dialog
แล้วกำหนด Layout ให้กับ Activity และสำหรับคลาส Bitmap
ที่สร้างจาก doInBackGround  ก็เอามากำหนด GridView ที่นี่
private class LoadViewTask extends AsyncTask<Void, Integer, Void> { ประกาศคลาส ProgressDialog ที่นี่ ประกาศคลาส ArrayList สำหรับเก็บคลาส Bitmap protected void onPreExecute() { กำหนดค่าให้กับ ProgressDialog } protected Void doInBackground(Void... params) { วนลูปตามจำนวนภาพที่มี • ดึงภาพจาก drawable มาเป็นคลาส Bitmap • แล้วย่อขนาดให้เหลือ 500 x 500 px • และเก็บ Bitmap ที่ได้ไว้ใน ArrayList • อัพเดทค่าใน Progress } protected void onProgressUpdate(Integer... values) { กำหนดให้ Progress ใน ProgressDialog แสดงค่าสถานะตามตัวแปร values } protected void onPostExecute(Void result) { ยกเลิก ProgressDialog กำหนด Layout ให้กับ Activity กำหนดภาพที่เก็บไว้ใน ArrayList ให้กับ GridView } }

ซึ่งในตัวอย่างจริงๆ จะไม่ตรงเป๊ะๆกับที่อธิบายข้างบนนี้มากนัก
เพราะต้องมีคลาสสำหรับดึงภาพมาแปลงเป็น Bitmap อีก

แล้วก็ให้ผู้ที่หลงเข้ามาอ่านเอาวิธีแบบนี้ไปประยุกต์เองนะ
เพราะ AsyncTask ไม่ได้ใช้กับการดึงไฟล์ภาพใหญ่ๆเท่านั้น 
อะไรก็ได้ที่เป็นการโหลดข้อมูลหนักๆ ก็ใช้ได้เหมือนกัน

ทีนี้ก็มาดูตัวอย่างของบทความนี้กันเลยดีกว่า

main.xml
<GridView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/gridView" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#000000" android:drawSelectorOnTop="true" android:horizontalSpacing="0dp" android:listSelector="@drawable/list_selector" android:numColumns="3" android:scrollbars="none" android:verticalSpacing="0dp" > </GridView>

สำหรับ Layout ก็ไม่มีอะไร แค่ GridView ธรรมดาๆ


Main.java
package app.akexorcist.loadingtask; import java.util.ArrayList; import android.os.AsyncTask; import android.os.Bundle; import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.util.DisplayMetrics; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.widget.BaseAdapter; import android.widget.GridView; import android.widget.ImageView; public class Main extends Activity { ArrayList<Bitmap> arr_bm = new ArrayList<Bitmap>(); int[] resId = { R.drawable.image01, R.drawable.image02, R.drawable.image03, R.drawable.image04, R.drawable.image05, R.drawable.image06, R.drawable.image07, R.drawable.image08, R.drawable.image09, R.drawable.image10, R.drawable.image11, R.drawable.image12, R.drawable.image13, R.drawable.image14, R.drawable.image15, R.drawable.image16, R.drawable.image17, R.drawable.image18, R.drawable.image19, R.drawable.image20, R.drawable.image21, R.drawable.image22, R.drawable.image23, R.drawable.image24, R.drawable.image25, R.drawable.image26, R.drawable.image27 }; int width; /* protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.main); DisplayMetrics displayMetrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); width = displayMetrics.widthPixels; for(int i = 0 ; i < resId.length ; i++) { Bitmap bm = decodeSampledBitmapFromResource(getResources() , resId[i], 500, 500); if(bm.getWidth() > bm.getHeight()) { bm = Bitmap.createScaledBitmap(bm , 500 * bm.getWidth() / bm.getHeight(), 500, false); } else if(bm.getWidth() < bm.getHeight()) { bm = Bitmap.createScaledBitmap(bm , 500, 500 * bm.getHeight() / bm.getWidth(), false); } else { bm = Bitmap.createScaledBitmap(bm, 500, 500, false); } arr_bm.add(bm); System.gc(); } GridView gridview = (GridView) findViewById(R.id.gridView); gridview.setAdapter(new ImageAdapter(Main.this)); } */ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); DisplayMetrics displayMetrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); width = displayMetrics.widthPixels; new LoadViewTask().execute(); } private class LoadViewTask extends AsyncTask<Void, Integer, Void> { ProgressDialog pd; protected void onPreExecute() { pd = new ProgressDialog(Main.this); pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); pd.setTitle("Loading..."); pd.setMessage("Loading images..."); pd.setCancelable(false); pd.setIndeterminate(false); pd.setMax(100); pd.setProgress(0); pd.show(); } protected Void doInBackground(Void... params) { try { synchronized (this) { for(int i = 0 ; i < resId.length ; i++) { Bitmap bm = decodeSampledBitmapFromResource(getResources() , resId[i], 500, 500); if(bm.getWidth() > bm.getHeight()) { bm = Bitmap.createScaledBitmap(bm , 500 * bm.getWidth() / bm.getHeight() , 500, false); } else if(bm.getWidth() < bm.getHeight()) { bm = Bitmap.createScaledBitmap(bm, 500 , 500 * bm.getHeight() / bm.getWidth(), false); } else { bm = Bitmap.createScaledBitmap(bm, 500, 500, false); } arr_bm.add(bm); int c = (int)((100f / resId.length) * (i + 1)); publishProgress(c); } } } catch (Exception e) { e.printStackTrace(); } return null; } protected void onProgressUpdate(Integer... values) { pd.setProgress(values[0]); } protected void onPostExecute(Void result) { pd.dismiss(); setContentView(R.layout.main); GridView gridview = (GridView) findViewById(R.id.gridView); gridview.setAdapter(new ImageAdapter(Main.this)); } } public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); } public 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) { final int heightRatio = Math.round((float)height / (float)reqHeight); final int widthRatio = Math.round((float)width / (float)reqWidth); inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; } return inSampleSize; } public class ImageAdapter extends BaseAdapter { private Context mContext; public ImageAdapter(Context c) { mContext = c; } public int getCount() { return resId.length; } public Object getItem(int position) { return null; } public long getItemId(int position) { return 0; } public View getView(int position, View convertView, ViewGroup parent) { ImageView imageView; if (convertView == null) { imageView = new ImageView(mContext); imageView.setLayoutParams(new GridView.LayoutParams(width / 3 , width / 3)); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); } else { imageView = (ImageView) convertView; } imageView.setImageBitmap(arr_bm.get(position)); return imageView; } } }

สำหรับคลาส ImageAdapter สำหรับแสดงภาพใน GridView
ส่วนฟังก์ชัน decodeSampledBitmapFromResource กับ
ฟังก์ชัน calculateInSampleSize ไม่ต้องสนใจอะไรมาก
เพราะใช้ในตัวอย่างนี้เท่านั้น ไม่ได้ทำเรื่องภาพก็ข้ามไป

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

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.akexorcist.loadingtask" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="8" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name="app.akexorcist.loadingtask.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 มาจัดการแทน
ถึงแม้ว่าการใช้ AsyncTask จะทำให้ทำงานช้ากว่าธรรมดา
แต่ในมุมมองของผู้ใช้ ผู้ใช้ก็คงไม่อยากรอโหลดจากหน้าขาว
ดังนั้นการมีหน้าต่างแสดงสถานะการโหลดก็ย่อมเหมาะสมกว่า

สำหรับผู้ที่หลงเข้ามาอ่านผู้ใดต้องการไฟล์ตัวอย่าง
สามารถดาวน์โหลดได้ที่ Loading Task [Google Drive]
ไฟล์ตัวอย่างใหญ่ไปหน่อย เพราะอัดภาพขนาดใหญ่ไว้เยอะ




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

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