Как мога да нарисувам стрелка с помощта на 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

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

образец на неначертана стрелка

Все пак трябва да внимавате. Този код не ви пречи да получавате страхотни резултати, ако направите ширината на главата по-малка от ширината на опашката или ако направите дължината на главата по-голяма от общата дължина на стрелката:

проба с тясна главаhead too long sample

person rob mayoff    schedule 26.11.2012
comment
@SashaPrent Ако съм отговорил на въпроса ви, моля, щракнете върху отметката до (горната част) на моя отговор. Благодаря. - person rob mayoff; 28.11.2012
comment
наистина много мощен и Роб .., трябва да кажа, че си майстор на обективните c функции, както и на математиката - 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
Бихте ли обяснили как това решава въпроса с OPs? Моля, вижте Как да отговорите. - person Paul Kertscher; 06.01.2017
comment
override 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) нека 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