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.
J'ai publié une version Swift version de cette réponse séparément.
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:
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:
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:
É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:
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:
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)
//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
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)
}
}