I'm working on an app where the main view fills up the whole screen. This main view contains subviews that you could think of as sprites. They wander around using their own code. The logic of how they wander around could have them wandering off screen. What I'd like to do is to resize the main view so that it encloses all the subviews. It's really not the view size that is changing, though. It is the scaling that is changing. Kind of like zooming out of a google maps view. How do I do that? It will be a gesture recognizer that triggers this.
You should do this using a three-level view hierarchy:
top-level view
scaled view
sprite view 0
sprite view 1
sprite view 2
etc.
Set the bounds of the scaled view to encompass the frames of all of the sprite views. Then set the transform of the scaled view so that it fits in the top-level view.
- (void)updateBoundsAndTransformOfView:(UIView *)scaledView {
CGRect scaledBounds = CGRectNull;
for (UIView *spriteView in scaledView.subviews) {
scaledBounds = CGRectUnion(scaledBounds, spriteView.frame);
}
scaledView.bounds = scaledBounds;
// Now scaledView's size is large enough to contain all of its subviews,
// and scaledView's coordinate system origin is at the left edge of the
// leftmost sprite and the top edge of the topmost sprite.
UIView *topLevelView = scaledView.superview;
CGRect topLevelBounds = topLevelView.bounds;
CGFloat xScale =topLevelBounds.size.width / scaledBounds.size.width;
CGFloat yScale = topLevelBounds.size.height / scaledBounds.size.height;
CGFloat scale = MIN(xScale, yScale);
scaledView.transform = CGAffineTransformMakeScale(scale, scale);
scaledView.center = CGPointMake(CGRectGetMidX(topLevelBounds), CGRectGetMidY(topLevelBounds));
}
Related
I'm using UIKit Dynamics to push a UIView off screen, similar to how Tweetbot performs it in their image overlay.
I use a UIPanGestureRecognizer, and when they end the gesture, if they exceed the velocity threshold it goes offscreen.
[self.animator removeBehavior:self.panAttachmentBehavior];
CGPoint velocity = [panGestureRecognizer velocityInView:self.view];
if (fabs(velocity.y) > 100) {
self.pushBehavior = [[UIPushBehavior alloc] initWithItems:#[self.scrollView] mode:UIPushBehaviorModeInstantaneous];
[self.pushBehavior setTargetOffsetFromCenter:centerOffset forItem:self.scrollView];
self.pushBehavior.active = YES;
self.pushBehavior.action = ^{
CGPoint lowestPoint = CGPointMake(CGRectGetMinX(self.imageView.bounds), CGRectGetMaxY(self.imageView.bounds));
CGPoint convertedPoint = [self.imageView convertPoint:lowestPoint toView:self.view];
if (!CGRectIntersectsRect(self.view.bounds, self.imageView.frame)) {
NSLog(#"outside");
}
};
CGFloat area = CGRectGetWidth(self.scrollView.bounds) * CGRectGetHeight(self.scrollView.bounds);
CGFloat UIKitNewtonScaling = 5000000.0;
CGFloat scaling = area / UIKitNewtonScaling;
CGVector pushDirection = CGVectorMake(velocity.x * scaling, velocity.y * scaling);
self.pushBehavior.pushDirection = pushDirection;
[self.animator addBehavior:self.pushBehavior];
}
I'm having an immense amount of trouble detecting when my view actually completely disappears from the screen.
My view is setup rather simply. It's a UIScrollView with a UIImageView within it. Both are just within a UIViewController. I move the UIScrollView with the pan gesture, but want to detect when the image view is off screen.
In the action block I can monitor the view as it moves, and I've tried two methods:
1. Each time the action block is called, find the lowest point in y for the image view. Convert that to the view controller's reference point, and I was just trying to see when the y value of the converted point was less than 0 (negative) for when I "threw" the view upward. (This means the lowest point in the view has crossed into negative y values for the view controller's reference point, which is above the visible area of the view controller.)
This worked okay, except the x value I gave to lowestPoint really messes everything up. If I choose the minimum X, that is the furthest to the left, it will only tell me when the bottom left corner of the UIView has gone off screen. Often times as the view can be rotating depending on where the user pushes from, the bottom right may go off screen after the left, making it detect it too early. If I choose the middle X, it will only tell me when the middle bottom has gone off, etc. I can't seem to figure out how to tell it "just get me the absolute lowest y value.
2. I tried CGRectIntersectsRect as shown in the code above, and it never says it's outside, even seconds after it went shooting outside of any visible area.
What am I doing wrong? How should I be detecting it no longer being visible?
If you take a look on UIDynamicItem protocol properties, you can see they are center, bounds and transform. So UIDynamicAnimator actually modifies only these three properties. I'm not really sure what happens with the frame during the Dynamics animations, but from my experience I can tell it's value inside the action block is not always reliable. Maybe it's because the frame is actually being calculated by CALayer based on center, transform and bounds, as described in this excellent blog post.
But you for sure can make use of center and bounds in the action block. The following code worked for me in a case similar to yours:
CGPoint parentCenter = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
self.pushBehavior.action = ^{
CGFloat dx = self.imageView.center.x - parentCenter.x;
CGFloat dy = self.imageView.center.y - parentCenter.y;
CGFloat distance = sqrtf(dx * dx + dy * dy);
if(distance > MIN(parentCenter.y + CGRectGetHeight(self.imageView.bounds), parentCenter.x + CGRectGetWidth(self.imageView.bounds))) {
NSLog(#"Off screen!");
}
};
I want to rotate a subview with the same angle, when its parent view is rotated. How do I achieve this ?
- (void)rotationDetected:(UIRotationGestureRecognizer *)rotationRecognizer {
rotationRecognizer.rotation = M_PI_4;
CGFloat angle = rotationRecognizer.rotation;
duplicateView.transform = CGAffineTransformRotate(duplicateView.transform, angle);
userView.transform = duplicateView.transform;
rotationRecognizer.rotation = 0.0;
}
If you rotate a UIView's parent view the sub view will also rotate in the same way. You do not have to write any special code to make that happen.
I have UI which hast two states of layouts (besides portrait-landscape). In each state some other part of UI is exposed (is larger).
Problem is that UIScrollView with some UIImageView is resized on those states changes, and in each state different part of image is shown since scale and offset remains unchanged.
Is there some nice way to update this scale and offset values so more or less same part of image is shown for a large and small sized UIScrollView?
How about the UIScrollView method
- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated
Rect is a rectangle in the coordinate space of the view returned by viewForZoomingInScrollView:.
Determine what rectangle of the zoomed view is being shown.
Change your views so that the UIScrollView bounds are changed.
Do the zoomToRect to show the same content, scaled as necessary.
Without having compiled and run this, it should be approximately...
CGSize rectSize;
rectSize.origin = scrollview.contentOffset;
rectSize.width = scrollview.bounds.size.width * scrollview.zoomScale;
rectSize.height = scrollview.bounds.size.height * scrollview.zoomScale;
// Do whatever makes the views change
[scrollView zoomToRect:rectSize animated:whateverYouLike];
Problem: I have a pinch gesture recognizer on a View Controller which I'm using to scale an image nested inside the View Controller. The transform below works fine, except that the image is scaled from the upper left instead of the center. I want it to scale from the center.
Setup:
a UIImageView set to Aspect Fill mode (nested within a few views, origin set to center).
a UIPinchGestureRecognizer on the container View Controller
I verified:
anchorPoint for image view is (0.5, 0.5)
the center is moving after every transform
no auto layout constraints on the view or its parent (at least at build time)
Also, I tried setting center of the UIImageView after the transform, the change doesn't take effect until after the user is done pinching.
I don't want to center the image on the touch because the image is smaller than the view controller.
CGFloat _lastScale = 1.0;
- (IBAction)pinch:(UIPinchGestureRecognizer *)sender {
if ([sender state] == UIGestureRecognizerStateBegan) {
_lastScale = 1.0;
}
CGFloat scale = 1.0 - (_lastScale - [sender scale]);
_lastScale = [sender scale];
CGAffineTransform currentTransform = self.imageView.transform;
CGAffineTransform newTransform = CGAffineTransformScale(currentTransform, scale, scale);
[self.imageView setTransform:newTransform];
NSLog(#"center: %#", NSStringFromCGPoint(self.imageView.center));
}
Here's a complete project demonstrating the issue.
https://github.com/adamloving/PinchDemo
no auto layout constraints on the view or its parent (at least at build time)
If your nib / storyboard uses auto layout, then there are certainly auto layout constraints, even if you did not deliberately construct them. And let's face it, auto layout does not play nicely with view transforms. A scale transform should scale from the center, but auto layout is fighting against you, forcing the frame to be reset by its top and its left (because that is where the constraints are).
See my essay on this topic here:
How do I adjust the anchor point of a CALayer, when Auto Layout is being used?
See also the discussion of this problem in my book.
In your case, the simplest solution is probably to apply the transform to the view's layer rather than to the view itself. In other words, instead of saying this sort of thing:
self.v.transform = CGAffineTransformMakeScale(1.3, 1.3);
Say this sort of thing:
self.v.layer.transform = CATransform3DMakeScale(1.3, 1.3, 1);
In picture 1, I have a UIView called parentView1 (it is the gray color view). It has subviews (4 in the picture).
Before I applied CGAffineTransformRotate to parentView1, I can drag a view (the red vertical) and drop into parentView1 and the result is as picture 2.
However, if I first do a CGAffineTransformRotate (see code) on parentView1, then drag and drop to add the red vertical view into the parentView1, the red vertical view also gets rotated to the same as the parentView1 (picture 3) immediately as I dropped the vertical view. I do not want it to happen. I need the red vertical view to rest in the same orientation from its original before it was dragged away.
The same effect also happens the other way around: move a subview from parentView1 to outside it boundary if parentView1 has been rotated.
Any suggestion?
Code to rotate parentView1:
self.parentView1.transform = CGAffineTransformRotate(self.parentView1.transform, degreesToRadians(45));
Code to move a view from a parent to another:
-(void)getNewPosition2:(UIView *)viewToConvert parentView:(UIView *)parentViewx superParentView:(UIView *)superParentView
{
CGPoint newPoint = [superParentView convertPoint:viewToConvert.center fromView:parentViewx];
// viewToConvert.layer.position = newPoint;
viewToConvert.center = newPoint;
[viewToConvert removeFromSuperview];
[superParentView addSubview:viewToConvert];
}
Calling the getNewPostition2 method above:
[self getNewPosition2:recognizer.view parentView:self.view superParentView:self.parentView1];
I came up with a solution by using the parentView1's transform matrix to calculate angle for both cases: moving object in and out of parentView1.
For moving object into parentView1:
float angle = (2 * M_PI) - atanf(self.parentView1.transform.b / self.parentView1.transform.a );
viewToConvert.transform = CGAffineTransformRotate(viewToConvert.transform, angle);
For moving object out of parentView1:
float angle = atanf(self.parentView1.transform.b / self.parentView1.transform.a);
viewToConvert.transform = CGAffineTransformRotate(viewToConvert.transform, angle);