บทความนี้เน้นการใช้ CA Animations ใน iOS เพื่อทำแอนิเมชันที่ลื่นไหล
ในช่วงแรกทำงานกับ iOS ผมจะหวั่นไหวทุกครั้งที่นักออกแบบมาหาและขอแอนิเมชันในแอปที่พวกเขาทำอยู่

ผมเคยคิดว่าออกแบบแอนิเมชันง่าย — แต่ Implement ฝั่งอื่น ยากมาก
ผมขอความช่วยเหลือจาก Google, StackOverflow, และเพื่อนร่วมงาน ในระหว่างกระบวนการ ผมพัฒนาภาวะฟوبيาเรื่องแอนิเมชัน และพยายามหลีกเลี่ยงมันตลอด แต่มันเปลี่ยนไปวันหนึ่ง
การค้นพบ CAAnimation
ครั้งหนึ่ง ผมต้องทำแอนิเมชัน Sequence ของ Images ใน View ขั้นแรกของผมคือ? แน่นอน StackOverflow!
ลิงก์แรกได้โค้ด
let image_1 = UIImage(named: "image-1")!
let image_2 = UIImage(named: "image-2")!
let image_3 = UIImage(named: "image-3")!
let images = [image_1, image_2, image_3]
let animatedImage = UIImage.animatedImage(with: images, duration: 2.0)
imageView.image = animatedImage
ดูตรงไปตรงมาใช่ไหม? ถ้าง่ายขนาดนั้นผมไม่เขียนบทความนี้
นี่คือแอนิเมชันที่ต้องการ:

และชัดเจนว่าผมอยู่ไกลมาก ผมติดอยู่ จะทำ Customizations เยอะอย่างนั้นและ Sync ทั้งหมดได้อย่างไร?
เพื่อนร่วมงานบอกให้ลอง CAAnimation ผมอ่านและลองบน Sample Project ตกตกใจ มันทรงพลังและใช้ง่ายจริงๆ
Core Animation คืออะไร?
Core Animation ช่วย Execute Multiple Animations ด้วย CPU Usage เกือบศูนย์
ให้ High Frame Rate และ Customizations เยอะที่ใช้ได้ด้วยโค้ดน้อย
รายละเอียดเพิ่มเติมใน Docs: https://developer.apple.com/documentation/quartzcore
ผม Implement Basic ได้ในไม่กี่ชั่วโมง:
func addAnimation(firstImageView: UIImageView, secondImageView: UIImageView) {
let basicAnimation1 = getBasicAnimation(withInitialPostion: centerPosition, finalPos: finalPosition)
firstImageView.layer.add(basicAnimation1, forKey: "position")
let basicAnimation2 = self.getBasicAnimation(withInitialPostion: self.initalPosition, finalPos: self.centerPosition)
secondImageView.layer.add(basicAnimation2, forKey: "position")
self.addNextImage(forImageView: firstImageView)
}
func getBasicAnimation(withInitialPostion initialPos: CGPoint, finalPos: CGPoint) -> CABasicAnimation {
let basicAnimation = CABasicAnimation(keyPath: "position")
basicAnimation.fromValue = NSValue(cgPoint: initialPos)
basicAnimation.toValue = NSValue(cgPoint: finalPos)
basicAnimation.duration = 1
basicAnimation.isRemovedOnCompletion = false
basicAnimation.fillMode = CAMediaTimingFillMode.forwards
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
return basicAnimation
}

สำหรับ Implementation นี้ ผมใช้ CABasicAnimation
Class CABasicAnimation ช่วย Animate Layer Property (Background Color, Opacity, Position, Scale) ระหว่างสองค่า คุณแค่ให้ Start และ End Value ส่วนที่เหลือมันจัดการ เริ่มทันทีใน Run Loop ถัดไป ตามที่อธิบายเพิ่มเติมที่นี่
กลับมาที่ปัญหาของเรา
เพื่อ Implement นี้ ผมเอา Image Views สองตัว ใส่ Images แยกต่างหาก แล้วทำแอนิเมชันทีละอันด้วย CAAnimation
Source Code อยู่ที่นี่
ถ้าสังเกต GIF ล่าสุด จะเห็นว่าบางอย่างผิดปกติ ก่อน Gift Box Image แรกจะหลุดออกจากมุมมอง Headphones จะกระพริบสั้นๆ แล้ว Image ก็เคลื่อนขึ้น
ทำไมถึงเกิด?
เพราะทันทีที่เราเพิ่ม Animation ให้ Image View เราก็เพิ่ม Next Image ให้ View นั้น (บรรทัด 5 และ 6):
private func addAnimation(firstImageView: UIImageView, secondImageView: UIImageView) {
let basicAnimation1 = getBasicAnimation(withInitialPostion: centerPosition, finalPos: finalPosition)
firstImageView.layer.add(basicAnimation1, forKey: "position")
let basicAnimation2 = self.getBasicAnimation(withInitialPostion: self.initalPosition, finalPos: self.centerPosition)
secondImageView.layer.add(basicAnimation2, forKey: "position")
self.addNextImage(forImageView: firstImageView)
}
ที่นี่เรากำลังต่อสู้กับปัญหา Sync ทั้งสอง Images ในแอนิเมชัน แต่ก็มี Solution ด้วย CAAnimation
CA Transactions
CA Transactions ช่วย Sync Multiple Animations พร้อมกัน รับรองว่า Animations ทั้งหมดที่ Bundle ไว้ด้วยกัน เริ่มพร้อมกัน
นอกจากนี้ คุณสามารถให้ Completion Block กับ Animations ได้ ซึ่งจะ Execute เมื่อ Animations ทั้งหมดใน Bundle เสร็จ
อ่านเพิ่มเติมที่นี่
private func addAnimation(firstImageView: UIImageView, secondImageView: UIImageView) {
CATransaction.begin()
CATransaction.setCompletionBlock {
self.addNextImage(forImageView: firstImageView)
}
let basicAnimation1 = getBasicAnimation(withInitialPostion: centerPosition, finalPos: finalPosition)
firstImageView.layer.add(basicAnimation1, forKey: "position")
CATransaction.commit()
let basicAnimation2 = self.getBasicAnimation(withInitialPostion: self.initalPosition, finalPos: self.centerPosition)
secondImageView.layer.add(basicAnimation2, forKey: "position")
}
เริ่มด้วย CATransaction.begin() จากนั้นเขียน Animations ทั้งหมดที่ต้องการ Sync สุดท้ายเรียก CATransaction.commit() ซึ่งจะเริ่ม Animation ใน Block
ลองดูแอนิเมชันตอนนี้:

อีกอย่างสุดท้าย ผมต้องเพิ่ม Spring Effect ให้แอนิเมชัน ขอขอบคุณ CAAnimation ที่มี Solution ให้
CA Spring Animation
CA Spring Animation เมื่อเพิ่มให้ Layer จะให้ Spring-like Effect ทำให้ดูเหมือนถูกดึงไปยัง Target โดย Spring
Layer อยู่ห่างจาก Target มากเท่าไร Acceleration ไปทางนั้นก็มากเท่านั้น
อนุญาตควบคุม Physically-based Attributes เช่น Damping และ Stiffness ของ Spring – Docs
อ่านเพิ่มเติมจาก Apple Documentation: https://developer.apple.com/documentation/quartzcore/caspringanimation
มา Implement กับโค้ดเดิม:
private func getSpringAnimation(withInitialPostion initialPos: CGPoint, finalPos: CGPoint) -> CASpringAnimation {
let basicAnimation = CASpringAnimation(keyPath: "position")
basicAnimation.fromValue = NSValue(cgPoint: initialPos)
basicAnimation.toValue = NSValue(cgPoint: finalPos)
basicAnimation.duration = basicAnimation.settlingDuration
basicAnimation.damping = 14
basicAnimation.initialVelocity = 5
basicAnimation.isRemovedOnCompletion = false
basicAnimation.fillMode = CAMediaTimingFillMode.forwards
return basicAnimation
}

งานของผมเสร็จแล้ว
สรุป ข้อดีของการใช้ CA Animations:
- ใช้ง่ายและ Implement ง่าย
- มี Customizations เยอะ
- Sync Multiple Animations ได้
- CPU Usage เกือบศูนย์
นี่เพียงข้อดีบางส่วน ความเป็นไปได้ไม่มีที่สิ้นสุด
ตอนนี้ ทุกครั้งที่มี Requirement เรื่อง Animation ผมมั่นใจ Design และ Implement ได้ และหวังว่าคุณก็รู้สึกเหมือนกันหลังอ่าน
อย่าลังเล Leave Suggestions หรือ Feedback