当使用自动布局时,如何调整CALayer的锚点?

注意 :这个问题提出后,事情已经开始了。 在这里看到一个很好的最近的概述。


在自动布局之前,您可以通过存储框架,设置锚点和恢复框架来更改视图图层的锚点,而无需移动视图。

在一个自动布局的世界中,我们不再设置框架,但是约束似乎不适合将视图的位置调整回到我们想要的位置。 您可以破解约束来重新定位您的视图,但是在旋转或其他resize事件时,这些事件会再次失效。

以下明智的想法不起作用,因为它创build了“布局属性的无效配对(左和宽)”:

layerView.layer.anchorPoint = CGPointMake(1.0, 0.5); // Some other size-related constraints here which all work fine... [self.view addConstraint: [NSLayoutConstraint constraintWithItem:layerView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:layerView attribute:NSLayoutAttributeWidth multiplier:0.5 constant:20.0]]; 

我的意图是将layerView的左边缘,调整后的锚点的视图设置为其宽度的一半加上20(我想要从layerView视图的左边缘插入的距离)。

是否有可能改变锚点,而不改变视图的位置,在自动布局布局的视图? 我是否需要使用硬编码值并在每次旋转时编辑约束? 我希望不是。

我需要更改定位点,以便在对视图应用转换时,可以获得正确的视觉效果。

[编辑: 警告:整个随后的讨论可能会过时或至less严重缓解的iOS 8,这可能不会再在视图变换应用时触发布局的错误。]

Autolayout与视图变换

Autolayout在视图转换方面效果不佳。 据我所知,原因是你不应该混淆有一个转换的视图的框架(除了默认的身份转换),但这正是autolayout所做的。 自动布局的工作方式是在layoutSubviews中运行时通过所有的约束,并相应地设置所有视图的帧。

换句话说,约束不是魔法; 他们只是一个待办事项清单。 layoutSubviews是待办事项完成的地方。 它通过设置框架来完成。

我不禁把这当成一个bug。 如果我将这个转换应用于一个视图:

 v.transform = CGAffineTransformMakeScale(0.5,0.5); 

我期望看到的观点与其中心在以前一样的地方,一半的大小。 但是根据它的限制,这可能不是我所看到的。

[实际上,这里还有一个惊喜:将一个转换应用到视图会立即触发布局。 这在我看来是另一个错误。 或者也许这是第一个bug的核心。 我期望的是能够摆脱一个转换,至less在布局时间,例如设备旋转 – 就像我可以摆脱框架animation,直到布局时间。 但实际上布局时间是即时的,这似乎是错误的。]

解决scheme1:没有约束

目前的一个解决scheme是,如果我要对视图应用半永久变换(而不是仅仅暂时摇摆它),以消除影响它的所有约束。 不幸的是,这通常会导致视图从屏幕上消失,因为自动布局仍然存在,现在没有任何限制来告诉我们在哪里放置视图。 所以除了删除约束,我把视图的translatesAutoresizingMaskIntoConstraints设置为YES。 该视图现在以旧的方式工作,实际上不受自动布局的影响。 (很显然,它受到自动布局的影响,但是隐式的自动调整掩码约束会导致其行为与自动布局之前的行为一样)。

解决scheme2:只使用适当的约束

如果这看起来有点激烈,另一个解决scheme是设置约束与预期的转换正常工作。 如果一个视图的大小完全由其内部固定的宽度和高度来确定,并且纯粹由其中心定位,那么我的大小变换将按我的预期工作。 在这个代码中,我删除了子视图( otherView )上的现有约束,并将它们replace为这四个约束,给它一个固定的宽度和高度,并纯粹由它的中心固定。 之后,我的规模转换工作:

 NSMutableArray* cons = [NSMutableArray array]; for (NSLayoutConstraint* con in self.view.constraints) if (con.firstItem == self.otherView || con.secondItem == self.otherView) [cons addObject:con]; [self.view removeConstraints:cons]; [self.otherView removeConstraints:self.otherView.constraints]; [self.view addConstraint: [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeCenterX relatedBy:0 toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1 constant:self.otherView.center.x]]; [self.view addConstraint: [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeCenterY relatedBy:0 toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:self.otherView.center.y]]; [self.otherView addConstraint: [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeWidth relatedBy:0 toItem:nil attribute:0 multiplier:1 constant:self.otherView.bounds.size.width]]; [self.otherView addConstraint: [NSLayoutConstraint constraintWithItem:self.otherView attribute:NSLayoutAttributeHeight relatedBy:0 toItem:nil attribute:0 multiplier:1 constant:self.otherView.bounds.size.height]]; 

结果是,如果没有影响视图框架的约束条件,则自动布局不会触及视图的框架 – 这正是涉及到变换时的情况。

解决scheme3:使用子视图

上述两个解决scheme的问题是,我们失去了约束的好处来定位我们的观点。 所以这里有一个解决scheme。 从一个看不见的视angular开始,其工作仅仅是充当主持人,并使用约束来定位它。 在里面,把真实的视图作为子视图。 使用约束来定位主视图中的子视图,但将这些约束限制为在应用变换时不会反击的约束。

这是一个例子:

在这里输入图像描述

白色视图是主机视图; 你应该假装它是透明的,因此是不可见的。 红色视图是其子视图,通过将其中心固定到主视图的中心来定位。 现在我们可以在没有任何问题的情况下将红色视图围绕中心进行缩放和旋转,而且实际上插图显示我们已经这样做了:

 self.otherView.transform = CGAffineTransformScale(self.otherView.transform, 0.5, 0.5); self.otherView.transform = CGAffineTransformRotate(self.otherView.transform, M_PI/8.0); 

同时,在我们旋转设备时,主机视图上的限制使它保持在正确的位置。

解决scheme4:使用图层转换

不使用视图变换,而是使用图层变换,它不会触发布局,因此不会立即与约束冲突。

例如,这个简单的“悸动”视图animation可能会在自动布局下崩溃:

 [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionAutoreverse animations:^{ v.transform = CGAffineTransformMakeScale(1.1, 1.1); } completion:^(BOOL finished) { v.transform = CGAffineTransformIdentity; }]; 

尽pipe最终没有改变视图的大小,但只是设置其transform会导致布局发生,并且约束可以使视图跳转。 (这是否觉得像一个错误或什么?)但是,如果我们与核心animation(使用CABasicAnimation并将animation应用到视图的图层)做同样的事情,布局不会发生,并且工作正常:

 CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:@"transform"]; ba.autoreverses = YES; ba.duration = 0.3; ba.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.1, 1.1, 1)]; [v.layer addAnimation:ba forKey:nil]; 

我有一个类似的Isuue,刚刚从苹果Autolayout团队听到了回来。 他们build议使用容器视图方法暗示build议,但他们创build一个UIView的子类覆盖layoutSubviews并应用自定义布局代码 – 它的作品像一个魅力

头文件看起来像这样,你可以直接从Interface Builder链接你的子视图

 #import <UIKit/UIKit.h> @interface BugFixContainerView : UIView @property(nonatomic,strong) IBOutlet UIImageView *knobImageView; @end 

和m文件应用这样的特殊代码:

 #import "BugFixContainerView.h" @implementation BugFixContainerView - (void)layoutSubviews { static CGPoint fixCenter = {0}; [super layoutSubviews]; if (CGPointEqualToPoint(fixCenter, CGPointZero)) { fixCenter = [self.knobImageView center]; } else { self.knobImageView.center = fixCenter; } } @end 

正如你所看到的,它首先调用视图的中心点,并在进一步调用中重新使用该位置,以便相应地放置视图。 这会覆盖Autolayout代码,它会在[super layoutSubviews]之后发生。 其中包含自动布局代码。

像这样,不再需要避免Autolayout,但是当默认行为不再合适时,你可以创build自己的自动布局。 当然你可以申请更复杂的东西比那个例子,但这是我所需要的,因为我的应用程序只能使用肖像模式。

我find一个简单的方法。 它适用于iOS 8和iOS 9。

像使用基于框架的布局时调整anchorPoint:

 let oldFrame = layerView.frame layerView.layer.anchorPoint = newAnchorPoint layerView.frame = oldFrame 

当您使用自动布局调整视图的锚点时,您会以约束的方式执行相同的操作。 当anchorPoint由(0.5,0.5)变为(1,0.5)时,layerView会向左移动一半距离,所以您需要对此进行补偿。

我不明白在问题中的约束。所以,假设你添加一个centerX约束相对于superView centerX与一个常量:layerView.centerX = superView.centerX +常量

 layerView.layer.anchorPoint = CGPoint(1, 0.5) let centerXConstraint = ..... centerXConstraint.constant = centerXConstraint.constant + layerView.bounds.size.width/2 

如果您使用的是自动布局,那么从长远来看,我不会看到手动设置位置的方式,因为最终,自动布局会在您计算自己的布局时重新设置您设置的位置值。

相反,我们需要修改布局约束来补偿设置anchorPoint所产生的变化。 以下function为未转换的视图。

 /** Set the anchorPoint of view without changing is perceived position. @param view view whose anchorPoint we will mutate @param anchorPoint new anchorPoint of the view in unit coords (eg, {0.5,1.0}) @param xConstraint an NSLayoutConstraint whose constant property adjust's view x.center @param yConstraint an NSLayoutConstraint whose constant property adjust's view y.center As multiple constraints can contribute to determining a view's center, the user of this function must specify which constraint they want modified in order to compensate for the modification in anchorPoint */ void SetViewAnchorPointMotionlesslyUpdatingConstraints(UIView * view,CGPoint anchorPoint, NSLayoutConstraint * xConstraint, NSLayoutConstraint * yConstraint) { // assert: old and new anchorPoint are in view's unit coords CGPoint const oldAnchorPoint = view.layer.anchorPoint; CGPoint const newAnchorPoint = anchorPoint; // Calculate anchorPoints in view's absolute coords CGPoint const oldPoint = CGPointMake(view.bounds.size.width * oldAnchorPoint.x, view.bounds.size.height * oldAnchorPoint.y); CGPoint const newPoint = CGPointMake(view.bounds.size.width * newAnchorPoint.x, view.bounds.size.height * newAnchorPoint.y); // Calculate the delta between the anchorPoints CGPoint const delta = CGPointMake(newPoint.x-oldPoint.x, newPoint.y-oldPoint.y); // get the x & y constraints constants which were contributing to the current // view's position, and whose constant properties we will tweak to adjust its position CGFloat const oldXConstraintConstant = xConstraint.constant; CGFloat const oldYConstraintConstant = yConstraint.constant; // calculate new values for the x & y constraints, from the delta in anchorPoint // when autolayout recalculates the layout from the modified constraints, // it will set a new view.center that compensates for the affect of the anchorPoint CGFloat const newXConstraintConstant = oldXConstraintConstant + delta.x; CGFloat const newYConstraintConstant = oldYConstraintConstant + delta.y; view.layer.anchorPoint = newAnchorPoint; xConstraint.constant = newXConstraintConstant; yConstraint.constant = newYConstraintConstant; [view setNeedsLayout]; } 

我承认这可能不是你所希望的一切,因为通常你想要修改anchorPoint的唯一原因就是设置一个变换。 这将需要更复杂的函数来更新布局约束,以反映可能由transform属性本身引起的所有帧更改。 这是棘手的,因为转换可以做很多框架。 缩放或旋转变换会使框架更大,所以我们需要更新任何宽度或高度约束等。

如果您只是将临时animation转换为临时animation,那么上面的内容就足够了,因为我不相信自动布局会阻止空中animation呈现代表纯粹是暂时违反约束条件的图像。

tl:dr:您可以为其中一个约束创build一个sockets,以便将其移除并再次添加。


我创build了一个新项目,并在中心添加了一个固定大小的视图。 约束如下图所示。

我能想到的最小例子的限制。

接下来,我为要旋转的视图和中心xalignment约束添加了一个出口。

 @property (weak, nonatomic) IBOutlet UIView *rotatingView; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *xAlignmentContstraint; 

后来在viewDidAppear我计算新的锚点

 UIView *view = self.rotatingView; CGPoint rotationPoint = // The point I'm rotating around... (only X differs) CGPoint anchorPoint = CGPointMake((rotationPoint.x-CGRectGetMinX(view.frame))/CGRectGetWidth(view.frame), (rotationPoint.y-CGRectGetMinY(view.frame))/CGRectGetHeight(view.bounds)); CGFloat xCenterDifference = rotationPoint.x-CGRectGetMidX(view.frame); view.layer.anchorPoint = anchorPoint; 

然后我删除了我有一个出口的约束,创build一个新的偏移量,并将其添加回来。 之后,我用改变的约束来告诉视图,它需要更新约束。

 [self.view removeConstraint:self.xAlignmentContstraint]; self.xAlignmentContstraint = [NSLayoutConstraint constraintWithItem:self.rotatingView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:xDiff]; [self.view addConstraint:self.xAlignmentContstraint]; [self.view needsUpdateConstraints]; 

最后,我只是将旋转animation添加到旋转视图。

 CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; rotate.toValue = @(-M_PI_2); rotate.autoreverses = YES; rotate.repeatCount = INFINITY; rotate.duration = 1.0; rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; [view.layer addAnimation:rotate forKey:@"myRotationAnimation"]; 

即使在旋转设备或以其他方式使其更新约束时,旋转层看起来仍然居中(它应该)。 新的约束和改变的锚点在视觉上相互抵消。

我目前的解决scheme是手动调整viewDidLayoutSubviews图层的位置。 这个代码也可以用在视图子类的layoutSubviews中,但在我的情况下,我的视图是一个视图控制器内的顶层视图,所以这意味着我不必制作一个UIView子类。

这似乎是太多的努力,所以其他答案是最受欢迎的。

 -(void)viewDidLayoutSubviews { for (UIView *view in self.view.subviews) { CGPoint anchorPoint = view.layer.anchorPoint; // We're only interested in views with a non-standard anchor point if (!CGPointEqualToPoint(CGPointMake(0.5, 0.5),anchorPoint)) { CGFloat xDifference = anchorPoint.x - 0.5; CGFloat yDifference = anchorPoint.y - 0.5; CGPoint currentPosition = view.layer.position; // Use transforms if we can, otherwise manually calculate the frame change // Assuming a transform is in use since we are changing the anchor point. if (CATransform3DIsAffine(view.layer.transform)) { CGAffineTransform current = CATransform3DGetAffineTransform(view.layer.transform); CGAffineTransform invert = CGAffineTransformInvert(current); currentPosition = CGPointApplyAffineTransform(currentPosition, invert); currentPosition.x += (view.bounds.size.width * xDifference); currentPosition.y += (view.bounds.size.height * yDifference); currentPosition = CGPointApplyAffineTransform(currentPosition, current); } else { CGFloat transformXRatio = view.bounds.size.width / view.frame.size.width; if (xDifference < 0) transformXRatio = 1.0/transformXRatio; CGFloat transformYRatio = view.bounds.size.height / view.frame.size.height; if (yDifference < 0) transformYRatio = 1.0/transformYRatio; currentPosition.x += (view.bounds.size.width * xDifference) * transformXRatio; currentPosition.y += (view.bounds.size.height * yDifference) * transformYRatio; } view.layer.position = currentPosition; } } } 

启发了我的马特的答案,我决定尝试一种不同的方法。 可以使用适当应用约束的容器视图。 带有修改的锚点的视图可以放在容器视图中,使用自动调整掩码和明确的框架设置,就像在过去不好的时代一样。

无论如何,这对我的情况是一种享受。 视图在viewDidLoad中设置:

 - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. UIView *redView = [UIView new]; redView.translatesAutoresizingMaskIntoConstraints = NO; redView.backgroundColor = [UIColor redColor]; [self.view addSubview:redView]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-[redView]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(redView)]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[redView]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(redView)]]; self.redView = redView; UIView *greenView = [UIView new]; greenView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; greenView.layer.anchorPoint = CGPointMake(1.0, 0.5); greenView.frame = redView.bounds; greenView.backgroundColor = [UIColor greenColor]; [redView addSubview:greenView]; self.greenView = greenView; CATransform3D perspective = CATransform3DIdentity; perspective.m34 = 0.005; self.redView.layer.sublayerTransform = perspective; } 

因为绿色视图上的自动识别掩码,所以红色视图的帧在这一点上是零的并不重要。

我在动作方法上添加了旋转变换,结果如下:

在这里输入图像描述

它似乎在设备旋转过程中丢失了自己,所以我将其添加到viewDidLayoutSubviews方法:

 -(void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; [CATransaction begin]; [CATransaction setDisableActions:YES]; CATransform3D transform = self.greenView.layer.transform; self.greenView.layer.transform = CATransform3DIdentity; self.greenView.frame = self.redView.bounds; self.greenView.layer.transform = transform; [CATransaction commit]; } 

tl; dr假设您将定位点更改为(0,0)。 锚点现在是左上angular。 任何时候当你在自动布局中看到单词中心时,都应该考虑左上angular

当你调整你的anchorPoint时,你只需要改变AutoLayout的语义。 自动布局不会干扰您的锚点,反之亦然。 如果你不明白这一点, 你会有一个不好的时间


例:

图A. 没有锚点修改

 #Before changing anchor point to top-left view.size == superview.size view.center == superview.center 

图B. 锚点改为左上angular

 view.layer.anchorPoint = CGPointMake(0, 0) view.size == superview.size view.center == superview.topLeft <----- L0-0K, center is now top-left 

图A和图B看起来完全一样。 没有改变。 只是中心所指的变化的定义。

我认为你用这种方法击败了自动布局的目的。 你确实提到了宽度和右边缘取决于超视图,那么为什么不在这个思路上添加约束呢?

丢失anchorPoint /转换范例,并尝试:

 [self.view addConstraint: [NSLayoutConstraint constraintWithItem:layerView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeWidth multiplier:1.0f constant:-somePadding]]; [self.view addConstraint: [NSLayoutConstraint constraintWithItem:layerView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:someViewWeDependTheWidthOn attribute:NSLayoutAttributeWidth multiplier:0.5f // because you want it to be half of someViewWeDependTheWidthOn constant:-20.0f]]; // your 20pt offset from the left 

NSLayoutAttributeRight约束完全像anchorPoint = CGPointMake(1.0, 0.5)NSLayoutAttributeWidth约束与您之前的代码的NSLayoutAttributeLeft大致相同。

这个问题和答案启发我解决自己的问题,Autolayout和缩放,但与滚动。 我在github上创build了一个解决scheme的例子:

https://github.com/hansdesmedt/AutoLayout-scrollview-scale

这是一个在AutoLayout中完全自定义分页的UIScrollView示例,可以通过长按和缩放来缩放(CATransform3DMakeScale)。 iOS 6和7兼容。

这是一个很大的话题,我没有阅读所有的意见,但面临同样的问题。

我有一个自动布局的XIB视图。 我想更新其转换属性。 将视图embedded到容器视图中并不能解决我的问题,因为自动布局在容器视图上的行为exception。 这就是为什么我只是添加第二个容器视图来包含容器视图,其中包含我的看法,并对其应用转换。