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

Animating between two bezier path shapes

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

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

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

而且它们不能只是任意两个形状图层路径,而是 "Animating the path of a shape layer is only guaranteed to work when you are animating from like to like"(参见 here

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

看来变数太多了;哪种组合会给我想要的动画效果?

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

// 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)

创建矩形的更简单的 'trivial' 贝塞尔曲线路径完全相同,但设置了 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

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

@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

另一种方法是使用显示器 link。它就像一个计时器,除了它与显示的更新相协调。然后你让显示的处理程序 link 根据它在动画的任何特定点应该看起来的样子修改视图。

例如,如果您想将蒙版的角从 0 点设置为圆角,则可以执行以下操作,其中 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)
        }
    }
}

显示 link 方法的优点在于它可用于为某些 属性 设置动画,否则这些内容无法设置动画。它还可以让您在动画期间精确地指示中间状态。

如果您可以使用 CAAnimationUIKit 基于块的动画,那可能是可行的方法。但是显示 link 有时是一个很好的后备方法。