Tạo placeholder loading animation giống Facebook, Youtube (Phần 1)

Introduction

Trong các ứng dụng iOS, mỗi khi cần báo hiệu cho người dùng biết rằng đang chờ một thao tác tốn thời gian nào đó ví dụ như: loading data từ server hoặc từ local database... chúng ta thường hiển thị animation loading dạng xoáy tròn.
Đơn giản nhất là dùng UIActivityIndicatorView mặc định của Apple, hoặc sử dụng các thư viện loading animation khác như MBProgressHUD, SVProgressHUD...
Tuy nhiên các loading animation này đôi khi khá đơn điệu và không phù hợp cho một số dạng UI biểu diễn thông tin dạng list, grid chứa nhiều hình ảnh.
Thậm chí một số khi hiện xoáy tròn còn block cả thao tác vuốt trên màn hình, khiến người dùng cảm thấy khó chịu.

Trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu về CAGradientLayer, CABasicAnimation để tạo placeholder cho view, tableview, collection view có loading animation giống app Facebook iOS, Youtube iOS.

Hiệu ứng này sẽ giúp cải thiện loading UI/UX. Các bạn front end web có thể tham khảo references sau:

Understand CAGradientLayer

CAGradientLayer cho phép chúng ta tạo các layer dạng gradient, màu sẽ được fill và chuyển đổi đều dựa trên sự phân bố tỉ lệ cho trước.
Tạo một CAGradientLayer rất đơn giản, chúng ta chỉ cần init, set frame, colors (CGColor), locations... cho nó và add vào layer của một view:

    func createGradientLayer() {
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame = view.bounds
        gradientLayer.colors = [
            UIColor.red.cgColor,
            UIColor.blue.cgColor,
            UIColor.yellow.cgColor
        ]
        view.layer.addSublayer(gradientLayer)
    }

Mảng colors có kiểu var colors: [Any]? { get set } nhưng bạn bắt buộc phải truyền vào mảng các CGColor. Nếu bạn truyền vào mảng UIColor, compiler sẽ không báo lỗi nhưng gradient layer của bạn sẽ không hiển thị màu nào cả.

Kết quả:

startPoint, endPoint

startPoint: CGPointendPoint: CGPoint chỉ định điểm bắt đầu và kết thúc của gradient layer.
Giá trị mặc định của startPoint = (0.5, 0)endPoint = (0.5, 1) Chúng ta có thể hình dung đơn giản như sau:

Ví dụ gradient layer có width = 400, height = 300.

  • startPoint = (0.5, 0) -> Điểm bắt đầu trong hệ tọa độ của layer bằng (0.5 * 400, 0 * 300) = (200, 0). endPoint = (0.5, 1) -> Điểm kết thúc trong hệ tọa độ của layer bằng (0.5 * 400, 1 * 300) = (200, 300).
  • startPoint = (0.25, 0,3) -> Điểm bắt đầu trong hệ tọa độ của layer bằng (0.25 * 400, 0.3 * 300) = (100, 100). endPoint = (1, 0.5) -> Điểm kết thúc trong hệ tọa độ của layer bằng (1 * 400, 0.5 * 300) = (400, 150).
  • startPoint = (-0.3, 0) -> Điểm bắt đầu trong hệ tọa độ của layer bằng (-0.5 * 400, 0 * 300) = (-120, 0). endPoint = (1.2, 0.15) -> Điểm kết thúc trong hệ tọa độ của layer bằng (1.2 * 400, 0.15 * 300) = (480, 45).

Từ 2 điểm bắt đầu và kết thúc xác định được trong hệ tọa độ của bản thân layer đó, hệ thống sẽ kẻ 2 đường thằng vuông góc với đường nối giữa startPointendPoint. Hai đường thằng này song song với nhau và xác định vùng gradient. Phần không nằm trong vùng gradient sẽ được draw màu alpha 100% của màu bắt đầu hoặc kết thúc tương ứng.

startPoint(0.2, 0) endPoint(0.5, 0) startPoint(0.25, 0.3) endPoint(1, 0.5) startPoint(-0.3, 0) endPoint(1.2, 0.15)

locations

An optional array of NSNumber objects defining the location of each gradient stop. Animatable.

locations là một property của CAGradientLayer, một mảng các giá trị NSNumber chỉ định mỗi màu trong mảng colors được draw đậm nhất ở tọa độ nào trước khi fade dần sang màu tiếp theo. locations có thể animate.

Khi không set locations, các màu sẽ được fill đều. Như ví dụ trên có 3 màu đỏ, xanh dương, vàng và locations bằng nil thì mặc định màu đỏ sẽ được draw đầu tiên và đậm nhất ở 0, màu xanh dương sẽ đậm nhất ở 0.5 rồi chuyển dần sang màu vàng ở vị trí 1.
Trên document của Apple có nói rằng giá trị trong mảng location có giá trị trong khoảng từ 0 đến 1 và phải tăng dần. Tuy nhiên thực tế thì các giá trị này có thể nằm ngoài khoảng [0, 1].
Ví dụ trường hợp:

    gradientLayer.startPoint = CGPoint(x: 0, y: 0)
    gradientLayer.endPoint = CGPoint(x: 1, y: 0)
    gradientLayer.locations = [-0.5, 0.3, 1.5]

Có thể thấy rằng màu đỏ đậm nhất ở vị trí -0.5 nằm bên ngoài màn hình bên trái rồi chuyển dần sang màu xanh dương, đậm nhất ở vị trí 0.3. Màu vàng đậm nhất ở 1,5 bên ngoài màn hình bên phải nên ta chỉ nhìn thấy một chút màu vàng ở rìa phải màn hình do quá trình màu xanh dương fade dần.

Make placeholder loading animation

Sau khi đã nắm rõ được CAGradientLayer, hãy áp dụng nó để tạo hiệu ứng loading animation của Facebook. Chúng ta sẽ cần thêm một chút animation đến từ CoreAnimation, cụ thể là CABasicAnimation.
Thêm đoạn code sau vào function viewDidLoad():

    override func viewDidLoad() {
        super.viewDidLoad()
        let myView = UIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 200))
        view.addSubview(myView)
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame = myView.bounds
        gradientLayer.colors = [
            backgroundGray.cgColor,
            lightGray.cgColor,
            darkGray.cgColor,
            lightGray.cgColor,
            backgroundGray.cgColor
        ]
        gradientLayer.startPoint = CGPoint(x: -0.85, y: 0)
        gradientLayer.endPoint = CGPoint(x: 1.15, y: 0)
        gradientLayer.locations = [-0.85, -0.85, 0, 0.15, 1.15]
        myView.layer.addSublayer(gradientLayer)
    }

với 3 màu constant:

    let backgroundGray = UIColor(red: 246.0 / 255, green: 247 / 255, blue: 248 / 255, alpha: 1)
    let lightGray = UIColor(red: 238.0 / 255, green: 238 / 255, blue: 238 / 255, alpha: 1)
    let darkGray = UIColor(red: 221.0 / 255, green: 221 / 255, blue: 221 / 255, alpha: 1)

Để tạo ra hiệu ứng chuyển màu từ trái sang phải, chúng ta sẽ animate giá trị locations từ [-0.85, -0.85, 0, 0.15, 1.15] sang [0, 1, 1, 1.05, 1.15].
Code hoàn chỉnh như sau:

    override func viewDidLoad() {
        super.viewDidLoad()
        let myView = UIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 200))
        view.addSubview(myView)
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame = myView.bounds
        gradientLayer.colors = [
            backgroundGray.cgColor,
            lightGray.cgColor,
            darkGray.cgColor,
            lightGray.cgColor,
            backgroundGray.cgColor
        ]
        gradientLayer.startPoint = CGPoint(x: -0.85, y: 0)
        gradientLayer.endPoint = CGPoint(x: 1.15, y: 0)
        gradientLayer.locations = [-0.85, -0.85, 0, 0.15, 1.15]
        // Khởi tạo CABasicAnimation với keyPath muốn animate là `locations`
        let animation = CABasicAnimation(keyPath: "locations")
        // Giá trị `locations` bắt đầu animate
        animation.fromValue = gradientLayer.locations
        // Giá trị `locations` kết thúc animate
        animation.toValue = [0, 1, 1, 1.05, 1.15]
        // Lặp animation vô hạn
        animation.repeatCount = .infinity
        animation.fillMode = kCAFillModeForwards
        animation.isRemovedOnCompletion = false
        animation.duration = 1
        // Add animation cho gradient layer
        gradientLayer.add(animation, forKey: "what.ever.it.take")
        myView.layer.addSublayer(gradientLayer)
    }

Kết quả, ta được hiệu ứng chuyển động màu xám:

Bài viết đã khá dài mình xin tạm dừng ở đây, phần tiếp theo chúng ta sẽ hoàn thiện nốt hiệu ứng này cho tableView, collectionView. Và một vài hiệu ứng khác dựa trên gradient layer này.

PHẦN 2: https://viblo.asia/p/tao-placeholder-loading-animation-giong-facebook-youtube-phan-2-OeVKBy0E5kW