Multiple UIView animations with UIBezierPath

I have below the screen:

enter image description here

When I click the Get More button, it will add a new rounded button. I added Pan Gesture to the UIView , where all the rounded buttons are added, which then will draw the UIBezierPath with only the rounded button. Until this moment, everything works.

Now I want the following:

  • I have a parent UIView called playGroundView containing the entire rounded button. This will create a Bezier path for all of these buttons.
  • Once the Bezier Path is drawn for all buttons, I click the Move button to animate all the rounded buttons at the same time.
  • Once all the buttons move along the end point of the Bezier path, then I can connect another Bezier button to the same button again (adding additional steps for the rounded button).
  • After that, the Final Move button will animate the entire button for the same button at the same time.

See below code for a subclass of UIView for PlayGround:

 @interface PlayGroundView : UIView @property (nonatomic, weak) PlayGroundViewController *playViewController; @end #import "PlayGroundView.h" #import "RoundedButton.h" #import "PlayGroundViewController.h" @interface PlayGroundView () { UIBezierPath *path; BOOL isDrawPointInside; } @end @implementation PlayGroundView #pragma mark - View Methods - - (id)initWithCoder:(NSCoder *)aDecoder { if (self = [super initWithCoder:aDecoder]) [self commonInit]; return self; } - (id)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) [self commonInit]; return self; } - (void)drawRect:(CGRect)rect { [[UIColor redColor] setStroke]; [path stroke]; } #pragma mark - Action Methods - - (void)pan:(UIPanGestureRecognizer *)pan { CGPoint currentPoint = [pan locationInView:self]; // NSLog(@"currentPoint: %@", NSStringFromCGPoint(currentPoint)); if (pan.state == UIGestureRecognizerStateBegan) { NSMutableArray *buttonAry = [NSMutableArray array]; buttonAry = self.playViewController.rBtnOpponentPlayerList; [buttonAry addObjectsFromArray:self.playViewController.rBtnPlayerList]; for (int i=0; i<buttonAry.count; i++) { UIButton *btn = [buttonAry objectAtIndex:i]; CGRect nearByRect = CGRectMake(btn.frame.origin.x - 20, btn.frame.origin.y - 20, btn.frame.size.width + 40, btn.frame.size.height + 40); if (CGRectContainsPoint(nearByRect, currentPoint)) { isDrawPointInside = YES; [path moveToPoint:btn.center]; // NSLog(@"point is inside...."); } } } if (pan.state == UIGestureRecognizerStateChanged) { if (isDrawPointInside) { [path addLineToPoint:currentPoint]; [self setNeedsDisplay]; } } if (pan.state == UIGestureRecognizerStateEnded) { isDrawPointInside = NO; // NSLog(@"pan ended"); } } #pragma mark - Helper Methods - - (void)commonInit { path = [UIBezierPath bezierPath]; path.lineWidth = 3.0; // Capture touches UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)]; pan.maximumNumberOfTouches = pan.minimumNumberOfTouches = 1; [self addGestureRecognizer:pan]; // Erase with long press [self addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(erase)]]; } - (void)erase { path = [UIBezierPath bezierPath]; path.lineWidth = 3.0; [self setNeedsDisplay]; } @end 

I still cannot manage the entire state of the Rounded Button. Let me know how best to control the entire step of a rounded button when moving.

I mentioned: http://nachbaur.com/blog/core-animation-part-4

My full demo code is available at: https://www.dropbox.com/s/f0ri0bff8ioxtpz/FreeHandDrawingDemo%202.zip?dl=0

+5
source share
2 answers

To achieve this, I used the UIBezierPath + Points category. In addition, the following classes and logic are used in my code:

In PlayGroundView, I edited the code in the drawRect method, the break code is the same:

 - (void)drawRect:(CGRect)rect { [[UIColor redColor] setStroke]; for (int i=0; i<[self subviews].count; i++) { RoundButton *tmpPlayerButton = [self.subviews objectAtIndex:i]; for (UIBezierPath *bezierPath in tmpPlayerButton.allPathsOfPlayer) { [bezierPath strokeWithBlendMode:kCGBlendModeNormal alpha:1.0]; } } [path stroke]; } 

In my ViewController class, in the Get More Buttons menu, the code is:

 - (IBAction)getBallPressed:(id)sender { [self moveBalls]; // self.playView.viewController = self; int xPos = self.view.frame.size.width-30; int yPos = self.view.frame.size.height-30; int btnXPos = arc4random_uniform(xPos); int btnYPos = arc4random_uniform(yPos); RoundButton *newButton = [[RoundButton alloc] initWithFrame:CGRectMake(btnXPos, btnYPos, 30, 30)]; [newButton setBackgroundColor:[UIColor redColor]]; newButton.layer.cornerRadius = newButton.frame.size.height/2; [newButton setTitle:[self randomStringWithLength:1] forState:UIControlStateNormal]; UILongPressGestureRecognizer *longGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longGestureRecognized:)]; [newButton addGestureRecognizer:longGesture]; [self.playView.playerArray addObject:newButton]; [self.playView addSubview:newButton]; } - (void)moveBalls { for (int i=0; i<[self.playView subviews].count; i++) { RoundButton *playerButton = [self.playView.subviews objectAtIndex:i]; if (playerButton.isEditPath) { id point = [[[playerButton.allPathsOfPlayer lastObject] points] lastObject]; CGPoint lastPoint = [point CGPointValue]; NSLog(@"lastPoint: %@", NSStringFromCGPoint(lastPoint)); [playerButton setCenter:lastPoint]; } } } 

In the move button, below the code goes, which allows me to simultaneously move my entire round button, which is my exact requirement:

 - (void)playAnimation { for (int i=0; i<self.playView.subviews.count; i++) { //Prepare the animation - we use keyframe animation for animations of this complexity CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"]; //Set some variables on the animation pathAnimation.calculationMode = kCAAnimationPaced; //We want the animation to persist - not so important in this case - but kept for clarity //If we animated something from left to right - and we wanted it to stay in the new position, //then we would need these parameters pathAnimation.fillMode = kCAFillModeForwards; pathAnimation.removedOnCompletion = NO; pathAnimation.duration = 2.0; pathAnimation.repeatCount = 0; RoundButton *tmpRoundButton; UIBezierPath *path; tmpRoundButton = [self.playView.subviews objectAtIndex:i]; for (int j=0; j<tmpRoundButton.allPathsOfPlayer.count; j++) { if (path) [path appendPath:[tmpRoundButton.allPathsOfPlayer objectAtIndex:j]]; else path = [tmpRoundButton.allPathsOfPlayer objectAtIndex:j]; //Now we have the path, we tell the animation we want to use this path - then we release the path pathAnimation.path = [path CGPath]; [tmpRoundButton.layer addAnimation:pathAnimation forKey:@"moveTheSquare"]; } [path moveToPoint:[[[path points] firstObject] CGPointValue]]; } [self.playView setNeedsDisplay]; } 
0
source

I assume that when you say โ€œcontrol the entire state of theโ€œ Rounded button โ€, you mean that you want to have a way to know that the view has started the animation, something like a callback viewHasMovedABitAutomatically :) If this is the case, as far as I know, there is no implemented way for CA to do this, however, look at this question: Callback progress for kernel animation Here you can make a workaround that looks pretty nice, maybe you can work on something from now on.

Oh, and one thing. I see that you are using a Pan gesture recognizer. I was just looking at both your code and tarmes (the guy in the other thread;)), but I think he used drawInContext to check the layer. It can also cause if your view is being dragged, so if that happens, consider using some viewIsBeingDragged flag in the pan method and check it on the callback.

EDIT: Sorry, this doesn't seem to have anything to do with your real problem. I understand that you mean controlling the speed of the animation so that they all move accordingly. Tell us about it.

As far as I know, there is no such thing as a "step" animation. Animating an image to move 20 pixels to the left does not necessarily mean that the image will be redrawn 20 times. The image will be displayed as many times as needed, so we cannot rely on the rendering method to calculate it. As far as I know, the only way to control the speed of an animation is with the duration animateWithDuration: parameter. And you do not know the duration, you want the speed to be constant, so the duration is what is required.

Basic physics: duration is equal to distance divided by speed, so if we had the distance we needed to cover, we could easily calculate the duration of the animation. At first I thought that maybe UIBezierPath had a method or attribute to calculate the total length, but I was wrong. However, there is code in this Get CGPath stream of total length that computes it through numerical integration, you should check it to see if it works. When you have this, you can do something like this (I have not tested it, I just entered the pseudo code)

 #define SPEED_CALIBRATION 30.0 // or whatever -(void)animateView:(UIView*)view withBezierPath:(UIBezierPath*)path { CGFloat distance = [self getBezierPathDistance:path]; CGFloat duration = distance / SPEED_CALIBRATION; [self animateView:view withDuration:duration andBezierPath:path]; } 

Something like that. getBezierPathDistance is the method from the stream above, if it works, and animateView:withDuration:andBezierPath: is your method for performing non-linear animation using the CA documentation (I donโ€™t remember all the details, but I remember that it was a rather long fragment code;)).

I apologize if the answer is a little vague, let him hope this helps!

+2
source

All Articles