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

注意 : 自从提出这个问题以来,事情一直在向前发展; 请参阅 给你获得最近的一个很好的概述。


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

在自动布局的世界里,我们不再设置框架,但是约束似乎不能胜任将视图的位置调整回我们想要的位置的任务。您可以破解约束来重新定位视图,但在旋转或其他调整大小事件时,这些约束又会变得无效。

下面这个聪明的想法不起作用,因为它创建了一个“无效的布局属性配对(左边和宽度)”:

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(距离我想插入左边缘的超视图)。

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

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

60893 次浏览

我目前的解决方案是手动调整层的位置在 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;
}


}
}

我认为你用这种方法破坏了自动布局的目的。您确实提到了宽度和右边缘取决于“超视图”,那么为什么不沿着这条思路添加约束呢?

放弃 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

如果你使用自动布局,那么我不知道如何手动设置的位置将服务于长期,因为最终自动布局将重击你已经设置的位置值,当它计算自己的布局。

相反,我们需要的是修改布局约束本身,以补偿通过设置 anchorPoint 产生的更改。下面的函数对未转换的视图执行此操作。

/**
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 (e.g., {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 的唯一原因是要设置一个转换。这将需要一个更复杂的函数来更新布局约束,以反映转换属性本身可能引起的帧更改。这很棘手,因为转换可以对框架做很多事情。缩放或旋转变换会使框架变大,所以我们需要更新任何宽度或高度约束,等等。.

如果你只是为了一个临时的动画而使用转换,那么以上的内容就足够了,因为我不相信自动布局会阻止飞行中的动画显示图像,这些图像代表了纯粹的临时违反约束的行为。

您可以为其中一个约束创建一个出口,以便可以删除它并重新添加。


我创建了一个新项目,并在中间添加了一个固定大小的视图。约束显示在下面的图像中。

The constraints for the smallest example I could think of.

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

@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;

然后我移除约束,我有一个出口,创建一个新的是偏移,并添加回来。之后,我告诉带有已更改约束的视图它需要更新约束。

[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];

最后,我只是将旋转动画添加到旋转视图中。

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"];

旋转图层看起来像是保持在中心位置(应该是这样的) ,即使旋转设备或其他原因导致它更新约束。新约束和更改后的锚点在视觉上相互抵消。

【编辑: 警告:整个接下来的讨论可能会被 iOS8淘汰,或者至少会被大大减弱,因为 iOS8可能不会在应用视图转换时错误地触发布局。】

自动布局与视图转换

自动布局在视图转换中完全不起作用。据我所知,原因是你不应该去改变一个有转换的视图的框架(除了默认的身份转换)——但是这正是自动布局所做的。自动布局的工作方式是,在 layoutSubviews中,运行时突破所有约束,并相应地设置所有视图的框架。

换句话说,约束并不神奇; 它们只是一个待办事项列表。layoutSubviews是完成任务清单的地方。它通过设置帧来完成。

如果我将这个转换应用于一个视图,我会不由自主地将其视为一个 bug:

v.transform = CGAffineTransformMakeScale(0.5,0.5);

我希望看到的视图出现与其中心在相同的地方,以前和在一半的大小。但是根据它的局限性,这可能根本不是我所看到的。

[实际上,这里还有第二个惊喜: 对视图应用转换会立即触发布局。在我看来,这是另一个错误。或者是第一只虫子的心脏。我所期望的是能够摆脱转换,至少直到布局时间,例如设备是旋转的-就像我可以摆脱一个框架动画,直到布局时间。但实际上,布局时间是即时的,这似乎是错误的。]

解决方案1: 没有约束

当前的一个解决方案是,如果我要对视图应用一个半永久的转换(而不仅仅是以某种方式暂时摇摆它) ,那么就要移除影响它的所有约束。不幸的是,这通常会导致视图从屏幕上消失,因为自动布局仍然会发生,而且现在没有约束来告诉我们将视图放在哪里。因此,除了去除约束之外,我还将视图的 translatesAutoresizingMaskIntoConstraints设置为 YES。该视图现在按照旧的方式工作,实际上不受自动布局的影响。(显然,受到自动布局的影响,但隐式的自动调整掩码约束使其行为与自动布局之前一样。)

解决方案2: 只使用适当的约束

如果这看起来有点过激,那么另一种解决方案是设置约束,使其能够正确地与预期的转换一起工作。例如,如果一个视图的大小纯粹由其内部固定的宽度和高度决定,而位置纯粹由其中心决定,那么我的缩放变换将如我所期望的那样工作。在这段代码中,我移除了子视图(otherView)上现有的约束,并用这四个约束替换它们,给它一个固定的宽度和高度,并完全按照它的中心钉住它。在那之后,我的尺度转换工作:

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]];

结果是,如果没有影响视图框架的约束,自动布局就不会触及视图的框架——这正是涉及到转换时所追求的。

解决方案3: 使用 Subview

上述两种解决方案的问题在于,我们失去了定位视图的约束的好处。所以这里有一个解决方案。从一个不可见的视图开始,它的工作仅仅是充当主机,并使用约束来定位它。在里面,把真实的视图作为一个子视图。使用约束将子视图定位在宿主视图中,但是将这些约束限制在应用转换时不会反击的约束中。

这里有一个例子:

enter image description here

白色视图是宿主视图; 您应该假装它是透明的,因此是不可见的。红色视图是它的子视图,通过将其中心固定在宿主视图的中心来定位。现在我们可以把红色的视图放大并绕着它的中心旋转,没有任何问题,实际上这幅图表明我们已经这样做了:

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

与此同时,主机视图的约束保持它在正确的地方,因为我们旋转设备。

解决方案4: 改用层转换

使用层转换代替视图转换,层转换不会触发布局,因此不会立即与约束发生冲突。

例如,这个简单的“悸动”视图动画可能会在自动布局下中断:

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

即使最终视图的大小没有改变,仅仅是设置它的 transform就会导致布局的发生,并且约束会使视图跳来跳去。(这感觉像虫子还是什么?)但是如果我们对 Core Animation 做同样的事情(使用 CABasicAnimation 并将动画应用到视图层) ,布局就不会发生,而且它工作得很好:

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];

启发了 Matt 的回答,我决定尝试另一种方法。可以使用具有适当应用的约束的容器视图。带有修改锚点的视图可以放置在容器视图中,使用自动调整大小的掩码和显式的帧设置,就像在糟糕的旧日子里一样。

无论如何,对于我的情况来说,它是一种享受。视图设置在 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;
}

此时红色视图的框架为零并不重要,因为绿色视图上有自动调整大小的掩码。

我在一个操作方法上添加了一个旋转变换,结果是这样的:

enter image description here

它似乎在设备旋转过程中丢失了自己,所以我把它添加到 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];


}

我有一个类似的问题,刚刚听到回来的自动布局团队在苹果公司。他们建议使用容器视图方法 Matt 建议,但是他们创建了一个 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

正如您所看到的,它在第一次调用时抓住视图的中心点,并在进一步的调用中重用该位置以相应地放置视图。从这个意义上说,它覆盖了自动布局代码,即它发生在[ super layoutSubviews ]之后; 它包含自动布局代码。

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

这个问题和答案启发我用自动布局和缩放来解决我自己的问题,但是是用滚动视图。我在 github 上创建了一个解决方案的例子:

Https://github.com/hansdesmedt/autolayout-scrollview-scale

这是一个 UIScrollView 的例子,它具有完全由 AutoLayout 制作的自定义分页,并且可以通过长按和点击缩放来扩展(CATransform3DMakeScale)。IOS6和 iOS7兼容。

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

我从 XIB 看到了自动布局。我想更新它的转换属性。将视图嵌入到容器视图中并不能解决我的问题,因为自动布局对容器视图的作用很奇怪。这就是为什么我刚刚添加了第二个容器视图来包含包含我的视图的容器视图,并对其应用转换。

假设您将锚点更改为(0,0)。锚点现在在左上角。任何时候你看到字 中心在自动布局,你应该考虑 左上角

调整锚点时,只需更改 AutoLayout 的语义。自动布局不会干扰您的锚点,反之亦然。如果你不明白这一点,你会过得很糟糕的


例如:

图 A

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

图 B

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

图 A 和图 B 看起来完全一样。什么都没变。只是 中心所指的定义发生了变化。

我找到了一个简单的方法,它可以在 iOS8和 iOS9上运行。

比如在使用基于帧的布局时调整锚点:

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

当你使用自动布局来调整视图的锚点时,你也可以做同样的事情,只不过是以约束的方式。当 anchorPoint 从(0.5,0.5)变为(1,0.5)时,layerView 将向左移动,距离是视图宽度的一半,因此您需要对此进行补偿。

我不理解这个问题中的约束。因此,假设您添加了一个与 superView centerX 相关的常量的 centerX 约束: layerView.centerX = superView.centerX + 常量

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

我有一个相对简单的情况下,我需要设置锚点等于 (0, 1)。显然,我能够使用一个容器视图来实现这种效果,并在其上映射值(如 (0, 1) -> (1, 0)) ,因此产生的视图就像任何普通的 (0.5, 0.5)一样

let scaledLabel = UILabel()
let container = UIView()


container.addSubview(scaledLabel)


// pin label to the edges of container with autolayout


scaledLabel.layer.anchorPoint = .init(x: 0, y: 1)
container.layer.anchorPoint = .init(x: 1, y: 0)

我不完全确定它如何与其他值一起工作,但它可能仍然是有帮助的