两个贝塞尔路径形状之间的动画

时间:2015-02-10 16:42:04

标签: ios swift animation uibezierpath caanimation

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

首先是认识到使用UIView块动画不起作用;显然,无法在animateWithDuration:块中执行子图层动画。 (请参阅herehere

留下CAAnimation,具有类似CABasicAnimation的具体子类。我很快意识到,无法从没有CAShapeLayer的视图制作动画(例如,请参阅here)。

它们不能只是任何两个形状图层路径,而是“动画形状图层的路径只能保证在你喜欢的时候动画”(见here

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

似乎有很多变数;哪个组合会给我我想要的动画?

3 个答案:

答案 0 :(得分:15)

第一个重点是类似地构造两个贝塞尔曲线路径,因此矩形是一个(平凡的)类似于更复杂的形状。

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

创建矩形的更简单的'琐碎'bezier路径完全相同,但设置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

答案 1 :(得分:9)

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

例如,如果您想要将蒙版的圆角从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基于块的动画,那可能就是这样。但显示链接有时可能是一个很好的后备方法。

答案 2 :(得分: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