中断和反转 CAKeyframeAnimation

Posted

技术标签:

【中文标题】中断和反转 CAKeyframeAnimation【英文标题】:Interrupt and reverse CAKeyframeAnimation 【发布时间】:2021-11-15 18:41:14 【问题描述】:

我有一个可以选中或取消选中的复选框视图。我想为复选标记的绘图设置动画,为此,我使用 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;

【问题讨论】:

【参考方案1】:

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

[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 值。

这是对你的类的修改(不更改头文件):

#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

【讨论】:

非常感谢您的帮助。这有效,除非我尝试修改它以在完成后删除动画并显示原始图层,然后我无法让它工作。我已在名为“更新”的部分中发布了问题中的代码。你看到我做错了什么吗?我的印象是,一旦将动画添加到图层中,我就可以在表示层显示动画的同时更改原始图层中的属性。但是,当我在将动画添加到图层后尝试更新 strokeEnd 时,会立即发生这种情况。再次感谢您的帮助。 我想我找到了一种方法来做前面的事情,方法是使用 CATransaction,然后在事务上将 disableActions 设置为 true。然后,在事务内部,我只是为 strokeEnd 设置了所需的最终值,它不会为它设置动画。我不确定这是否是最好的方法,但它确实有效。再次感谢。 我只是想确切地建议一下。没有推荐的方法,但我喜欢按照您的描述进行操作(在禁用动画的 CATransaction 中更新模型)。

以上是关于中断和反转 CAKeyframeAnimation的主要内容,如果未能解决你的问题,请参考以下文章

MicroPython ESP32利用中断控制电机正反转示例

外部按键中断精准控制步进电机起保停,正反转,加减速Arduino+TB6600驱动器)

CAKeyframeAnimation 和音频同步

CAAnimationGroup 与 CAKeyframeAnimation 和 CABasicAnimation

优先级调度优先级反转优先级继承优先级天花板

如何使用 CAKeyframeAnimation 变换旋转检测某些旋转进度?