AutoLayout 动画滑出视图并滑出其他视图以取代它的位置
AutoLayout animate sliding view out and sliding other views over to take its place
这是 的后续问题。
按照@DonMag 提出的解决方案,其中使用 AutoLayout 限制了视图的水平中心,我有以下 class 并且一切正常,但我正在尝试将动画添加到插入和删除了子视图,但我 运行 遇到了让它正常工作的问题。
下面是一些现在发生的事情的截图。出于某种原因,我认为货架视图向左移动了大约项目宽度的一半。但我不知道为什么要这样做。我的动画代码是否正确,或者是否有更好的方法来处理 AutoLayout 更改的动画?
我问的动画代码在removeArrangedSubview:animated:
方法中。它首先调用 layoutIfNeeded 以在开始动画之前首先刷新所有更改。然后,在动画内部,它将更改应用于约束(它更新剩余子视图的 centerX 约束),然后再次调用 layoutIfNeeded 以应用 AutoLayout 更改并为它们设置动画。
这不是激活这些变化的正确方法吗?但这里有一些屏幕截图显示发生了什么。第一个屏幕截图显示它有七个项目,一切正常。
第二个屏幕截图显示了视图何时因某种原因跳到左侧。我不知道是什么导致了这种情况发生。所有子视图的 centerX 约束都已停用。
其余屏幕截图显示其他子视图动画回到原位。但理想情况下,如果您有五个子视图并删除中间的子视图,那么该子视图左侧的两个子视图将保留在原处。只有右边的两个会动画并滑过以填充中间子视图所在的 space。
问题
- 有谁知道我的动画代码哪里做错了吗?为什么父视图一开始就跳到左边?我只是想滑出正在删除的视图,然后同时滑过已删除视图右侧的项目。这就是我想要做的。
代码
这是我的代码。动画代码在removeArrangedSubview:animated:
方法中。
MyShelf.h
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, MyShelfItemShape) {
MyShelfItemShapeNone = 0,
MyShelfItemShapeCircular
};
@interface MyShelf : UIView
@property (copy, nonatomic, readonly) NSArray<__kindof UIView *> *arrangedSubviews;
@property (assign, nonatomic) CGSize itemSize;
@property (assign, nonatomic) MyShelfItemShape itemShape;
@property (strong, nonatomic) UIColor *itemBorderColor;
@property (assign, nonatomic) CGFloat itemBorderWidth;
@property (assign, nonatomic) CGFloat preferredMinimumSpacing;
@property (assign, nonatomic) CGFloat preferredMaximumSpacing;
#pragma mark - Managing the Horizontal Order of Arranged Subviews
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex inFront:(BOOL)inFront animated:(BOOL)animated;
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex animated:(BOOL)animated;
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex;
- (void)addArrangedSubview:(UIView *)view inFront:(BOOL)inFront animated:(BOOL)animated;
- (void)addArrangedSubview:(UIView *)view animated:(BOOL)animated;
- (void)addArrangedSubview:(UIView *)view;
- (void)removeArrangedSubview:(UIView *)view animated:(BOOL)animated;
- (void)removeArrangedSubview:(UIView *)view;
#pragma mark - Managing the Vertical Order of Arranged Subviews
- (void)bringArrangedSubviewToFront:(UIView *)view;
@end
NS_ASSUME_NONNULL_END
MyShelf.m
#import "MyShelf.h"
@interface MyShelf ()
@property (strong, nonatomic) UIView *positionView;
@property (strong, nonatomic) UIView *framingView;
@property (strong, nonatomic) NSLayoutConstraint *framingViewTrailingConstraint;
@property (strong, nonatomic, readwrite) NSMutableArray<__kindof UIView *> *mutableArrangedSubviews;
@end
@implementation MyShelf
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self initialize];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self initialize];
}
return self;
}
- (void)initialize {
// self.previousFrame = CGRectZero;
// self.spacing = -10;
// self.axis = UILayoutConstraintAxisHorizontal;
// self.alignment = UIStackViewAlignmentCenter;
// self.distribution = UIStackViewDistributionFill;
//self.itemSize = CGSizeZero;
self.itemSize = CGSizeMake(35, 35);
self.itemShape = MyShelfItemShapeNone;
self.itemBorderColor = [UIColor blackColor];
self.itemBorderWidth = 1.0;
self.mutableArrangedSubviews = [[NSMutableArray alloc] init];
self.directionalLayoutMargins = NSDirectionalEdgeInsetsZero;
//framingView will match the bounds of the items and it will look like their superview,
//but it is not the superview of the items
[self addSubview:self.framingView];
//positionView is used for the item position constraints but it is not seen
[self addSubview:self.positionView];
[NSLayoutConstraint activateConstraints:@[
//center the position view vertically with no height
[self.positionView.centerYAnchor constraintEqualToAnchor:self.layoutMarginsGuide.centerYAnchor],
[self.positionView.heightAnchor constraintEqualToConstant:0],
//both the leading and trailing edges of the position view should be inset by 1/2 of the item width
[self.positionView.leadingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.leadingAnchor constant:self.itemSize.width / 2.0],
[self.positionView.trailingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.trailingAnchor constant:-self.itemSize.width / 2.0],
//framing view leading is at the positioning view leading mius 1/2 of the item width
[self.framingView.leadingAnchor constraintEqualToAnchor:self.positionView.leadingAnchor constant:-self.itemSize.width / 2.0],
[self.framingView.topAnchor constraintEqualToAnchor:self.layoutMarginsGuide.topAnchor],
[self.framingView.bottomAnchor constraintEqualToAnchor:self.layoutMarginsGuide.bottomAnchor]
]];
}
- (CGSize)intrinsicContentSize {
return CGSizeMake(self.mutableArrangedSubviews.count * self.itemSize.width, self.itemSize.height);
}
- (void)updateHorizontalPositions {
if (self.mutableArrangedSubviews.count == 0) {
//no items, so all we have to do is to update the framing view
self.framingViewTrailingConstraint.active = NO;
self.framingViewTrailingConstraint = [self.framingView.trailingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.leadingAnchor];
self.framingViewTrailingConstraint.active = YES;
return;
}
//clear the existing centerX constraints
for (NSLayoutConstraint *constraint in self.positionView.constraints) {
if (constraint.firstAttribute == NSLayoutAttributeCenterX || constraint.firstAttribute == NSLayoutAttributeCenterXWithinMargins) {
constraint.active = NO;
}
}
//the first item will be equal to the positionView's leading
UIView *currentItem = [self.mutableArrangedSubviews firstObject];
[NSLayoutConstraint activateConstraints:@[
[currentItem.centerXAnchor constraintEqualToAnchor:self.positionView.leadingAnchor]
]];
// percentage for remaining item spacing
// examples:
// we have 3 items
// item 0 centerX is at leading
// item 1 centerX is at 50%
// item 2 centerX is at 100%
// we have 4 items
// item 0 centerX is at leading
// item 1 centerX is at 33.33%
// item 2 centerX is at 66.66%
// item 3 centerX is at 100%
CGFloat percent = 1.0 / (CGFloat)(self.mutableArrangedSubviews.count - 1);
UIView *previousItem;
for (int x = 1; x < self.mutableArrangedSubviews.count; x++) {
previousItem = currentItem;
currentItem = self.mutableArrangedSubviews[x];
CGFloat currentPercent = percent * x;
//keep items next to each other (left-aligned) when overlap is not needed
[currentItem.centerXAnchor constraintLessThanOrEqualToAnchor:previousItem.centerXAnchor constant:self.itemSize.width].active = YES;
//centerX as a percentage of the positionView width
//note: this method is being used as opposed to the layout anchor API because the layout anchor API does not support setting the multiplier
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:currentItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.positionView
attribute:NSLayoutAttributeTrailing
multiplier:currentPercent
constant:0.0];
//this constraint needs a less-than-required priority so the left-aligned constraint can be enforced
constraint.priority = UILayoutPriorityRequired - 1;
constraint.active = YES;
}
//update the trailing anchor of the framing view to the last shelf item
self.framingViewTrailingConstraint.active = NO;
self.framingViewTrailingConstraint = [self.framingView.trailingAnchor constraintEqualToAnchor:currentItem.trailingAnchor];
self.framingViewTrailingConstraint.active = YES;
}
- (void)addArrangedSubview:(UIView *)view inFront:(BOOL)inFront animated:(BOOL)animated {
[self insertArrangedSubview:view atIndex:self.mutableArrangedSubviews.count inFront:inFront animated:animated];
}
- (void)addArrangedSubview:(UIView *)view animated:(BOOL)animated {
[self addArrangedSubview:view inFront:NO animated:animated];
}
- (void)addArrangedSubview:(UIView *)view {
[self addArrangedSubview:view animated:NO];
}
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex inFront:(BOOL)inFront animated:(BOOL)animated {
CGFloat height = MAX(view.bounds.size.height, view.bounds.size.width);
//if the itemSize is CGSizeZero, then that means to use the size of the provided views
if (!CGSizeEqualToSize(self.itemSize, CGSizeZero)) {
[NSLayoutConstraint activateConstraints:@[
[view.widthAnchor constraintEqualToConstant:self.itemSize.width],
[view.heightAnchor constraintEqualToConstant:self.itemSize.height]
]];
height = MAX(self.itemSize.height, self.itemSize.width);
}
switch (self.itemShape) {
case MyShelfItemShapeNone:
break;
case MyShelfItemShapeCircular:
view.layer.cornerRadius = height / 2.0;
break;
}
view.layer.borderColor = self.itemBorderColor.CGColor;
view.layer.borderWidth = self.itemBorderWidth;
view.translatesAutoresizingMaskIntoConstraints = NO;
[self.mutableArrangedSubviews insertObject:view atIndex:stackIndex];
if (inFront) {
[self.positionView addSubview:view];
} else {
//insert the view as a subview of positionView at index zero so it will be underneath existing items
[self.positionView insertSubview:view atIndex:0];
}
[NSLayoutConstraint activateConstraints:@[
[view.centerYAnchor constraintEqualToAnchor:self.positionView.centerYAnchor]
]];
[self invalidateIntrinsicContentSize];
[self updateHorizontalPositions];
[self updateVerticalPositions];
}
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex animated:(BOOL)animated {
[self insertArrangedSubview:view atIndex:stackIndex inFront:NO animated:animated];
}
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex {
[self insertArrangedSubview:view atIndex:stackIndex animated:NO];
}
- (void)removeArrangedSubview:(UIView *)view animated:(BOOL)animated {
BOOL wasInFront = NO;
if ([self.positionView.subviews lastObject] == view) {
wasInFront = YES;
}
if (animated) {
[self.mutableArrangedSubviews removeObject:view];
[self invalidateIntrinsicContentSize];
//clear the existing centerX constraints
// for (NSLayoutConstraint *constraint in self.positionView.constraints) {
// if (constraint.firstAttribute == NSLayoutAttributeCenterX || constraint.firstAttribute == NSLayoutAttributeCenterXWithinMargins) {
// constraint.active = NO;
// }
// }
[self layoutIfNeeded];
__weak MyShelf *weakSelf = self;
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:^{
view.alpha = 0.0;
view.center = CGPointMake(-weakSelf.itemSize.width, view.center.y);
[weakSelf updateHorizontalPositions];
[weakSelf layoutIfNeeded];
}
completion:^(BOOL finished){
[view removeFromSuperview];
//only reorder the views vertically if the one being removed was the top-most view
if (wasInFront) {
[weakSelf updateVerticalPositions];
}
}];
} else {
[view removeFromSuperview];
[self.mutableArrangedSubviews removeObject:view];
[self invalidateIntrinsicContentSize];
[self updateHorizontalPositions];
//only reorder the views verticall if the one being removed was the top-most view
if (wasInFront) {
[self updateVerticalPositions];
}
}
}
- (void)removeArrangedSubview:(UIView *)view {
[self removeArrangedSubview:view animated:NO];
}
- (NSArray<__kindof UIView *> *)arrangedSubviews {
return self.mutableArrangedSubviews;
}
#pragma mark - Managing the Vertical Order of Arranged Subviews
- (void)updateVerticalPositions {
if (!self.positionView.subviews.count) {
return;
}
//get the view that is on the top and find out what horizontal position it is in
UIView *topView = [self.positionView.subviews lastObject];
NSUInteger horizontalIndex = [self.mutableArrangedSubviews indexOfObject:topView];
for (NSInteger x = horizontalIndex - 1; x >= 0; x--) {
UIView *view = self.mutableArrangedSubviews[x];
[self.positionView sendSubviewToBack:view];
}
for (NSInteger x = horizontalIndex + 1; x < self.mutableArrangedSubviews.count; x++) {
UIView *view = self.mutableArrangedSubviews[x];
[self.positionView sendSubviewToBack:view];
}
}
- (void)bringArrangedSubviewToFront:(UIView *)view {
[self.positionView bringSubviewToFront:view];
[self updateVerticalPositions];
}
- (UIView *)framingView {
if (!self->_framingView) {
self->_framingView = [[UIView alloc] init];
self->_framingView.translatesAutoresizingMaskIntoConstraints = NO;
self->_framingView.backgroundColor = [UIColor systemYellowColor];
}
return self->_framingView;
}
- (UIView *)positionView {
if (!self->_positionView) {
self->_positionView = [[UIView alloc] init];
self->_positionView.translatesAutoresizingMaskIntoConstraints = NO;
self->_positionView.backgroundColor = nil;
}
return self->_positionView;
}
- (NSLayoutConstraint *)framingViewTrailingConstraint {
if (!self->_framingViewTrailingConstraint) {
self->_framingViewTrailingConstraint = [self.framingView.trailingAnchor constraintEqualToAnchor:self.positionView.leadingAnchor];
self->_framingViewTrailingConstraint.priority = UILayoutPriorityRequired;
}
return self->_framingViewTrailingConstraint;
}
@end
我不是苹果工程师,所以我不知道ins-and-outs,但我经常看到它。
作为一般规则 ...在动画约束时,我们希望允许auto-layout从“自上而下”的角度管理视图层次结构。
如果您更改约束并将子视图告诉 layoutIfNeeded
,那么 superview
要么没有意识到发生了什么,要么时间导致了问题。
如果将动画块更改为 [weakSelf.superview layoutIfNeeded];
应该可以解决“跳跃”问题:
__weak MyShelf *weakSelf = self;
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:^{
view.alpha = 0.0;
view.center = CGPointMake(-weakSelf.itemSize.width, view.center.y);
[weakSelf updateHorizontalPositions];
// tell the superview to initiate auto-layout updates
//[weakSelf layoutIfNeeded];
[weakSelf.superview layoutIfNeeded];
} completion:^(BOOL finished){
[view removeFromSuperview];
//only reorder the views vertically if the one being removed was the top-most view
if (wasInFront) {
[weakSelf updateVerticalPositions];
}
}];
这是
按照@DonMag 提出的解决方案,其中使用 AutoLayout 限制了视图的水平中心,我有以下 class 并且一切正常,但我正在尝试将动画添加到插入和删除了子视图,但我 运行 遇到了让它正常工作的问题。
下面是一些现在发生的事情的截图。出于某种原因,我认为货架视图向左移动了大约项目宽度的一半。但我不知道为什么要这样做。我的动画代码是否正确,或者是否有更好的方法来处理 AutoLayout 更改的动画?
我问的动画代码在removeArrangedSubview:animated:
方法中。它首先调用 layoutIfNeeded 以在开始动画之前首先刷新所有更改。然后,在动画内部,它将更改应用于约束(它更新剩余子视图的 centerX 约束),然后再次调用 layoutIfNeeded 以应用 AutoLayout 更改并为它们设置动画。
这不是激活这些变化的正确方法吗?但这里有一些屏幕截图显示发生了什么。第一个屏幕截图显示它有七个项目,一切正常。
第二个屏幕截图显示了视图何时因某种原因跳到左侧。我不知道是什么导致了这种情况发生。所有子视图的 centerX 约束都已停用。
其余屏幕截图显示其他子视图动画回到原位。但理想情况下,如果您有五个子视图并删除中间的子视图,那么该子视图左侧的两个子视图将保留在原处。只有右边的两个会动画并滑过以填充中间子视图所在的 space。
问题
- 有谁知道我的动画代码哪里做错了吗?为什么父视图一开始就跳到左边?我只是想滑出正在删除的视图,然后同时滑过已删除视图右侧的项目。这就是我想要做的。
代码
这是我的代码。动画代码在removeArrangedSubview:animated:
方法中。
MyShelf.h
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, MyShelfItemShape) {
MyShelfItemShapeNone = 0,
MyShelfItemShapeCircular
};
@interface MyShelf : UIView
@property (copy, nonatomic, readonly) NSArray<__kindof UIView *> *arrangedSubviews;
@property (assign, nonatomic) CGSize itemSize;
@property (assign, nonatomic) MyShelfItemShape itemShape;
@property (strong, nonatomic) UIColor *itemBorderColor;
@property (assign, nonatomic) CGFloat itemBorderWidth;
@property (assign, nonatomic) CGFloat preferredMinimumSpacing;
@property (assign, nonatomic) CGFloat preferredMaximumSpacing;
#pragma mark - Managing the Horizontal Order of Arranged Subviews
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex inFront:(BOOL)inFront animated:(BOOL)animated;
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex animated:(BOOL)animated;
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex;
- (void)addArrangedSubview:(UIView *)view inFront:(BOOL)inFront animated:(BOOL)animated;
- (void)addArrangedSubview:(UIView *)view animated:(BOOL)animated;
- (void)addArrangedSubview:(UIView *)view;
- (void)removeArrangedSubview:(UIView *)view animated:(BOOL)animated;
- (void)removeArrangedSubview:(UIView *)view;
#pragma mark - Managing the Vertical Order of Arranged Subviews
- (void)bringArrangedSubviewToFront:(UIView *)view;
@end
NS_ASSUME_NONNULL_END
MyShelf.m
#import "MyShelf.h"
@interface MyShelf ()
@property (strong, nonatomic) UIView *positionView;
@property (strong, nonatomic) UIView *framingView;
@property (strong, nonatomic) NSLayoutConstraint *framingViewTrailingConstraint;
@property (strong, nonatomic, readwrite) NSMutableArray<__kindof UIView *> *mutableArrangedSubviews;
@end
@implementation MyShelf
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self initialize];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self initialize];
}
return self;
}
- (void)initialize {
// self.previousFrame = CGRectZero;
// self.spacing = -10;
// self.axis = UILayoutConstraintAxisHorizontal;
// self.alignment = UIStackViewAlignmentCenter;
// self.distribution = UIStackViewDistributionFill;
//self.itemSize = CGSizeZero;
self.itemSize = CGSizeMake(35, 35);
self.itemShape = MyShelfItemShapeNone;
self.itemBorderColor = [UIColor blackColor];
self.itemBorderWidth = 1.0;
self.mutableArrangedSubviews = [[NSMutableArray alloc] init];
self.directionalLayoutMargins = NSDirectionalEdgeInsetsZero;
//framingView will match the bounds of the items and it will look like their superview,
//but it is not the superview of the items
[self addSubview:self.framingView];
//positionView is used for the item position constraints but it is not seen
[self addSubview:self.positionView];
[NSLayoutConstraint activateConstraints:@[
//center the position view vertically with no height
[self.positionView.centerYAnchor constraintEqualToAnchor:self.layoutMarginsGuide.centerYAnchor],
[self.positionView.heightAnchor constraintEqualToConstant:0],
//both the leading and trailing edges of the position view should be inset by 1/2 of the item width
[self.positionView.leadingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.leadingAnchor constant:self.itemSize.width / 2.0],
[self.positionView.trailingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.trailingAnchor constant:-self.itemSize.width / 2.0],
//framing view leading is at the positioning view leading mius 1/2 of the item width
[self.framingView.leadingAnchor constraintEqualToAnchor:self.positionView.leadingAnchor constant:-self.itemSize.width / 2.0],
[self.framingView.topAnchor constraintEqualToAnchor:self.layoutMarginsGuide.topAnchor],
[self.framingView.bottomAnchor constraintEqualToAnchor:self.layoutMarginsGuide.bottomAnchor]
]];
}
- (CGSize)intrinsicContentSize {
return CGSizeMake(self.mutableArrangedSubviews.count * self.itemSize.width, self.itemSize.height);
}
- (void)updateHorizontalPositions {
if (self.mutableArrangedSubviews.count == 0) {
//no items, so all we have to do is to update the framing view
self.framingViewTrailingConstraint.active = NO;
self.framingViewTrailingConstraint = [self.framingView.trailingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.leadingAnchor];
self.framingViewTrailingConstraint.active = YES;
return;
}
//clear the existing centerX constraints
for (NSLayoutConstraint *constraint in self.positionView.constraints) {
if (constraint.firstAttribute == NSLayoutAttributeCenterX || constraint.firstAttribute == NSLayoutAttributeCenterXWithinMargins) {
constraint.active = NO;
}
}
//the first item will be equal to the positionView's leading
UIView *currentItem = [self.mutableArrangedSubviews firstObject];
[NSLayoutConstraint activateConstraints:@[
[currentItem.centerXAnchor constraintEqualToAnchor:self.positionView.leadingAnchor]
]];
// percentage for remaining item spacing
// examples:
// we have 3 items
// item 0 centerX is at leading
// item 1 centerX is at 50%
// item 2 centerX is at 100%
// we have 4 items
// item 0 centerX is at leading
// item 1 centerX is at 33.33%
// item 2 centerX is at 66.66%
// item 3 centerX is at 100%
CGFloat percent = 1.0 / (CGFloat)(self.mutableArrangedSubviews.count - 1);
UIView *previousItem;
for (int x = 1; x < self.mutableArrangedSubviews.count; x++) {
previousItem = currentItem;
currentItem = self.mutableArrangedSubviews[x];
CGFloat currentPercent = percent * x;
//keep items next to each other (left-aligned) when overlap is not needed
[currentItem.centerXAnchor constraintLessThanOrEqualToAnchor:previousItem.centerXAnchor constant:self.itemSize.width].active = YES;
//centerX as a percentage of the positionView width
//note: this method is being used as opposed to the layout anchor API because the layout anchor API does not support setting the multiplier
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:currentItem
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.positionView
attribute:NSLayoutAttributeTrailing
multiplier:currentPercent
constant:0.0];
//this constraint needs a less-than-required priority so the left-aligned constraint can be enforced
constraint.priority = UILayoutPriorityRequired - 1;
constraint.active = YES;
}
//update the trailing anchor of the framing view to the last shelf item
self.framingViewTrailingConstraint.active = NO;
self.framingViewTrailingConstraint = [self.framingView.trailingAnchor constraintEqualToAnchor:currentItem.trailingAnchor];
self.framingViewTrailingConstraint.active = YES;
}
- (void)addArrangedSubview:(UIView *)view inFront:(BOOL)inFront animated:(BOOL)animated {
[self insertArrangedSubview:view atIndex:self.mutableArrangedSubviews.count inFront:inFront animated:animated];
}
- (void)addArrangedSubview:(UIView *)view animated:(BOOL)animated {
[self addArrangedSubview:view inFront:NO animated:animated];
}
- (void)addArrangedSubview:(UIView *)view {
[self addArrangedSubview:view animated:NO];
}
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex inFront:(BOOL)inFront animated:(BOOL)animated {
CGFloat height = MAX(view.bounds.size.height, view.bounds.size.width);
//if the itemSize is CGSizeZero, then that means to use the size of the provided views
if (!CGSizeEqualToSize(self.itemSize, CGSizeZero)) {
[NSLayoutConstraint activateConstraints:@[
[view.widthAnchor constraintEqualToConstant:self.itemSize.width],
[view.heightAnchor constraintEqualToConstant:self.itemSize.height]
]];
height = MAX(self.itemSize.height, self.itemSize.width);
}
switch (self.itemShape) {
case MyShelfItemShapeNone:
break;
case MyShelfItemShapeCircular:
view.layer.cornerRadius = height / 2.0;
break;
}
view.layer.borderColor = self.itemBorderColor.CGColor;
view.layer.borderWidth = self.itemBorderWidth;
view.translatesAutoresizingMaskIntoConstraints = NO;
[self.mutableArrangedSubviews insertObject:view atIndex:stackIndex];
if (inFront) {
[self.positionView addSubview:view];
} else {
//insert the view as a subview of positionView at index zero so it will be underneath existing items
[self.positionView insertSubview:view atIndex:0];
}
[NSLayoutConstraint activateConstraints:@[
[view.centerYAnchor constraintEqualToAnchor:self.positionView.centerYAnchor]
]];
[self invalidateIntrinsicContentSize];
[self updateHorizontalPositions];
[self updateVerticalPositions];
}
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex animated:(BOOL)animated {
[self insertArrangedSubview:view atIndex:stackIndex inFront:NO animated:animated];
}
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex {
[self insertArrangedSubview:view atIndex:stackIndex animated:NO];
}
- (void)removeArrangedSubview:(UIView *)view animated:(BOOL)animated {
BOOL wasInFront = NO;
if ([self.positionView.subviews lastObject] == view) {
wasInFront = YES;
}
if (animated) {
[self.mutableArrangedSubviews removeObject:view];
[self invalidateIntrinsicContentSize];
//clear the existing centerX constraints
// for (NSLayoutConstraint *constraint in self.positionView.constraints) {
// if (constraint.firstAttribute == NSLayoutAttributeCenterX || constraint.firstAttribute == NSLayoutAttributeCenterXWithinMargins) {
// constraint.active = NO;
// }
// }
[self layoutIfNeeded];
__weak MyShelf *weakSelf = self;
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:^{
view.alpha = 0.0;
view.center = CGPointMake(-weakSelf.itemSize.width, view.center.y);
[weakSelf updateHorizontalPositions];
[weakSelf layoutIfNeeded];
}
completion:^(BOOL finished){
[view removeFromSuperview];
//only reorder the views vertically if the one being removed was the top-most view
if (wasInFront) {
[weakSelf updateVerticalPositions];
}
}];
} else {
[view removeFromSuperview];
[self.mutableArrangedSubviews removeObject:view];
[self invalidateIntrinsicContentSize];
[self updateHorizontalPositions];
//only reorder the views verticall if the one being removed was the top-most view
if (wasInFront) {
[self updateVerticalPositions];
}
}
}
- (void)removeArrangedSubview:(UIView *)view {
[self removeArrangedSubview:view animated:NO];
}
- (NSArray<__kindof UIView *> *)arrangedSubviews {
return self.mutableArrangedSubviews;
}
#pragma mark - Managing the Vertical Order of Arranged Subviews
- (void)updateVerticalPositions {
if (!self.positionView.subviews.count) {
return;
}
//get the view that is on the top and find out what horizontal position it is in
UIView *topView = [self.positionView.subviews lastObject];
NSUInteger horizontalIndex = [self.mutableArrangedSubviews indexOfObject:topView];
for (NSInteger x = horizontalIndex - 1; x >= 0; x--) {
UIView *view = self.mutableArrangedSubviews[x];
[self.positionView sendSubviewToBack:view];
}
for (NSInteger x = horizontalIndex + 1; x < self.mutableArrangedSubviews.count; x++) {
UIView *view = self.mutableArrangedSubviews[x];
[self.positionView sendSubviewToBack:view];
}
}
- (void)bringArrangedSubviewToFront:(UIView *)view {
[self.positionView bringSubviewToFront:view];
[self updateVerticalPositions];
}
- (UIView *)framingView {
if (!self->_framingView) {
self->_framingView = [[UIView alloc] init];
self->_framingView.translatesAutoresizingMaskIntoConstraints = NO;
self->_framingView.backgroundColor = [UIColor systemYellowColor];
}
return self->_framingView;
}
- (UIView *)positionView {
if (!self->_positionView) {
self->_positionView = [[UIView alloc] init];
self->_positionView.translatesAutoresizingMaskIntoConstraints = NO;
self->_positionView.backgroundColor = nil;
}
return self->_positionView;
}
- (NSLayoutConstraint *)framingViewTrailingConstraint {
if (!self->_framingViewTrailingConstraint) {
self->_framingViewTrailingConstraint = [self.framingView.trailingAnchor constraintEqualToAnchor:self.positionView.leadingAnchor];
self->_framingViewTrailingConstraint.priority = UILayoutPriorityRequired;
}
return self->_framingViewTrailingConstraint;
}
@end
我不是苹果工程师,所以我不知道ins-and-outs,但我经常看到它。
作为一般规则 ...在动画约束时,我们希望允许auto-layout从“自上而下”的角度管理视图层次结构。
如果您更改约束并将子视图告诉 layoutIfNeeded
,那么 superview
要么没有意识到发生了什么,要么时间导致了问题。
如果将动画块更改为 [weakSelf.superview layoutIfNeeded];
应该可以解决“跳跃”问题:
__weak MyShelf *weakSelf = self;
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowAnimatedContent|UIViewAnimationOptionAllowUserInteraction
animations:^{
view.alpha = 0.0;
view.center = CGPointMake(-weakSelf.itemSize.width, view.center.y);
[weakSelf updateHorizontalPositions];
// tell the superview to initiate auto-layout updates
//[weakSelf layoutIfNeeded];
[weakSelf.superview layoutIfNeeded];
} completion:^(BOOL finished){
[view removeFromSuperview];
//only reorder the views vertically if the one being removed was the top-most view
if (wasInFront) {
[weakSelf updateVerticalPositions];
}
}];