Tạo màn hình Splash của Uber
Màn hình Splash ấn tượng sẽ tạo trải nghiệm thú vị cho người dùng. Thay vì phải nhìn một màn hình tĩnh, thì ta có thể tạo ra một giao diện thân thiện hơn như là animation(loanding, chuyển dộng của character...). Trong bài hướng dẫn này chúng ta sẽ tạo ra 1 animation cho màn hình Splash giống như ...
Màn hình Splash ấn tượng sẽ tạo trải nghiệm thú vị cho người dùng. Thay vì phải nhìn một màn hình tĩnh, thì ta có thể tạo ra một giao diện thân thiện hơn như là animation(loanding, chuyển dộng của character...). Trong bài hướng dẫn này chúng ta sẽ tạo ra 1 animation cho màn hình Splash giống như giao diện của ứng dụng Uber.
Dó có khá nhiều animations sẽ được thực hiện trong bài viết này, nên chúng ta sẽ bắt đầu với một project đã được tạo sẵn đã bao gồm CALayers. Donwnload here
Project ở trên cso tên là Fuber. Fuber là một dịch vụ chia sẻ xe theo yêu cầu, cho phép hành khách yêu cầu tài xê (Segway) để di chuyển tới những địa điểm khác nhau trong môi trường đô thị. Fuber được phát triển nhanh chóng và hiện tại đã được áp dụng trên 60 quốc gia, nhưng có những trở ngại nhất định do hợp đồng giữa Segway và Fuber vì liên quan tới chính sách của mỗi quốc gia.
Thông qua bài hướng dẫn này, chúng ta sẽ tạo animation cho màn hình Splash giống như:
Mở và run Fuber project và chú ý. Từ UIViewController, app mở SplashViewController từ view-controller cha, RootContainerViewControlller. Animation của màn hình Splash sẽ lặp đi lặp lại cho tới khi app sẵn sàng được sử dụng. Trong thời gian đó sẽ app sẽ call API để lấy dữ liệu cần thiết.
Có 2 phương thức được khai báo trong RootContainerViewController: showSplashViewController() và showSplashViewControllerNoPing(). Chúng ta sẽ gọi showSplashViewControllerNoPing() chỉ để lặp thông qua animations, nên chỉ cần tập trung vào cách tạo chuyển động được khai báo trong SplashViewController và sau đó sẽ sử dụng showSplashViewController() để minh hoạ delay khi gọi API và chuyển tới màn hình chính.
SplashViewController gồm 2 subview. Một subview gồm "ripple grid"- TileGridView, sẽ gồm ô kẻ-TileView. Subview còn lại chuyển dộng theo chữ "U" - AnimatedUlogoView.
AnimatedULogoView bao gồm 4 đối tượng CAShapeLayer:
- circleLayer: diễn tả nền hình tròn màu trắng của chữ "U".
- lineLayer: là đường thẳng phân cách giữa circleLayer tới mép ngoài cuả nó.
- squareLayer: là hình vuông giữa circleLayer.
- maskLayer: được sử dụng để tạo mặn nạ. Nó được sử dụng để đóng tất cả những layer khác.
Bây giờ bạn đã biết rõ những thần phần được kết hợp tạo ra animation.
Animating the Circle
Trong class AnimatedULogoView.swift , init(frame:), comment tất cả các lớp con được thêm vào ngoại trừ circleLayer. Bạn sẽ thêm chúng trở lại trong một lúc một thời gian khi bạn hoàn thành các hình ảnh động. Code bây giờ sẽ giống như thế này:
override init(frame: CGRect) { super.init(frame: frame) circleLayer = generateCircleLayer() lineLayer = generateLineLayer() squareLayer = generateSquareLayer() maskLayer = generateMaskLayer() // layer.mask = maskLayer layer.addSublayer(circleLayer) // layer.addSublayer(lineLayer) // layer.addSublayer(squareLayer) }
Tìm generateCircleLayer()hiểu làm thế nào các vòng tròn được tạo ra. Đó là một CAShapeLayer đơn giản tạo ra với một UIBezierPath. Hãy chú ý đến dòng này:
layer.path = UIBezierPath(arcCenter: CGPointZero, radius: radius/2, startAngle: -CGFloat(M_PI_2), endAngle: CGFloat(3*M_PI_2), clockwise: true).CGPath
Theo mặc định và nếu startAngle = 0, cung Bezier bắt đầu từ bên phải(vị trí số 3 trên đồng hồ). Bằng cách cung cấp -M_PI_2 đó là -90 độ, bạn bắt đầu từ đầu và endAngle sẽ là 270 độ hay 3*M_PI_2 mà lại là hàng đầu của vòng tròn. Cũng chú ý rằng bởi vì bạn muốn animate stroke, bạn đang sử dụng giá trị radius bằng lineWidth. circleLayer cần phải được bao gồm ba CAAnimations: một CAKeyframeAnimation sinh động một strokeEnd, một CABasicAnimation sinh động một transformvà một CAAnimationGroup dùng để bọc hai cùng nhau. Bạn sẽ tạo ra những hình ảnh động cùng một lúc. Điều hướng đến animateCircleLayer() thêm những điều sau đây:
// strokeEnd let strokeEndAnimation = CAKeyframeAnimation(keyPath: "strokeEnd") strokeEndAnimation.timingFunction = strokeEndTimingFunction strokeEndAnimation.duration = kAnimationDuration - kAnimationDurationDelay strokeEndAnimation.values = [0.0, 1.0] strokeEndAnimation.keyTimes = [0.0, 1.0]
Bằng cách cung cấp 0.0 và 1.0 còn hình ảnh của values bạn hướng dẫn khung Core Animation để bắt đầu từ startAngle và đột quỵ vòng tròn cho đến endAngle, tạo sự mát mẻ "đồng hồ giống như" hình ảnh động. Vì vậy, giá trị của strokeEnd tăng, chiều dài của đoạn đường dọc theo chu vi tăng lên, và vòng tròn đang dần "điền". Đối với ví dụ này, nếu bạn đã thay đổi các valuesthuộc tính để [0.0, 0.5], chỉ có một nửa của vòng tròn sẽ được rút ra bởi vì các strokeEnd sẽ chỉ đạt được một nửa chiều xung quanh chu vi của hình tròn trong các hình ảnh động. Bây giờ thêm các animation:
// transform let transformAnimation = CABasicAnimation(keyPath: "transform") transformAnimation.timingFunction = strokeEndTimingFunction transformAnimation.duration = kAnimationDuration - kAnimationDurationDelay var startingTransform = CATransform3DMakeRotation(-CGFloat(M_PI_4), 0, 0, 1) startingTransform = CATransform3DScale(startingTransform, 0.25, 0.25, 1) transformAnimation.fromValue = NSValue(caTransform3D: startingTransform) transformAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity)
Animation này hoạt động cả một quy mô biến đổi và luân chuyển quanh trục z. Điều này dẫn đến các circleLayer ngày càng tăng trong khi xoay chiều kim đồng hồ 45 độ. Vòng xoay này là quan trọng bởi vì nó cần phải phù hợp với vị trí và tốc độ của lineLayerkhi nó được hoạt hình cùng với phần còn lại của lớp.
Cuối cùng, thêm một CAAnimationGroup xuống đáy animateCircleLayer(). Animation này gói gọn trong hai hình ảnh động trước, do đó bạn chỉ cần thêm một hình ảnh động để các circleLayerlớp.
// Group let groupAnimation = CAAnimationGroup() groupAnimation.animations = [strokeEndAnimation, transformAnimation] groupAnimation.repeatCount = Float.infinity groupAnimation.duration = kAnimationDuration groupAnimation.beginTime = beginTime groupAnimation.timeOffset = startTimeOffset circleLayer.add(groupAnimation, forKey: "looping")
CAAnimationGroup này có hai đặc tính đáng chú ý đang được sửa đổi: beginTimevà timeOffset. Nếu bạn không quen với một trong hai, một mô tả tuyệt vời của các tài sản và làm thế nào họ đang sử dụng có thể tìm thấy ở đây . Đây groupAnimation's beginTimeđang được đặt trong tài liệu tham khảo đến thời gian xem cha mẹ của nó. Việc timeOffsetcần thiết vì các hình ảnh động thực sự bắt đầu nửa chừng trên chạy đầu tiên của nó. Khi bạn có nhiều hình ảnh động hoàn tất, hãy thử thay đổi giá trị của startTimeOffsetvà quan sát sự khác biệt trực. Thêm groupAnimation tới circleLayer, sau đó xây dựng và chạy các ứng dụng để xem những gì nó trông giống như.
Animating the Line
Với các hình ảnh động của circleLayerhoàn chỉnh, đó là thời gian để giải quyết lineLayer's hình ảnh động. Trong khi vẫn còn trong AnimatedULogoView.swift , điều hướng đến startAnimating()và nhận xét ra tất cả các cuộc gọi đến hiệu ứng động phương pháp ngoại trừ animateLineLayer(). Kết quả sẽ trông giống như các mã dưới đây:
public func startAnimating() { beginTime = CACurrentMediaTime() layer.anchorPoint = CGPointZero // animateMaskLayer() // animateCircleLayer() animateLineLayer() // animateSquareLayer() }
Ngoài ra, thay đổi nội dung trong init(frame:)do đó circleLayervà lineLayerlà CALayers chỉ được sử dụng:
override init(frame: CGRect) { super.init(frame: frame) circleLayer = generateCircleLayer() lineLayer = generateLineLayer() squareLayer = generateSquareLayer() maskLayer = generateMaskLayer() // layer.mask = maskLayer layer.addSublayer(circleLayer) layer.addSublayer(lineLayer) // layer.addSublayer(squareLayer) }
Với CALayers / hình động đúng nhận xét ra, đi đến animateLineLayer()và thực hiện các nhóm tiếp theo của hình ảnh động:
// lineWidth let lineWidthAnimation = CAKeyframeAnimation(keyPath: "lineWidth") lineWidthAnimation.values = [0.0, 5.0, 0.0] lineWidthAnimation.timingFunctions = [strokeEndTimingFunction, circleLayerTimingFunction] lineWidthAnimation.duration = kAnimationDuration lineWidthAnimation.keyTimes = [0.0, (1.0 - (kAnimationDurationDelay / kAnimationDuration)) as NSNumber, 1.0]
Hoạt hình này có trách nhiệm tăng sau đó giảm lineLayercủa chiều rộng. Đối với các hình ảnh động tiếp theo, thêm vào như sau:
// transform let transformAnimation = CAKeyframeAnimation(keyPath: "transform") transformAnimation.timingFunctions = [strokeEndTimingFunction, circleLayerTimingFunction] transformAnimation.duration = kAnimationDuration transformAnimation.keyTimes = [0.0, 1.0 - (kAnimationDurationDelay/kAnimationDuration) as NSNumber, 1.0] var transform = CATransform3DMakeRotation(-CGFloat(M_PI_4), 0.0, 0.0, 1.0) transform = CATransform3DScale(transform, 0.25, 0.25, 1.0) transformAnimation.values = [NSValue(caTransform3D: transform),NSValue(caTransform3D: CATransform3DIdentity), NSValue(caTransform3D: CATransform3DMakeScale(0.15, 0.15, 1.0))]
Giống như circleLayerhình ảnh động chuyển đổi, ở đây bạn đang xác định một đồng hồ luân phiên về các trục z. Đối với các dòng, tuy nhiên, bạn cũng thực hiện một quy mô 25% biến đổi, nhanh chóng theo sau bởi một danh tính chuyển đổi trước khi một mô thức đến 15% kích thước ban đầu của nó. Nhóm các hình ảnh động với nhau bằng cách sử dụng CAAnimationGroupvà thêm nó vào lineLayer:
// Group let groupAnimation = CAAnimationGroup() groupAnimation.repeatCount = Float.infinity groupAnimation.isRemovedOnCompletion = false groupAnimation.duration = kAnimationDuration groupAnimation.beginTime = beginTime groupAnimation.animations = [lineWidthAnimation, transformAnimation] groupAnimation.timeOffset = startTimeOffset lineLayer.add(groupAnimation, forKey: "looping")
Animating the Square
Việc khoan nên việc quen thuộc của bây giờ. Đến startAnimating()và nhận xét ra các cuộc gọi phương thức hoạt hình trừ animateSquareLayer(). Ngoài ra, thay đổi init(frame:)để nó trông như thế này:
override init(frame: CGRect) { super.init(frame: frame) circleLayer = generateCircleLayer() lineLayer = generateLineLayer() squareLayer = generateSquareLayer() maskLayer = generateMaskLayer() // layer.mask = maskLayer layer.addSublayer(circleLayer) // layer.addSublayer(lineLayer) layer.addSublayer(squareLayer) }
Sau khi thực hiện, đi qua animateSquareLayer()và nhận được nứt trên các tập tiếp theo của hình ảnh động:
// bounds let b1 = NSValue(cgRect: CGRect(x: 0.0, y: 0.0, awidth: 2.0/3.0 * squareLayerLength, height: 2.0/3.0 * squareLayerLength)) let b2 = NSValue(cgRect: CGRect(x: 0.0, y: 0.0, awidth: squareLayerLength, height: squareLayerLength)) let b3 = NSValue(cgRect: CGRect.zero) let boundsAnimation = CAKeyframeAnimation(keyPath: "bounds") boundsAnimation.values = [b1, b2, b3] boundsAnimation.timingFunctions = [fadeInSquareTimingFunction, squareLayerTimingFunction] boundsAnimation.duration = kAnimationDuration boundsAnimation.keyTimes = [0, 1.0-(kAnimationDurationDelay/kAnimationDuration) as NSNumber, 1.0]
Hoạt hình đặc biệt này thay đổi CALayer's vọt. Một hình ảnh động keyframe được tạo ra mà đi từ hai phần ba chiều dài, chiều dài đầy đủ, sau đó không. Tiếp theo, biến đổi màu nền:
// backgroundColor let backgroundColorAnimation = CABasicAnimation(keyPath: "backgroundColor") backgroundColorAnimation.fromValue = UIColor.white.cgColor backgroundColorAnimation.toValue = UIColor.fuberBlue().CGColor backgroundColorAnimation.timingFunction = squareLayerTimingFunction backgroundColorAnimation.fillMode = kCAFillModeBoth backgroundColorAnimation.beginTime = kAnimationDurationDelay * 2.0 / kAnimationDuration backgroundColorAnimation.duration = kAnimationDuration / (kAnimationDuration - kAnimationDurationDelay)
Hãy lưu ý của các fillModetài sản. Kể từ khi beginTimekhông phải là zero, các hình ảnh động sẽ kẹp cả hai bắt đầu và kết thúc CGColors để các hình ảnh động. Điều này dẫn đến không nhấp nháy từ các hình ảnh động khi thêm vào CAAnimationGroup mẹ. Phát biểu trong đó, đó là thời gian để thực hiện điều đó:
// Group let groupAnimation = CAAnimationGroup() groupAnimation.animations = [boundsAnimation, backgroundColorAnimation] groupAnimation.repeatCount = Float.infinity groupAnimation.duration = kAnimationDuration groupAnimation.isRemovedOnCompletion = false groupAnimation.beginTime = beginTime groupAnimation.timeOffset = startTimeOffset squareLayer.add(groupAnimation, forKey: "looping")
The Mask
Đầu tiên, bỏ ghi chú tất cả các lớp được thêm vào trong init(frame:)và bỏ ghi chú tất cả các hình ảnh động trong startAnimating(). Với tất cả các hình ảnh động đặt lại với nhau, xây dựng và chạy Fuber.
Vẫn có vẻ một chút đi, phải không? Có một bước nhảy đột ngột trong boundskhi circleLayer sụp đổ trong kích thước. May mắn thay, các hình ảnh động mặt nạ sẽ khắc phục điều đó, thu hẹp các lớp con tất cả trong một mịn. Đến animateMaskLayer()và thêm những điều sau đây:
// bounds let boundsAnimation = CABasicAnimation(keyPath: "bounds") boundsAnimation.fromValue = NSValue(cgRect: CGRect(x: 0.0, y: 0.0, awidth: radius * 2.0, height: radius * 2)) boundsAnimation.toValue = NSValue(cgRect: CGRect(x: 0.0, y: 0.0, awidth: 2.0/3.0 * squareLayerLength, height: 2.0/3.0 * squareLayerLength)) boundsAnimation.duration = kAnimationDurationDelay boundsAnimation.beginTime = kAnimationDuration - kAnimationDurationDelay boundsAnimation.timingFunction = circleLayerTimingFunction
Đây là những hình ảnh động cho các giới hạn. Hãy nhớ rằng khi các giới hạn thay đổi, toàn bộ AnimatedULogoViewsẽ biến mất kể từ khi lớp này là mặt nạ được áp dụng với tất cả các lớp con. Bây giờ thực hiện một bán kính góc hình ảnh động để giữ cho mặt nạ tròn:
// cornerRadius let cornerRadiusAnimation = CABasicAnimation(keyPath: "cornerRadius") cornerRadiusAnimation.beginTime = kAnimationDuration - kAnimationDurationDelay cornerRadiusAnimation.duration = kAnimationDurationDelay cornerRadiusAnimation.fromValue = radius cornerRadiusAnimation.toValue = 2 cornerRadiusAnimation.timingFunction = circleLayerTimingFunction
Thêm hai hình ảnh động này để một CAAnimationGroup để hoàn thành lớp này:
// Group let groupAnimation = CAAnimationGroup() groupAnimation.isRemovedOnCompletion = false groupAnimation.fillMode = kCAFillModeBoth groupAnimation.beginTime = beginTime groupAnimation.repeatCount = Float.infinity groupAnimation.duration = kAnimationDuration groupAnimation.animations = [boundsAnimation, cornerRadiusAnimation] groupAnimation.timeOffset = startTimeOffset maskLayer.add(groupAnimation, forKey: "looping")
The Grid
Một biên giới kỹ thuật số. Hãy thử hình dung cụm UIViews khi chúng di chuyển thông qua các TileGridViewví dụ. Họ trông như thế nào? Vâng ... thời gian ngừng sản xuất các tài liệu tham khảo để Tron và có một cái nhìn! Lưới nền bao gồm một loạt các TileViews tất cả gắn liền với phụ huynh TileGridViewlớp. Để có được một sự hiểu biết trực quan nhanh chóng này, mở ra TileView.swift và tìm thấy init(frame:). Thêm dòng sau vào dưới cùng của phương pháp này:
layer.borderWidth = 2.0
Như bạn có thể thấy, TileViews được sắp xếp để chúng chồng với nhau trong một mạng lưới. Việc tạo ra tất cả các logic điều này xảy ra trong một phương pháp gọi renderTileViews()trong TileGridView.swift . May mắn thay, logic đã được tạo ra trên danh nghĩa của bạn để bố trí lưới này. Tất cả bạn cần làm là animate nó!
Animating the TileView
TileGridViewcó một subview con trực tiếp duy nhất được gọi containerView. Nó cho biết thêm tất cả các con TileViews. Ngoài ra, có một tài sản được gọi tileViewRows, mà là một mảng hai chiều có chứa tất cả các TileViews thêm vào xem container. Điều hướng trở lại TileView's init(frame:). Hủy bỏ dòng bạn thêm vào để hiển thị chiều rộng biên giới và cho phép dòng nhận xét ra có thêm các chimeSplashImagenội dung của layer. Phương pháp này nên bây giờ trông như thế này:
override init(frame: CGRect) { super.init(frame: frame) layer.contents = TileView.chimesSplashImage.cgImage layer.shouldRasterize = true }
Coooooool ... Chúng tôi đang nhận được ở đó! Tuy nhiên, TileGridView(và tất cả của nó TileViews) cần một số hình ảnh động. Mở TileView.swift , đi đến startAnimatingWithDuration(_:beginTime:rippleDelay:rippleOffset:)và tiếng tom xuống đoạn tiếp theo của hình ảnh động:
let timingFunction = CAMediaTimingFunction(controlPoints: 0.25, 0, 0.2, 1) let linearFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) let easeOutFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) let easeInOutTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) let zeroPointValue = NSValue(cgPoint: CGPoint.zero) var animations = [CAAnimation]()
Mã này thiết lập một loạt các chức năng thời gian bạn sẽ sử dụng ngay bây giờ. Thêm mã này:
if shouldEnableRipple { // Transform.scale let scaleAnimation = CAKeyframeAnimation(keyPath: "transform.scale") scaleAnimation.values = [1, 1, 1.05, 1, 1] scaleAnimation.keyTimes = TileView.rippleAnimationKeyTimes as [NSNumber]? scaleAnimation.timingFunctions = [linearFunction, timingFunction, timingFunction, linearFunction] scaleAnimation.beginTime = 0.0 scaleAnimation.duration = duration animations.append(scaleAnimation) // Position let positionAnimation = CAKeyframeAnimation(keyPath: "position") positionAnimation.duration = duration positionAnimation.timingFunctions = [linearFunction, timingFunction, timingFunction, linearFunction] positionAnimation.keyTimes = TileView.rippleAnimationKeyTimes as [NSNumber]? positionAnimation.values = [zeroPointValue, zeroPointValue, NSValue(cgPoint:rippleOffset), zeroPointValue, zeroPointValue] positionAnimation.isAdditive = true animations.append(positionAnimation) }
shouldEnableRipplelà một boolean điều khiển khi chuyển đổi hình ảnh động và vị trí được thêm vào animationsmảng bạn vừa tạo ra. Giá trị của nó được thiết lập để truecho tất cả các TileViews mà không vào chu vi TileGridView. Logic này đã được thực hiện cho bạn khi TileViews được tạo ra trong các renderTileViews()phương pháp TileGridView. Thêm một hình ảnh động opacity:
// Opacity let opacityAnimation = CAKeyframeAnimation(keyPath: "opacity") opacityAnimation.duration = duration opacityAnimation.timingFunctions = [easeInOutTimingFunction, timingFunction, timingFunction, easeOutFunction, linearFunction] opacityAnimation.keyTimes = [0.0, 0.61, 0.7, 0.767, 0.95, 1.0] opacityAnimation.values = [0.0, 1.0, 0.45, 0.6, 0.0, 0.0] animations.append(opacityAnimation)
Đây là một hình ảnh động tự giải thích khá với một số rất cụ thể keyTimes. Bây giờ thêm tất cả các hình ảnh động để một nhóm:
// Group let groupAnimation = CAAnimationGroup() groupAnimation.repeatCount = Float.infinity groupAnimation.fillMode = kCAFillModeBackwards groupAnimation.duration = duration groupAnimation.beginTime = beginTime + rippleDelay groupAnimation.isRemovedOnCompletion = false groupAnimation.animations = animations groupAnimation.timeOffset = kAnimationTimeOffset layer.add(groupAnimation, forKey: "ripple")
Điều này sẽ thêm groupAnimationcho trường hợp TileView. Lưu ý rằng các hình ảnh động nhóm, hoặc có thể có một hoặc ba hình ảnh động trong nhóm, tùy thuộc vào giá trị của shouldEnableRipple. Bây giờ bạn đã viết các phương pháp để animate mỗi TileView, đó là thời gian để gọi nó từ TileGridView. Trụ sở để TileGridView.swift và thêm đoạn mã sau vào startAnimatingWithBeginTime(_