I want to create an animation that moves up or down the screen according to how fast the user taps the screen. The problem I am having is that I don't know how to create an infinite loop so I am firing a timer which presents issues. Here is my current code.
-(void)setPosOfCider {
CGFloat originalY = CGRectGetMinY(cider.frame);
float oY = originalY;
float posY = averageTapsPerSecond * 100;
float dur = 0;
dur = (oY - posY) / 100;
[UIImageView animateWithDuration:dur animations:^(void) {
cider.frame = CGRectMake(768, 1024 - posY, 768, 1024);
}];
}
Suggested fix(Doesn't work):
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
scroll.pagingEnabled = YES;
scroll.scrollEnabled = YES;
scroll.contentSize = CGSizeMake(768 * 3, 1024); // 3 pages wide.
scroll.delegate = self;
self.speedInPointsPerSecond = 200000;
self.tapEvents = [NSMutableArray array];
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self startDisplayLink];
}
-(IBAction)tapped {
[self.tapEvents addObject:[NSDate date]];
// if less than two taps, no average speed
if ([self.tapEvents count] < 1)
return;
// only average the last three taps
if ([self.tapEvents count] > 2)
[self.tapEvents removeObjectAtIndex:0];
// now calculate the average taps per second of the last three taps
NSDate *start = self.tapEvents[0];
NSDate *end = [self.tapEvents lastObject];
self.averageTapsPerSecond = [self.tapEvents count] / [end timeIntervalSinceDate:start];
}
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.lastTime = CACurrentMediaTime();
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (CGFloat)yAxisValueBasedUponTapsPerSecond
{
CGFloat y = 1024 - (self.averageTapsPerSecond * 100.0);
return y;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
CFTimeInterval now = CACurrentMediaTime();
CGFloat elapsed = now - self.lastTime;
self.lastTime = now;
if (elapsed <= 0) return;
CGPoint center = self.cider.center;
CGFloat destinationY = [self yAxisValueBasedUponTapsPerSecond];
if (center.y == destinationY)
{
// we don't need to move it at all
return;
}
else if (center.y > destinationY)
{
// we need to move it up
center.y -= self.speedInPointsPerSecond * elapsed;
if (center.y < destinationY)
center.y = destinationY;
}
else
{
// we need to move it down
center.y += self.speedInPointsPerSecond * elapsed;
if (center.y > destinationY)
center.y = destinationY;
}
self.cider.center = center;
}
An even simpler approach is to employ the UIViewAnimationOptionBeginFromCurrentState option when using block-based animations:
[UIView animateWithDuration:2.0
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction
animations:^{
// set the new frame
}
completion:NULL];
That stops the current block-based animation and starts the new one from the current location. Note, effective iOS 8 and later, this is now the default behavior, generally obviating the need for the UIViewAnimationOptionBeginFromCurrentState altogether. In fact, the default behavior considers not only the position of the view in question, but also the current velocity, ensuring a smooth transition. See WWDC 2014 video Building Interruptible and Responsive Interactions for more information.)
Another approach would be to have your taps (which adjust the averageTapsPerSecond) to stop any existing animation, grab the view's current frame from the presentation layer (which reflects the state of the view mid-animation), and then just start a new animation:
// grab the frame from the presentation layer (which is the frame mid-animation)
CALayer *presentationLayer = self.viewToAnimate.layer.presentationLayer;
CGRect frame = presentationLayer.frame;
// stop the animation
[self.viewToAnimate.layer removeAllAnimations];
// set the frame to be location we got from the presentation layer
self.viewToAnimate.frame = frame;
// now, starting from that location, animate to the new frame
[UIView animateWithDuration:3.0 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
self.viewToAnimate.frame = ...; // set the frame according to the averageTapsPerSecond
} completion:nil];
In iOS 7, you would use the rendition of animateWithDuration with the initialSpringVelocity: parameter, to ensure a smooth transition from the object's current movement to the new animation. See the aforementioned WWDC video for examples.
Original answer:
To adjust a view on the basis of the number of taps per second, you could use a CADisplayLink, which is like a NSTimer, but ideally suited for animation efforts like this. Bottom line, translate your averageTapsPerSecond into a y coordinate, and then use the CADisplayLink to animate the moving of some view to that y coordinate:
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#interface ViewController ()
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (nonatomic) CFTimeInterval lastTime;
#property (nonatomic) CGFloat speedInPointsPerSecond;
#property (nonatomic, strong) NSMutableArray *tapEvents;
#property (nonatomic) CGFloat averageTapsPerSecond;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.speedInPointsPerSecond = 200.0;
self.tapEvents = [NSMutableArray array];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
[self.view addGestureRecognizer:tap];
}
- (void)handleTap:(UITapGestureRecognizer *)gesture
{
[self.tapEvents addObject:[NSDate date]];
// if less than two taps, no average speed
if ([self.tapEvents count] < 2)
return;
// only average the last three taps
if ([self.tapEvents count] > 3)
[self.tapEvents removeObjectAtIndex:0];
// now calculate the average taps per second of the last three taps
NSDate *start = self.tapEvents[0];
NSDate *end = [self.tapEvents lastObject];
self.averageTapsPerSecond = [self.tapEvents count] / [end timeIntervalSinceDate:start];
}
- (CGFloat)yAxisValueBasedUponTapsPerSecond
{
CGFloat y = 480 - self.averageTapsPerSecond * 100.0;
return y;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self startDisplayLink];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[self stopDisplayLink];
}
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.lastTime = CACurrentMediaTime();
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink
{
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
CFTimeInterval now = CACurrentMediaTime();
CGFloat elapsed = now - self.lastTime;
self.lastTime = now;
if (elapsed <= 0) return;
CGPoint center = self.viewToAnimate.center;
CGFloat destinationY = [self yAxisValueBasedUponTapsPerSecond];
if (center.y == destinationY)
{
// we don't need to move it at all
return;
}
else if (center.y > destinationY)
{
// we need to move it up
center.y -= self.speedInPointsPerSecond * elapsed;
if (center.y < destinationY)
center.y = destinationY;
}
else
{
// we need to move it down
center.y += self.speedInPointsPerSecond * elapsed;
if (center.y > destinationY)
center.y = destinationY;
}
self.viewToAnimate.center = center;
}
#end
Hopefully this illustrates the idea.
Also, to use the above, make sure you've added the QuartzCore framework to your project (though in Xcode 5, it seems to do that for you).
For standard, constant speed animation, you can use the following:
You would generally do this with an animation that repeats (i.e. UIViewAnimationOptionRepeat) and that autoreverses (i.e. UIViewAnimationOptionAutoreverse):
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat
animations:^{
cider.frame = ... // specify the end destination here
}
completion:nil];
For a simple up-down-up animation, that's all you need. No timers needed.
There is one line you need to add to Rob's answer.
[UIView animateWithDuration:dur delay:del options:UIViewAnimationOptionAutoreverse|UIViewAnimationOptionRepeat animations:^{
[UIView setAnimationRepeatCount:HUGE_VAL];
//Your code here
} completion:nil];
If you need to stop that animation, use this line (remember to add QuartzCore framework):
[view.layer removeAllAnimations];
Related
So here is what I am trying to do, I want there to be a queue of animations that occur. These cannot occur simualtaneously, because that would ruin the effect and only show the last animation in the queue. How can I create a queue that waits for the previous animation to complete before starting a new one (in Swift)?
The following shows only the final animation in the array of xpItems.
var xpItems = XPItem.loadCustomObjectWithKey()
for (var i = 0; i < xpItems.count; i++) { //for each XP Item.
let rewardName = xpItems[i].title
let rewardAmount = xpItems[i].xpAmount
println(rewardName + " " + String(rewardAmount))
let currentLevelXPRequired = 100
let newProgress = CGFloat(rewardAmount) / CGFloat(currentLevelXPRequired) as CGFloat
self.xpCircle.setProgress(xpCircle.progress + newProgress, animated: true)
}
//Rest of the code is Obj-c.
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated
{
if (self.progress == progress) {
return;
}
if (animated == NO) {
if (_displayLink) {
//Kill running animations
[_displayLink removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
_displayLink = nil;
}
[self setNeedsDisplay];
} else {
_animationStartTime = CACurrentMediaTime();
_animationFromValue = self.progress;
_animationToValue = progress;
if (!_displayLink) {
//Create and setup the display link
[self.displayLink removeFromRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(animateProgress:)];
[self.displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
} /*else {
//Reuse the current display link
}*/
}
}
- (void)animateProgress:(CADisplayLink *)displayLink
{
dispatch_async(dispatch_get_main_queue(), ^{
CGFloat dt = (displayLink.timestamp - _animationStartTime) / self.animationDuration;
if (dt >= 1.0) {
//Order is important! Otherwise concurrency will cause errors, because setProgress: will detect an animation in progress and try to stop it by itself. Once over one, set to actual progress amount. Animation is over.
[self.displayLink removeFromRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
self.displayLink = nil;
[super setProgress:_animationToValue animated:NO];
[self setNeedsDisplay];
return;
}
//Set progress
[super setProgress:_animationFromValue + dt * (_animationToValue - _animationFromValue) animated:YES];
[self setNeedsDisplay];
});
}
The super.setProgress[..]:
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated
{
_progress = progress;
}
Sorry about having the code be in obj-c/swift.
Most animation methods, certainly in UIKit have a completion handler that is optionally called when the animation is complete. All you need to do is create an array of UIViews or some other object that you can then perform an animation on (maybe an array of CGPoints), then just animate through all the objects in order. E.g:
- (void)animateArrayWithCompletion:(void(^)(void))handler
{
if (!viewArray.count) {
if (handler) {
handler();
}
}
else {
UIView *myView = viewArray.lastObject;
[viewArray removeObject:myView];
CGRect frame = myView.frame;
frame.origin.x += 100;
[UIView animateWithDuration:1 animations:^{
myView.frame = frame;
} completion:^(BOOL complete) {
[self animateArrayWithCompletion:handler];
}];
}
}
In my application I have a view that sits on top of another view. The user is able to swipe the top view to the right, which fades it out and "fades in" the back view.
When the user pans to the right and lets go, the animation kicks in and it should translate from its current position to the new off screen position. The problem is that the animation instead snaps the view back to its starting point and then does the animation.
Here is my code for the pan gesture and animation:
- (void)handlePanGesture:(UIPanGestureRecognizer *)recognizer
{
CGPoint translation = [recognizer translationInView:recognizer.view];
CGPoint velocity = [recognizer velocityInView:recognizer.view];
switch (recognizer.state) {
case UIGestureRecognizerStateBegan: {
[recognizer setTranslation:CGPointMake(self.slidingView.frame.origin.x, 0) inView:recognizer.view];
break;
}
case UIGestureRecognizerStateChanged: {
[self.slidingView setTransform:CGAffineTransformMakeTranslation(MAX(0,translation.x), 0)];
CGFloat percentage = fmaxf(0,(translation.x/recognizer.view.bounds.size.width));
[self.backgroundBlurImageView setAlpha:1-percentage];
[self.slidingView setAlpha:1-percentage];
[self.backgroundImageView setTransform:CGAffineTransformMakeScale(1 + (percentage * 0.05), 1 + (percentage * 0.05))];
[self.backgroundBlurImageView setTransform:CGAffineTransformMakeScale(1 + (percentage * 0.05), 1 + (percentage * 0.05))];
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled: {
if (velocity.x > 5.0 || (velocity.x >= -1.0 && translation.x > kMenuViewControllerMinimumPanDistanceToOpen * self.slidingView.bounds.size.width)) {
CGFloat transformedVelocity = velocity.x/ABS(self.slidingView.bounds.size.width - translation.x);
CGFloat duration = 0.66;
[self showViewAnimated:YES duration:duration initialVelocity:transformedVelocity];
} else {
[self hideViewAnimated:YES];
}
}
default:
break;
}
}
- (void)showViewAnimated:(BOOL)animated duration:(CGFloat)duration
initialVelocity:(CGFloat)velocity;
{
// animate
__weak typeof(self) blockSelf = self;
[UIView animateWithDuration:animated ? duration : 0.0 delay:0
usingSpringWithDamping:0.8f initialSpringVelocity:velocity options:UIViewAnimationOptionAllowUserInteraction animations:^{
blockSelf.slidingView.transform = CGAffineTransformMakeTranslation(blockSelf.slidingView.bounds.size.width, 0);
[blockSelf.backgroundBlurImageView setTransform:CGAffineTransformMakeScale(1.05, 1.05)];
[blockSelf.backgroundImageView setTransform:CGAffineTransformMakeScale(1.05, 1.05)];
[blockSelf.backgroundBlurImageView setAlpha:0];
[blockSelf.slidingView setAlpha:0];
} completion:^(BOOL finished) {
blockSelf.tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(didTapOnAaron:)];
[blockSelf.view addGestureRecognizer:self.tapGestureRecognizer];
}];
}
- (void)hideViewAnimated:(BOOL)animated;
{
__weak typeof(self) blockSelf = self;
[UIView animateWithDuration:0.3f animations:^{
blockSelf.slidingView.transform = CGAffineTransformIdentity;
[blockSelf.backgroundBlurImageView setTransform:CGAffineTransformIdentity];
[blockSelf.backgroundImageView setTransform:CGAffineTransformIdentity];
[blockSelf.backgroundBlurImageView setAlpha:1];
[blockSelf.slidingView setAlpha:1];
} completion:^(BOOL finished) {
[blockSelf.view removeGestureRecognizer:self.tapGestureRecognizer];
blockSelf.tapGestureRecognizer = nil;
}];
}
I have already tried settings the animation options to:
UIViewAnimationOptionBeginFromCurrentState
with no effect.
Anyone have a clue what I am missing? Thanks
Kyle, sometimes this sort of thing happens if you are using autolayout. If you don't need it try disabling it and see if it fixes it.
Second time on here hoping that someone can guide me in the right direction.
I'm trying to implement a Pong game within an app for iOS devices (iPhone/iPod/iPad) and it works just fine, except for the fact that I can't beat the CPU, let alone score a point. Here's the implementation .m code:
#import "BTM_pongGame.h"
#import <QuartzCore/QuartzCore.h>
#define STEP_DURATION 0.05
#define RESET_BALL_ANIMATION_DURATION 0.5f
#define MARGIN_WHERE_BALL_IS_LEAVING 40
//#define CPU_SKILL 20
#interface BTM_pongGame ()
#end
#implementation BTM_pongGame
#synthesize cpuSkill, ballShape;
- (void)viewDidLoad
{
[super viewDidLoad];
cpuSkill = [BT_strings getJsonPropertyValue:self.screenData.jsonVars nameOfProperty:#"cpuSkill" defaultValue:#"20"];
ballShape = [BT_strings getJsonPropertyValue:self.screenData.jsonVars nameOfProperty:#"ballShape" defaultValue:#"1"];
if ([ballShape isEqual: #"1"]) {
self.ball.layer.cornerRadius = self.ball.frame.size.width / 2;
} else {
//self.ball.layer.cornerRadius = self.ball.frame.size.width / 2;
}
}
- (void) wordsPerLineInfoShowAlert {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Choose Game"
message:#"Multiplayer or Single Player? You decide! Choose below."
delegate:self
cancelButtonTitle:#"Single Player"
otherButtonTitles:#"Go Back",#"Multi Player", nil];
[alert show];
}
- (void)alertView: (UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
NSString *title = [alertView buttonTitleAtIndex:buttonIndex];
if([title isEqualToString:#"Single Player"]) {
[self resetPlayerScoreLabels];
[self updatePlayerScoreLabels];
self.mode = kGameSinglePlayer;
[self moveComputerPaddle];
//Disable panning of computer paddle
[self.playerOne.superview setGestureRecognizers:#[]];
[self resetBoardForNewRound];
} else if ([title isEqualToString:#"Multi Player"]) {
[self resetPlayerScoreLabels];
[self updatePlayerScoreLabels];
self.mode = kGameMultiPlayer;
[self resetBoardForNewRound];
} else if ( [title isEqualToString:#"Go Back"]){
[self navLeftTap];
}
}
// thanks to MrsVanBeveren for coding the reset score stuff
-(void)resetPlayerScoreLabels
{
playerOneScore = 0;
playerTwoScore = 0;
}
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self wordsPerLineInfoShowAlert];
[self startGameTimer];
}
-(void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[_gameTimer invalidate];
}
-(void)quitPressed:(id)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
-(void)startGameTimer
{
[_gameTimer invalidate];
_gameTimer = [NSTimer scheduledTimerWithTimeInterval:STEP_DURATION target:self selector:#selector(step:) userInfo:nil repeats:YES];
}
#pragma mark Game setup
/*
* Some maths to calculate a new starting direction
* By choosing mid left/right part of the unit circle
* we avoid getting straight up/down directions
*/
-(void)resetBallDirection
{
float randomUnity = arc4random_uniform(100)/100.0;
int horizontalDirection = (arc4random() % 2 ? 1 : -1);
float angle = M_PI_4 + randomUnity * M_PI_2;
float direction = horizontalDirection * angle;
dirX = sin(direction);
dirY = cos(direction);
}
-(void)resetBoardForNewRound
{
speed = self.view.frame.size.width / 50.0;
[self resetBallDirection];
[_gameTimer invalidate];
[UIView animateWithDuration:RESET_BALL_ANIMATION_DURATION animations:^{
self.ball.center = CGPointMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0);
} completion:^(BOOL finished){
[self startGameTimer];
}];
[self enlargeAnimation:self.ball];
}
-(void)updatePlayerScoreLabels
{
self.playerOneScoreLabel.text = [NSString stringWithFormat:#"%d",playerOneScore];
self.playerTwoScoreLabel.text = [NSString stringWithFormat:#"%d",playerTwoScore];
}
#pragma mark Paddle handling
- (IBAction)paddlePanned:(UIPanGestureRecognizer*)recognizer {
UIView *paddleWrapper = recognizer.view;
UIView *paddle = [[paddleWrapper subviews] lastObject];
switch([recognizer state]) {
case UIGestureRecognizerStateBegan: {
paddle.backgroundColor = UIColor.whiteColor;
}
break;
case UIGestureRecognizerStateChanged: {
CGPoint position = [recognizer locationInView:self.view];
CGFloat haldPaddleHeight = paddleWrapper.frame.size.height / 2.0;
CGPoint newCenter = paddleWrapper.center;
newCenter.y = position.y;
newCenter.y = MAX(haldPaddleHeight, newCenter.y);
newCenter.y = MIN(self.view.bounds.size.height - haldPaddleHeight, newCenter.y);
paddleWrapper.center = newCenter;
}
break;
case UIGestureRecognizerStateEnded: {
paddle.backgroundColor = UIColor.grayColor;
}
break;
default:
break;
}
}
#pragma mark Game loop
/*
* Game loop
* - Moves the ball and checks for obstacles
*/
- (void)step:(NSTimer*)timer
{
speed += 0.05;
[self checkForBallLevingSide];
CGPoint newCenter = self.ball.center;
CGFloat ballRadius = self.ball.frame.size.height / 2.0;
newCenter.x += dirX * speed;
newCenter.y += dirY * speed;
CGFloat upperEdge = ballRadius;
CGFloat bottomEdge = self.view.bounds.size.height - ballRadius;
// Bounce ball of top/bottom walls
if (newCenter.y <= upperEdge) {
dirY = ABS(dirY);
newCenter.y = upperEdge;
} else if (newCenter.y >= bottomEdge) {
dirY = -ABS(dirY);
newCenter.y = bottomEdge;
}
[UIView animateWithDuration:STEP_DURATION animations:^{
self.ball.center = newCenter;
}];
}
-(BOOL)didBallHitPaddle:(UIView *)paddle withinLimit:(CGFloat)limit
{
if (CGRectIntersectsRect(paddle.frame, self.ball.frame)){
[self deflectBallFromPaddle:paddle];
CGRect ballFrame = self.ball.frame;
ballFrame.origin.x = limit;
self.ball.frame = ballFrame;
return YES;
} else
return NO;
}
-(void)checkForBallLevingSide
{
float limitLeft = MARGIN_WHERE_BALL_IS_LEAVING;
float limitRight = self.view.bounds.size.width - self.ball.frame.size.width - MARGIN_WHERE_BALL_IS_LEAVING;
CGRect ballFrame = self.ball.frame;
CGFloat ballX = ballFrame.origin.x;
if (ballX < limitLeft) {
if (![self didBallHitPaddle:self.playerOne.superview withinLimit:limitLeft])
[self playerDidMiss:kPlayerOne];
}else if (ballX > limitRight) {
if (![self didBallHitPaddle:self.playerTwo.superview withinLimit:limitRight])
[self playerDidMiss:kPlayerTwo];
}
}
/*
* Calculates new dirX and dirY after the bounce.
* The longer from the paddle's middle the bigger the result angle gets. (Pong style)
*/
-(void)deflectBallFromPaddle:(UIView *)paddle
{
dirX *= -1;
CGFloat diff = self.ball.center.y - paddle.center.y;
float p = diff / paddle.frame.size.height * 2.0f;
dirY += p * 0.5;
}
-(void)playerDidMiss:(kPlayer)player
{
if (player == kPlayerOne) {
playerTwoScore++;
[self victoryShake:self.playerTwo];
[self enlargeAnimation:self.playerTwoScoreLabel];
}
else if (player == kPlayerTwo) {
playerOneScore++;
[self victoryShake:self.playerOne];
[self enlargeAnimation:self.playerOneScoreLabel];
}
[self resetBoardForNewRound];
[self updatePlayerScoreLabels];
}
-(void)moveComputerPaddle
{
UIView *paddle = self.playerOne.superview;
CGPoint cpuCenter = paddle.center;
CGFloat diff = self.ball.center.y - paddle.center.y;
CGFloat movement = MIN((int)cpuSkill, ABS(diff));
movement *= diff > 0 ? 1 : -1;
cpuCenter.y += movement;
[UIView animateWithDuration:0.1 animations:^{
self.playerOne.superview.center = cpuCenter;
} completion:^(BOOL b) {
[self moveComputerPaddle];
}];
}
#pragma mark Animation
-(void)victoryShake:(UIView *)view
{
CAKeyframeAnimation * anim = [ CAKeyframeAnimation animationWithKeyPath:#"transform" ] ;
anim.values = [ NSArray arrayWithObjects:
[ NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0.0f, -20.0f, 0.0f) ],
[ NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0.0f, 20.0f, 0.0f) ],
nil ] ;
anim.autoreverses = YES ;
anim.repeatCount = 4.0f ;
anim.duration = 0.07f ;
[view.layer addAnimation:anim forKey:nil];
}
-(void)enlargeAnimation:(UIView *)view
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0, 1.0, 1.0)];
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(3.0, 3.0, 1.0)];
[animation setDuration:RESET_BALL_ANIMATION_DURATION * 0.5];
animation.repeatCount = 1.0f ;
[animation setAutoreverses:YES];
[view.layer addAnimation:animation forKey:nil];
}
#end
I've simply tried changing the number where it states #define CPU_SkILL and again #"cpuSkill" defaultValue:#"20"];, but I've had no luck with changing the number in any direction.
Any ideas?
Thank you in advance.
The cpuSkill variable controls the movement of the CPU-controlled paddle:
CGFloat movement = MIN((int)cpuSkill, ABS(diff));
So maybe the cpuSkill is higher than intended in every situation. Try much lower values or make some random little adjustment to movement after the calculation.
Also, you should ABS() the cpuSkill to avoid perfect moving when negative moving.
I want to add an UIImageView to my view at the users touch location and have the UIImageView grow while the user is holding their finger down. Think of a ballon being blown up. I want the center of the UIImageView to remain at the user's touch location while its growing.
I figured the best way would be a UILongPressGestureRecognizer and wrote the below. This does work as I planed except the visual effect is somewhat choppy and clumsy.
Is there any way that I can animate the UIImageView's size until the UILongPressGestureRecognizer calls UIGestureRecognizerStateEnded?
Or, is there a better way to do this altogether?
declared in .h: CGPoint longPressLocation;
.m:
- (IBAction) handleInflation:(UILongPressGestureRecognizer *) inflateGesture {
longPressLocation= [inflateGesture locationInView:self.view];
switch (inflateGesture.state) {
case UIGestureRecognizerStateBegan:{
NSLog(#"Long press Began .................");
inflateTimer = [NSTimer scheduledTimerWithTimeInterval:.4 target:self selector:#selector(inflate) userInfo:nil repeats:YES];
UIImage *tempImage=[UIImage imageNamed:#"bomb.png"];
UIImageView *inflatableImageView = [[UIImageView alloc] initWithFrame:CGRectMake(longPressLocation.x-tempImage.size.width/2,
longPressLocation.y-tempImage.size.height/2,
tempImage.size.width, tempImage.size.height)];
inflatableImageView.image = tempImage;
[bonusGame addSubview:inflatableImageView];
inflatable=inflatableImageView;
}
break;
case UIGestureRecognizerStateChanged:{
NSLog(#"Long press Changed .................");
}
break;
case UIGestureRecognizerStateEnded:{
NSLog(#"Long press Ended .................");
[inflateTimer invalidate];
}
break;
default:
break;
}
}
-(void)inflate{
inflatable.frame=CGRectMake(inflatable.frame.origin.x,inflatable.frame.origin.y , inflatable.bounds.size.width+15, inflatable.bounds.size.height+15);
inflatable.center=longPressLocation;
}
Final Working Code:
- (IBAction) handleInflation:(UILongPressGestureRecognizer *) inflateGesture {
inflateGesture.minimumPressDuration = .01;
longPressLocation= [inflateGesture locationInView:self.view];
switch (inflateGesture.state) {
case UIGestureRecognizerStateBegan:{
NSLog(#"Long press Began .................");
inflateStart = [NSDate date];
inflateDisplayLink = [NSClassFromString(#"CADisplayLink") displayLinkWithTarget:self selector:#selector(inflate)];
[inflateDisplayLink setFrameInterval:1];
[inflateDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
UIImage *tempImage=[UIImage imageNamed:#"bomb.png"];
UIImageView *inflatableImageView = [[UIImageView alloc] initWithFrame:CGRectMake(longPressLocation.x-tempImage.size.width/2,
longPressLocation.y-tempImage.size.height/2,
tempImage.size.width, tempImage.size.height)];
inflatableImageView.image = tempImage;
[bonusGame addSubview:inflatableImageView];
inflatable=inflatableImageView;
}
break;
case UIGestureRecognizerStateChanged:{
NSLog(#"Long press Changed .................");
}
break;
case UIGestureRecognizerStateEnded:{
NSLog(#"Long press Ended .................");
[inflateDisplayLink invalidate];
}
break;
default:
break;
}
}
-(void)inflate{
NSDate *inflateEnd = [NSDate date];
NSTimeInterval inflateInterval;
inflateInterval = ([inflateEnd timeIntervalSince1970] - [inflateStart timeIntervalSince1970])*25;
inflatable.frame=CGRectMake(inflatable.frame.origin.x,
inflatable.frame.origin.y ,
inflatable.bounds.size.width+inflateInterval,
inflatable.bounds.size.height+inflateInterval);
inflatable.center=longPressLocation;
if(inflatable.bounds.size.width>200){
[inflateDisplayLink invalidate];
}
}
A timer may not be smooth. Instead check out CADisplayLink, which will provide a delegate callback whenever the device's screen is redrawn (~60hz), so you will get a per frame chance to adjust your balloon size.
Another thing to consider is the time between refreshes isn't constant, it could be a lot slower than when the last refresh occured, so if you are incrementing the size by a constant 15 every time you get a callback, then the animation may not seem smooth.
To combat this, when you start the animation take a timestamp and hold onto it, then when you inflate the balloon take another timestamp and determine the difference between now and the last redraw, then multiply the difference by some value, which will ensure a constant smooth size growth - this is called a timestep.
https://developer.apple.com/library/ios/documentation/QuartzCore/Reference/CADisplayLink_ClassRef/Reference/Reference.html
Getting creative here, but I think this might work:
You start an animation that animates the images frame from its current size towards a maximum size.
Start the animation as soon as you detect the long press gesture.
If the gesture ends, get the current frame size from the presentationLayer of the animated view/image and update the view/image's frame to that size by starting a new animation with UIViewAnimationOptionBeginFromCurrentState so that the old animation will stop.
That should give you a smoothly growing balloon.
Try this....
imageView = [[UIImageView alloc] initWithFrame:(CGRectMake(120, 100, 30, 30))];
imageView.backgroundColor = [UIColor redColor];
[self.view addSubview:imageView];
imageView.userInteractionEnabled = TRUE;
UILongPressGestureRecognizer *g = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPress:)];
[g setMinimumPressDuration:0.001];
[imageView addGestureRecognizer:g];
And Add these methods
-(void)longPress:(UITapGestureRecognizer *)t
{
if (t.state == UIGestureRecognizerStateBegan)
{
[timer invalidate];
timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:#selector(makeIncrc) userInfo:nil repeats:TRUE];
}
else if (t.state == UIGestureRecognizerStateEnded || t.state == UIGestureRecognizerStateCancelled)
{
[timer invalidate];
timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:#selector(makeDec) userInfo:nil repeats:TRUE];
}
}
-(void)makeIncrc
{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.5];
[UIView setAnimationCurve:(UIViewAnimationCurveLinear)];
CGRect frame = imageView.frame;
frame.origin.x = frame.origin.x - 5;
frame.origin.y = frame.origin.y - 5;
frame.size.width = frame.size.width + 10;
frame.size.height = frame.size.height + 10;
imageView.frame = frame;
[UIView commitAnimations];
}
-(void)makeDec
{
if (imageView.frame.size.width < 20 || imageView.frame.size.height < 20)
{
[timer invalidate];
}
else
{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.5];
[UIView setAnimationCurve:(UIViewAnimationCurveLinear)];
CGRect frame = imageView.frame;
frame.origin.x = frame.origin.x + 5;
frame.origin.y = frame.origin.y + 5;
frame.size.width = frame.size.width - 10;
frame.size.height = frame.size.height - 10;
imageView.frame = frame;
[UIView commitAnimations];
}
}
Here's a Swift version. I had to replace invalidate() calls with "paused" in order to avoid runaway inflation.
var inflateDisplayLink: CADisplayLink?
var inflateStart: NSDate!
var longPressLocation: CGPoint!
var inflatable: UIImageView!
func handleInflation(inflateGesture: UILongPressGestureRecognizer) {
longPressLocation = inflateGesture.locationInView(self.view)
let imageView = inflateGesture.view as! UIImageView
switch (inflateGesture.state) {
case .Began:
println("Long press Began .................")
inflateStart = NSDate()
let tempImageView = UIImageView(image: imageView.image)
tempImageView.contentMode = .ScaleAspectFit
tempImageView.frame = imageView.frame
self.view.addSubview(tempImageView)
inflatable = tempImageView
if inflateDisplayLink == nil {
inflateDisplayLink = CADisplayLink(target: self, selector: Selector("inflate"))
inflateDisplayLink!.frameInterval = 1
inflateDisplayLink!.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
} else {
inflateDisplayLink!.paused = false
}
break
case .Changed:
println("Long press Changed .................")
break
case .Ended:
println("Long press Ended .................")
inflateDisplayLink!.paused = true
break
default:
break
}
}
func inflate() {
var inflateEnd = NSDate()
// Convert from Double to CGFloat, due to bug (?) on iPhone5
var inflateInterval: CGFloat = CGFloat((Double(inflateEnd.timeIntervalSince1970) - Double(inflateStart.timeIntervalSince1970))) * 5.0
inflatable.frame = CGRectMake(inflatable.frame.origin.x, inflatable.frame.origin.y, inflatable.bounds.size.width + inflateInterval, inflatable.bounds.size.height + inflateInterval)
inflatable.center = longPressLocation.center
if inflatable.bounds.size.width > 200 {
inflateDisplayLink!.paused = true
}
}
Is there a way to change contentOffset animation speed without creating your own animation that sets the contentOffset?
The reason why I can't create my own animation for contentOffset change is that this will not call -scrollViewDidScroll: in regular intervals during animation.
Unfortunately there is no clean and easy way to do that.
Here is a slightly brutal, but working approach:
1) Add CADisplayLink as a property:
#property (nonatomic, strong) CADisplayLink *displayLink;
2) Animate content offset:
CGFloat duration = 2.0;
// Create CADisplay link and add it to the run loop
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(_displayLinkTick)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[UIView animateWithDuration:duration animations:^{
self.scrollView.contentOffset = newContentOffset;
} completion:^(BOOL finished) {
// Cleanup the display link
[self.displayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
self.displayLink = nil;
}];
3) Finally observe the changes on presentationLayer like:
- (void)_displayLinkTick {
CALayer *presentationLayer = (CALayer *)self.scrollView.layer.presentationLayer;
CGPoint contentOffset = presentationLayer.bounds.origin;
[self _handleContentOffsetChangeWithOffset:contentOffset];
}
- (void)_handleContentOffsetChangeWithOffset:(CGPoint)offset {
// handle offset change
}
To get periodic information about the scroll state, you could run the animation in steps. The delegate will get called once (scrollViewDidScroll:) for each step
- (void)scrollTo:(CGPoint)offset completion:(void (^)(BOOL))completion {
// this presumes an outlet called scrollView. You could generalize by passing
// the scroll view, or even more generally, place this in a UIScrollView category
CGPoint contentOffset = self.scrollView.contentOffset;
// scrollViewDidScroll delegate will get called 'steps' times
NSInteger steps = 10;
CGPoint offsetStep = CGPointMake((offset.x-contentOffset.x)/steps, (offset.y-contentOffset.y)/steps);
NSMutableArray *offsets = [NSMutableArray array];
for (int i=0; i<steps; i++) {
CGFloat stepX = offsetStep.x * (i+1);
CGFloat stepY = offsetStep.y * (i+1);
NSValue *nextStep = [NSValue valueWithCGPoint:CGPointMake(contentOffset.x+stepX, contentOffset.y+stepY)];
[offsets addObject:nextStep];
}
[self scrollBySteps:offsets completion:completion];
}
// run several scroll animations back-to-back
- (void)scrollBySteps:(NSMutableArray *)offsets completion:(void (^)(BOOL))completion {
if (!offsets.count) return completion(YES);
CGPoint offset = [[offsets objectAtIndex:0] CGPointValue];
[offsets removeObjectAtIndex:0];
// total animation time == steps * duration. naturally, you can fool with both
// constants. to keep the rate constant, set duration == steps * k, where k
// is some constant time per step
[UIView animateWithDuration:0.1 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
self.scrollView.contentOffset = offset;
} completion:^(BOOL finished) {
[self scrollBySteps:offsets completion:completion];
}];
}
Call it like this...
CGPoint bottomOffset = CGPointMake(0, self.scrollView.contentSize.height - self.scrollView.bounds.size.height);
[self scrollTo:bottomOffset completion:^(BOOL finished) {}];
// BONUS completion handler! you can omit if you don't need it
Below code solved my issue
CGPoint leftOffset = CGPointMake(0, 0);
[UIView animateWithDuration:.5
delay:0
options:UIViewAnimationOptionCurveLinear
animations:^{
[self.topBarScrollView setContentOffset:leftOffset animated:NO];
} completion:nil];
Here's a simple solution that works.
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.5];
[self setContentOffset:<offset> animated:YES];
[UIView commitAnimations];
Hope this helps.
Brian