【问题标题】:Animating a CALayer's mask size change动画 CALayer 的遮罩尺寸变化
【发布时间】:2011-02-24 01:02:15
【问题描述】:

我有一个UIView 子类,它在其CALayer 上使用CAShapeLayer 掩码。蒙版使用了独特的形状,三个圆角和一个切掉的矩形在剩余的角上。

当我使用标准动画块调整UIView 的大小时,UIView 本身和它的CALayer 调整大小就好了。但是,蒙版会立即应用,这会导致一些绘图问题。

我尝试使用 CABasicAnimation 为蒙版调整大小设置动画,但没有任何运气让调整大小变为动画。

我能否以某种方式在蒙版上实现动画调整大小的效果?我是否需要摆脱面具,或者我是否必须改变我目前绘制面具的方式(使用- (void)drawInContext:(CGContextRef)ctx)。

干杯, 亚历克斯

【问题讨论】:

    标签: iphone objective-c core-animation calayer mask


    【解决方案1】:

    我找到了解决这个问题的方法。其他答案部分正确并且很有帮助。

    以下几点对于理解解决方案很重要:

    • mask 属性本身不可设置动画。
    • 由于蒙版是 CALayer,因此可以自行设置动画。
    • 框架不可动画,使用边界和位置。这可能不适用于您(如果您不尝试为框架设置动画),但对我来说是个问题。 (见Apple QA 1620
    • 视图层的掩码未绑定到 UIView,因此它不会接收应用于视图层的核心动画事务。
    • 我们正在直接修改 CALayer,所以我们不能指望 UIView 会知道我们要做什么,所以 UIView 动画不会创建核心动画事务来包含对我们属性的更改。

    为了解决这个问题,我们将不得不自己利用 Core Animation,而不能依赖 UIView 动画块来为我们完成工作。

    只需创建一个CATransaction,其持续时间与您在[UIView animateWithDuration:...] 中使用的持续时间相同。这将创建一个单独的动画,但如果您的持续时间和缓动函数相同,它应该与动画块中的其他动画完全一致。

    NSTimeInterval duration = 0.5;// match this to the value of the UIView animateWithDuration: call
    
    [CATransaction begin];
    [CATransaction setValue:[NSNumber numberWithFloat:duration] forKey:kCATransactionAnimationDuration];
    
    self.myView.layer.mask.position = CGPointMake(newX, 0);
    self.myView.layer.mask.bounds = CGRectMake(0, 0, newWidth, newHeight);
    
    [CATransaction commit];
    

    【讨论】:

    • @rockyracoon 抱歉,这太旧了,我无法提供完整的示例。如果您有具体问题,我可能会提供帮助。
    【解决方案2】:

    我通过将self.layer.mask 设置为该形状层,使用CAShapeLayer 来掩盖UIView

    为了在视图大小发生变化时为蒙版设置动画,我覆盖了-setBounds:,以便在动画期间更改边界时为蒙版层路径设置动画。

    我是这样实现的:

    - (void)setBounds:(CGRect)bounds
    {
        [super setBounds:bounds];
        CAPropertyAnimation *boundsAnimation = (CABasicAnimation *)[self.layer animationForKey:@"bounds"];
    
        // update the mask
        self.maskLayer.frame = self.layer.bounds;
    
        // if the bounds change happens within an animation, also animate the mask path
        if (!boundsAnimation) {
            self.maskLayer.path = [self createMaskPath];
        } else {
            // copying the original animation allows us to keep all animation settings
            CABasicAnimation *animation = [boundsAnimation copy];
            animation.keyPath = @"path";
    
            CGPathRef newPath = [self createMaskPath];
            animation.fromValue = (id)self.maskLayer.path;
            animation.toValue = (__bridge id)newPath;
    
            self.maskLayer.path = newPath;
    
            [self.maskLayer addAnimation:animation forKey:@"path"];
        }
    }
    

    (例如,self.maskLayer 设置为 `self.layer.mask)

    我的-createMaskPath 计算我用来屏蔽视图的 CGPathRef。我还更新了-layoutSubviews 中的掩码路径。

    【讨论】:

    • 这个被低估的答案救了我的命 +1,我将更新实施以实现 swift
    【解决方案3】:

    CALayer 的 mask 属性是不可动画的,这解释了你在那个方向上缺乏运气。

    蒙版的绘制是否取决于蒙版的框架/边界? (你能提供一些代码吗?)掩码是否设置了 needsDisplayOnBoundsChange 属性?

    干杯, 科林

    【讨论】:

    • 我在这里遇到了同样的问题,是的,蒙版适合视图/图层的边界。它正在添加圆角。属性已设置。
    【解决方案4】:

    要为 UIView 的遮罩层的边界更改设置动画:子类 UIView,并使用 CATransaction 为遮罩设置动画 - 类似于 Kekodas 的答案,但更通用:

    @implementation UIMaskView
    
    - (void) layoutSubviews {
        [super layoutSubviews];
    
        CAAnimation* animation = [self.layer animationForKey:@"bounds"];
        if (animation) {
            [CATransaction begin];
            [CATransaction setAnimationDuration:animation.duration];
        }
    
        self.layer.mask.bounds = self.layer.bounds;
        if (animation) {
            [CATransaction commit];
        }
    }
    
    @end
    

    【讨论】:

    • 密钥应该是bounds.size,而不是bounds
    • 也值得搭配计时功能
    • 这对我有用,除了(与 Kekoa 的建议相反)我必须为框架而不是边界设置动画。不过,这是一个很好的解决方案。
    【解决方案5】:

    mask参数不做动画,但是你可以给设置为mask的图层做动画……

    如果您为 CAShapeLayer 的 Path 属性设置动画,则应该为蒙版设置动画。我可以验证这在我自己的项目中是否有效。不过不确定是否使用非矢量蒙版。您是否尝试过为面具的内容属性设置动画?

    谢谢, 乔恩

    【讨论】:

    • 你能给我示例代码吗?我有 CAShapeLayer ,然后我在上面调用 setPath ,然后我设置为 uiview setMask : shapeLayer ... 现在它作为 calayer 可见。如何访问此路径并更改它?谢谢!
    • 我确实可以为具有蒙版的图层设置动画,但只有在删除将其耦合到 UIView 的委托之后。然后我可以平滑地调整图层的大小。但是,在调整大小时不会更新蒙版(圆角)。视图内容也不会被调整大小。
    【解决方案6】:

    我找不到任何编程解决方案,所以我只绘制了一个具有正确形状和 alpha 值的 png 图像并使用它来代替。这样我就不用戴口罩了……

    【讨论】:

      【解决方案7】:

      可以为蒙版更改设置动画。

      我更喜欢使用 CAShapeLayer 作为遮罩层。借助属性路径对蒙版变化进行动画处理非常方便。

      在动画任何更改之前,将源的内容转储到实例 CGImageRef 中,并为动画创建一个新层。动画期间隐藏原始图层,动画结束时显示。

      以下是在属性路径上创建关键动画的示例代码。 如果您想创建自己的路径动画,请确保路径中的点数始终相同。

      - (CALayer*)_mosaicMergeLayer:(CGRect)bounds content:(CGImageRef)content isUp:(BOOL)isUp {
      
          CALayer* layer = [CALayer layer];
          layer.frame = bounds;
          layer.backgroundColor = [[UIColor clearColor] CGColor];
          layer.contents = (id)content;
      
          CAShapeLayer* maskLayer = [CAShapeLayer layer];
          maskLayer.fillColor = [[UIColor blackColor] CGColor];
          maskLayer.frame = bounds;
          maskLayer.fillRule = kCAFillRuleEvenOdd;
          maskLayer.path = ( isUp ? [self _maskArrowUp:-bounds.size.height*2] : [self _maskArrowDown:bounds.size.height*2] );
          layer.mask = maskLayer;
      
          CAKeyframeAnimation* ani = [CAKeyframeAnimation animationWithKeyPath:@"path"];
          ani.removedOnCompletion = YES;
          ani.duration = 0.3f;
          ani.fillMode = kCAFillModeForwards;
          ani.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
      
          NSArray* values = ( isUp ?
              [NSArray arrayWithObjects:
              (id)[self _maskArrowUp:0],
              (id)[self _maskArrowUp:-ceilf(bounds.size.height*1.2)],
              nil] 
          :       
              [NSArray arrayWithObjects:
              (id)[self _maskArrowDown:0],
              (id)[self _maskArrowDown:bounds.size.height],
              nil] 
          );
          ani.values = values;
          ani.delegate = self;
          [maskLayer addAnimation:ani forKey:nil];
      
          return layer;
      }
      
      - (void)_startMosaicMergeAni:(BOOL)up {
      
          CALayer* overlayer = self.aniLayer;
          CGRect bounds = overlayer.bounds;
          self.firstHalfAni = NO;
      
          CALayer* frontLayer = nil;
          frontLayer = [self _mosaicMergeLayer:bounds 
                                      content:self.toViewSnapshot 
                                      isUp:up];
          overlayer.contents = (id)self.fromViewSnapshot;
          [overlayer addSublayer:frontLayer];
      }
      

      【讨论】:

        【解决方案8】:

        Swift 3+ 答案基于Kekoa's solution

        let duration = 0.15 //match this to the value of the UIView.animate(withDuration:) call
        
        CATransaction.begin()
        CATransaction.setValue(duration, forKey: kCATransactionAnimationDuration)
        
        myView.layer.mask.position = CGPoint(x: [new X], y: [new Y]) //just an example
        
        CATransaction.commit()
        

        【讨论】:

          【解决方案9】:

          @stigi 答案的 Swift 实现,我的遮罩层称为形状层

          override var bounds: CGRect {
              didSet {
                  // debugPrint(self.layer.animationKeys()) //Very useful for know if animation is happening and key name
                  let propertyAnimation = self.layer.animation(forKey: "bounds.size")
          
                  self.shapeLayer?.frame = self.layer.bounds
          
                  // if the bounds change happens within an animation, also animate the mask path
                  if let boundAnimation = propertyAnimation as? CABasicAnimation {
                      // copying the original animation allows us to keep all animation settings
                      if let basicAnimation = boundAnimation.copy() as? CABasicAnimation {
                          basicAnimation.keyPath = "path"
          
                          let newPath = UIBezierPathUtils.customShapePath(rect: self.layer.bounds, cornersTriangleSize: cornerTriangleSize).cgPath
                          basicAnimation.fromValue = self.shapeLayer?.path
                          basicAnimation.toValue = newPath
          
                          self.shapeLayer?.path = newPath
                          self.shapeLayer?.add(basicAnimation, forKey: "path")
                      }
                  } else {
                      self.shapeLayer?.path = UIBezierPathUtils.customShapePath(rect: self.layer.bounds, cornersTriangleSize: cornerTriangleSize).cgPath
                  }
              }
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-04-15
            • 1970-01-01
            • 1970-01-01
            • 2022-08-09
            相关资源
            最近更新 更多