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

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

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

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

4b9b3361

Ответ 1

UPDATE

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

ORIGINAL

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

arrow parts

Мы хотим написать функцию, которая принимает начальную точку, конечную точку, ширину хвоста, ширину головы и длину головы и возвращает путь, обозначающий форму стрелки. Позвольте создать категорию с именем 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:

axis-aligned arrow points

Довольно легко вычислить координаты точек по оси, ориентированной по оси, но для этого нам понадобится общая длина стрелки. Мы будем использовать функцию 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];

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

    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, но если вы помните достаточно тригонометрию, вы можете создать ее напрямую. Проконсультируйтесь с "Математика за матрицами" в "Руководстве по 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

Я поместил весь код в текст для удобной копии.

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

sample arrow 1sample arrow 2

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

unstroked arrow sample

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

narrow head samplehead too long sample

Ответ 2

//This is the integration into the view of the previous exemple
//Attach the following class to your view in the xib file

#import <UIKit/UIKit.h>

@interface Arrow : UIView

@end

#import "Arrow.h"
#import "UIBezierPath+dqd_arrowhead.h"

@implementation Arrow
{
    CGPoint startPoint;
    CGPoint endPoint;
    CGFloat tailWidth;
    CGFloat headWidth;
    CGFloat headLength;
    UIBezierPath *path;

}


- (id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super initWithCoder:aDecoder])
    {
        [self setMultipleTouchEnabled:NO];
        [self setBackgroundColor:[UIColor whiteColor]];

    }
    return self;
}

- (void)drawRect:(CGRect)rect {

    [[UIColor redColor] setStroke];
    tailWidth = 4;
    headWidth = 8;
    headLength = 8;
    path = [UIBezierPath dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint
                                                  toPoint:(CGPoint)endPoint
                                                tailWidth:(CGFloat)tailWidth
                                                headWidth:(CGFloat)headWidth
                                               headLength:(CGFloat)headLength];
    [path setLineWidth:2.0];

    [path stroke];

}
- (void) touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
    UITouch* touchPoint = [touches anyObject];
    startPoint = [touchPoint locationInView:self];
    endPoint = [touchPoint locationInView:self];


    [self setNeedsDisplay];
}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch* touch = [touches anyObject];
    endPoint=[touch locationInView:self];
    [self setNeedsDisplay];
}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch* touch = [touches anyObject];
    endPoint = [touch locationInView:self];
    [self setNeedsDisplay];
}


@end

Ответ 3

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

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)

Ответ 4

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

}