I would like to to animate (blink) thumb of the UISlider when user first enter the app (to indicate what to click).
I set UIImage as a thumb :
[self.textNavigationSlider setThumbImage:thumbImage forState:UIControlStateNormal];
Can I somehow animate it (maybe just change alpha of this UIImage as animation) ?
PS (This play button is the UIImage)
I don't believe you can animate the alpha value of an image, and we don't have access to an the thumb's image view (if it has one) to animate its alpha. One thing you can do is add an image view as a subview of the slider, and make it slightly bigger than the thumb image, and animate its alpha value (it will be behind the thumb's image, so it will show up as a pulsating ring). In this example, I set the image view's frame in thumbRectForBounds:trackRect:value:, and cancel the animation in touchesBegan,
#interface RDSlider ()
#property (strong, nonatomic) UIImageView *iv;
#end
#implementation RDSlider // subclass of UISlider
-(void)awakeFromNib {
self.minimumTrackTintColor = [UIColor colorWithRed:75/255.0 green:180/255.0 blue:150/255.0 alpha:1];
self.maximumTrackTintColor = [UIColor colorWithRed:50/255.0 green:120/255.0 blue:100/255.0 alpha:1];
[self setThumbImage:[self createThumbImage] forState:UIControlStateNormal];
self.iv = [UIImageView new];
self.iv.backgroundColor = [UIColor greenColor];
[self addSubview:self.iv];
}
-(void)didMoveToWindow {
[UIView animateWithDuration:.6 delay:0 options:UIViewAnimationOptionAutoreverse|UIViewAnimationOptionTransitionCrossDissolve|UIViewAnimationOptionRepeat|UIViewAnimationOptionCurveEaseInOut animations:^{
self.iv.alpha = 0;
} completion:^(BOOL finished) {
[self.iv removeFromSuperview];
self.iv = nil;
}];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.iv.layer removeAllAnimations]; // calling this causes the animation's completion block to be executed
[super touchesBegan:touches withEvent:event];
}
- (CGRect)thumbRectForBounds:(CGRect)bounds trackRect:(CGRect)rect value:(float)value {
CGRect thumbRect = [super thumbRectForBounds:bounds trackRect:rect value:value];
self.iv.frame = CGRectInset(thumbRect,-2,-2);
self.iv.layer.cornerRadius = self.iv.frame.size.width/2.0;
self.iv.layer.masksToBounds = YES;
return thumbRect;
}
-(UIImage *) createThumbImage {
UIBezierPath *border = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 28, 28)];
UIBezierPath *center = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(.5, .5, 27, 27)];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(border.bounds.size.width, border.bounds.size.height), NO, 0.0);
[self.minimumTrackTintColor setFill];
[border fill];
[[UIColor whiteColor] setFill];
[center fill];
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
If you want it for one time follow below steps.
Create new UIImageView let's say fakeImageView.
Animate it using UIViewAnimation
Once animation is done, hide it... (fakeImageView.hidden = YES)
For UIViewAnimation tutorial, visit here or here
You could make an exact copy of the slider, except for no thumb image, and put it underneath the first one. Then animate the alpha of the whole first slider, which should give the impression of only animating the thumb, in theory ;-) When animation is completed, you can delete the second slider.
You can access the UISlider object.subviews. Usually (not guaranteed) the third element (which should be the .lastObject) is the UIImageView for the thumb image.
Related
I am creating an app where I want to draw lines on scrollView. I am able to draw lines.
Here is my code
#interface GraphOnScrollView : UIScrollView
#property(strong,nonatomic)NSMutableArray *intensityArray;
#end
import "GraphOnScrollView.h"
#implementation GraphOnScrollView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.userInteractionEnabled=YES;
self.scrollEnabled=YES;
UIButton *DirectMsgBtn1 =[UIButton buttonWithType:UIButtonTypeCustom];
DirectMsgBtn1.titleLabel.font =[UIFont systemFontOfSize:12.0];
[DirectMsgBtn1 setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[DirectMsgBtn1 setTitle:#"direct message" forState:UIControlStateNormal];
[DirectMsgBtn1 setBackgroundColor:[UIColor clearColor]];
[DirectMsgBtn1 addTarget:self
action:#selector(DirectMessageViewPopUp:)
forControlEvents:UIControlEventTouchDown];
DirectMsgBtn1.frame = CGRectMake(0.0, 0, 100, 30);
[self addSubview:DirectMsgBtn1];
DirectMsgBtn1 = nil;
}
return self; }
// Only override drawRect: if you perform custom drawing. // An
empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
CGFloat y_Axix =20.0;
CGFloat lineWidth=1.0;
for (int i=0; i<[self.intensityArray count]; i++)
{
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(c, 2.0);
lineWidth+=1;
CGFloat red[4] = {1.0f, 0.5f, 0.0f, 1.0f};
CGContextSetStrokeColor(c, red);
CGContextBeginPath(c);
CGFloat width=[[self.intensityArray objectAtIndex:i] floatValue];
CGContextMoveToPoint(c, 5.0f, y_Axix);
CGContextAddLineToPoint(c, width, y_Axix);
CGContextStrokePath(c);
CGContextAddArc(c,width,y_Axix,1.0f,0,2*3.1415926535898,1);
CGContextDrawPath(c,kCGPathStroke);
y_Axix=y_Axix+50;
}
NSLog(#"intensity array %#", self.intensityArray);
self.contentSize = CGSizeMake(self.frame.size.width, 1000);
}
This is code I am using for adding scrollview on my view
GraphOnScrollView *GraphView =[[GraphOnScrollView
alloc]initWithFrame:CGRectMake(0.0, 150.0, 320.0, 280.0)];
GraphView.backgroundColor =[UIColor whiteColor];
GraphView.intensityArray =[NSMutableArray arrayWithObjects:#"60",#"100",#"40",#"10",#"20",#"40",#"40",#"100",
nil];
[self.view addSubview:GraphView];
Using this code the lines which i have drawn is not getting scrolled but the scrollview is scrolled. I don't know what is the problem.
Thanks
While drawing on any view it draws everything on the canvas of that view.
In the case of your, you are drawing on UIScrollView so all drawing performs on the canvas of the UIScrollView, so it is not scrollable.
To solve that problem you can create on separate UIView with the size you want, perform any drawing on it and add that view in scrollview. I think this will be the best solution.
You can also refer to this link for some more help.
Alternate Approach (i actually used uiview for drawing and made it look like scroll)
save the touch location in
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
}
record the touch point in touches moved.
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
}
now you can translate coordinate of your points as
newX=x+(touchstart.x-touchMoved.x);//do this for x of every point
newy=y+(touchstart.y-touchMoved.y);// do this for y of every point
put the above lines in a function and call them from touches moved you can do it in touches ended method as well if lag is ok with you.
Point is translate the point and calculate the degree of translation from user touch began and moved or swipe
I'm trying to make a circle-masked image view with animated mask, and played with different solutions. The example below does the job but I've got two problems with it:
1) Why is it that I cannot make the image tappable? Adding eg. a UITapGestureRecognizer does not work. My guess is that the mask prevents the touch actions being propagated to the lower level in the view hierarchy.
2) Animating the mask is running very fast and I cannot adjust the duration using UIView block animation
How can I solve these?
- (void) addCircle {
// this is the encapsulating view
//
base = [[UIView alloc] init];
//
// this is the button background
//
base_bgr = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"c1_bgr.png"]];
base_bgr.center = CGPointMake(60, 140);
[base addSubview:base_bgr];
//
// icon image
//
base_icon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"c1_ico.png"]];
base_icon.center = CGPointMake(186*0.3/2, 182*0.3/2);
base_icon.transform = CGAffineTransformMakeScale(0.3, 0.3);
[base addSubview:base_icon];
//
// the drawn circle mask layer
//
circleLayer = [CAShapeLayer layer];
// Give the layer the same bounds as your image view
[circleLayer setBounds:CGRectMake(0.0f, 0.0f, [base_icon frame].size.width,
[base_icon frame].size.height)];
// Position the circle
[circleLayer setPosition:CGPointMake(186*0.3/2-7, 182*0.3/2-10)];
// Create a circle path.
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:
CGRectMake(0.0f, 0.0f, 70.0f, 70.0f)];
// Set the path on the layer
[circleLayer setPath:[path CGPath]];
[[base layer] setMask:circleLayer];
[self.view addSubview:base];
base.center = CGPointMake(100, 100);
base.userInteractionEnabled = YES;
base_bgr.userInteractionEnabled = YES;
base_icon.userInteractionEnabled = YES;
//
// NOT working: UITapGestureRecognizer
//
UITapGestureRecognizer *tapgesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapit:)];
[base_icon addGestureRecognizer:tapgesture];
//
// BAD but WORKS :) properly positioned UIButton over the masked image
//
base_btn = [UIButton buttonWithType:UIButtonTypeCustom];
base_btn.frame = CGRectMake(base.frame.origin.x, base.frame.origin.y, base_icon.frame.size.width, base_icon.frame.size.height);
[base_btn addTarget:self action:#selector(tapit:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:base_btn];
}
This is the tap handler, and here's the mask animation. Whatever number I tried in duration, it is animating fast - approximately 0.25 second, and I cannot adjust it.
- (void) tapit:(id) sender {
//...
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^ {
[circleLayer setTransform:CATransform3DMakeScale(10.0, 10.0, 1.0)];
}
completion:^(BOOL finished) {
// it is not necessary if I manage to make the icon image tappable
base_btn.frame = [base convertRect:base_icon.frame toView:self.view];
}];
}
}
1) touches are not propagated down from base because it's initiated without a frame, so its frame will be CGRectZero. Views don't get touch events that start outside of their bounds. Simply set a valid frame on base that includes entire tap target.
2) setTransform: on a layer invokes an implicit animation which uses Core Animation's default duration of 0.25 (you guessed it right :)). The best solution would be to use CABasicAnimation instead of UIView-based animation. Something like this:
CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
scaleAnimation.toValue = #(10.0f);
scaleAnimation.duration = 2;
[circleLayer addAnimation:scaleAnimation forKey:nil];
Note: by default, CABasicAnimation will remove itself from a layer when complete and the layer will snap back to old values. You can prevent it by for example setting that animation's removedOnCompletion property to NO and removing it yourself later using CALayer's removeAnimationForKey: method (just set a key instead of passing nil wheen adding the animation), but that depends on what exactly you want to accomplish with this.
I have below code:
IBOutlet UIImageView *img_Logo;
...
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
CALayer *ca_Logo = img_Logo.layer;
ca_Logo.position = CGPointMake(400, 400);
ca_Logo.duration = 2.0f;
[ca_Logo setNeedsDisplay];
}
When a touch event is triggered, the logo will disappear, but when I remove ca_Logo.duration = 2.0f, that ca_Logo will move, but it was not an animation, just instantly disappear and appear to a new location.
Is there anything wrong with it? img_Logo is an UIImageView with IBOutlet or I need to program it without Interface builder?
I want to move the image from CGPointMake(400, 400) to CGPointMake(200, 400)
I think you need to program it without using the interface builder try this code
UIImage *image = [UIImage imageNamed:#"img_Logo.png"];
CALayer *img = [CALayer layer];
img.contents = (id)image.CGImage;
img.bounds = CGRectMake(0, 0, 100, 200);
img.position = CGPointMake(400, 400);
[self.view.layer addSublayer:img];
if you just want to animate from one frame to another, try below:
[UIView animateWithDuration:1.0 animations:^{
[self.img_Logo setFrame:CGRectMake(200, 400, youImageViewWidth, youImageViewHeight)];
}];
I don't get your point using touchesBegan and touchesMoved
I want to do something similar to the following:
How to mask an image in IOS sdk?
I want to cover the entire screen with translucent black. Then, I want to cut a circle out of the translucent black covering so that you can see through clearly. I'm doing this to highlight parts of the screen for a tutorial.
I then want to animate the cut-out circle to other parts of the screen. I also want to be able to stretch the cut-out circle horizontally & vertically, as you would do with a generic button background image.
(UPDATE: Please see also my other answer which describes how to set up multiple independent, overlapping holes.)
Let's use a plain old UIView with a backgroundColor of translucent black, and give its layer a mask that cuts a hole out of the middle. We'll need an instance variable to reference the hole view:
#implementation ViewController {
UIView *holeView;
}
After loading the main view, we want to add the hole view as a subview:
- (void)viewDidLoad {
[super viewDidLoad];
[self addHoleSubview];
}
Since we want to move the hole around, it will be convenient to make the hole view be very large, so that it covers the rest of the content regardless of where it's positioned. We'll make it 10000x10000. (This doesn't take up any more memory because iOS doesn't automatically allocate a bitmap for the view.)
- (void)addHoleSubview {
holeView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10000, 10000)];
holeView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.5];
holeView.autoresizingMask = 0;
[self.view addSubview:holeView];
[self addMaskToHoleView];
}
Now we need to add the mask that cuts a hole out of the hole view. We'll do this by creating a compound path consisting of a huge rectangle with a smaller circle at its center. We'll fill the path with black, leaving the circle unfilled and therefore transparent. The black part has alpha=1.0 and so it makes the hole view's background color show. The transparent part has alpha=0.0, so that part of the hole view is also transparent.
- (void)addMaskToHoleView {
CGRect bounds = holeView.bounds;
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = bounds;
maskLayer.fillColor = [UIColor blackColor].CGColor;
static CGFloat const kRadius = 100;
CGRect const circleRect = CGRectMake(CGRectGetMidX(bounds) - kRadius,
CGRectGetMidY(bounds) - kRadius,
2 * kRadius, 2 * kRadius);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:circleRect];
[path appendPath:[UIBezierPath bezierPathWithRect:bounds]];
maskLayer.path = path.CGPath;
maskLayer.fillRule = kCAFillRuleEvenOdd;
holeView.layer.mask = maskLayer;
}
Notice that I've put the circle at the center of the 10000x10000 view. This means that we can just set holeView.center to set the center of the circle relative to the other content. So, for example, we can easily animate it up and down over the main view:
- (void)viewDidLayoutSubviews {
CGRect const bounds = self.view.bounds;
holeView.center = CGPointMake(CGRectGetMidX(bounds), 0);
// Defer this because `viewDidLayoutSubviews` can happen inside an
// autorotation animation block, which overrides the duration I set.
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:2 delay:0
options:UIViewAnimationOptionRepeat
| UIViewAnimationOptionAutoreverse
animations:^{
holeView.center = CGPointMake(CGRectGetMidX(bounds),
CGRectGetMaxY(bounds));
} completion:nil];
});
}
Here's what it looks like:
But it's smoother in real life.
You can find a complete working test project in this github repository.
This is not a simple one. I can get you a good bit of the way there. It's the animating that is tricky. Here's the output of some code I threw together:
The code is like this:
- (void)viewDidLoad
{
[super viewDidLoad];
// Create a containing layer and set it contents with an image
CALayer *containerLayer = [CALayer layer];
[containerLayer setBounds:CGRectMake(0.0f, 0.0f, 500.0f, 320.0f)];
[containerLayer setPosition:[[self view] center]];
UIImage *image = [UIImage imageNamed:#"cool"];
[containerLayer setContents:(id)[image CGImage]];
// Create your translucent black layer and set its opacity
CALayer *translucentBlackLayer = [CALayer layer];
[translucentBlackLayer setBounds:[containerLayer bounds]];
[translucentBlackLayer setPosition:
CGPointMake([containerLayer bounds].size.width/2.0f,
[containerLayer bounds].size.height/2.0f)];
[translucentBlackLayer setBackgroundColor:[[UIColor blackColor] CGColor]];
[translucentBlackLayer setOpacity:0.45];
[containerLayer addSublayer:translucentBlackLayer];
// Create a mask layer with a shape layer that has a circle path
CAShapeLayer *maskLayer = [CAShapeLayer layer];
[maskLayer setBorderColor:[[UIColor purpleColor] CGColor]];
[maskLayer setBorderWidth:5.0f];
[maskLayer setBounds:[containerLayer bounds]];
// When you create a path, remember that origin is in upper left hand
// corner, so you have to treat it as if it has an anchor point of 0.0,
// 0.0
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:
CGRectMake([translucentBlackLayer bounds].size.width/2.0f - 100.0f,
[translucentBlackLayer bounds].size.height/2.0f - 100.0f,
200.0f, 200.0f)];
// Append a rectangular path around the mask layer so that
// we can use the even/odd fill rule to invert the mask
[path appendPath:[UIBezierPath bezierPathWithRect:[maskLayer bounds]]];
// Set the path's fill color since layer masks depend on alpha
[maskLayer setFillColor:[[UIColor blackColor] CGColor]];
[maskLayer setPath:[path CGPath]];
// Center the mask layer in the translucent black layer
[maskLayer setPosition:
CGPointMake([translucentBlackLayer bounds].size.width/2.0f,
[translucentBlackLayer bounds].size.height/2.0f)];
// Set the fill rule to even odd
[maskLayer setFillRule:kCAFillRuleEvenOdd];
// Set the translucent black layer's mask property
[translucentBlackLayer setMask:maskLayer];
// Add the container layer to the view so we can see it
[[[self view] layer] addSublayer:containerLayer];
}
You would have to animate the mask layer which you could build up based on user input, but it will be a bit challenging. Notice the lines where I append a rectangular path to the circle path and then set the fill rule a few lines later on the shape layer. These are what make the inverted mask possible. If you leave those out you will instead show the translucent black in the center of the circle and then nothing on the outer part (if that makes sense).
Maybe try to play with this code a bit and see if you can get it animating. I'll play with it some more as I have time, but this is a pretty interesting problem. Would love to see a complete solution.
UPDATE: So here's another stab at it. The trouble here is that this one makes the translucent mask look white instead of black, but the upside is that circle can be animated pretty easily.
This one builds up a composite layer with the translucent layer and the circle layer being siblings inside of a parent layer that gets used as the mask.
I added a basic animation to this one so we could see the circle layer animate.
- (void)viewDidLoad
{
[super viewDidLoad];
CGRect baseRect = CGRectMake(0.0f, 0.0f, 500.0f, 320.0f);
CALayer *containerLayer = [CALayer layer];
[containerLayer setBounds:baseRect];
[containerLayer setPosition:[[self view] center]];
UIImage *image = [UIImage imageNamed:#"cool"];
[containerLayer setContents:(id)[image CGImage]];
CALayer *compositeMaskLayer = [CALayer layer];
[compositeMaskLayer setBounds:baseRect];
[compositeMaskLayer setPosition:CGPointMake([containerLayer bounds].size.width/2.0f, [containerLayer bounds].size.height/2.0f)];
CALayer *translucentLayer = [CALayer layer];
[translucentLayer setBounds:baseRect];
[translucentLayer setBackgroundColor:[[UIColor blackColor] CGColor]];
[translucentLayer setPosition:CGPointMake([containerLayer bounds].size.width/2.0f, [containerLayer bounds].size.height/2.0f)];
[translucentLayer setOpacity:0.35];
[compositeMaskLayer addSublayer:translucentLayer];
CAShapeLayer *circleLayer = [CAShapeLayer layer];
UIBezierPath *circlePath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0.0f, 0.0f, 200.0f, 200.0f)];
[circleLayer setBounds:CGRectMake(0.0f, 0.0f, 200.0f, 200.0f)];
[circleLayer setPosition:CGPointMake([containerLayer bounds].size.width/2.0f, [containerLayer bounds].size.height/2.0f)];
[circleLayer setPath:[circlePath CGPath]];
[circleLayer setFillColor:[[UIColor blackColor] CGColor]];
[compositeMaskLayer addSublayer:circleLayer];
[containerLayer setMask:compositeMaskLayer];
[[[self view] layer] addSublayer:containerLayer];
CABasicAnimation *posAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
[posAnimation setFromValue:[NSValue valueWithCGPoint:[circleLayer position]]];
[posAnimation setToValue:[NSValue valueWithCGPoint:CGPointMake([circleLayer position].x + 100.0f, [circleLayer position].y + 100)]];
[posAnimation setDuration:1.0f];
[posAnimation setRepeatCount:INFINITY];
[posAnimation setAutoreverses:YES];
[circleLayer addAnimation:posAnimation forKey:#"position"];
}
Here's an answer that works with multiple independent, possibly overlapping spotlights.
I'll set up my view hierarchy like this:
SpotlightsView with black background
UIImageView with `alpha`=.5 (“dim view”)
UIImageView with shape layer mask (“bright view”)
The dim view will appear dimmed because its alpha mixes its image with the black of the top-level view.
The bright view is not dimmed, but it only shows where its mask lets it. So I just set the mask to contain the spotlight areas and nowhere else.
Here's what it looks like:
I'll implement it as a subclass of UIView with this interface:
// SpotlightsView.h
#import <UIKit/UIKit.h>
#interface SpotlightsView : UIView
#property (nonatomic, strong) UIImage *image;
- (void)addDraggableSpotlightWithCenter:(CGPoint)center radius:(CGFloat)radius;
#end
I'll need QuartzCore (also called Core Animation) and the Objective-C runtime to implement it:
// SpotlightsView.m
#import "SpotlightsView.h"
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>
I'll need instance variables for the subviews, the mask layer, and an array of individual spotlight paths:
#implementation SpotlightsView {
UIImageView *_dimImageView;
UIImageView *_brightImageView;
CAShapeLayer *_mask;
NSMutableArray *_spotlightPaths;
}
To implement the image property, I just pass it through to your image subviews:
#pragma mark - Public API
- (void)setImage:(UIImage *)image {
_dimImageView.image = image;
_brightImageView.image = image;
}
- (UIImage *)image {
return _dimImageView.image;
}
To add a draggable spotlight, I create a path outlining the spotlight, add it to the array, and flag myself as needing layout:
- (void)addDraggableSpotlightWithCenter:(CGPoint)center radius:(CGFloat)radius {
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(center.x - radius, center.y - radius, 2 * radius, 2 * radius)];
[_spotlightPaths addObject:path];
[self setNeedsLayout];
}
I need to override some methods of UIView to handle initialization and layout. I'll handle being created either programmatically or in a xib or storyboard by delegating the common initialization code to a private method:
#pragma mark - UIView overrides
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
[self commonInit];
}
return self;
}
I'll handle layout in separate helper methods for each subview:
- (void)layoutSubviews {
[super layoutSubviews];
[self layoutDimImageView];
[self layoutBrightImageView];
}
To drag the spotlights when they are touched, I need to override some UIResponder methods. I want to handle each touch separately, so I just loop over the updated touches, passing each one to a helper method:
#pragma mark - UIResponder overrides
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches){
[self touchBegan:touch];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches){
[self touchMoved:touch];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
[self touchEnded:touch];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
[self touchEnded:touch];
}
}
Now I'll implement the private appearance and layout methods.
#pragma mark - Implementation details - appearance/layout
First I'll do the common initialization code. I want to set my background color to black, since that is part of making the dimmed image view dim, and I want to support multiple touches:
- (void)commonInit {
self.backgroundColor = [UIColor blackColor];
self.multipleTouchEnabled = YES;
[self initDimImageView];
[self initBrightImageView];
_spotlightPaths = [NSMutableArray array];
}
My two image subviews will be configured mostly the same way, so I'll call another private method to create the dim image view, then tweak it to actually be dim:
- (void)initDimImageView {
_dimImageView = [self newImageSubview];
_dimImageView.alpha = 0.5;
}
I'll call the same helper method to create the bright view, then add its mask sublayer:
- (void)initBrightImageView {
_brightImageView = [self newImageSubview];
_mask = [CAShapeLayer layer];
_brightImageView.layer.mask = _mask;
}
The helper method that creates both image views sets the content mode and adds the new view as a subview:
- (UIImageView *)newImageSubview {
UIImageView *subview = [[UIImageView alloc] init];
subview.contentMode = UIViewContentModeScaleAspectFill;
[self addSubview:subview];
return subview;
}
To lay out the dim image view, I just need to set its frame to my bounds:
- (void)layoutDimImageView {
_dimImageView.frame = self.bounds;
}
To lay out the bright image view, I need to set its frame to my bounds, and I need to update its mask layer's path to be the union of the individual spotlight paths:
- (void)layoutBrightImageView {
_brightImageView.frame = self.bounds;
UIBezierPath *unionPath = [UIBezierPath bezierPath];
for (UIBezierPath *path in _spotlightPaths) {
[unionPath appendPath:path];
}
_mask.path = unionPath.CGPath;
}
Note that this isn't a true union that encloses each point once. It relies on the fill mode (the default, kCAFillRuleNonZero) to ensure that repeatedly-enclosed points are included in the mask.
Next up, touch handling.
#pragma mark - Implementation details - touch handling
When UIKit sends me a new touch, I'll find the individual spotlight path containing the touch, and attach the path to the touch as an associated object. That means I need an associated object key, which just needs to be some private thing I can take the address of:
static char kSpotlightPathAssociatedObjectKey;
Here I actually find the path and attach it to the touch. If the touch is outside any of my spotlight paths, I ignore it:
- (void)touchBegan:(UITouch *)touch {
UIBezierPath *path = [self firstSpotlightPathContainingTouch:touch];
if (path == nil)
return;
objc_setAssociatedObject(touch, &kSpotlightPathAssociatedObjectKey,
path, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
When UIKit tells me a touch has moved, I see if the touch has a path attached. If so, I translate (slide) the path by the amount that the touch has moved since I last saw it. Then I flag myself for layout:
- (void)touchMoved:(UITouch *)touch {
UIBezierPath *path = objc_getAssociatedObject(touch,
&kSpotlightPathAssociatedObjectKey);
if (path == nil)
return;
CGPoint point = [touch locationInView:self];
CGPoint priorPoint = [touch previousLocationInView:self];
[path applyTransform:CGAffineTransformMakeTranslation(
point.x - priorPoint.x, point.y - priorPoint.y)];
[self setNeedsLayout];
}
I don't actually need to do anything when the touch ends or is cancelled. The Objective-C runtime will de-associated the attached path (if there is one) automatically:
- (void)touchEnded:(UITouch *)touch {
// Nothing to do
}
To find the path that contains a touch, I just loop over the spotlight paths, asking each one if it contains the touch:
- (UIBezierPath *)firstSpotlightPathContainingTouch:(UITouch *)touch {
CGPoint point = [touch locationInView:self];
for (UIBezierPath *path in _spotlightPaths) {
if ([path containsPoint:point])
return path;
}
return nil;
}
#end
I have uploaded a full demo to github.
I've been struggling with this same problem and found some great help here on SO so I thought I'd share my solution combining a few different ideas I found online. One additional feature I added was for the cut-out to have a gradient effect. The added benefit to this solution is that it works with any UIView and not just with images.
First subclass UIView to black out everything except the frames you want cut out:
// BlackOutView.h
#interface BlackOutView : UIView
#property (nonatomic, retain) UIColor *fillColor;
#property (nonatomic, retain) NSArray *framesToCutOut;
#end
// BlackOutView.m
#implementation BlackOutView
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetBlendMode(context, kCGBlendModeDestinationOut);
for (NSValue *value in self.framesToCutOut) {
CGRect pathRect = [value CGRectValue];
UIBezierPath *path = [UIBezierPath bezierPathWithRect:pathRect];
// change to this path for a circular cutout if you don't want a gradient
// UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:pathRect];
[path fill];
}
CGContextSetBlendMode(context, kCGBlendModeNormal);
}
#end
If you don't want the blur effect, then you can swap paths to the oval one and skip the blur mask below. Otherwise, the cutout will be square and filled with a circular gradient.
Create a gradient shape with the center transparent and slowly fading in black:
// BlurFilterMask.h
#interface BlurFilterMask : CAShapeLayer
#property (assign) CGPoint origin;
#property (assign) CGFloat diameter;
#property (assign) CGFloat gradient;
#end
// BlurFilterMask.m
#implementation CRBlurFilterMask
- (void)drawInContext:(CGContextRef)context
{
CGFloat gradientWidth = self.diameter * 0.5f;
CGFloat clearRegionRadius = self.diameter * 0.25f;
CGFloat blurRegionRadius = clearRegionRadius + gradientWidth;
CGColorSpaceRef baseColorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat colors[8] = { 0.0f, 0.0f, 0.0f, 0.0f, // Clear region colour.
0.0f, 0.0f, 0.0f, self.gradient }; // Blur region colour.
CGFloat colorLocations[2] = { 0.0f, 0.4f };
CGGradientRef gradient = CGGradientCreateWithColorComponents (baseColorSpace, colors, colorLocations, 2);
CGContextDrawRadialGradient(context, gradient, self.origin, clearRegionRadius, self.origin, blurRegionRadius, kCGGradientDrawsAfterEndLocation);
CGColorSpaceRelease(baseColorSpace);
CGGradientRelease(gradient);
}
#end
Now you just need to call these two together and pass in the UIViews that you want cutout
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self addMaskInViews:#[self.viewCutout1, self.viewCutout2]];
}
- (void) addMaskInViews:(NSArray *)viewsToCutOut
{
NSMutableArray *frames = [NSMutableArray new];
for (UIView *view in viewsToCutOut) {
view.hidden = YES; // hide the view since we only use their bounds
[frames addObject:[NSValue valueWithCGRect:view.frame]];
}
// Create the overlay passing in the frames we want to cut out
BlackOutView *overlay = [[BlackOutView alloc] initWithFrame:self.view.frame];
overlay.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.8];
overlay.framesToCutOut = frames;
[self.view insertSubview:overlay atIndex:0];
// add a circular gradients inside each view
for (UIView *maskView in viewsToCutOut)
{
BlurFilterMask *blurFilterMask = [BlurFilterMask layer];
blurFilterMask.frame = maskView.frame;
blurFilterMask.gradient = 0.8f;
blurFilterMask.diameter = MIN(maskView.frame.size.width, maskView.frame.size.height);
blurFilterMask.origin = CGPointMake(maskView.frame.size.width / 2, maskView.frame.size.height / 2);
[self.view.layer addSublayer:blurFilterMask];
[blurFilterMask setNeedsDisplay];
}
}
If you just want something that is plug and play, I added a library to CocoaPods that allows you to create overlays with rectangular/circular holes, allowing the user to interact with views behind the overlay. It is a Swift implementation of similar strategies used in other answers. I used it to create this tutorial for one of our apps:
The library is called TAOverlayView, and is open source under Apache 2.0.
Note: I haven't implemented moving holes yet (unless you move the entire overlay as in other answers).
I'm having some problems in my application. The error occurs when I'm in a screen with a lot of icons (UIViews), each one running a animation. This screen reminds the springboard screen, and the animation visual is similar too.
So, if I press the home button, the application doesn't goes to background and I can't do anything. Even the power button doesn't work. And the icons continue shaking.
If I remove the call for label creation method, this don't happens.
Any advice?
Thanks!
Animation method (extracted from Three20 api):
- (void)wobble {
static BOOL wobblesLeft = NO;
if (isEditing)
{
CGFloat rotation = (kWobbleRadians * M_PI) / 180.0;
CGAffineTransform wobbleLeft = CGAffineTransformMakeRotation(rotation);
CGAffineTransform wobbleRight = CGAffineTransformMakeRotation(-rotation);
[UIView beginAnimations:nil context:nil];
NSInteger i = 0;
NSInteger nWobblyButtons = 0;
for(Icon *ic in iconList)
{
++nWobblyButtons;
i++;
if (i % 2)
{
ic.transform = wobblesLeft ? wobbleRight : wobbleLeft;
} else {
ic.transform = wobblesLeft ? wobbleLeft : wobbleRight;
}
}
if (nWobblyButtons >= 1)
{
[UIView setAnimationDuration:kWobbleTime];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(wobble)];
wobblesLeft = !wobblesLeft;
} else {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self performSelector:#selector(wobble) withObject:nil afterDelay:kWobbleTime];
}
[UIView commitAnimations];
}
}
Label creation
-(void)layoutLabel
{
// If title isn`t builded.
if(_lblName == nil)
{
// Create new label.
_lblName = [[UILabel alloc] initWithFrame:CGRectMake(LABEL_POS_X,
LABEL_POS_Y,
LABEL_WIDTH,
LABEL_HEIGHT)];
// Clear the background.
[_lblName setBackgroundColor:[UIColor clearColor]];
// Sets the font.
[_lblName setFont:[UIFont fontWithName:#"Helvetica-Bold" size:11.3]];
[_lblName setAdjustsFontSizeToFitWidth:NO];
// Sets text color
[_lblName setTextColor:[UIColor whiteColor]];
// Adjust the number of lines.
[_lblName setNumberOfLines:2];
// Adjust the aligment to center.
[_lblName setTextAlignment:UITextAlignmentCenter];
// Adjust shadow like the springboad`s icons.
_lblName.layer.shadowOpacity = 1.0;
_lblName.layer.shadowRadius = 0.8;
_lblName.layer.shadowColor = [[UIColor blackColor] CGColor];
_lblName.layer.shadowOffset = CGSizeMake(0, 1.2);
// Add label to container.
[self addSubview:_lblName];
}
}
The problem is:
For each icon I have many subviews, so, the calculations of each icon, at animation time, resulted in a slow code.
So, I found some light to solve this. I builded the same icon structure, but, after, I merged everithing inside a UIImage with a code below.
+ (UIImage *) imageWithView:(UIView *)view
{
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage * img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
This increased my FPS from ~15 to 60 O.o
Follow some help that I found
Link
Shadows don't animate well at all. Disable them during animations and turn them back on once the animation is complete.
I haven't had freezes when animating with shadows, but performance is unacceptably jerky. In my experience, trying to animate anything that has shadows yields unacceptable results.