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

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

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

Ответ 1

[РЕДАКТИРОВАТЬ: Предупреждение: вся последующая дискуссия, возможно, устареет или, по крайней мере, сильно смягчается iOS 8, которая больше не может совершать ошибку при запуске макета в то время, когда применяется преобразование вида.]

Автоотключение и просмотр трансформирования

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

Другими словами, ограничения не являются волшебными; они просто список дел. layoutSubviews - это список выполняемых дел. И он делает это, устанавливая кадры.

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

v.transform = CGAffineTransformMakeScale(0.5,0.5);

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

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

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

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

Решение 2: используйте только соответствующие ограничения

Если это кажется немного резким, другое решение - установить ограничения для правильной работы с предполагаемым преобразованием. Если вид имеет размер только по его внутренней фиксированной ширине и высоте и, например, позиционируется исключительно его центром, мое масштабирование будет работать, как я ожидаю. В этом коде я удаляю существующие ограничения на subview (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];

Ответ 2

У меня был аналогичный Isuue, и я только что услышал "Назад от команды Autolayout в Apple". Они предлагают использовать подход Container View Approach 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

Как вы можете видеть, он захватывает центральную точку представления при первом вызове и повторно использует эту позицию в дальнейших вызовах, чтобы соответственно поместить вид. Это заменяет код Autolayout в том смысле, что это происходит после [super layoutSubviews]; который содержит код автозапуска.

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

Ответ 3

Я нахожу простой способ. И он работает на 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 будет перемещаться влево с расстоянием на половину длины ширины обзора, поэтому вам нужно компенсировать это.

Я не понимаю ограничения в вопросе. Итак, предположим, что вы добавляете ограничение центра X относительно супер-центра centerX с константой: layerView.centerX = superView.centerX + constant

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

Ответ 4

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

Скорее всего, необходимо изменить сами ограничения компоновки, чтобы компенсировать изменения, вызванные установкой 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 view x.center
 @param yConstraint an NSLayoutConstraint whose constant property adjust view y.center

  As multiple constraints can contribute to determining a view 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 unit coords
  CGPoint const oldAnchorPoint = view.layer.anchorPoint;
  CGPoint const newAnchorPoint = anchorPoint;

  // Calculate anchorPoints in view 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 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. Это сложно, потому что преобразования могут многое сделать для фрейма. Преобразование масштабирования или поворота сделает раму более крупным, поэтому нам нужно будет обновить любые ограничения ширины или высоты и т.д.

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

Ответ 5

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

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

Ответ 6

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

    }
}

Ответ 7

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

В любом случае это работает, для моей ситуации. Представления создаются здесь в представленииDidLoad:

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

}

Ответ 8

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

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


Пример:

Рисунок 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 выглядят точно так же. Ничего не изменилось. Просто определение того, что означает центр, изменено.

Ответ 9

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

Потеряйте парадигму anchorPoint/transform и попробуйте:

[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.

Ответ 10

Этот вопрос и ответы вдохновили меня решить мои собственные проблемы с Autolayout и масштабированием, но с scrollviews. Я создал пример моего решения на github:

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

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

Ответ 11

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

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