i am writing an App where i use UIKit Dynamics to simulate the interactions of different circles with one another.
I create my circles with the following code:
self = [super initWithFrame:CGRectMake(location.x - radius/2.0, location.y - radius/2, radius, radius)];
if (self) {
[self.layer setCornerRadius: radius /2.0f];
self.clipsToBounds = YES;
self.layer.masksToBounds = YES;
self.backgroundColor = color;
self.userInteractionEnabled = NO;
}
return self;
where location represents the desired location of the circle, and radius its radius.
I then add these circles to different UIBehaviours, by doing:
[_collision addItem:circle];
[_gravity addItem:circle];
[_itemBehaviour addItem:circle];
The itemBaviour is defined as follows:
_itemBehaviour = [[UIDynamicItemBehavior alloc] initWithItems:#[square]];
_itemBehaviour.elasticity = 1;
_itemBehaviour.friction = 0;
_itemBehaviour.resistance = 0;
_itemBehaviour.angularResistance = 0;
_itemBehaviour.allowsRotation = NO;
The problem i am having, is that my circles are behaving as squares. When hit in certain ways they gain angular momentum and lose speed. If they collide again, sometimes the angular momentum is again reverted to speed. This looks normal for squares, but when the view is round, like in my case, this behaviour looks weird and unnatural.
Turning on some debug options, i made this screenshot:
As you can see, the circle is appearently a square.
So my question is, how can i create an UIVIew that is truly a circle and will behave as such in UIKit Dynamics?
I know this question predated iOS 9, but for the benefit of future readers, you can now define a view with collisionBoundsType of UIDynamicItemCollisionBoundsTypePath and a circular collisionBoundingPath.
So, while you cannot "create an UIView that is truly a circle", you can define a path that defines both the shape that is rendered inside the view as well as the collision boundaries for the animator, yielding an effect of a round view (even though the view, itself, is obviously still rectangular, as all views are):
#interface CircleView: UIView
#property (nonatomic) CGFloat lineWidth;
#property (nonatomic, strong) CAShapeLayer *shapeLayer;
#end
#implementation CircleView
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self configure];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self configure];
}
return self;
}
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (void)configure {
self.translatesAutoresizingMaskIntoConstraints = false;
// create shape layer for circle
self.shapeLayer = [CAShapeLayer layer];
self.shapeLayer.strokeColor = [[UIColor blueColor] CGColor];
self.shapeLayer.fillColor = [[[UIColor blueColor] colorWithAlphaComponent:0.5] CGColor];
self.lineWidth = 3;
[self.layer addSublayer:self.shapeLayer];
}
- (void)layoutSubviews {
[super layoutSubviews];
// path of shape layer is with respect to center of the `bounds`
CGPoint center = CGPointMake(self.bounds.origin.x + self.bounds.size.width / 2, self.bounds.origin.y + self.bounds.size.height / 2);
self.shapeLayer.path = [[self circularPathWithLineWidth:self.lineWidth center:center] CGPath];
}
- (UIDynamicItemCollisionBoundsType)collisionBoundsType {
return UIDynamicItemCollisionBoundsTypePath;
}
- (UIBezierPath *)collisionBoundingPath {
// path of collision bounding path is with respect to center of the dynamic item, so center of this path will be CGPointZero
return [self circularPathWithLineWidth:0 center:CGPointZero];
}
- (UIBezierPath *)circularPathWithLineWidth:(CGFloat)lineWidth center:(CGPoint)center {
CGFloat radius = (MIN(self.bounds.size.width, self.bounds.size.height) - self.lineWidth) / 2;
return [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:0 endAngle:M_PI * 2 clockwise:true];
}
#end
Then, when you do your collision, it will honor the collisionBoundingPath values:
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
// create circle views
CircleView *circle1 = [[CircleView alloc] initWithFrame:CGRectMake(60, 100, 80, 80)];
[self.view addSubview:circle1];
CircleView *circle2 = [[CircleView alloc] initWithFrame:CGRectMake(250, 150, 120, 120)];
[self.view addSubview:circle2];
// have them collide with each other
UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:#[circle1, circle2]];
[self.animator addBehavior:collision];
// with perfect elasticity
UIDynamicItemBehavior *behavior = [[UIDynamicItemBehavior alloc] initWithItems:#[circle1, circle2]];
behavior.elasticity = 1;
[self.animator addBehavior:behavior];
// and push one of the circles
UIPushBehavior *push = [[UIPushBehavior alloc] initWithItems:#[circle1] mode:UIPushBehaviorModeInstantaneous];
[push setAngle:0 magnitude:1];
[self.animator addBehavior:push];
That yields:
By the way, it should be noted that the documentation outlines a few limitations to the path:
The path object you create must represent a convex polygon with counter-clockwise or clockwise winding, and the path must not intersect itself. The (0, 0) point of the path must be located at the center point of the corresponding dynamic item. If the center point does not match the path’s origin, collision behaviors may not work as expected.
But a simple circle path easily meets those criteria.
Or, for Swift users:
class CircleView: UIView {
var lineWidth: CGFloat = 3
var shapeLayer: CAShapeLayer = {
let _shapeLayer = CAShapeLayer()
_shapeLayer.strokeColor = UIColor.blue.cgColor
_shapeLayer.fillColor = UIColor.blue.withAlphaComponent(0.5).cgColor
return _shapeLayer
}()
override func layoutSubviews() {
super.layoutSubviews()
layer.addSublayer(shapeLayer)
shapeLayer.lineWidth = lineWidth
let center = CGPoint(x: bounds.midX, y: bounds.midY)
shapeLayer.path = circularPath(lineWidth: lineWidth, center: center).cgPath
}
private func circularPath(lineWidth: CGFloat = 0, center: CGPoint = .zero) -> UIBezierPath {
let radius = (min(bounds.width, bounds.height) - lineWidth) / 2
return UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
}
override var collisionBoundsType: UIDynamicItemCollisionBoundsType { return .path }
override var collisionBoundingPath: UIBezierPath { return circularPath() }
}
class ViewController: UIViewController {
let animator = UIDynamicAnimator()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let circle1 = CircleView(frame: CGRect(x: 60, y: 100, width: 80, height: 80))
view.addSubview(circle1)
let circle2 = CircleView(frame: CGRect(x: 250, y: 150, width: 120, height: 120))
view.addSubview(circle2)
animator.addBehavior(UICollisionBehavior(items: [circle1, circle2]))
let behavior = UIDynamicItemBehavior(items: [circle1, circle2])
behavior.elasticity = 1
animator.addBehavior(behavior)
let push = UIPushBehavior(items: [circle1], mode: .instantaneous)
push.setAngle(0, magnitude: 1)
animator.addBehavior(push)
}
}
Ok, well first off.
The debug options you enabled show areas of transparent cells. The view that is the circle is actually a square with rounded edges.
All views are rectangular. The way they appear circular is by making the corners transparent (hence corner radius).
Second, what is it you're trying to do with UIKit Dynamics? What is on the screen looks like you're trying to create a game of some sort.
Dynamics is meant to be used for more natural and real looking animation of UI. It isn't meant to be a full-on physics engine.
If you want something like that then you're best using Sprite Kit.
Related
Given two UIViews with borders that have UIPanGestureRecognizers attached to them:
If I drag the UIView on the left over the UIView on the right, this is the usual behavior:
Is it possible to get them to do the behavior below where it looks like they merge?:
Looking for the simplest way possible to do this!
One way is to use multiple sibling layers and zPosition. To achieve the effect you add two layers, one for border, one for content. And the border layer has a smaller zPosition than the content. And, of course, move the layers with the UIPanGestureRecognizer.
MP4 version
Swift:
import UIKit
class MergingView: UIView {
let borderLayer = CALayer()
let backgroundLayer = CALayer()
override func layoutSubviews() {
super.layoutSubviews()
addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))))
borderLayer.borderWidth = 5
borderLayer.frame = frame
borderLayer.zPosition = 10
borderLayer.borderColor = UIColor.black.cgColor
superview?.layer.addSublayer(borderLayer)
backgroundLayer.frame = CGRect(x: frame.origin.x + 5, y: frame.origin.y + 5, width: frame.width - 10, height: frame.height - 10)
backgroundLayer.zPosition = 20
backgroundLayer.backgroundColor = UIColor.white.cgColor
superview?.layer.addSublayer(backgroundLayer);
}
#objc func handlePan(_ recognizer: UIPanGestureRecognizer) {
CATransaction.begin()
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
let translation = recognizer.translation(in: self)
frame = self.frame.offsetBy(dx: translation.x, dy: translation.y)
recognizer.setTranslation(CGPoint.zero, in: self)
borderLayer.frame = borderLayer.frame.offsetBy(dx: translation.x, dy: translation.y)
backgroundLayer.frame = backgroundLayer.frame.offsetBy(dx: translation.x, dy: translation.y)
CATransaction.commit()
}
}
Objective-C header:
#import <UIKit/UIKit.h>
#interface MVMergingView : UIView
#end
Objective-C implementation:
#import "MVMergingView.h"
#interface MVMergingView ()
#property (strong) CALayer *borderLayer;
#property (strong) CALayer *backgroundLayer;
#end
#implementation MVMergingView
- (void)layoutSubviews {
[super layoutSubviews];
[self addGestureRecognizer:[[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)]];
CALayer *borderLayer = [CALayer layer];
borderLayer.borderWidth = 5.f;
borderLayer.frame = self.frame;
borderLayer.zPosition = 10;
borderLayer.borderColor = UIColor.blackColor.CGColor;
self.borderLayer = borderLayer;
[self.superview.layer addSublayer:borderLayer];
CALayer *backgroundLayer = [CALayer layer];
backgroundLayer.frame = CGRectMake(self.frame.origin.x + 5.f, self.frame.origin.y + 5.f, self.frame.size.width - 10, self.frame.size.height - 10);
backgroundLayer.zPosition = 20;
backgroundLayer.backgroundColor = UIColor.whiteColor.CGColor;
self.backgroundLayer = backgroundLayer;
[self.superview.layer addSublayer:backgroundLayer];
}
- (void)handlePan:(UIPanGestureRecognizer *)recognizer {
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
CGPoint translation = [recognizer translationInView:self];
self.frame = CGRectOffset(self.frame, translation.x, translation.y);
[recognizer setTranslation:CGPointZero inView:self];
self.borderLayer.frame = CGRectOffset(self.borderLayer.frame, translation.x, translation.y);
self.backgroundLayer.frame = CGRectOffset(self.backgroundLayer.frame, translation.x, translation.y);
[CATransaction commit];
}
#end
Example repo: https://github.com/dimitarnestorov/MergingView
It seems like your requirement to "merge" rather than overlap exists only because of the borders. If there were no borders, then there's no need for any merging; you simply overlap them. So the question becomes how to deal with the borders.
I'm thinking perhaps you can have a special UIView subclass (MergingView?) which would act as a parent of 2 views; a border view and a content view. Now that the border and content are in separate views, "merging" becomes trivial.
When we drop A on top of B, all we have to do is give A's border view and content view to B (so they are now subviews of B), and then send all of B's border views to the back.
You can then proceed to merge more with B, the procedure is the same.
Would this suit your use case? Happy to post code if needed.
I wrote an example about Wave Animation.The animation is ok,but I don't understand why the custom UIView needs to add "self.layer.masksToBounds = YES" to have the round Corner.
This is a custom UIView. I have rewritten its drawRect. If i don't set "masksToBounds" to YES, the round corner disappear.
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.frame = CGRectMake(10, 40, 300, 300);
self.layer.cornerRadius = 150;
self.backgroundColor = [UIColor greenColor];
self.layer.borderColor = [UIColor blueColor].CGColor;
self.layer.borderWidth = 2;
// self.layer.masksToBounds = YES; //if write this line,the round corner appear
self.x_move = 0;
self.y_move = 300;
}
return self;
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
CGContextSetLineWidth(context, 1);
CGContextSetFillColorWithColor(context, [UIColor grayColor].CGColor);
CGPathMoveToPoint(path, NULL, 0, 0);
for(float i=0; i<=300; i++){
float x=i;
float y = 5 * sin( 0.05 * x+ self.x_move) + self.y_move;
CGPathAddLineToPoint(path, nil, x, y);
}
CGPathAddLineToPoint(path, nil, 300, 0);
CGPathAddLineToPoint(path, nil, 0, 0);
CGContextAddPath(context, path);
CGContextFillPath(context);
CGContextDrawPath(context, kCGPathStroke);
CGPathRelease(path);
}
- (void)startAnimation {
if (self.waveTimer == nil) {
self.waveTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(refresh) userInfo:nil repeats:YES];
}
}
- (void)refresh {
self.x_move += 0.3;
self.y_move -= 0.2;
if(self.y_move - 100 < 0.00001){
[self.waveTimer invalidate];
}else{
[self setNeedsDisplay];
}
}
ViewController:
self.wave = [[waveAnimation alloc] init];
[self.wave startAnimation];
[self.view addSubview:self.wave];
This is a normal UIView. Its "masksToBunds" is NO, but its round corner shows normal. Compared with the examples above, why one should add, one does not need.
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(20, 60, 100, 100)];
view.backgroundColor = [UIColor greenColor];
view.layer.cornerRadius = 50;
view.layer.borderWidth = 2;
view.layer.borderColor = [UIColor blackColor].CGColor;
[self.view addSubview:view];
The reason for this is from your override of drawRect:(CGRect)frame.
You are attempting to set the corner radius of the layer that is not the element you assigned the fill color to. The reason setting masksToBounds achieves the desired rounded corner is because here you are telling the view to mask all of it's layers to the bounds from your frame.
In your second example with the UIView the corner radius appears as expected because you have not adjusted it's layers.
I suggest either adopting setting masksToBounds or redraw the view's bounds a different way if you need masksToBounds = NO. Since you have the "squiggle" sin curve and you want rounded corners, perhaps just create a generic rounded rect (one with the desired corner radius) then merge that with the path you have already created with the sin wave.
Merge multiple CGPaths together to make one path
That link may help you merge your rounded view with your custom view to achieve the desired end result.
Otherwise.. probably best bet to just set this to yes.
I found it tricky to animate a UIImageView between two states: its original rectangle frame, and a new shape created with a UIBezierPath. There are many different techniques mentioned, most of which did not work for me.
First was the realization that using UIView block animation would not work; evidently one can't perform sublayer animations in an animateWithDuration: block. (see here and here)
That left CAAnimation, with the concrete subclasses like CABasicAnimation. I soon realized that one can't animate from a view that doesn't have a CAShapeLayer to one that does (see here, for example).
And they can't be just any two shape layer paths, but rather "Animating the path of a shape layer is only guaranteed to work when you are animating from like to like" (see here)
With that in place, comes the more mundane problems, like what to use for fromValue and toValue (should they be a CAShapeLayer, or a CGPath?), what to add the animation to (the layer, or the mask?), etc.
It seemed there were so many variables; which combination would give me the animation I was looking for?
The first important point is to construct the two bezier paths similarly, so the rectangle is a (trivial) analogue to the more complex shape.
// the complex bezier path
let initialPoint = CGPoint(x: 0, y: 0)
let curveStart = CGPoint(x: 0, y: (rect.size.height) * (0.2))
let curveControl = CGPoint(x: (rect.size.width) * (0.6), y: (rect.size.height) * (0.5))
let curveEnd = CGPoint(x: 0, y: (rect.size.height) * (0.8))
let firstCorner = CGPoint(x: 0, y: rect.size.height)
let secondCorner = CGPoint(x: rect.size.width, y: rect.size.height)
let thirdCorner = CGPoint(x: rect.size.width, y: 0)
var myBezierArc = UIBezierPath()
myBezierArc.moveToPoint(initialPoint)
myBezierArc.addLineToPoint(curveStart)
myBezierArc.addQuadCurveToPoint(curveEnd, controlPoint: curveControl)
myBezierArc.addLineToPoint(firstCorner)
myBezierArc.addLineToPoint(secondCorner)
myBezierArc.addLineToPoint(thirdCorner)
The simpler 'trivial' bezier path, that creates a rectangle, is exactly the same but the controlPoint is set so that it appears to not be there:
let curveControl = CGPoint(x: 0, y: (rect.size.height) * (0.5))
( Try removing the addQuadCurveToPoint line to get a very strange animation! )
And finally, the animation commands:
let myAnimation = CABasicAnimation(keyPath: "path")
if (isArcVisible == true) {
myAnimation.fromValue = myBezierArc.CGPath
myAnimation.toValue = myBezierTrivial.CGPath
} else {
myAnimation.fromValue = myBezierTrivial.CGPath
myAnimation.toValue = myBezierArc.CGPath
}
myAnimation.duration = 0.4
myAnimation.fillMode = kCAFillModeForwards
myAnimation.removedOnCompletion = false
myImageView.layer.mask.addAnimation(myAnimation, forKey: "animatePath")
If anyone is interested, the project is here.
Another approach is to use a display link. It's like a timer, except it's coordinated with the update of the display. You then have the handler of the display link modify the view according to what it should look like at any particular point of the animation.
For example, if you wanted to animate the rounding of the corners of the mask from 0 to 50 points, you could do something like the following, where percent is a value between 0.0 and 1.0 indicating what percentage of the animation is done:
let path = UIBezierPath(rect: imageView.bounds)
let mask = CAShapeLayer()
mask.path = path.CGPath
imageView.layer.mask = mask
let animation = AnimationDisplayLink(duration: 0.5) { percent in
let cornerRadius = percent * 50.0
let path = UIBezierPath(roundedRect: self.imageView.bounds, cornerRadius: cornerRadius)
mask.path = path.CGPath
}
Where:
class AnimationDisplayLink : NSObject {
var animationDuration: CGFloat
var animationHandler: (percent: CGFloat) -> ()
var completionHandler: (() -> ())?
private var startTime: CFAbsoluteTime!
private var displayLink: CADisplayLink!
init(duration: CGFloat, animationHandler: (percent: CGFloat)->(), completionHandler: (()->())? = nil) {
animationDuration = duration
self.animationHandler = animationHandler
self.completionHandler = completionHandler
super.init()
startDisplayLink()
}
private func startDisplayLink () {
startTime = CFAbsoluteTimeGetCurrent()
displayLink = CADisplayLink(target: self, selector: "handleDisplayLink:")
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
}
private func stopDisplayLink() {
displayLink.invalidate()
displayLink = nil
}
func handleDisplayLink(displayLink: CADisplayLink) {
let elapsed = CFAbsoluteTimeGetCurrent() - startTime
var percent = CGFloat(elapsed) / animationDuration
if percent >= 1.0 {
stopDisplayLink()
animationHandler(percent: 1.0)
completionHandler?()
} else {
animationHandler(percent: percent)
}
}
}
The virtue of the display link approach is that it can be used to animate some property that is otherwise unanimatable. It also lets you to precisely dictate the interim state during the animation.
If you can use CAAnimation or UIKit block-based animation, that's probably the way to go. But the display link can sometimes be a good fallback approach.
I was inspired by your example to try a circle to square animation using the techniques that are mentioned in your answer and some of the links. I intend to extend this to be a more general circle to polygon animation, but currently it only works for squares. I have a class called RDPolyCircle (a subclass of CAShapeLayer) that does the heavy lifting. Here is its code,
#interface RDPolyCircle ()
#property (strong,nonatomic) UIBezierPath *polyPath;
#property (strong,nonatomic) UIBezierPath *circlePath;
#end
#implementation RDPolyCircle {
double cpDelta;
double cosR;
}
-(instancetype)initWithFrame:(CGRect) frame numberOfSides:(NSInteger)sides isPointUp:(BOOL) isUp isInitiallyCircle:(BOOL) isCircle {
if (self = [super init]) {
self.frame = frame;
_isPointUp = isUp;
_isExpandedPolygon = !isCircle;
double radius = (frame.size.width/2.0);
cosR = sin(45 * M_PI/180.0) * radius;
double fractionAlongTangent = 4.0*(sqrt(2)-1)/3.0;
cpDelta = fractionAlongTangent * radius * sin(45 * M_PI/180.0);
_circlePath = [self createCirclePathForFrame:frame];
_polyPath = [self createPolygonPathForFrame:frame numberOfSides:sides];
self.path = (isCircle)? self.circlePath.CGPath : self.polyPath.CGPath;
self.fillColor = [UIColor clearColor].CGColor;
self.strokeColor = [UIColor blueColor].CGColor;
self.lineWidth = 6.0;
}
return self;
}
-(UIBezierPath *)createCirclePathForFrame:(CGRect) frame {
CGPoint ctr = CGPointMake(frame.origin.x + frame.size.width/2.0, frame.origin.y + frame.size.height/2.0);
// create a circle using 4 arcs, with the first one symmetrically spanning the y-axis
CGPoint leftUpper = CGPointMake(ctr.x - cosR, ctr.y - cosR);
CGPoint cp1 = CGPointMake(leftUpper.x + cpDelta, leftUpper.y - cpDelta);
CGPoint rightUpper = CGPointMake(ctr.x + cosR, ctr.y - cosR);
CGPoint cp2 = CGPointMake(rightUpper.x - cpDelta, rightUpper.y - cpDelta);
CGPoint cp3 = CGPointMake(rightUpper.x + cpDelta, rightUpper.y + cpDelta);
CGPoint rightLower = CGPointMake(ctr.x + cosR, ctr.y + cosR);
CGPoint cp4 = CGPointMake(rightLower.x + cpDelta, rightLower.y - cpDelta);
CGPoint cp5 = CGPointMake(rightLower.x - cpDelta, rightLower.y + cpDelta);
CGPoint leftLower = CGPointMake(ctr.x - cosR, ctr.y + cosR);
CGPoint cp6 = CGPointMake(leftLower.x + cpDelta, leftLower.y + cpDelta);
CGPoint cp7 = CGPointMake(leftLower.x - cpDelta, leftLower.y - cpDelta);
CGPoint cp8 = CGPointMake(leftUpper.x - cpDelta, leftUpper.y + cpDelta);
UIBezierPath *circle = [UIBezierPath bezierPath];
[circle moveToPoint:leftUpper];
[circle addCurveToPoint:rightUpper controlPoint1:cp1 controlPoint2:cp2];
[circle addCurveToPoint:rightLower controlPoint1:cp3 controlPoint2:cp4];
[circle addCurveToPoint:leftLower controlPoint1:cp5 controlPoint2:cp6];
[circle addCurveToPoint:leftUpper controlPoint1:cp7 controlPoint2:cp8];
[circle closePath];
circle.lineCapStyle = kCGLineCapRound;
return circle;
}
-(UIBezierPath *)createPolygonPathForFrame:(CGRect) frame numberOfSides:(NSInteger) sides {
CGPoint leftUpper = CGPointMake(self.frame.origin.x, self.frame.origin.y);
CGPoint cp1 = CGPointMake(leftUpper.x + cpDelta, leftUpper.y);
CGPoint rightUpper = CGPointMake(self.frame.origin.x + self.frame.size.width, self.frame.origin.y);
CGPoint cp2 = CGPointMake(rightUpper.x - cpDelta, rightUpper.y);
CGPoint cp3 = CGPointMake(rightUpper.x, rightUpper.y + cpDelta);
CGPoint rightLower = CGPointMake(self.frame.origin.x + self.frame.size.width, self.frame.origin.y + self.frame.size.height);
CGPoint cp4 = CGPointMake(rightLower.x , rightLower.y - cpDelta);
CGPoint cp5 = CGPointMake(rightLower.x - cpDelta, rightLower.y);
CGPoint leftLower = CGPointMake(self.frame.origin.x, self.frame.origin.y + self.frame.size.height);
CGPoint cp6 = CGPointMake(leftLower.x + cpDelta, leftLower.y);
CGPoint cp7 = CGPointMake(leftLower.x, leftLower.y - cpDelta);
CGPoint cp8 = CGPointMake(leftUpper.x, leftUpper.y + cpDelta);
UIBezierPath *square = [UIBezierPath bezierPath];
[square moveToPoint:leftUpper];
[square addCurveToPoint:rightUpper controlPoint1:cp1 controlPoint2:cp2];
[square addCurveToPoint:rightLower controlPoint1:cp3 controlPoint2:cp4];
[square addCurveToPoint:leftLower controlPoint1:cp5 controlPoint2:cp6];
[square addCurveToPoint:leftUpper controlPoint1:cp7 controlPoint2:cp8];
[square closePath];
square.lineCapStyle = kCGLineCapRound;
return square;
}
-(void)toggleShape {
if (self.isExpandedPolygon) {
[self restore];
}else{
CABasicAnimation *expansionAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
expansionAnimation.fromValue = (__bridge id)(self.circlePath.CGPath);
expansionAnimation.toValue = (__bridge id)(self.polyPath.CGPath);
expansionAnimation.duration = 0.5;
expansionAnimation.fillMode = kCAFillModeForwards;
expansionAnimation.removedOnCompletion = NO;
[self addAnimation:expansionAnimation forKey:#"Expansion"];
self.isExpandedPolygon = YES;
}
}
-(void)restore {
CABasicAnimation *contractionAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
contractionAnimation.fromValue = (__bridge id)(self.polyPath.CGPath);
contractionAnimation.toValue = (__bridge id)(self.circlePath.CGPath);
contractionAnimation.duration = 0.5;
contractionAnimation.fillMode = kCAFillModeForwards;
contractionAnimation.removedOnCompletion = NO;
[self addAnimation:contractionAnimation forKey:#"Contraction"];
self.isExpandedPolygon = NO;
}
From the view controller, I create an instance of this layer, and add it to a simple view's layer, then do the animations on a button push,
#import "ViewController.h"
#import "RDPolyCircle.h"
#interface ViewController ()
#property (weak, nonatomic) IBOutlet UIView *circleView; // a plain UIView 150 x 150 centered in the superview
#property (strong,nonatomic) RDPolyCircle *shapeLayer;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// currently isPointUp and numberOfSides are not implemented (the shape created has numberOfSides=4 and isPointUp=NO)
// isInitiallyCircle is implemented
self.shapeLayer = [[RDPolyCircle alloc] initWithFrame:self.circleView.bounds numberOfSides: 4 isPointUp:NO isInitiallyCircle:YES];
[self.circleView.layer addSublayer:self.shapeLayer];
}
- (IBAction)toggleShape:(UIButton *)sender {
[self.shapeLayer toggleShape];
}
The project can be found here, http://jmp.sh/iK3kuVs.
I know it is simple to create the effect making the moon circling around the earth in IOS. Suppose the moon is a CALayer object, just change the anchorPoint of this object to the earth then it will animate circling around the earth. But how to create the moon that rotate by itself at the same time? since the moon can only have one anchorPoint, seems I can not make this CALayer object rotate by itself anymore. What do you guys think? thanks.
You can cause the "moon" to revolve around a point by animating it along a bezier path, and at the same time animate a rotation transform. Here is a simple example,
#interface ViewController ()
#property (strong,nonatomic) UIButton *moon;
#property (strong,nonatomic) UIBezierPath *circlePath;
#end
#implementation ViewController
-(void)viewDidLoad {
self.moon = [UIButton buttonWithType:UIButtonTypeInfoDark];
[self.moon addTarget:self action:#selector(clickedCircleButton:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.moon];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
CGRect circleRect = CGRectMake(60,100,200,200);
self.circlePath = [UIBezierPath bezierPathWithOvalInRect:circleRect];
self.moon.center = CGPointMake(circleRect.origin.x + circleRect.size.width, circleRect.origin.y + circleRect.size.height/2.0);
}
- (void)clickedCircleButton:(UIButton *)sender {
CAKeyframeAnimation *orbit = [CAKeyframeAnimation animationWithKeyPath:#"position"];
orbit.path = self.circlePath.CGPath;
orbit.calculationMode = kCAAnimationPaced;
orbit.duration = 4.0;
orbit.repeatCount = CGFLOAT_MAX;
[self.moon.layer addAnimation:orbit forKey:#"circleAnimation"];
CABasicAnimation *fullRotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
fullRotation.fromValue = 0;
fullRotation.byValue = #(2.0*M_PI);
fullRotation.duration = 4.0;
fullRotation.repeatCount = CGFLOAT_MAX;
[self.moon.layer addAnimation:fullRotation forKey:#"Rotate"];
}
These particular values will cause the "moon" to keep the same face toward the center like earth's moon does.
Use two layers.
One is an invisible "arm" reaching from the earth to the moon. It does a rotation transform around its anchor point, which is the center of the earth. This causes the moon, out at the end of the "arm", to revolve around the earth.
The other is the moon. It is a sublayer of the "arm", sitting out at the end of the arm. If you want it to rotate independently, rotate it round its anchor point, which is its own center.
(Be aware, however, that the real moon does not do this. For the real moon, the "arm" is sufficient, because the real moon rotates in sync with its own revolution around the earth - so that we see always the same face of the moon.)
I was looking for this kind of implementation myself.
I followed rdelmar answer and used this swift version:
class ViewController: UIViewController {
var circlePath: UIBezierPath!
var moon = UIButton(frame: CGRect(x: 10, y: 10, width: 50, height: 50))
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
func setup() {
let circleRect = CGRect(x: 60, y: 100, width: 200, height: 200)
self.circlePath = UIBezierPath(ovalIn: circleRect)
self.moon.center = CGPoint(x: circleRect.origin.x + circleRect.size.width, y: circleRect.origin.y + circleRect.size.height/2.0)
self.moon.addTarget(self, action: #selector(didTap), for: .touchUpInside)
moon.backgroundColor = .blue
self.view.addSubview(moon)
}
#objc func didTap() {
let orbit = CAKeyframeAnimation(keyPath: "position")
orbit.path = self.circlePath.cgPath
orbit.calculationMode = .paced
orbit.duration = 4.0
orbit.repeatCount = Float(CGFloat.greatestFiniteMagnitude)
moon.layer.add(orbit, forKey: "circleAnimation")
let fullRotation = CABasicAnimation(keyPath: "transform.rotation.z")
fullRotation.fromValue = 0
fullRotation.byValue = CGFloat.pi*2
fullRotation.duration = 4
fullRotation.repeatCount = Float(CGFloat.greatestFiniteMagnitude)
moon.layer.add(orbit, forKey: "Rotate")
}
}
(Using iOS 5 and Xcode 4.2.)
I've followed the instructions here: http://developer.apple.com/library/ios/#documentation/UserExperience/Conceptual/LocationAwarenessPG/AnnotatingMaps/AnnotatingMaps.html#//apple_ref/doc/uid/TP40009497-CH6-SW15 and used the MKCircle and MKCircleView classes to add a circle overlay on my MKMapView.
However what I actually want is an inverted circle overlay, like the left map in the sketch below (currently I have a circle overlay like the one on the right):
For the inverted circle, the overlay should cover the entire map - apart from the visible circle.
Is there an easy way to accomplish this using the MKCircle/MKCircleView classes? Or will I have to go deeper and define a custom overlay object/view?
Thank you for your help :)
I had the same task and here is how I solve it:
NOTE: this code will only work starting from iOS7
Add an overlay to the map, somewhere in your view controller:
MyMapOverlay *overlay = [[MyMapOverlay alloc] initWithCoordinate:coordinate];
[self.mapView addOverlay:overlay level:MKOverlayLevelAboveLabels];
In the MKMapViewDelegate methods write next:
- (MKOverlayRenderer *)mapView:(MKMapView *)map rendererForOverlay:(id<MKOverlay>)overlay {
/// we need to draw overlay on the map in a way when everything except the area in radius of 500 should be grayed
/// to do that there is special renderer implemented - NearbyMapOverlay
if ([overlay isKindOfClass:[NearbyMapOverlay class]]) {
MyMapOverlayRenderer *renderer = [[MyMapOverlayRenderer alloc] initWithOverlay:overlay];
renderer.fillColor = [UIColor whateverColor];/// specify color which you want to use for gray out everything out of radius
renderer.diameterInMeters = 1000;/// choose whatever diameter you need
return renderer;
}
return nil;
}
The MyMapOverlay itself should be something like followed:
#interface MyMapOverlay : NSObject<MKOverlay>
- (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate;
#end
#implementation MyMapOverlay
#synthesize coordinate = _coordinate;
- (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate {
self = [super init];
if (self) {
_coordinate = coordinate;
}
return self;
}
- (MKMapRect)boundingMapRect {
return MKMapRectWorld;
}
#end
And the MyMapOverlayRenderer:
#interface MyMapOverlayRenderer : MKOverlayRenderer
#property (nonatomic, assign) double diameterInMeters;
#property (nonatomic, copy) UIColor *fillColor;
#end
#implementation MyMapOverlayRenderer
/// this method is called as a part of rendering the map, and it draws the overlay polygon by polygon
/// which means that it renders overlay by square pieces
- (void)drawMapRect:(MKMapRect)mapRect
zoomScale:(MKZoomScale)zoomScale
inContext:(CGContextRef)context {
/// main path - whole area
UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(mapRect.origin.x, mapRect.origin.y, mapRect.size.width, mapRect.size.height)];
/// converting to the 'world' coordinates
double radiusInMapPoints = self.diameterInMeters * MKMapPointsPerMeterAtLatitude(self.overlay.coordinate.latitude);
MKMapSize radiusSquared = {radiusInMapPoints, radiusInMapPoints};
MKMapPoint regionOrigin = MKMapPointForCoordinate(self.overlay.coordinate);
MKMapRect regionRect = (MKMapRect){regionOrigin, radiusSquared}; //origin is the top-left corner
regionRect = MKMapRectOffset(regionRect, -radiusInMapPoints/2, -radiusInMapPoints/2);
// clamp the rect to be within the world
regionRect = MKMapRectIntersection(regionRect, MKMapRectWorld);
/// next path is used for excluding the area within the specific radius from current user location, so it will not be filled by overlay fill color
UIBezierPath *excludePath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(regionRect.origin.x, regionRect.origin.y, regionRect.size.width, regionRect.size.height) cornerRadius:regionRect.size.width / 2];
[path appendPath:excludePath];
/// setting overlay fill color
CGContextSetFillColorWithColor(context, self.fillColor.CGColor);
/// adding main path. NOTE that exclusionPath was appended to main path, so we should only add 'path'
CGContextAddPath(context, path.CGPath);
/// tells the context to fill the path but with regards to even odd rule
CGContextEOFillPath(context);
}
As a result you will have exact same view like on the left image that was posted in the question.
The best way to do it, would be to subclass MKMapView and override the drawRect method call super, then paint over the map with the color you want.
Then each time the user moves, drawRect should respond by drawing appropriately.
Here a Swift version. Thanks Valerii.
https://github.com/dariopellegrini/MKInvertedCircle
i tried to use this swift version and it didn't work, so im posting my implementation (tested on iOS 12)
import Foundation
import UIKit
import MapKit
class MKInvertedCircleOverlayRenderer: MKOverlayRenderer {
var fillColor: UIColor = UIColor.red
var strokeColor: UIColor = UIColor.blue
var lineWidth: CGFloat = 3
var circle: MKCircle
init(circle: MKCircle) {
self.circle = circle
super.init(overlay: circle)
}
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
let path = UIBezierPath(rect: rect(for: MKMapRectWorld))
let excludePath: UIBezierPath = UIBezierPath(roundedRect: CGRect(x: circle.coordinate.latitude,
y: circle.coordinate.longitude,
width: circle.boundingMapRect.size.width,
height: circle.boundingMapRect.size.height),
cornerRadius: CGFloat(circle.boundingMapRect.size.width))
context.setFillColor(fillColor.cgColor)
path.append(excludePath)
context.addPath(path.cgPath)
context.fillPath(using: .evenOdd)
context.addPath(excludePath.cgPath)
context.setLineWidth(9 / zoomScale)
context.setStrokeColor(strokeColor.cgColor)
context.strokePath()
//line showing circle radius
let lineBeginPoint = CGPoint(x: excludePath.bounds.midX, y: excludePath.bounds.midY)
let lineEndPoint = CGPoint(x: excludePath.bounds.maxX, y: excludePath.bounds.midY)
let linePath: UIBezierPath = UIBezierPath()
linePath.move(to: lineBeginPoint)
linePath.addLine(to: lineEndPoint)
context.addPath(linePath.cgPath)
context.setLineWidth(6/zoomScale)
context.setStrokeColor(UIColor.black.cgColor)
context.setLineDash(phase: 1, lengths: [20 / zoomScale, 10 / zoomScale])
context.strokePath()
// circle at the end of the line above
let circleSize: CGFloat = 30/zoomScale
let circleRect = CGRect(origin: CGPoint(x: lineEndPoint.x - (circleSize/2), y: lineEndPoint.y - (circleSize/2)),
size: CGSize(width: circleSize, height: circleSize))
let circlePath: UIBezierPath =
UIBezierPath(roundedRect: circleRect, cornerRadius: circleSize)
context.addPath(circlePath.cgPath)
context.setFillColor(UIColor.black.cgColor)
context.fillPath()
}