The story is that, the user selects a part of an image by drawing lines or curves. Based on the selection, a mask will be created using an edge detector.
How can i achieve this?
Are CoreImage and GPUImage could help to make this possible?
Is edge detection enough to make a mask?
I don't have any advanced knowledge on image processing and manipulation. I am just a starter trying to understand and learn it. By the way, this should be done in iOS.
Just like this one (the masking part).
I wrote a sample for you to show you how to use mask to select part of image, if you want to implement more complex effect, you just need to change the path, then you can get any shape you want.
#interface MainViewController ()
#property UIImageView* imageV;
#property UIView* selectView;
#property UIView *blockView;
#property CGPoint startPoint;
#property CGPoint endPoint;
#end
#implementation MainViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
UIBarButtonItem *recoverItem = [[UIBarButtonItem alloc] initWithTitle:#"Recover" style:UIBarButtonItemStyleDone target:self action:#selector(recover)];
[self.navigationItem setLeftBarButtonItem:recoverItem];
_imageV = [[UIImageView alloc] initWithFrame:[UIScreen mainScreen].bounds];
_imageV.image = [UIImage imageNamed:#"test"];
[self.view addSubview:_imageV];
_blockView = [[UIView alloc] init];
_blockView.frame = _imageV.bounds;
_blockView.backgroundColor = [UIColor whiteColor];
[_imageV addSubview:_blockView];
_blockView.hidden = true;
_selectView = [[UIView alloc] init];
_selectView.layer.borderColor = [UIColor greenColor].CGColor;
_selectView.layer.borderWidth = 2;
[_imageV addSubview:_selectView];
_selectView.hidden = true;
UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[self.view addGestureRecognizer:panGR];
}
- (void) handlePan: (UIPanGestureRecognizer *)pan{
if (UIGestureRecognizerStateBegan == pan.state) {
_startPoint = [pan locationInView:_imageV];
}
else if (UIGestureRecognizerStateChanged == pan.state) {
_selectView.hidden = false;
CGPoint currentP = [pan locationInView:_imageV];
float rectWidth = currentP.x - _startPoint.x;
float rectHeight = currentP.y - _startPoint.y;
_selectView.frame = CGRectMake(_startPoint.x, _startPoint.y, rectWidth,rectHeight);
}
else if (UIGestureRecognizerStateEnded == pan.state) {
_endPoint = [pan locationInView:_imageV];
float rectWidth = _endPoint.x - _startPoint.x;
float rectHeight = _endPoint.y - _startPoint.y;
//create path
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRect:[UIScreen mainScreen].bounds];
UIBezierPath *otherPath = [[UIBezierPath bezierPathWithRect:CGRectMake(_startPoint.x, _startPoint.y, rectWidth,rectHeight)] bezierPathByReversingPath];
[maskPath appendPath:otherPath];
//set mask
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = maskPath.CGPath;
[_blockView.layer setMask:maskLayer];
_blockView.hidden = false;
_selectView.hidden = true;
}
}
-(void) recover{
_blockView.hidden = true;
}
#end
Hope it can help you.
Related
I'm using a container for elements which I'd like for it to be blurred. In order to add rounded corners I modified the layer while for the shadow I created a second view named containerShadow and placed it below it.
It works, but not flawlessly. The shadow darkens the effect of the blur. Is there a way to perfect it?
.h
#property (strong) UIVisualEffectView *containerView;
#property (strong) UIView *containerShadowView;
.m
- (instancetype)init {
if (self = [super init]) {
self.containerShadowView = [[UIView alloc] init];
self.containerShadowView.layer.masksToBounds = NO;
self.containerShadowView.layer.shadowRadius = 80.0;
self.containerShadowView.layer.shadowColor = [[UIColor blackColor] CGColor];
self.containerShadowView.layer.shadowOffset = CGSizeZero;
self.containerShadowView.layer.shadowOpacity = 0.25;
[self addSubview:self.containerShadowView];
self.containerView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]];
self.containerView.clipsToBounds = YES;
self.containerView.layer.cornerRadius = 20.0;
[self addSubview:self.containerView];
}
return self;
}
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
// Random container frame for testing...
self.containerView.frame =
CGRectMake(20.0,
200.0,
380,
480);
self.containerShadowView.frame = self.containerView.frame;
self.containerShadowView.layer.shadowPath =
[[UIBezierPath bezierPathWithRoundedRect:self.containerShadowView.bounds cornerRadius:self.containerView.layer.cornerRadius] CGPath];
}
You can do this by masking your containerShadowView with a "cutout" that matches the containerView effect view.
So, this is how it looks - I centered the 380x480 view, and used 0.9 for the .shadowOpacity to emphasize the differences.
Your original on the left, masked version on the right:
Kinda difficult to tell what's really going on, since that could be an opaque layer, we'll add a label behind it:
and, to clarify what we're doing, let's look at it with the containerView effect view hidden:
Here's the source code I used for that - each tap anywhere will cycle through the 8 different layouts:
#import <UIKit/UIKit.h>
#interface OrigShadowView : UIView
#property (strong) UIVisualEffectView *containerView;
#property (strong) UIView *containerShadowView;
#end
#implementation OrigShadowView
- (instancetype)init {
if (self = [super init]) {
self.containerShadowView = [[UIView alloc] init];
self.containerShadowView.layer.masksToBounds = NO;
self.containerShadowView.layer.shadowRadius = 80.0;
self.containerShadowView.layer.shadowColor = [[UIColor blackColor] CGColor];
self.containerShadowView.layer.shadowOffset = CGSizeZero;
self.containerShadowView.layer.shadowOpacity = 0.9;
[self addSubview:self.containerShadowView];
self.containerView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]];
self.containerView.clipsToBounds = YES;
self.containerView.layer.cornerRadius = 20.0;
[self addSubview:self.containerView];
}
return self;
}
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
// let's center a 380 x 480 rectangle in self
CGFloat w = 380.0;
CGFloat h = 480.0;
CGRect vRect = CGRectMake((frame.size.width - w) * 0.5, (frame.size.height - h) * 0.5, w, h);
self.containerView.frame = vRect;
self.containerShadowView.frame = self.containerView.frame;
// change origin to 0,0 because the following will be relative to the subviews
vRect.origin = CGPointZero;
self.containerShadowView.layer.shadowPath =
[[UIBezierPath bezierPathWithRoundedRect:vRect cornerRadius:self.containerView.layer.cornerRadius] CGPath];
}
#end
#interface MaskShadowView : UIView
#property (strong) UIVisualEffectView *containerView;
#property (strong) UIView *containerShadowView;
#end
#implementation MaskShadowView
- (instancetype)init {
if (self = [super init]) {
self.containerShadowView = [[UIView alloc] init];
self.containerShadowView.layer.masksToBounds = NO;
self.containerShadowView.layer.shadowRadius = 80.0;
self.containerShadowView.layer.shadowColor = [[UIColor blackColor] CGColor];
self.containerShadowView.layer.shadowOffset = CGSizeZero;
self.containerShadowView.layer.shadowOpacity = 0.9;
[self addSubview:self.containerShadowView];
self.containerView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]];
self.containerView.clipsToBounds = YES;
self.containerView.layer.cornerRadius = 20.0;
[self addSubview:self.containerView];
}
return self;
}
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
// let's center a 380 x 480 rectangle in self
CGFloat w = 380.0;
CGFloat h = 480.0;
CGRect vRect = CGRectMake((frame.size.width - w) * 0.5, (frame.size.height - h) * 0.5, w, h);
self.containerView.frame = vRect;
self.containerShadowView.frame = self.containerView.frame;
// change origin to 0,0 because the following will be relative to the subviews
vRect.origin = CGPointZero;
self.containerShadowView.layer.shadowPath =
[[UIBezierPath bezierPathWithRoundedRect:vRect cornerRadius:self.containerView.layer.cornerRadius] CGPath];
UIBezierPath *bigBez;
UIBezierPath *clipBez;
// we need a rectangle that will encompass the shadow radius
// double the shadowRadius is probably sufficient, but since it won't be seen
// and won't affect anything else, we'll make it 4x
CGRect expandedRect = CGRectInset(vRect, -self.containerShadowView.layer.shadowRadius * 4.0, -self.containerShadowView.layer.shadowRadius * 4.0);
bigBez = [UIBezierPath bezierPathWithRect:expandedRect];
// we want to "clip out" a rounded rect in the center
// which will be the same size as the visual effect view
clipBez = [UIBezierPath bezierPathWithRoundedRect:vRect cornerRadius:self.containerView.layer.cornerRadius];
[bigBez appendPath:clipBez];
bigBez.usesEvenOddFillRule = YES;
CAShapeLayer *maskLayer = [CAShapeLayer new];
maskLayer.fillRule = kCAFillRuleEvenOdd;
maskLayer.fillColor = UIColor.whiteColor.CGColor;
maskLayer.path = bigBez.CGPath;
self.containerShadowView.layer.mask = maskLayer;
}
#end
#interface BlurTestViewController : UIViewController
{
OrigShadowView *origView;
MaskShadowView *newView;
UILabel *bkgLabel;
// so we can step through on taps to see the results
NSInteger step;
UILabel *infoLabel;
}
#end
#implementation BlurTestViewController
- (void)viewDidLoad {
[super viewDidLoad];
bkgLabel = [UILabel new];
bkgLabel.textColor = UIColor.blueColor;
bkgLabel.font = [UIFont systemFontOfSize:48.0 weight:UIFontWeightBlack];
bkgLabel.textAlignment = NSTextAlignmentCenter;
bkgLabel.numberOfLines = 0;
bkgLabel.text = #"A label can contain an arbitrary amount of text, but UILabel may shrink, wrap, or truncate the text, depending on the size of the bounding rectangle and properties you set. You can control the font, text color, alignment, highlighting, and shadowing of the text in the label.";
bkgLabel.text = #"I'm using a container for elements which I'd like for it to be blurred. In order to add rounded corners I modified the layer while for the shadow I created a second view named containerShadow and placed it below it.";
origView = [OrigShadowView new];
newView = [MaskShadowView new];
[self.view addSubview:bkgLabel];
[self.view addSubview:origView];
[self.view addSubview:newView];
infoLabel = [UILabel new];
infoLabel.font = [UIFont systemFontOfSize:20.0 weight:UIFontWeightBold];
infoLabel.textAlignment = NSTextAlignmentCenter;
[self.view addSubview:infoLabel];
step = 0;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// let's inset the "shadow blur" views 40-points
CGRect r = CGRectInset(self.view.frame, 40.0, 40.0);
origView.frame = r;
newView.frame = r;
// let's put the background label midway down the screen
r.origin.y += r.size.height * 0.5;
r.size.height *= 0.5;
bkgLabel.frame = r;
// put the info label near the top
infoLabel.frame = CGRectMake(40.0, 80.0, r.size.width, 40.0);
[self nextStep];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self nextStep];
}
- (void)nextStep {
bkgLabel.hidden = YES;
origView.hidden = YES;
newView.hidden = YES;
origView.containerView.hidden = NO;
newView.containerView.hidden = NO;
step++;
switch (step) {
case 1:
origView.hidden = NO;
infoLabel.text = #"1: Original View";
break;
case 2:
newView.hidden = NO;
infoLabel.text = #"2: Masked View";
break;
case 3:
bkgLabel.hidden = NO;
origView.hidden = NO;
infoLabel.text = #"3: Original View";
break;
case 4:
bkgLabel.hidden = NO;
newView.hidden = NO;
infoLabel.text = #"4: Masked View";
break;
case 5:
origView.hidden = NO;
origView.containerView.hidden = YES;
infoLabel.text = #"5: Original View - effect view hidden";
break;
case 6:
newView.hidden = NO;
newView.containerView.hidden = YES;
infoLabel.text = #"6: Masked View - effect view hidden";
break;
case 7:
bkgLabel.hidden = NO;
origView.hidden = NO;
origView.containerView.hidden = YES;
infoLabel.text = #"7: Original View - effect view hidden";
break;
default:
bkgLabel.hidden = NO;
newView.hidden = NO;
newView.containerView.hidden = YES;
infoLabel.text = #"8: Masked View - effect view hidden";
step = 0;
break;
}
}
#end
End Goal
I'm doing a tooltip feature for one of my views. The tooltip view will be presented in a modal fashion over the view it's giving tips on. Most of the view will be a dark translucent background, except for one or two key points that need to be completely translucent. These points would be circles or rectangles themselves.
Basically I need to create a UIImage so it can go nicely into a UIImageView.
So far...
Currently I know how to draw an image with one color of any size:
- (UIImage *)imageWithColor:(UIColor *)color scaledToSize:(CGSize)size {
UIImage *image;
CGRect rect = CGRectMake(0.0f, 0.0f, size.width, size.height);
UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, color.CGColor);
CGContextFillRect(context, rect);
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
In other words...
In other words, I could use the above code to make a translucent rectangle. Then all I'd need to do is find a way to "punch holes" in the rectangle of certain sizes at certain points.
Question
How can I create these "keyhole"-like images? Is there another approach?
I don't think you need to do this with an image. You can do it with a CALayer with a mask that's the same size as the view, with added sublayers that are opaque (which will act as holes). The code below adds a darkened layer with square and a circle "holes".
-(IBAction)addMask:(id)sender {
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = self.view.bounds;
CALayer *square = [CALayer layer];
square.frame = CGRectMake(100, 200, 50, 50);
square.backgroundColor = [UIColor blackColor].CGColor;
CAShapeLayer *circle = [CAShapeLayer layer];
UIBezierPath *circlePath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(150, 280, 50, 50)];
circle.path = circlePath.CGPath;
[maskLayer addSublayer:square];
[maskLayer addSublayer:circle];
maskLayer.backgroundColor = [UIColor colorWithWhite:1 alpha:.4].CGColor;
self.view.layer.mask = maskLayer;
}
Here is my full tooltip solution. Credit goes to #rdelmar for showing me that a UIIMage was not needed.
It's probably redundant in some places and the frame work looks atrocious, but it gets the job done. I'd love to hear some improvements:
Home.h file
#property (strong, nonatomic) CALayer *maskLayer;
#property (strong, nonatomic) CATextLayer *profileTitleLayerBack;
#property (strong, nonatomic) CATextLayer *profileDetailLayerBack;
#property (strong, nonatomic) CATextLayer *checkinTitleLayerBack;
#property (strong, nonatomic) CATextLayer *checkinDetailLayerBack;
#property (strong, nonatomic) CATextLayer *dismissLayerBack;
#property (strong, nonatomic) UIButton *tooltipButton;
Home.m file
- (void)viewDidLoad {
[super viewDidLoad];
if ([self firstTimeViewingHome]) {
[self showTooltipLayers];
}
}
//FYI if you want to allow the user to see the tooltip again - just remove the 'DoesNotNeedHomeTooltip' key
- (BOOL)firstTimeViewingHome {
BOOL firstTime = YES;
if ([[NSUserDefaults standardUserDefaults] boolForKey:#"DoesNotNeedHomeTooltip"])
{
firstTime = NO;
}
else {
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:#"DoesNotNeedHomeTooltip"];
[[NSUserDefaults standardUserDefaults] synchronize];
firstTime = YES;
}
return firstTime;
}
- (void)showTooltipLayers {
//I use JASidePanels so if I want to mask the full screen I need to use its view that I store in my session controller
UIView *sidePanelView = self.sessionController.sidePanelController.view;
//A mask is used to cover the view
self.maskLayer = [CALayer layer];
self.maskLayer.frame = sidePanelView.bounds;
self.maskLayer.backgroundColor = [UIColor colorWithWhite:1 alpha:.25].CGColor;
sidePanelView.layer.mask = self.maskLayer;
//Shapes are then used to highlight points in view behind the mask
CAShapeLayer *circle1 = [CAShapeLayer layer];
UIBezierPath *circlePath1 = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(-4, 10, 60, 60)];
circle1.path = circlePath1.CGPath;
[self.maskLayer addSublayer:circle1];
CAShapeLayer *circle2 = [CAShapeLayer layer];
CGRect checkinCircleFrame = CGRectMake(sidePanelView.frame.size.width/2 - 35, sidePanelView.frame.size.height - 65, 70, 70);
UIBezierPath *circlePath2 = [UIBezierPath bezierPathWithOvalInRect:checkinCircleFrame];
circle2.path = circlePath2.CGPath;
[self.maskLayer addSublayer:circle2];
//Text Layers
//
//Each layer is added twice.
//
//The first layer goes in the back (in the main view).
//
//The second layer goes in front (in the mask),
//creating a clear window to the text layer in the back.
//
//This solution was used because the front layer text color
//could not be changed to white (only clear).
//Using only white in the back results being affected by the mask
//Title for Profile tip
self.profileTitleLayerBack = [[CATextLayer alloc] init];
self.profileTitleLayerBack.contentsScale = UIScreen.mainScreen.scale;
self.profileTitleLayerBack.frame = CGRectMake(30, 70, 250, 40);
self.profileTitleLayerBack.font = (__bridge CFTypeRef)(#"Oswald-Light");
self.profileTitleLayerBack.fontSize = 35;
self.profileTitleLayerBack.alignmentMode = kCAAlignmentLeft;
self.profileTitleLayerBack.string = #"OBFUSCATED";
self.profileTitleLayerBack.foregroundColor = UIColor.whiteColor.CGColor;
[sidePanelView.layer addSublayer:self.profileTitleLayerBack];
CATextLayer *profileTitleLayerFront = [[CATextLayer alloc] init];
profileTitleLayerFront.contentsScale = UIScreen.mainScreen.scale;
profileTitleLayerFront.frame = self.profileTitleLayerBack.frame;
profileTitleLayerFront.font = self.profileTitleLayerBack.font;
profileTitleLayerFront.fontSize = self.profileTitleLayerBack.fontSize;
profileTitleLayerFront.alignmentMode = self.profileTitleLayerBack.alignmentMode;
profileTitleLayerFront.string = self.profileTitleLayerBack.string;
[self.maskLayer addSublayer:profileTitleLayerFront];
//Detail for Profile Tip
self.profileDetailLayerBack = [[CATextLayer alloc] init];
self.profileDetailLayerBack.contentsScale = UIScreen.mainScreen.scale;
self.profileDetailLayerBack.frame = CGRectMake(self.profileTitleLayerBack.frame.origin.x + 5,
self.profileTitleLayerBack.frame.origin.y +
self.profileTitleLayerBack.frame.size.height + 8,
300,
150);
self.profileDetailLayerBack.font = (__bridge CFTypeRef)(#"Oswald-Light");
self.profileDetailLayerBack.fontSize = 20;
self.profileDetailLayerBack.alignmentMode = kCAAlignmentLeft;
self.profileDetailLayerBack.string = #"This is your space.\nEverything about your obfuscated,\nobfuscated, and obfuscated.";
self.profileDetailLayerBack.foregroundColor = UIColor.whiteColor.CGColor;
[sidePanelView.layer addSublayer:self.profileDetailLayerBack];
CATextLayer *profileDetailLayerFront = [[CATextLayer alloc] init];
profileDetailLayerFront.contentsScale = UIScreen.mainScreen.scale;
profileDetailLayerFront.frame = self.profileDetailLayerBack.frame;
profileDetailLayerFront.font = self.profileDetailLayerBack.font;
profileDetailLayerFront.fontSize = self.profileDetailLayerBack.fontSize;
profileDetailLayerFront.alignmentMode = self.profileDetailLayerBack.alignmentMode;
profileDetailLayerFront.string = self.profileDetailLayerBack.string;
[self.maskLayer addSublayer:profileDetailLayerFront];
//Title for Checkin tip
self.checkinTitleLayerBack = [[CATextLayer alloc] init];
self.checkinTitleLayerBack.contentsScale = UIScreen.mainScreen.scale;
self.checkinTitleLayerBack.frame = CGRectMake(sidePanelView.frame.size.width/2 - 125,
checkinCircleFrame.origin.y - 40 - 115,
250,
40);
self.checkinTitleLayerBack.font = (__bridge CFTypeRef)(#"Oswald-Light");
self.checkinTitleLayerBack.fontSize = 35;
self.checkinTitleLayerBack.alignmentMode = kCAAlignmentCenter;
self.checkinTitleLayerBack.string = #"OBFUSCATED";
self.checkinTitleLayerBack.foregroundColor = UIColor.whiteColor.CGColor;
[sidePanelView.layer addSublayer:self.checkinTitleLayerBack];
CATextLayer *checkinTitleLayerFront = [[CATextLayer alloc] init];
checkinTitleLayerFront.contentsScale = UIScreen.mainScreen.scale;
checkinTitleLayerFront.frame = self.checkinTitleLayerBack.frame;
checkinTitleLayerFront.font = self.checkinTitleLayerBack.font;
checkinTitleLayerFront.fontSize = self.checkinTitleLayerBack.fontSize;
checkinTitleLayerFront.alignmentMode = self.checkinTitleLayerBack.alignmentMode;
checkinTitleLayerFront.string = self.checkinTitleLayerBack.string;
[self.maskLayer addSublayer:checkinTitleLayerFront];
//Detail for Checkin Tip
self.checkinDetailLayerBack = [[CATextLayer alloc] init];
self.checkinDetailLayerBack.contentsScale = UIScreen.mainScreen.scale;
self.checkinDetailLayerBack.frame = CGRectMake(sidePanelView.frame.size.width/2 - 150,
checkinCircleFrame.origin.y - 115 + 8,
300,
150);
self.checkinDetailLayerBack.font = (__bridge CFTypeRef)(#"Oswald-Light");
self.checkinDetailLayerBack.fontSize = 20;
self.checkinDetailLayerBack.alignmentMode = kCAAlignmentCenter;
self.checkinDetailLayerBack.string = #"Tap to view your obfuscated\nobfuscated and check in to your\nobfuscated and obfuscated.";
self.checkinDetailLayerBack.foregroundColor = UIColor.whiteColor.CGColor;
[sidePanelView.layer addSublayer:self.checkinDetailLayerBack];
CATextLayer *checkinDetailLayerFront = [[CATextLayer alloc] init];
checkinDetailLayerFront.contentsScale = UIScreen.mainScreen.scale;
checkinDetailLayerFront.frame = self.checkinDetailLayerBack.frame;
checkinDetailLayerFront.font = self.checkinDetailLayerBack.font;
checkinDetailLayerFront.fontSize = self.checkinDetailLayerBack.fontSize;
checkinDetailLayerFront.alignmentMode = self.checkinDetailLayerBack.alignmentMode;
checkinDetailLayerFront.string = self.checkinDetailLayerBack.string;
[self.maskLayer addSublayer:checkinDetailLayerFront];
//Add a notice disclosing how to dismiss the tooltip
self.dismissLayerBack = [[CATextLayer alloc] init];
self.dismissLayerBack.contentsScale = UIScreen.mainScreen.scale;
self.dismissLayerBack.frame = CGRectMake(sidePanelView.frame.size.width/2 - 75,
sidePanelView.frame.size.height/2 - 10,
150,
20);
self.dismissLayerBack.font = (__bridge CFTypeRef)(#"Oswald-Light");
self.dismissLayerBack.fontSize = 16;
self.dismissLayerBack.alignmentMode = kCAAlignmentCenter;
self.dismissLayerBack.string = #"(Tap anywhere to dismiss)";
self.dismissLayerBack.foregroundColor = self.view.backgroundColor.CGColor;
[sidePanelView.layer addSublayer:self.dismissLayerBack];
CATextLayer *dismissLayerFront = [[CATextLayer alloc] init];
dismissLayerFront.contentsScale = UIScreen.mainScreen.scale;
dismissLayerFront.frame = self.dismissLayerBack.frame;
dismissLayerFront.font = self.dismissLayerBack.font;
dismissLayerFront.fontSize = self.dismissLayerBack.fontSize;
dismissLayerFront.alignmentMode = self.dismissLayerBack.alignmentMode;
dismissLayerFront.string = self.dismissLayerBack.string;
[self.maskLayer addSublayer:dismissLayerFront];
//Add a clear button over top the view
self.tooltipButton = [[UIButton alloc] initWithFrame:CGRectMake(sidePanelView.frame.origin.x,
sidePanelView.frame.origin.y,
sidePanelView.frame.size.width,
sidePanelView.frame.size.height)];
self.tooltipButton.backgroundColor = UIColor.clearColor;
[self.tooltipButton addTarget:self action:#selector(tooltipButtonPressed) forControlEvents:UIControlEventTouchUpInside];
[sidePanelView addSubview:self.tooltipButton];
[sidePanelView bringSubviewToFront:self.tooltipButton];
}
//dismisses the tooltip view / cleans up
- (void)tooltipButtonPressed {
self.sessionController.sidePanelController.view.layer.mask = nil;
self.maskLayer = nil;
[self.profileTitleLayerBack removeFromSuperlayer];
self.profileTitleLayerBack = nil;
[self.profileDetailLayerBack removeFromSuperlayer];
self.profileDetailLayerBack = nil;
[self.checkinTitleLayerBack removeFromSuperlayer];
self.checkinTitleLayerBack = nil;
[self.checkinDetailLayerBack removeFromSuperlayer];
self.checkinDetailLayerBack = nil;
[self.dismissLayerBack removeFromSuperlayer];
self.dismissLayerBack = nil;
[self.tooltipButton removeFromSuperview];
self.tooltipButton = nil;
}
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'm using UIDynamicsAnimator with good results except that after the animations are done the image that have been pushed and bounced is blurry. Please help me.
#property (nonatomic,weak) IBOutlet UIImageView *someImage;
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
self.gravity = [[UIGravityBehavior alloc] initWithItems:#[self.someImage]];
CGVector gravityDirection = {-1.0, 0.5};
[self.gravity setGravityDirection:gravityDirection];
self.collision = [[UICollisionBehavior alloc] initWithItems:#[self.someImage]];
self.collision.translatesReferenceBoundsIntoBoundary = YES;
self.itemBehaviour = [[UIDynamicItemBehavior alloc] initWithItems:#[self.someImage]];
self.itemBehaviour.elasticity = 0.5;
[self.animator removeAllBehaviors]; // to avoid problems from the last behaviour animation
[self.collision addBoundaryWithIdentifier:#"barrier"
fromPoint:CGPointMake(444,0)
toPoint:CGPointMake(444,768)];
self.pushBehavior = [[UIPushBehavior alloc] initWithItems:#[self.someImage] mode:UIPushBehaviorModeInstantaneous];
self.pushBehavior.magnitude = 1.0f;
self.pushBehavior.angle = 0.0f;
[self.animator addBehavior:self.pushBehavior];
self.pushBehavior.pushDirection = CGVectorMake(1.0f, 0.0f)
self.pushBehavior.active = YES;
[self.animator addBehavior:self.itemBehaviour];
[self.animator addBehavior:self.collision];
[self.animator addBehavior:self.gravity];
I think your problem is that the image isn't on an exact point when the animation is done. I would sugest that you add a delegate and when the animation pauses you round the x and y for the image to the closest integer.
I'm wanna create an effect like firework. I know that I can use for it CAEmitterCell. I tried to download few examples, but I still in progress with this feature in iOS 5.0
Anybody know good tutorial how to create firework effect using CAEmitterCell?
I need to create something like below:
All particles are going from the center (red circle) in direction to green circles. Red circle this is initial point that will have some CGPoint value.
I have found this solution:
it's how DWFParticleView.h file looks
#import <UIKit/UIKit.h>
#interface DWFParticleView : UIView
#property (nonatomic) CGFloat time;
-(void)configurateEmmiter;
-(void)setEmitterPositionFromTouch: (UITouch*)t;
-(void)setIsEmitting:(BOOL)isEmitting andPoint:(CGPoint)point;
#end
this is how looks DWFParticleView.m
#import "DWFParticleView.h"
#import <QuartzCore/QuartzCore.h>
#implementation DWFParticleView
{
CAEmitterLayer* fireEmitter; //1
}
-(void)configurateEmmiter
{
//set ref to the layer
fireEmitter = (CAEmitterLayer*)self.layer; //2
//configure the emitter layer
fireEmitter.emitterPosition = CGPointMake(50, 50);
fireEmitter.emitterSize = CGSizeMake(5, 5);
CAEmitterCell* fire = [CAEmitterCell emitterCell];
fire.birthRate = 0;
fire.lifetime = 2.0;
fire.lifetimeRange = 0.5;
//fire.color = [[UIColor colorWithRed:0.8 green:0.4 blue:0.2 alpha:0.6] CGColor];
fire.contents = (id)[[UIImage imageNamed:#"star_icon.png"] CGImage];
[fire setName:#"fire"];
fire.velocity = 80;
fire.velocityRange = 20;
fire.emissionRange = M_PI * 2.0f;
fire.scaleSpeed = 0.1;
fire.spin = 0.5;
//add the cell to the layer and we're done
fireEmitter.emitterCells = [NSArray arrayWithObject:fire];
fireEmitter.renderMode = kCAEmitterLayerAdditive;
}
+ (Class) layerClass {
return [CAEmitterLayer class];
}
-(void)setEmitterPositionFromTouch: (UITouch*)t
{
//change the emitter's position
fireEmitter.emitterPosition = [t locationInView:self];
}
-(void)setIsEmitting:(BOOL)isEmitting andPoint:(CGPoint)point
{
fireEmitter.emitterPosition = point;
//turn on/off the emitting of particles
[fireEmitter setValue:[NSNumber numberWithInt:isEmitting?50:0]
forKeyPath:#"emitterCells.fire.birthRate"];
[self performSelector:#selector(decayStep) withObject:nil afterDelay:self.time];
}
- (void)decayStep {
[fireEmitter setValue:[NSNumber numberWithInt:0]
forKeyPath:#"emitterCells.fire.birthRate"];
}
#end
this is method one tap on screen for example that shows our effect
- (void)tap {
DWFParticleView *fireView = [[DWFParticleView alloc] init];
fireView.time = 0.3;
[fireView setUserInteractionEnabled:NO];
[fireView configurateEmmiter];
[fireView setFrame:CGRectMake(0, 0, 0, 0)];
[fireView setBackgroundColor:[UIColor redColor]];
[self.view addSubview:fireView];
}
the material I have found here: ray