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