มาทำความรู้จักกับ Object Animator กันดีกว่า~!
วันนี้ขอกลับเข้าสู่เรื่องการทำ Animation กันอีกครั้ง ซึ่งในอดีตกาลเมื่อนานมาแล้ว เจ้าของบล็อกได้ทำบทความเกี่ยวกับคลาส Animation ไปแล้ว แต่ทว่าคลาสดังกล่าวนั้นเป็นอะไรที่เก่าไปแล้วล่ะ เพราะว่าหลังจากที่ Android 3.0 Honeycomb (API 11) ได้เปิดตัว ก็มีการประกาศ Animation API ตัวใหม่ขึ้นมาที่มีชื่อว่า Object Animator นั่นเอง
ดังนั้นบทความนี้จะมาเกริ่นคร่าวๆกันก่อนว่า Object Animator มันต่างจาก Animation ของเดิมอย่างไร และทำไมจึงควรเปลีย่นมาใช้ Object Animator กันได้แล้ว
สำหรับปัญหาเดิมๆของคลาส Animation ที่เจ้าของบล็อกนั้นเจออยู่บ่อยๆนั้นก็คือ การที่จะต้องกำหนดค่า Animation ตอนเริ่มต้นและปลายทาง เช่น เจ้าของบล็อกอยากให้ภาพค่อยๆจางหายไปโดยใช้คลาส Animation เจ้าของบล็อกก็ต้องกำหนดว่าให้ค่าเริ่มต้นของ Alpha เป็น 100% แล้วปลายทางเป็น 0% เวลาที่ทำการแสดงผลก็จะค่อยๆจางลงเรื่อยๆจนหายไป
แต่ถ้าสั่งงานซ้ำแบบเดิมอีกครั้งล่ะ?
เมื่อสั่งงานแบบเดิมซ้ำอีกครั้งหลังจากที่ภาพดังกล่าวจางหายไปเรียบร้อยแล้ว ค่า Alpha ของภาพก็จะกลับมาเป็น 100% เหมือนเดิมอีกครั้ง แล้วค่อยๆจางหายไปอีกครั้ง ซึ่งทำให้ Animation รู้สึกไม่สมเหตุสมผล
วิธีแก้ปัญหาดังกล่าวก็คือ ต้องเก็บสถานะของภาพดังกล่าวซะก่อนว่าตอนนั้นกำลังแสดงหรือซ่อนอยู่ ถ้าแสดงอยู่ก็ค่อยให้จางหายไป แต่ถ้าซ่อนอยู่ก็ไม่ทำอะไร
ยกตัวอย่างเป็น Button 2 ตัวและ Image View อีกหนึ่งตัว เมื่อกดปุ่มแรกจะเป็นการสั่งงานให้ภาพจางหายไป และอีกปุ่มคือสั่งงานให้ภาพแสดงขึ้นมา โดยจะใช้คลาส AlphaAnimation ที่เป็นคลาสลูกของ Animation นั่นเอง
Button btnFadeOut, btnFadeIn;
ImageView imageView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
imageView = (ImageView)findViewById(R.id.imageView);
btnFadeOut = (Button)findViewById(R.id.btnFadeOut);
btnFadeOut.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
AlphaAnimation anim = new AlphaAnimation(1, 0);
anim.setDuration(1000);
anim.setFillEnabled(true);
anim.setFillBefore(true);
anim.setFillAfter(true);
imageView.startAnimation(anim);
}
});
btnFadeIn = (Button)findViewById(R.id.btnFadeIn);
btnFadeIn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
AlphaAnimation anim = new AlphaAnimation(0, 1);
anim.setDuration(1000);
anim.setFillEnabled(true);
anim.setFillBefore(true);
anim.setFillAfter(true);
imageView.startAnimation(anim);
}
});
}
ในเวลาที่ทดสอบกดปุ่ม btnFadeOut แล้วภาพจางหายไปและพอกด btnFadeIn ภาพก็จะแสดงขึ้นมาใหม่ก็จริง แต่ทว่าถ้าดันไปกดปุ่มเดิมซ้ำแทนที่จะกดอีกปุ่มสลับกันล่ะ?
จะเห็นว่าภาพถูกกำหนดเป็นค่าเริ่มต้นใหม่ก่อนที่จะเริ่ม Animation ไปยังค่าปลายทาง ซึ่งทำให้เกิดความไม่เหมาะสมของการแสดงผลเอาเสียเลย ดังนั้นก็ต้องมานั่งเขียนโค๊ดเพิ่มเพื่อเก็บสถานะของภาพว่าแสดงหรือซ่อนอยู่แล้วเช็คจากค่าดังกล่าวแทน
แต่ Object Animator ได้ตอบโจทย์ในเรื่องนี้แล้ว
คราวนี้เจ้าของบล็อกจะเขียนใหม่ โดยเปลี่ยนจากคลาส AlphaAnimation เป็น ObjectAnimator แทน แล้วดูว่าผลลัพธ์จะต่างจากของเก่าอย่างไร
Button btnFadeOut, btnFadeIn;
ImageView imageView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
imageView = (ImageView)findViewById(R.id.imageView);
btnFadeOut = (Button)findViewById(R.id.btnFadeOut);
btnFadeOut.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
ObjectAnimator anim = ObjectAnimator.ofFloat(imageView, View.ALPHA, 0f);
anim.setDuration(1000);
anim.start();
}
});
btnFadeIn = (Button)findViewById(R.id.btnFadeIn);
btnFadeIn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
ObjectAnimator anim = ObjectAnimator.ofFloat(imageView, View.ALPHA, 1f);
anim.setDuration(1000);
anim.start();
}
});
}
จะเห็นว่า ObjectAnimator ไม่ต้องกำหนดค่าเริ่มต้นแต่อย่างใด เพียงแค่กำหนดค่าของปลายทางก็ใช้ได้เลย งั้นเรามาดูผลลัพธ์กันเถอะว่าเป็นอย่างไร
จะเห็นว่าเมื่อภาพจางหายไปแล้ว เจ้าของบล็อกลองกดปุ่ม btnFadeOut ซ้ำๆดู ก็จะไม่มีผลอะไร เพราะภาพได้ถูกสั่งให้จางหายไปตั้งแต่แรกแล้ว
จากตัวอย่างนี้จึงสรุปได้ง่ายๆว่า Object Animator สามารถสั่งให้วัตถุทำ Animation ตามที่กำหนดไว้เลย โดยไม่สนว่าตอนนั้นจะมีค่าเป็นอะไร เช่น ภาพมี Alpha เป็น 100% อยู่ ถ้าสั่งให้ Alpha เป็น 0% ก็จะเปลี่ยนค่า Alpha ไปเรื่อยๆจนถึง 0% โดยทันที และถ้ามี Alpha เริ่มต้นเป็น 50% ก็จะเปลี่ยนค่าไปเรื่อยๆจนถึง 0% เช่นกัน ซึ่งข้อดีคือผู้ที่หลงเข้ามาอ่านไม่จำเป็นต้องรู้ค่าเริ่มต้น สามารถสั่งค่าที่ต้องการจะทำ Animation ได้เลย
อะไรนะ? ยังไม่ค่อยเข้าใจหรอ?
ยกตัวอย่างเพิ่มเติมอีกซักหน่อยละกัน แต่ว่าไม่มีโค๊ดตัวอย่างนะ อธิบายคร่าวๆให้เห็นภาพแทนละกัน (ขี้เกียจพิมพ์โค๊ดเยอะน่ะแหละ)
สมมติว่าเจ้าของบล็อกต้องการให้วัตถุใดๆเคลื่อนที่ไปตามตำแหน่งดังนี้
จากตัวอย่างจะเห็นว่า Animation แบ่งเป็น 4 จุด โดยแต่ละจุดจะมีจุดหมายที่ต่างกัน ซึ่งถ้าใช้คลาส Animation ก็เตรียมใจไว้เลยว่าการเคลื่อนที่ไปแต่ละจุดจะต้องเก็บค่าตำแหน่งนั้นๆไว้ทุกครั้งเพื่อใช้กำหนดค่าในคำสั่งครั้งต่อไป ถ้าถามว่าเขียนได้มั้ย ก็ไม่ยากหรอก แต่ถ้ามองในแง่ของความลำบากก็ถือว่าลำบากไม่น้อยที่ต้องมานั่งคอยเก็บค่าตำแหน่งทุุกครั้งที่ Animation แต่ละจุดเสร็จสิ้น
ภาพข้างบนน่าจะช่วยให้เห็นภาพการเคลื่อนไหวได้ง่ายขึ้น โดยการใช้ Animation สั่งให้เคลื่อนไหวในลักษณะดังกล่าวจะมีรูปแบบในลักษณะนี้
- จาก 1ไป 2 : สั่งให้เคลื่อนที่จากตำแหน่ง x, y ไปยัง x+25, y+5
- จาก 2 ไป 3 : สั่งให้เคลื่อนที่จากตำแหน่ง x+25, y+5 ไปยัง x+15, y+20
- จาก 3 ไป 4 : สั่งให้เคลื่อนที่จาก x+15, y+20 ไปยัง x+5, y+15
จะเห็นว่าการจะเคลื่อนที่จากจุดหนึ่งไปอีกจุดหนึ่งนั้นต้องกำหนดจุดเริ่มต้นในแต่ละช่วงด้วย ซึ่งตรงนี้นี่แหละที่ยุ่งยาก เพราะว่าต้องคอยเก็บค่า x, y ล่าสุดไว้
แต่สำหรับ Object Animator นั้นจะง่ายกว่ามาก เพราะว่าไม่จำเป็นต้องรู้ว่า ณ ตอนนั้นๆวัตถุอยู่ตำแหน่งไหน แต่บอกแค่ว่าให้วัตถุเคลื่อนที่ไปที่ตำแหน่งไหนก็พอ
อาจจะดูไม่ค่อยต่างกัน แต่ทว่าขั้นตอนในการสั่งงานนั้นต่างกันดังนี้
- จาก 1ไป 2 : สั่งให้เคลื่อนที่ไปยัง x+25, y+20
- จาก 2 ไป 3 : สั่งให้เคลื่อนที่ไปยัง x+15, y+20
- จาก 3 ไป 4 : สั่งให้เคลื่อนที่ไปยัง x+5, y+15
จะเห็นว่า Object Animator ไม่สนว่า ณ ตอนนั้นวัตถุอยู่ที่ตำแหน่งใด เพียงแค่บอกว่าให้ไปที่ตำแหน่งนั้นๆ วัตถุไม่ว่าจะอยู่ตำแหน่งใดมันก็จะเคลื่อนที่ไปยังตำแหน่งที่กำหนดให้ทันที ซึ่งเป็นอะไรที่ง่ายขึ้นมาก! (ผู้ที่หลงเข้ามาอ่านคนใดยังไม่เข้าใจว่ามันง่ายกว่ายังไง ให้ไปลองเขียนด้วยคลาส Animation ให้เคลื่อนที่แบบในภาพแล้วจะรู้ว่ามันลำบากเช่นใด)
เพิ่มเติม — นอกจากนี้ยังแก้ปัญหาในเรื่องความต่อเนื่องของ Animation หลายๆตัวได้อีกด้วย เช่นเจ้าของบล็อกทำปุ่มสองปุ่มด้วยกัน ปุ่มแรกเคลื่อนที่ไปทางขวาของตำแหน่ง Origin เป็นระยะทาง 200px ส่วนอีกปุ่มเคลื่อนที่ไปทางซ้ายของตำแหน่ง Origin เป็นระยะทาง 200px เมื่อกดสลับไปมาก็จะทำให้วัตถุย้ายตำแหน่งไปมาตามที่กด
สำหรับคำสั่งที่ใช้จะเป็นการเคลื่อนที่โดยใช้เวลา 1 วินาที (ในภาพจะเร็วกว่าของจริง) สมมติว่าจุดเริ่มต้นคือ 0, 0 เมื่อกดให้เคลื่อนไปทางขวา ก็จะเลื่อนไปตำแหน่ง 200, 0 โดยใช้เวลา 1 วินาที และเมื่อกดให้เคลื่อนที่ทางซ้ายก็จะเลื่อนไปตำแหน่ง -200, 0 โดยใช้เวลา 1 วินาทีเช่นกัน ดังนั้นการเคลื่อนที่จะไม่สนระยะทางซักเท่าไร ความเร็วจะอิงไปตามระยะเวลาที่กำหนดไว้เป็นหลัก
กายย้ายตำแหน่ง วิญญาณก็ตามไปด้วย
ไม่ต้องตกใจกันนะครับ ไม่ได้เข้าสู่รายการล่าท้าผีแต่อย่างใด แต่ที่พูดถึงนี่หมายถึง Object Animator ต่างหาก ถ้าผู้ที่หลงเข้ามาอ่านเคยใช้ Translate Animation เพื่อย้ายตำแหน่งของ View ก็อาจจะทราบกันดีว่า “มันไปแค่ภาพ” เพราะว่าเวลากดมันก็ต้องกดที่ตำแหน่งเริ่มต้นอยู่ดี
ยกตัวอย่างเช่น สร้าง Image View ขึ้นมาหนึ่งตัวโดยให้กดแล้วมี Toast แสดงว่า Touching~! โดยที่ Image View จะเลื่อนตำแหน่งได้ด้วยคลาส Animation โดยกดที่ Button
TranslateAnimation anim = new TranslateAnimation(0, 200, 0, 0);
anim.setDuration(1000);
anim.setFillEnabled(true);
anim.setFillBefore(true);
anim.setFillAfter(true);
ImageView imageView = (ImageView)findViewById(R.id.imageView);
imageView.startAnimation(anim);
imageView.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
Toast.makeText(Main.this, "Touching~!" , Toast.LENGTH_SHORT).show();
}
});
ขอย่อโค๊ดแค่ส่วนที่สำคัญก็พอเนอะ และเมื่อลองทดสอบเจ้าของบล็อกก็ขอเปิด Layout Bounds บนอุปกรณ์แอนดรอยด์ซะหน่อย จะได้เห็นกรอบของวัตถุ (เปิดได้ที่ Settings > Developer options > Show layout bounds
)
จะเห็นว่าเมื่อภาพได้เคลื่อนที่ไปทางขวามือเรียบร้อยแล้ว เมื่อกดบนภาพดันไม่เกิดอะไรขึ้น แต่พอกดที่ตำแหน่งเริ่มแรกของภาพ (ในกรอบสี่เหลี่ยมตรงกลางแสดงถึงจุด Origin ของวัตถุ) มันดันกดได้ซะงั้น! เพราะงั้นในกรณีนี้สรุปได้ว่าคลาส Animation ทำให้ “กาย” ย้ายตำแหน่งได้ แต่ทว่า “วิญญาณ” มันดันไม่ตามไปด้วย
แต่สำหรับ Object Animator นั้นจะทำให้ทั้งกายและวิญญาณไปด้วยกันทั้งคู่ (ฟังดูหลอนดี)
ImageView imageView = (ImageView)findViewById(R.id.imageView);
imageView.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
Toast.makeText(Main.this, "Touching~!", Toast.LENGTH_SHORT).show();
}
});
ObjectAnimator anim = ObjectAnimator.ofFloat(imageView, View.TRANSLATION_X, 200f);
anim.setDuration(1000);
anim.start();
ลองทดสอบดูก็จะเห็นว่าการใช้ Object Animator จะช่วยให้กายกับวิญญาณไปด้วยกันแล้ว ถึงแม้จะเห็นกรอบสี่เหลี่ยมอยู่ที่เดิมก็เถอะ แต่เมื่อภาพเคลื่อนที่ไปแล้วก็สามารถกดที่ภาพได้เลย
จริงๆพูดถึงแค่เรื่องนี้ก็คงชูข้อดีของคลาส Object Animator ได้แล้วล่ะ เพราะเดิมทีปัญหานี้เป็นปัญหาที่วุ่นวายมากสำหรับคลาส Animation เพราะอยากจะทำแอพที่ย้าย Button ไปมา แต่ดันกดไม่ได้ซะงั้น
ยกยอมาระยะนึงละ เพราะงั้นเข้าสู่วิธีการใช้ Object Animator กันต่อเลยดีกว่า
สำหรับ Object Animator จะมีลูกเล่นในการใช้งานได้หลายแบบ แต่เจ้าของบล็อกจะยกตัวอย่างแบบง่ายสุดก่อนก็แล้วกันเนอะ
ObjectAnimator anim = ObjectAnimator.ofFloat(View, Properties, Value);
anim.setDuration(Milliseconds);
anim.start();
จะเห็นว่าการเรียกใช้งานนั้นง่ายและสั้น โดยจะมีพารามิเตอร์ที่ต้องกำหนด ดังนี้
- View : Widget ที่ต้องการทำ Object Animator เช่น Linear Layout หรือ Button (Object)
- Properties: คุณสมบัติที่จะทำ Object Animator เช่น Alpha หรือ Rotation (Properties)
- Value : ค่าที่จะกำหนดให้กับ Object Animator (Float)
- Milliseconds: ระยะเวลาในการทำ Object Animator (Long)
พูดเพิ่มเติมในส่วนของ Properties เสียหน่อยว่าเอามาจากไหน? แล้วจะรู้ได้ไงว่าควรใส่ค่าอะไรลงไป เพราะว่าในช่องดังกล่าวนั้นจะต้องใส่เป็น String ซึ่งจะเห็นตัวอย่างจากบางที่ (แม้แต่ Android Developer) นั้นใส่เป็น “x” หรือก็คือใส่ String ลงไปตรงๆ ซึ่งไม่ผิดหรอก เพราะว่าเมธอด ofFloat นั้น Overload ไว้เป็นสองชุดด้วยกัน รองรับทั้งการใส่เป็น String และ Properties<View, Float>
กรณีที่อยากจะใส่เป็นแบบ String ก็จะเปลี่ยนแค่ส่วนของ Properties เป็น String ดังนี้
ObjectAnimator anim = ObjectAnimator.ofFloat(View, String, Value);
anim.setDuration(Milliseconds);
anim.start();
โดยเจ้าของบล็อกลองหาข้อมูลว่าใส่เป็นแบบ Properties<View, Float> ยังไง ก็ได้ไปพบว่าค่าดังกล่าวเก็บไว้ในคลาส View ซึ่งสามารถนำมาเรียกใช้งานได้เลย
ให้สังเกตว่าจะมีต่อท้ายว่า Property<android.view.View, java.land.Float> แปลว่าอันนั้นนั่นแหละคือ Properties ที่สามารถกำหนดในนี้ได้ ซึ่งจะมีทั้งหมดดังนี้
View.ALPHA = "alpha"
View.ROTATION = "rotation"
View.ROTATION_X = "rotationX"
View.ROTATION_Y = "rotationY"
View.SCALE_X = "scaleX"
View.SCALE_Y = "scaleY"
View.TRANSLATION_X = "translationX"
View.TRANSLATION_Y = "translationY"
View.X = "x"
View.Y = "y"
ก็จะเห็นว่ามีทั้ง Alpha, Rotate, Scale และ Translate
แต่ก็คงมีผู้ที่หลงเข้ามาอ่านที่เอะใจว่า TRANSLATION_X
กับ X
และ TRANSLATION_Y
กับ Y
มันต่างกันอย่างไร เพราะทั้งสองนี้ก็คือ Translate เหมือนกัน แต่ทว่ามีจุดอ้างอิงที่ไม่เหมือนกัน โดยที่ X
กับ Y
จะอิงที่ Parent ส่วน TRANSLATION_X
กับ TRANSLATION_Y
จะอิงที่วัตถุนั้นๆ ดังภาพข้างล่างจะเห็นว่าตำแหน่ง 0, 0 ที่ใช้สั่งงานให้กับภาพมีพิกัดไม่ตรงกัน ทั้งนี้เพราะเจ้าของบล็อกวางภาพไว้ที่กลางจอ ดังนั้นจุดเริ่มต้น (0, 0)ของ TRANSLATION_X
กับ TRASLATION_Y
จะอยู่ที่มุมซ้ายบนของตัวภาพ แต่ X
กับ Y
จะอิงจากมุมซ้ายบนของ Parent ซึ่งก็คือ Relative Layout ที่เจ้าของบล็อกเอา Image View ใส่ไว้ในนี้นั่นเอง
ในกรณีที่ต้องการเคลื่อนที่ในหน่วย dp ก็ให้เพิ่มฟังก์ชันแปลงค่าจาก dp เป็น px แล้วค่อยเอาไปกำหนดในคำสั่งของ Object Animator ละกันนะ
แล้ว Rotation, Rotation X และ Rotation Y ต่างกันอย่างไร?
คำตอบคือแนวแกนในการหมุนวัตถุครับ ดูจากภาพข้างล่างเลย
ถ้าอยากทำ Flip Clock ก็ใช้ Rotation X ทำได้เลยนะเนี่ย XD
นอกจากนี้ยังสามารถกำหนด Interpolator ได้เหมือนเดิม
ObjectAnimator anim = ObjectAnimator.ofFloat(View, Properties, Value);
anim.setDuration(Milliseconds);
anim.setInterpolator(Interpolator);
anim.start();
ตัวอย่างการใช้งาน
ImageView imageView = (ImageView)findViewById(R.id.imageView);
ObjectAnimator anim = ObjectAnimator.ofFloat(imageView, View.TRANSLATION_X, 200f);
anim.setDuration(1000);
anim.setInterpolator(new AccelerateInterpolator());
anim.start();
ยังไม่หมดนะ สามารถใช้คำสั่งเหล่านี้ได้อีกด้วย
ObjectAnimator anim = ...
anim.pause();
anim.resume();
anim.reverse();
anim.end();
anim.cancel();
- Pause : สั่งให้ Animation หยุดชั่วคราว
- Resume: สั่งให้ Animation เล่นต่อจากของเดิมที่หยุดไว้
- Reverse: แสดง Animation แบบย้อนกลับ (เจ๋งป่ะล่ะ)
- End : หยุด Animation แล้วไปแสดงที่ตอนจบเลย (
onAnimationEnd
) - Cancel: ยกเลิก Animation ทันที (
onAnimationCancel
)
สำหรับ End กับ Cancel จะต่างกันตรงที่วัตถุ ณ ตอนนั้นๆ ถ้า End จะทำให้วัตถุข้ามไปยังตอนจบทันที ส่วน Cancel จะเป็นการยกเลิกกลางคัน ถ้าวัตถุกำลังเคลื่อนที่อยู่ก็จะหยุดอยู่ที่ตรงนั้นทันที (ในขณะที่ End จะเลื่อนไปที่ปลายทางให้ทันที)
ส่วนเมธอดอื่นๆอย่างเช่น setRepeat
อะไรพวกนี้ก็ยังคงมีอยู่เหมือนเดิม ว่างๆก็ลองนั่งอ่าน Reference ของ Object Animator ก็ได้นะ
ถึงจุดนี้แล้วก็คงจะสงสัยเพิ่มเติมกันว่า อย่าง Translate มันเลือกว่าเป็น X หรือ Y แล้วถ้าอยากให้สั่งงานได้ทั้ง X และ Y ล่ะ จะต้องทำยังไง?
ดังนั้นเจ้าของบล็อกก็จึงขอแนะนำคลาส AnimatorSet กันต่อ ซึ่งมีไว้สำหรับ Combine เจ้า Object Animator เข้าด้วยกันนั่นเอง ซึ่งมีวิธีการใช้งานดังนี้
ObjectAnimator animX = ObjectAnimator.ofFloat(imageView, View.TRANSLATION_X, 250f);
ObjectAnimator animY = ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, 500f);
AnimatorSet animSet = new AnimatorSet();
animSet.playTogether(animX, animY);
animSet.setDuration(1000);
animSet.start();
หัวใจสำคัญนั้นอยู่ที่คำสั่ง playTogether เสียมากกว่า ซึ่งในนี้สามารถกำหนด Object Animator กี่ตัวก็ได้ จึงสามารถผสม Object Animator ได้หลากหลายแบบ
animSet.playTogether(anim1, anim2, anim3, anim4, ...);
และสำหรับ Duration หรือระยะเวลาจะกำหนดที่ Animator Set ก็ได้ หรือจะกำหนดแยกย่อยลงไปใน Object Animator ก็ได้เช่นกัน ซึ่งโค๊ดจะอิงความสำคัญไปที่ Animator Set เป็นหลัก ดังนั้นถึงจะกำหนด Duration ใน Object Animator แต่ละตัวแล้ว แต่ถ้ามีการกำหนดค่าที่ Animator Set เวลาที่ทำงานจริง Animation ก็จะอิงที่ Duration ของ Animator Set แทน
แต่ถ้าอยากให้เคลื่อนที่แยกกันคือ เคลื่อนที่ในแกน X ก่อน แล้วค่อยเคลื่อนที่ในแกน Y ก็จะมีคำสั่ง playSequentially ให้ใช้แทน playTogether
animSet.playSequentially(animX, animY);
สำหรับ Duration ใน playSequenctially จะไม่ได้คิดรวมกัน แต่จะมีผลแยกกัน เช่น กำหนดไว้ว่า 1 วินาที (1000ms) ในแต่ละสเตปก็จะใช้เวลาทุกๆ 1 วินาทีนั่นเอง แต่ก็สามารถกำหนดที่ Object Animator ได้เหมือนกัน
สรุปก็คือ playTogether
มีไว้ทำให้ Object Animator ทำงานพร้อมๆกัน ส่วน playSequenctially
มีไว้ทำให้ Object Animator ทำงานเรียงตามลำดับนั่นเอง
ถ้าอยากให้เคลื่อนที่ต่อเนื่องกันโดยกำหนด Properties มากกว่า 1 อย่างล่ะ?
ย้อนกลับไปตอนต้นเรื่องที่เจ้าของบล็อกสมมติการเคลื่อนไหวต่อเนื่อง ทีนี้ในทางปฏิบัติจริงควรทำอย่างไรล่ะ จึงจะให้วัตถุสามารถเคลื่อนที่ได้ต่อเนื่องเหมือนกับภาพตัวอย่าง
หรือพูดง่ายๆก็คือการผสมระหว่าง playTogether
กับ playSequenctially
เข้าด้วยกันนั่นเอง อยากจะให้ Object Animator ทำงานร่วมกัน แล้วทำเป็นหลายๆชุดมาเรียงต่อๆกัน ซึ่ง Animation API ตัวใหม่นี้ก็ทำให้เป็นเรื่องง่ายๆทันใด ดังนั้นจึงไม่ใช่เรื่องยากอีกต่อไปแล้ว (ลองทำด้วยคลาส Animation แล้วน้ำตาแทบจะไหล)
โดยหัวใจสำคัญก็อยู่ที่ playSequentially
นั่นแหละ ที่สามารถกำหนดพารามิเตอร์เป็น Animator Set ได้ ดังนั้นเจ้าของบล็อกก็สร้าง Animator Set ไว้สามชุดให้เคลื่อนที่แบบ playTogether
ไว้สามตำแหน่งที่ต้องการ แล้วนำมากำหนดใน Animator Set ด้วยคำสั่ง playSequenctially
นั่นเอง
อ้าวงง? สงสัยจะเยอะเกินไป ดูตัวอย่างเลยละกัน
ObjectAnimator animX1 = ObjectAnimator.ofFloat(imageView, View.TRANSLATION_X, 250f);
ObjectAnimator animY1 = ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, 50f);
animSet1.playTogether(animX1, animY1);
AnimatorSet animSet2 = new AnimatorSet();
ObjectAnimator animX2 = ObjectAnimator.ofFloat(imageView, View.TRANSLATION_X, 150f);
ObjectAnimator animY2 = ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, 200f);
animSet2.playTogether(animX2, animY2);
AnimatorSet animSet3 = new AnimatorSet();
ObjectAnimator animX3 = ObjectAnimator.ofFloat(imageView, View.TRANSLATION_X, 50f);
ObjectAnimator animY3 = ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, 100f);
animSet3.playTogether(animX3, animY3);
AnimatorSet animSet = new AnimatorSet();
animSet.playSequentially(animSet1, animSet2, animSet3);
animSet.setDuration(1000);
animSet.start();
บร่ะเหมือนโค๊ดจะเยอะ แต่เอาจริงๆมันง่ายกว่าของเดิมเยอะ เพราะของเดิมเขียนเยอะกว่านี้อีก แถมอันนี้ดูเข้าใจง่ายมาก แบ่ง Animator เป็นชุดๆแล้วค่อยเอามารวมกัน
Animation Resource
นอกจากนี้ Object Animator ยังทำเป็น Resource เพื่อดึงมาใช้งานได้อีกด้วยนะ ซึ่งจะเรียกว่า Animator โดยชื่อโฟลเดอร์ใน res
จะชื่อว่า animator
สมมติว่าสร้าง Animator Resource เป็น Object Animator ขึ้นมาดังนี้
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="rotationX"
android:interpolator="@android:anim/accelerate_interpolator"
android:duration="1000"
android:valueType="floatType"
android:valueTo="180" />
เวลาเรียกใช้งานก็จะเป็นแบบนี้แทน
Context context = ...
ObjectAnimator anim = (ObjectAnimator)AnimatorInflater.loadAnimator(context, R.animator.my_anim);
anim.setTarget(imageView);
anim.start();
- R.animator.my_anim คือที่อยู่ของ Resource File ที่ได้สร้างขึ้นมา (Int)
- imageView คือวัตถุที่ต้องการสั่งงาน (Object)
เห็นมั้ยล่ะ ง่ายๆสั้นๆ ได้ใจความ (ใช่เรอะ!) แต่แค่นี้คงไม่สะใจซักเท่าไรนัก ถ้าจะให้สะใจก็ต้องทำเป็น Animator Set ถึงจะคุ้มค่าเสียหน่อย เพราะถ้ากำหนดค่าไว้ในนี้แล้วต้องไปทำ Animator Set ผ่านโค๊ดก็คงไม่ได้ช่วยให้ดีขึ้นซักเท่าไร ดังนั้นมายำโค๊ดไว้ใน Animator Resource กันเถอะ!!
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="sequentially" >
<set android:ordering="together" >
<objectAnimator
android:propertyName="translationX"
android:valueType="floatType"
android:valueTo="-50dp"
android:duration="300" />
<objectAnimator
android:propertyName="translationY"
android:valueType="floatType"
android:valueTo="-50dp"
android:duration="300" />
<objectAnimator
android:propertyName="scaleX"
android:valueType="floatType"
android:valueTo="2"
android:duration="300" />
<objectAnimator
android:propertyName="scaleY"
android:valueType="floatType"
android:valueTo="2"
android:duration="300" />
</set>
<set android:ordering="together" >
<objectAnimator
android:propertyName="scaleX"
android:valueType="floatType"
android:valueTo="0"
android:duration="500" />
<objectAnimator
android:propertyName="scaleY"
android:valueType="floatType"
android:valueTo="0"
android:duration="500" />
<objectAnimator
android:propertyName="alpha"
android:valueType="floatType"
android:valueTo="0"
android:duration="500" />
<objectAnimator
android:propertyName="translationX"
android:valueType="floatType"
android:valueTo="100dp"
android:duration="500"
android:interpolator="@android:anim/accelerate_interpolator" />
<objectAnimator
android:propertyName="translationY"
android:valueType="floatType"
android:valueTo="100dp"
android:duration="500"
android:interpolator="@android:anim/accelerate_interpolator" />
</set>
</set>
Context context = ...
AnimatorSet animSet = (AnimatorSet)AnimatorInflater.loadAnimator(context, R.animator.my_set);
animSet.setTarget(imageView);
animSet.start();
ผลลัพธ์ที่ได้ก็จะออกมาเป็นแบบนี้~!
สำหรับข้อสังเกตในโค๊ดตัวอย่างนี้ก็จะเห็นว่าเจ้าของบล็อกสามารถกำหนดเรื่อง Translate เป็นหน่วย dp ได้เลย ส่วนค่าที่กำหนดใน Scale จะเป็นจำนวนเท่าของภาพ เช่น 2 ก็คือขยายสองเท่าของภาพปกตินั่นเอง และ Alpha กำหนดเป็น % โดยค่าอยู่ในช่วง 0–100
และอีกจุดหนึ่งที่ผิดหวังเล็กน้อยคือไม่สามารถกำหนด Duration โดยรวมของแต่ละ Set จากแท็ก <set>
ได้ เพราะในแท็กนี้กำหนดได้แค่ว่าจะเป็น Together หรือ Sequenctially เท่านั้น จึงทำให้เจ้าของบล็อกต้องมานั่งกำหนด Duration ให้กับ Object Animator ทุกๆอัน
จุดสุดท้ายก็คือสามารถใส่ Interpolator ไว้ในนี้ได้เลยเช่นกัน ดังนั้นจึงสามารถรังสรรค์รูปแบบของ Animation ผ่าน Animator Resource ได้เต็มที่!!
เพิ่มเติม — นอกจากนี้ยังสามารถกำหนดค่าเริ่มต้นให้กับ Object Animator ก็ได้นะถ้าต้องการ
android:valueFrom="0" />
android:valueTo="500" />
การกำหนดค่าแบบนี้ก็จะทำให้มีผลเมื่อสั่งให้ Object Animation ใหม่ในทุกๆครั้งวัตถุก็จะกลับสู่ค่าเริ่มต้นตามที่กำหนดไว้ตลอด อย่างเช่น สมมติว่าค่าตัวอย่างข้างบนนี้คือระยะแกน X เมื่อ Object Animator ตัวนี้ทำงาน วัตถุก็จะถูกเซ็ตไว้ที่ตำแหน่ง 0 ก่อนแล้วค่อยเคลื่อนที่ไปที่ตำแหน่ง 500 ทุกๆครั้งที่สั่งงาน
ยังไม่หมดนะ
สำหรับ Animator Set ยังประยุกต์ใช้กับวัตถุที่มากกว่าหนึ่งก็ได้เช่นกันนะ อย่างเช่น อยากจะให้วัตถุเคลื่อนที่พร้อมๆกันเป็นต้น ก็เพียงแค่กำหนด Object Animator แยกวัตถุกันแล้วใช้ Together เพื่อให้ทำงานพร้อมๆกันเป็นต้น
AnimatorSet animSet = new AnimatorSet();
ObjectAnimator animX1 = ObjectAnimator.ofFloat(imageView1, View.TRANSLATION_X, 250f);
ObjectAnimator animX2 = ObjectAnimator.ofFloat(imageView2, View.TRANSLATION_X, -250f);
animSet.playTogether(animX1, animX2);
animSet.setDuration(1000);
animSet.start();
จะเห็นว่า Image View มีสองอัน (จินตนาการเอาจากโค๊ดตัวอย่างนะ) โดยกำหนด Object Animator ต่างกันแต่ทว่าเอามารวมไว้ใน Animator Set ตัวเดียวกันแล้วสั่งให้ทำงานพร้อมๆกัน ผลที่ได้ก็จะออกมาดังนี้
ยังไม่พอๆ สำหรับค่า Value ที่ใช้ในการกำหนดค่าให้กับ Object Animator ยังสามารถกำหนดแบบหลายๆค่าพร้อมๆกันแบบนี้ได้ด้วยนะเออ
ObjectAnimator anim = ObjectAnimator.ofFloat(imageView, View.TRANSLATION_X, new float[] { 0f, 250f, -100f, 200f });
anim.setDuration(2000);
anim.start();
โดยที่ค่าตัวแรกสุด ( 0f ) คือตำแหน่งเริ่มต้น และจะเคลื่อนที่ไปตามค่าที่กำหนดไว้ในชุดอาร์เรย์จนถึงตัวสุดท้ายแล้วก็หยุดที่ตรงนั้น
Animation Listener
สำหรับ Object Animator นั้นมี Event Listener อยู่ด้วยกัน 3 ตัวดังนี้
- AnimatorListener
- AnimatorUpdateListener
- AnimatorPauseListener
AnimatorListener
เป็น Event Listener เบื้องต้นของ Animator โดยจะมีดังนี้
ObjectAnimator anim = ...
...
anim.addListener(new AnimatorListener() {
public void onAnimationStart(Animator animation) {
}
public void onAnimationRepeat(Animator animation) {
}
public void onAnimationEnd(Animator animation) {
}
public void onAnimationCancel(Animator animation) {
}
});
onAnimationStart
- เมื่อ Animator เริ่มทำงานonAnimationRepeat
— เมื่อ Animator ทำงานวนซ้ำใหม่อีกรอบonAnimationEnd
- เมื่อ Animator ทำงานจบแล้วonAnimationCancel
- เมื่อ Animator ถูกยกเลิกกลางคัน
หน้าที่หลักๆก็คือเอาไว้สั่งงานบางอย่างในระหว่างการทำงานของ Object Animator นั่นเอง เช่น ต้องการ กำหนดค่าบางอย่างเมื่อ Animation เริ่มทำงาน หรือซ่อน View บางตัวเมื่อ Animation ทำงานเสร็จแล้วเป็นต้น
AnimatorUpdateListener
เป็น Event Listener เพื่อเสริมการทำงานของ Animator โดยจะมีดังนี้
ObjectAnimator anim = ...
anim.addUpdateListener(new AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
}
});
onAnimationUpdate
- เมื่อ Animator ทำงาน โดยจะถูกเรียกทุกครั้งที่ Animation มีการอัพเดททุกๆเฟรม เพราะงั้นในฟังก์ชันนี้จะถูกเรียกซ้ำแบบรัวๆในระหว่างที่ Object Animator ทำงานนั่นเอง
น่าจะเหมาะกับการใช้ในแอพบางอย่างที่ต้องการคำนวณค่าจาก Object Animator เช่นระยะในการเคลื่อนที่เป็นต้น ซึ่งต้องมีการเช็คค่าตลอดเวลาที่ Animation ทำงานแล้วนำค่าไปใช้งานต่างๆตามต้องการ
AnimationPauseListener
เป็น Event Listener ที่เพิ่มเข้ามาใน API 19 โดยมีดังนี้
ObjectAnimator anim = ...
anim.addUpdateListener(new AnimatorUpdateListener() {
public void onAnimationResumeAnimator animation) {
}
public void onAnimationPause(Animator animation) {
}
});
onAnimationResume
— เมื่อ Animator หยุดทำงานชั่วคราวแล้วถูกสั่งให้ทำงานต่อonAnimationPause
- เมื่อ Animator ถูกสั่งให้หยุดชั่วคราว
มีไว้เพื่อรองรับการแสดง Animation บางอย่างที่อาจจะมีการหยุดชั่วคราว เช่น เมื่อสั่ง Pause ก็จะแสดงปุ่ม Resume ขึ้นมาเพื่อให้สามารถกด Resume ต่อได้ เป็นต้น
ทิ้งท้าย
สำหรับ Object Animator นั้นเป็นแค่ส่วนหนึ่งของ Animation API ที่มากับ Android 3.0 Honeycomb (API 11) นะครับ ซึ่งจริงๆแล้วมีเยอะกว่านี้อีก แต่เจ้าของบล็อกก็ขอหยิบ Object Animator มาทำบทความให้ได้อ่านกัน เพราะว่านิยมใช้เยอะสุดแล้ว และที่สำคัญคือ เวอร์ชันต่ำกว่าจะไม่รองรับนะ แต่ถ้า ณ ปัจจุบันนี้เวอร์ชันขั้นต่ำก็ควรจะเป็น Android 4.0 Ice Cream Sandwich (API14) กันได้แล้วล่ะ
และที่ทำบทความเรื่อง Animation API ตัวนี้ขึ้นมาก็เพราะว่า อยากให้ผู้ที่หลงเข้ามาอ่านใส่ใจกับเรื่อง UI มากกว่าเลย์เอาท์และดีไซน์บ้าง เพราะการใส่ Animation จะช่วยให้แอปพลิเคชันดูมีชีวิตชีวาและตอบโต้กับผู้ใช้ได้ดียิ่งขึ้น ทำให้แอปพลิเคชันดูน่าสนใจกว่าเดิม
ดังนั้นถ้าเป็นไปได้ก็อยากจะให้ยอมเสียเวลาใส่ใจกับเรื่องเล็กๆน้อยๆแบบนี้เสียหน่อย แล้วแอปพลิเคชันของผู้ที่หลงเข้ามาอ่านก็จะดูดีมีชาติตระกูลยิ่งกว่าเดิมเลยล่ะ และบทความนี้ไม่มีโค๊ดตัวอย่างให้ดาวน์โหลดนะ เพราะอยากให้ไปลองทำเองเสียมากกว่า จะได้เข้าใจยิ่งขึ้น ลองนู่นนั่นนี่ ลอง Animation แปลกๆตามที่ต้องการ จะได้นำไปประยุกต์ใช้ได้ดีกว่าเอาตัวอย่างไปดูครับ
ขอให้พลังของ Animation API จงสถิตอยู่กับท่าน~