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