web-dev-qa-db-fra.com

Comment dessiner une flèche à l'aide de Core Graphics?

J'ai besoin de tracer une ligne avec une flèche à son extrémité dans mon application Draw. Je ne suis pas bon en trigonométrie, donc je ne peux pas résoudre ce problème.

L'utilisateur pose son doigt sur l'écran et trace la ligne dans n'importe quelle direction. Ainsi, la flèche devrait apparaître à la fin de la ligne.

36
Sasha Prent

MISE À JOUR

J'ai publié une version Swift version de cette réponse séparément.

ORIGINAL

C'est un petit problème amusant. Tout d'abord, il existe de nombreuses façons de dessiner des flèches, avec des côtés courbes ou droits. Choisissons un moyen très simple et étiquetons les mesures dont nous aurons besoin:

arrow parts

Nous voulons écrire une fonction qui prend le point de départ, le point final, la largeur de queue, la largeur de tête et la longueur de tête, et renvoie un chemin décrivant la forme de la flèche. Créons une catégorie nommée dqd_arrowhead pour ajouter cette méthode à 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

Puisqu'il y a sept coins sur le chemin de la flèche, commençons notre implémentation en nommant cette 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 partie facile est terminée. Maintenant, comment pouvons-nous trouver les coordonnées de ces sept points sur le chemin? Il est beaucoup plus facile de trouver les points si la flèche est alignée le long de l'axe X:

axis-aligned arrow points

Il est assez facile de calculer les coordonnées du point sur une flèche alignée sur l'axe, mais nous aurons besoin de la longueur totale de la flèche pour le faire. Nous utiliserons la fonction hypotf de la bibliothèque standard:

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

Nous ferons appel à une méthode d'aide pour calculer réellement les sept points:

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

Mais nous devons transformer ces points, car en général nous n'essayons pas de créer une flèche alignée sur l'axe. Heureusement, Core Graphics prend en charge une sorte de transformation appelée transformation affine , qui nous permet de faire pivoter et de traduire (faire glisser) les points. Nous appellerons une autre méthode d'assistance pour créer la transformation qui transforme notre flèche alignée sur l'axe en la flèche qui nous a été demandée:

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

Maintenant, nous pouvons créer un chemin Core Graphics en utilisant les points de la flèche alignée sur l'axe et la transformation qui le transforme en la flèche que nous voulons:

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

Enfin, nous pouvons enrouler un UIBezierPath autour du CGPath et le renvoyer:

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

Voici la méthode d'assistance qui calcule les coordonnées du point. C'est assez simple. Si nécessaire, reportez-vous au schéma de la flèche alignée sur l'axe.

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

Le calcul de la transformation affine est plus compliqué. C'est là que la trigonométrie entre en jeu. Vous pouvez utiliser atan2 et les fonctions CGAffineTransformRotate et CGAffineTransformTranslate pour le créer, mais si vous vous souvenez de suffisamment de trigonométrie, vous pouvez le créer directement. Consultez "Les mathématiques derrière les matrices" dans le Guide de programmation Quartz 2D pour plus d'informations sur ce que je fais ici:

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

J'ai mis tout le code dans n Gist pour un copier/coller facile .

Avec cette catégorie, vous pouvez facilement dessiner des flèches:

sample arrow 1sample arrow 2

Étant donné que vous générez simplement un chemin, vous pouvez choisir de ne pas le remplir ou de ne pas le caresser comme dans cet exemple:

unstroked arrow sample

Sois quand même prudent. Ce code ne vous empêche pas d'obtenir des résultats géniaux si vous faites la largeur de la tête inférieure à la largeur de la queue, ou si vous faites la longueur de la tête plus grande que la longueur totale de la flèche:

narrow head samplehead too long sample

194
rob mayoff

Voici une version Swift de mon ancien code Objective-C. Elle devrait fonctionner dans Swift 3.2 et versions ultérieures).

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

}

Voici un exemple de la façon dont vous l'appeleriez:

let arrow = UIBezierPath.arrow(from: CGPoint(x: 50, y: 100), to: CGPoint(x: 200, y: 50),
        tailWidth: 10, headWidth: 25, headLength: 40)
15
rob mayoff
//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
copter244

Dans Swift 3.0, vous pouvez y parvenir avec

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
Sujatha Girijala