Tạo placeholder loading animation giống Facebook, Youtube (Phần 1)
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 ...
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:
- https://cloudcannon.com/deconstructions/2014/11/15/facebook-content-placeholder-deconstruction.html
- https://viblo.asia/p/tao-loading-timeline-giong-facebook-QpmleQO7lrd
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: CGPoint và endPoint: 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) và endPoint = (0.5, 1)
Chúng ta có thể hình dung đơn giản như sau:
Ví dụ gradient layer có awidth = 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 startPoint và endPoint. 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.
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, awidth: view.bounds.awidth, 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, awidth: view.bounds.awidth, 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.
(TO BE CONTINUED)