Zoom a rotated image inside scroll view to fit (fi

2019-03-14 00:49发布

问题:

Through this question and answer I've now got a working means of detecting when an arbitrarily rotated image isn't completely outside a cropping rect.

The next step is to figure out how to correctly adjust it's containing scroll view zoom to ensure that there are no empty spaces inside the cropping rect. To clarify, I want to enlarge (zoom in) the image; the crop rect should remain un-transformed.

The layout hierarchy looks like this:

containing UIScrollView
    UIImageView (this gets arbitrarily rotated)
        crop rect overlay view

... where the UIImageView can also be zoomed and panned inside the scrollView.

There are 4 gesture events that occur that need to be accounted for:

  1. Pan gesture (done): accomplished by detecting if it's been panned incorrectly and resets the contentOffset.
  2. Rotation CGAffineTransform
  3. Scroll view zoom
  4. Adjustment of the cropping rect overlay frame

As far as I can tell, I should be able to use the same logic for 2, 3, and 4 to adjust the zoomScale of the scroll view to make the image fit properly.

How do I properly calculate the zoom ratio necessary to make the rotated image fit perfectly inside the crop rect?

To better illustrate what I'm trying to accomplish, here's an example of the incorrect size:

I need to calculate the zoom ratio necessary to make it look like this:

Here's the code I've got so far using Oluseyi's solution below. It works when the rotation angle is minor (e.g. less than 1 radian), but anything over that and it goes really wonky.

CGRect visibleRect = [_scrollView convertRect:_scrollView.bounds toView:_imageView];
CGRect cropRect = _cropRectView.frame;

CGFloat rotationAngle = fabs(self.rotationAngle);

CGFloat a = visibleRect.size.height * sinf(rotationAngle);
CGFloat b = visibleRect.size.width * cosf(rotationAngle);
CGFloat c = visibleRect.size.height * cosf(rotationAngle);
CGFloat d = visibleRect.size.width * sinf(rotationAngle);

CGFloat zoomDiff = MAX(cropRect.size.width / (a + b), cropRect.size.height / (c + d));
CGFloat newZoomScale = (zoomDiff > 1) ? zoomDiff : 1.0 / zoomDiff;

[UIView animateWithDuration:0.2
                      delay:0.05
                    options:NO
                 animations:^{
                         [self centerToCropRect:[self convertRect:cropRect toView:self.zoomingView]];
                         _scrollView.zoomScale = _scrollView.zoomScale * newZoomScale;
                 } completion:^(BOOL finished) {
                     if (![self rotatedView:_imageView containsViewCompletely:_cropRectView]) 
                     {
                         // Damn, it's still broken - this happens a lot
                     }
                     else
                     {
                         // Woo! Fixed
                     }
                     _didDetectBadRotation = NO;
                 }];

Note I'm using AutoLayout which makes frames and bounds goofy.

回答1:

Assume your image rectangle (blue in the diagram) and crop rectangle (red) have the same aspect ratio and center. When rotated, the image rectangle now has a bounding rectangle (green) which is what you want your crop scaled to (effectively, by scaling down the image).

To scale effectively, you need to know the dimensions of the new bounding rectangle and use a scale factor that fits the crop rect into it. The dimensions of the bounding rectangle are rather obviously

(a + b) x (c + d)

Notice that each segment a, b, c, d is either the adjacent or opposite side of a right triangle formed by the bounding rect and the rotated image rect.

a = image_rect_height * sin(rotation_angle)
b = image_rect_width * cos(rotation_angle)
c = image_rect_width * sin(rotation_angle)
d = image_rect_height * cos(rotation_angle)

Your scale factor is simply

MAX(crop_rect_width / (a + b), crop_rect_height / (c + d))

Here's a reference diagram:



回答2:

Fill frame of overlay rect:

For a square crop you need to know new bounds of the rotated image which will fill the crop view.

Let's take a look at the reference diagram: You need to find the altitude of a right triangle (the image number 2). Both altitudes are equal.

CGFloat sinAlpha = sin(alpha);
CGFloat cosAlpha = cos(alpha);
CGFloat hypotenuse = /* calculate */;
CGFloat altitude = hypotenuse * sinAlpha * cosAlpha;

Then you need to calculate the new width for the rotated image and the desired scale factor as follows:

 CGFloat newWidth = previousWidth + altitude * 2;
 CGFloat scale = newWidth / previousWidth;

I have implemented this method here.



回答3:

I will answer using sample code, but basically this problem becomes really easy, if you will think in rotated view coordinate system.

UIView* container = [[UIView alloc] initWithFrame:CGRectMake(80, 200, 100, 100)];
container.backgroundColor = [UIColor blueColor];

UIView* content2 = [[UIView alloc] initWithFrame:CGRectMake(-50, -50, 150, 150)];
content2.backgroundColor = [[UIColor greenColor] colorWithAlphaComponent:0.5];
[container addSubview:content2];

[self.view setBackgroundColor:[UIColor blackColor]];
[self.view addSubview:container];

[container.layer setSublayerTransform:CATransform3DMakeRotation(M_PI / 8.0, 0, 0, 1)];

//And now the calculations
CGRect containerFrameInContentCoordinates = [content2 convertRect:container.bounds fromView:container];
CGRect unionBounds = CGRectUnion(content2.bounds, containerFrameInContentCoordinates);

CGFloat midX = CGRectGetMidX(content2.bounds);
CGFloat midY = CGRectGetMidY(content2.bounds);

CGFloat scaleX1 = (-1 * CGRectGetMinX(unionBounds) + midX) / midX;
CGFloat scaleX2 = (CGRectGetMaxX(unionBounds) - midX) / midX;
CGFloat scaleY1 = (-1 * CGRectGetMinY(unionBounds) + midY) / midY;
CGFloat scaleY2 = (CGRectGetMaxY(unionBounds) - midY) / midY;

CGFloat scaleX = MAX(scaleX1, scaleX2);
CGFloat scaleY = MAX(scaleY1, scaleY2);
CGFloat scale = MAX(scaleX, scaleY);

content2.transform = CGAffineTransformScale(content2.transform, scale, scale);