Pregunta ¿Cómo puedo dibujar una flecha usando Core Graphics?


Necesito dibujar una línea con una flecha en su extremo en mi aplicación Draw. No soy bueno en trigonometría, así que no puedo resolver este problema.

El usuario coloca su dedo en la pantalla y dibuja la línea en cualquier dirección. Entonces, la flecha debería aparecer en el extremo de la línea.


31
2017-11-23 12:10


origen


Respuestas:


ACTUALIZAR

He publicado una versión Swift de esta respuesta por separado.

ORIGINAL

Este es un pequeño problema divertido. En primer lugar, hay muchas formas de dibujar flechas, con lados curvos o rectos. Vamos a elegir una manera muy simple y etiquetar las medidas que necesitaremos:

arrow parts

Queremos escribir una función que tome el punto de inicio, el punto final, el ancho de la cola, el ancho de la cabeza y la longitud de la cabeza, y devuelve una ruta que delinee la forma de la flecha. Vamos a crear una categoría llamada dqd_arrowhead para agregar este método a 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

Como hay siete esquinas en el camino de la flecha, comencemos nuestra implementación nombrando esa constante:

// 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 {

OK, la parte fácil ya está hecha. Ahora, ¿cómo encontramos las coordenadas de esos siete puntos en el camino? Es mucho más fácil encontrar los puntos si la flecha está alineada a lo largo del eje X:

axis-aligned arrow points

Es bastante fácil calcular las coordenadas de puntos en una flecha alineada con el eje, pero necesitaremos la longitud total de la flecha para hacerlo. Usaremos el hypotf función de la biblioteca estándar:

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

Llamaremos a un método de ayuda para calcular realmente los siete puntos:

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

Pero tenemos que transformar esos puntos, porque en general estamos no tratando de crear una flecha alineada con el eje. Afortunadamente, Core Graphics admite un tipo de transformación llamada transformacion afin, que nos permite rotar y traducir (deslizar) puntos. Llamaremos a otro método auxiliar para crear la transformación que convierte nuestra flecha alineada con el eje en la flecha que se nos pidió:

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

Ahora podemos crear una ruta de gráficos centrales usando los puntos de la flecha alineada al eje y la transformación que la convierte en la flecha que queremos:

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

Finalmente, podemos envolver un UIBezierPath alrededor de CGPath y devolverlo:

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

Aquí está el método de ayuda que calcula las coordenadas del punto. Es bastante simple. Consulte de nuevo el diagrama de la flecha alineada al eje si es necesario.

+ (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);
}

Computar la transformación afín es más complicado. Aquí es donde entra la trigonometría. Puedes usar atan2 y el CGAffineTransformRotate y CGAffineTransformTranslate funciones para crearlo, pero si recuerda suficiente trigonometría, puede crearlo directamente. Consultar "The Math Behind the Matrices" en el Quartz 2D Programming Guide para obtener más información sobre lo que estoy haciendo aquí:

+ (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

He puesto todo el código en una esencia para copiar y pegar.

Con esta categoría, puedes dibujar flechas fácilmente:

sample arrow 1  sample arrow 2

Como solo está generando una ruta, puede elegir no llenarla, o no acariciarla como en este ejemplo:

unstroked arrow sample

Sin embargo, tienes que ser cuidadoso. Este código no le impide obtener resultados originales si hace que el ancho de la cabeza sea inferior al ancho de la cola, o si hace que la longitud de la cabeza sea mayor que la longitud total de la flecha:

narrow head sample  head too long sample


178
2017-11-26 06:01



Aquí hay una versión Swift de mi antiguo código Objective-C. Debería funcionar en Swift 3.2 o 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)
    }

}

Aquí hay un ejemplo de cómo lo llamarías:

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

10
2018-06-23 08:06



//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

7
2018-06-18 12:21



En Swift 3.0 puedes lograr esto con

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

}

1
2018-01-06 13:35