анимация длины маски круглой стрелки в основной анимации


Я создал круговую анимацию, используя CAShapeLayer и маскировку. Вот мой код:

- (void) maskAnimation{


    animationCompletionBlock theBlock;
    imageView.hidden = FALSE;//Show the image view

    CAShapeLayer *maskLayer = [CAShapeLayer layer];

    CGFloat maskHeight = imageView.layer.bounds.size.height;
    CGFloat maskWidth = imageView.layer.bounds.size.width;


    CGPoint centerPoint;
    centerPoint = CGPointMake( maskWidth/2, maskHeight/2);

    //Make the radius of our arc large enough to reach into the corners of the image view.
    CGFloat radius = sqrtf(maskWidth * maskWidth + maskHeight * maskHeight)/2;

    //Don't fill the path, but stroke it in black.
    maskLayer.fillColor = [[UIColor clearColor] CGColor];
    maskLayer.strokeColor = [[UIColor blackColor] CGColor];

    maskLayer.lineWidth = 60;

    CGMutablePathRef arcPath = CGPathCreateMutable();

    //Move to the starting point of the arc so there is no initial line connecting to the arc
    CGPathMoveToPoint(arcPath, nil, centerPoint.x, centerPoint.y-radius/2);

    //Create an arc at 1/2 our circle radius, with a line thickess of the full circle radius
    CGPathAddArc(arcPath,
                 nil,
                 centerPoint.x,
                 centerPoint.y,
                 radius/2,
                 3*M_PI/2,
                 -M_PI/2,
                 NO);



    maskLayer.path = arcPath;//[aPath CGPath];//arcPath;

    //Start with an empty mask path (draw 0% of the arc)
    maskLayer.strokeEnd = 0.0;


    CFRelease(arcPath);

    //Install the mask layer into out image view's layer.
    imageView.layer.mask = maskLayer;

    //Set our mask layer's frame to the parent layer's bounds.
    imageView.layer.mask.frame = imageView.layer.bounds;

    //Create an animation that increases the stroke length to 1, then reverses it back to zero.
    CABasicAnimation *swipe = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    swipe.duration = 5;
    swipe.delegate = self;
    [swipe setValue: theBlock forKey: kAnimationCompletionBlock];
    swipe.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    swipe.fillMode = kCAFillModeForwards;
    swipe.removedOnCompletion = NO;
    swipe.autoreverses = YES;
    swipe.toValue = [NSNumber numberWithFloat: 1.0];

    [maskLayer addAnimation: swipe forKey: @"strokeEnd"];


}

Это мое фоновое изображение.: Введите описание изображения здесь

Вот как это выглядит, когда я запускаю анимацию: Введите описание изображения здесь

Но что я хочу, головка стрелы отсутствует, как это добавить? Введите описание изображения здесь

3 4

3 ответа:

Посколькумой другой ответ (анимация двух уровней масок) имеет некоторые графические сбои, я решил попробовать перерисовать путь на каждом кадре анимации. Итак, сначала давайте напишем подкласс CALayer, который похож на CAShapeLayer, но просто рисует стрелку. Первоначально я попытался сделать его подклассом CAShapeLayer, но не смог заставить основную анимацию должным образом анимировать его.

В любом случае, вот интерфейс, который мы собираемся реализовать:

@interface ArrowLayer : CALayer

@property (nonatomic) CGFloat thickness;
@property (nonatomic) CGFloat startRadians;
@property (nonatomic) CGFloat lengthRadians;
@property (nonatomic) CGFloat headLengthRadians;

@property (nonatomic, strong) UIColor *fillColor;
@property (nonatomic, strong) UIColor *strokeColor;
@property (nonatomic) CGFloat lineWidth;
@property (nonatomic) CGLineJoin lineJoin;

@end

Свойство startRadians - это позиция (в радианы) конца хвоста. lengthRadians - это длина (в радианах) от конца хвоста до кончика наконечника стрелы. headLengthRadians - это длина (в радианах) наконечника стрелы.

Мы также воспроизводим некоторые свойства CAShapeLayer. Нам не нужно свойство lineCap, потому что мы всегда рисуем замкнутый путь.

Итак, как мы реализуем эту сумасшедшую вещь? Как это бывает, CALayer позаботится о сохранении любого старого свойства, которое вы хотите определить на подклассе . Итак, сначала, мы просто говорим компилятору, чтобы он не беспокоился о синтезе свойств:
@implementation ArrowLayer

@dynamic thickness;
@dynamic startRadians;
@dynamic lengthRadians;
@dynamic headLengthRadians;
@dynamic fillColor;
@dynamic strokeColor;
@dynamic lineWidth;
@dynamic lineJoin;
Но мы должны сказать Core Animation, что нам нужно перерисовать слой, если какие-либо из этих свойств изменятся. Для этого нам нужен список имен объектов недвижимости. Мы будем использовать Objective-C runtime для получения списка, поэтому нам не придется повторно вводить имена свойств. Нам нужно #import <objc/runtime.h> в верхней части файла, и тогда мы можем получить список следующим образом:
+ (NSSet *)customPropertyKeys {
    static NSMutableSet *set;
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        unsigned int count;
        objc_property_t *properties = class_copyPropertyList(self, &count);
        set = [[NSMutableSet alloc] initWithCapacity:count];
        for (int i = 0; i < count; ++i) {
            [set addObject:@(property_getName(properties[i]))];
        }
        free(properties);
    });
    return set;
}

Теперь мы можем написать метод, который является основной анимацией использует, чтобы узнать, какие свойства должны вызвать перерисовку:

+ (BOOL)needsDisplayForKey:(NSString *)key {
    return [[self customPropertyKeys] containsObject:key] || [super needsDisplayForKey:key];
}

Также оказывается, что основная анимация будет делать копию нашего слоя в каждом кадре анимации. Нам нужно убедиться, что мы копируем все эти свойства, когда Core Animation делает копию:

- (id)initWithLayer:(id)layer {
    if (self = [super initWithLayer:layer]) {
        for (NSString *key in [self.class customPropertyKeys]) {
            [self setValue:[layer valueForKey:key] forKey:key];
        }
    }
    return self;
}

Нам также нужно сказать основной анимации, что мы должны перерисовать, если границы слоя изменяются:

- (BOOL)needsDisplayOnBoundsChange {
    return YES;
}
Наконец, мы можем перейти к самому главному-нарисовать стрелу. Во-первых, мы изменим графику начало контекста должно находиться в центре границ слоя. Затем мы построим путь, очерчивающий стрелку (которая теперь находится в центре начала координат). Наконец, мы будем заполнять и / или обводить путь соответствующим образом.
- (void)drawInContext:(CGContextRef)gc {
    [self moveOriginToCenterInContext:gc];
    [self addArrowToPathInContext:gc];
    [self drawPathOfContext:gc];
}

Перемещение начала координат в центр наших границ тривиально:

- (void)moveOriginToCenterInContext:(CGContextRef)gc {
    CGRect bounds = self.bounds;
    CGContextTranslateCTM(gc, CGRectGetMidX(bounds), CGRectGetMidY(bounds));
}

Построение пути стрелки не является тривиальным. Во-первых, нам нужно получить радиальное положение, в котором начинается хвост, радиальное положение, в котором заканчивается хвост и начинается наконечник стрелы, и радиальное положение наконечника стрелы. Мы будем использовать вспомогательный метод для вычисления этих трех радиальных положений:

- (void)addArrowToPathInContext:(CGContextRef)gc {
    CGFloat startRadians;
    CGFloat headRadians;
    CGFloat tipRadians;
    [self getStartRadians:&startRadians headRadians:&headRadians tipRadians:&tipRadians];
Затем нам нужно вычислить радиус внутренней и внешней дуг стрелы, а также радиус наконечника:
    CGFloat thickness = self.thickness;

    CGFloat outerRadius = self.bounds.size.width / 2;
    CGFloat tipRadius = outerRadius - thickness / 2;
    CGFloat innerRadius = outerRadius - thickness;

Нам также нужно знать, проводим ли мы внешнюю дугу по часовой стрелке или против часовой стрелки:

    BOOL outerArcIsClockwise = tipRadians > startRadians;
Внутренняя дуга будет нарисована в противоположном направлении.

, наконец, мы можем построить путь. Мы переместитесь к кончику наконечника стрелы, затем добавьте две дуги. Вызов CGPathAddArc автоматически добавляет прямую линию от текущей точки пути до начальной точки дуги, поэтому нам не нужно добавлять прямые линии самостоятельно:

    CGContextMoveToPoint(gc, tipRadius * cosf(tipRadians), tipRadius * sinf(tipRadians));
    CGContextAddArc(gc, 0, 0, outerRadius, headRadians, startRadians, outerArcIsClockwise);
    CGContextAddArc(gc, 0, 0, innerRadius, startRadians, headRadians, !outerArcIsClockwise);
    CGContextClosePath(gc);
}
Теперь давайте выясним, как вычислить эти три радиальных положения. Это было бы тривиально, за исключением того, что мы хотим быть изящными, когда длина головы больше, чем общая длина, путем отсечения длины головы к общей длине. Мы также хотим, чтобы позволить общая длина будет отрицательной, чтобы нарисовать стрелку в противоположном направлении. Мы начнем с определения начальной позиции, общей длины и длины головы. Мы будем использовать помощник, который зажимает длину головы, чтобы быть не больше, чем общая длина:
- (void)getStartRadians:(CGFloat *)startRadiansOut headRadians:(CGFloat *)headRadiansOut tipRadians:(CGFloat *)tipRadiansOut {
    *startRadiansOut = self.startRadians;
    CGFloat lengthRadians = self.lengthRadians;
    CGFloat headLengthRadians = [self clippedHeadLengthRadians];
Затем мы вычисляем радиальное положение, в котором хвост встречается с наконечником стрелы. Мы делаем это осторожно, так что если мы обрезали длину головы, мы точно вычисляем начальное положение. Это важно для того, чтобы при вызове CGPathAddArc с две позиции, это не добавляет неожиданную дугу из-за округления с плавающей запятой.
    // Compute headRadians carefully so it is exactly equal to startRadians if the head length was clipped.
    *headRadiansOut = *startRadiansOut + (lengthRadians - headLengthRadians);

Наконец, мы вычисляем радиальное положение наконечника стрелы:

    *tipRadiansOut = *startRadiansOut + lengthRadians;
}

Нам нужно написать помощник, который зажимает длину головы. Он также должен гарантировать, что длина головы имеет тот же знак, что и общая длина, поэтому вычисления выше работают правильно:

- (CGFloat)clippedHeadLengthRadians {
    CGFloat lengthRadians = self.lengthRadians;
    CGFloat headLengthRadians = copysignf(self.headLengthRadians, lengthRadians);

    if (fabsf(headLengthRadians) > fabsf(lengthRadians)) {
        headLengthRadians = lengthRadians;
    }
    return headLengthRadians;
}

Чтобы нарисовать путь в графическом контексте, нам нужно установить параметры заполнения и штриховки контекст, основанный на наших свойствах, а затем вызовите CGContextDrawPath:

- (void)drawPathOfContext:(CGContextRef)gc {
    CGPathDrawingMode mode = 0;
    [self setFillPropertiesOfContext:gc andUpdateMode:&mode];
    [self setStrokePropertiesOfContext:gc andUpdateMode:&mode];

    CGContextDrawPath(gc, mode);
}

Мы заполняем путь, если нам дали цвет заливки:

- (void)setFillPropertiesOfContext:(CGContextRef)gc andUpdateMode:(CGPathDrawingMode *)modeInOut {
    UIColor *fillColor = self.fillColor;
    if (fillColor) {
        *modeInOut |= kCGPathFill;
        CGContextSetFillColorWithColor(gc, fillColor.CGColor);
    }
}

Мы вычерчиваем путь, если нам дали цвет Штриха и ширину линии:

- (void)setStrokePropertiesOfContext:(CGContextRef)gc andUpdateMode:(CGPathDrawingMode *)modeInOut {
    UIColor *strokeColor = self.strokeColor;
    CGFloat lineWidth = self.lineWidth;
    if (strokeColor && lineWidth > 0) {
        *modeInOut |= kCGPathStroke;
        CGContextSetStrokeColorWithColor(gc, strokeColor.CGColor);
        CGContextSetLineWidth(gc, lineWidth);
        CGContextSetLineJoin(gc, self.lineJoin);
    }
}

Конец!

@end

Итак, теперь мы можем вернуться к контроллеру вида и использовать ArrowLayer в качестве маски вида изображения:

- (void)setUpMask {
    arrowLayer = [ArrowLayer layer];
    arrowLayer.frame = imageView.bounds;
    arrowLayer.thickness = 60;
    arrowLayer.startRadians = -M_PI_2;
    arrowLayer.lengthRadians = 0;
    arrowLayer.headLengthRadians = M_PI_2 / 8;
    arrowLayer.fillColor = [UIColor whiteColor];
    imageView.layer.mask = arrowLayer;
}

И мы можем просто анимировать свойство lengthRadians от 0 до 2 π:

- (IBAction)goButtonWasTapped:(UIButton *)goButton {
    goButton.hidden = YES;
    [CATransaction begin]; {
        [CATransaction setAnimationDuration:2];
        [CATransaction setCompletionBlock:^{
            goButton.hidden = NO;
        }];

        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"lengthRadians"];
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
        animation.autoreverses = YES;
        animation.fromValue = @0.0f;
        animation.toValue = @((CGFloat)(2.0f * M_PI));
        [arrowLayer addAnimation:animation forKey:animation.keyPath];
    } [CATransaction commit];
}

И мы получаем без сбоев анимация:

анимация стрелок без глюков

Я профилировал это на своем iPhone 4S под управлением iOS 6.0.1, используя основной инструмент анимации. Он, кажется, получает 40-50 кадров в секунду. Ваш пробег может варьироваться. Я попытался включить свойство drawsAsynchronously (новое в iOS 6), но это ничего не изменило.

Я загрузил код в этом ответе как суть для легкого копирования.

Обновление

Смотрите мой другой ответ для решения, которое не имеет сбоев.

Оригинал

Это забавная маленькая проблема. Я не думаю, что мы можем решить ее идеально с помощью только основной анимации, но мы можем сделать это довольно хорошо.

Мы должны настроить маску, когда вид выложен, поэтому мы должны сделать это только тогда, когда вид изображения впервые появляется или когда он меняет размер. Итак, давайте сделаем это из viewDidLayoutSubviews:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    [self setUpMask];
}

- (void)setUpMask {
    arrowLayer = [self arrowLayerWithFrame:imageView.bounds];
    imageView.layer.mask = arrowLayer;
}

Здесь arrowLayer - переменная экземпляра, так что я могу оживить слой.

Чтобы на самом деле создать слой в форме стрелки, мне нужны некоторые константы:

static CGFloat const kThickness = 60.0f;
static CGFloat const kTipRadians = M_PI_2 / 8;
static CGFloat const kStartRadians = -M_PI_2;

static CGFloat const kEndRadians = kStartRadians + 2 * M_PI;
static CGFloat const kTipStartRadians = kEndRadians - kTipRadians;

Теперь я могу создать слой. Поскольку нет "стреловидной" концевой крышки линии, я должен сделать путь, который очерчивает весь путь, включая заостренный наконечник:

- (CAShapeLayer *)arrowLayerWithFrame:(CGRect)frame {
    CGRect bounds = (CGRect){ CGPointZero, frame.size };
    CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
    CGFloat outerRadius = bounds.size.width / 2;
    CGFloat innerRadius = outerRadius - kThickness;
    CGFloat pointRadius = outerRadius - kThickness / 2;

    UIBezierPath *path = [UIBezierPath bezierPath];
    [path addArcWithCenter:center radius:outerRadius startAngle:kStartRadians endAngle:kTipStartRadians clockwise:YES];
    [path addLineToPoint:CGPointMake(center.x + pointRadius * cosf(kEndRadians), center.y + pointRadius * sinf(kEndRadians))];
    [path addArcWithCenter:center radius:innerRadius startAngle:kTipStartRadians endAngle:kStartRadians clockwise:NO];
    [path closePath];

    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.frame = frame;
    layer.path = path.CGPath;
    layer.fillColor = [UIColor whiteColor].CGColor;
    layer.strokeColor = nil;
    return layer;
}

Если мы сделаем все это, то это будет выглядеть так:

полная стрелка

Теперь мы хотим, чтобы стрелка двигалась по кругу, поэтому мы применяем анимацию вращения к маске:
- (IBAction)goButtonWasTapped:(UIButton *)goButton {
    goButton.enabled = NO;
    [CATransaction begin]; {
        [CATransaction setAnimationDuration:2];
        [CATransaction setCompletionBlock:^{
            goButton.enabled = YES;
        }];

        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
        animation.autoreverses = YES;
        animation.fromValue = 0;
        animation.toValue = @(2 * M_PI);
        [arrowLayer addAnimation:animation forKey:animation.keyPath];

    } [CATransaction commit];
}

Когда мы нажимаем на Go кнопка, она выглядит так:

вращающаяся незамкнутая стрелка

Это не правильно, конечно. Нам нужно подрезать хвост стрелы. Для этого нам нужно приложить маску к маске. Мы не можем применить его непосредственно (я пытался). Вместо этого нам нужен дополнительный слой, который будет действовать как маска представления изображения. Иерархия выглядит следующим образом:

Image view layer
  Mask layer (just a generic `CALayer` set as the image view layer's mask)
      Arrow layer (a `CAShapeLayer` as a regular sublayer of the mask layer)
          Ring layer (a `CAShapeLayer` set as the mask of the arrow layer)

Новый кольцевой слой будет точно таким же, как и ваша первоначальная попытка нарисовать маску: один гладкий сегмент дуги. Мы установим иерархию путем переписывания setUpMask:

- (void)setUpMask {
    CALayer *layer = [CALayer layer];
    layer.frame = imageView.bounds;
    imageView.layer.mask = layer;
    arrowLayer = [self arrowLayerWithFrame:layer.bounds];
    [layer addSublayer:arrowLayer];
    ringLayer = [self ringLayerWithFrame:arrowLayer.bounds];
    arrowLayer.mask = ringLayer;
    return;
}

Теперь у нас есть еще один Ивар, ringLayer, потому что нам нужно будет оживить и его. Метод arrowLayerWithFrame: остается неизменным. Вот как мы создаем кольцевой слой:

- (CAShapeLayer *)ringLayerWithFrame:(CGRect)frame {
    CGRect bounds = (CGRect){ CGPointZero, frame.size };
    CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
    CGFloat radius = (bounds.size.width - kThickness) / 2;

    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.frame = frame;
    layer.path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:kStartRadians endAngle:kEndRadians clockwise:YES].CGPath;
    layer.fillColor = nil;
    layer.strokeColor = [UIColor whiteColor].CGColor;
    layer.lineWidth = kThickness + 2; // +2 to avoid extra anti-aliasing
    layer.strokeStart = 1;
    return layer;
}
Обратите внимание, что мы устанавливаем strokeStart на 1, вместо того, чтобы устанавливать strokeEnd на 0. Конец штриха находится на кончике стрелы, и мы всегда хотим, чтобы наконечник был виден, поэтому мы оставляем его в покое.

Наконец, мы переписываем goButtonWasTapped, чтобы анимировать кольцевой слой strokeStart (в дополнение к анимации слоя стрелки вращение):

- (IBAction)goButtonWasTapped:(UIButton *)goButton {
    goButton.hidden = YES;
    [CATransaction begin]; {
        [CATransaction setAnimationDuration:2];
        [CATransaction setCompletionBlock:^{
            goButton.hidden = NO;
        }];

        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
        animation.autoreverses = YES;
        animation.fromValue = 0;
        animation.toValue = @(2 * M_PI);
        [arrowLayer addAnimation:animation forKey:animation.keyPath];

        animation.keyPath = @"strokeStart";
        animation.fromValue = @1;
        animation.toValue = @0;
        [ringLayer addAnimation:animation forKey:animation.keyPath];

    } [CATransaction commit];
}

Конечный результат выглядит так:

вращая закрепить стрелку

Это все еще не идеально. Есть небольшое покачивание хвоста, и иногда вы получаете колонку синих пикселей там. На кончике вы также иногда получаете шепот белой линии. Я думаю, что это связано с тем, как основная анимация представляет дугу внутри (как кубический сплайн Безье). Он не может точно измерить расстояние вдоль пути для strokeStart, поэтому он приближается, и иногда приближение отключен достаточно, чтобы пропустить несколько пикселей. Вы можете решить проблему чаевых, изменив kEndRadians на это:
static CGFloat const kEndRadians = kStartRadians + 2 * M_PI - 0.01;

И вы можете удалить синие пиксели из хвоста, настроив конечные точки анимации strokeStart:

        animation.keyPath = @"strokeStart";
        animation.fromValue = @1.01f;
        animation.toValue = @0.01f;
        [ringLayer addAnimation:animation forKey:animation.keyPath];

Но вы все равно увидите, как шевелится хвост:

вращающаяся обрезанная стрелка с хитростями

Если вы хотите сделать лучше, чем это, вы можете попробовать фактически воссоздать форму стрелки на каждом кадре. Я не знаю, как быстро это произойдет.

К сожалению, в контурном чертеже нет вариантов, чтобы иметь заостренную линию cap, как тот, который вы описываете (варианты доступны с помощью свойства CAShapeLayer's lineCap, просто не тот, который вам нужен).

Вам придется нарисовать границу пути самостоятельно и заполнить ее, вместо того, чтобы полагаться на ширину штриха. Это означает 3 линии и 2 дуги, которые должны быть управляемыми, хотя и не такими прямыми, как то, что вы пытались сделать.