Как настроить точку привязки CALayer, когда используется Auto Layout?
Примечание: с тех пор, как был задан этот вопрос, дела пошли дальше; смотрите здесь для хорошего недавнего обзора.
Перед автоматическим размещением можно изменить точку привязки слоя вида, не перемещая вид, сохранив рамку, установив точку привязки и восстановив рамку.
В мире автоматической компоновки мы больше не устанавливаем фреймы, но ограничения, похоже, не соответствуют задаче корректировки положения представления туда, где мы хотим его видеть. Вы можете взломать ограничения, чтобы изменить положение своего представления, но при повороте или других событиях изменения размера они снова становятся недействительными.
Следующая яркая идея не работает, так как создает "Неверное сопряжение атрибутов макета (слева и по ширине)":
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 (расстояния Я хочу вставки от левого края надтаблицы).
Можно ли изменить опорную точку, не меняя местоположения вида, в виде с автоматическим макетом? Нужно ли использовать жестко закодированные значения и редактировать ограничение для каждого поворота? Я надеюсь, что нет.
Мне нужно изменить опорную точку, чтобы при применении преобразования к представлению я получал правильный визуальный эффект.
12 ответов
[РЕДАКТИРОВАТЬ: Предупреждение: все последующее обсуждение, возможно, будет устаревшим или, по крайней мере, сильно смягченным iOS 8, которая может больше не совершать ошибку запуска макета во время применения преобразования представления.]
Autolayout vs. View Transforms
Autolayout не очень хорошо работает с трансформациями вида. Причина, насколько я могу заметить, заключается в том, что вы не должны связываться с фреймом представления, имеющего преобразование (отличное от преобразования идентификаторов по умолчанию) - но это именно то, что делает autolayout. Способ автопоставки работает так: layoutSubviews
среда выполнения преодолевает все ограничения и соответственно устанавливает рамки всех представлений.
Другими словами, ограничения не волшебство; они просто список дел. layoutSubviews
где список дел делается. И делает это, устанавливая рамки.
Я не могу не расценивать это как ошибку. Если я применю это преобразование к представлению:
v.transform = CGAffineTransformMakeScale(0.5,0.5);
Я ожидаю увидеть вид с центром в том же месте, что и раньше, и в половине размера. Но в зависимости от его ограничений, это может быть совсем не то, что я вижу.
[На самом деле, здесь есть второй сюрприз: применение преобразования к представлению немедленно запускает компоновку. Мне кажется, это еще одна ошибка. Или, возможно, это сердце первой ошибки. То, что я ожидал бы, - это иметь возможность уйти с преобразованием по крайней мере до времени макета, например, устройство будет повернуто - так же, как я могу сойти с анимацией кадра до времени макета. Но на самом деле время макета является немедленным, что кажется просто неправильным.]
Решение 1: нет ограничений
Одним из текущих решений является, если я собираюсь применить полупостоянное преобразование к представлению (а не просто как-то временно его покачивать), чтобы удалить все ограничения, влияющие на него. К сожалению, это обычно приводит к тому, что представление исчезает с экрана, поскольку автоматическое расположение по-прежнему имеет место, и теперь нет никаких ограничений, указывающих нам, куда поместить представление. Таким образом, в дополнение к удалению ограничений, я установил translatesAutoresizingMaskIntoConstraints
ДА. Представление теперь работает по-старому, фактически не затрагиваемое автоматическим размещением. (Очевидно, что на него влияет autolayout, но неявные ограничения маски с автоматическим изменением размера приводят к тому, что его поведение будет таким же, как и до 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]];
В результате, если у вас нет ограничений, которые влияют на фрейм представления, автоматическая разметка не коснется фрейма вида - это то, что вам нужно, когда задействовано преобразование.
Решение 3. Используйте подпредставление
Проблема с обоими вышеупомянутыми решениями состоит в том, что мы теряем преимущества ограничений для позиционирования нашей точки зрения. Итак, вот решение, которое решает это. Начните с невидимого представления, работа которого состоит исключительно в том, чтобы выступать в роли хоста, и используйте ограничения для его позиционирования. Внутри этого, поместите реальный взгляд как подпредставление. Используйте ограничения, чтобы расположить подпредставление в представлении хоста, но ограничьте эти ограничения ограничениями, которые не будут сопротивляться, когда мы применяем преобразование.
Вот иллюстрация:
Белый вид - это вид хоста; Вы должны делать вид, что оно прозрачное и, следовательно, невидимое. Красный вид - это его подпредставление, расположенное путем закрепления его центра к центру основного вида. Теперь мы можем без проблем масштабировать и вращать красный вид вокруг его центра, и на самом деле иллюстрация показывает, что мы сделали это:
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];
У меня был похожий Isuue, и я только что услышал Back от команды Autolayout в 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
Как вы можете видеть, он захватывает центральную точку просмотра при первом вызове и повторно использует эту позицию в последующих вызовах, чтобы разместить представление соответствующим образом. Это переписывает код Autolayout в том смысле, что он происходит после [super layoutSubviews]; который содержит код разметки.
Таким образом, больше нет необходимости избегать Autolayout, но вы можете создать свою собственную autolayout, когда поведение по умолчанию больше не подходит. Конечно, вы можете применять более сложные вещи, чем в этом примере, но это было все, что мне было нужно, так как мое приложение может использовать только портретный режим.
Я нахожу простой способ. И это работает на 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 + constant
layerView.layer.anchorPoint = CGPoint(1, 0.5)
let centerXConstraint = .....
centerXConstraint.constant = centerXConstraint.constant + layerView.bounds.size.width/2
Если вы используете автоматическую разметку, то я не вижу, как ручная настройка позиции будет служить в долгосрочной перспективе, потому что в конечном итоге автоматическая разметка приведет к засорению значения позиции, которое вы установили, при вычислении собственной разметки.
Скорее, необходимо изменить сами ограничения макета, чтобы компенсировать изменения, произведенные установкой 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, - это установить преобразование. Это потребовало бы более сложной функции, которая обновляет ограничения макета, чтобы отразить все изменения кадра, которые могут быть вызваны самим свойством transform. Это сложно, потому что преобразования могут многое сделать для кадра. Преобразование масштабирования или поворота сделает кадр больше, поэтому нам нужно обновить любые ограничения ширины или высоты и т. Д.
Если вы используете преобразование только для временной анимации, то вышеописанного может быть достаточно, поскольку я не верю, что автоматическая компоновка не позволит анимации в полете представить изображения, которые представляют собой чисто временные нарушения ограничений.
tl:dr: Вы можете создать выход для одного из ограничений, чтобы его можно было удалить и снова добавить.
Я создал новый проект и добавил вид с фиксированным размером в центре. Ограничения показаны на рисунке ниже.
Затем я добавил выход для вида, который будет вращаться, и для ограничения выравнивания центра х.
@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"];
Вращающийся слой выглядит так, как будто он остается в центре (что и должно быть) даже при вращении устройства или иным образом, что заставляет его обновлять ограничения. Новое ограничение и измененная точка привязки визуально отменяют друг друга.
Вдохновленный ответом моего Мэтта, я решил попробовать другой подход. Можно использовать контейнерное представление с соответствующими ограничениями. Вид с модифицированной якорной точкой может быть помещен в поле зрения контейнера, с помощью автоматического изменения маски и явной настройки кадра так же, как в старые времена.
Это работает удовольствие для моей ситуации в любом случае. Представления устанавливаются здесь в 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];
}
Мое текущее решение состоит в том, чтобы вручную отрегулировать положение слоя в 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;
}
}
}
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 выглядят одинаково. Ничего не изменилось. Просто определение того, на что ссылается центр, изменилось.
Этот вопрос и ответы вдохновили меня на решение моих собственных проблем с Autolayout и масштабированием, но с прокруткой. Я создал пример моего решения на github:
https://github.com/hansdesmedt/AutoLayout-scrollview-scale
Это пример UIScrollView с пользовательской страницей, полностью выполненной в AutoLayout и масштабируемой (CATransform3DMakeScale) при длительном нажатии и нажатии для увеличения. iOS 6 и 7 совместимы.
У меня был относительно простой случай, когда мне нужно было установить точку привязки равной
(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)
Я не совсем уверен, как это будет работать с другими значениями, но все же может быть полезно
Я думаю, что вы победили цель автоматического размещения с помощью этого метода. Вы упомянули, что ширина и правый край зависят от суперпредставления, так почему бы просто не добавить ограничения вдоль этого мышления?
Потеряйте парадигму 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
,
Это большая тема, и я не прочитал все комментарии, но столкнулся с той же проблемой.
У меня был вид из XIB с autolayout. И я хотел обновить его свойство transform. Внедрение представления в представление контейнера не решает мою проблему, потому что автоматическое расположение действовало странно на представление контейнера. Вот почему я просто добавил второе контейнерное представление, которое будет содержать контейнерное представление, которое содержит мое представление и применяет к нему преобразования.