中断和反转 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驱动器)