我正在使用一对CALayers作为图像蒙版,允许我以设定的速度为灯泡填充或清空动画,同时也跟随当前的触摸位置。也就是说,一个蒙版跳跃跟随触摸,另一个蒙版滑动到该位置。由于我使用显式动画,因此在添加动画时我不得不设置蒙版滑动蒙版的位置。这意味着如果我开始填充然后在填充完成之前开始空,则空将从完成的填充位置开始(相反的情况也是如此)。
有没有办法获取动画的位置,在动画的每一步设置位置,或让新动画从活动动画的当前状态开始?
处理动画的代码如下:
- (void)masksFillTo:(CGFloat)height {
// Clamp the height we fill to inside the bulb. Remember Y gets bigger going down.
height = MIN(MAX(BULB_TOP, height), BULB_BOTTOM);
// We can find the target Y location by subtracting the Y value for the top of the
// bulb from the height.
CGFloat targetY = height - BULB_TOP;
// Find the bottom of the transparent mask to determine where the solid fill
// is sitting. Then find how far that fill needs to move.
// TODO: This works with the new set position, so overriding old anime doesn't work
CGFloat bottom = transMask.frame.origin.y + transMask.frame.size.height;
// If the target is above the bottom of the solid, we want to fill up.
// This means the empty mask jumps and the transparent mask slides.
CALayer *jumper;
CALayer *slider;
if (bottom - targetY >= 0) {
jumper = emptyMask;
slider = transMask;
// We need to reset the bottom to the emptyMask
bottom = emptyMask.frame.origin.y + emptyMask.frame.size.height;
} else {
jumper = transMask;
slider = emptyMask;
}
[jumper removeAllAnimations];
[slider removeAllAnimations];
CGFloat dy = bottom - targetY;
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
[jumper setPosition:CGPointMake(jumper.position.x, jumper.position.y - dy)];
[self slideMaskFillTo:height withMask:slider]; // Do this inside here or an odd flash glitch appears.
[CATransaction commit];
}
// TODO: Always starts from new position, even if animation hasn't reached it.
- (void)slideMaskFillTo:(CGFloat)height withMask:(CALayer *)slider {
// We can find the target Y location by subtracting the Y value for the top of the
// bulb from the height.
CGFloat targetY = height - BULB_TOP;
// We then find the bottom of the mask.
CGFloat bottom = slider.frame.origin.y + slider.frame.size.height;
CGFloat dy = bottom - targetY;
// Do the animation. Animating with duration doesn't appear to work properly.
// Apparently "When modifying layer properties from threads that don’t have a runloop,
// you must use explicit transactions."
CABasicAnimation *a = [CABasicAnimation animationWithKeyPath:@"position"];
a.duration = (dy > 0 ? dy : -dy) / PXL_PER_SEC; // Should be 2 seconds for a full fill
a.fromValue = [NSValue valueWithCGPoint:slider.position];
CGPoint newPosition = slider.position;
newPosition.y -= dy;
a.toValue = [NSValue valueWithCGPoint:newPosition];
a.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[slider addAnimation:a forKey:@"colorize"];
// Update the actual position
slider.position = newPosition;
}
以及如何调用它的一个例子。请注意,这意味着它可以被称为动画中期。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.view];
[self masksFillTo:point.y];
}
如果有人发现它是相关的,那就是图像和蒙版的创建。
// Instantiate the different bulb images - empty, transparent yellow, and solid yellow. This
// includes setting the frame sizes. This approach found at http://stackoverflow.com/a/11218097/264775
emptyBulb = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Light.png"]];
transBulb = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Light-moving.png"]];
solidBulb = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Light-on.png"]];
[emptyBulb setFrame:CGRectMake(10, BULB_TOP, 300, BULB_HEIGHT)]; // 298 x 280
[transBulb setFrame:CGRectMake(10, BULB_TOP, 300, BULB_HEIGHT)]; // 298 x 280
[solidBulb setFrame:CGRectMake(10, BULB_TOP, 300, BULB_HEIGHT)]; // 298 x 280
[self.view addSubview:solidBulb]; // Empty on top, then trans, then solid.
[self.view addSubview:transBulb];
[self.view addSubview:emptyBulb];
// Create a mask for the empty layer so it will cover the other layers.
emptyMask = [CALayer layer];
[emptyMask setContentsScale:emptyBulb.layer.contentsScale]; // handle retina scaling
[emptyMask setFrame:emptyBulb.layer.bounds];
[emptyMask setBackgroundColor:[UIColor blackColor].CGColor];
emptyBulb.layer.mask = emptyMask;
// Also create a mask for the transparent image.
transMask = [CALayer layer];
[transMask setContentsScale:transBulb.layer.contentsScale]; // handle retina scaling
[transMask setFrame:transBulb.layer.bounds];
[transMask setBackgroundColor:[UIColor blackColor].CGColor];
transBulb.layer.mask = transMask;
刚刚通过this answer找到解决方案。如果查看文档的右侧部分,您将找到以下内容:
- (ID)表示层
返回包含当前事务开始时所有属性的图层副本,并应用任何活动动画。
因此,如果我在首先检查透明遮罩位置(也称为实体级别)之前添加此代码,我会抓住当前的动画位置并可以在填充和填充之间切换。
CALayer *temp;
if ([[transMask animationKeys] count] > 0) {
temp = (CALayer *) [transMask presentationLayer];
[transMask setPosition:[temp position]];
}
if ([[emptyMask animationKeys] count] > 0) {
temp = (CALayer *) [emptyMask presentationLayer];
[emptyMask setPosition:[temp position]];
}
layer.presentation()
每次获得原始图层的副本时,您需要为每个自定义CALayer覆盖init(layer: Any)
。
从当前状态开始的另一种可能方式是使用当前动画的beginTime设置动画的beginTime。它允许以偏移量启动新动画。
在你的情况下它可能是(斯威夫特):
if let currentAnimation = slider.animation(forKey: "colorize") {
let currentTime = CACurrentMediaTime()
let animationElapsedTime = currentAnimation.beginTime + currentAnimation.duration - currentTime
a.beginTime = currentTime - animationElapsedTime
}
然而,这对线性时序非常有用,但对于其他时间,可能很难计算出适当的时间。