The design comes from Vadim Gromov of Dribbble. Want to see the final implementation of the renderings can be pulled directly to the end of the article.

It may seem complicated, but take your time. First, let’s break down this complex animation a bit. It can actually be viewed as a superposition of multiple droplet falling animations:

Superimposed by 7 separate dripping animations

Let’s begin the drop by drop implementation

Drip analysis at the top

And God said, let there be water

The top dripping animation is decomposed as follows:

Bessel curve is the only way to realize the effect of water droplets. Here you can use the principle I mentioned in my previous post “implementation of Water Drop animation on iOS”, and paste the schematic diagram again:

The overall idea is: use a circular View to represent falling water droplets, and then re-draw the shape of water droplets with Bezier curves according to the position of water droplets in the process of falling. Initialize some members first:

- (void)setupTopWater {// water self.topWaterLayer = [CAShapeLayer]; self.topWaterLayer.frame = CGRectMake(0, 0, self.frame.size.width, 0); self.topWaterLayer.fillColor = [UIColor whiteColor].CGColor; self.topWaterLayer.strokeColor = [UIColor clearColor].CGColor; [self.layer addSublayer:self.topWaterLayer]; // drop self.topWaterDrop = [[UIView alloc] initWithFrame:CGRectMake(0, 0, TOPWATERDROP_W, TOPWATERDROP_W)]; self.topWaterDrop.backgroundColor = [UIColor whiteColor]; Self. TopWaterDrop. Layer. The cornerRadius = TOPWATERDROP_W / 2.0 f; self.topWaterDrop.clipsToBounds = YES; Self.topwaterdrop. center = CGPointMake(self.frame.size. Width / 2.0f, -topwaterdrop_w / 2.0f); [self addSubview:self.topWaterDrop]; }Copy the code

Write the code to draw the Bezier curve according to the position of water droplets:

- (void) drawTopWater {UIBezierPath * path = [self getBezierPathFromPoint1: CGPointMake (self. Frame. The size, width 2.0 / f, 0) radius1: TOPWATER_W / 2.0 f Point2: self. TopWaterDrop. Center radius2: TOPWATERDROP_W f / 2.0]; self.topWaterLayer.path = path.CGPath; } - (UIBezierPath *)getBezierPathFromPoint1:(CGPoint)point1 radius1:(CGFloat)r1 Point2:(CGPoint)point2 radius2:(CGFloat)r2 { CGFloat x1 = point1.x; CGFloat y1 = point1.y; CGFloat x2 = point2.x; CGFloat y2 = point2.y; CGFloat distance = sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); CGFloat sinDegree = (x2 - x1) / distance; CGFloat cosDegree = (y2 - y1) / distance; CGPoint pointA = CGPointMake(x1 - r1 * cosDegree, y1 + r1 * sinDegree); CGPoint pointB = CGPointMake(x1 + r1 * cosDegree, y1 - r1 * sinDegree); CGPoint pointC = CGPointMake(x2 + r2 * cosDegree, y2 - r2 * sinDegree); CGPoint pointD = CGPointMake(x2 - r2 * cosDegree, y2 + r2 * sinDegree); CGPoint pointN = CGPointMake(pointB.x + (distance / 2) * sinDegree, pointB.y + (distance / 2) * cosDegree); CGPoint pointM = CGPointMake(pointA.x + (distance / 2) * sinDegree, pointA.y + (distance / 2) * cosDegree); UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:pointA]; [path addLineToPoint:pointB]; [path addQuadCurveToPoint:pointC controlPoint:pointN]; [path addLineToPoint:pointD]; [path addQuadCurveToPoint:pointA controlPoint:pointM]; return path; }Copy the code

2. Let it fall! The fall of a water drop is not linear; it is affected by gravity. There are two ways to realize it. One is to calculate 60 key positions (if duration does not exceed 1s) as animation key frames according to the quadratic function relation between physical displacement and time; the other is to directly use UIKit Dynamic to simulate gravity effect for water droplets. Here I use the second method and do some physical world setup first:

- (void)setupDynamic
{
    self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self];
    
    //gravity
    self.gravityBehavior = [[UIGravityBehavior alloc] init];
    [self.animator addBehavior:self.gravityBehavior];
    
    //collision
    self.collisionBehavior = [[UICollisionBehavior alloc] init];
    self.collisionBehavior.collisionMode = UICollisionBehaviorModeEverything;
    [self.collisionBehavior addBoundaryWithIdentifier:@"floor" fromPoint:CGPointMake(0, self.frame.size.height) toPoint:CGPointMake(self.frame.size.width, self.frame.size.height)];
    [self.animator addBehavior:self.collisionBehavior];
    }
Copy the code

Add the following code to the end of the setupTopWater function:

- (void)setupTopWater
{
    ...
    
    [self.collisionBehavior addItem:self.topWaterDrop];
    
    self.topWaterDropBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.topWaterDrop]];
    self.topWaterDropBehavior.elasticity = 0;
    self.topWaterDropBehavior.allowsRotation = NO;
    [self.animator addBehavior:self.topWaterDropBehavior];
    }
Copy the code

Whereabouts!

- (void)play
{
    [self.gravityBehavior addItem:self.topWaterDrop];
    }
Copy the code

You can see more about UIKit Dynamic in my translated post on using UIKit Dynamics to simulate physics effects. Add this view to rootViewController, run it, and you can see:

Oh, I think we forgot to plot the Bezier curve. Next we need to use CADisplayLink. Learn more about kitten in this blog post. First set displayLink: obj-c

You can see that there is a gap between the water droplets and the Bezier curve. This gap is generated because the displayLink call can’t keep up with the drop position change. It’s not hard to solve this problem, but it may not be the best way. There are two steps:

  1. When the Bezier path is drawn, an extra semicircle is drawn at the low end to replace the original water drop.
  2. The original drop alpha is set to transparent and is used as an auxiliary view;

The code will not be posted, you can download the demo to see more at the end of the article, after the modification effect is as follows:

3. When the drop has reached a certain distance, the drop should be disconnected and the water above it should be “retracted”. To do this, I use an auxiliary view:

The black, little booger thing you see above is the view that is used as an auxiliary. The code will not go up, nothing special:

Our white, gooey drops of liquid are finally starting to look something. So much for the drip. It’s worth noting that the original image used a smooth four-step Bezier curve for the rebound after the drop breaks, but iOS can only draw it three times, so a compromise was used:

The original design is on the left, and my implementation is on the right

I would appreciate it if you have a better way to achieve it. ~ (≧ del ≦) / ~

Bottom spring water analysis

The steps are as follows:

1. Add an impact boundary “below the bottom level” and offset the droplet height by one

Add an impact boundary

2. When the water drop hits the boundary, it will naturally bounce back. At this time, the method similar to the one above is adopted to achieve the effect of water drop breaking and water surface leveling. The system delegate can be used to detect the droplet collision with the boundary, and notice that when the droplet collides with the boundary, the boundary needs to be removed so that the droplet can hit the floor, and then we go to the next step.

Here’s a little bit of a problem that took a little bit of time. Instead of drawing in drawInRect, my code uses the path property in case Ayer. Therefore, when drawing paths, note that coordinates are set by the frame of this layer as the coordinate system.

Water droplets springback

3. The water drops will eventually hit the floor. You can do some reset here to start a new drop.

The code is not up, and the final effect is as follows:

Again, when the water below is flat, the auxiliary view that you’re using also needs UIView Dynamic to simulate the effect of gravity on it.

And finally, trypophobia:

conclusion

Doing this dynamic effect is really not difficult, just a little bit cumbersome. It’s basically bezier curves and UIKit Dynamic, and then a little bit more careful segmentation of the motion, nothing else.

Special thanks to @Kitten Yang for learning so much about iOS graphics on his blog.

What ‘s more?

The effect is there, but a lot of the details are not very smooth. Try setting different control points for the Bessel path to make the effect more smooth.

The entire project is coded on Github