12/08/2018, 14:17

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.

fuber_logo.png

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ư:

ezgif.com-optimize.gif

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.

Fuber-View-Hierarchy-1.png

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.

RiderIconView.gif

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ư.

CircleIn-Animation.gif

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")

Knockoutline-Animation.gif

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.

PreMask-Animation.gif

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")

RiderIconView-Animation.gif

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

Fuber-Grid-View.png

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
}

Grid-Starting.gif

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(_            </div>
            
            <div class=

0