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。

问题

  1. 有谁知道我的动画代码哪里做错了吗?为什么父视图一开始就跳到左边?我只是想滑出正在删除的视图,然后同时滑过已删除视图右侧的项目。这就是我想要做的。

代码

这是我的代码。动画代码在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];
        }
    }];