I've spent a lot of time trying to find a way to use CGAffineScale to transform a view to a given point, including messing around with anchor points, moving the centre of a view before and after transforming and comprehensive Googling. I am aware this would be a lot simpler with a UIScrollview; but I know it's technically possible to do without one, and it's become a splinter in my mind.
This answer gets remarkably close to what I want to achieve, but the answer only gives details on how to zoom to a given corner (instead of a given point) by cleverly moving the centre to the corner opposite the one you want to zoom in to.
How can I modify mvds' code to scale a UIView to any given point in a UIView?
CGFloat s = 3;
CGAffineTransform tr = CGAffineTransformScale(self.view.transform, s, s);
CGFloat h = self.view.frame.size.height;
CGFloat w = self.view.frame.size.width;
[UIView animateWithDuration:2.5 delay:0 options:0 animations:^{
self.view.transform = tr;
self.view.center = CGPointMake(w-w*s/2,h*s/2);
} completion:^(BOOL finished) {}];
There are 2 steps involved: First you scale up the view you want to zoom in to. Then you set the center of this blown up view such that the part you want to see ends up in the middle of the view.
You should draw this out on paper and the formulas will follow: (untested)
CGFloat s = 3;
CGPoint p = CGPointMake(100, 200);
CGAffineTransform tr = CGAffineTransformScale(self.view.transform, s, s);
CGFloat h = self.view.frame.size.height;
CGFloat w = self.view.frame.size.width;
[UIView animateWithDuration:2.5 delay:0 options:0 animations:^{
self.view.transform = tr;
CGFloat cx = w/2-s*(p.x-w/2);
CGFloat cy = h/2-s*(p.y-h/2);
self.view.center = CGPointMake(cx, cy); //was: (w*s/2,h-h*s/2);
} completion:^(BOOL finished) {}];
I actually ran into this very same problem myself. To fix it, all I did was change the anchor point of the view I was scaling because CGAffineTransforms are performed on the view in relation to its anchor point, so depending on where the anchor point is, the transform will scale, translate, or rotate the view differently. Here's the basic idea:
CGPoint pointToScaleTo = CGPointMake(x, y); //Just enter the coordinates you
//want to scale your view towards
CGFloat viewWidth = self.view.bounds.size.width;
CGFloat viewHeight = self.view.bounds.size.height;
CGFloat scaleFactorX = ...;
CGFloat scaleFactorY = ...;
CGAffineTransform scaleTransform = CGAffineTransformMakeScale(scaleFactorX, scaleFactorY);
[UIView animateWithDuration:2.5f delay:0.0f options:0 animations:^{
//I divide the x and y coordinates by the view width and height
//because the anchor point coordinates are normalized to the range
//0.0-1.0.
self.view.layer.anchorPoint = CGPointMake(pointToScaleTo.x/viewWidth, pointToScaleTo.y/viewHeight);
//Now that the anchor point has been changed, apply the scale transform
self.view.layer.transform = scaleTransform;
} completion:^(BOOL finished) {}];
Related
I have drawn a view(black rectangle) straight to the x-axis then I am applying some rotation by using CGAffineTransformMakeRotation.Please see image 1 after rotation.
After this I need to reduce the width from that rotated position.
For that I am using CGAffineTransformScale but image is not scaling down along the slope.Please see image 2.
If I change width from bound property then it works fine.
Any idea why CGAffineTransformScale behaves like that.
Below is my sample code.
**__block CGAffineTransform rotationTransform;
rotationTransform = CGAffineTransformMakeRotation(angle);
lineview.transform = rotationTransform;
[UIView animateWithDuration:10 animations:^{
CGAffineTransform finalTransform = CGAffineTransformScale(lineview.transform, 0.4, 1);
lineview.transform = finalTransform;
} completion:^(BOOL finished) {
}];
Try this to move the frame and change the width :)
VIEW.frame = CGRectMake(0,39, 320, 111);
This is the format
CGRectMake(CGFloat x, CGFloat y, CGFloat width, CGFloat height)
I need two animations on a UIView:
Make the view move down and slightly grow.
Make the view grow even bigger about its new center.
When I attempt to do that, the second animation starts in a weird location but ends up in the right location and size. How would I make the second animation start at the same position that the first animation ended in?
#import "ViewController.h"
static const CGFloat kStartX = 100.0;
static const CGFloat kStartY = 20.0;
static const CGFloat kStartSize = 30.0;
static const CGFloat kEndCenterY = 200.0;
#interface ViewController ()
#property (nonatomic, strong) UIView *box;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.box = [[UIView alloc] initWithFrame:CGRectMake(kStartX, kStartY, kStartSize, kStartSize)];
self.box.backgroundColor = [UIColor brownColor];
[self.view addSubview:self.box];
[UIView animateWithDuration:2.0
delay:1.0
usingSpringWithDamping:1.0
initialSpringVelocity:0.0
options:0
animations:^{
self.box.transform = [self _transformForSize:50.0 centerY:kEndCenterY];
}
completion:^(BOOL finished) {
[UIView animateWithDuration:2.0
delay:1.0
usingSpringWithDamping:1.0
initialSpringVelocity:0.0
options:0
animations:^{
self.box.transform = [self _transformForSize:100.0 centerY:kEndCenterY];
}
completion:^(BOOL finished) {
}];
}];
}
- (CGAffineTransform)_transformForSize:(CGFloat)newSize centerY:(CGFloat)newCenterY
{
CGFloat newScale = newSize / kStartSize;
CGFloat startCenterY = kStartY + kStartSize / 2.0;
CGFloat deltaY = newCenterY - startCenterY;
CGAffineTransform translation = CGAffineTransformMakeTranslation(0.0, deltaY);
CGAffineTransform scaling = CGAffineTransformMakeScale(newScale, newScale);
return CGAffineTransformConcat(scaling, translation);
}
#end
There's one caveat: I'm forced to use setTransform rather than setFrame. I'm not using a brown box in my real code. My real code is using a complex UIView subclass that doesn't scale smoothly when I use setFrame.
This looks like it might be a UIKit bug with how UIViews resolve their layout when you apply a transform on top of an existing one. I was able to at least get the starting coordinates for the second animation correct by doing the following, at the very beginning of the second completion block:
[UIView animateWithDuration:2.0
delay:1.0
usingSpringWithDamping:1.0
initialSpringVelocity:0.0
options:0
animations:^{
self.box.transform = [self _transformForSize:50.0 centerY:kEndCenterY];
}
completion:^(BOOL finished) {
// <new code here>
CGRect newFrame = self.box.frame;
self.box.transform = CGAffineTransformIdentity;
self.box.frame = newFrame;
// </new code>
[UIView animateWithDuration:2.0
delay:1.0
usingSpringWithDamping:1.0
initialSpringVelocity:0.0
options:0
animations:^{
self.box.transform = [self _transformForSize:100.0 centerY:kEndCenterY];
}
completion:^(BOOL finished) {
}];
}];
Using the same call to -_transformForSize:centerY: results in the same Y translation being performed in the second animation, though, so the box ends up further down in the view than you want when all is said and done.
To fix this, you need to calculate deltaY based on the box's starting Y coordinate at the end of the first animation rather than the original, constant Y coordinate:
- (CGAffineTransform)_transformForSize:(CGFloat)newSize centerY:(CGFloat)newCenterY
{
CGFloat newScale = newSize / kStartSize;
// Replace this line:
CGFloat startCenterY = kStartY + kStartSize / 2.0;
// With this one:
CGFloat startCenterY = self.box.frame.origin.y + self.box.frame.size.height / 2.0;
// </replace>
CGFloat deltaY = newCenterY - startCenterY;
CGAffineTransform translation = CGAffineTransformMakeTranslation(0.0, deltaY);
CGAffineTransform scaling = CGAffineTransformMakeScale(newScale, newScale);
return CGAffineTransformConcat(scaling, translation);
}
and it should do the trick.
UPDATE
I should say, I consider this "trick" more of a work-around than an actual solution, but the box's frame and transform look correct at each stage of the animation pipeline, so if there's a true "solution" it's eluding me at the moment. This trick at least solves the translation problem for you, so you can experiment with your more complex view hierarchy and see how far you get.
Ive done a Scaling animation for background image with an arrow in it,well by scaling the image(it includes the arrow) meaning its a whole background. it scales from the center of the background. but i want to start scaling from the center of arrow.
i want to achieve this :
Then to become with scale:
What i have to change in my code:
self.blueBackground.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1);
[UIView animateWithDuration:5 animations:^{
self.blueBackground.transform = CGAffineTransformScale(CGAffineTransformIdentity, 30, 30);
} completion:^(BOOL finished) {
}];
With my current code the scale start from center of background so the arrow end up at the right (out the bounds of screen).
Try something like this:
- (void)animateArrow
{
self.blueBackground.transform = CGAffineTransformIdentity;
[UIView animateWithDuration:5 animations:^{
ScaleViewAboutPointInBounds(self.blueBackground, arrowCenter, 30);
} completion:^(BOOL finished) {
}];
}
static void ScaleViewAboutPointInBounds(UIView *view, CGPoint point, CGFloat scaleAmount)
{
CGFloat xAnchor = view.layer.anchorPoint.x * CGRectGetWidth(view.bounds);
CGFloat yAnchor = view.layer.anchorPoint.y * CGRectGetHeight(view.bounds);
CGFloat xOffset = point.x - xAnchor;
CGFloat yOffset = point.y - yAnchor;
CGAffineTransform shift = CGAffineTransformMakeTranslation(-xOffset, -yOffset);
CGAffineTransform unshift = CGAffineTransformMakeTranslation(xOffset, yOffset);
CGAffineTransform scale = CGAffineTransformMakeScale(scaleAmount, scaleAmount);
view.transform = CGAffineTransformConcat(shift, CGAffineTransformConcat(scale, unshift));
}
It's probably more general than what you need, and you could do it simpler, but this should work too. Just change the point to something that looks like the center of the arrow.
I want to perform some rotation animation. Right now I do it like that:
#define degreesToRadians(x) (M_PI * x / 180.0)
[self.crossButton setTransform:CGAffineTransformRotate(self.crossButton.transform, degreesToRadians(-rotationDegree))];
but when I pass for instance 360 degrees nothong happens. When I pass value above 180 it starts not wokring well. Do you know what im doing wrong?
Your problem is that 'setTransformation' works with matrix rotation. For this reason you will always get the shortest way to the end result. When you pass in a 360 degree rotation, your object will be the same after the transformation. For this reason, the transformation will just do nothing, since it is already where it is supposed to end up. For values between 180 and 360 degrees your rotation will be 'backwards', since again. The rotation uses the 'shortest' path to the end result.
You can try this code:
UIView* toRotate = VIEW_TO_ROTATE;
CGFloat degreesToRotate = DEGREES;
CGFloat animationTime = TOTAL_ANIMATION_TIME;
NSInteger intervals = ((int)degreesToRotate)/179.9;
CGFloat rest = degreesToRotate-(intervals*179.9);
CGFloat radInterval = degreesToRotate>=0?179.9:-179.9;
CGFloat radRest = (M_PI * rest / 180.0);
CGFloat intervalTime = (1-(radRest/M_PI/2))/intervals;
CGFloat restTime = (radRest/M_PI/2)/intervals;
[UIView animateKeyframesWithDuration:animationTime
delay:0.0f
options:UIViewKeyframeAnimationOptionCalculationModeLinear
animations:
^{
for (int i=0; i<intervals; i++) {
[UIView addKeyframeWithRelativeStartTime:intervalTime*i relativeDuration:intervalTime animations:^{
toRotate.transform = CGAffineTransformConcat(toRotate.transform, CGAffineTransformMakeRotation(radInterval));
}];
}
[UIView addKeyframeWithRelativeStartTime:intervalTime*intervals relativeDuration:restTime animations:^{
toRotate.transform = CGAffineTransformConcat(toRotate.transform, CGAffineTransformMakeRotation(radRest));
}];
} completion:^(BOOL finished) {
}];
Make sure to replace VIEW_TO_ROTATE,DEGREES and TOTAL_ANIMATION_TIME with the values you need!
I'm trying to scale my view like so:
CGRect endFrame = self.detailView.bounds;
[UIView animateWithDuration:0.25 animations:^{
self.descriptionButton.bounds = endFrame;
}completion:^(BOOL finished) {
self.containerView.alpha = 0.0;
self.detailView.alpha = 1.0;
}];
My detailView is the container view. My descriptionButton is in the upper left corner. I want to give a zoom in type effect by having the button take up the whole screen. However, by default Core Animation scales from the anchor point at its center.
I tried setting the anchor point to the lower right corner of the view's layer like this:
self.descriptionButton.layer.anchorPoint = CGPointMake(self.descriptionButton.bounds.size.width, self.descriptionButton.bounds.size.height);
CGRect endFrame = self.detailView.bounds;
[UIView animateWithDuration:0.25 animations:^{
self.descriptionButton.bounds = endFrame;
}completion:^(BOOL finished) {
self.containerView.alpha = 0.0;
self.detailView.alpha = 1.0;
}];
But when I do that, I don't see any animation at all. Any thoughts? Thanks.
You can just use a little math
CGRect endFrame = self.detailView.frame;
CGRect currentFrame = self.detailView.frame;
[UIView animateWithDuration:0.25 animations:^
{
self.descriptionButton.frame = CGRectMake(currentFrame.x - (endFrame.size.width - currentFrame.size.width), currentFrame.y - (endFrame.size.height - currentFrame.size.height), endFrame.size.width, endFrame.size.height);
}
completion:^(BOOL finished)
{
self.containerView.alpha = 0.0;
self.detailView.alpha = 1.0;
}];
The anchorPoint property is "normalized" to values from 0 to 1. 0.5, 0.5 is the center. 0,0 is top left. 1,1 is bottom right.
Your code is setting the anchorPoint property using pixel coordinates, which is wrong. I don't know what it would do, but it's wrong.