在两个贝塞尔路径形状之间制作动画

Posted

技术标签:

【中文标题】在两个贝塞尔路径形状之间制作动画【英文标题】:Animating between two bezier path shapes 【发布时间】:2015-02-10 16:42:04 【问题描述】:

我发现在两种状态之间为UIImageView 设置动画很棘手:它的原始矩形框架和使用UIBezierPath 创建的新形状。提到了许多不同的技术,其中大部分对我不起作用。

首先是意识到使用 UIView 块动画是行不通的;显然不能在animateWithDuration: 块中执行子层动画。 (见here和here)

剩下的是CAAnimation,还有像CABasicAnimation 这样的具体子类。我很快意识到,不能从没有CAShapeLayer 的视图到有的视图(例如,参见here)。

它们不能只是任意两个形状层路径,而是“只有在从喜欢到喜欢的动画时才能保证为形状层的路径设置动画”(请参阅​​here)

有了这些,就会出现更普通的问题,比如 fromValuetoValue 使用什么(它们应该是 CAShapeLayer 还是 CGPath?),添加动画到什么( layer,或mask?)等

似乎有很多变数;哪种组合可以提供我正在寻找的动画?

【问题讨论】:

【参考方案1】:

第一个重点是类似地构造两条贝塞尔路径,因此矩形是更复杂形状的(微不足道的)模拟。

// the complex bezier path
let initialPoint = CGPoint(x: 0, y: 0)
let curveStart = CGPoint(x: 0, y: (rect.size.height) * (0.2))
let curveControl = CGPoint(x: (rect.size.width) * (0.6), y: (rect.size.height) * (0.5))
let curveEnd = CGPoint(x: 0, y: (rect.size.height) * (0.8))
let firstCorner = CGPoint(x: 0, y: rect.size.height)
let secondCorner = CGPoint(x: rect.size.width, y: rect.size.height)
let thirdCorner = CGPoint(x: rect.size.width, y: 0)

var myBezierArc = UIBezierPath()
myBezierArc.moveToPoint(initialPoint)
myBezierArc.addLineToPoint(curveStart)
myBezierArc.addQuadCurveToPoint(curveEnd, controlPoint: curveControl)
myBezierArc.addLineToPoint(firstCorner)
myBezierArc.addLineToPoint(secondCorner)
myBezierArc.addLineToPoint(thirdCorner)

创建矩形的更简单的“平凡”贝塞尔路径完全相同,但设置了controlPoint,使其看起来不存在:

let curveControl = CGPoint(x: 0, y: (rect.size.height) * (0.5))

(尝试删除addQuadCurveToPoint 行以获得非常奇怪的动画!)

最后,动画命令:

let myAnimation = CABasicAnimation(keyPath: "path")

if (isArcVisible == true) 
    myAnimation.fromValue = myBezierArc.CGPath
    myAnimation.toValue = myBezierTrivial.CGPath
 else 
    myAnimation.fromValue = myBezierTrivial.CGPath
    myAnimation.toValue = myBezierArc.CGPath
       
myAnimation.duration = 0.4
myAnimation.fillMode = kCAFillModeForwards
myAnimation.removedOnCompletion = false

myImageView.layer.mask.addAnimation(myAnimation, forKey: "animatePath")

如果有人有兴趣,项目是here。

【讨论】:

感谢该示例以及您问题中的所有链接。阅读有关此问题的讨论会很有趣且内容丰富。 如何在动画命令中设置tovalue(myBezierTrivial.CGPath)?【参考方案2】:

另一种方法是使用显示链接。它就像一个计时器,只是它与显示的更新相协调。然后,您可以让显示链接的处理程序根据动画的任何特定点的外观来修改视图。

例如,如果您想要将蒙版角的圆角从 0 点设置为 50 点,您可以执行以下操作,其中 percent 是介于 0.0 和 1.0 之间的值,表示动画的百分比完成:

let path = UIBezierPath(rect: imageView.bounds)
let mask = CAShapeLayer()
mask.path = path.CGPath
imageView.layer.mask = mask

let animation = AnimationDisplayLink(duration: 0.5)  percent in
    let cornerRadius = percent * 50.0
    let path = UIBezierPath(roundedRect: self.imageView.bounds, cornerRadius: cornerRadius)
    mask.path = path.CGPath

地点:

class AnimationDisplayLink : NSObject 
    var animationDuration: CGFloat
    var animationHandler: (percent: CGFloat) -> ()
    var completionHandler: (() -> ())?

    private var startTime: CFAbsoluteTime!
    private var displayLink: CADisplayLink!

    init(duration: CGFloat, animationHandler: (percent: CGFloat)->(), completionHandler: (()->())? = nil) 
        animationDuration = duration
        self.animationHandler = animationHandler
        self.completionHandler = completionHandler

        super.init()

        startDisplayLink()
    

    private func startDisplayLink () 
        startTime = CFAbsoluteTimeGetCurrent()
        displayLink = CADisplayLink(target: self, selector: "handleDisplayLink:")
        displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
    

    private func stopDisplayLink() 
        displayLink.invalidate()
        displayLink = nil
    

    func handleDisplayLink(displayLink: CADisplayLink) 
        let elapsed = CFAbsoluteTimeGetCurrent() - startTime
        var percent = CGFloat(elapsed) / animationDuration

        if percent >= 1.0 
            stopDisplayLink()
            animationHandler(percent: 1.0)
            completionHandler?()
         else 
            animationHandler(percent: percent)
        
    

显示链接方法的优点是它可以用于动画一些原本无法动画化的属性。它还可以让您精确地指示动画期间的临时状态。

如果您可以使用CAAnimationUIKit 基于块的动画,那可能就是要走的路。但显示链接有时可能是一种很好的后备方法。

【讨论】:

这可能有一天会派上用场,当CAAnimationUIKit 无法处理某些事情时!【参考方案3】:

我受到您的示例的启发,尝试使用您的答案和一些链接中提到的技术来制作圆形到方形的动画。我打算将其扩展为更通用的圆形到多边形动画,但目前它仅适用于正方形。我有一个名为 RDPolyCircle 的类(CAShapeLayer 的一个子类)来完成繁重的工作。这是它的代码,

@interface RDPolyCircle ()
@property (strong,nonatomic) UIBezierPath *polyPath;
@property (strong,nonatomic) UIBezierPath *circlePath;
@end

@implementation RDPolyCircle 
    double cpDelta;
    double cosR;


-(instancetype)initWithFrame:(CGRect) frame numberOfSides:(NSInteger)sides isPointUp:(BOOL) isUp isInitiallyCircle:(BOOL) isCircle 
    if (self = [super init]) 
        self.frame = frame;
        _isPointUp = isUp;
        _isExpandedPolygon = !isCircle;

        double radius = (frame.size.width/2.0);
        cosR = sin(45 * M_PI/180.0) * radius;
        double fractionAlongTangent = 4.0*(sqrt(2)-1)/3.0;
        cpDelta = fractionAlongTangent * radius * sin(45 * M_PI/180.0);

        _circlePath = [self createCirclePathForFrame:frame];
        _polyPath = [self createPolygonPathForFrame:frame numberOfSides:sides];
        self.path = (isCircle)? self.circlePath.CGPath : self.polyPath.CGPath;
        self.fillColor = [UIColor clearColor].CGColor;
        self.strokeColor = [UIColor blueColor].CGColor;
        self.lineWidth = 6.0;
    
    return self;



-(UIBezierPath *)createCirclePathForFrame:(CGRect) frame 

    CGPoint ctr = CGPointMake(frame.origin.x + frame.size.width/2.0, frame.origin.y + frame.size.height/2.0);

    // create a circle using 4 arcs, with the first one symmetrically spanning the y-axis
    CGPoint leftUpper = CGPointMake(ctr.x - cosR, ctr.y - cosR);
    CGPoint cp1 = CGPointMake(leftUpper.x + cpDelta, leftUpper.y - cpDelta);

    CGPoint rightUpper = CGPointMake(ctr.x + cosR, ctr.y - cosR);
    CGPoint cp2 = CGPointMake(rightUpper.x - cpDelta, rightUpper.y - cpDelta);
    CGPoint cp3 = CGPointMake(rightUpper.x + cpDelta, rightUpper.y + cpDelta);

    CGPoint rightLower = CGPointMake(ctr.x + cosR, ctr.y + cosR);
    CGPoint cp4 = CGPointMake(rightLower.x + cpDelta, rightLower.y - cpDelta);
    CGPoint cp5 = CGPointMake(rightLower.x - cpDelta, rightLower.y + cpDelta);

    CGPoint leftLower = CGPointMake(ctr.x - cosR, ctr.y + cosR);
    CGPoint cp6 = CGPointMake(leftLower.x + cpDelta, leftLower.y + cpDelta);
    CGPoint cp7 = CGPointMake(leftLower.x - cpDelta, leftLower.y - cpDelta);
    CGPoint cp8 = CGPointMake(leftUpper.x - cpDelta, leftUpper.y + cpDelta);

    UIBezierPath *circle = [UIBezierPath bezierPath];
    [circle moveToPoint:leftUpper];
    [circle addCurveToPoint:rightUpper controlPoint1:cp1  controlPoint2:cp2];
    [circle addCurveToPoint:rightLower controlPoint1:cp3 controlPoint2:cp4];
    [circle addCurveToPoint:leftLower controlPoint1:cp5 controlPoint2:cp6];
    [circle addCurveToPoint:leftUpper controlPoint1:cp7 controlPoint2:cp8];
    [circle closePath];
    circle.lineCapStyle = kCGLineCapRound;
    return circle;



-(UIBezierPath *)createPolygonPathForFrame:(CGRect) frame numberOfSides:(NSInteger) sides 
    CGPoint leftUpper = CGPointMake(self.frame.origin.x, self.frame.origin.y);
    CGPoint cp1 = CGPointMake(leftUpper.x + cpDelta, leftUpper.y);

    CGPoint rightUpper = CGPointMake(self.frame.origin.x + self.frame.size.width, self.frame.origin.y);
    CGPoint cp2 = CGPointMake(rightUpper.x - cpDelta, rightUpper.y);
    CGPoint cp3 = CGPointMake(rightUpper.x, rightUpper.y + cpDelta);

    CGPoint rightLower = CGPointMake(self.frame.origin.x + self.frame.size.width, self.frame.origin.y + self.frame.size.height);
    CGPoint cp4 = CGPointMake(rightLower.x , rightLower.y - cpDelta);
    CGPoint cp5 = CGPointMake(rightLower.x - cpDelta, rightLower.y);

    CGPoint leftLower = CGPointMake(self.frame.origin.x, self.frame.origin.y + self.frame.size.height);
    CGPoint cp6 = CGPointMake(leftLower.x + cpDelta, leftLower.y);
    CGPoint cp7 = CGPointMake(leftLower.x, leftLower.y - cpDelta);
    CGPoint cp8 = CGPointMake(leftUpper.x, leftUpper.y + cpDelta);

    UIBezierPath *square = [UIBezierPath bezierPath];
    [square moveToPoint:leftUpper];
    [square addCurveToPoint:rightUpper controlPoint1:cp1  controlPoint2:cp2];
    [square addCurveToPoint:rightLower controlPoint1:cp3 controlPoint2:cp4];
    [square addCurveToPoint:leftLower controlPoint1:cp5 controlPoint2:cp6];
    [square addCurveToPoint:leftUpper controlPoint1:cp7 controlPoint2:cp8];
    [square closePath];
    square.lineCapStyle = kCGLineCapRound;
    return square;



-(void)toggleShape 
    if (self.isExpandedPolygon) 
        [self restore];
    else

        CABasicAnimation *expansionAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
        expansionAnimation.fromValue = (__bridge id)(self.circlePath.CGPath);
        expansionAnimation.toValue = (__bridge id)(self.polyPath.CGPath);
        expansionAnimation.duration = 0.5;
        expansionAnimation.fillMode = kCAFillModeForwards;
        expansionAnimation.removedOnCompletion = NO;

        [self addAnimation:expansionAnimation forKey:@"Expansion"];
        self.isExpandedPolygon = YES;
    



-(void)restore 
    CABasicAnimation *contractionAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
    contractionAnimation.fromValue = (__bridge id)(self.polyPath.CGPath);
    contractionAnimation.toValue = (__bridge id)(self.circlePath.CGPath);
    contractionAnimation.duration = 0.5;
    contractionAnimation.fillMode = kCAFillModeForwards;
    contractionAnimation.removedOnCompletion = NO;

    [self addAnimation:contractionAnimation forKey:@"Contraction"];
    self.isExpandedPolygon = NO;

从视图控制器,我创建了这个层的一个实例,并将它添加到一个简单的视图层,然后在按钮按下时制作动画,

#import "ViewController.h"
#import "RDPolyCircle.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *circleView; // a plain UIView 150 x 150 centered in the superview
@property (strong,nonatomic) RDPolyCircle *shapeLayer;
@end

@implementation ViewController

- (void)viewDidLoad 
    [super viewDidLoad];

    // currently isPointUp and numberOfSides are not implemented (the shape created has numberOfSides=4 and isPointUp=NO)
    // isInitiallyCircle is implemented
    self.shapeLayer = [[RDPolyCircle alloc] initWithFrame:self.circleView.bounds numberOfSides: 4 isPointUp:NO isInitiallyCircle:YES];
    [self.circleView.layer addSublayer:self.shapeLayer];



- (IBAction)toggleShape:(UIButton *)sender 
    [self.shapeLayer toggleShape];

项目可以在这里找到,http://jmp.sh/iK3kuVs。

【讨论】:

可以分享我简单的func bezierPathWithPolygonInRect(rect: CGRect, numberOfSides: Int) -> UIBezierPath 方法

以上是关于在两个贝塞尔路径形状之间制作动画的主要内容,如果未能解决你的问题,请参考以下文章

贝塞尔曲线与CAShapeLayer的关系以及Stroke动画

以曲线形状剪切贝塞尔路径

使用贝塞尔路径作为剪贴蒙版

如何检测贝塞尔曲线和圆形物体之间的碰撞?

如何在 XAML 中为 WinRT 中的贝塞尔路径设置动画

如何使用贝塞尔曲线沿路径为图像设置动画