Как нарисовать стрелку с помощью Core Graphics?

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

Пользователь кладет палец на экран и проводит линию в любом направлении. Итак, на конце строки должна появиться стрелка.


person Sasha Prent    schedule 23.11.2012    source источник


Ответы (4)


ОБНОВИТЬ

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

ОРИГИНАЛ

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

части стрелок

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

// UIBezierPath+dqd_arrowhead.h

@interface UIBezierPath (dqd_arrowhead)

+ (UIBezierPath *)dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint
                                           toPoint:(CGPoint)endPoint
                                         tailWidth:(CGFloat)tailWidth
                                         headWidth:(CGFloat)headWidth
                                        headLength:(CGFloat)headLength;

@end

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

// UIBezierPath+dqd_arrowhead.m

#import "UIBezierPath+dqd_arrowhead.h"

#define kArrowPointCount 7

@implementation UIBezierPath (dqd_arrowhead)

+ (UIBezierPath *)dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint
                                           toPoint:(CGPoint)endPoint
                                         tailWidth:(CGFloat)tailWidth
                                         headWidth:(CGFloat)headWidth
                                        headLength:(CGFloat)headLength {

Хорошо, простая часть сделана. Теперь, как нам найти координаты этих семи точек на пути? Находить точки намного проще, если стрелка выровнена по оси X:

точки стрелки, выровненные по оси

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

    CGFloat length = hypotf(endPoint.x - startPoint.x, endPoint.y - startPoint.y);

Мы вызовем вспомогательный метод для вычисления семи точек:

    CGPoint points[kArrowPointCount];
    [self dqd_getAxisAlignedArrowPoints:points
                              forLength:length
                              tailWidth:tailWidth
                              headWidth:headWidth
                             headLength:headLength];

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

    CGAffineTransform transform = [self dqd_transformForStartPoint:startPoint
                                                          endPoint:endPoint
                                                            length:length];

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

    CGMutablePathRef cgPath = CGPathCreateMutable();
    CGPathAddLines(cgPath, &transform, points, sizeof points / sizeof *points);
    CGPathCloseSubpath(cgPath);

Наконец, мы можем обернуть UIBezierPath вокруг CGPath и вернуть его:

    UIBezierPath *uiPath = [UIBezierPath bezierPathWithCGPath:cgPath];
    CGPathRelease(cgPath);
    return uiPath;
}

Вот вспомогательный метод, который вычисляет координаты точки. Все очень просто. При необходимости вернитесь к диаграмме со стрелкой, направленной вдоль оси.

+ (void)dqd_getAxisAlignedArrowPoints:(CGPoint[kArrowPointCount])points
                            forLength:(CGFloat)length
                            tailWidth:(CGFloat)tailWidth
                            headWidth:(CGFloat)headWidth
                           headLength:(CGFloat)headLength {
    CGFloat tailLength = length - headLength;
    points[0] = CGPointMake(0, tailWidth / 2);
    points[1] = CGPointMake(tailLength, tailWidth / 2);
    points[2] = CGPointMake(tailLength, headWidth / 2);
    points[3] = CGPointMake(length, 0);
    points[4] = CGPointMake(tailLength, -headWidth / 2);
    points[5] = CGPointMake(tailLength, -tailWidth / 2);
    points[6] = CGPointMake(0, -tailWidth / 2);
}

Вычислить аффинное преобразование сложнее. Здесь на помощь приходит тригонометрия. Вы можете использовать atan2 и CGAffineTransformRotate и CGAffineTransformTranslate для ее создания, но если вы помните достаточно тригонометрии, вы можете создать ее напрямую. Проконсультируйтесь с "Математика за матрицами" в Руководстве по программированию Quartz 2D для получения дополнительной информации о том, что я здесь делаю:

+ (CGAffineTransform)dqd_transformForStartPoint:(CGPoint)startPoint
                                       endPoint:(CGPoint)endPoint
                                         length:(CGFloat)length {
    CGFloat cosine = (endPoint.x - startPoint.x) / length;
    CGFloat sine = (endPoint.y - startPoint.y) / length;
    return (CGAffineTransform){ cosine, sine, -sine, cosine, startPoint.x, startPoint.y };
}

@end

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

С помощью этой категории вы можете легко нарисовать стрелки:

образец стрелки 1образец стрелки 2

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

образец стрелки без штрихов

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

образец узкой головыобразец слишком длинной головы

person rob mayoff    schedule 26.11.2012
comment
@SashaPrent Если я ответил на ваш вопрос, нажмите на галочку рядом с моим ответом (вверху). Спасибо. - person rob mayoff; 28.11.2012
comment
действительно очень мощный и Роб .., я должен сказать, что ты мастер онективных функций Си, а также математики - person YogiAR; 12.02.2013
comment
Блестящее решение. Процедура проще и понятнее геометрической - person Slavik; 09.01.2014
comment
Я не искал ответа на этот вопрос, но этот ответ так хорош, я все равно проголосовал за него. Действительно отличная работа. - person Matt Lewin; 26.03.2017

Вот Swift-версия моего старого кода Objective-C. Он должен работать в Swift 3.2 и более поздних версиях.

extension UIBezierPath {

    static func arrow(from start: CGPoint, to end: CGPoint, tailWidth: CGFloat, headWidth: CGFloat, headLength: CGFloat) -> UIBezierPath {
        let length = hypot(end.x - start.x, end.y - start.y)
        let tailLength = length - headLength

        func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { return CGPoint(x: x, y: y) }
        let points: [CGPoint] = [
            p(0, tailWidth / 2),
            p(tailLength, tailWidth / 2),
            p(tailLength, headWidth / 2),
            p(length, 0),
            p(tailLength, -headWidth / 2),
            p(tailLength, -tailWidth / 2),
            p(0, -tailWidth / 2)
        ]

        let cosine = (end.x - start.x) / length
        let sine = (end.y - start.y) / length
        let transform = CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: start.x, ty: start.y)

        let path = CGMutablePath()
        path.addLines(between: points, transform: transform)
        path.closeSubpath()

        return self.init(cgPath: path)
    }

}

Вот пример того, как вы бы это назвали:

let arrow = UIBezierPath.arrow(from: CGPoint(x: 50, y: 100), to: CGPoint(x: 200, y: 50),
        tailWidth: 10, headWidth: 25, headLength: 40)
person rob mayoff    schedule 23.06.2016

В Swift 3.0 этого можно добиться с помощью

extension UIBezierPath {

class func arrow(from start: CGPoint, to end: CGPoint, tailWidth: CGFloat, headWidth: CGFloat, headLength: CGFloat) -> Self {
    let length = hypot(end.x - start.x, end.y - start.y)
    let tailLength = length - headLength

    func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { return CGPoint(x: x, y: y) }
    var points: [CGPoint] = [
        p(0, tailWidth / 2),
        p(tailLength, tailWidth / 2),
        p(tailLength, headWidth / 2),
        p(length, 0),
        p(tailLength, -headWidth / 2),
        p(tailLength, -tailWidth / 2),
        p(0, -tailWidth / 2)
    ]

    let cosine = (end.x - start.x) / length
    let sine = (end.y - start.y) / length
    var transform = CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: start.x, ty: start.y)        
    let path = CGMutablePath()
    path.addLines(between: points, transform: transform)
    path.closeSubpath()
    return self.init(cgPath: path)
}

}
person Sujatha Girijala    schedule 06.01.2017
comment
Не могли бы вы объяснить как это решает вопрос OP? См. Как ответить. - person Paul Kertscher; 06.01.2017
comment
переопределить func draw (_ rect: CGRect) {let arrow = UIBezierPath.arrow (from: CGPoint (x: 10, y: 10), to: CGPoint (x: 200, y: 10), tailWidth: 10, headWidth: 25 , headLength: 40) let shapeLayer = CAShapeLayer () shapeLayer.path = arrow.cgPath self.layer.addSublayer (shapeLayer)} - person Sujatha Girijala; 09.01.2017

person    schedule
comment
Было бы более эффективно использовать CAShapeLayer вместо реализации drawRect:. - person rob mayoff; 18.01.2014
comment
какое красиво подробное, исчерпывающее, элегантное объяснение. От всего этого исходит искренняя забота. Спасибо. - person johnrubythecat; 22.10.2014