How to prevent "bounce" effect when a custom view redraws after zooming? - ios

Note to casual readers: Despite the title, this question has nothing to do with the UIScrollView properties bounces (scrolling related) or bouncesZoom.
I am using UIScrollView to add zooming to a custom view. The custom view uses sublayers to draw its content. Each sublayer is a CALayer instance that is added to the view's main layer with [CALayer addSublayer:]. Sublayers use CoreGraphics to render their content.
After each zoom completes, the custom view needs to redraw its content at the new zoom scale so that the content appears crisp and sharp again. I am currently trying to get the
approach to work that is shown in this SO question, i.e. I reset the scroll view's zoomScale property to 1.0 after each zoom operation, then I adjust the minimumZoomScale and maximumZoomScale properties so that the user cannot zoom in/out more than originally intended.
The content redrawing already works correctly (!), but what I am missing is a smooth GUI update so that the zoomed content is redrawn in place without appearing to move. With my current solution (code example follows at bottom of this question), I observe a kind of "bounce" effect: As soon as the zoom operation ends, the zoomed content briefly moves to a different location, then immediately moves back to its original location.
I am not entirely sure what the reason for the "bounce" effect is: Either there are two GUI update cycles (one for resetting zoomScale to 1.0, and another for setNeedsDisplay), or some sort of animation is taking place that makes both changes visible, one after the other.
My question is: How can I prevent the "bounce" effect described above?
UPDATE: The following is a minimal but complete code example that you can simply copy&paste to observe the effect that I am talking about.
Create a new Xcode project using the "Empty application" template.
Add the code below to AppDelegate.h and AppDelegate.m, respectively.
In the project's Link build phase, add a reference to QuartzCore.framework.
Stuff that goes into AppDelegate.h:
#import <UIKit/UIKit.h>
#class LayerView;
#interface AppDelegate : UIResponder <UIApplicationDelegate, UIScrollViewDelegate>
#property (nonatomic, retain) UIWindow* window;
#property (nonatomic, retain) LayerView* layerView;
#end
Stuff that goes into AppDelegate.m:
#import "AppDelegate.h"
#import <QuartzCore/QuartzCore.h>
#class LayerDelegate;
#interface LayerView : UIView
#property (nonatomic, retain) LayerDelegate* layerDelegate;
#end
#interface LayerDelegate : NSObject
#property(nonatomic, retain) CALayer* layer;
#property (nonatomic, assign) CGFloat zoomScale;
#end
static CGFloat kMinimumZoomScale = 1.0;
static CGFloat kMaximumZoomScale = 5.0;
#implementation AppDelegate
- (void) dealloc
{
self.window = nil;
self.layerView = nil;
[super dealloc];
}
- (BOOL) application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
[UIApplication sharedApplication].statusBarHidden = YES;
self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
self.window.backgroundColor = [UIColor whiteColor];
UIScrollView* scrollView = [[[UIScrollView alloc] initWithFrame:self.window.bounds] autorelease];
[self.window addSubview:scrollView];
scrollView.contentSize = scrollView.bounds.size;
scrollView.delegate = self;
scrollView.minimumZoomScale = kMinimumZoomScale;
scrollView.maximumZoomScale = kMaximumZoomScale;
scrollView.zoomScale = 1.0f;
scrollView.bouncesZoom = NO;
self.layerView = [[[LayerView alloc] initWithFrame:scrollView.bounds] autorelease];
[scrollView addSubview:self.layerView];
[self.window makeKeyAndVisible];
return YES;
}
- (UIView*) viewForZoomingInScrollView:(UIScrollView*)scrollView
{
return self.layerView;
}
- (void) scrollViewDidEndZooming:(UIScrollView*)scrollView withView:(UIView*)view atScale:(float)scale
{
CGPoint contentOffset = scrollView.contentOffset;
CGSize contentSize = scrollView.contentSize;
scrollView.maximumZoomScale = scrollView.maximumZoomScale / scale;
scrollView.minimumZoomScale = scrollView.minimumZoomScale / scale;
// Big change here: This resets the scroll view's contentSize and
// contentOffset, and also the LayerView's frame, bounds and transform
// properties
scrollView.zoomScale = 1.0f;
CGFloat newZoomScale = self.layerView.layerDelegate.zoomScale * scale;
self.layerView.layerDelegate.zoomScale = newZoomScale;
self.layerView.frame = CGRectMake(0, 0, contentSize.width, contentSize.height);
scrollView.contentSize = contentSize;
[scrollView setContentOffset:contentOffset animated:NO];
[self.layerView setNeedsDisplay];
}
#end
#implementation LayerView
- (id) initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.layerDelegate = [[[LayerDelegate alloc] init] autorelease];
[self.layer addSublayer:self.layerDelegate.layer];
// super's initWithFrame already invoked setNeedsDisplay, but we need to
// repeat because at that time our layerDelegate property was still empty
[self setNeedsDisplay];
}
return self;
}
- (void) dealloc
{
self.layerDelegate = nil;
[super dealloc];
}
- (void) setNeedsDisplay
{
[super setNeedsDisplay];
// Zooming changes the view's frame, but not the frame of the layer
self.layerDelegate.layer.frame = self.bounds;
[self.layerDelegate.layer setNeedsDisplay];
}
#end
#implementation LayerDelegate
- (id) init
{
self = [super init];
if (self)
{
self.layer = [CALayer layer];
self.layer.delegate = self;
self.zoomScale = 1.0f;
}
return self;
}
- (void) dealloc
{
self.layer = nil;
[super dealloc];
}
- (void) drawLayer:(CALayer*)layer inContext:(CGContextRef)context
{
CGRect layerRect = self.layer.bounds;
CGFloat radius = 25 * self.zoomScale;
CGFloat centerDistanceFromEdge = 5 * self.zoomScale + radius;
CGPoint topLeftCenter = CGPointMake(CGRectGetMinX(layerRect) + centerDistanceFromEdge,
CGRectGetMinY(layerRect) + centerDistanceFromEdge);
[self drawCircleWithCenter:topLeftCenter radius:radius fillColor:[UIColor redColor] inContext:context];
CGPoint layerCenter = CGPointMake(CGRectGetMidX(layerRect), CGRectGetMidY(layerRect));
[self drawCircleWithCenter:layerCenter radius:radius fillColor:[UIColor greenColor] inContext:context];
CGPoint bottomRightCenter = CGPointMake(CGRectGetMaxX(layerRect) - centerDistanceFromEdge,
CGRectGetMaxY(layerRect) - centerDistanceFromEdge);
[self drawCircleWithCenter:bottomRightCenter radius:radius fillColor:[UIColor blueColor] inContext:context];
}
- (void) drawCircleWithCenter:(CGPoint)center
radius:(CGFloat)radius
fillColor:(UIColor*)color
inContext:(CGContextRef)context
{
const int startRadius = [self radians:0];
const int endRadius = [self radians:360];
const int clockwise = 0;
CGContextAddArc(context, center.x, center.y, radius,
startRadius, endRadius, clockwise);
CGContextSetFillColorWithColor(context, color.CGColor);
CGContextFillPath(context);
}
- (double) radians:(double)degrees
{
return degrees * M_PI / 180;
}
#end

Based on your sample project, the key is that you're manipulating a CALayer directly. By default, setting CALayer properties, such as frame, cause animations. The suggestion to use [UIView setAnimationsEnabled:NO] was on the right track, but only affects UIView-based animations. If you do the CALayer equivalent, say in your setNeedsDisplay: method:
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.layerDelegate.layer.frame = self.bounds;
[CATransaction commit];
It prevents the implicit frame-changing animation and looks right to me. You can also disable these implicit animations via a CALayerDelegate method in your LayerDelegate class:
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
return (id)[NSNull null]; // NSNull means "don't do any implicit animations"
}
Original Suggestions:
Perhaps you are in an animation block without knowing it? Or, perhaps one of the methods you're calling is setting up an animation block? What if you [UIView setAnimationsEnabled:NO] before your code and re-enable them after?
If it's not an animation, then it's probably as you suspect; two view updates of some kind. (Perhaps one from the scroll view, and one from your code somehow?) Some runnable sample code would be great in that case.
(Out of curiosity, have you tried using CALayer's shouldRasterize and rasterizationScale rather than faking out the zoom level?)

In the X Code user interface builder there's a Bounce setting (it's under Scroll View).

Related

How to manage CALayer animations throughout a hierarchy

This is a follow-up question to How to synchronize CALayer and UIView animations up and down a complex hierarchy
Lets say I have a composite layer (Top) that is a subclass of CALayer and has any number of children. Top has 2 child layers within it. The first sublayer (A) should always be a fixed width - lets say 100 pixels wide. The second sublayer (B) should be the remainder of the size of Top. Both A and B should occupy the entire height of Top. This is pretty straightforward to code up in layoutSubviews.
Let's presume that Top has no knowledge of A or B. Also presume that Top has a delegate that controls when it should be animated (the delegate provides actionForLayer:forKey: and no other CALayer delegate functions).
I'd like to devise a strategy where for every possible size of Top, the user will always see A and B rendered according to the constraints listed above - even when the size of Top is being animated, even when it is being animated with any variety of animation parameters (durations, functions, offsets, etc).
Just as Top's animations are driven from some containing view or layer through its delegate - it seems that A and B should have their animations setup their containing layer - Top. I want to keep things well-composed, so I don't want the layout of A & B within Top to need to be understood by anything other than Top.
So - the question is what's the best strategy to chain the animations down the layer tree to keep all of the animation parameters in sync?
Here's some sample code that does chaining through the use of actionForLayer:forKey:, but middle function has to go through some fairly involved work (which isn't included) to translate all of the settings from its animation to the sublayer's animation. Not included in this sample is any code that deals with interpolating the values of the bounds. For example, imagine a case where an animation is setup to use a different fromValue, or a keyframe animation. Those values would need to be solved for the sublayers and applied accordingly.
#import "ViewController.h"
#interface MyTopLayer : CALayer
#end
static const CGFloat fixedWidth = 100.0;
#implementation MyTopLayer
-(instancetype)init {
self = [super init];
if (self) {
self.backgroundColor = [[UIColor redColor] CGColor];
CALayer *fixedLayer = [[CALayer alloc] init];
CALayer *slackLayer = [[CALayer alloc] init];
[self addSublayer:fixedLayer];
[self addSublayer:slackLayer];
fixedLayer.anchorPoint = CGPointMake(0,0);
fixedLayer.position = CGPointMake(0,0);
slackLayer.anchorPoint = CGPointMake(0,0);
slackLayer.position = CGPointMake(fixedWidth,0);
fixedLayer.backgroundColor = [[UIColor yellowColor] CGColor];
slackLayer.backgroundColor = [[UIColor purpleColor] CGColor];
//fixedLayer.delegate = self; // no reason to ever animate this layer since it is static
slackLayer.delegate = self;
}
return self;
}
-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if (![event isEqualToString:#"bounds"]) {
return nil;
}
CAAnimation *boundsAnim = [self animationForKey:#"bounds"];
NSLog(#"boundsAnim=%#", boundsAnim);
if (!boundsAnim) {
return (id<CAAction>)[NSNull null];
}
CAAnimation *sublayerBoundsAnim;
if ([boundsAnim isKindOfClass:[CABasicAnimation class]]) {
CABasicAnimation *subAnim = [CABasicAnimation animationWithKeyPath:#"bounds"];
// transform properties, like from, to & by value from boundsAnim (outer) to the inner layer's animation
sublayerBoundsAnim = subAnim;
} else {
CAKeyframeAnimation *subAnim = [CAKeyframeAnimation animationWithKeyPath:#"bounds"];
// copy/interpolate keyframes
sublayerBoundsAnim = subAnim;
}
sublayerBoundsAnim.timeOffset = boundsAnim.timeOffset;
sublayerBoundsAnim.duration = boundsAnim.duration;
sublayerBoundsAnim.timingFunction = boundsAnim.timingFunction;
return sublayerBoundsAnim;
}
-(void)layoutSublayers {
{
CALayer *fixedLayer = [self.sublayers firstObject];
CGRect b = self.bounds;
b.size.width = fixedWidth;
fixedLayer.bounds = b;
}
{
CALayer *slackLayer = [self.sublayers lastObject];
CGRect b = self.bounds;
b.size.width -= fixedWidth;
slackLayer.bounds = b;
}
}
#end
#interface MyView : UIView
#end
#implementation MyView
{
bool _shouldAnimate;
}
+(Class)layerClass {
return [MyTopLayer class];
}
-(instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.layer.delegate = self;
UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(doubleTapRecognizer:)];
doubleTapRecognizer.numberOfTapsRequired = 2;
[self addGestureRecognizer:doubleTapRecognizer];
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(tapRecognizer:)];
[tapRecognizer requireGestureRecognizerToFail:doubleTapRecognizer];
[self addGestureRecognizer:tapRecognizer];
}
return self;
}
CGFloat getRandWidth() {
const static int maxWidth=1024;
const static int minWidth=fixedWidth*1.1;
return minWidth+((((CGFloat)rand())/(CGFloat)RAND_MAX)*(maxWidth-minWidth));
}
-(void)tapRecognizer:(UITapGestureRecognizer*) gr {
_shouldAnimate = true;
CGFloat w = getRandWidth();
self.layer.bounds = CGRectMake(0,0,w,self.layer.bounds.size.height);
}
-(void)doubleTapRecognizer:(UITapGestureRecognizer*) gr {
_shouldAnimate = false;
CGFloat w = getRandWidth();
self.layer.bounds = CGRectMake(0,0,w,self.layer.bounds.size.height);
}
-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if (_shouldAnimate) {
if ([event isEqualToString:#"bounds"]) {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:event];
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anim.duration = 2.0;
//anim.timeOffset = 0.5;
anim.fromValue = [NSValue valueWithCGRect:CGRectMake(0,0,100,100)];
return anim;
} else {
return nil;
}
} else {
return (id<CAAction>)[NSNull null];
}
}
#end
My question is - does anybody have a better way to get this done? It seems a little bit scary that I've not seen any mention of this sort of hierarchical chaining anywhere. I'm aware that I would probably also need to do some more work on canceling sublayer animations when the top layer's animation is canceled. Relying simply on the currently attached animation, especially w/out concern for the current time that that function is in seems like it could be a source of errors somewhere down the line.
I'm also not sure how well this would perform in the wild since they aren't in the same animation group. Any thoughts there would be greatly appreciated.

Moving a frame half of the parent's size moves it all the way across (Coordinates doubled)?

I have no idea what is going on.
I am not doing anything special.
I have a UIViewController that I create programmatically, no storyboard.
Then I create a UIView with nothing special
#import "SettingsView.h"
#interface SettingsView()
#property UIImageView* bg;
#end
#implementation SettingsView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.bg = [[UIImageView alloc]initWithImage:[UIImage imageNamed:#"happy.png"]];
[self addSubview:self.bg];
}
return self;
}
-(void)layoutSubviews
{
self.bg.frame = self.frame;
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
}
*/
#end
I instantiate it in the VC and when I want to show it I do:
-(void)showSettingsView
{
self.settings.frame = self.view.frame;
self.btnInfo.userInteractionEnabled = NO;
self.btnPause.userInteractionEnabled = NO;
self.btnSettings.userInteractionEnabled = NO;
self.view.userInteractionEnabled = NO;
[self.view addSubview:self.settings];
CGRect frame = self.settings.frame;
frame.origin.x = frame.size.width * 0.48;
frame.origin.y = 0;
self.settings.frame = frame;
}
Instead of seeing the image view roughly half way on the screen, it starts at exactly double (0.48 * 2 = 96%) of the screen.
I cannot understand why coordinates are doubled or something?
When I check the frame, I clearly see the origin at around 150 which is half the width of the iPhone.
What could be happening?
Thanks
Could it be that in...
-(void)layoutSubviews
{
self.bg.frame = self.frame;
}
you should be using bounds instead of frame of your self object...
-(void)layoutSubviews
{
self.bg.frame = self.bounds;
}

IOS ScrollView Issues

Hey I'm following this tutorial and currently on the first step but am experiencing some weird behavior. The tutorial is on creating scroll views for images and ends with showing how to create a paged horizontal ScrollView with a page controller. I followed the first section and things seemed to be working fine. However when I ran the code it did not perform properly, after about an hour of debugging I downloaded the final project, ran it on my phone to check if it was working correctly (it was) then copied the code into the required files in my project. My project still wouldn't run. So I figured it must be the way in which my story board was configured on the project setup. I then oped the working example cleared their storyboard and created the views and segues my self, in the exact same way and order I had done in my project. Bam! it still worked, when I deleted all views in my story board and recreated them in the exact same fashion, still nothing. Could someone look into this tutorial and tell me if I'm going crazy or they experience similar results. I have narrowed it down to two possibilities, the project provided at the end of the tutorial was created differently and the delegates or outlets were hooked up in a way that only works with the project, or the project was created with an earlier version of the iPhone development kit that works with the current version of XCode but not when I create a new project. Then again I am fairly new to this so I will throw up the code I'm using and hope for the best.
ViewController.h file:
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController <UIScrollViewDelegate>
#property (nonatomic, strong) IBOutlet UIScrollView *scrollView;
#end
ViewController.m file:
#import "ViewController.h"
#interface ViewController ()
#property (nonatomic, strong) UIImageView *imageView;
- (void)centerScrollViewContents;
- (void)scrollViewDoubleTapped:(UITapGestureRecognizer*)recognizer;
- (void)scrollViewTwoFingerTapped:(UITapGestureRecognizer*)recognizer;
#end
#implementation ViewController
#synthesize scrollView = _scrollView;
#synthesize imageView = _imageView;
- (void)centerScrollViewContents {
CGSize boundsSize = self.scrollView.bounds.size;
CGRect contentsFrame = self.imageView.frame;
if (contentsFrame.size.width < boundsSize.width) {
contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2.0f;
} else {
contentsFrame.origin.x = 0.0f;
}
if (contentsFrame.size.height < boundsSize.height) {
contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2.0f;
} else {
contentsFrame.origin.y = 0.0f;
}
self.imageView.frame = contentsFrame;
}
- (void)scrollViewDoubleTapped:(UITapGestureRecognizer*)recognizer {
// Get the location within the image view where we tapped
CGPoint pointInView = [recognizer locationInView:self.imageView];
// Get a zoom scale that's zoomed in slightly, capped at the maximum zoom scale specified by the scroll view
CGFloat newZoomScale = self.scrollView.zoomScale * 1.5f;
newZoomScale = MIN(newZoomScale, self.scrollView.maximumZoomScale);
// Figure out the rect we want to zoom to, then zoom to it
CGSize scrollViewSize = self.scrollView.bounds.size;
CGFloat w = scrollViewSize.width / newZoomScale;
CGFloat h = scrollViewSize.height / newZoomScale;
CGFloat x = pointInView.x - (w / 2.0f);
CGFloat y = pointInView.y - (h / 2.0f);
CGRect rectToZoomTo = CGRectMake(x, y, w, h);
[self.scrollView zoomToRect:rectToZoomTo animated:YES];
}
- (void)scrollViewTwoFingerTapped:(UITapGestureRecognizer*)recognizer {
// Zoom out slightly, capping at the minimum zoom scale specified by the scroll view
CGFloat newZoomScale = self.scrollView.zoomScale / 1.5f;
newZoomScale = MAX(newZoomScale, self.scrollView.minimumZoomScale);
[self.scrollView setZoomScale:newZoomScale animated:YES];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Set a nice title
self.title = #"Image";
// Set up the image we want to scroll & zoom and add it to the scroll view
UIImage *image = [UIImage imageNamed:#"photo1.png"];
self.imageView = [[UIImageView alloc] initWithImage:image];
self.imageView.frame = (CGRect){.origin=CGPointMake(0.0f, 0.0f), .size=image.size};
[self.scrollView addSubview:self.imageView];
// Tell the scroll view the size of the contents
self.scrollView.contentSize = image.size;
UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(scrollViewDoubleTapped:)];
doubleTapRecognizer.numberOfTapsRequired = 2;
doubleTapRecognizer.numberOfTouchesRequired = 1;
[self.scrollView addGestureRecognizer:doubleTapRecognizer];
UITapGestureRecognizer *twoFingerTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(scrollViewTwoFingerTapped:)];
twoFingerTapRecognizer.numberOfTapsRequired = 1;
twoFingerTapRecognizer.numberOfTouchesRequired = 2;
[self.scrollView addGestureRecognizer:twoFingerTapRecognizer];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// Set up the minimum & maximum zoom scales
CGRect scrollViewFrame = self.scrollView.frame;
CGFloat scaleWidth = scrollViewFrame.size.width / self.scrollView.contentSize.width;
CGFloat scaleHeight = scrollViewFrame.size.height / self.scrollView.contentSize.height;
CGFloat minScale = MIN(scaleWidth, scaleHeight);
self.scrollView.minimumZoomScale = minScale;
self.scrollView.maximumZoomScale = 1.0f;
self.scrollView.zoomScale = minScale;
[self centerScrollViewContents];
}
- (void)viewDidUnload
{
[super viewDidUnload];
// Release any retained subviews of the main view.
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
- (UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
// Return the view that we want to zoom
return self.imageView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
// The scroll view has zoomed, so we need to re-center the contents
[self centerScrollViewContents];
}
#end
The above code doesn't work as it does in the tutorials project. The following UIScrollView delegate method viewForZoomingInScrollView produces this error:
2013-05-14 10:01:18.585 TestScrollView[3809:907] Made it
May 14 10:01:18 Scott-Laroses-iPhone TestScrollView[3809] : CGAffineTransformInvert: singular matrix.
May 14 10:01:18 Scott-Laroses-iPhone TestScrollView[3809] : CGAffineTransformInvert: singular matrix.
which makes me think the delate isn't properly set up despite me doing it exactly the same as in the tutorial and programmatically when that didn't work. Any ideas?
Your problem may be from the zoomScale and minimumZoomScale if the value assigned is 0. Make sure that the value is never 0 for those two properties.
I figured it out! The issues was that the auto layout constraints were messing up the off screen views. So I disabled auto layout and it worked fine.

Rotating rectangle around circumference of a circle (iOS)?

I am trying to rotate the rectangle around the circle. So far after putting together some code I found in various places (mainly here: https://stackoverflow.com/a/4657476/861181) , I am able to rotate rectangle around it's center axis.
How can I make it rotate around the circle?
Here is what I have:
OverlaySelectionView.h
#import <QuartzCore/QuartzCore.h>
#interface OverlaySelectionView : UIView {
#private
UIView* dragArea;
CGRect dragAreaBounds;
UIView* vectorArea;
UITouch *currentTouch;
CGPoint touchLocationpoint;
CGPoint PrevioustouchLocationpoint;
}
#property CGRect vectorBounds;
#end
OverlaySelectionView.m
#import "OverlaySelectionView.h"
#interface OverlaySelectionView()
#property (nonatomic, retain) UIView* vectorArea;
#end
#implementation OverlaySelectionView
#synthesize vectorArea, vectorBounds;
#synthesize delegate;
- (void) initialize {
self.userInteractionEnabled = YES;
self.multipleTouchEnabled = NO;
self.backgroundColor = [UIColor clearColor];
self.opaque = NO;
self.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(rotateVector:)];
panRecognizer.maximumNumberOfTouches = 1;
[self addGestureRecognizer:panRecognizer];
}
- (id) initWithCoder: (NSCoder*) coder {
self = [super initWithCoder: coder];
if (self != nil) {
[self initialize];
}
return self;
}
- (id) initWithFrame: (CGRect) frame {
self = [super initWithFrame: frame];
if (self != nil) {
[self initialize];
}
return self;
}
- (void)drawRect:(CGRect)rect {
if (vectorBounds.origin.x){
UIView* area = [[UIView alloc] initWithFrame: vectorBounds];
area.backgroundColor = [UIColor grayColor];
area.opaque = YES;
area.userInteractionEnabled = NO;
vectorArea = area;
[self addSubview: vectorArea];
}
}
- (void)rotateVector: (UIPanGestureRecognizer *)panRecognizer{
if (touchLocationpoint.x){
PrevioustouchLocationpoint = touchLocationpoint;
}
if ([panRecognizer numberOfTouches] >= 1){
touchLocationpoint = [panRecognizer locationOfTouch:0 inView:self];
}
CGPoint origin;
origin.x=240;
origin.y=160;
CGPoint previousDifference = [self vectorFromPoint:origin toPoint:PrevioustouchLocationpoint];
CGAffineTransform newTransform =CGAffineTransformScale(vectorArea.transform, 1, 1);
CGFloat previousRotation = atan2(previousDifference.y, previousDifference.x);
CGPoint currentDifference = [self vectorFromPoint:origin toPoint:touchLocationpoint];
CGFloat currentRotation = atan2(currentDifference.y, currentDifference.x);
CGFloat newAngle = currentRotation- previousRotation;
newTransform = CGAffineTransformRotate(newTransform, newAngle);
[self animateView:vectorArea toPosition:newTransform];
}
-(CGPoint)vectorFromPoint:(CGPoint)firstPoint toPoint:(CGPoint)secondPoint
{
CGPoint result;
CGFloat x = secondPoint.x-firstPoint.x;
CGFloat y = secondPoint.y-firstPoint.y;
result = CGPointMake(x, y);
return result;
}
-(void)animateView:(UIView *)theView toPosition:(CGAffineTransform) newTransform
{
[UIView setAnimationsEnabled:YES];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationDuration:0.0750];
vectorArea.transform = newTransform;
[UIView commitAnimations];
}
#end
here is attempt to clarify. I am creating the rectangle from a coordinates on a map. Here is the function that creates that rectangle in the main view. Essentially it is the middle of the screen:
overlay is the view created with the above code.
- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation
{
if (!circle){
circle = [MKCircle circleWithCenterCoordinate: userLocation.coordinate radius:100];
[mainMapView addOverlay:circle];
CGPoint centerPoint = [mapView convertCoordinate:userLocation.coordinate toPointToView:self.view];
CGPoint upPoint = CGPointMake(centerPoint.x, centerPoint.y - 100);
overlay = [[OverlaySelectionView alloc] initWithFrame: self.view.frame];
overlay.vectorBounds = CGRectMake(upPoint.x, upPoint.y, 30, 100);
[self.view addSubview: overlay];
}
}
Here is the sketch of what I am trying to achieve:
Introduction
A rotation is always done around (0,0).
What you already know:
To rotate around the center of the rectangle you translate the rect to origin, rotate and translate back.
Now for your question:
to rotate around a center point of a circle, simply move the center of the rectangle such that the circle is at (0,0) then rotate, and move back.
start positioning the rectangle at 12 o clock, with the center line at 12.
1) as explained you always rotate around 0,0, so move the center of the circle to 0,0
CGAffineTransform trans1 = CGAffineTransformTranslation(-circ.x, -circ.y);
2) rotate by angle
CGAffineTransform transRot = CGAffineTransformRotation(angle); // or -angle try out.
3) Move back
CGAffineTransform transBack = CGAffineTransformTranslation(circ.x, circ.y);
Concat these 3 rotation matrices to one combibed matrix, and apply it to the rectangle.
CGAffineTransformation tCombo = CGAffineTransformConcat(trans1, transRot);
tCombo = CGTransformationConcat(tCombo, transback);
Apply
rectangle.transform = tCombo;
You probably should also read the chapter about Transformation matrices in Quartz docu.
This code is written with a text editor only, so expect slighly different function names.

ios bar that changes colors while loading

I am currently working on a project of my own and I would like to implement a custom loading control.
It's pretty much what you can find in the app Amen. The colored bar appears only when the app is loading content from the server.
Any tips will be much appreciated. Thanks.
Here are some images to make it more clear
The "hard" part will be writing a UIView subclass that will handle drawing the colors. You'll want to override the drawRect: method and figure out how to know the progress (or will it just "auto-increment"?) and draw/fill in based on that. Then, you can simply add a UIView in Interface Builder, change the Class type of the view, size it appropriately and off you go!
The "easy" part is that when you want the view to not be visible, you can do one of a number of things:
Move the view off-screen by changing its frame property. (This can be "instantaneous" or animated.)
Set the view invisible by changing its hidden property. (You can animate this, too!)
Get rid of the view entirely by using [barView removeFromSuperview].
Update/Edit
For the actual drawing, try this (done quickly and not tested, so YMMV):
// ColorProgressBar.h
#import <UIKit/UIKit.h>
#interface ColorProgressBar : UIView {
float colorWidth;
float progressPercent;
NSMutableArray *colors;
}
#property (assign, nonatomic) float colorWidth;
#property (assign, nonatomic) float progressPercent;
#property (strong, nonatomic) NSMutableArray *colors;
#end
// ColorProgressBar.m
#import "ColorProgressBar.h"
#implementation ColorProgressBar
#synthesize colors;
#synthesize colorWidth;
#synthesize progressPercent;
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Make the color array to use (this is spelled out for clarity)
[self setColors:[NSMutableArray array]];
[[self colors] addObject:[UIColor redColor]];
[[self colors] addObject:[UIColor orangeColor]];
[[self colors] addObject:[UIColor yellowColor]];
[[self colors] addObject:[UIColor greenColor]];
[[self colors] addObject:[UIColor blueColor]];
[[self colors] addObject:[UIColor purpleColor]];
}
return self;
}
- (void)drawRect:(CGRect)rect {
CGFloat left = 0;
CGRect drawBox = CGRectZero;
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextClearRect(ctx, rect);
int colorIndex = -1;
// Draw each color block from left to right, switching to the next color each time
do {
colorIndex = colorIndex + 1;
if (colorIndex >= [[self colors] count]) colorIndex = 0;
[(UIColor *)[[self colors] objectAtIndex:colorIndex] setFill];
drawBox = CGRectMake(left, 0, [self colorWidth], rect.size.height);
CGContextFillRect(ctx, drawBox);
} while (left < rect.size.width);
// Figure out where the "faded/empty" part should start
left = [self progressPercent] * rect.size.width;
drawBox = CGRectMake(left, 0, rect.size.width - left, rect.size.height);
[[UIColor colorWithWhite:1.0 alpha:0.5] setFill];
CGContextFillRect(ctx, drawBox);
}
#end
With this code, you could use this UIView subclass and each time you wanted to update your progress, you would simply set your progressPercent (it's a float with a designed range from 0.00 to 1.00) and call [myView setNeedsDisplay]. That should be it! ;-)

Resources