สำหรับบทความนี้ก็ขอพูดถึงเรื่องการทำ Splash Screen กันหน่อย เผื่อผู้ที่หลงเข้ามาอ่านคนไหนยังไม่รู้ งั้นก็เกริ่นหน่อยละกัน Splash Screen ก็คือหน้า Loading ของแอปพลิเคชันนั่นแหละ ส่วนมากจะแสดงโลโก้ค่ายหรือบริษัท หรือโลโก้แอปพลิเคชันนั้นๆ ซึ่งจะพบได้ทั่วไปอยู่แล้วในแอปพลิเคชันทั่วไปโดยเฉพาะเกม
หลักการของ Splash Screen นั้นก็ไม่ยากอะไรมากมาย ใช้วิธีสร้าง Thread แยกมาตัวหนึ่ง ให้หน่วงเวลาพักนึง แล้วก็ให้ใช้คำสั่ง Intent เพื่อไปยังหน้าหลักนั่นเอง
สำหรับผู้ที่หลงเข้ามาอ่านมือใหม่ที่ยังไม่รู้เรื่องหน่วงเวลา ว่าทำไมต้องสร้าง Thread แยกด้วย ไม่ใช่ Thread หลักล่ะ? อยากจะให้จำจุดนี้ให้ขึ้นใจว่า Thread หลักบนแอนดรอยด์ จะไม่ยอมให้มีโหลดใดๆก็ตามที่ทำให้ Thread นี้ต้องหยุด เพราะว่า Thread หลักจะเป็นส่วนที่ผู้ใช้ Interactive กับมัน ดังนั้นถ้ามีการหน่วงเวลาจะกลายเป็นว่า Thread หยุด ทำให้แอปพลิเคชันเกิดการค้างไปชั่วขณะหนึ่งเลย ซึ่งภาษาขั้นสูงอย่าง C# หรือ Java ปกติ ก็เป็นแบบนี้เช่นกัน (จริงๆเรื่องมันลึกกว่านี้เยอะ แต่อธิบายคร่าวๆแค่นี้พอ)
ดังนั้นจึงใช้วิธีสร้าง Thread แยกขึ้นมาหนึ่งตัวแล้วให้ทำการหน่วงเวลาใน Thread นั้นแทน ก็จะทำให้ Thread หลักยังคงทำงานต่อไปได้ เพราะ Thread แยกจะหยุดทำงานเพื่อหน่วงเวลาแทน ดังนั้นคำสั่ง Intent ก็จะต้องอยู่ใน Thread ย่อยนะ เดี๋ยวมีผู้ที่หลงเข้ามาอ่านเข้าใจผิดใส่ที่ Thread หลัก
เพิ่มเติม - บางคำสั่งไม่สามารถใช้งานใน Thread แยกได้ โดยเฉพาะคำสั่งที่เกี่ยวกับคลาสที่เป็น View ส่วนใหญ่ ไม่ว่าจะ Button, Text View หรือ Image View เป็นต้น ที่บังคับว่าต้องเรียกใช้งานคำสั่งเหล่านั้นใน Thread หลัก โดยสังเกตุได้จาก LogCat ที่จะเกิด Exception ดังนี้
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
ตัดจบเรื่อง Thread ล่ะ เดี๋ยวจะยาวไปกว่านี้ เข้าเรื่องดีกว่า
จากหลักการดังกล่าว ก็มีตัวอย่างง่ายๆให้นำไปใช้งานกันและก็พบว่าโค๊ด Splash Screen นั้นๆ มีข้อผิดพลาดอยู่บ้าง โดยข้อผิดพลาดที่ว่าเจ้าของบล็อกขอยกตัวอย่างจากโค๊ด
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.splashscreen);
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) { }
Intent intent = new Intent(Splash.this, Main.class);
startActivity(intent);
}
}).start();
}
สำหรับโค๊ดนี้ดูแล้วคงเข้าใจไม่ยาก คือสร้าง Thread แยก โดยให้ Thread แยกทำการหน่วงเวลา 3 วินาทีแล้ว Intent
ปัญหาอย่างแรกเลยก็คือการ Intent ไป Main โดยไม่ปิด Activity
จะเห็นว่าหลังจากที่ขึ้นหน้า Splash Screen เสร็จแล้วก็ไปหน้า Main แต่ว่าพอเจ้าของบล็อกกดปุ่ม Back เพื่อปิดแอปพลิเคชันลง ก็พบว่าเมื่อกดปุ่ม Back แล้วจะกลับไปที่หน้า Splash Screen แทน เพราะไม่ได้ปิด Activity ทิ้งเมื่อตอนที่ Intent ไปหน้า Main ในตอนแรก
ดังนั้นเพื่อให้สมบูรณ์ หลังจาก Intent แล้วให้ใช้คำสั่ง finish() เพื่อให้ปิด Activity ของ Splash Screen ทิ้งด้วยนั่นเอง
private Handler handler;
private Runnable runnable;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.splashscreen);
handler = new Handler();
runnable = new Runnable() {
public void run() {
Intent intent = new Intent(Splash.this, Main.class);
startActivity(intent);
finish();
}
};
}
public void onResume() {
super.onResume();
handler.postDelayed(runnable, 3000);
}
public void onStop() {
super.onStop();
handler.removeCallbacks(runnable);
}
ทีนี้ปัญหาต่อมาก็คือเวลานักพัฒนาทดสอบแอปพลิเคชันเกือบทั้งหมดทดสอบด้วยการทำงานตามที่ตัวเองคิดไว้ คือเขียนโปรแกรมให้มันทำอะไรก็ทดสอบกดตามนั้น แต่ไม่ได้คำนึงว่าถ้าผู้ใช้กดแล้วจะกดเหมือนกันเสมอไป? ซึ่งจุดนี้นักพัฒนาหลายๆคนได้มองข้ามไปอย่างไม่รู้ตัว
จะเห็นว่าระหว่างที่แสดงหน้า Splash Screen อยู่ ถ้ากด Back เพื่อปิดแอปพลิเคชัน ดูเหมือนว่ามันจะปิดจริงๆ แต่จริงๆกลับไม่ใช่อย่างนั้น เพราะว่า Thread แยกที่สร้างไว้ตอนแรก ยังคงทำงานอยู่ ทำให้เมื่อกลับมาที่หน้า Homescreen ซักพักก็เด้งไป Main
สำหรับกรณีนี้ส่วนมากมักจะเกิดขึ้นในเวลาที่เปิดผิดแอปพลิเคชัน คือจิ้มผิดแอปพลิเคชันแล้วจะรีบกดปิดทันที แต่ว่ามันกลับไม่ปิด เพราะเมื่อผ่านไป 3 วินาทีตามที่กำหนดไว้ก็จะเด้งหน้า Main ขึ้นมา ซึ่งนักพัฒนาไม่สามารถห้ามได้หรอก ว่าผู้ใช้ห้ามกดตอนนั้น ที่ต้องทำก็คือปิดจุดบกพร่องที่มีอยู่ให้น้อยที่สุดเท่าที่ทำได้ซะ
Handler handler;
Runnable runnable;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.splashscreen);
handler = new Handler();
runnable = new Runnable() {
public void run() {
Intent intent = new Intent(Splash.this, Main.class);
startActivity(intent);
finish();
}
};
}
public void onResume() {
super.onResume();
handler.postDelayed(runnable, 3000);
}
public void onStop() {
super.onStop();
handler.removeCallbacks(runnable);
}
โค๊ดข้างต้นนี้เจ้าของบล็อกจะเปลี่ยนจากการใช้ Thread มาใช้เป็น Handler แทน ซึ่งคลาสตัวนี้จะทำงานต่างกับ Thread อยู่เล็กน้อย แต่ว่าทำงานในหลักษณะ Thread เหมือนกัน แต่ข้อดีคือกำหนดเวลาที่จะเริ่มทำงานได้
จากเดิมจะใช้ Thread.sleep(3000) แต่พอมาใช้เป็น Handler จะใช้คำสั่ง postDelayed แทน ซึ่งสามารถกำหนดให้ทำคำสั่งเมื่อผ่านไป 3000 ms แทน แต่ Handler จะสามารถยกเลิกได้ด้วยการใช้คำสั่ง removeCallbacks ซึ่งต่างจาก Thread ที่ใช้คำสั่งยกเลิก Thread ที่ทำงานอยู่ไม่ได้ (หรือแบบมีข้อจำกัด)
โดยเจ้าของบล็อกจะใส่คำสั่งไว้ใน onResume กับ onStop โดยให้ Handler เริ่มทำงานในอีก 3 วินาทีที่ onResume และเมื่อผู้ใช้กดปุ่ม Back หรือว่าปุ่ม Home ผลก็คือ onStop ทำงานทั้งคู่ ดังนั้นเจ้าของบล็อกจึงให้คำสั่งยกเลิก Handler ที่รอครบ 3 วินาทีเลิกทำงานทันที
ผลก็คือ เมื่อเปิดแอปพลิเคชันขึ้นมา ก็จะเข้าสู่หน้า Splash Screen เหมือนปกติ และเมื่อผ่านไป 3 วินาทีก็จะ Intent ไปที่หน้า Main ทันที แต่ถ้าขณะที่อยู่หน้า Splash Screen แล้วกด Back หน้า Splash Screen ก็จะเข้าสู่ onStop ทันที ผลก็คือ Handler ถูกสั่งให้หยุดทำงาน
แต่กรณีที่กดปุ่ม Home จะคล้ายๆกับ Back แต่ก็ไม่เหมือนกันทั้งหมด ในการกดปุ่ม Home จะทำให้ Activity นั้นๆเกิด onStop เช่นกัน แต่เวลาที่กดเปิดแอปพลิเคชันใหม่อีกครั้ง onCreate จะไม่ถูกทำงานใหม่ เพราะการกดปุ่ม Home คือการย่อแอปพลิเคชัน ดังนั้นเมื่อเปิดแอปพลิเคชั้นขึ้นมาใหม่ จะทำงานที่ onResume ทันที ในขณะที่การกด Back แล้วกลับเข้ามาในแอปพลิเคชัน จะเป็นลักษณะของการปิดแล้วเปิดแอปพลิเคชันขึ้นมาใหม่ ดังนั้น onCreate จะทำงาน แล้วตามด้วย onResume
จะเห็นว่าเมื่อออกจากแอปพลิเคชันด้วยการกด Back หรือ Home สิ่งที่ทำงานเหมือนกันคือ onStop และเมื่อเปิดแอปพลิเคชันขึ้นมาใหม่อีกครั้ง สิ่งที่เหมือนกันจะมีแค่ onResume เท่านั้น จึงเป็นที่มาว่าทำไมเจ้าของบล็อกจึงใส่คำสั่งให้ Handler เริ่มทำงาน ไว้ใน onResume
จากโค๊ดดังกล่าวจะเห็นว่าสามารถทำ Splash Screen ได้เกือบสมบูรณ์แล้ว
อ่ะ เกือบสมบูรณ์ ? ทำไมล่ะ ? จากคลิปข้างบนนี้น่าจะมีผู้ที่หลงเข้ามาอ่านหลายคนที่ไม่ทันสังเกตุเห็นว่าทำไมมันถึงไม่สมบูรณ์ ให้ดูช่วงท้ายวีดีโอที่กดปุ่ม Home ก่อนที่จะ Intent จากเดิมที่เจ้าของบล็อกบอกไว้ว่า Home คือย่อแอปพลิเคชัน ดังนั้นเมื่อเปิดแอปพลิเคชันขึ้นมาใหม่ก็ไม่ควรจะรอ 3 วินาทีสิสมมติว่าผู้ใช้เปิดขึ้นมาเจอหน้า Splash Screen พอผ่านไป 1 วินาที ผู้ใช้กดปุ่ม Home เพื่อย่อแอปพลิเคชัน ถ้าเปิดขึ้นมาใหม่อีกครั้ง ก็ควรจะรอแค่ 2 วินาที แต่ในวีดีโอ ต่อให้ย่อแอปพลิเคชันแล้วเปิดใหม่ก็รอ 3 วินาทีอยู่ดี
นี่ล่ะคือจุดที่เจ้าของบล็อกบอกว่าทำไมมันไม่สมบูรณ์
จะไม่ซีเรียสเรื่องนี้ก็ได้นะ เพราะแค่นี้ก็ถือว่าโอเคแล้ว ปัญหานี้ผู้ที่หลงเข้ามาอ่านหลายๆคนอาจจะมองว่าไม่จำเป็นด้วยซ้ำ ไม่จำเป็นต้องซีเรียสถึงขนาดนั้นเลยนี่ แต่เพื่อเขียนเอามันส์จึงขอเขียนเพิ่มเติมเข้าไปอีก
เพื่อแก้ปัญหาดังกล่าว เจ้าของบล็อกจึงเพิ่มคำสั่งเข้าไป โดยใช้วิธีเก็บค่าเวลาด้วยว่าตอนที่เกิด onStop ตอนนั้นหน้า Splash Screen ได้ถูกเปิดขึ้นมาแล้วเป็นเวลากี่วินาที เมื่อกลับมาที่แอปพลิเคชันใหม่อีกครั้งก็จะให้นำค่าที่เก็บไว้ไปลบกับ 3 วินาที ผลก็คือเวลาที่ต้องรอจะเหลือน้อยกว่า 3 วินาที ขึ้นอยู่กับว่าตอนแรกเปิดหน้านี้นานเท่าไร
เพิ่มเติม - สำหรับค่าเวลาที่เก็บไว้จะมีผลกับตอนที่กดปุ่ม Home เท่านั้นแหละ เพราะเมื่อกด Back จะเป็นการปิดแอปพลิเคชัน ทำให้ค่าเวลาที่เก็บไว้ถูกเคลียร์ทิ้งโดยปริยาย แต่สำหรับการกดปุ่ม Home จะทำให้ค่าเวลายังคงเก็บค่าไว้อยู่
งั้นมาดูโค๊ดตัวอย่างทั้งหมดที่ใช้ในบทความนี้เลยดีกว่า
เพิ่มเติม - ในตัวอย่างนี้มีเรื่อง Multiple Support Screen เล็กๆน้อยๆที่โฟลเดอร์ drawable สามารถดูการใช้งานเป็นตัวอย่างได้
splashscreen.xml
Splash.java
main.xml
Main.java
AndroidManifest.xml
สำหรับผู้ที่หลงเข้ามาอ่านที่ต้องการไฟล์ตัวอย่างสามารถดาวนโหลดที่ Splash Screen [Google Drive] หรือ Splash Screen [GitHub]
ในชื่อบทความนี้ที่เจ้าของบล็อกต่อท้ายว่า (แต่ไม่ใช่ที่สุด) ก็เพราะโค๊ดในบทความนี้เป็นโค๊ดที่ถูก แต่ไม่ได้ถูกที่สุด เนื่องจากโค๊ดที่ทำงานได้ถูกต้อง ไม่ได้มีแค่โค๊ดเดียว อยู่ที่ผู้เขียนโปรแกรมจะคิดอัลกอริทึมมาใช้งานเท่านั้น ดังนั้นโค๊ดที่เจ้าของบล็อกใช้ในตัวอย่างนี้จึงไม่ได้ดีที่สุด เพราะอาจจะมีวิธีเขียนแบบอื่นที่ดีกว่านี้ก็เป็นได้นั่นเอง
เพื่อแก้ปัญหาดังกล่าว เจ้าของบล็อกจึงเพิ่มคำสั่งเข้าไป โดยใช้วิธีเก็บค่าเวลาด้วยว่าตอนที่เกิด onStop ตอนนั้นหน้า Splash Screen ได้ถูกเปิดขึ้นมาแล้วเป็นเวลากี่วินาที เมื่อกลับมาที่แอปพลิเคชันใหม่อีกครั้งก็จะให้นำค่าที่เก็บไว้ไปลบกับ 3 วินาที ผลก็คือเวลาที่ต้องรอจะเหลือน้อยกว่า 3 วินาที ขึ้นอยู่กับว่าตอนแรกเปิดหน้านี้นานเท่าไร
เพิ่มเติม - สำหรับค่าเวลาที่เก็บไว้จะมีผลกับตอนที่กดปุ่ม Home เท่านั้นแหละ เพราะเมื่อกด Back จะเป็นการปิดแอปพลิเคชัน ทำให้ค่าเวลาที่เก็บไว้ถูกเคลียร์ทิ้งโดยปริยาย แต่สำหรับการกดปุ่ม Home จะทำให้ค่าเวลายังคงเก็บค่าไว้อยู่
private Handler handler;
private Runnable runnable;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.splashscreen);
handler = new Handler();
runnable = new Runnable() {
public void run() {
Intent intent = new Intent(Splash.this, Main.class);
startActivity(intent);
finish();
}
};
}
public void onResume() {
super.onResume();
handler.postDelayed(runnable, 3000);
}
public void onStop() {
super.onStop();
handler.removeCallbacks(runnable);
}
งั้นมาดูโค๊ดตัวอย่างทั้งหมดที่ใช้ในบทความนี้เลยดีกว่า
เพิ่มเติม - ในตัวอย่างนี้มีเรื่อง Multiple Support Screen เล็กๆน้อยๆที่โฟลเดอร์ drawable สามารถดูการใช้งานเป็นตัวอย่างได้
splashscreen.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="#3b92d1" >
<ImageView
android:id="@+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:src="@drawable/image" />
</RelativeLayout>
Splash.java
package app.akexorcist.splashscreen;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.view.Window;
public class Splash extends Activity {
Handler handler;
Runnable runnable;
long delay_time;
long time = 3000L;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.splashscreen);
handler = new Handler();
runnable = new Runnable() {
public void run() {
Intent intent = new Intent(Splash.this, Main.class);
startActivity(intent);
finish();
}
};
}
public void onResume() {
super.onResume();
delay_time = time;
handler.postDelayed(runnable, delay_time);
time = System.currentTimeMillis();
}
public void onPause() {
super.onPause();
handler.removeCallbacks(runnable);
time = delay_time - (System.currentTimeMillis() - time);
}
}
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="#3b92d1" >
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="Main Page"
android:textColor="#FFFFFF"
android:textSize="40sp" />
</RelativeLayout>
Main.java
package app.akexorcist.splashscreen;
import android.os.Bundle;
import android.view.Window;
import android.app.Activity;
public class Main extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.main);
}
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.akexorcist.splashscreen"
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:theme="@style/AppTheme" >
<activity
android:name=".Splash"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".Main" />
</application>
</manifest>
สำหรับผู้ที่หลงเข้ามาอ่านที่ต้องการไฟล์ตัวอย่างสามารถดาวนโหลดที่ Splash Screen [Google Drive] หรือ Splash Screen [GitHub]
ในชื่อบทความนี้ที่เจ้าของบล็อกต่อท้ายว่า (แต่ไม่ใช่ที่สุด) ก็เพราะโค๊ดในบทความนี้เป็นโค๊ดที่ถูก แต่ไม่ได้ถูกที่สุด เนื่องจากโค๊ดที่ทำงานได้ถูกต้อง ไม่ได้มีแค่โค๊ดเดียว อยู่ที่ผู้เขียนโปรแกรมจะคิดอัลกอริทึมมาใช้งานเท่านั้น ดังนั้นโค๊ดที่เจ้าของบล็อกใช้ในตัวอย่างนี้จึงไม่ได้ดีที่สุด เพราะอาจจะมีวิธีเขียนแบบอื่นที่ดีกว่านี้ก็เป็นได้นั่นเอง