应用UIView变换(CGAffineTransform)后查找框架坐标

36

我使用 CGAffineTransform 旋转我的视图

[view setTransform:newTransform];

应用变换后,框架的值保持不变,但我该如何找到这个框架的“旋转”或变换后的值?


(来源: informit.com)

我想要旋转框架边缘的精确坐标,也就是a、b、c、d点。


你不需要做任何三角函数计算。你应该使用 CGPointApplyAffineTransform()CGRectApplyAffineTransform(),正如 @LeoNatan @Bladebunny 等人所建议的那样。 - nielsbot
所有这些答案都很复杂而且冗长,没有必要。自iOS2以来,苹果提供了一种优化的方法,在一行代码中完成此操作。我鼓励您更新所选答案:https://dev59.com/jGIk5IYBdhLWcg3wKLWd#66096889 - Albert Renshaw
7个回答

55

需要记住的一件事是,变换会改变坐标系,因此您需要能够在父“视图”和子(经过变换的)视图之间进行转换。此外,变换会保留变换对象的中心,但不保留任何其他坐标。因此,您需要以中心为基础计算。您还需要使用几个辅助方法。(我大多数采用这个方法是从Erica Sadun的书《Core iOS Developer's Cookbook》中得来的。)

我通常将它们添加为UIView的一个类别。

为了将子视图的坐标转换为父视图的坐标,您需要类似以下代码:

// helper to get pre transform frame
-(CGRect)originalFrame {
   CGAffineTransform currentTransform = self.transform;
   self.transform = CGAffineTransformIdentity;
   CGRect originalFrame = self.frame;
   self.transform = currentTransform;

   return originalFrame;
}

// helper to get point offset from center
-(CGPoint)centerOffset:(CGPoint)thePoint {
    return CGPointMake(thePoint.x - self.center.x, thePoint.y - self.center.y);
}
// helper to get point back relative to center
-(CGPoint)pointRelativeToCenter:(CGPoint)thePoint {
  return CGPointMake(thePoint.x + self.center.x, thePoint.y + self.center.y);
}
// helper to get point relative to transformed coords
-(CGPoint)newPointInView:(CGPoint)thePoint {
    // get offset from center
    CGPoint offset = [self centerOffset:thePoint];
    // get transformed point
    CGPoint transformedPoint = CGPointApplyAffineTransform(offset, self.transform);
    // make relative to center
    return [self pointRelativeToCenter:transformedPoint];
}

// now get your corners
-(CGPoint)newTopLeft {
    CGRect frame = [self originalFrame];
    return [self newPointInView:frame.origin];
}
-(CGPoint)newTopRight {
    CGRect frame = [self originalFrame];
    CGPoint point = frame.origin;
    point.x += frame.size.width;
    return [self newPointInView:point];
}
-(CGPoint)newBottomLeft {
    CGRect frame = [self originalFrame];
    CGPoint point = frame.origin;
    point.y += frame.size.height;
    return [self newPointInView:point];
}
-(CGPoint)newBottomRight {
    CGRect frame = [self originalFrame];
    CGPoint point = frame.origin;
    point.x += frame.size.width;
    point.y += frame.size.height;
    return [self newPointInView:point];
}

Swift 4

extension UIView {
    /// Helper to get pre transform frame
    var originalFrame: CGRect {
        let currentTransform = transform
        transform = .identity
        let originalFrame = frame
        transform = currentTransform
        return originalFrame
    }

    /// Helper to get point offset from center
    func centerOffset(_ point: CGPoint) -> CGPoint {
        return CGPoint(x: point.x - center.x, y: point.y - center.y)
    }

    /// Helper to get point back relative to center
    func pointRelativeToCenter(_ point: CGPoint) -> CGPoint {
        return CGPoint(x: point.x + center.x, y: point.y + center.y)
    }

    /// Helper to get point relative to transformed coords
    func newPointInView(_ point: CGPoint) -> CGPoint {
        // get offset from center
        let offset = centerOffset(point)
        // get transformed point
        let transformedPoint = offset.applying(transform)
        // make relative to center
        return pointRelativeToCenter(transformedPoint)
    }

    var newTopLeft: CGPoint {
        return newPointInView(originalFrame.origin)
    }

    var newTopRight: CGPoint {
        var point = originalFrame.origin
        point.x += originalFrame.width
        return newPointInView(point)
    }

    var newBottomLeft: CGPoint {
        var point = originalFrame.origin
        point.y += originalFrame.height
        return newPointInView(point)
    }

    var newBottomRight: CGPoint {
        var point = originalFrame.origin
        point.x += originalFrame.width
        point.y += originalFrame.height
        return newPointInView(point)
    }
}

你能否尝试给出这个答案? https://dev59.com/upLea4cB1Zd3GeqP9Pg7 - Hardik Vyas
2
这是否意味着(例如)CGAffineTransformTranslate翻译的是对象的坐标系,而不是对象在其坐标系中的位置? - user5539357
@Bladebunny,你能告诉我如何编写代码来获取每个边的中心点吗?包括上、右、下、左。 - john afordis

19

通过使用基本三角函数,可以找到旋转视图的坐标。以下是您可以执行此操作的步骤:

enter image description here

  1. 第一步是知道视图的宽度和高度。将它们除以2,即可得到三角形的相邻边和对边(分别为青色和绿色)。在上面的示例中,宽度=300,高度=300。因此相邻边=150,对边=150。

  2. 查找斜边长度(红色)。为此,使用公式:h^2 = a^2 + b^2。应用此公式后,我们发现斜边长度=212.13。

  3. 查找角度θ。这是相邻边(青色)和斜边(红色)之间的夹角。为此,使用公式cos(theta) = (h^2 + a^2 - o^2)/2*h*o。应用此公式后,我们发现theta = 0.785(弧度)。要将其转换为度数,应使用公式degrees = radians * 180 / PI = 45(度)。这是斜边的初始(偏移)角度。这非常重要。如果您的视图旋转为零,则斜边具有45(度)的偏移角度。我们马上需要θ。

  4. 现在,我们知道了斜边(红色),还需要旋转角度。在此示例中,我使用了UIRotationGestureRecognizer类来旋转正方形视图。该类具有“rotation”属性,告诉我们视图旋转了多少。该值以弧度为单位。在上面的示例中,旋转角度为0.3597弧度。要将其转换为度数,应使用公式degrees = radians * PI / 180。应用该公式后,我们发现旋转角度为20.61度。

  5. 最终,我们可以找到偏移宽度(橙色)和高度(紫色)。对于宽度,我们使用公式width = cos(rotationAngle - theta) * hypotenuse,而对于高度,我们使用公式height = sen(rotationAngle - theta)。我们必须从旋转角度(以弧度为单位)中减去theta(也是以弧度为单位),因为theta是初始偏移量。可以这样看:当旋转角度为零时,斜边的角度为45 (度)。应用公式后,我们发现宽度=193.20,高度=-87.60。

  6. 最后,我们可以将这些值(宽度和高度)添加到正方形的中心,以找到蓝色点的坐标。

-示例-

// Get the center point
CGPoint squareCenter = self.squareView.center;

// Get the blue point coordinates
CGPoint bluePointCoordinates = CGPointMake(squareCenter.x + width, squareCenter.y + height);

蓝色点的坐标为 (963.20, 272.40)

为了更好地理解公式,请查看以下链接:

此外,如果您想尝试玩弄我创建的测试项目(就是图片中的那个),请随意从以下链接下载。

更新

这里提供了一种简明的方法来计算您正在寻找的右上角偏移点(蓝色)。

/* Params:
/  viewCenter:      The center point (in superView coordinates) of your view
/  width:           The total width of your view
/  height:          The total height of your view
/  angleOfRotation: The rotation angle of your view. Can be either DEGREES or RADIANS
/  degrees:         A boolean flag indicating whether 'angleOfRotation' is degrees
/                   or radians. E.g.: If 'angleOfRotation' is expressed in degrees
/                   this parameter must be 'YES'
*/
-(CGPoint)calculateTopRightCoordinatesFromViewCenter:(CGPoint)viewCenter viewWidth:(CGFloat)viewWidth viewHeight:(CGFloat)viewHeight angleOfRotation:(CGFloat)angleOfRotation degrees:(BOOL)degrees {

    CGFloat adjacent = viewWidth/2.0;
    CGFloat opposite = viewHeight/2.0;
    CGFloat hipotenuse = sqrtf(powf(adjacent, 2.0) + pow(opposite, 2.0));
    CGFloat thetaRad = acosf((powf(hipotenuse, 2.0) + powf(adjacent, 2.0) - pow(opposite, 2.0)) / (2 * hipotenuse * adjacent));

    CGFloat angleRad = 0.0;
    if (degrees) {
        angleRad = angleOfRotation*M_PI/180.0;
    } else {
        angleRad = angleOfRotation;
    }

    CGFloat widthOffset = cosf(angleRad - thetaRad) * hipotenuse;
    CGFloat heightOffset = sinf(angleRad - thetaRad) * hipotenuse;

    CGPoint offsetPoint = CGPointMake(viewCenter.x + widthOffset, viewCenter.y + heightOffset);
    return offsetPoint;
}
希望这可以帮到您!

我总结了一下,但它似乎不起作用:CGFloat adjacent=rte.frame.size.width/2.0;CGFloat opposite=rte.frame.size.height/2.0;CGFloat hipotenuse=sqrtf(powf(adjacent,2.0)+pow(opposite,2.0));CGFloat theta=acosf((powf(hipotenuse,2.0)+powf(adjacent,2.0)-pow(opposite, 2.0))/(2hipotenuseadjacent));theta = theta*180/M_PI;CGFloat radians = atan2f(rte.transform.b, rte.transform.a);CGFloat degrees = radians * (180 / M_PI);CGFloat width = cosf(degrees - theta) * hipotenuse;CGFloat height = sinf(degrees - theta) * hipotenuse;rightTopNode.center = CGPointMake(rte.center.x + width, rte.center.y + height); - Vad
嗨@Vad。你理解得很对。你代码中的错误是(这是因为我忘了解释那部分),cosf()和sinf()计算宽度和高度的参数必须是弧度。另外,请查看我的更新答案。我添加了一个简化的方法,可以为您计算点的坐标。最后,我不确定为什么您要使用atan2f。您提到您为视图使用旋转变换。旋转变换函数需要一个以弧度为单位的角度来旋转。您应该使用相同的角度进行计算。希望这有所帮助。 - LuisCien
有时候我无法使用旋转变换,我想使用变换后的对象属性来计算弧度 - 这就是为什么我使用了 atan2f(rte.transform.b, rte.transform.a);这不是同样的事情吗?谢谢。 - Vad
嗨@Vad,抱歉回复晚了。我刚刚使用atan2f方法测试了一下,它可以正常工作。只需在计算“宽度”和“高度”时记得使用弧度制。不要将“theta”转换为弧度制,直接使用“atan2f”的结果,因为该函数返回弧度制。 - LuisCien
请不要使用此函数,建议使用 @LeoNatan 和 @Bladebunny 等人建议的 CGPointApplyAffineTransform()CGRectApplyAffineTransform() - nielsbot
不知何故,这个解决方案对我来说从未完全奏效,但我必须要给予赞誉,因为它确实帮了我很多。感谢@LuisCien。下面是完整的解决方案,它完美地适用于我,并且包含了每个边缘点(的情况)。我非常重视你的贡献。 - Vad

5

所有这些答案都很疯狂,这很简单...

CGPoint topLeft = [rotatedView convertPoint:CGPointMake(0, 0) toView:rotatedView.superview];

CGPoint topRight = [rotatedView convertPoint:CGPointMake(rotatedView.bounds.size.width, 0) toView:rotatedView.superview];

CGPoint bottomLeft = [rotatedView convertPoint:CGPointMake(0, rotatedView.bounds.size.height) toView:rotatedView.superview];

CGPoint bottomRight = [rotatedView convertPoint:CGPointMake(rotatedView.bounds.size.width, rotatedView.bounds.size.height) toView:rotatedView.superview];

5

你应该使用:

CGPoint CGPointApplyAffineTransform (
   CGPoint point,
   CGAffineTransform t
);

要获取特定点,请使用视图的边界和中心,然后应用视图的变换以在变换后获得新点。这比专门添加旋转变换代码更好,因为它可以支持任何变换以及链接。

1

为什么要弄得这么复杂?保持简单吗?在变换之前,点x与锚点的距离会增加q弧度/角度,就像围绕锚点的其他点一样。

本来想讲解所有内容,但是这篇文章中的人用更短的语境解释了:

获取UIview的当前角度/旋转/弧度?

CGFloat radians = atan2f(yourView.transform.b, yourView.transform.a); 
CGFloat degrees = radians * (180 / M_PI);

1
尝试这段代码。
CGPoint localBeforeTransform = CGPointMake( view.bounds.size.width/2.0f, view.bounds.size.height/2.0f ); // lower left corner
CGPoint localAfterTransform = CGPointApplyAffineTransform(localBeforeTransform, transform);
CGPoint globalAfterTransform = CGPointMake(localAfterTransform.x + view.center.x, localAfterTransform.y + view.center.y);

0
我写了这个可以帮助我们的类:

TransformedViewFrameCalculator.h

#import <Foundation/Foundation.h>

@interface TransformedViewFrameCalculator : NSObject

@property (nonatomic, strong) UIView *viewToProcess;

- (void)calculateTransformedCornersWithTranslation:(CGPoint)translation
                                             scale:(CGFloat)scale
                                          rotation:(CGFloat)rotation;

@property (nonatomic, readonly) CGPoint transformedTopLeftCorner;
@property (nonatomic, readonly) CGPoint transformedTopRightCorner;
@property (nonatomic, readonly) CGPoint transformedBottomLeftCorner;
@property (nonatomic, readonly) CGPoint transformedBottomRightCorner;

@end

TransformedViewFrameCalculator.m:

#import "TransformedViewFrameCalculator.h"

@interface TransformedViewFrameCalculator ()

@property (nonatomic, assign) CGRect viewToProcessNotTransformedFrame;
@property (nonatomic, assign) CGPoint viewToProcessNotTransformedCenter;

@end

@implementation TransformedViewFrameCalculator

- (void)setViewToProcess:(UIView *)viewToProcess {
    _viewToProcess = viewToProcess;

    CGAffineTransform t = _viewToProcess.transform;
    _viewToProcess.transform = CGAffineTransformIdentity;

    _viewToProcessNotTransformedFrame = _viewToProcess.frame;
    _viewToProcessNotTransformedCenter = _viewToProcess.center;

    _viewToProcess.transform = t;
}

- (void)calculateTransformedCornersWithTranslation:(CGPoint)translation
                                             scale:(CGFloat)scale
                                          rotation:(CGFloat)rotation {
    double viewWidth = _viewToProcessNotTransformedFrame.size.width * scale;
    double viewHeight = _viewToProcessNotTransformedFrame.size.height * scale;
    CGPoint viewCenter = CGPointMake(_viewToProcessNotTransformedCenter.x + translation.x,
                                     _viewToProcessNotTransformedCenter.y + translation.y);

    _transformedTopLeftCorner = [self calculateCoordinatesForViewPoint:CGPointMake(0, 0)
                                                        fromViewCenter:viewCenter
                                                             viewWidth:viewWidth
                                                            viewHeight:viewHeight
                                                       angleOfRotation:rotation];

    _transformedTopRightCorner = [self calculateCoordinatesForViewPoint:CGPointMake(0, viewHeight)
                                                         fromViewCenter:viewCenter
                                                              viewWidth:viewWidth
                                                             viewHeight:viewHeight
                                                        angleOfRotation:rotation];

    _transformedBottomLeftCorner = [self calculateCoordinatesForViewPoint:CGPointMake(viewWidth, 0)
                                                           fromViewCenter:viewCenter
                                                                viewWidth:viewWidth
                                                               viewHeight:viewHeight
                                                          angleOfRotation:rotation];

    _transformedBottomRightCorner = [self calculateCoordinatesForViewPoint:CGPointMake(viewWidth, viewHeight)
                                                            fromViewCenter:viewCenter
                                                                 viewWidth:viewWidth
                                                                viewHeight:viewHeight
                                                           angleOfRotation:rotation];
}

- (CGPoint)calculateCoordinatesForViewPoint:(CGPoint)viewPoint
                             fromViewCenter:(CGPoint)viewCenter
                                  viewWidth:(CGFloat)viewWidth
                                 viewHeight:(CGFloat)viewHeight
                            angleOfRotation:(CGFloat)angleOfRotation {
    CGPoint centeredViewPoint = CGPointMake(viewPoint.x - viewWidth/2.0, viewPoint.y - viewHeight/2.0);
    CGPoint rotatedCenteredViewPoint = CGPointApplyAffineTransform(centeredViewPoint, CGAffineTransformMakeRotation(angleOfRotation));
    CGPoint rotatedViewPoint = CGPointMake(rotatedCenteredViewPoint.x + viewCenter.x, rotatedCenteredViewPoint.y + viewCenter.y);
    return rotatedViewPoint;
}

例如,我使用它来限制容器视图内贴纸的移动/缩放/旋转,具体方法如下:
@property (nonatomic, strong) TransformedViewFrameCalculator *transformedFrameCalculator;

...

self.transformedFrameCalculator = [TransformedViewFrameCalculator new];
self.transformedFrameCalculator.viewToProcess = someView;

...

- (BOOL)transformedView:(UIView *)view
        withTranslation:(CGPoint)translation
                  scale:(double)scale
               rotation:(double)rotation
isFullyInsideValidFrame:(CGRect)validFrame {

    [self.transformedFrameCalculator calculateTransformedCornersWithTranslation:translation
                                                                          scale:scale

    BOOL topRightIsInsideValidFrame = CGRectContainsPoint(validFrame, self.transformedFrameCalculator.transformedTopRightCorner);
    BOOL topLeftIsInsideValidFrame = CGRectContainsPoint(validFrame, self.transformedFrameCalculator.transformedTopLeftCorner);
    BOOL bottomRightIsInsideValidFrame = CGRectContainsPoint(validFrame, self.transformedFrameCalculator.transformedBottomRightCorner);
    BOOL bottomLeftIsInsideValidFrame = CGRectContainsPoint(validFrame, self.transformedFrameCalculator.transformedBottomLeftCorner);

    return topRightIsInsideValidFrame && topLeftIsInsideValidFrame && bottomRightIsInsideValidFrame && bottomLeftIsInsideValidFrame;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接