Ответ 1
UPDATE
Я опубликовал версию ответа Swift отдельно.
ORIGINAL
Это интересная маленькая проблема. Прежде всего, есть много способов рисовать стрелы с изогнутыми или прямыми сторонами. Пусть выберете очень простой способ и отметьте необходимые нам измерения:
Мы хотим написать функцию, которая принимает начальную точку, конечную точку, ширину хвоста, ширину головы и длину головы и возвращает путь, обозначающий форму стрелки. Позвольте создать категорию с именем 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];
Теперь мы можем создать путь основной графики, используя точки оси, ориентированной по оси, и преобразование, которое превращает его в нужную стрелку:
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
Я поместил весь код в текст для удобной копии.
С помощью этой категории вы можете легко рисовать стрелки:
Поскольку вы просто создаете путь, вы можете не заполнять его, или не обрабатывать его, как в этом примере:
Однако вы должны быть осторожны. Этот код не мешает вам получать фанковые результаты, если вы делаете ширину головы меньше, чем ширина хвоста, или если длина головы больше общей длины стрелки: