I'm trying to create a sort of "radar" that would look like this :
The idea is that the blue part will act like a compass to indicate proximity of stuff to the user. The shape would be narrow, long and blue when the user is far from the objective, and become shorter, thicker and red when he gets closer, as shown below:
To achieve this, I drew a few circles (actually I tried IBDesignable and IBInspectable but I keep getting "Build failed" but that is another subject)
My question is : Is this possible to draw the "cone" part that is supposed to change its dimensions at a high rate without risking lag ?
Can I use UIBezierPath / AddArc or Addline methods to achieve this or I am heading to a dead end ?
There are two approaches:
Define complex bezier shapes and animate use CABasicAnimation:
- (UIBezierPath *)pathWithCenter:(CGPoint)center angle:(CGFloat)angle radius:(CGFloat)radius {
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint point1 = CGPointMake(center.x - radius * sinf(angle), center.y - radius * cosf(angle));
[path moveToPoint:center];
[path addLineToPoint:point1];
NSInteger curves = 8;
CGFloat currentAngle = M_PI_2 * 3.0 - angle;
for (NSInteger i = 0; i < curves; i++) {
CGFloat nextAngle = currentAngle + angle * 2.0 / curves;
CGPoint point2 = CGPointMake(center.x + radius * cosf(nextAngle), center.y + radius * sinf(nextAngle));
CGFloat controlPointRadius = radius / cosf(angle / curves);
CGPoint controlPoint = CGPointMake(center.x + controlPointRadius * cosf(currentAngle + angle / curves), center.y + controlPointRadius * sinf(currentAngle + angle / curves));
[path addQuadCurveToPoint:point2 controlPoint:controlPoint];
currentAngle = nextAngle;
point1 = point2;
}
[path closePath];
return path;
}
I just split the arc into a series of quad curves. I find cubic curves can get closer, but with just a few quad curves, we get a very good approximation.
And then animate it with CABasicAnimation:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
CAShapeLayer *layer = [CAShapeLayer layer];
layer.fillColor = self.startColor.CGColor;
layer.path = self.startPath.CGPath;
[self.view.layer addSublayer:layer];
self.radarLayer = layer;
}
- (IBAction)didTapButton:(id)sender {
self.radarLayer.path = self.finalPath.CGPath;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
pathAnimation.fromValue = (id)self.startPath.CGPath;
pathAnimation.toValue = (id)self.finalPath.CGPath;
pathAnimation.removedOnCompletion = false;
self.radarLayer.fillColor = self.finalColor.CGColor;
CABasicAnimation *fillAnimation = [CABasicAnimation animationWithKeyPath:#"fillColor"];
fillAnimation.fromValue = (id)self.startColor.CGColor;
fillAnimation.toValue = (id)self.finalColor.CGColor;
fillAnimation.removedOnCompletion = false;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = #[pathAnimation, fillAnimation];
group.duration = 2.0;
[self.radarLayer addAnimation:group forKey:#"bothOfMyAnimations"];
}
That yields:
The other approach is to use simple bezier, but animate with display link:
If you wanted to use a CAShapeLayer and a CADisplayLink (which is like a timer that's optimized for screen updates), you could do something like:
#interface ViewController ()
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (nonatomic) CFAbsoluteTime startTime;
#property (nonatomic) CFAbsoluteTime duration;
#property (nonatomic, weak) CAShapeLayer *radarLayer;
#end
#implementation ViewController
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
CAShapeLayer *layer = [CAShapeLayer layer];
[self.view.layer addSublayer:layer];
self.radarLayer = layer;
[self updateRadarLayer:layer percent:0.0];
}
- (IBAction)didTapButton:(id)sender {
[self startDisplayLink];
}
- (void)updateRadarLayer:(CAShapeLayer *)radarLayer percent:(CGFloat)percent {
CGFloat angle = M_PI_4 * (1.0 - percent / 2.0);
CGFloat distance = 200 * (0.5 + percent / 2.0);
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:self.view.center];
[path addArcWithCenter:self.view.center radius:distance startAngle:M_PI_2 * 3.0 - angle endAngle:M_PI_2 * 3.0 + angle clockwise:true];
[path closePath];
radarLayer.path = path.CGPath;
radarLayer.fillColor = [self colorForPercent:percent].CGColor;
}
- (UIColor *)colorForPercent:(CGFloat)percent {
return [UIColor colorWithRed:percent green:0 blue:1.0 - percent alpha:1];
}
- (void)startDisplayLink {
self.startTime = CFAbsoluteTimeGetCurrent();
self.duration = 2.0;
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)handleDisplayLink:(CADisplayLink *)link {
CGFloat percent = (CFAbsoluteTimeGetCurrent() - self.startTime) / self.duration;
if (percent < 1.0) {
[self updateRadarLayer:self.radarLayer percent:percent];
} else {
[self stopDisplayLink];
[self updateRadarLayer:self.radarLayer percent:1.0];
}
}
- (void)stopDisplayLink {
[self.displayLink invalidate];
self.displayLink = nil;
}
That yields:
You should probably use one or more CAShapeLayer objects and Core Animation.
You can install the CGPath from a UIBezierPath into a shape layer, and you can animate the changes to the shape layers, although to get the animation to work right you want to have the same number and type of control points in the shape at the beginning and ending of your animation.
Related
I know how to draw the circle according to the offset.
If i drop down the tableView too fast to see the process of the draw,so i want to make it slower.How to make it? Thanks.
This is my circle.m
property progress is the offset (0.0~1.0).
- (void)drawRect:(CGRect)rect {
[WMFontColor888888 setStroke];
CGFloat startAngle = - M_PI * 80 / 180;
CGFloat step = 0.0;
step = 33 * M_PI/18 * self.progress;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2) radius:self.bounds.size.width / 2 - 3 startAngle:startAngle endAngle:startAngle + step clockwise:YES];
path.lineWidth = 1.5;
path.lineCapStyle = kCGLineCapRound;
path.lineJoinStyle = kCGLineJoinRound;
[path stroke];
}
If you dig into the layer level, you can subclass CAShapeLayer and use -[CAShapeLayer strokeStart] and -[CAShapeLayer strokeEnd]. Then, you can simply move your CG code into -drawInContext:
Example:
#implementation MyView
- (Class)layerClass
{
return [MyCircleLayer class];
}
- (void)setProgress:(CGFloat)progress
{
MyCircleLayer *layer = (id)self.layer;
[layer setProgress:progress];
}
#end
#interface MyCircleLayer : CAShapeLayer
#property (nonatomic, assign) CGFloat progress;
#end
#implementation MyCircleLayer
// Vary strokeStart/strokeEnd based on where or how you want to animate the drawing
- (void)setProgress:(CGFloat)progress
{
_progress = progress;
/**
Constantly updating strokeStart/strokeEnd should queue up redraws and
should draw fluidly, but this could be delayed with an interval or via layer animation
*/
[self setStrokeEnd:progress];
}
- (void)drawInContext:(CGContextRef)ctx
{
[WMFontColor888888 setStroke];
CGFloat startAngle = - M_PI * 80 / 180;
CGFloat step = 0.0;
// Draw the full circle
...
[path stroke];
}
I have created a simple audio player which plays a single audio. The views shows CAShapeLayer circular progress and also shows current time using CATextLayer. The figure below shows, the view:
Everything works fine until now, I can play, pause and the CAShapeLayer shows the progress. Now, I want to make it so that, when I touch the stroke (track) portion of the CAShapeLayer path, I would want to seek the player to that time. I tried few approaches but I could not detect touches on all parts of the stroke. It seems like the calculations I have done is not quite appropriate. I would be very happy if any body could help me with this.
Here is my complete code,
#interface ViewController ()
#property (nonatomic, weak) CAShapeLayer *progressLayer;
#property (nonatomic, weak) CAShapeLayer *trackLayer;
#property (nonatomic, weak) CATextLayer *textLayer;
#property (nonatomic, strong) AVAudioPlayer *audioPlayer;
#property (nonatomic, weak) NSTimer *audioPlayerTimer;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self prepareLayers];
[self prepareAudioPlayer];
[self prepareGestureRecognizers];
}
- (void)prepareLayers
{
CGFloat lineWidth = 40;
CGRect shapeRect = CGRectMake(0,
0,
CGRectGetWidth(self.view.bounds),
CGRectGetWidth(self.view.bounds));
CGRect actualRect = CGRectInset(shapeRect, lineWidth / 2.0, lineWidth / 2.0);
CGPoint center = CGPointMake(CGRectGetMidX(actualRect), CGRectGetMidY(actualRect));
CGFloat radius = CGRectGetWidth(actualRect) / 2.0;
UIBezierPath *track = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:0 endAngle:2 * M_PI
clockwise:true];
UIBezierPath *progressLayerPath = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:-M_PI_2
endAngle:2 * M_PI - M_PI_2
clockwise:true];
progressLayerPath.lineWidth = lineWidth;
CAShapeLayer *trackLayer = [CAShapeLayer layer];
trackLayer.contentsScale = [UIScreen mainScreen].scale;
trackLayer.shouldRasterize = YES;
trackLayer.rasterizationScale = [UIScreen mainScreen].scale;
trackLayer.bounds = actualRect;
trackLayer.strokeColor = [UIColor lightGrayColor].CGColor;
trackLayer.fillColor = [UIColor whiteColor].CGColor;
trackLayer.position = self.view.center;
trackLayer.lineWidth = lineWidth;
trackLayer.path = track.CGPath;
self.trackLayer = trackLayer;
CAShapeLayer *progressLayer = [CAShapeLayer layer];
progressLayer.contentsScale = [UIScreen mainScreen].scale;
progressLayer.shouldRasterize = YES;
progressLayer.rasterizationScale = [UIScreen mainScreen].scale;
progressLayer.masksToBounds = NO;
progressLayer.strokeEnd = 0;
progressLayer.bounds = actualRect;
progressLayer.fillColor = nil;
progressLayer.path = progressLayerPath.CGPath;
progressLayer.position = self.view.center;
progressLayer.lineWidth = lineWidth;
progressLayer.lineJoin = kCALineCapRound;
progressLayer.lineCap = kCALineCapRound;
progressLayer.strokeColor = [UIColor redColor].CGColor;
CATextLayer *textLayer = [CATextLayer layer];
textLayer.contentsScale = [UIScreen mainScreen].scale;
textLayer.shouldRasterize = YES;
textLayer.rasterizationScale = [UIScreen mainScreen].scale;
textLayer.font = (__bridge CTFontRef)[UIFont systemFontOfSize:30.0];
textLayer.position = self.view.center;
textLayer.bounds = CGRectMake(0, 0, 300, 100);
[self.view.layer addSublayer:trackLayer];
[self.view.layer addSublayer:progressLayer];
[self.view.layer addSublayer:textLayer];
self.trackLayer = trackLayer;
self.progressLayer = progressLayer;
self.textLayer = textLayer;
[self displayText:#"Play"];
}
- (void)prepareAudioPlayer
{
NSError *error;
self.audioPlayer = [[AVAudioPlayer alloc]
initWithContentsOfURL:[[NSBundle mainBundle] URLForResource:#"Song" withExtension:#"mp3"]
error:&error];
self.audioPlayer.volume = 0.2;
if (!self.audioPlayer) {
NSLog(#"Error occurred, could not create audio player");
return;
}
[self.audioPlayer prepareToPlay];
}
- (void)prepareGestureRecognizers
{
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(playerViewTapped:)];
[self.view addGestureRecognizer:tap];
}
- (void)playerViewTapped:(UITapGestureRecognizer *)tap
{
CGPoint tappedPoint = [tap locationInView:self.view];
if ([self.view.layer hitTest:tappedPoint] == self.progressLayer) {
CGPoint locationInProgressLayer = [self.view.layer convertPoint:tappedPoint toLayer:self.progressLayer];
NSLog(#"Progress view tapped %#", NSStringFromCGPoint(locationInProgressLayer));
// this is called sometimes but not always when I tap the stroke
} else if ([self.view.layer hitTest:tappedPoint] == self.textLayer) {
if ([self.audioPlayer isPlaying]) {
[self.audioPlayerTimer invalidate];
[self displayText:#"Play"];
[self.audioPlayer pause];
} else {
[self.audioPlayer play];
self.audioPlayerTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(increaseProgress:)
userInfo:nil
repeats:YES];
}
}
}
- (void)increaseProgress:(NSTimer *)timer
{
NSTimeInterval currentTime = self.audioPlayer.currentTime;
NSTimeInterval totalDuration = self.audioPlayer.duration;
float progress = currentTime / totalDuration;
self.progressLayer.strokeEnd = progress;
int minute = ((int)currentTime) / 60;
int second = (int)currentTime % 60;
NSString *progressString = [NSString stringWithFormat:#"%02d : %02d ", minute,second];
[self displayText:progressString];
}
- (void)displayText:(NSString *)text
{
UIColor *redColor = [UIColor redColor];
UIFont *font = [UIFont fontWithName:#"HelveticaNeue" size:70];
NSDictionary *attribtues = #{
NSForegroundColorAttributeName: redColor,
NSFontAttributeName: font,
};
NSAttributedString *progressAttrString = [[NSAttributedString alloc] initWithString:text
attributes:attribtues];
self.textLayer.alignmentMode = kCAAlignmentCenter;
self.textLayer.string = progressAttrString;
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
void(^animationBlock)(id<UIViewControllerTransitionCoordinatorContext>) =
^(id<UIViewControllerTransitionCoordinatorContext> context) {
CGRect rect = (CGRect){.origin = CGPointZero, .size = size};
CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
self.progressLayer.position = center;
self.textLayer.position = center;
self.trackLayer.position = center;
};
[coordinator animateAlongsideTransitionInView:self.view
animation:animationBlock completion:nil];
}
#end
As far as I know the CALAyer hitTest method does a very primitive bounds check. If the point is inside the layer's bounds, it returns YES, otherwise it returns NO.
CAShapeLayer doesn't make any attempt to tell if the point intersects the shape layer's path.
UIBezierPath does have a hitTest method, but I'm pretty sure that detects touches in the interior of a closed path, and would not work with a thick line arc line the one you're using.
If you want to hit test using paths I think you're going to have to roll your own hitTest logic that builds a UIBezierPath that is a closed path defining the shape you are testing for (your thick arc line). Doing that for an animation that's in-flight might be too slow.
Another problem you'll face: You're interrogating a layer that has an in-flight animation. Even when you're simply animating the center property of a layer that doesn't work. Instead you have to interrogate the layer's presentationLayer, which is a layer that approximates the settings of an in-flight animation.
I think your best solution will be to take the starting and ending position of your arc, convert the start-to end value to starting and ending angles, use trig to convert your touch position to an angle and radius, and see if the touch position is in the range of the start-to-end touch and within the inner and outer radius of the indicator line. That would just require some basic trig.
Edit:
Hint: The trig function you want to use to convert x and y position to an angle is arc tangent. arc tangent takes an X and a Y value and gives back an angle. However, to use the arc tangent properly you need to implement a bunch of logic to figure out what quadrant of the circle your point is in.In C, the math library function you want is atan2(). The atan2() function takes care of everything for you. You'll convert your point so 0,0 is in the center, and atan2() will give you the angle along the circle. To calculate the distance from the center of the circle you use the distance formula, a² + b² = c².
I am trying to draw a circle that will be used to indicate progress. The progress updates might come in quickly, and I want to animate the changes to the circle.
I have tried doing this with the below methods, but nothing seems to work. Is this possible?
- (id)initWithFrame:(CGRect)frame strokeWidth:(CGFloat)strokeWidth insets:(UIEdgeInsets)insets
{
if (self = [super initWithFrame:frame])
{
self.strokeWidth = strokeWidth;
CGPoint arcCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
CGFloat radius = CGRectGetMidX(self.bounds) - insets.top - insets.bottom;
self.circlePath = [UIBezierPath bezierPathWithArcCenter:arcCenter
radius:radius
startAngle:M_PI
endAngle:-M_PI
clockwise:YES];
[self addLayer];
}
return self;
}
- (void)setProgress:(double)progress {
_progress = progress;
[self updateAnimations];
}
- (void)addLayer {
CAShapeLayer *progressLayer = [CAShapeLayer layer];
progressLayer.path = self.circlePath.CGPath;
progressLayer.strokeColor = [[UIColor whiteColor] CGColor];
progressLayer.fillColor = [[UIColor clearColor] CGColor];
progressLayer.lineWidth = self.strokeWidth;
progressLayer.strokeStart = 10.0f;
progressLayer.strokeEnd = 40.0f;
[self.layer addSublayer:progressLayer];
self.currentProgressLayer = progressLayer;
}
- (void)updateAnimations{
self.currentProgressLayer.strokeEnd = self.progress;
[self.currentProgressLayer didChangeValueForKey:#"endValue"];
}
Currently this code doesn't even draw the circle, but removing the progressLayer's strokeStart and strokeEnd will draw the full circle.
How can I get it to begin at a certain point and start drawing the circle based on me setting my progress property?
I figured it out. The strokeStart and strokeEnd values are from 0.0 to 1.0, so the values I was giving it were way too high.
So, I just updated my setting to:
- (void)setProgress:(double)progress {
_progress = progress * 0.00460;
[self updateAnimations];
}
My answer with example here. In general you should add the animation to CAShapeLayer.
I have a CAShapeLayer in which an arc is added using UIBezierPath. I saw a couple of posts (one actually) here on stackoverflow but none seemed to give me the answer. As said in the title I would like to animate an arc (yes it will be for a pie chart).
How can one accomplish an animation from an "empty" arc to the fully extended one? Kinda like a curved progress bar. Is it really impossible to do it via CoreAnimation?
Here's how I "do" the arc. Oh and ignore the comments and calculation since I'm used to counter clockwise unit circle and apple goes clockwise. The arcs appear just fine, I only need animation:
//Apple's unit circle goes in clockwise rotation while im used to counter clockwise that's why my angles are mirrored along x-axis
workAngle = 6.283185307 - DEGREES_TO_RADIANS(360 * item.value / total);
//My unit circle, that's why negative currentAngle
[path moveToPoint:CGPointMake(center.x + cosf(outerPaddingAngle - currentAngle) * (bounds.size.width / 2), center.y - (sinf(outerPaddingAngle - currentAngle) * (bounds.size.height / 2)))];
//Apple's unit circle, clockwise rotation, we add the angles
[path addArcWithCenter:center radius:120 startAngle:currentAngle endAngle:currentAngle + workAngle clockwise:NO];
//My unit circle, counter clockwise rotation, we subtract the angles
[path addLineToPoint:CGPointMake(center.x + cosf(-currentAngle - workAngle) * ((bounds.size.width / 2) - pieWidth), center.y - sinf(-currentAngle - workAngle) * ((bounds.size.height / 2) - pieWidth))];
//Apple's unit circle, clockwise rotation, addition of angles
[path addArcWithCenter:center radius:120 - pieWidth startAngle:currentAngle + workAngle endAngle:currentAngle - innerPaddingAngle clockwise:YES];
//No need for my custom calculations since I can simply close the path
[path closePath];
shape = [CAShapeLayer layer];
shape.frame = self.bounds;
shape.path = path.CGPath;
shape.fillColor = kRGBAColor(255, 255, 255, 0.2f + 0.1f * (i + 1)).CGColor; //kRGBAColor is a #defined
[self.layer addSublayer:shape];
//THIS IS THE PART IM INTERESTED IN
if (_shouldAnimate)
{
CABasicAnimation *pieAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
pieAnimation.duration = 1.0;
pieAnimation.removedOnCompletion = NO;
pieAnimation.fillMode = kCAFillModeForwards;
//from and to are just dummies so I dont get errors
pieAnimation.fromValue = [NSNumber numberWithInt:0]; //from what
pieAnimation.toValue = [NSNumber numberWithFloat:1.0f]; //to what
[shape addAnimation:pieAnimation forKey:#"animatePath"];
}
Here is an implementation of a CALayer based on CAShapeLayer:
ProgressLayer.h/m
#import <QuartzCore/QuartzCore.h>
#interface ProgressLayer : CAShapeLayer
-(void) computePath:(CGRect)r;
-(void) showProgress;
#end
#import "ProgressLayer.h"
#implementation ProgressLayer
-(id)init {
self=[super init];
if (self) {
self.path = CGPathCreateWithEllipseInRect(CGRectMake(0, 0, 50, 50), 0);
self.strokeColor = [UIColor greenColor].CGColor;
self.lineWidth=40;
self.lineCap = kCALineCapRound;
self.strokeEnd=0.001;
}
return self;
}
-(void) computePath:(CGRect)r {
self.path = CGPathCreateWithEllipseInRect(r, 0);
}
-(void)showProgress {
float advance=0.1;
if (self.strokeEnd>1) self.strokeEnd=0;
CABasicAnimation * swipe = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
swipe.duration=0.25;
swipe.fromValue=[NSNumber numberWithDouble:self.strokeEnd];
swipe.toValue= [NSNumber numberWithDouble:self.strokeEnd + advance];
swipe.fillMode = kCAFillModeForwards;
swipe.timingFunction= [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
swipe.removedOnCompletion=NO;
self.strokeEnd = self.strokeEnd + advance;
[self addAnimation:swipe forKey:#"strokeEnd animation"];
}
#end
I used this layer as a backing layer of an UIView:
#import "ProgressView.h"
#import "ProgressLayer.h"
#implementation ProgressView
-(id)initWithCoder:(NSCoder *)aDecoder {
self=[super initWithCoder:aDecoder];
if (self) {
[(ProgressLayer *)self.layer computePath:self.bounds];
}
return self;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
ProgressLayer * l = self.layer;
[l showProgress];
}
- (void)drawRect:(CGRect)rect {
[[UIColor redColor] setStroke];
UIRectFrame(self.bounds);
}
+(Class)layerClass {
return [ProgressLayer class];
}
#end
Before you create your animation you want to set your shape's strokeEnd to 0.
Then, use the key #"strokeEnd" instead of #"path"... Keys for CAAnimations are generally the name of the property you want to animate.
Then you can do the following:
[CATransaction begin]
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 1.0f;
animation.removedOnCompletion = NO;
animation.fromValue = #0; //shorthand for creating an NSNumber
animation.toValue = #1; //shorthand for creating an NSNumber
[self addAnimation:animation forKey:#"animateStrokeEnd"];
[CATransaction commit];
You have to use the proper keys. "path" and "animatePath" don't mean anything to it - you have to use "strokeEnd" for your key/keypath. If you need to fiddle with your fill color, you use fillColor. The full list is here:
https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CAShapeLayer_class/Reference/Reference.html
I've never worked with animations on iOS specifically and was hoping to be pointed in the right direction. I have so far animated a bunch of pill shaped objects in a fashion similar to what I have below, however I have yet to implement the correct masking based on the y positioning of the shape.
The pill shapes are just UIViews. I'm looking for a way to get their backgrounds to change based on their positioning; the background itself does not move, just the pills.
EDIT:
Based on Rob's answer below, I am now tying the animation to the device's movement like so:
- (void)setAnimationToDeviceMotion {
motionManager = [[CMMotionManager alloc] init];
motionManager.deviceMotionUpdateInterval = 0.1f;
NSOperationQueue *motionQueue = [[NSOperationQueue alloc] init];
[motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXArbitraryZVertical toQueue:motionQueue withHandler:^(CMDeviceMotion *motion, NSError *error) {
if (!error) {
double pitch = motion.attitude.pitch;
NSLog(#"%f", pitch);
[self addMaskToView:self.imageView withAdjustment:pitch];
}
}];
}
Assuming this is eight pill shapes that will reveal a single image underneath, you
create a UIBezierPath for the eight pill shapes
create a CAShapeLayer layer that uses that UIBezierPath (or more accurately, its CGPath)
apply this CAShapeLayer as the mask for the layer for your UIImageView
If you want to animate this over time, just replace the mask (e.g. in response to a accelerometer event, a CADisplayLink or NSTimer, or a UIPanGesture), or just update the path of the CAShapeLayer you have applied as the mask to the UIImageView's layer.
Thus, you might have something like:
- (void)addMaskToView:(UIView *)view withAdjustment:(CGFloat)adjustment
{
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = [[self pillsPathWithAdjustment:adjustment forView:view] CGPath];
self.imageView.layer.mask = mask;
}
- (UIBezierPath *)pillsPathWithAdjustment:(CGFloat)adjustment forView:(UIView *)view
{
UIBezierPath *path = [UIBezierPath bezierPath];
for (NSInteger i = 0; i < kNumberOfPills; i++)
[self addPillPathNumber:i forView:view toPath:path adjustment:adjustment];
return path;
}
- (void)addPillPathNumber:(NSInteger)index forView:(UIView *)view toPath:(UIBezierPath *)path adjustment:(CGFloat)adjustment
{
CGFloat const pillSpacing = 5.0f;
CGFloat const pillWidth = view.bounds.size.width / kNumberOfPills - pillSpacing;
CGFloat const pillHeight = view.bounds.size.height * 0.4;
CGFloat const cornerRounding = 10.0f;
CGPoint point = CGPointMake(index * (pillWidth + pillSpacing) + pillSpacing / 2.0, sinf((float) index * M_PI / kNumberOfPills + adjustment * M_PI * 2.0) * 100.0);
// top
point.x += cornerRounding;
[path moveToPoint:point];
point.x += pillWidth - cornerRounding * 2.0;
[path addLineToPoint:point];
// top right corner
[path addArcWithCenter:CGPointMake(point.x, point.y + cornerRounding) radius:cornerRounding startAngle:3 * M_PI_2 endAngle:2.0 * M_PI clockwise:YES];
point.x += cornerRounding;
point.y += cornerRounding;
// right
point.y += pillHeight - cornerRounding * 2.0;
[path addLineToPoint:point];
// lower right corner
[path addArcWithCenter:CGPointMake(point.x - cornerRounding, point.y) radius:cornerRounding startAngle:0 endAngle:M_PI_2 clockwise:YES];
// bottom
point.y += cornerRounding;
point.x -= cornerRounding;
point.x -= pillWidth - cornerRounding * 2.0;
[path addLineToPoint:point];
// lower left corner
[path addArcWithCenter:CGPointMake(point.x, point.y - cornerRounding) radius:cornerRounding startAngle:M_PI_2 endAngle:M_PI clockwise:YES];
// left
point.y -= cornerRounding;
point.x -= cornerRounding;
point.y -= pillHeight - cornerRounding * 2.0;
[path addLineToPoint:point];
// upper left corner
[path addArcWithCenter:CGPointMake(point.x + cornerRounding, point.y) radius:cornerRounding startAngle:M_PI endAngle:3.0 * M_PI_2 clockwise:YES];
[path closePath];
}
By way of illustration, this is how I'd animate this using a CADisplayLink:
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.startTime = CACurrentMediaTime();
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink
{
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
CGFloat seconds;
CGFloat fractionsOfSecond = modff(CACurrentMediaTime() - self.startTime, &seconds);
[self addMaskToView:self.imageView withAdjustment:fractionsOfSecond];
}
To do masking, you'll need to work with the view's layers. Instead of moving the views themselves up and down, create views that occupy the whole height of the parent, and then create a CALayer in the pill shape. Set the pill layer as the view's layer's mask, and move it up and down.
Something like this:
UIImageView *myImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"redAndBlueBackground"]];
CALayer *pillLayer = [[CALayer alloc] initWithFrame:CGRectMake(0, 0, myImageView.bounds.size.width, myImageView.bounds.size.height / 2];
pillLayer.cornerRadius = 8;
myImageView.layer.maskLayer = pillLayer;
// move the pill layer to height y:
pillLayer.center = CGPointMake(myImageView.bounds.size.width / 2,
(myImageView.bounds.size.height / 2) + y);