Подтвердить что ты не робот

Нарисуйте сегменты из круга или пончика

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

enter image description here

Я бы хотел:

  • нарисуйте сегмент
  • включают градиенты
  • включить тени
  • оживить рисунок от 0 до n угла

Я пытаюсь сделать это с помощью CGContextAddArc и подобных вызовов, но не очень далеко.

Может ли кто-нибудь помочь?

4b9b3361

Ответ 1

На ваш вопрос много вопросов.

Получение пути

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

Path to be stroked

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

CGMutablePathRef arc = CGPathCreateMutable();
CGPathMoveToPoint(arc, NULL,
                  startPoint.x, startPoint.y);
CGPathAddArc(arc, NULL,
             centerPoint.x, centerPoint.y,
             radius,
             startAngle,
             endAngle,
             YES);

Затем, когда у вас есть этот путь (единственная дуга), вы можете создать новый сегмент, погладив его с определенной шириной. Результирующий путь будет иметь две прямые и две дуги. Инсульт происходит от центра на равном расстоянии внутрь и наружу.

CGFloat lineWidth = 10.0;
CGPathRef strokedArc =
    CGPathCreateCopyByStrokingPath(arc, NULL,
                                   lineWidth,
                                   kCGLineCapButt,
                                   kCGLineJoinMiter, // the default
                                   10); // 10 is default miter limit

Рисование

Далее идет рисование, и обычно есть два основных варианта: Core Graphics в drawRect: или формировать слои с помощью Core Animation. Core Graphics даст вам более мощный рисунок, но Core Animation даст вам лучшую производительность анимации. Поскольку задействованы пути, чистая анимация Cora не будет работать. Вы получите странные артефакты. Однако мы можем использовать комбинацию слоев и Core Graphics, рисуя графический контекст слоя.

Заполнение и поглаживание сегмента

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

CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, strokedArc);
CGContextSetFillColorWithColor(c, [UIColor lightGrayColor].CGColor);
CGContextSetStrokeColorWithColor(c, [UIColor blackColor].CGColor);
CGContextDrawPath(c, kCGPathFillStroke);

Это будет выглядеть примерно так на экране

Filled and stroked shape

Добавление теней

Я собираюсь изменить порядок и сделать тень перед градиентом. Чтобы нарисовать тень, нам нужно настроить тень для контекста и нарисовать фигуру, чтобы нарисовать ее с помощью тени. Затем нам нужно восстановить контекст (перед тенью) и снова обвести фигуру.

CGColorRef shadowColor = [UIColor colorWithWhite:0.0 alpha:0.75].CGColor;
CGContextSaveGState(c);
CGContextSetShadowWithColor(c,
                            CGSizeMake(0, 2), // Offset
                            3.0,              // Radius
                            shadowColor);
CGContextFillPath(c);
CGContextRestoreGState(c);

// Note that filling the path "consumes it" so we add it again
CGContextAddPath(c, strokedArc);
CGContextStrokePath(c);

В этот момент результат выглядит примерно так:

enter image description here

Рисование градиента

Для градиента нам нужен градиентный слой. Здесь я делаю очень простой двухцветный градиент, но вы можете настроить все, что захотите. Чтобы создать градиент, нам нужно получить цвета и подходящее цветовое пространство. Затем мы можем нарисовать градиент поверх заливки (но до инсульта). Нам также необходимо скрыть градиент до того же пути, что и раньше. Для этого клипируем путь.

CGFloat colors [] = {
    0.75, 1.0, // light gray   (fully opaque)
    0.90, 1.0  // lighter gray (fully opaque)
};

CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceGray(); // gray colors want gray color space
CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace, colors, NULL, 2);
CGColorSpaceRelease(baseSpace), baseSpace = NULL;

CGContextSaveGState(c);
CGContextAddPath(c, strokedArc);
CGContextClip(c);

CGRect boundingBox = CGPathGetBoundingBox(strokedArc);
CGPoint gradientStart = CGPointMake(0, CGRectGetMinY(boundingBox));
CGPoint gradientEnd   = CGPointMake(0, CGRectGetMaxY(boundingBox));

CGContextDrawLinearGradient(c, gradient, gradientStart, gradientEnd, 0);
CGGradientRelease(gradient), gradient = NULL;
CGContextRestoreGState(c);

Это завершает чертеж, поскольку мы в настоящее время имеем этот результат

Masked gradient

Анимация

Когда дело доходит до анимации формы, в которой она была написана ранее: Анимация кусочков пирога с использованием пользовательского CALayer. Если вы попробуете сделать рисунок, просто анимируя свойство path, вы увидите действительно напуганное перекос пути во время анимации. Тень и градиент оставлены нетронутыми для иллюстративных целей на изображении ниже.

Funky warping of path

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


Для справки: тот же чертеж с использованием Core Animation

Обычная форма

CAShapeLayer *segment = [CAShapeLayer layer];
segment.fillColor = [UIColor lightGrayColor].CGColor;
segment.strokeColor = [UIColor blackColor].CGColor;
segment.lineWidth = 1.0;
segment.path = strokedArc;

[self.view.layer addSublayer:segment];

Добавление теней

У слоя есть некоторые свойства, связанные с тенью, которые вы можете настроить. Howerever, вы должны установить свойство shadowPath для повышения производительности.

segment.shadowColor = [UIColor blackColor].CGColor;
segment.shadowOffset = CGSizeMake(0, 2);
segment.shadowOpacity = 0.75;
segment.shadowRadius = 3.0;
segment.shadowPath = segment.path; // Important for performance

Рисование градиента

CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.colors = @[(id)[UIColor colorWithWhite:0.75 alpha:1.0].CGColor,  // light gray
                    (id)[UIColor colorWithWhite:0.90 alpha:1.0].CGColor]; // lighter gray
gradient.frame = CGPathGetBoundingBox(segment.path);

Если бы мы нарисовали градиент, теперь он был бы поверх формы, а не внутри. Нет, мы не можем иметь градиентную заливку формы (я знаю, что вы об этом думали). Нам нужно замаскировать градиент, чтобы он вышел за пределы сегмента. Для этого мы создаем еще один слой для маски этого сегмента. Это должен быть еще один уровень, в документации ясно, что поведение "undefined", если маска является частью иерархии слоев. Поскольку система координат маски будет такой же, как у подслоев для градиента, нам придется перевести форму сегмента перед ее настройкой.

CAShapeLayer *mask = [CAShapeLayer layer];
CGAffineTransform translation = CGAffineTransformMakeTranslation(-CGRectGetMinX(gradient.frame),
                                                                 -CGRectGetMinY(gradient.frame));
mask.path = CGPathCreateCopyByTransformingPath(segment.path,
                                               &translation);
gradient.mask = mask;

Ответ 2

Все, что вам нужно, описано в Quartz 2D Programming Guide. Я предлагаю вам просмотреть его.

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

arc with outline, gradient, and shadow

Мы начинаем определение функции следующим образом:

static UIImage *imageWithSize(CGSize size) {

Нам понадобится константа для толщины сегмента:

    static CGFloat const kThickness = 20;

и константу для ширины линии, излагающей сегмент:

    static CGFloat const kLineWidth = 1;

и константа для размера тени:

    static CGFloat const kShadowWidth = 8;

Далее нам нужно создать контекст изображения, в котором нужно рисовать:

    UIGraphicsBeginImageContextWithOptions(size, NO, 0); {

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

Поскольку многие функции, которые нам нужно вызвать, являются функциями Core Graphics (aka Quartz 2D), а не функциями UIKit, нам нужно получить CGContext:

        CGContextRef gc = UIGraphicsGetCurrentContext();

Теперь мы готовы начать работу. Сначала добавим дугу к пути. Дуга проходит по центру сегмента, который мы хотим нарисовать:

        CGContextAddArc(gc, size.width / 2, size.height / 2,
            (size.width - kThickness - kLineWidth) / 2,
            -M_PI / 4, -3 * M_PI / 4, YES);

Теперь мы попросим Core Graphics заменить путь "поглаженной" версией, которая описывает путь. Сначала мы устанавливаем толщину штриха на толщину, которую хотим, чтобы сегмент имел:

        CGContextSetLineWidth(gc, kThickness);

и мы установили стиль линии с надписью "butt" , чтобы у нас были квадратные концы:

        CGContextSetLineCap(gc, kCGLineCapButt);

Затем мы можем попросить Core Graphics заменить путь поглаженной версией:

        CGContextReplacePathWithStrokedPath(gc);

Чтобы заполнить этот путь линейным градиентом, мы должны сообщить Core Graphics об обрезании всех операций во внутреннюю часть пути. Это сделает Core Graphics reset путь, но нам понадобится путь позже, чтобы нарисовать черную линию вокруг края. Итак, мы скопируем путь здесь:

        CGPathRef path = CGContextCopyPath(gc);

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

        CGContextSetShadowWithColor(gc,
            CGSizeMake(0, kShadowWidth / 2), kShadowWidth / 2,
            [UIColor colorWithWhite:0 alpha:0.3].CGColor);

Мы собираемся как заполнить сегмент (с градиентом), так и погладить его (нарисовать черную контур). Мы хотим иметь одну тень для обеих операций. Мы говорим Core Graphics, что, начав слой прозрачности:

        CGContextBeginTransparencyLayer(gc, 0); {

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

Поскольку мы собираемся изменить область контекстного клипа для заполнения, но мы не захотим обрезать, когда мы погладим контур позже, нам нужно сохранить состояние графики:

            CGContextSaveGState(gc); {

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

Чтобы заполнить путь градиентом, нам нужно создать объект градиента:

                CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
                CGGradientRef gradient = CGGradientCreateWithColors(rgb, (__bridge CFArrayRef)@[
                    (__bridge id)[UIColor grayColor].CGColor,
                    (__bridge id)[UIColor whiteColor].CGColor
                ], (CGFloat[]){ 0.0f, 1.0f });
                CGColorSpaceRelease(rgb);

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

                CGRect bbox = CGContextGetPathBoundingBox(gc);
                CGPoint start = bbox.origin;
                CGPoint end = CGPointMake(CGRectGetMaxX(bbox), CGRectGetMaxY(bbox));

и мы заставим градиент рисоваться горизонтально или вертикально, в зависимости от того, что больше:

                if (bbox.size.width > bbox.size.height) {
                    end.y = start.y;
                } else {
                    end.x = start.x;
                }

Теперь у нас есть все необходимое для рисования градиента. Сначала мы кликаем по пути:

                CGContextClip(gc);

Затем нарисуем градиент:

                CGContextDrawLinearGradient(gc, gradient, start, end, 0);

Затем мы можем освободить градиент и восстановить сохраненное состояние графики:

                CGGradientRelease(gradient);
            } CGContextRestoreGState(gc);

Когда мы вызывали CGContextClip, Core Graphics reset путь к контексту. Путь не является частью сохраненного состояния графики; вот почему мы сделали копию ранее. Теперь пришло время использовать эту копию, чтобы снова установить путь в контексте:

            CGContextAddPath(gc, path);
            CGPathRelease(path);

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

            CGContextSetLineWidth(gc, kLineWidth);
            CGContextSetLineJoin(gc, kCGLineJoinMiter);
            [[UIColor blackColor] setStroke];
            CGContextStrokePath(gc);

Затем мы говорим Core Graphics о завершении слоя прозрачности. Это заставит его взглянуть на то, что мы нарисовали, и добавить тень под:

        } CGContextEndTransparencyLayer(gc);

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

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

Вы можете найти код вместе в этом значении.

Ответ 3

Это версия Swift 3 ответа Роба Майоффа. Посмотрите, насколько эффективнее этот язык! Это может быть содержимое файла MView.swift:

import UIKit

class MView: UIView {

    var size = CGSize.zero

    override init(frame: CGRect) {
    super.init(frame: frame)
    size = frame.size
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    var niceImage: UIImage {

        let kThickness = CGFloat(20)
        let kLineWidth = CGFloat(1)
        let kShadowWidth = CGFloat(8)

        UIGraphicsBeginImageContextWithOptions(size, false, 0)

            let gc = UIGraphicsGetCurrentContext()!
            gc.addArc(center: CGPoint(x: size.width/2, y: size.height/2),
                   radius: (size.width - kThickness - kLineWidth)/2,
                   startAngle: -45°,
                   endAngle: -135°,
                   clockwise: true)

            gc.setLineWidth(kThickness)
            gc.setLineCap(.butt)
            gc.replacePathWithStrokedPath()

            let path = gc.path!

            gc.setShadow(
                offset: CGSize(width: 0, height: kShadowWidth/2),
                blur: kShadowWidth/2,
                color: UIColor.gray.cgColor
            )

            gc.beginTransparencyLayer(auxiliaryInfo: nil)

                gc.saveGState()

                    let rgb = CGColorSpaceCreateDeviceRGB()

                    let gradient = CGGradient(
                        colorsSpace: rgb,
                        colors: [UIColor.gray.cgColor, UIColor.white.cgColor] as CFArray,
                        locations: [CGFloat(0), CGFloat(1)])!

                    let bbox = path.boundingBox
                    let startP = bbox.origin
                    var endP = CGPoint(x: bbox.maxX, y: bbox.maxY);
                    if (bbox.size.width > bbox.size.height) {
                        endP.y = startP.y
                    } else {
                        endP.x = startP.x
                    }

                    gc.clip()

                    gc.drawLinearGradient(gradient, start: startP, end: endP,
                                          options: CGGradientDrawingOptions(rawValue: 0))

                gc.restoreGState()

                gc.addPath(path)

                gc.setLineWidth(kLineWidth)
                gc.setLineJoin(.miter)
                UIColor.black.setStroke()
                gc.strokePath()

            gc.endTransparencyLayer()


        let image = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return image
    }

    override func draw(_ rect: CGRect) {
        niceImage.draw(at:.zero)
    }
}

Вызовите его из viewController следующим образом:

let vi = MView(frame: self.view.bounds)
self.view.addSubview(vi)

Чтобы преобразовать градусы в радианы, я создал оператор postfix °. Таким образом, вы можете использовать, например, 45 °, и это делает переход от 45 градусов к радианам. Этот пример для Ints, расширьте их также для типов Float, если у вас есть необходимость:

postfix operator °

protocol IntegerInitializable: ExpressibleByIntegerLiteral {
  init (_: Int)
}

extension Int: IntegerInitializable {
  postfix public static func °(lhs: Int) -> CGFloat {
    return CGFloat(lhs) * .pi / 180
  }
}

Поместите этот код в быстрый файл утилиты.