12/08/2018, 10:56

View Animation in Swift

Trong phiên bản iOS7 và đặc biệt là iOS8 được phát hành thì animation và motion trở thành trung tâm cho việc thiết kế của ứng dụng từ Apple và các nhà phát triển bên thứ 3. iOS7 giới thiệu một dạng phẳng, tối thiểu hoá thiết kế cho ứng dụng mà kết quả trả về chắc chắn xảy ra trong một số ứng dụng ...

Trong phiên bản iOS7 và đặc biệt là iOS8 được phát hành thì animation và motion trở thành trung tâm cho việc thiết kế của ứng dụng từ Apple và các nhà phát triển bên thứ 3. iOS7 giới thiệu một dạng phẳng, tối thiểu hoá thiết kế cho ứng dụng mà kết quả trả về chắc chắn xảy ra trong một số ứng dụng có cùng kiểu UI. Để phân biệt ứng dụng của họ với các ứng dụng khác, các nhà phát triển làm việc với các tính năng như animation và motion để tạo ra sự vượt trội của ứng dụng của họ. Không chỉ dùng animation làm nên sự khác biệt của ứng dụng của họ với số khác mà họ có thể cải thiện trải nghiệm người dùng cho ứng dụng của bạn. Cho một ví dụ tuyết vời là làm sao để animation được sử dụng để cải thiện UX, bạn có thể thấy Apple sử dụng animation trong các ứng dụng của họ. Ví dụ trong ứng dụng Photos khi bạn chọn một ảnh từ một bộ sưu tập, ảnh sẽ phóng to ra từ một cái được chọn và tắt nó, khi nó co lại để trở về với ảnh được chọn. Việc này là thêm một navigation của ứng dụng trong đó nó cho bạn biết chính xác bạn đang ở đâu nếu bạn trình duyệt một số bức ảnh. Có rất nhiều cách để có thể tích hợp animation trong ứng dụng, một số được thực hiện bằng cách sử dụng UIKit Dynamic, tầng animation, dịch chuyển view controller hoặc bằng cách sử dụng thư viện của bên thứ 3 như Facebook Pop hoặc JNWSpringAnimation.

View Animation Cơ Bản

Tạo một animation trên các view của bạn là việc thay đổi thuộc tính của chúng và cho UIKit animate thực hiện tự động. Những thuộc tính chúng ta thay đổi là một trong những thuộc tính sau

center
alpha
frame
bounds
transform
backgroundColor
contentStretch

Bạn có thể tìm tất cả các animation liên quan đến thay đổi một hoặc nhiều thuộc tính bên trên. Ví dụ đơn giản, UIKit cung cấp các APIs sau đây có thể tạo các view animation trên màn hình

UIView.animateWithDuration(_, animations:)
UIView.animateWithDuration(_, animations:, completion:)
UIView.animateWithDuration(_, delay:, options:, animations:, completion:)

phương thức đầu tiên cần có 2 tham số một giá trị cho thời gian của animation được tính bằng giây và một closure nơi mà bạn xác định thuộc tính mà bạn muốn thay đổi. UIKit sẽ lấy trạng thái nguồn của view sau đó tạo một dịch chuyển mịn từ trạng thái này tới trạng thái kết thúc theo những gì mà bạn xác định trong animation closure.

Hai phương thức còn lại cũng giống với phương thức đầu tiên nhưng chúng cần thêm các tham số để thêm vào cấu hình của animation. Cái thứ hai cần một closure khi hoàn thành animation nơi mà bạn có thể xác định một animation khác mà bạn muốn hoàn thành sau cái thứ nhất hoặc bạn có thể thực hiện làm sạch UI, ví dụ quá trình xoá bỏ một view từ hệ thống view và một view khác xuất hiện trên màn hình.

Phương thức thứ ba cần thêm 2 tham số là delay thời gian mà cần phải chờ trước khi animation bắt đầu và options - một hằng số UIViewAnimationOptions cái để chỉ rõ cho bạn muốn thực hiện animation thế nào. Các options bạn có thể tham khảo ở UIViewAnimationOptions struct.

Spring Animation

spring animation theo mẫu của hành vi của cuộc sống mùa xuân thực tế, trong đó, khi một view được di chuyển từ một điểm tới điểm khác, nó sẽ tung lên hoặc dao động về phía điểm kết thúc trước khi hạ xuống vị trí. Dưới đây là block phương thức chúng ta sử dụng cho spring animations

UIView.animateWithDuration(_, delay:, usingSpringWithDamping:, initialSpringVelocity:, options:, animations:,
completion:)

Đoạn code trên cũng giống với các phương thức mà chúng ta đã biết đến ở phần trước nhưng nó có thêm 2 tham số mới là usingSpringWithDamping và initialSpringVelocity. Damping là một giá trị từ 0 tới 1 nó phát hiện độ nảy lên đến điểm kết thúc của animation. Càng gần giá trị 1 thì độ nảy càng ít. initialSpringVeloccity như bản thân tên của nó thì đây là giá trị khởi tạo tốc độ của animation. Đây là sức mạnh của animation khi bắt đầu giảm. Nếu bạn muốn nó bắt đầu mạnh mẽ thì cài đặt giá trị lớn, nếu bạn muốn một animation mịn thì bạn có thể cài đặt giá trị là 0.

Keyframe Animations

Keyframe animations cho phép bạn sử dụng một dạng animation khác. Bạn có thể nhóm các animation khác nhau lại cùng nhau chia sẻ một số thuộc tính chung, tuy vậy có thể điều khiển chúng một cách riêng rẽ. Thay cho việc một animation mà di chuyển dọc theo một đường thẳng, UIKit có thể thực hiện các giai đoạn khác nhau của một animation. Các APIs của Keyfram animation như sau

UIView.animateKeyframesWithDuration(_:, delay:, options:, animations:)
UIView.addKeyframeWithRelativeStartTime(_:, relativeDuration:)

Hai phương thức trên được sử dụng cùng nhau, phương thức thứ 2 lấy một nested từ animation closure của phương thức thứ nhất. Phương thức thứ nhất cài đặt cấu hình tổng thể cho animation, như việc là nó dài bao lâu, thời gian chờ đợi và các lựa chọn của nó. Sau đó bạn định nghĩa một hoặc nhiều phương thức thứ hai (the frames) trong animation closure để cài đặt các giai đoạn khoác nhau của animation. Mối quan hệ về thời điểm bắt đầu và khoảng thời gian của mỗi frame là một giá trị nằm giữa 0 và 1 nó thể hiện tỉ lệ phần trăm thời gian với tổng thời gian của animation.

View Transitions

view transition được sử dụng khi bạn muốn thêm một view mới vào hệ thống view của bạn hoặc xoá bỏ một view từ hệ thống view. Các APIs được sử dụng để tạo ra view transitions là:

UIView.transitionWithView(_:, duration:, options:, animations:, completion:)
UIView.transitionFromView(_:, toView:, duration:, options:, completion:)

Chúng ta sẽ sử dụng phương thức thứ nhất để thông báo 1 view với hệ thống view. Phương thức cũng có các tham số giống với các phương thức khác của animaiton. Phương thức thứ hai sử dụng để đặt một view trong hệ thống view vào vị trí của nó.

Ví dụ 1

Trong ví dụ này ta có một màn hình để đăng nhập vào ứng dụng bao gồm 2 textfield để người dùng có thể nhập username và password. Một button để thực hiện đăng nhập. Ở đây chúng ta sẽ thực hiện tạo các animaiton cho textfield và loginbutton khi hiển thị và khi người dùng bấm vào loginbutton. Với textfield chúng ta sẽ thực hiện để cho nó di chuyển từ bên phải của màn hình sang. Như thế thì khi màn hình login chuẩn bị xuất hiện thì các textfied này sẽ đang ở vị trí ngoài bên phải màn hình, ngoài ra khi màn hình login xuất hiện thì loginButton dần dần hiện lên có nghĩa là lúc chuẩn bị xuất hiện nó sẽ mờ. Để thực hiện điều này chúng ta cần căn chỉnh cho 2 textfield sang bên phải màn hình và loginButton có alpha bằng 0 trong hàm viewWillAppear. Chúng ta thực hiện override viewWillAppear như sau:

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    centerAlignPassword.constant -= view.bounds.awidth
    centerAlignUserName.constant -= view.bounds.awidth
    loginButton.alpha = 0.0
}

Tiếp sau đó trong hàm viewDidAppear thì chúng ta sẽ thực hiện animation là cho các textfield di chuyển về vị trí ban đầu và cho hiện loginButton lên như sau:

override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        UIView.animateWithDuration(0.5, delay: 0.0, options: UIViewAnimationOptions.CurveEaseOut, animations: {
            self.centerAlignUserName.constant += self.view.bounds.awidth
            self.centerAlignPassword.constant += self.view.bounds.awidth
            self.loginButton.alpha = 1
            self.view.layoutIfNeeded()
        }, completion: nil)
    }

Ở đây ta đã sử dụng phương thức UIView.animateWithDuration() mà chúng ta đã biết trong phần trên của bài viết. Ở đây chúng ta có thêm một options vào phương thức là UIViewAnimationOptions.CurveEaseOut nó có thể làm cho animation bắt đầu nhanh và chậm dần đến khi kết thúc. Animation này sẽ chạy trong 0.5s và bắt đầu ngay khi gọi hàm vì không có thời gian delay. Với đoạn code trên thì chúng ta đã tạo ra một animation đơn giản đã thấy thú vị hơn rất nhiều so với trạng thái tĩnh của nó. Nhưng ở đây thì tất cả các animation chuyển động với thời gian giống nhau thì nó chưa phải là một animation tốt. Chúng ta có thể sửa lại để chúng không chuyển động cùng thời gian để có được một animation tốt hơn. Tiếp theo chúng ta sẽ thực hiện animation cho loginButton khi mà nó bị failed. Thêm đoạn code sau vào phương thức login()

@IBAction func login(sender: AnyObject) {
        let bounds = self.loginButton.bounds
        UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.2,
            initialSpringVelocity: 10, options: nil, animations: {
                self.loginButton.bounds = CGRect(x: bounds.origin.x - 20, y: bounds.origin.y,
                    awidth: bounds.size.awidth + 60, height: bounds.size.height)
                self.loginButton.enabled = false
            }, completion: nil)
    }

Ví dụ 2

Trong ứng dụng của bạn thì bạn có thể thay ảnh nền của view tự động khi có một số hành động xảy ra. Bạn có thể thực hiện nó bằng cách thay ảnh này bằng ảnh khác trên view hoặc có thể để nó từ từ nhỏ lại, ... là có thể tạo được một animation đẹp. Ở đây chúng ta sẽ tạo ra hiệu ứng này bằng cách thêm đoạn code sau vào file ExampleIIViewController.swift như sau:

	override func viewWillAppear(animated: Bool) {
        let firstImageView = UIImageView(image: UIImage(named: "bg01.png"))
        firstImageView.frame = view.frame
        view.addSubview(firstImageView)
        imageFadeIn(firstImageView)
    }

    func imageFadeIn(imageView: UIImageView) {
        let secondImageView = UIImageView(image: UIImage(named: "bg02.png"))
        secondImageView.frame = view.frame
        secondImageView.alpha = 0.0
        view.insertSubview(secondImageView, aboveSubview: imageView)
        UIView.animateWithDuration(2.0, delay: 2.0, options: .CurveEaseOut, animations: {
            secondImageView.alpha = 1.0
            }, completion: {_ in
                imageView.image = secondImageView.image
                secondImageView.removeFromSuperview()
        })
    }

Ở đây chúng ta thực hiện tạo một image View và thêm nó vào view chính. Sau đó chúng ta gọi phương thức imageFadeIn() để tạo một cái view thứ 2 với 1 bức ảnh khác. Chúng ta thêm vào view trên image View thứ nhất và để giá trị alpha của nó bằng 0. Trong block animation chúng ta sẽ để cho giá trị của alpha thay đổi. Ban đầu nó là 0 thì ảnh sẽ bị ẩn đi. Sau đó trong trong complete closure thì chúng ta để vào image View là cái ảnh thứ 2 và xoá bỏ image View thứ 2 từ trong hệ thống View vì nó không còn cần thiết nữa.

Ví dụ 3

Tiếp theo chúng ta sẽ làm quen với keyframe đã giới thiệu ở bên trên.

	var alerView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        createView()
    }

    func createView() {
        let alertWidth: CGFloat = view.bounds.awidth
        let alertHeight: CGFloat = view.bounds.height
        let alertViewFrame: CGRect = CGRectMake(0, 0, alertWidth, alertHeight)
        alerView = UIView(frame: alertViewFrame)
        alerView.backgroundColor = UIColor.redColor()

        let imageView = UIImageView(frame: CGRectMake(0, 0, alertWidth, alertHeight / 2))
        imageView.image = UIImage(named: "bike_traveler.png")
        alerView.addSubview(imageView)

        let button = UIButton.buttonWithType(UIButtonType.System) as! UIButton
        button.setTitle("Dismiss", forState: UIControlState.Normal)
        button.backgroundColor = UIColor.whiteColor()
        let buttonWidth: CGFloat = alertWidth / 2
        let buttonHeight: CGFloat = 40
        button.frame = CGRectMake(alerView.center.x - buttonWidth / 2,
            alerView.center.y - buttonHeight / 2, buttonWidth, buttonHeight)
        button.addTarget(self, action: Selector("dismissAlert"),
        forControlEvents: UIControlEvents.TouchUpInside)
        alerView.addSubview(button)
        view.addSubview(alerView)
    }

    func dismissAlert() {}

Tiếp theo chúng ta sẽ để cho alertView co lại và đi ra ngoài. Khi chúng ta tạo ra một button thì cần có thêm listener để lắng nghe sự kiện từ nó. Hàm dismissAlert là hàm được gọi khi button được tap vào. Chúng ta sẽ thêm đoạn code sau vào dismissAlert

	func dismissAlert() {
        let bounds = alerView.bounds
        let smallFrame = CGRectInset(alerView.frame, alerView.frame.size.awidth / 4,
            alerView.frame.size.height / 4)
        let finalFrame = CGRectOffset(smallFrame, 0, bounds.size.height)
        UIView.animateKeyframesWithDuration(4, delay: 0, options: .CalculationModeCubic, animations:{
            UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 0.5) {
                self.alerView.frame = smallFrame
            }
            UIView.addKeyframeWithRelativeStartTime(0.5, relativeDuration: 0.5) {
                self.alerView.frame = finalFrame
            }
        }, completion: nil)
    }

Ở phần code trên chúng ta tạo ra một khung mà chúng ta sẽ sử dụng trong hai giai đoạn của animation. smallFrame co lại đến bằng nửa size của alertView, khi đó duy trì điểm trung tâm và finalFrame có một vị trí ở dưới cùng cảu màn hình và thoát ra ngoài. Chúng ta sử dụng keyframe animation với 2 keyframe. Cái đầu tiên là frame của alertView tới smallFrame và cái thứ hai đến finalFrame. Kết quả là alertView sẽ co lại đến nửa kích thước của nó và đi ra ngoài view. Chú ý ở đây chúng ta để duration là 4 thì chúng ta có thể thay đổi tham số này nếu chúng ta muốn. Với animation này thì chúng ta thấy là animation của alertView khá ổn nhưng các thành phần con của nó lại không co dãn theo khung của alertView như thế nhìn không được hợp lý cho lắm. Việc thay đổi khung của cha thì không tự động thay đổi khung của con. Chúng ta cso thể sử dụng tính năng mới được giới thiệu ở iOS7 gọi là UIView snappshots để cải thiện animaiton này. Nó cho phép chúng ta lấy 1 snapshot của một UIView cùng với hệ thống của nó để tạo ra một UIView mới vào trong nó. Thêm vào hàm dismissAlert() đoạn code sau vào trước phần code keyframe

	let snapshot = alerView.snapshotViewAfterScreenUpdates(false)
    snapshot.frame = alerView.frame
    view.addSubview(snapshot)
    alerView.removeFromSuperview()

Ở đây chúng ta tạo ra một snapshot view và thêm nó vào view chính. Sau đó chúng ta xoá bỏ alertView từ view như là để snapshot sẽ thay thế nó. Sửa lại phần code animation của keyframe như sau

		UIView.animateKeyframesWithDuration(4, delay: 0, options: .CalculationModeCubic, animations:{
            UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 0.5) {
                self.alerView.frame = smallFrame
            }
            UIView.addKeyframeWithRelativeStartTime(0.5, relativeDuration: 0.5) {
                self.alerView.frame = finalFrame
            }
        }, completion: nil)

như thế chúng ta được một animation có vẻ tốt hơn animation trước.

Ví dụ 4

Ở các ví dụ trên chúng ta đã làm các animation với các view đơn giản thì trong ví dụ này chúng ta sẽ làm animation với tableView. Chúng ta chỉ cần thêm đoạn code đơn giản sau

	override func viewWillAppear(animated: Bool) {
        animateTable()
    }

    func animateTable() {
        tableView.reloadData()

        let cells = tableView.visibleCells()
        let tableHeight: CGFloat = tableView.bounds.size.height

        for i in cells {
            let cell: UITableViewCell = i as! UITableViewCell
            cell.transform = CGAffineTransformMakeTranslation(0, tableHeight)
        }
        var index = 0
        for a in cells {
            let cell:UITableViewCell = a as! UITableViewCell
            UIView.animateWithDuration(1.5, delay: 0.05 * Double(index),
                usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: nil,
                animations:{
                    cell.transform = CGAffineTransformMakeTranslation(0, 0)
            }, completion: nil)
            index += 1
        }
    }

Khi mà view xuất hiện thì hàm animateTable() được gọi. Tableview sẽ reloaData, và vòng lặp chạy qua các cell mà hiển thị trên screen và di chuyển mỗi chúng trong đến cuối của màn hình. Sau đó animation thực hiện đưa các cell về vị trí đúng của nó với spring animation.

0