中断和反转 CAKeyframeAnimation

Interrupt and reverse CAKeyframeAnimation

我有一个可以选中或取消选中的复选框视图。我想为复选标记的绘图设置动画,为此,我正在使用 CAKeyframeAnimation 并在 CAShapeLayer 上绘图。

这很好用,但我也希望能够支持在抽签中撤销检查决定。现在动画的持续时间配置为一秒。因此,如果一个人点击视图并开始绘制复选标记,然后在 0.5 秒的时间点击视图,那么我希望动画停止绘制并开始反转。同样,如果复选标记未被选中并且有人点击它,那么我希望它反转其清除动画并再次开始绘制复选标记。

我只是不确定该怎么做。我不知道使用 CAKeyframeAnimation 是否可行,或者我是否应该使用 UIViewPropertyAnimator 或其他东西,或者我是否可以使用 UIViewPropertyAnimator,因为它是一个视图动画器,我正在 CAShapeLayer 上制作 属性 动画。而且我将复选标记分解为三个部分(一个起始点,复选标记的第一个向下部分,以及整个复选标记),所以我不确定如何使用 UIViewPropertyAnimator 为它制作动画(也许链接动画,但是那么这似乎会使反转动画变得困难。

这是我的代码。有没有人对如何使这个可中断和可逆有任何想法?

MyCheckmarkView.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface MyCheckmarkView : UIView<CAAnimationDelegate>

@end

NS_ASSUME_NONNULL_END

MyCheckmarkView.m

#import "MyCheckmarkView.h"

@interface MyCheckmarkView ()

@property (strong, nonatomic) UIColor *strokeColor;
@property (assign, nonatomic, getter=isChecked) BOOL checked;
@property (strong, nonatomic) CAShapeLayer *contentLayer;

@end


@implementation MyCheckmarkView

+ (Class)layerClass {
    return [CAShapeLayer class];
}


- (instancetype)init {
    return [self initWithFrame:CGRectMake(0, 0, 100, 100)];
}

- (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->_strokeColor = [UIColor colorWithRed: 0.2 green: 0.6 blue: 1 alpha: 1];
    
    CAShapeLayer *backgroundLayer = (CAShapeLayer *)self.layer;
    backgroundLayer.fillColor = nil;
    backgroundLayer.strokeColor = self.strokeColor.CGColor;
    backgroundLayer.lineWidth = 7.88;
    backgroundLayer.miterLimit = 7.88;
    backgroundLayer.lineCap = kCALineCapRound;
    backgroundLayer.lineJoin = kCALineJoinRound;
    
    UIBezierPath* rectanglePath = [UIBezierPath bezierPathWithRoundedRect: CGRectMake(4.95, 4.92, 90, 90) cornerRadius: 22.6];
    backgroundLayer.path = rectanglePath.CGPath;

    [backgroundLayer addSublayer:self.contentLayer];
    
    UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleCheckedState)];
    [self addGestureRecognizer:tapGestureRecognizer];
}

- (CGSize)intrinsicContentSize {
    return CGSizeMake(100, 100);
}



- (void)toggleCheckedState {
    if (self.checked) {
        [self animateToUncheckedState];
    } else {
        [self animateToCheckedState];
    }
    self.checked = ![self isChecked];
}






- (CAShapeLayer *)contentLayer {
    if (!self->_contentLayer) {
        self->_contentLayer = [[CAShapeLayer alloc] init];
        self->_contentLayer.fillColor = nil;
        self->_contentLayer.strokeColor = self.strokeColor.CGColor;
        self->_contentLayer.lineWidth = 7.88;
        self->_contentLayer.miterLimit = 7.88;
        self->_contentLayer.lineCap = kCALineCapRound;
        self->_contentLayer.lineJoin = kCALineJoinRound;
    }
    return self->_contentLayer;
}

- (void)animateToCheckedState {
    UIBezierPath *initialPath = [UIBezierPath bezierPath];
    [initialPath moveToPoint:CGPointMake(25.94, 48.05)];
    [initialPath addLineToPoint:CGPointMake(25.94, 48.05)];

    UIBezierPath *startPath = [UIBezierPath bezierPath];
    [startPath moveToPoint:CGPointMake(25.94, 48.05)];
    [startPath addLineToPoint: CGPointMake(43.81, 65.34)];
    
    UIBezierPath* checkmarkPath = [UIBezierPath bezierPath];
    [checkmarkPath moveToPoint: CGPointMake(25.94, 48.05)];
    [checkmarkPath addLineToPoint: CGPointMake(43.81, 65.34)];
    [checkmarkPath addLineToPoint: CGPointMake(73.94, 34.53)];
    
    
    UIViewPropertyAnimator *animator = [[UIViewPropertyAnimator alloc] init];
    [animator addAnimations:^{}];
    
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"path"];
    animation.duration = 1;
    animation.values = @[
        (id)initialPath.CGPath,
        (id)startPath.CGPath,
        (id)checkmarkPath.CGPath
    ];
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    animation.fillMode = kCAFillModeBoth;
    
    animation.repeatCount = 0;
    animation.autoreverses = NO;
    animation.removedOnCompletion = YES;
    
    [self.contentLayer addAnimation:animation forKey:@"checkmarkAnimation"];
    self.contentLayer.path = checkmarkPath.CGPath;
}


- (void)animateToUncheckedState {
    UIBezierPath *initialPath = [UIBezierPath bezierPath];
    [initialPath moveToPoint:CGPointMake(25.94, 48.05)];
    [initialPath addLineToPoint:CGPointMake(25.94, 48.05)];
    

    UIBezierPath *startPath = [UIBezierPath bezierPath];
    [startPath moveToPoint:CGPointMake(25.94, 48.05)];
    [startPath addLineToPoint: CGPointMake(43.81, 65.34)];
    
    UIBezierPath* checkmarkPath = [UIBezierPath bezierPath];
    [checkmarkPath moveToPoint: CGPointMake(25.94, 48.05)];
    [checkmarkPath addLineToPoint: CGPointMake(43.81, 65.34)];
    [checkmarkPath addLineToPoint: CGPointMake(73.94, 34.53)];

    
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"path"];
    animation.duration = 1;
    animation.values = @[
        (id)checkmarkPath.CGPath,
        (id)startPath.CGPath,
        (id)initialPath.CGPath
    ];
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    animation.fillMode = kCAFillModeBoth;
    
    animation.repeatCount = 0;
    animation.autoreverses = NO;
    animation.removedOnCompletion = YES;
    
    [self.contentLayer addAnimation:animation forKey:@"checkmarkAnimation"];
    self.contentLayer.path = nil;
}

@end

更新

这是我根据@DonMag 的代码更新的动画部分代码。一切正常,除了动画开始时,它似乎首先直接将图层的 strokeEnd 值设置为所需的结束值,然后开始动画。这可能是因为我在方法的最后设置了 strokeEnd 属性,但这样在动画被移除并且 presentationLayer 更新为原始内容层中的值后该值将被保留 属性.

我也尝试过使用 CATransaction 然后在 completionBlock 中设置它,但这只是产生了相反的效果。动画开始并成功完成,但随后动画被删除并短暂显示旧状态然后(出于某种原因)快速动画即使我没有做任何明确动画 属性 (我想这可能是隐式动画 属性?).

但我认为无论如何我都不必使用 CATransaction,因为我将动画添加到层,然后在表示层显示动画时更新 属性。这是不正确的吗?有更好的方法吗?如果可能的话,我希望动画能够被移除,并且原始图层在动画被移除后显示正确的状态。

这是我针对选中和未选中状态的代码。

选中状态的动画

NSTimeInterval animationDuration = 3.0;


// get current strokeEnd value
double f = self.contentLayer.presentationLayer.strokeEnd;

CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];

// animate strokeEnd from current to 1.0
[anim setFromValue:[NSNumber numberWithDouble:f]];
[anim setToValue:[NSNumber numberWithDouble:1.0]];

[anim setDuration:((1.0 - f) * animationDuration)];

[anim setRemovedOnCompletion:YES];

// start animation
[self.contentLayer addAnimation:anim forKey:@"draw"];

// if checkMark was being "un-drawn"
//  remove that animation
[self.contentLayer removeAnimationForKey:@"undraw"];

// update the original "model" layer so that when the animation is
// finished, the updates will persist to the layer
self.contentLayer.strokeEnd = 1.0;

未选中状态的动画


NSTimeInterval animationDuration = 3.0;


// get current strokeEnd value
double f = self.contentLayer.presentationLayer.strokeEnd;

CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];

// animate strokeEnd from current to 0.0
[anim setFromValue:[NSNumber numberWithDouble:f]];
[anim setToValue:[NSNumber numberWithDouble:0.0]];

[anim setDuration:(f * animationDuration)];

[anim setRemovedOnCompletion:YES];

// start animation
[self.contentLayer addAnimation:anim forKey:@"undraw"];

// if checkMark was being "drawn"
//  remove that animation
[self.contentLayer removeAnimationForKey:@"draw"];

// persist the changes to the original layer
self.contentLayer.strokeEnd = 0.0;

首先,我建议您为复选标记使用单一路径:

[checkmarkPath moveToPoint:CGPointMake(25.94, 48.05)];
[checkmarkPath addLineToPoint: CGPointMake(43.81, 65.34)];
[checkmarkPath addLineToPoint: CGPointMake(73.94, 34.53)];

然后我们可以使用 strokeEnd 属性 从 0.0 到 1.0“绘制”它,或者从 1.0 到 0.0“取消绘制”它

接下来,要中断和反转它,我们可以从 .presentationLayer 中获取当前的 .strokeEnd 值并将其用作我们动画的 from 值。

这里是对您的 class 的修改(不更改头文件):

#import "MyCheckmarkView.h"

@interface MyCheckmarkView ()
@property (strong, nonatomic) UIColor *strokeColor;
@property (assign, nonatomic, getter=isChecked) BOOL checked;
@property (strong, nonatomic) CAShapeLayer *contentLayer;
@end


@implementation MyCheckmarkView

+ (Class)layerClass {
    return [CAShapeLayer class];
}


- (instancetype)init {
    return [self initWithFrame:CGRectMake(0, 0, 100, 100)];
}

- (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->_strokeColor = [UIColor colorWithRed: 0.2 green: 0.6 blue: 1 alpha: 1];
    
    CAShapeLayer *backgroundLayer = (CAShapeLayer *)self.layer;
    backgroundLayer.fillColor = nil;
    backgroundLayer.strokeColor = self.strokeColor.CGColor;
    backgroundLayer.lineWidth = 7.88;
    backgroundLayer.miterLimit = 7.88;
    backgroundLayer.lineCap = kCALineCapRound;
    backgroundLayer.lineJoin = kCALineJoinRound;
    
    UIBezierPath* rectanglePath = [UIBezierPath bezierPathWithRoundedRect: CGRectMake(4.95, 4.92, 90, 90) cornerRadius: 22.6];
    backgroundLayer.path = rectanglePath.CGPath;
    
    [backgroundLayer addSublayer:self.contentLayer];
    
    UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleCheckedState)];
    [self addGestureRecognizer:tapGestureRecognizer];
}

- (CGSize)intrinsicContentSize {
    return CGSizeMake(100, 100);
}

- (void)toggleCheckedState {
    if (self.checked) {
        [self animateToUncheckedState];
    } else {
        [self animateToCheckedState];
    }
    self.checked = ![self isChecked];
}

- (void)layoutSubviews {
    [super layoutSubviews];

    // single path for checkmark shape
    UIBezierPath *checkmarkPath = [UIBezierPath bezierPath];

    [checkmarkPath moveToPoint:CGPointMake(25.94, 48.05)];
    [checkmarkPath addLineToPoint: CGPointMake(43.81, 65.34)];
    [checkmarkPath addLineToPoint: CGPointMake(73.94, 34.53)];

    _contentLayer.path = [checkmarkPath CGPath];
    
    // start with strokeEnd at Zero if not checked
    _contentLayer.strokeEnd = self.checked ? 1.0 : 0.0;
}

- (void)animateToCheckedState {
    
    // get current strokeEnd value
    double f = _contentLayer.presentationLayer.strokeEnd;

    CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    
    // animate strokeEnd from current to 1.0
    [anim setFromValue:[NSNumber numberWithDouble:f]];
    [anim setToValue:[NSNumber numberWithDouble:1.0]];

    [anim setDuration:1.0];

    // we're "showing" the checkMark,
    //  so leave it when "finished"
    [anim setRemovedOnCompletion:NO];

    [anim setFillMode:kCAFillModeBoth];

    // start animation
    [self.contentLayer addAnimation:anim forKey:@"draw"];
    
    // if checkMark was being "un-drawn"
    //  remove that animation
    [self.contentLayer removeAnimationForKey:@"undraw"];
    
}

- (void)animateToUncheckedState {

    // get current strokeEnd value
    double f = _contentLayer.presentationLayer.strokeEnd;

    CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];

    // animate strokeEnd from current to 0.0
    [anim setFromValue:[NSNumber numberWithDouble:f]];
    [anim setToValue:[NSNumber numberWithDouble:0.0]];
    
    [anim setDuration:1.0];

    // we're "un-drawing" the checkMark,
    //  so remove it when "finished"
    [anim setRemovedOnCompletion:YES];

    [anim setFillMode:kCAFillModeBoth];

    // start animation
    [self.contentLayer addAnimation:anim forKey:@"undraw"];

    // if checkMark was being "drawn"
    //  remove that animation
    [self.contentLayer removeAnimationForKey:@"draw"];
    
}

- (CAShapeLayer *)contentLayer {
    if (!self->_contentLayer) {
        self->_contentLayer = [[CAShapeLayer alloc] init];
        self->_contentLayer.fillColor = nil;
        self->_contentLayer.strokeColor = self.strokeColor.CGColor;
        self->_contentLayer.lineWidth = 7.88;
        self->_contentLayer.miterLimit = 7.88;
        self->_contentLayer.lineCap = kCALineCapRound;
        self->_contentLayer.lineJoin = kCALineJoinRound;
    }
    return self->_contentLayer;
}

@end