มาทำปุ่มเด้งๆ แบบ spotify กัน

Ta Theerasan Tonthongkam
ta tonthongkam
Published in
5 min readOct 6, 2017

หลังจากที่แอพพลิเคชัน spotify เปิดตัวอย่างเป็นทางการในไทย มีสิ่งหนึ่งที่พูดถึงกันอย่างมากคือ ปุ่ม Shuffle ที่มันจะเด้งๆ หน่อย — ถ้ามองในมุมของ UX, มันทำให้รู้สึกว่าปุ่มมันโดนกดแล้วจริงๆ ดังนั้นการใส่ Animation(interactive UI) เล็กๆ น้อยๆ มันก็ทำให้ แอพพลิเคชันดูดีขึ้นมาก ตอนนี้ที่เห็นได้ชัดก็มี Spotify และ AppStore ของ iOS 11 ที่ทำแบบนี้

โดยส่วนตัวคิดว่า interactive UI จะเป็นเทรนในไม่ช้า เรามาศึกษาวิธีทำกันดีกว่า

ตัวอย่างแอพพลิเคชันที่มี event interractive

Resource ที่จำเป็นสำหรับบทความนี้

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

ในโปรเจคมีอะไรบ้าง?

ความตั้งใจในการสร้าง Code repository สำหรับ Android Workshop คือต้องการแชร์ความรู้, ปัญหาที่เจอในการทำงานจริง และคัดย่อเอาเฉพาะส่วนย่อยๆ มาเขียนเป็นบทความ

เมื่อโคลนโค้ดมาแล้วเข้าไปดูที่ workshop3

Disclaimer: โปรเจคนี้จะเขียนโดย Kotlin และจะใช้ความสามารถของ Kotlin ที่ Java ไม่มีด้วย — ดังนั้นใครอยากหัด Kotlin ลองใช้โอกาสนี้ลองเล่นไปพร้อมๆ กันเลยจ้าาา

ขอบเขตของโปรเจค

จะทำเป็น scrollView ที่มี cardView และ button โดยที่แต่ละ view สามารถกดแล้วเด้งๆ ได้ดังรูป

แต่ไหนๆ ก็ไหนๆ แล้ว จะทำตาม Spotify ทั้งหมดก็อาจจะยังไม่พอ ในตัวอย่างนี้จะมีการเพิ่มเมื่อ CardView กำลังเคลื่อนไหว ภาพพื้นหลังจะเคลื่อนแบบ parallax ด้วย — ไม่เชื่อลองกลับไปดูภาพข้างบน

ใน Workshop ชุดนี้ใช้วิธีเขียน 2 แบบคือ

  1. Just Old school style, OnTouchListener + OnClickListener
  2. Extension Functions with Kotlin Feature

Note:

Animation ถูก เพิ่มเข้ามาตั้งแต่ API level 11 มันรองรับหลายรุ่นมาก — งั้นใช้ไปเหอะ ใช้ได้หลาย version อิอิ

เรื่องสำคัญก่อนเริ่ม implement

ใน Workshop นี้จะทำงานกับ event OnTouch และ event OnClick; แต่ Event ทั้ง 2 นี้รอรับ input จาก Screen เหมือนกัน — สิ่งแรกที่ Event Handler เริ่มทำเมื่อมี input จากจอคือ OnTouch และถัดไปคือ OnClick; แต่ถ้า OnTouch คืนค่า event กลับไปก่อน (return true) มันจะไม่ทำ Event OnClick — สามารถดู Diagram การทำ Animation ได้จากภาพข้างล่าง

event diagram

จาก Diagram ถ้ามีการ กดค้างบนปุ่ม ปุ่มจะค่อยๆ เล็กลง → เมื่อปล่อยปุ่ม ปุ่มจะขยายใหญ่กว่าเดิม ก่อนที่จะเข้า สู่ onClick event และจากนั้นปุ่มจะลดขนาดเหลือเท่าเดิม

แต่ในกรณีที่ กดค้าง แล้ว scroll ขึ้น/ลง จะเข้าสู่ Cancel event และปุ่มจะกลับสู่ขนาดเดิม

เมื่อเข้าใจหลักการทำงานของ event แล้ว มาเริ่มโค้ดกัน

Just Old school style, OnTouchListener + OnClickListener

เริ่มแรกเรามาสร้าง view กันก่อนนะ

main_activity.xml

ใน Activity นี้จะมี Parent เป็น NestedScrollView แล้วมี CardView และ ปุ่มอยู่ภายใน

กลับมาที่ MainActivity.kt

ใน Activity นี้ สิ่งที่เราต้องทำหลักๆ คือ Handle Event จาก OnTouchListener แล้วก็คืนค่า Event ให้ถูกต้อง

implementation function ของ OnTouchListener จะมี Return Type เป็น Boolean เสมอ — ถ้าเราต้องการคืน event ทั้งหมดที่เกี่ยวกับการอ่าน input จากจอ แค่ return true กลับไป — แต่ถ้าเรายังอยากโยน Event ให้กับคนอื่นที่รอ input จากจอเช่น OnClickListener ให้เรา return false กลับไป จากนั้น OnClick จะทำงานต่อ

Note: เรื่องการจัดการ event สำคัญมากๆ นะไม่งั้น Animation มันจะแปลกๆ

ข้อสังเกต

6  when (event.action) {
7 MotionEvent.ACTION_DOWN -> {
: :
13 false
14 }
15 MotionEvent.ACTION_UP -> {
: :
47 false
48 }
49 MotionEvent.ACTION_CANCEL -> {
: :
54 true
55 }
56
57 else -> true
58 }

Event Action Down/Up จะไม่คืน Event จนกว่าจะทำ OnClick แต่ event Cancel และ event อื่นๆ จะทำการคืน event นะจุดๆนี้ ปุ่มจะไม่ทำ OnClick แล้ว — เรื่อง event ได้แล้ว งั้นเรามาดู Animation กันต่อเลย

ว่าด้วยเรื่อง Animation

มาเริ่มที่ Action Down ก่อน

v.animate().cancel()                    v.animate().scaleY(0.96f).scaleX(0.96f).setDuration(200).start()                    val img = v.findViewById(R.id.imageBackground)                    img.animate().scaleY(1.1f).scaleX(1.1f).alpha(0.7f).setDuration(200).start()
  1. เริ่มด้วยเราจะทำการ Cancel Animation ทั้งหมดที่มีก่อน เวลากดปุ่มย้ำๆ Animation จะได้ไม่ย่อขยายแบบแปลกๆ
  2. จากนั้นทำการย่อ View ลงไป 96% ในเวลา 200 ms
  3. ในทางกลับกัน image จะขยายขึ้น 10% และค่าความจางเหลือเพียง 70% ในกรณีที่ CardView สีเข้ม มันจะให้ให้รูปภาพมืดลง และ CardView จะดูเหมือนโดนกดจริงๆ

Action Up ใจความสำคัญของความเด้งๆ

ในจุดนี้ให้เราจินตาการถึงลูกโป่งยาง ที่เราเป่าลมเข้าไปแล้ว เมื่อเรากด มันจะเล็กลง แต่เมื่อเราปล่อย มันจะดีดตัวขยายออก เนื่องด้วยแรงดีด และความยืดหยุ่นของลูกโป่ง มันจะขยายเกิดขนาดเดิมไปนิดนึงก่อน แล้วหดลง แล้วขยายกลับ ดังกราฟข้างล่าง

กราฟแสดงขนาดของปุ่มเมื่อเทียบกับเวลา

จะเห็นว่าถ้าเราอยากทำ Animation ให้มันดูเด้งๆ ต้องทำหลายจังหวะหน่อย แต่ในบทความนี้จะทำแค่ 3 จังหวะ (จังหวะเยอะแล้วมันจะดูเยอะเกินไป) ตามคอนเซปท์ น้อยแต่มาก เรียบแต่โก้ ไฮแฟชั่น

การจะทำ Animation หลายๆ จังหวะใน Android ต้องใช้ AnimatorSet เข้ามาช่วย ก่อนอื่นเรามาสร้าง ObjectAnimation 3 ชิ้นกันก่อน คือ ใหญ่ เล็ก และธรรมดา

val xBigScale = ObjectAnimator.ofFloat(v, "scaleX", 1.03f)                    xBigScale.setDuration(160).repeatCount = 0       

val yBigScale = ObjectAnimator.ofFloat(v, "scaleY", 1.03f) yBigScale.setDuration(160).repeatCount = 0

val xSmallScale = ObjectAnimator.ofFloat(v, "scaleX", 0.985f) xSmallScale.setDuration(140).repeatCount = 0

val ySmallScale = ObjectAnimator.ofFloat(v, "scaleY", 0.985f) ySmallScale.setDuration(140).repeatCount = 0

val xNormalScale = ObjectAnimator.ofFloat(v, "scaleX", 1f) xNormalScale.setDuration(70).repeatCount = 0

val yNormalScale = ObjectAnimator.ofFloat(v, "scaleY", 1f) yNormalScale.setDuration(70).repeatCount = 0

Note : เรื่องของ เวลาในแต่ละเฟรม ได้ทำการคำนวณมาแล้ว จากสมการข้างล่าง และมีการปรับค่าภายหลังให้เลขลงตัว เล็กๆ น้อยๆ

y = sin(x) * (1/n)*(x) --- sin * exponential decay

เมื่อเราได้ ObjectAnimation มาแล้ว เราแค่ “เล่น” Animation ให้ถูกลำดับก็พอ

al animateSet = AnimatorSet()                    animateSet.play(xBigScale).with(yBigScale)                    animateSet.play(xSmallScale).after(xBigScale)                    animateSet.play(ySmallScale).after(yBigScale)                    animateSet.play(xNormalScale).after(xSmallScale)                    animateSet.play(yNormalScale).after(ySmallScale)                    animateSet.start()

ส่วน ImageView เราแค่ให้มันกลับสู่สภาพเดิมแบบธรรมดาๆ Animation จะได้ไม่เยอะเกินไป

val img = v.findViewById(R.id.imageBackground)                     img.animate()
.scaleY(1f)
.scaleX(1f)
.alpha(1f)
.setDuration(370)
.start()

Note : Duration คือค่าเวลาของ ObjectAnimation ทั้งหมดรวมกัน (160 + 140 + 70 ) มันจะได้ Animate กลับมาพร้อมๆ กัน

Event Cancel

การจะเข้า event นี้ได้มันจะผ่าน event down ก่อน ซึ่งขนาดของปุ่มอาจโดนลดไปแล้ว ดังนั้น เพื่อให้ปุ่มมีขนาดเดิมแบบดูไม่กระตุก ก็ต้องทำ Animation เช่นกัน

val img = v.findViewById(R.id.imageBackground)                    v.animate().scaleY(1f).scaleX(1f).setDuration(200).start()                    img.animate()
.scaleY(1f)
.scaleX(1f)
.alpha(1f)
.setDuration(370)
.start()

EventOnClick

สำหรับ event นี้มันควรทำหลังการ Animation ของ Action Up จบลง แต่ Event Onclick ดันถูกเรียกทันทีหลังจาก Action Up เลย ดังนั้นเพื่อความสมูท เราควรใส่ Delay ที่ OnClick ให้เท่ากับระยะเวลาของ Animation

Handler().postDelayed({                
Toast.makeText(this, "Clicked " + view.id,
Toast.LENGTH_SHORT).show()
}, 370)

เห็นมะ แค่นี้เอง เราก็จะได้ปุ่ม เด้งๆ แล้ว สนุกดีจัง — แต่เดี่ยวก่อน ถ้าคุณอยากทำปุ่มเด้งๆ หลายๆ ที่หล่ะ ติ๊กต็อกๆๆๆ — อ๋อ คิออกและ ทำ MyButton extend Button สิ่ง แล้วสร้างฟังก์ชัน คลิกแบบเด้งๆ ก็ได้แล้วหนิ แต่ว่าใน Android CardView ก็ OnClick ได้นะ — งั้นสร้าง MyCardView extend CardView, แล้วถ้า Layout เด้งได้ รูปเด้งได้ ทุกอย่างเด้งได้หล่ะ เราคง Extend ทุกอย่างไม่ไหวแน่

ถึงจุดๆ นี้มีความจริงข้อนึงที่ทุกคนต้องยอมรับ – View คือ Class แม่ ของ View ต่างๆ บน Android, นั้นหมายความว่าถ้าเราเพิ่มฟังก์ชัน เด้งๆ ที่ View ได้คราวนี้ไม่ว่าจะ ปุ่ม รูป เลเอาท์ หรืออื่นๆ มันก็จะเด้งๆ ได้แล้ว — แล้วเราจะทำอย่างไรหล่ะ?

ใช้พลังของ Kotlin สิ, Kotlin สามารถ ทำ Extension Functions ให้กับ Class แม่ได้, และ Class อื่นๆ ที่สืบทอด Class แม่ จะได้รับความสามารถนี้ด้วย

ตัวอย่างต่อไปเราจะเพิ่ม function setOnAnimateClickListener ให้กับ Class View เพื่อให้ทุกคนที่สืบทอด View สามารเรียก function นี้ได้

Extension Functions with Kotlin Feature

Extension Functions คืออะไร? เข้าใจง่ายๆ นะคือ การเพิ่มฟังก์ชันใดๆ ก็ตามให้กับ class ที่เราอยากเพิ่มความสามารถให้มัน

Syntax ก็ง่ายๆ ดังนี้

fun ClassName.functionName(parameters): ReturnType {
//function body
}

งั้นมาเริ่มกันเลย

ViewExtentions.kt

ง่ายๆ เลยคือย้าย logic ต่างๆ มาไว้ใน function นี้แทน

แต่จากโค้ดข้างต้นมีการ refactor นิดหน่อยเพื่อให้มี Background หรือไม่มีก็ได้ ข้อสังเกต

var secondaryView: View? = null  

if (resId != null) {
secondaryView = this.findViewById(resId)
}

จากโค้ดจะเห็นว่า secondaryView เป็น optional จะมีก็ได้ ไม่มีก็ได้

MainActivity.kt

หลังจากสร้าง function extension แล้ว มาดูวิธีการเรียกใช้ใน Activity กัน

function นี้ต้องการ Unit หลังจากโดน Click และ id ของ Background — แต่ถ้าไม่มี Background หล่ะ? — งั้นเรามาสร้าง function เพิ่มกัน

Extension เพิ่มเติม

จากโค้ดข้างบน เราสามารถใส่ Animation เด้งๆ ให้กับ view ทุกๆ แบบ ได้เลย โดยไม่จำเป็นต้องมี Background ก็ได้

สรุป

การทำ Animation ที่ดี ควรเข้าใจ “ธรรมชาติ” ของการเคลื่อนไหว เพื่อให้ Animation ดูสมูท — และต้องเข้าใจการจัดการ event บน Android ด้วย เพื่อให้จังหวะการแสดง Animation มันไม่กระตุก

Kotlin เป็นอีกอย่างนึง ที่มันมีความสามารถเยอะกว่า Java และเราควรเลือกใช้ความสามารถของมันให้ถูกจังหวะ เหมือนในบทความนี้ ที่เลือก function extension มาตอบโจทย์ความต้องการในที่นี้

การตามเทรน — developer ควรตามเทรนตลอดที่เกี่ยวข้องกับการพัฒนาแอพพลิเคชันตอน แม้ว่ามันจะเป็นเรื่องของ UX/UI เพื่อให้ตัวเราพร้อมที่จะพัฒนาโจทย์ใหม่ๆ เสมอ

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

ขอบคุณที่อ่านจนจบจ้าาา

— จบ —

ที่ eatigo เรารับสมัคร dev Android, iOS และ Designer อยู่ด้วยนะ ที่สำคัญคือเราเริ่มทำ แอพพลิเคชันใหม่ต้ังแต่ต้น เพราะฉนั้นยังเหลือที่ปล่อยของอีกเยอะ ใครสนใจ inbox ใน Facebook หรือคอมเมนท์ได้เลยจ้า

--

--