0

Circular Image Loader Animation

Trong bài viết này sẽ hướng dẫn bạn cách tạo ra hiệu ứng chuyển động loading bằng Swift và Core Animation.

Getting Started

  Đầu tiên hay download  [Source code](https://goo.gl/uITz1P)  này về. Sau khi build một vài giây bạn sẽ thấy hiển thị hình ảnh đơn giản :
  
  ![](https://images.viblo.asia/94cf11b3-2dee-450d-b2f0-7c0b59c62802.png)
  
  Chức năng của bản demo này sẽ thực hiện việc load image từ URL. Nhìn vào source code chúng ta có thể thấy class CustomImageView - subclass của UIImageView, sử dụng phương thức của thư viện SDWebImage để thực hiện chức năng này. Khi lần đầu chạy app, chúng ta có thể dễ dàng nhận thấy rằng, app sẽ bị dừng lại trong 1 vài giây trong khi SDWebImage thực hiện download image, và sau đó image mới hiện thị lên screen. Về mặt trải nghiệm người dùng thì nó khá là nhàm chán. Cũng giống như bài viết trước đây  [Viblo](https://viblo.asia/ThanhTa/posts/DZrGNDwdkVB)  để cải thiện UX chúng ta sẽ thực hiện thêm progress indicator dạng vòng tròn.
  Chúng ta sẽ tạo animation với 2 phase khác nhau :
  - Circular progress : đầu tiên, bạn sẽ vẽ circular progress indicator và nó dựa trên progress của việc download.
  - Expanding circular image : sau đó bạn sẽ thực hiện tạo hiệu ứng mở ảnh sau khi download.

Creating the Circular Indicator

Progess indicator ban đầu sẽ được khởi tạo rỗng với value 0%, sau đó sẽ được fill đầy khi image đã được download. Điều đó khá đơn giản khi bạn sử dụng CAShapeLayer. Class này cung cấp 2 properties : stroke Start và strokeEnd. Bằng cách gán gía trị từ 0 tới 1 cho strokeEnd, tương tự với việc download từ 0% tới 100% bạn sẽ có điều mong muốn. Dĩ nhiên bên cạnh đó chúng ta sẽ phải sử dụng các thuộc tính khác để vẽ đường tròn, ví dụ như Stroke. Tạo class CircularLoaderView - subclass của UIView.

Tại đây chúng ta sẽ khởi tạo instance của CAShapeLayer

   let circlePathLayer = CAShapeLayer()
   let circleRadius: CGFloat = 20.0

circlePathLayer sẽ thể hiện đường tròn, trong đó circleRadius là bán kính của nó.

   override init(frame: CGRect) {
  super.init(frame: frame)
  configure()
  }
 
  required init(coder aDecoder: NSCoder) {
  super.init(coder: aDecoder)
  configure()
}
 
func configure() {
  circlePathLayer.frame = bounds
  circlePathLayer.lineWidth = 2
  circlePathLayer.fillColor = UIColor.clearColor().CGColor
  circlePathLayer.strokeColor = UIColor.redColor().CGColor
  layer.addSublayer(circlePathLayer)
  backgroundColor = UIColor.whiteColor()
}

Cả 2 phương thức khởi tạo đều gọi phương thức configure. Phương thức này sẽ thiết lập giá trị cho circlePathLayer, tuỳ theo mong muốn về hình dạng của đường tròn mà bạn sẽ truyền những giá trị khác nhau cho nó.

Adding the Path

Tiếp theo chúng ta sẽ thiết lập path cho circlePathLayer để có 1 đường tròn. Đầu tiên cần khởi tạo frame :

  func circleFrame() -> CGRect {
  var circleFrame = CGRect(x: 0, y: 0, width: 2*circleRadius, height: 2*circleRadius)
  circleFrame.origin.x = CGRectGetMidX(circlePathLayer.bounds) - CGRectGetMidX(circleFrame)
  circleFrame.origin.y = CGRectGetMidY(circlePathLayer.bounds) - CGRectGetMidY(circleFrame)
  return circleFrame
}

Phương thức trên sẽ trả ra 1 hình chữ nhật có cạnh gấp 2 lần bán kính đường tròn, và tâm trùng với tâm đường tròn. Bạn cần tính toán lại circleFrame khi mỗi lần size của view thay đổi, vì vậy bạn nên sử dụng lại giá trị bound cuả nó để tính toán. Thiết lập path cho layer :

  func circlePath() -> UIBezierPath {
  return UIBezierPath(ovalInRect: circleFrame())
}

Phương thức trên đơn giản trả ra đường tròn - UIBezierPath được bao quanh bởi hình chữ nhật circleFrame. Khi circleFrame trả ra 1 hình vuông, thì oval sẽ là 1 đường tròn. Layer không có thuộc tính autoresizingMask như UIView, bạn nên cập nhật lại gía trị frame của circlePathLayer trong phương thức layoutSubView.

  override func layoutSubviews() {
  super.layoutSubviews()
  circlePathLayer.frame = bounds
  circlePathLayer.path = circlePath().CGPath
}

OK, bây giờ bạn file CustomeImageView.swift để thêm đường tròn mà mình đã tạo ở trên

  let progressIndicatorView = CircularLoaderView(frame: CGRectZero)
  

Thêm câu lệnh vào trong function init(coder:)

addSubview(self.progressIndicatorView)
progressIndicatorView.frame = bounds
progressIndicatorView.autoresizingMask = .FlexibleWidth | .FlexibleHeight

Build và RUn project, bạn sẽ thấy đường tròn được hiển thị

Modifying the Stroke Length

Tiếp theo chúng ta sẽ tạo chuyển động cho vòng. Quay trở lại đầu file CircularLoaderView.swift và thêm những dòng lệnh sau :

  var progress: CGFloat {
  get {
    return circlePathLayer.strokeEnd
  }
  set {
    if (newValue > 1) {
      circlePathLayer.strokeEnd = 1
    } else if (newValue < 0) {
      circlePathLayer.strokeEnd = 0
    } else {
      circlePathLayer.strokeEnd = newValue
    }
  }
}

Như đã giải thích ở trên, CAShapeLayer sẽ có 2 thuộc tính strokeStart và strokeEnd để thay đổi Path của layer. Khi giá trị progress bị thay đổi thì giá trị của strokeEnd sẽ thay đổi tương ứng. Khi strokeEnd = 0 thì bạn sẽ không nhìn thấy đường tròn, giá trị strokeEnd càng gần về 1 thì đường tròn sẽ dần được hiển thị trọn vẹn. Như vậy chúng ta chỉ cần tính toán giá trị progress dựa trên size dữ liệu đã nhận về so với size của file ảnh. Mở file CustomImageView.swift và thêm câu lệnh

self!.progressIndicatorView.progress = CGFloat(receivedSize)/CGFloat(expectedSize)

Build và Run project bạn sẽ thấy progress indicator dạng đường tròn 😄

Creating the Reval Animation

Mặc dù bạn đã tạo được progres indicator như mong muốn, nhưng CoreAnimation còn có thể giúp app của bạn cải thiện được nhiều hơn thế. Thay vì sẽ hiển thị ngày hình ảnh sau khi hoàn thành loading. Thì ta có thể có thể tạo hiệu ứng mở dần ảnh theo dạng nhìn xuyên qua hình tròn. Mở file CircularLoaderView.swift

  func reveal() {
 
  // 1
  backgroundColor = UIColor.clearColor()
  progress = 1
  // 2
  circlePathLayer.removeAnimationForKey("strokeEnd")
  // 3
  circlePathLayer.removeFromSuperlayer()
  superview?.layer.mask = circlePathLayer
}

Chúng ta sẽ giải thích qua đoạn code trên:

  1. Clear background color để hình anh rbeen dưới không bị che lấp bởi bất kì 1 view nào.
  2. Xoá bỏ bất kì 1 animation không rõ ràng nào đối với key strokeEnd.
  3. Xoas circlePathLayer khoir superLayer vaf assign nos cho "mask" cuar superView.Layer, vif vaayj iamge sẽ xuất hiện từ bên trong circle. Việc gán như vậy sẽ giúp bạn sử dụng lại layer có sẵn và tránh duplicate code.

Bây giờ bạn chỉ cần gọi function reveal() trong file CustomImageView.swift : self!.progressIndicatorView.reveal() Build và Run app, bạn sẽ thấy thành quả của mình.

CoreAnimation sẽ có nhiều ứng dụng hơn nữa, hẹn gặp lại tại những chủ đề tiếp theo.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.