Как настроить точку привязки 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 ответов:
[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: Используйте подвиды
проблема с обоими вышеуказанными решениями заключается в том, что мы теряем преимущества ограничений для позиционирования нашего взгляда. Итак, вот решение, которое решает это. Начните с невидимого представления, чья работа заключается исключительно в том, чтобы выступать в качестве хоста, и используйте ограничения для его размещения. Внутри этого поместите реальный вид в качестве подвида. Используйте ограничения для размещения подвида в представлении узла, но ограничьте эти ограничения ограничениями, которые не будут сопротивляться при применении преобразования.
вот пример:
белый вид-это вид хоста; вы должны притворяться, что он прозрачен и, следовательно, невидим. Красный вид является его подвидом, расположенным путем закрепления его центра на хосте вид в центре. Теперь мы можем масштабировать и вращать красный вид вокруг его центра без каких-либо проблем, и действительно иллюстрация показывает, что мы сделали это:
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: вы можете создать выход для одного из ограничений, чтобы его можно было удалить и добавить обратно.
Я создал новый проект и добавил вид с фиксированным размером в центре. Ограничения показаны на рисунке ниже.
затем я добавил выход для вида, который будет вращаться, и для ограничения выравнивания центра 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; }
это не имеет значения, что кадры для красного вида равны нулю в этой точке, потому что функциями автоматического изменения маски на зеленый вид.
я добавил преобразование вращения на метод действия, и это был результат:
он, похоже, потерял себя во время вращения устройства, поэтому я добавил Это в метод 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 выглядят точно так же. Ничего не изменилось. Просто определение того, что центр относится к изменен.