This is the second article in a series.

Those of you who read the last article already know that “view” in the title refers to view and “window” refers to view.mask. If you’re not familiar with masks on iOS, I recommend reading the first article.

Compared with the scenery, the change of the window is more diverse, so this article we focus on the effect of the window.

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

Many animations are the single embodiment of these three dimensions, or the combined effect. Let’s first look at the effects of each dimension individually, and then look at their combined effects.

A, window

In the previous article, we used a circle as a window. Let’s post a picture to remember:

Most of us have done basic animation, so we can figure out how to animate the window by animating the center of the circle mask.

The effect can be seen in the GIF below:

The schematic code is as follows:

/// viewDidLoad
/ / scene
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
startAnimation()

/// startAnimation
// Animate the center of the mask
private func startAnimation(a) {
    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)}Copy the code

Moving a window is very simple, and this simple effect can become the basis for other effects.

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

The idea is simple:

  1. It starts out black and the window size is 0
  2. When the PAN gesture begins, the window begins to display
  3. Pan gestures when dragging to move the window
  4. At the end of the PAN gesture, the window size reverts to 0, returning to black

The schematic code is as follows:

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

@objc func onPan(_ pan: UIPanGestureRecognizer) {
    switch pan.state {
    case .began:
        // Drag to start the window
        mask.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
        mask.center = pan.location(in: pan.view)
    case .changed:
        // Drag process to move window
        mask.center = pan.location(in: pan.view)
    default:
        // Otherwise, hide Windows
        mask.frame = CGRect.zero
    }
}
Copy the code

All right, so let’s look at the window change dimension.

Second, the window

Let’s use the round window example again, this time using the front and back views, the circle as the mask of the frontView;

Again, take a look at the image above:

This time we made the round window dynamically larger (zoomed). Zooming is also a basic animation, as shown in the following GIF:

The schematic code is as follows:

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

/ / scene
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
startAnimation()

/// startAnimation
// Animation changes the size of the mask
private func startAnimation(a) {
    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)
    
    // Actually change the layer's transform to prevent reverting after the animation ends
    mask.layer.transform = CATransform3DMakeScale(scale, scale, 1)}Copy the code

I think you can already see that combining this effect with the iOS transition mechanism is a very common transition effect.

Let’s take another common example of window changes: the progress loop effect. Take a look at the effects first, as shown in the GIF below:

In fact, it is a gradient scene with a circular window, which is no different from the text window we saw above, as shown below:

Only the window gradually changes from nothing into a complete circle; Best suited for this variation is the Stroke animation.

Stroke, also known as the strokeStart and strokeEnd properties of CAShapeLayer, has full-fledged tutorials online

In order to understand this effect, this article only gives a basic introduction to Stroke:

  1. We want to draw a circle. First of all, we should design the beginning and end of the circle. If we draw a line from the beginning to the end, we can draw a complete circle.
  2. But now we only want to draw part of the circle, say from 1/4 (0.25) to 3/4 (0.75); StrokeStart = 0.25, strokeEnd = 0.75, so that the circle (path) will only show 1/4 to 3/4
  3. The strokeStart and strokeEnd attributes are what segment we want to display relative to the full path
  4. We want the circle not to show at the beginning, so strokeStart = 0, strokeEnd = 0 will do it
  5. We want the circle to be fully displayed at the end, so strokeStart = 0 and strokeEnd = 1 (100%) will do the trick
  6. The animation is the strokeEnd going from 0 to 1.

The schematic code is as follows:

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

/ / 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 the degree of completion of the mask circle animatrically (from the beginning of the circle until the circle is completely closed)
startAnimation()


RingView (RingView)
// Set progress for the ring view to 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
    }
}
Copy the code

We can use The path of CAShapeLayer to draw all kinds of Windows. With strokeStart and strokeEnd, there will be many interesting stroke window paintings.

Next, let’s look at the dimension of “multi-windows”. Since multi-windows alone have no effect, this time we will directly combine it with “window movement” or “window transformation”.

Three or more Windows

Since the view has only one mask property, when we say multi-window, we are not talking about multiple masks, but working on them. For example, we can achieve this effect in a crude but intuitive way:

The implementation idea is as follows:

  1. The mask has six sub-views, equivalent to six small Windows
  2. The child views become transparent one by one, and the Windows open one by one

The schematic code is as follows:

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

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

// Multi-window (shutter)
// Mask child views, hidden in sequence
let mask = ShutterView(frame: frontView.bounds, count: 8)
frontView.mask = mask

mask.startAnimation()


/// ShutterView (multi-window view)
func startAnimation(a) {
    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
        // Layer animation is delayed with beginTime
        Opacity == 0: opacity = 0: opacity = 0: opacity = 0: opacity = 0: opacity = 0: opacity = 0
        // We use backwards to ensure that the layer displays the fromValue (opacity == 1) before animating
        anim.fillMode = CAMediaTimingFillMode.backwards
        $0.element.add(anim, forKey: nil)
        // Modify the true value of opacity to prevent it from reverting after animation is complete
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        $0.element.opacity = 0
        CATransaction.commit()
    }
}
Copy the code

This is a combination of “multiple Windows” and “windowing” (changes in transparency),

When you see a similar set of little Windows, you might think of a CAReplicatorLayer class that specializes in copying sub-layers,

Then, we use CAReplicatorLayer as a window to try, to achieve a “multi-window” and “window” combination.

Four, multi window (CAReplicatorLayer)

CAReplicatorLayer is already available on the web, so let’s just make a simple analogy to give those who have not been exposed to it an impression.

CAReplicatorLayer is like a UITableView, you can give it a subLayer and a number, and it can copy the subLayer to the number that 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 by arranging them one by one. Similarly, CAReplicatorLayer can arrange a set of subLayer in a regular way, depending on your Settings.

CAReplicatorLayer can also set a set of subLayer to have various transitions, such as a white background for the first subLayer and a fading background for the middle subLayer until the last subLayer is black. The effects of this article only cover subLayer locations, so I won’t discuss any other Settings.

In this example, we still use gradient View as the scene, and let CAReplicatorLayer copy 3 sub-layers (circles) as the window to realize a loading animation, as shown in the following GIF:

From the previous experience, it is easy to see that this animation is just three small round Windows changing their positions over the gradient scene, as shown below:

The schematic code is as follows:

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

// Multiple Windows (CAReplicatorLayer)
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

// The window moves (3 balls rotate)
mask.startAnimation()

/// TriangleLoadingView
// Create 3 ball Windows
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 rotation starts with the center of the CAReplicatorLayer (CAReplicatorLayer) of the view and the z-axis
    // The previous cellLayer is in the initial state, rotated 120° clockwise
    // Form an equilateral triangle
    layer.instanceTransform = CATransform3DMakeRotation(CGFloat.pi / 3 * 2.0.0.1)
    layer.addSublayer(cellLayer)
}

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

// Execute animation (3 ball rotation)
func startAnimation(a) {
    cellLayer.removeAllAnimations()
    
    let anim = CABasicAnimation(keyPath: "position")
    let from = cellLayer.position
    anim.fromValue = from
    // Use a little equilateral triangle knowledge
    // r: outer diameter of equilateral triangle
    let r = bounds.midY -  Constants.cellRadius
    // Find the coordinates of the lower right vertex of an equilateral triangle according to the coordinates of the upper vertex and the outer diameter
    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 implemented the circular window from the top to the bottom right vertex
    // CAReplicatorLayer can automatically help us move between the other two vertices based on the instanceTransform we set earlier
}

Copy the code

See CAReplicatorLayer, some students think of CAEmitterLayer, that is, to achieve the particle effect of the layer, particle can also be a window?

Of course, all view (layer) can be used as a window, next we look at a CAEmitterLayer implementation of “multi-window”, “window moving”, “window change” three dimensions of the combination.

Fifth, particle window

We will not expand the knowledge of CAEmitterLayer and directly look at an effect, as shown in the following GIF:

The implementation idea is very simple:

  1. Use a picture as the scene
  2. Use as a window a view of heart-shaped particles (multiwindow) emitting from the bottom up (window motion) and growing (window change)

There are well-developed tutorials on using CAEmitterLayer online

The schematic code is as follows:

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

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

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

/// EmitterView
/// configure the particle window
private func configLayer(a) {
    // Heart-shaped particles
    let cell = CAEmitterCell(a)/ / style
    cell.contents = UIImage(named: "love")? .cgImage cell.scale =0.5
    cell.scaleSpeed = 2
    // The rate at which particles are produced
    cell.birthRate = 20
    // Duration of life
    cell.lifetime = 3
    / / direction
    cell.emissionLongitude = CGFloat(Float.pi / 2)
    cell.emissionRange = CGFloat.pi / 3
    / / speed
    cell.velocity = -250
    cell.velocityRange = 50

    / / transmitters
    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]
}
Copy the code

The end of the

In this paper, we take the window as an example to sort out some examples of mask animation from the three dimensions of “moving window”, “changing window” and “multi-window”. The idea of the window has been opened, so the more simple scene, we will not start alone.

In the next article, we’ll take a look at an effect that looks complex at first glance but is actually simple. The point of the article is not to talk about the effect itself, but to remind you that what looks complicated is not necessarily complicated.

The complete code for all of this article’s examples is available in the GitHub repository.

Thanks for reading, and we’ll see you in the next article.

portal

  • Making library
  • IOS Animations – Windows Part 1
  • IOS Animation – Window View (3 · End)