2015-02-10 18 views
6

Ho trovato difficile animare uno UIImageView tra due stati: la sua cornice rettangolare originale e una nuova forma creata con uno UIBezierPath. Ci sono molte tecniche diverse menzionate, la maggior parte delle quali non ha funzionato per me.Animazione tra due forme di percorso Bezier

Per prima cosa è stato compreso che l'utilizzo dell'animazione di blocchi UIView non funzionava; evidentemente non è possibile eseguire animazioni di sottostrato in un blocco animateWithDuration:. (Vedi here e here)

che ha lasciato CAAnimation, con le sottoclassi concrete come CABasicAnimation. Mi sono presto reso conto che non è possibile animare da una vista che non ha uno CAShapeLayer a uno che fa (vedi here, ad esempio).

E non possono essere solo due percorsi di livello di forma, ma piuttosto "Animare il percorso di un livello di forma è garantita solo a lavorare quando si sta animando da like a come" (vedi here)

Con che al posto, arriva i problemi più banali, come quello che da utilizzare per fromValue e toValue (dovrebbero essere un CAShapeLayer, o un CGPath?), cosa aggiungere l'animazione (il layer, o la mask?), ecc

Sembrava che ci fossero così tante variabili; quale combinazione mi avrebbe dato l'animazione che stavo cercando?

risposta

11

Il primo punto importante è costruire i due percorsi di Bezier in modo simile, quindi il rettangolo è un analogo (banale) alla forma più complessa.

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

Il semplice tracciato Bezier 'banale', che crea un rettangolo, è esattamente lo stesso ma il controlPoint è impostato in modo che sembra non esserci:

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

(Prova a rimuovere la addQuadCurveToPoint linea per ottenere un'animazione molto strano)

E, infine, comanda l'animazione:

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

Se qualcuno è interessato, il progetto è here.

+0

Grazie dell'esempio e tutti i link nella tua domanda. È interessante e informativo leggere le discussioni su questo argomento. – rdelmar

+0

come impostare tovalue (myBezierTrivial.CGPath) nel comando di animazione? – ABJ

3

Mi sono ispirato al tuo esempio per provare un'animazione circolare a quella quadrata utilizzando le tecniche menzionate nella risposta e in alcuni dei collegamenti. Intendo estendere questo per essere un cerchio più generale per l'animazione del poligono, ma attualmente funziona solo per i quadrati. Ho una classe chiamata RDPolyCircle (una sottoclasse di CAShapeLayer) che fa il sollevamento pesante. Ecco il suo codice,

@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; 
} 

Dal controller della vista, creo un'istanza di questo livello, e aggiungerlo a strato semplice di vista, poi fare le animazioni su un pulsante,

#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]; 
} 

Il progetto può essere trovato qui, http://jmp.sh/iK3kuVs.

+0

Posso condividere il mio semplice 'func bezierPathWithPolygonInRect (rect: CGRect, numberOfSides: Int) -> UIBezierPath {' metodo – coco

9

Un altro approccio consiste nell'utilizzare un collegamento di visualizzazione. È come un timer, tranne che è coordinato con l'aggiornamento del display.Quindi il gestore del link di visualizzazione modifica la vista in base a come dovrebbe apparire in un punto particolare dell'animazione.

Ad esempio, se si desidera animare l'arrotondamento degli angoli della maschera da 0 a 50 punti, è possibile fare qualcosa come il seguente, dove percent è un valore compreso tra 0.0 e 1.0 che indica quale percentuale dell'animazione è fatto:

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 
} 

Dove:

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

la virtù dell'approccio di collegamento di visualizzazione è che può essere utilizzato per animare alcune proprietà che è altrimenti unanimatable. Consente inoltre di dettare con precisione lo stato provvisorio durante l'animazione.

Se è possibile utilizzare l'animazione basata su blocco CAAnimation o UIKit, questa è probabilmente la strada da percorrere. Ma il link del display può a volte essere un buon approccio fallback.

+0

Questo può risultare molto utile un giorno, quando 'CAAnimation' e' UIKit' non gestiranno qualcosa! – coco