Pertanyaan Bagaimana saya bisa menggambar panah menggunakan Core Graphics?


Saya harus menggambar garis dengan panah di ujungnya di aplikasi Draw saya. Saya tidak pandai dalam trigonometri, jadi tidak bisa memecahkan masalah ini.

Pengguna meletakkan jarinya di layar dan menarik garis ke segala arah. Jadi, panah harus muncul di ujung garis.


31
2017-11-23 12:10


asal


Jawaban:


MEMPERBARUI

Saya telah memposting versi Swift dari jawaban ini secara terpisah.

ASLI

Ini adalah masalah kecil yang menyenangkan. Pertama-tama, ada banyak cara untuk menggambar anak panah, dengan sisi melengkung atau lurus. Mari kita pilih cara yang sangat sederhana dan beri label pengukuran yang kita butuhkan:

arrow parts

Kami ingin menulis fungsi yang mengambil titik awal, titik akhir, lebar ekor, lebar kepala, dan panjang kepala, dan mengembalikan jalur yang menguraikan bentuk panah. Mari buat kategori bernama dqd_arrowhead untuk menambahkan metode ini ke 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

Karena ada tujuh sudut di jalur panah, mari mulai penerapan kami dengan memberi nama yang konstan:

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

Oke, bagian yang mudah dilakukan. Sekarang, bagaimana kita menemukan koordinat dari ketujuh titik tersebut di jalan? Jauh lebih mudah untuk menemukan titik-titik jika panah sejajar sepanjang sumbu X:

axis-aligned arrow points

Ini sangat mudah untuk menghitung titik koordinat pada panah yang disejajarkan dengan sumbu, tetapi kita akan membutuhkan panjang keseluruhan panah untuk melakukannya. Kami akan menggunakan hypotf fungsi dari pustaka standar:

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

Kami akan memanggil metode pembantu untuk benar-benar menghitung tujuh poin:

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

Tapi kita perlu mengubah titik-titik itu, karena secara umum kita tidak mencoba membuat panah yang disejajarkan dengan sumbu. Untungnya, Core Graphics mendukung sejenis transformasi yang disebut transformasi affine, yang memungkinkan kami memutar dan menerjemahkan (slide) poin. Kami akan memanggil metode pembantu lain untuk membuat transformasi yang mengubah panah sumbu-sumbu kami ke panah yang kami minta:

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

Sekarang kita dapat membuat jalur Core Graphics menggunakan titik-titik panah yang disejajarkan dengan sumbu dan transformasi yang mengubahnya menjadi panah yang kita inginkan:

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

Akhirnya, kita bisa membungkus UIBezierPath sekitar CGPath dan kembalikan:

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

Inilah metode pembantu yang menghitung koordinat titik. Ini cukup sederhana. Rujuk kembali ke diagram panah poros sejajar jika Anda perlu.

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

Komputasi transformasi affine lebih rumit. Di sinilah trigonometri datang. Anda bisa menggunakan atan2 dan CGAffineTransformRotate dan CGAffineTransformTranslate berfungsi untuk membuatnya, tetapi jika Anda ingat cukup trigonometri, Anda dapat membuatnya secara langsung. Berkonsultasi "The Math Behind the Matrices" dalam Panduan Pemrograman 2D Quartz untuk informasi lebih lanjut tentang apa yang saya lakukan di sini:

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

Saya sudah memasukkan semua kode sebuah intisari untuk mudahnya menyalin 'n'paste.

Dengan kategori ini, Anda dapat dengan mudah menggambar panah:

sample arrow 1  sample arrow 2

Karena Anda baru saja membuat jalur, Anda dapat memilih untuk tidak mengisinya, atau tidak mengelusnya seperti dalam contoh ini:

unstroked arrow sample

Anda harus berhati-hati. Kode ini tidak mencegah Anda mendapatkan hasil yang funky jika Anda membuat lebar kepala lebih kecil dari lebar ekor, atau jika Anda membuat panjang kepala lebih besar dari total panjang panah:

narrow head sample  head too long sample


178
2017-11-26 06:01



Ini adalah versi Swift dari kode Objective-C saya yang lama. Ini harus bekerja baik Swift 3.2 atau 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)
    }

}

Berikut ini contoh bagaimana Anda menyebutnya:

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



Di Swift 3.0 Anda dapat mencapai ini dengan

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