Related
I need to calculate the visible CGRect of a UIView subview, in the coordinates of the original view. I've got it working if the scale is 1, but if one of the superviews or the view itself is scaled (pinch), the visible CGRect origin is offset slightly.
This works when the scale of the views is 1 or the view is a subview of the root view:
// return the part of the passed view that is visible
// TODO: figure out why result origin is wrong for scaled subviews
//
- (CGRect)getVisibleRect:(UIView *)view {
// get the root view controller (and it's view is vc.view)
UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
// get the view's frame in the root view's coordinate system
CGRect frame = [vc.view convertRect:view.frame fromView:view.superview];
// get the intersection of the root view bounds and the passed view frame
CGRect intersection = CGRectIntersection(vc.view.bounds, frame);
// adjust the intersection coordinates thru any nested views
UIView *loopView = view;
do {
intersection = [loopView convertRect:intersection fromView:loopView.superview];
loopView = loopView.superview;
} while (loopView != vc.view);
return intersection; // may be same as the original view frame
}
When a subview is scaled, the size of the resultant view is correct, but the origin is offset by a small amount. It appears that the convertRect does not calculate the origin properly for scaled subviews.
I tried adjusting the origin relative to the X/Y transform scale but I could not get the calculation correct. Perhaps someone can help?
To save time, here is a complete test ViewController.m, where a box with an X is drawn on the visible part of the views - just create a reset button in the Main.storyboard and connect it to the reset method:
//
// ViewController.m
// VisibleViewDemo
//
// Copyright © 2018 ByteSlinger. All rights reserved.
//
#import "ViewController.h"
CG_INLINE void drawLine(UIView *view,CGPoint point1,CGPoint point2, UIColor *color, NSString *layerName) {
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:point1];
[path addLineToPoint:point2];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [path CGPath];
shapeLayer.strokeColor = color.CGColor;
shapeLayer.lineWidth = 2.0;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.name = layerName;
[view.layer addSublayer:shapeLayer];
}
CG_INLINE void removeShapeLayers(UIView *view,NSString *layerName) {
if (view.layer.sublayers.count > 0) {
for (CALayer *layer in [view.layer.sublayers copy]) {
if ([layer.name isEqualToString:layerName]) {
[layer removeFromSuperlayer];
}
}
}
}
CG_INLINE void drawXBox(UIView *view, CGRect rect,UIColor *color) {
NSString *layerName = #"xbox";
removeShapeLayers(view, layerName);
CGPoint topLeft = CGPointMake(rect.origin.x,rect.origin.y);
CGPoint topRight = CGPointMake(rect.origin.x + rect.size.width,rect.origin.y);
CGPoint bottomLeft = CGPointMake(rect.origin.x, rect.origin.y + rect.size.height);
CGPoint bottomRight = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y + rect.size.height);
drawLine(view,topLeft,topRight,color,layerName);
drawLine(view,topRight,bottomRight,color,layerName);
drawLine(view,topLeft,bottomLeft,color,layerName);
drawLine(view,bottomLeft,bottomRight,color,layerName);
drawLine(view,topLeft,bottomRight,color,layerName);
drawLine(view,topRight,bottomLeft,color,layerName);
}
#interface ViewController ()
#end
#implementation ViewController
UIView *view1;
UIView *view2;
UIView *view3;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
CGFloat width = [UIScreen mainScreen].bounds.size.width / 2;
CGFloat height = [UIScreen mainScreen].bounds.size.height / 4;
view1 = [[UIView alloc] initWithFrame:CGRectMake(width / 2, height / 2, width, height)];
view1.backgroundColor = UIColor.yellowColor;
[self.view addSubview:view1];
[self addGestures:view1];
view2 = [[UIView alloc] initWithFrame:CGRectMake(width / 2, height / 2 + height + 16, width, height)];
view2.backgroundColor = UIColor.greenColor;
[self.view addSubview:view2];
[self addGestures:view2];
view3 = [[UIView alloc] initWithFrame:CGRectMake(10, 10, width / 2, height / 2)];
view3.backgroundColor = [UIColor.blueColor colorWithAlphaComponent:0.5];
[view1 addSubview:view3]; // this one will behave differently
[self addGestures:view3];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self checkOnScreen:view1];
[self checkOnScreen:view2];
[self checkOnScreen:view3];
}
- (IBAction)reset:(id)sender {
view1.transform = CGAffineTransformIdentity;
view2.transform = CGAffineTransformIdentity;
view3.transform = CGAffineTransformIdentity;
[self.view setNeedsLayout];
}
- (void)addGestures:(UIView *)view {
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc]
initWithTarget:self action:#selector(handlePan:)];
[view addGestureRecognizer:panGestureRecognizer];
UIPinchGestureRecognizer *pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc]
initWithTarget:self action:#selector(handlePinch:)];
[view addGestureRecognizer:pinchGestureRecognizer];
}
// return the part of the passed view that is visible
- (CGRect)getVisibleRect:(UIView *)view {
// get the root view controller (and it's view is vc.view)
UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
// get the view's frame in the root view's coordinate system
CGRect frame = [vc.view convertRect:view.frame fromView:view.superview];
// get the intersection of the root view bounds and the passed view frame
CGRect intersection = CGRectIntersection(vc.view.bounds, frame);
// adjust the intersection coordinates thru any nested views
UIView *loopView = view;
do {
intersection = [loopView convertRect:intersection fromView:loopView.superview];
loopView = loopView.superview;
} while (loopView != vc.view);
return intersection; // may be same as the original view
}
- (void)checkOnScreen:(UIView *)view {
CGRect visibleRect = [self getVisibleRect:view];
if (CGRectEqualToRect(visibleRect, CGRectNull)) {
visibleRect = CGRectZero;
}
drawXBox(view,visibleRect,UIColor.blackColor);
}
//
// Pinch (resize) an image on the ViewController View
//
- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer {
static CGAffineTransform initialTransform;
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self.view bringSubviewToFront:recognizer.view];
initialTransform = recognizer.view.transform;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
} else {
recognizer.view.transform = CGAffineTransformScale(initialTransform,recognizer.scale,recognizer.scale);
[self checkOnScreen:recognizer.view];
[self.view setNeedsLayout]; // update subviews
}
}
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
static CGAffineTransform initialTransform;
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self.view bringSubviewToFront:recognizer.view];
initialTransform = recognizer.view.transform;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
} else {
//get the translation amount in x,y
CGPoint translation = [recognizer translationInView:recognizer.view];
recognizer.view.transform = CGAffineTransformTranslate(initialTransform,translation.x,translation.y);
[self checkOnScreen:recognizer.view];
[self.view setNeedsLayout]; // update subviews
}
}
#end
So you need to know the real visible frame of a view that is somehow derived from bounds+center+transform and calculate everything else from that, instead of the ordinary frame value. This means you'll also have to recreate convertRect:fromView: to be based on that. I always sidestepped the problem by using transform only for short animations where such calculations are not necessary. Thinking about coding such a -getVisibleRect: method makes me want to run away screaming ;)
What is a frame?
The frame property is derived from center and bounds.
Example:
center is (60,50)
bounds is (0,0,100,100)
=> frame is (10,0,100,100)
Now you change the frame to (10,20,100,100). Because the size of the view did not change, this results only in a change to the center. The new center is now (60,70).
How about transform?
Say you now transform the view, by scaling it to 50%.
=> the view has now half the size than before, while still keeping the same center. It looks like the new frame is (35,45,50,50). However the real result is:
center is still (60,50): this is expected
bounds is still (0,0,100,100): this should be expected too
frame is still (10,20,100,100): this is somewhat counterintuitive
frame is a calculated property, and it doesn't care at all about the current transform. This means that the value of the frame is meaningless whenever transform is not the identity transform. This is even documented behaviour. Apple calls the value of frame to be "undefined" in this case.
Consequences
This has the additional consequences that methods such as convertRect:fromView: do not work properly when there are non-standard transforms involved. This is because all these methods rely on either frame or bounds of views, and they break as soon as there are transforms involved.
What can be done?
Say you have three views:
view1 (no transform)
view2 (scale transform 50%)
view3 (no transform)
and you want to know the coordinates of view3 from the point of view of view1.
From the point of view of view2, view3 has frame view3.frame. Easy.
From the point of view of view1, view2 has not frame view2.frame, but the visible frame is a rectangle with size view2.bounds/2 and center view2.center.
To get this right you need some basic linear algebra (with matrix multiplications). (And don't forget the anchorPoint..)
I hope it helps..
What can be done for real?
In your question you said that there is an offset. Maybe you can just calculate the error now? The error should be something like 0.5 * (1-scale) * (bounds.size) . If you can calculate the error, you can subtract it and call it a day :)
Thanks to #Michael for putting in so much effort in his answer. It didn't solve the problem but it made me think some more and try some other things.
And voila, I tried something that I'm certain I had done before, but this time I started with my latest code. It turns out a simple solution did the trick. The builtin UIView convertRect:fromView and convertRect:toView worked as expected when used together.
I apologize to anyone that has spent time on this. I'm humbled in my foolishness and how much time I have spent on this. I must have made a mistake somewhere when I tried this before because it didn't work. But this works very well now:
// return the part of the passed view that is visible
- (CGRect)getVisibleRect:(UIView *)view {
// get the root view controller (and it's view is vc.view)
UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
// get the view's frame in the root view's coordinate system
CGRect rootRect = [vc.view convertRect:view.frame fromView:view.superview];
// get the intersection of the root view bounds and the passed view frame
CGRect rootVisible = CGRectIntersection(vc.view.bounds, rootRect);
// convert the rect back to the initial view's coordinate system
CGRect visible = [view convertRect:rootVisible fromView:vc.view];
return visible; // may be same as the original view frame
}
If someone uses the Viewcontroller.m from my question, just replace the getVisibleRect method with this one and it will work very nicely.
NOTE: I tried rotating the view and the visible rect is rotated too because I displayed it on the view itself. I guess I could reverse whatever the view rotation is on the shape layers, but that's for another day!
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².
In one of my views I have a bottom bar. A tap on this view animates a temporary cart half way to the screen. Now, where there is no item in the temporary table I show a no data overlay which is nothing but a view with an empty cart image and text underneath it.
The issue is: when this table animates to expand and collapse, the no data overlay does not look good during animation. It appears that the image is also shrinking/expanding until the temporary table view stops animating.
So, I tried by playing with the alpha of the temporary table view while this animation happens so that it does not look that bad. But this is not helping either. There is an abrupt flash in the end.
Any suggestions?
self.temporaryTable.backgroundView = self.noDataOverlay;
[UIView animateWithDuration:0.5 animations:^{
self.temporaryTable.alpha = 0.5;
} completion:^(BOOL finished) {
self.temporaryTable.alpha = 1.0;
}];
Here is my code for drawing the image:
- (void)drawRect:(CGRect)iRect scalingImage:(BOOL)iShouldScale {
CGRect aRect = self.superview.bounds;
// Draw our image
CGRect anImageRect = iRect;
if (self.image) {
//scale the image based on the height
anImageRect = CGRectZero;
anImageRect.size.height = self.image.size.height;
anImageRect.size.width = self.image.size.width;
#ifdef ACINTERNAL
if (iShouldScale) {
anImageRect = [self aspectFittedRect:anImageRect max:aRect];
} else {
anImageRect.origin.x = (iRect.size.width/2) - (anImageRect.size.width/2);
anImageRect.origin.y = kTopMargin;
}
#else
anImageRect.origin.x = (iRect.size.width/2) - (anImageRect.size.width/2);
anImageRect.origin.y = kTopMargin;
#endif
if (self.shouldCenterImage)
anImageRect.origin.y = (iRect.size.height/2) - (anImageRect.size.height/2);
[self.image drawInRect:anImageRect];
} else {
anImageRect.origin.y = (iRect.size.height/6);
anImageRect.size.height = (iRect.size.height/6);
}
// Draw our title and message
if (self.title) {
CGFloat aTop = anImageRect.origin.y + anImageRect.size.height + kSpacer;
CGFloat aWidth = aRect.size.width;
UIColor *aColor = [UIColor colorWithRed:96/256.0 green:106/256.0 blue:122/256.0 alpha:1];
[aColor set];
CGRect aTextRect = CGRectMake(0, aTop, aWidth, kTitleHeight * 2);
UIFont *aTitleFont = [UIFont boldSystemFontOfSize:kTitleFontSize];
[self.title drawInRect:aTextRect withFont:aTitleFont lineBreakMode:NSLineBreakByClipping alignment:NSTextAlignmentCenter];
if (self.subTitle) {
UIFont *aSubTitleFont = [UIFont systemFontOfSize:kSubTitleFontSize];
aTextRect = CGRectMake(0, aTop+kSpacer, aWidth, kTitleHeight);
[self.subTitle drawInRect:aTextRect withFont:aSubTitleFont lineBreakMode:NSLineBreakByClipping alignment:NSTextAlignmentCenter];
}
}
}
- (void)addToView:(UIView *)iView {
// setting a unique tag number here so that if the user has any other tag there should not be a conflict
UIView *aView = [iView viewWithTag:699];
if (aView) {
[aView removeFromSuperview];
}
self.tag = 699;
[iView addSubview:self];
}
- (void)removeView {
[super removeFromSuperview];
}
-(void)setViewFrame:(CGRect) iFrame {
CGRect aRect = self.frame;
aRect.size.width = iFrame.size.width;
aRect.size.height = iFrame.size.height;
self.frame = aRect;
[self setNeedsDisplay];
}
- (CGRect)aspectFittedRect:(CGRect)iRect max:(CGRect)iMaxRect {
float anOriginalAspectRatio = iRect.size.width / iRect.size.height;
float aMaxAspectRatio = iMaxRect.size.width / iMaxRect.size.height;
CGRect aNewRect = iMaxRect;
if (anOriginalAspectRatio > aMaxAspectRatio) { // scale by width
aNewRect.size.height = iMaxRect.size.width * iRect.size.height / iRect.size.width;
} else {
aNewRect.size.width = iMaxRect.size.height * iRect.size.width / iRect.size.height;
}
aNewRect.origin.y = (iMaxRect.size.height - aNewRect.size.height)/2.0;
aNewRect.origin.x = (iMaxRect.size.width - aNewRect.size.width)/2.0;
return CGRectIntegral(aNewRect);
}
One possibility here is to fix the original problem, namely the fact that the empty cart image is expanding/collapsing as the view animates. Without seeing your code it is difficult to solve this problem, but in my own code what I do is set the contentMode of the UIImageView to UIViewContentModeBottom. This causes the image to stay the same size even though the image view that contains it may grow and shrink as part of the animation.
You see a flash because you animate alpha up to 0.5 and then when animation completes you set it from 0.5 to 1.0. Just animate the alpha up to 1.0:
[UIView animateWithDuration:0.5
animations:^
{
self.temporaryTable.alpha = 1.0;
}];
i am trying to crate a dynamic rectangle on imageview. it means rectangle should start on the touch and as long as user move the touch it should be bigger in that direction.
friend suggest me, please.
You need to use Core Graphics for that, try this tutorial:
Quartz 2D Programming Guide
It sounds like maybe you want a selection rectangle on top of the UIImage, not just the ability to resize the image.
If that's the case, I'd recommend the following structure:
Load the UIImage into a UIImageView and place that on screen.
Create a UIView that is a sibling of the UIImageView
Override the -drawRect: method of the UIView from #2 to simply draw a bordered outline.
Detect your user's touches using -touchesBegan:/-touchesMoved:/-touchesEnded:, etc.
Based on your logic and math from #4, adjust the UIView's frame in an animation block.
In my experience, animations of this nature are easier to make functional when using shocks and struts and -touchesBegan, etc, as opposed to Autolayout and UIGestureRecognizers, but YMMV.
Of course, what you're doing might require some advanced math, based on how you answer questions like the following:
Can the user move the selection rectangle?
Do I want the user to grab an "edge" to be able to resize it?
How close does the user have to be to "grab" the edge?
If not, is the user just "pushing" in a direction to make the rectangle bigger, no matter where their finger is?
The following code does the following:
Loads an image into a UIImageView
Allows the user to place a 100x100 selection rectangle (at first)
Allows the user to move the selection rectangle by touching/dragging in interior of rectangle
Allows the user to resize the select rectangle by grabbing one of 8 edge areas
Updates drawing of selection rectangle to indicate "active" mode from "inactive" mode
Assumptions made by the code:
User will interact with screen using 1 finger
UIImageView takes up the entire view
Possible updates needed by this code:
User can move selection rectangle partly offscreen
This code doesn't DO anything with the selection rectangle (like, take a snapshot/image of selection rectangle contents)
#define GRAB_DISTANCE 10
#define VIEW_PLACEMENT_ANIMATION_DURATION 0.1
#define VIEW_SIZING_ANIMATION_DURATION 0.1
typedef enum {
kMSSLineTypeDashed,
kMSSLineTypeSolid
} MSSLineType;
typedef enum {
kMSSRectangleGrabZoneBottom,
kMSSRectangleGrabZoneBottomLeft,
kMSSRectangleGrabZoneBottomRight,
kMSSRectangleGrabZoneLeft,
kMSSRectangleGrabZoneNone,
kMSSRectangleGrabZoneRight,
kMSSRectangleGrabZoneTop,
kMSSRectangleGrabZoneTopLeft,
kMSSRectangleGrabZoneTopRight
} MSSRectangleGrabZone;
typedef enum {
kMSSRectangleStatusNone,
kMSSRectangleStatusPlacement,
kMSSRectangleStatusResizing
} MSSRectangleStatus;
#interface MSSSelectionView : UIView
#property (assign, nonatomic) MSSLineType currentLineType;
#property (strong, nonatomic) UIColor *borderColor;
#end
#implementation MSSSelectionView
- (void)awakeFromNib {
[super awakeFromNib];
self.currentLineType = kMSSLineTypeSolid;
}
- (void)drawRect:(CGRect)rect {
// Just make a border, 2 points wide, 1 point inset (so it is all contained by the view)
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect borderRect = CGRectInset(rect, 1, 1);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, 2.0f);
switch (self.currentLineType) {
case kMSSLineTypeDashed:
{
CGFloat lengths[2] = {3, 4};
CGContextSetLineDash(context, 0.0f, lengths, 2);
}
break;
case kMSSLineTypeSolid:
{
CGContextSetLineDash(context, 0.0f, NULL, 0);
}
break;
default:
break;
}
[self.borderColor setStroke];
CGContextStrokeRect(context, borderRect);
}
#end
#import "MSSViewController.h"
#interface MSSViewController ()
#property (assign, nonatomic) BOOL selectionIsVisible;
#property (assign, nonatomic) MSSRectangleGrabZone currentGrabZone;
#property (assign, nonatomic) MSSRectangleStatus currentStatus;
#property (strong, nonatomic) IBOutlet MSSSelectionView *selectionView;
#property (strong, nonatomic) IBOutlet UIImageView *imageView;
#end
#implementation MSSViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.imageView.image = [UIImage imageNamed:#"image.jpg"];
self.currentGrabZone = kMSSRectangleGrabZoneNone;
self.currentStatus = kMSSRectangleStatusNone;
}
#pragma mark - Touch Handling
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Get a touch object (assuming just a 1-finger touch here)
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self.view];
if (self.selectionIsVisible == NO) {
// Showing the selection view for the first time
// Update instance property
self.selectionIsVisible = YES;
// Place the rectangle (centered around touch)
self.selectionView.center = location;
// Unhide the rectangle
self.selectionView.hidden = NO;
// Set the status flag to placement
self.currentStatus = kMSSRectangleStatusPlacement;
// Change the border color to indicate that it's active
self.selectionView.borderColor = [UIColor lightGrayColor];
self.selectionView.currentLineType = kMSSLineTypeDashed;
[self.selectionView setNeedsDisplay];
} else {
// Selection view already visible, so first make sure the touch was inside the selection view
if (CGRectContainsPoint(self.selectionView.frame, location)) {
// The touch was inside the selection view, so update the selection view's line drawing properties
self.selectionView.borderColor = [UIColor lightGrayColor];
self.selectionView.currentLineType = kMSSLineTypeDashed;
[self.selectionView setNeedsDisplay];
// Set status flag based on proximity to edge
BOOL edgeGrabbed = [self location:location inGrabZoneForRect:self.selectionView.frame];
if (edgeGrabbed == YES) {
// The user has grabbed the edge, so allow selection view resizing
self.currentStatus = kMSSRectangleStatusResizing;
self.currentGrabZone = [self zoneGrabbedForPoint:location inRect:self.selectionView.frame];
} else {
// The user has touched the interior, so allow selection view movement/placement
self.currentStatus = kMSSRectangleStatusPlacement;
self.currentGrabZone = kMSSRectangleGrabZoneNone;
}
}
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
// Get a touch object (assuming just a 1-finger touch here)
UITouch *touch = [touches anyObject];
CGPoint currentLocation = [touch locationInView:self.view];
CGPoint previousLocation = [touch previousLocationInView:self.view];
CGFloat xDelta = currentLocation.x - previousLocation.x;
CGFloat yDelta = currentLocation.y - previousLocation.y;
CGRect frame = self.selectionView.frame;
switch (self.currentStatus) {
case kMSSRectangleStatusNone:
// Do nothing
break;
case kMSSRectangleStatusPlacement:
{
// The entire selection view should be moved under the user's finger
frame.origin.x = frame.origin.x + xDelta;
frame.origin.y = frame.origin.y + yDelta;
}
break;
case kMSSRectangleStatusResizing:
{
switch (self.currentGrabZone) {
case kMSSRectangleGrabZoneBottom:
{
// Make the view's frame taller or shorter based on yDelta
frame.size.height = frame.size.height + yDelta;
}
break;
case kMSSRectangleGrabZoneBottomLeft:
{
// Make the view's frame taller or shorter based on yDelta
// Make the view's frame wider or narrower PLUS move origin based on xDelta
frame.origin.x = frame.origin.x + xDelta;
frame.size.height = frame.size.height + yDelta;
frame.size.width = frame.size.width - xDelta;
}
break;
case kMSSRectangleGrabZoneBottomRight:
{
// Make the view's frame taller or shorter based on yDelta
// Make the view's frame wider or narrower based on xDelta
frame.size.height = frame.size.height + yDelta;
frame.size.width = frame.size.width + xDelta;
}
break;
case kMSSRectangleGrabZoneLeft:
{
// Make the view's frame wider or narrower PLUS move origin based on xDelta
frame.origin.x = frame.origin.x + xDelta;
frame.size.width = frame.size.width - xDelta;
}
break;
case kMSSRectangleGrabZoneNone:
// Do nothing
break;
case kMSSRectangleGrabZoneRight:
{
// Make the view's frame wider or narrower based on xDelta
frame.size.width = frame.size.width + xDelta;
}
break;
case kMSSRectangleGrabZoneTop:
{
// Make the view's frame taller or shorter PLUS move origin based on yDelta
frame.origin.y = frame.origin.y + yDelta;
frame.size.height = frame.size.height - yDelta;
}
break;
case kMSSRectangleGrabZoneTopLeft:
{
// Make the view's frame wider or narrower PLUS move origin based on xDelta
// Make the view's frame taller or shorter PLUS move origin based on yDelta
frame.origin.x = frame.origin.x + xDelta;
frame.origin.y = frame.origin.y + yDelta;
frame.size.width = frame.size.width - xDelta;
frame.size.height = frame.size.height - yDelta;
}
break;
case kMSSRectangleGrabZoneTopRight:
{
// Make the view's frame wider or narrower based on xDelta
// Make the view's frame taller or shorter PLUS move origin based on yDelta
frame.origin.y = frame.origin.y + yDelta;
frame.size.height = frame.size.height - yDelta;
frame.size.width = frame.size.width + xDelta;
}
break;
default:
break;
}
}
break;
default:
break;
}
// Any frame changes made above should be animated here
[UIView animateWithDuration:VIEW_PLACEMENT_ANIMATION_DURATION
animations:^{
self.selectionView.frame = frame;
}
completion:^(BOOL finished) {
[self.selectionView setNeedsDisplay];
}
];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
// Nothing much to do, just make the border black to indicate activity is done
self.currentGrabZone = kMSSRectangleGrabZoneNone;
self.currentStatus = kMSSRectangleStatusNone;
self.selectionView.borderColor = [UIColor blackColor];
self.selectionView.currentLineType = kMSSLineTypeSolid;
[self.selectionView setNeedsDisplay];
}
#pragma mark - Rectangle helper methods
- (BOOL)location:(CGPoint)location inGrabZoneForRect:(CGRect)rect {
if (CGRectContainsPoint(rect, location)) {
// The point is inside the rectangle, so determine if it's in the grab zone or the interior
CGRect nonGrabZoneRect = CGRectInset(rect, GRAB_DISTANCE, GRAB_DISTANCE);
if (CGRectContainsPoint(nonGrabZoneRect, location)) {
// This point is in the interior (non-grab zone)
return NO;
} else {
// This point is in the grab zone
return YES;
}
} else {
// The point is not inside the rectangle, which means they didn't grab the edge/border
return NO;
}
}
- (MSSRectangleGrabZone)zoneGrabbedForPoint:(CGPoint)point inRect:(CGRect)rect {
CGRect topLeftGrabZone = CGRectMake(rect.origin.x, rect.origin.y, GRAB_DISTANCE, GRAB_DISTANCE);
CGRect topGrabZone = CGRectMake(rect.origin.x + GRAB_DISTANCE, rect.origin.y, rect.size.width - (2 * GRAB_DISTANCE), GRAB_DISTANCE);
CGRect topRightGrabZone = CGRectMake(rect.origin.x + rect.size.width - GRAB_DISTANCE, rect.origin.y, GRAB_DISTANCE, GRAB_DISTANCE);
CGRect leftGrabZone = CGRectMake(rect.origin.x, rect.origin.y + GRAB_DISTANCE, GRAB_DISTANCE, rect.size.height - (2 * GRAB_DISTANCE));
CGRect rightGrabZone = CGRectMake(rect.origin.x + rect.size.width - GRAB_DISTANCE, rect.origin.y + GRAB_DISTANCE, GRAB_DISTANCE, rect.size.height - (2 * GRAB_DISTANCE));
CGRect bottomLeftGrabZone = CGRectMake(rect.origin.x, rect.origin.y + rect.size.height - GRAB_DISTANCE, GRAB_DISTANCE, GRAB_DISTANCE);
CGRect bottomGrabZone = CGRectMake(rect.origin.x + GRAB_DISTANCE, rect.origin.y + rect.size.height - GRAB_DISTANCE, rect.size.width - (2 * GRAB_DISTANCE), GRAB_DISTANCE);
CGRect bottomRightGrabZone = CGRectMake(rect.origin.x + rect.size.width - GRAB_DISTANCE, rect.origin.y + rect.size.height - GRAB_DISTANCE, GRAB_DISTANCE, GRAB_DISTANCE);
if (CGRectContainsPoint(topLeftGrabZone, point)) {
return kMSSRectangleGrabZoneTopLeft;
} else if (CGRectContainsPoint(topGrabZone, point)) {
return kMSSRectangleGrabZoneTop;
} else if (CGRectContainsPoint(topRightGrabZone, point)) {
return kMSSRectangleGrabZoneTopRight;
} else if (CGRectContainsPoint(leftGrabZone, point)) {
return kMSSRectangleGrabZoneLeft;
} else if (CGRectContainsPoint(rightGrabZone, point)) {
return kMSSRectangleGrabZoneRight;
} else if (CGRectContainsPoint(bottomLeftGrabZone, point)) {
return kMSSRectangleGrabZoneBottomLeft;
} else if (CGRectContainsPoint(bottomGrabZone, point)) {
return kMSSRectangleGrabZoneBottom;
} else if (CGRectContainsPoint(bottomRightGrabZone, point)) {
return kMSSRectangleGrabZoneBottomRight;
} else {
return kMSSRectangleGrabZoneNone;
}
}
#end
You can create imageview programmatically and resize its frame as touch moves. See below link may it help you.
How can I draw dynamic rectangle on screen according to touchMove event on iOS
I'm at the end of my wisdom...
I have a view which should display an animated circular section which is pre-rendered and available as png files (both retina and non-retina versions, correctly named; pie_p_000.png vs. pie_p_000#2x.png). This section should be animated according the change to some percentage value within the app. So I made a subclass of UIView which has a custom CALayer within its layer hierarchy. The plan is to implement a custom animatable property for the CALayer subclass and change pictures in the overridden method drawInContext:. So far so good, the plan worked and the animation is shown when I change the percentage value using the function setPercentageWithFloat: of the view (full source below).
The thing is: I really don't know why my iPhone4 always presents the low-res image. I tried already playing around with scale factors, but that didn't help. Either the images are presented in the right size and low-res or the images are presented double size.
Overriding display: and setting the contents property directly has the effect that the animation doesn't appear (during the animation no image is presented); after the animation time the final image is presented. In this case the correct resolution image is presented.
Btw: the following code is not very sophisticated in error-handling, flexibility and elegance yet as it is an attempt just to get that thing running ;) - so the image is still presented flipped and so on...
I hope that somebody has a hint for me.
Thanks
The view:
#import "ScrollBarView.h"
#import "ScrollBarLayer.h"
#implementation ScrollBarView
#synthesize sbl;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
- (void)setImagesWithName:(NSString*)imageName {
ScrollBarLayer *ssbl = [[ScrollBarLayer alloc] init];
ssbl.frame = CGRectMake(0, 0, 30, 30);
[ssbl setImagesWithName:imageName];
[self.layer addSublayer:ssbl];
self.sbl = ssbl;
[ssbl release];
}
- (void) dealloc {
self.sbl = nil;
[super dealloc];
}
- (void)setPercentageWithFloat:(CGFloat)perc {
if(perc > 1.0){
perc = 1.0;
} else if(perc < 0) {
perc = 0;
}
[self.sbl setPercentage:perc];
CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:#"percentage"];
ba.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
ba.duration = 0.8;
ba.toValue = [NSNumber numberWithFloat:perc];
[self.sbl addAnimation:ba forKey:nil];
}
#end
The View can be configured to work with different images (using the function setImagesWithName:) using different names (theName). Within this method the View adds the ScrollBarLayer to its layer property.
The Layer:
#import "ScrollBarLayer.h"
#implementation ScrollBarLayer
#synthesize filename;
#dynamic percentage;
- (id)init {
self = [super init];
if (self) {
}
return self;
}
- (void)setImagesWithName:(NSString*)imageName {
self.frame = CGRectMake(0, 0, 30, 30);
self.percentage = 0;
self.filename = imageName;
}
+ (BOOL) needsDisplayForKey:(NSString*)key {
if([key isEqualToString:#"percentage"]) {
return YES;
}
return [super needsDisplayForKey:key];
}
- (void) drawInContext:(CGContextRef)ctx {
int imageIdx = (int)roundf((float)199 * self.percentage);
NSString *thisfilename = [self.filename stringByAppendingString:[NSString stringWithFormat:#"%03d.png", i+1]];
UIImage* c = [UIImage imageNamed:thisfilename];
CGImageRef img = [c CGImage];
CGSize sz = CGSizeMake(CGImageGetWidth(img), CGImageGetHeight(img));
UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width, sz.height), NO, 0.0);
CGContextDrawImage(ctx, CGRectMake(0, 0, sz.width, sz.height), img);
UIGraphicsEndImageContext();
}
- (void) dealloc {
self.pictures = nil;
self.filename = nil;
[super dealloc];
}
#end
I also would need light on this.
I think that the issue is between points and pixels... I think that you should check if the device has a retina display, and if so, you should replace the CGContextDrawImage(...) CGRect (in second parameter) for one with half width and height.
#define IS_RETINA_DISPLAY() ([[UIScreen mainScreen] respondsToSelector:#selector(scale)] == YES && [[UIScreen mainScreen] scale] == 2.00)
...
UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width, sz.height), NO, 0.0);
if (IS_RETINA_DISPLAY())
{
CGContextDrawImage(ctx, CGRectMake(0, 0, sz.width/2.0f, sz.height/2.0f), img);
} else
{
CGContextDrawImage(ctx, CGRectMake(0, 0, sz.width, sz.height), img);
}
UIGraphicsEndImageContext();
...
But I'm not sure this gill give you a "smooth" result...