iOS Animation-Panorama

Keywords: Swift Windows iOS github Attribute

This is the second in a series.

Students who have read the previous article already know that the title "scene" refers to view and "window" refers to view.View.maskThe panorama combs masks and mask animations.If you're not familiar with iOS masks, it's a good idea to look at them first First.

Windows change more than scenes, so this article focuses on the effect of windows.

We look at it in three dimensions: is the window moving?Is the window changing?How many windows are there?

Many animations are the result of these three dimensions alone or combined.Let's first look at the individual effects of each dimension, then at their combined effects.

1. Window movement

In the previous section, we use a circle as a window, first paste a picture to recall:

We've done a lot of basic animations, so it's conceivable that if the animation changes the center of the circle mask, you can make the window move.

The effect is as follows:

The schematic code is as follows:

/// viewDidLoad
// view
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)

// Round window
let mask = CircleView()
mask.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
mask.center = CGPoint(x: 100, y: 100)
self.mask = mask
frontView.mask = mask

// Window movement
startAnimation()

/// startAnimation
// Animate changes to the center of the mask
private func startAnimation() {
    mask.layer.removeAllAnimations()
    
    let anim = CAKeyframeAnimation(keyPath: "position")
    let bound = UIScreen.main.bounds
    anim.values = [CGPoint(x: 100, y: 250), CGPoint(x:bound.width - 100 , y: 250), CGPoint(x: bound.midX, y: 450), CGPoint(x: 100, y: 250)]
    anim.duration = 4
    anim.repeatCount = Float.infinity
    mask.layer.add(anim, forKey: nil)
}

It's very easy to make windows move, and this simple effect can be the basis for other effects as well.

For example, we add a pan gesture to achieve this effect:

The idea is simple:

  1. Initially black, window size 0
  2. Start showing windows when pan gesture starts
  3. Move the window when pan gestures drag
  4. At the end of the pan gesture, the window returns to zero size, returning to black

The schematic code is as follows:

// On the basis of the code you just window
// Add pan gestures to control the center of the mask

@objc func onPan(_ pan: UIPanGestureRecognizer) {
    switch pan.state {
    case .began:
        // Drag Start, Show Window
        mask.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
        mask.center = pan.location(in: pan.view)
    case .changed:
        // Drag process, move window
        mask.center = pan.location(in: pan.view)
    default:
        // Others, Hide Windows
        mask.frame = CGRect.zero
    }
}

Okay, Window Move first sees this, and next, let's look at the dimension of Window Change.

2. Window Change

Let's also use the example of a round window, this time using the front and back view s, circles as mask s for frontView s;

Or take a look at the previous picture:

This time we make the round window dynamically larger (zoom), and zoom is also the basic animation, as shown in the following motion picture:

The schematic code is as follows:

/// viewDidLoad
// back view
backView.frame = UIScreen.main.bounds
view.addSubview(backView)

// view
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)

// Round window
let mask = CircleView()
mask.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
mask.center = CGPoint(x: frontView.bounds.midX, y: frontView.bounds.midY)
self.mask = mask
frontView.mask = mask

// Window Change
startAnimation()

/// startAnimation
// Animation changes the size of the mask
private func startAnimation() {
    mask.layer.removeAllAnimations()
    
    let scale: CGFloat = 5.0
    let anim = CABasicAnimation(keyPath: "transform.scale.xy")
    anim.fromValue = 1.0
    anim.toValue = scale
    anim.duration = 1
    mask.layer.add(anim, forKey: nil)
    
    // Really change the layer's transform to prevent it from returning to its original state after the animation ends
    mask.layer.transform = CATransform3DMakeScale(scale, scale, 1)
}

I think you have found that combining this effect with the iOS transfer mechanism is a very common transfer effect.

Another common example of window change is the progress loop effect.
First look at the effect, as shown in the following motion map:

In fact, it's a gradient view, with a circular window, which is no different from the text window we saw earlier, as shown in the following figure:

It's just that the window has changed from nothing to a complete circle; stroke animation is best suited for this change.

stroke, also known as strokeStart and strokeEnd attributes of CAShapeLayer, has a well-developed tutorial Online

To understand this effect, stroke is only briefly introduced in this paper:

  1. We want to draw a circle. First, we want to design the starting and ending points of the circle. If we draw a line from the beginning to the end, we can draw a complete circle. We can call stroke from the beginning to the end a path.
  2. But now we only want to draw a part of the circle, for example, from 1/4 (0.25) of the circle to 3/4 (0.75); then we set strokeStart = 0.25, strokeEnd = 0.75, so the circle will only show 1/4 to 3/4 of the circle.
  3. The strokeStart, strokeEnd attributes show which segment we want to display relative to the full path
  4. We want the rings to start off without being shown, so set strokeStart = 0, strokeEnd = 0
  5. We want the ring to be completely displayed at the end, set strokeStart = 0 and strokeEnd = 1 (that is, 100%) to do that.
  6. The animation process is a change in strokeEnd from 0 to 1.

The schematic code is as follows:

/// ViewController
// Gradient Scene
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)

// Ring Window
let mask = RingView()
mask.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
mask.center = CGPoint(x: frontView.bounds.midX, y: frontView.bounds.midY)
self.mask = mask
frontView.mask = mask

// Window Change
// Animate changes the completeness of the mask ring (from the beginning of the ring to the end of the ring)
startAnimation()


/// RingView
// Setting programs for ring view can change its strokeEnd
var progress: CGFloat = 0 {
    didSet {
        if progress < 0 {
            progress = 0
        } else if progress > 1 {
            progress = 1
        } else {}
        
        (layer as! CAShapeLayer).strokeEnd = progress
    }
}

We can use CAShapeLayer's path to draw a wide variety of windows, with strokeStart, strokeEnd, there will be a lot of interesting stroke window changes.

Next, let's look at the dimension of "multiple windows".
Since simple multiple windows have no effect, this time we'll combine them directly with "window moving" or "window changing".

3. Multiple windows

Since view has only one mask attribute, what we call multi-window is not multiple masks, but articles on masks.
For example, we can achieve such an effect in a rough but intuitive way:

The ideas are as follows:

  1. mask has six sub view s, equivalent to six small windows
  2. Sub view s become transparent one after another, windows open one after another

The schematic code is as follows:

/// ViewController
// back view
backView.frame = UIScreen.main.bounds
view.addSubview(backView)

// view
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)

// Multiple windows (blinds)
// mask's child view s, hiding in turn
let mask = ShutterView(frame: frontView.bounds, count: 8)
frontView.mask = mask

mask.startAnimation()


/// ShutterView (multi-window view)
func startAnimation() {
    layers.enumerated().forEach {
        let anim = CABasicAnimation(keyPath: "opacity")
        anim.duration = 0.5
        anim.beginTime = $0.element.convertTime(CACurrentMediaTime(), from: nil) + anim.duration * Double($0.offset)
        anim.fromValue = 1
        anim.toValue = 0
        // Because layer's animation was delayed with beginTime
        // After the latter code modifies the opacity's true value, the layer starts showing the state (opacity == 0)
        // So we use backwards to ensure that before the animation executes, the layer displays the state of fromValue (opacity == 1)
        anim.fillMode = CAMediaTimingFillMode.backwards
        $0.element.add(anim, forKey: nil)
        // Modify opacity's real value to prevent the animation from restoring to its original state
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        $0.element.opacity = 0
        CATransaction.commit()
    }
}

Above is the combination of "multi-window" and "window change" (change in transparency).

Looking at a group of similar windows, some students might think of CAReplicator layer, a class specializing in replicator layers.

Next, we'll try using CAReplicator Layer as a window to achieve a combination of "multiple windows" and "window movement".

4. Multi-window (CAReplicator Layer)

There are already mature tutorials for CAReplicator Layer on the web. Here we just make a simple analogy to make an impression on the students we have not met.

CAReplicator Layer is just like UITableView. You can specify a subLayer and a number to it. It can copy the subLayer to the number you specify, just like UITableView creates and manages a set of Cells based on the Cell class you specify.

UITableView can manage the layout of Cells, allowing Cells to be arranged one by one, similarly, CAReplicator Layer can also have a set of subLayer s arranged in a regular manner according to your settings.

The CAReplicator Layer also allows a group of subLayers to have various transition effects depending on the settings, such as the first subLayer background color being white and the middle subLayer background color decreasing until the last subLayer is black.The effect of this article only concerns the subLayer location, so no other settings will be discussed.

In this example, we still use the gradient view as the scene and let the CAReplicator layer copy three sublayers (circles) as windows to implement a loading animation, as shown in the following motion map:

With previous experience, it is easy to see that this animation consists of three small circular windows, swapping their positions over the gradient scene as shown in the following image:

The schematic code is as follows:

/// ViewController
// Gradient Scene
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)

// Multi-window (3-sphere window, CAReplicator Layer window)
let mask = TriangleLoadingView()
mask.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
mask.center = CGPoint(x: frontView.bounds.midX, y: frontView.bounds.midY)
self.mask = mask
frontView.mask = mask

// Window (3-ball rotation)
mask.startAnimation()

/// TriangleLoadingView
// Create 3 Sphere Window
override init(frame: CGRect) {
    super.init(frame: frame)
    
    let layer = (self.layer as! CAReplicatorLayer)
    layer.backgroundColor = UIColor.clear.cgColor
    layer.instanceCount = 3
    // 3 Balls
    // Each layer (CAReplicator Layer) centered on this view is the origin and the z-axis is the rotation axis
    // The last cell Layer is in the initial state, rotated 120 degrees clockwise
    // Form an equilateral triangle
    layer.instanceTransform = CATransform3DMakeRotation(CGFloat.pi / 3 * 2, 0, 0, 1)
    layer.addSublayer(cellLayer)
}

// Positioning Ball
override func layoutSubviews() {
    super.layoutSubviews()
    
    // The first ball, at the top of this view, horizontally centered
    cellLayer.position = CGPoint(x: bounds.midX, y: Constants.cellRadius)
}

// Perform animation (3-ball rotation)
func startAnimation() {
    cellLayer.removeAllAnimations()
    
    let anim = CABasicAnimation(keyPath: "position")
    let from = cellLayer.position
    anim.fromValue = from
    // Knowledge of using a point equilateral triangle
    // r: The radius of an equilateral triangle (the radius of an outer circle)
    let r = bounds.midY -  Constants.cellRadius
    // Find the coordinates of the lower right vertex based on the coordinates and outer diameter of the upper vertex of an equilateral triangle
    let radian = CGFloat.pi / 6
    anim.toValue = CGPoint(x: from.x + r * cos(radian), y: from.y + r + r * sin(radian))
    anim.duration = 1
    anim.repeatCount = Float.infinity
    cellLayer.add(anim, forKey: nil)
    
    // Note: We have achieved the movement of a circular window from the top vertex to the bottom right vertex
    // CAReplicator Layer can automatically help us move between the other two vertices based on the instanceTransform we set up earlier
}

When I see CAReplicator Layer, some students think of CAEmitterLayer, which is the layer to achieve the particle effect.
Can particles also act as windows?

Of course, all view s (layer s) can be used as windows. Next, let's look at a combination of the three dimensions of CAEmitterLayer: multi-window, window-moving and window-changing.

V. Particle Window

Neither do we expand our knowledge of CAEmitterLayer, just look at an effect as shown in the following motion map:

The idea is simple:

  1. Use a picture as a scene
  2. Use a view of a heart-shaped particle (multi-window) that is projecting upwards (window moving) from the bottom as a window

There are mature online tutorials on using CAEmitterLayer

The schematic code is as follows:

/// ViewController
// back view
backView.frame = UIScreen.main.bounds
view.addSubview(backView)

// view
frontView.frame = UIScreen.main.bounds
view.addSubview(frontView)

// Particle window
let mask = EmitterView()
mask.frame = frontView.bounds
frontView.mask = mask

/// EmitterView
///Configure Particle Window
private func configLayer() {
    // Heart-shaped Particles
    let cell = CAEmitterCell()
    // style
    cell.contents = UIImage(named: "love")?.cgImage
    cell.scale = 0.5
    cell.scaleSpeed = 2
    // Rate at which particles are generated
    cell.birthRate = 20
    // Time to live
    cell.lifetime = 3
    // direction
    cell.emissionLongitude = CGFloat(Float.pi / 2)
    cell.emissionRange = CGFloat.pi / 3
    // speed
    cell.velocity = -250
    cell.velocityRange = 50

    // Launcher
    let emitterLayer = (layer as! CAEmitterLayer)
    emitterLayer.emitterPosition = CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.height)
    emitterLayer.birthRate = 1
    emitterLayer.emitterSize = CGSize(width: UIScreen.main.bounds.width, height: 0)
    emitterLayer.emitterShape = CAEmitterLayerEmitterShape.point
    emitterLayer.emitterCells = [cell]
}

End

In this article, we take window as an example, starting from the three dimensions of "window moving", "window changing", "multi-window", comb some examples of mask animation.
The window has been opened, so the simpler view, we will no longer start alone.

In the next article, we'll look at a complex, but actually simple, effect at first glance.The focus of the article is not on the effect itself, but on the principle that what looks complicated may not be really complex.

All the examples in this article are listed in GitHub Library There is complete code in it.

Thank you for your reading. See you in the next article.

Portal

Posted by Petrushka on Sun, 07 Jun 2020 18:45:30 -0700