Как настроить точку привязки 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 (расстояние, которое я хочу вставить от левого края супервизора).

можно ли изменить точку привязки, не меняя расположение вида, в виде, который выложен с автоматической компоновкой? Нужно ли использовать жестко закодированные значения и редактировать ограничение при каждом вращении? Надеюсь, что нет.

мне нужно изменить точку привязки, чтобы при применении преобразования к виду я получал правильный визуальный эффект.

11 149

11 ответов:

[EDIT:предупреждение: вся последующая дискуссия будет, возможно, устаревшей или, по крайней мере, сильно смягченной iOS 8, которая больше не может совершать ошибку запуска макета в то время, когда применяется преобразование представления.]

Autolayout vs. View Transforms

Autolayout не играет вообще хорошо с преобразованиями вида. Причина, насколько я могу понять, заключается в том, что вы не должны возиться с рамкой представления, которая имеет преобразование (кроме преобразования идентификатора по умолчанию) - но это именно то, что делает autolayout. Кстати автомакет работы заключается в том, что в layoutSubviews среда выполнения проходит через все ограничения и устанавливает рамки всех представлений соответственно.

другими словами, ограничения не являются магией; они просто список дел. layoutSubviews где список дел не делается. И он делает это, устанавливая рамки.

Я не могу не считать это ошибкой. Если я применю это преобразование к вид:

v.transform = CGAffineTransformMakeScale(0.5,0.5);

Я ожидаю, что вид появится с его центром в том же месте, что и раньше, и в половине размера. Но в зависимости от его ограничений, это может быть совсем не то, что я вижу.

[на самом деле, здесь есть второй сюрприз: применение преобразования к представлению немедленно запускает макет. Мне кажется, это еще один баг. Или, возможно, это сердце первого жука. Я бы ожидал, что смогу уйти с преобразованием, по крайней мере, до времени макета, например устройство вращается-так же, как я могу уйти с анимацией кадра до времени макета. Но на самом деле время макета является немедленным, что кажется просто неправильным.]

Решение 1: Нет Ограничений

одно из текущих решений заключается в том, что если я собираюсь применить полупостоянное преобразование к представлению (а не просто временно качать его как-то), чтобы удалить все ограничения, влияющие на него. К сожалению, это обычно приводит к тому, что представление исчезает с экрана, так как автозапуск все еще имеет место, и теперь нет никаких ограничений, чтобы сказать нам, куда поместить представление. Поэтому в дополнение к удалению ограничений я устанавливаю представление translatesAutoresizingMaskIntoConstraints "да". Вид теперь работает по-старому, эффективно не зависит от autolayout. (Это и влияет на autolayout, очевидно, но неявные ограничения autoresizing mask заставляют его поведение быть таким же, как и до autolayout.)

Решение 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]];

результат заключается в том, что если у вас нет ограничений, которые влияют на фрейм представления, autolayout не будет касаться фрейма представления - это именно то, что вам нужно, когда происходит преобразование.

решение 3: Используйте подвиды

проблема с обоими вышеуказанными решениями заключается в том, что мы теряем преимущества ограничений для позиционирования нашего взгляда. Итак, вот решение, которое решает это. Начните с невидимого представления, чья работа заключается исключительно в том, чтобы выступать в качестве хоста, и используйте ограничения для его размещения. Внутри этого поместите реальный вид в качестве подвида. Используйте ограничения для размещения подвида в представлении узла, но ограничьте эти ограничения ограничениями, которые не будут сопротивляться при применении преобразования.

вот пример:

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: Вместо Этого Используйте Преобразования Слоев

вместо преобразований вида используйте преобразования слоев, которые не вызывают компоновку и, следовательно, не вызывают немедленного конфликта с ограничения.

например, эта простая "пульсирующая" анимация просмотра вполне может сломаться под autolayout:

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

хотя в конце концов не было никаких изменений в размере представления, просто установив его transform вызывает макет, и ограничения могут заставить вид прыгать вокруг. (Это похоже на ошибку или что?) Но если мы сделаем то же самое с основной анимацией (используя 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];

у меня была похожая вопросу и получили ответ от команды Автомакет в Apple. Они предлагают использовать подход представления контейнера, который предлагает Мэтт, но они создают подкласс 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]; который содержит код автозапуска.

Я нахожу простой способ. И он работает на iOS 8 и iOS 9.

Как настроить точку привязки при использовании макета на основе кадра:

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

когда вы настраиваете якорь вида с автоматической компоновкой, вы делаете то же самое, но в ограничениях. При изменении точки привязки от (0.5, 0.5) до (1, 0.5), layerView будет двигаться влево на расстояние в половину длины ширины вида, поэтому вам нужно компенсировать это.

Я не понимаю ограничения в question.So, предположим, что вы добавляете ограничение 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

Если вы используете auto layout, то я не вижу, как ручная настройка позиции будет служить в долгосрочной перспективе, потому что в конечном итоге auto layout будет сбивать значение позиции, которое вы установили, когда он вычисляет свой собственный макет.

скорее, необходимо изменить сами ограничения компоновки, чтобы компенсировать изменения, вызванные установкой точки привязки. Следующая функция это для непреобразованных видом.

/**
  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];
}

Я признаю, что это, вероятно, не так все, на что вы надеялись, так как обычно единственная причина, по которой вы хотите изменить точку привязки, - это установить преобразование. Это потребует более сложной функции, которая обновляет ограничения макета для отражения все изменения кадра, которые могут быть вызваны самим свойством transform. Это сложно, потому что преобразования могут сделать много для кадра. Преобразование масштабирования или поворота сделает кадр больше, поэтому нам нужно будет обновить любые ограничения ширины или высоты, так далее..

Если вы используете преобразование только для временной анимации, то того, что выше, может быть достаточно, поскольку я не верю, что автоматическая компоновка предотвратит анимацию в полете от представления изображений, которые представляют собой чисто временные нарушения ограничений.

tl: dr: вы можете создать выход для одного из ограничений, чтобы его можно было удалить и добавить обратно.


Я создал новый проект и добавил вид с фиксированным размером в центре. Ограничения показаны на рисунке ниже.

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

вращающийся слой выглядит так, как будто он остается центрированным (что и должно быть) даже при вращении устройства или иным образом заставляет его обновлять ограничения. Новое ограничение и измененная точка привязки визуально отменяют друг друга.

мое текущее решение-вручную настроить положение слоя в 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;
}

это не имеет значения, что кадры для красного вида равны нулю в этой точке, потому что функциями автоматического изменения маски на зеленый вид.

я добавил преобразование вращения на метод действия, и это был результат:

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

}

Я думаю, что вы побеждаете цель autolayout с помощью этого метода. Вы упомянули, что ширина и правый край зависят от супервизора, так почему бы просто не добавить ограничения вдоль этой линии мышления?

потеряйте парадигму якорной точки / преобразования и попробуйте:

[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

The NSLayoutAttributeRight ограничение означает точно так же, как anchorPoint = CGPointMake(1.0, 0.5) и NSLayoutAttributeWidth ограничение примерно эквивалентно вашему предыдущему коду NSLayoutAttributeLeft.

этот вопрос и ответы вдохновили меня на решение моих собственных проблем с автозапуском и масштабированием, но с прокруткой. Я создал пример моего решения на GitHub:

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

Это пример UIScrollView с пользовательской подкачки полностью выполнен в AutoLayout и масштабируется (CATransform3DMakeScale) с длительным нажатием и нажмите, чтобы увеличить. iOS 6 и 7 совместимы.

Это большая тема и я не читал все комментарии, но сталкивается с той же проблемой.

У меня был вид из XIB с автозапуском. И я хотел обновить его свойство преобразования. Внедрение представления в представление контейнера не решает мою проблему, потому что autolayout странно действовал на представление контейнера. Вот почему я просто добавил второе представление контейнера, чтобы содержать представление контейнера, которое содержит мое представление, и применял к нему преобразования.

tl; dr допустим, вы изменили точку привязки на (0, 0). Точка привязки теперь находится вверху слева. В любое время вы видите слово центр в авто макет, вы должны думать верхний левый.

когда вы настраиваете свою точку привязки, вы просто меняете семантику автозапуска. Автоматическая компоновка не будет мешать вашей точке привязки и наоборот. Если ты этого не понимаешь,у вас будет плохо время.


пример:

Рисунок А. нет изменений точки привязки

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

Рисунок Б. точка привязки изменена на верхний левый

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

рисунок A и рисунок B выглядят точно так же. Ничего не изменилось. Просто определение того, что центр относится к изменен.