Zeichnen Sie Segmente aus einem Kreis oder Donut

72

Ich habe versucht, einen Weg zu finden, um Segmente zu zeichnen, wie in der folgenden Abbildung dargestellt:

Geben Sie hier die Bildbeschreibung ein

Ich möchte:

  1. Zeichnen Sie das Segment
  2. Farbverläufe einschließen
  3. Schatten einschließen
  4. Animieren Sie die Zeichnung von 0 bis n Winkel

Ich habe versucht, dies mit CGContextAddArcähnlichen Anrufen zu tun, bin aber nicht weit gekommen.

Kann jemand helfen ?

Chris Baxter
quelle
4
Kannst du posten, was du versucht hast? Ich habe immer gedacht, dass die eingebauten Zeichenfunktionen sehr intuitiv sind ...
Dave
Ich weiß, dass Sie vor langer Zeit gefragt und vielleicht eine Lösung gefunden haben. Aber überprüfe meinen Code, finde vielleicht etwas. github.com/sakrist/VBPieChart
SAKrisT
OP, können Sie bitte Ihr Bild erneut hochladen? Die Frage macht keinen Sinn, ohne sie zu sehen. Danke! :)
Mark A. Donohoe

Antworten:

181

Ihre Frage besteht aus vielen Teilen.

Den Weg finden

Das Erstellen des Pfads für ein solches Segment sollte nicht zu schwierig sein. Es gibt zwei Bögen und zwei gerade Linien. Ich habe zuvor erklärt, wie Sie einen solchen Pfad abbrechen können, damit ich es hier nicht mache. Stattdessen werde ich ausgefallen sein und den Pfad erstellen, indem ich einen anderen Pfad streichle. Sie können natürlich die Aufschlüsselung lesen und den Pfad selbst erstellen. Der Bogen, von dem ich spreche, ist der orangefarbene Bogen innerhalb des grau gestrichelten Endergebnisses.

Pfad gestreichelt werden

Um den Weg zu streicheln, brauchen wir ihn zuerst. Das ist im Grunde so einfach wie das Bewegen zum Startpunkt und das Zeichnen eines Bogens um die Mitte vom aktuellen Winkel zu dem Winkel, den das Segment abdecken soll.

CGMutablePathRef arc = CGPathCreateMutable();
CGPathMoveToPoint(arc, NULL,
                  startPoint.x, startPoint.y);
CGPathAddArc(arc, NULL,
             centerPoint.x, centerPoint.y,
             radius,
             startAngle,
             endAngle,
             YES);

Wenn Sie diesen Pfad (den einzelnen Bogen) haben, können Sie das neue Segment erstellen, indem Sie es mit einer bestimmten Breite streichen. Der resultierende Pfad wird die zwei geraden Linien und die zwei Bögen haben. Der Hub erfolgt von der Mitte in gleichem Abstand nach innen und außen.

CGFloat lineWidth = 10.0;
CGPathRef strokedArc =
    CGPathCreateCopyByStrokingPath(arc, NULL,
                                   lineWidth,
                                   kCGLineCapButt,
                                   kCGLineJoinMiter, // the default
                                   10); // 10 is default miter limit

Zeichnung

Als nächstes wird gezeichnet, und es gibt im Allgemeinen zwei Hauptoptionen: Kerngrafiken in drawRect:oder Formebenen mit Kernanimation. Mit Core Graphics erhalten Sie eine leistungsstärkere Zeichnung, mit Core Animation erhalten Sie jedoch eine bessere Animationsleistung. Da es sich um Pfade handelt, funktioniert die reine Cora-Animation nicht. Sie werden mit seltsamen Artefakten enden. Wir können jedoch eine Kombination aus Ebenen und Kerngrafiken verwenden, indem wir den Grafikkontext der Ebene zeichnen.

Das Segment füllen und streicheln

Wir haben bereits die Grundform, aber bevor wir Farbverläufe und Schatten hinzufügen, führe ich eine Grundfüllung und einen Strich durch (Sie haben einen schwarzen Strich in Ihrem Bild).

CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, strokedArc);
CGContextSetFillColorWithColor(c, [UIColor lightGrayColor].CGColor);
CGContextSetStrokeColorWithColor(c, [UIColor blackColor].CGColor);
CGContextDrawPath(c, kCGPathFillStroke);

Das wird so etwas auf den Bildschirm bringen

Gefüllte und gestreichelte Form

Schatten hinzufügen

Ich werde die Reihenfolge ändern und den Schatten vor dem Farbverlauf machen. Um den Schatten zu zeichnen, müssen wir einen Schatten für den Kontext konfigurieren und zeichnen, füllen Sie die Form, um ihn mit dem Schatten zu zeichnen. Dann müssen wir den Kontext wiederherstellen (bis vor den Schatten) und die Form erneut streichen.

CGColorRef shadowColor = [UIColor colorWithWhite:0.0 alpha:0.75].CGColor;
CGContextSaveGState(c);
CGContextSetShadowWithColor(c,
                            CGSizeMake(0, 2), // Offset
                            3.0,              // Radius
                            shadowColor);
CGContextFillPath(c);
CGContextRestoreGState(c);

// Note that filling the path "consumes it" so we add it again
CGContextAddPath(c, strokedArc);
CGContextStrokePath(c);

Zu diesem Zeitpunkt ist das Ergebnis ungefähr so

Geben Sie hier die Bildbeschreibung ein

Zeichnen des Verlaufs

Für den Verlauf benötigen wir eine Verlaufsebene. Ich mache hier einen sehr einfachen Zweifarbenverlauf, aber Sie können alles anpassen, was Sie wollen. Um den Verlauf zu erzeugen, müssen wir die Farben und den geeigneten Farbraum erhalten. Dann können wir den Farbverlauf über die Füllung zeichnen (aber vor dem Strich). Wir müssen auch den Farbverlauf auf demselben Pfad wie zuvor maskieren. Dazu schneiden wir den Pfad ab.

CGFloat colors [] = {
    0.75, 1.0, // light gray   (fully opaque)
    0.90, 1.0  // lighter gray (fully opaque)
};

CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceGray(); // gray colors want gray color space
CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace, colors, NULL, 2);
CGColorSpaceRelease(baseSpace), baseSpace = NULL;

CGContextSaveGState(c);
CGContextAddPath(c, strokedArc);
CGContextClip(c);

CGRect boundingBox = CGPathGetBoundingBox(strokedArc);
CGPoint gradientStart = CGPointMake(0, CGRectGetMinY(boundingBox));
CGPoint gradientEnd   = CGPointMake(0, CGRectGetMaxY(boundingBox));

CGContextDrawLinearGradient(c, gradient, gradientStart, gradientEnd, 0);
CGGradientRelease(gradient), gradient = NULL;
CGContextRestoreGState(c);

Damit ist die Zeichnung fertig, da wir derzeit dieses Ergebnis haben

Maskierter Farbverlauf

Animation

Wenn es um die Animation der Form geht, wurde alles schon einmal geschrieben: Animieren von Tortenschnitten mit einem benutzerdefinierten CALayer . Wenn Sie versuchen, die Zeichnung durch einfaches Animieren der Pfadeigenschaft zu erstellen, werden Sie während der Animation einige wirklich funky Verzerrungen des Pfads sehen. Der Schatten und der Farbverlauf wurden zur Veranschaulichung im Bild unten intakt gelassen.

Funky Verzerrung des Weges

Ich schlage vor, dass Sie den Zeichnungscode, den ich in dieser Antwort gepostet habe, in den Animationscode aus diesem Artikel übernehmen. Dann sollten Sie am Ende das haben, wonach Sie fragen.


Als Referenz: dieselbe Zeichnung mit Core Animation

Einfache Form

CAShapeLayer *segment = [CAShapeLayer layer];
segment.fillColor = [UIColor lightGrayColor].CGColor;
segment.strokeColor = [UIColor blackColor].CGColor;
segment.lineWidth = 1.0;
segment.path = strokedArc;

[self.view.layer addSublayer:segment];

Schatten hinzufügen

Die Ebene verfügt über einige schattenbezogene Eigenschaften, die Sie anpassen können. Sie sollten die Eigenschaft jedoch für eine verbesserte Leistung festlegenshadowPath .

segment.shadowColor = [UIColor blackColor].CGColor;
segment.shadowOffset = CGSizeMake(0, 2);
segment.shadowOpacity = 0.75;
segment.shadowRadius = 3.0;
segment.shadowPath = segment.path; // Important for performance

Zeichnen des Verlaufs

CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.colors = @[(id)[UIColor colorWithWhite:0.75 alpha:1.0].CGColor,  // light gray
                    (id)[UIColor colorWithWhite:0.90 alpha:1.0].CGColor]; // lighter gray
gradient.frame = CGPathGetBoundingBox(segment.path);

Wenn wir den Farbverlauf jetzt zeichnen würden, würde er über der Form liegen und nicht darin. Nein, wir können keine Verlaufsfüllung der Form haben (ich weiß, dass Sie daran gedacht haben). Wir müssen den Farbverlauf so maskieren, dass er außerhalb des Segments liegt. Dazu erstellen wir eine weitere Ebene als Maske dieses Segments. Es muss sich um eine andere Ebene handeln. Aus der Dokumentation geht hervor, dass das Verhalten "undefiniert" ist, wenn die Maske Teil der Ebenenhierarchie ist. Da das Koordinatensystem der Maske mit dem der Unterschichten für den Gradienten identisch sein wird, müssen wir die Segmentform verschieben, bevor wir sie einstellen.

CAShapeLayer *mask = [CAShapeLayer layer];
CGAffineTransform translation = CGAffineTransformMakeTranslation(-CGRectGetMinX(gradient.frame),
                                                                 -CGRectGetMinY(gradient.frame));
mask.path = CGPathCreateCopyByTransformingPath(segment.path,
                                               &translation);
gradient.mask = mask;
David Rönnqvist
quelle
1
Das Problem, das ich bei all diesen Antworten sehe, ist, dass sie einen linearen Gradienten verwenden, der den Übergang entlang einer geraden Linie ausführt. Ich denke, das OP (und sicherlich ich) möchte, dass der Gradientenübergang entlang des Bogens stattfindet . Stellen Sie sich das so vor: Wenn der gezeichnete Bogen 359 ° eines vollständigen Kreises von 360 ° wäre, sollte sich auf beiden Seiten der Lücke ein Ende der Verlaufsfarbe und das andere befinden.
Rick
@ Rick Für mich sieht es so aus, als würde das OP in der Abbildung einen linearen Farbverlauf verwenden. Deshalb habe ich so geantwortet, wie ich es getan habe, und ich vermute, dass dies auch für andere Antworten gilt. Wenn Sie möchten, können Sie eine neue Frage speziell zu anderen Arten von Verläufen stellen und auf diese Frage verweisen (auf diese verlinken).
David Rönnqvist
@ DavidRönnqvist können Sie dieses Problem lösen stackoverflow.com/q/32498195/3767017
Anurag Bhakuni
Für den ersten Schritt ist es überflüssig, zum Ausgangspunkt zu gelangen. Der Bogen ist gut definiert, ohne zuerst zum Startpunkt zu gehen.
Victor Engel
Wenn ich ein unregelmäßiges Bild habe, wie können wir CAGradientLayer in ImageView hinzufügen? In Core Graphic können wir CGContextClipToMask verwenden. Wie geht das jetzt?
rbbtsn0w
39

Alles, was Sie brauchen, finden Sie im Quartz 2D-Programmierhandbuch . Ich schlage vor, Sie schauen es durch.

Es kann jedoch schwierig sein, alles zusammenzusetzen, also werde ich Sie durch das Ganze führen. Wir schreiben eine Funktion, die eine Größe annimmt und ein Bild zurückgibt, das ungefähr wie eines Ihrer Segmente aussieht:

Bogen mit Umriss, Farbverlauf und Schatten

Wir starten die Funktionsdefinition folgendermaßen:

static UIImage *imageWithSize(CGSize size) {

Wir brauchen eine Konstante für die Dicke des Segments:

    static CGFloat const kThickness = 20;

und eine Konstante für die Breite der Linie, die das Segment umreißt:

    static CGFloat const kLineWidth = 1;

und eine Konstante für die Größe des Schattens:

    static CGFloat const kShadowWidth = 8;

Als nächstes müssen wir einen Bildkontext erstellen, in dem gezeichnet werden soll:

    UIGraphicsBeginImageContextWithOptions(size, NO, 0); {

Ich habe eine linke Klammer am Ende dieser Zeile angebracht, weil ich eine zusätzliche Einrückungsstufe mag, um mich daran zu erinnern, UIGraphicsEndImageContextspäter anzurufen .

Da viele der Funktionen, die wir aufrufen müssen, Core Graphics-Funktionen (auch bekannt als Quartz 2D) und keine UIKit-Funktionen sind, müssen wir Folgendes erhalten CGContext:

        CGContextRef gc = UIGraphicsGetCurrentContext();

Jetzt sind wir bereit, richtig loszulegen. Zuerst fügen wir dem Pfad einen Bogen hinzu. Der Bogen verläuft entlang der Mitte des Segments, das wir zeichnen möchten:

        CGContextAddArc(gc, size.width / 2, size.height / 2,
            (size.width - kThickness - kLineWidth) / 2,
            -M_PI / 4, -3 * M_PI / 4, YES);

Jetzt werden wir Core Graphics bitten, den Pfad durch eine "gestrichene" Version zu ersetzen, die den Pfad umreißt. Wir setzen zuerst die Dicke des Strichs auf die Dicke, die das Segment haben soll:

        CGContextSetLineWidth(gc, kThickness);

und wir setzen den Linienkappenstil auf "Hintern", damit wir quadratische Enden haben :

        CGContextSetLineCap(gc, kCGLineCapButt);

Dann können wir Core Graphics bitten, den Pfad durch eine gestrichene Version zu ersetzen:

        CGContextReplacePathWithStrokedPath(gc);

Um diesen Pfad mit einem linearen Farbverlauf zu füllen, müssen wir Core Graphics anweisen, alle Operationen im Inneren des Pfads zu befestigen. Dadurch wird der Pfad durch Core Graphics zurückgesetzt, der Pfad wird jedoch später benötigt, um die schwarze Linie um den Rand zu zeichnen. Also kopieren wir den Pfad hier:

        CGPathRef path = CGContextCopyPath(gc);

Da das Segment einen Schatten werfen soll, legen wir die Schattenparameter fest, bevor wir zeichnen:

        CGContextSetShadowWithColor(gc,
            CGSizeMake(0, kShadowWidth / 2), kShadowWidth / 2,
            [UIColor colorWithWhite:0 alpha:0.3].CGColor);

Wir werden das Segment sowohl füllen (mit einem Farbverlauf) als auch streichen (um den schwarzen Umriss zu zeichnen). Wir wollen einen einzigen Schatten für beide Operationen. Wir teilen Core Graphics dies mit, indem wir eine Transparenzebene beginnen:

        CGContextBeginTransparencyLayer(gc, 0); {

Ich habe eine linke Klammer am Ende dieser Zeile angebracht, weil ich eine zusätzliche Einrückungsstufe haben möchte, um mich daran zu erinnern, CGContextEndTransparencyLayerspäter anzurufen .

Da wir den Clip-Bereich des Kontexts zum Füllen ändern, aber nicht schneiden möchten, wenn wir den Umriss später streichen, müssen wir den Grafikstatus speichern:

            CGContextSaveGState(gc); {

Ich habe eine linke Klammer am Ende dieser Zeile angebracht, weil ich eine zusätzliche Einrückungsstufe haben möchte, um mich daran zu erinnern, CGContextRestoreGStatespäter anzurufen .

Um den Pfad mit einem Verlauf zu füllen, müssen wir ein Verlaufsobjekt erstellen:

                CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
                CGGradientRef gradient = CGGradientCreateWithColors(rgb, (__bridge CFArrayRef)@[
                    (__bridge id)[UIColor grayColor].CGColor,
                    (__bridge id)[UIColor whiteColor].CGColor
                ], (CGFloat[]){ 0.0f, 1.0f });
                CGColorSpaceRelease(rgb);

Wir müssen auch einen Startpunkt und einen Endpunkt für den Gradienten herausfinden. Wir werden den Pfadbegrenzungsrahmen verwenden:

                CGRect bbox = CGContextGetPathBoundingBox(gc);
                CGPoint start = bbox.origin;
                CGPoint end = CGPointMake(CGRectGetMaxX(bbox), CGRectGetMaxY(bbox));

und wir erzwingen, dass der Farbverlauf entweder horizontal oder vertikal gezeichnet wird, je nachdem, welcher Zeitraum länger ist:

                if (bbox.size.width > bbox.size.height) {
                    end.y = start.y;
                } else {
                    end.x = start.x;
                }

Jetzt haben wir endlich alles, was wir brauchen, um den Farbverlauf zu zeichnen. Zuerst schneiden wir auf den Pfad:

                CGContextClip(gc);

Dann zeichnen wir den Farbverlauf:

                CGContextDrawLinearGradient(gc, gradient, start, end, 0);

Dann können wir den Farbverlauf freigeben und den gespeicherten Grafikstatus wiederherstellen:

                CGGradientRelease(gradient);
            } CGContextRestoreGState(gc);

Als wir anriefen CGContextClip, setzte Core Graphics den Pfad des Kontexts zurück. Der Pfad ist nicht Teil des gespeicherten Grafikstatus. Deshalb haben wir früher eine Kopie gemacht. Jetzt ist es Zeit, diese Kopie zu verwenden, um den Pfad im Kontext erneut festzulegen:

            CGContextAddPath(gc, path);
            CGPathRelease(path);

Jetzt können wir den Pfad streichen, um den schwarzen Umriss des Segments zu zeichnen:

            CGContextSetLineWidth(gc, kLineWidth);
            CGContextSetLineJoin(gc, kCGLineJoinMiter);
            [[UIColor blackColor] setStroke];
            CGContextStrokePath(gc);

Als nächstes weisen wir Core Graphics an, die Transparenzebene zu beenden. Dadurch wird angezeigt, was wir gezeichnet haben, und der darunter liegende Schatten wird hinzugefügt:

        } CGContextEndTransparencyLayer(gc);

Jetzt sind wir alle mit dem Zeichnen fertig. Wir bitten UIKit, ein UIImageaus dem Bildkontext zu erstellen , dann den Kontext zu zerstören und das Bild zurückzugeben:

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

Sie finden den Code alle zusammen in dieser Übersicht .

Rob Mayoff
quelle
1
Hehe, es scheint, dass sowohl Sie als auch ich einige Zeit damit verbracht haben, eine lange Erklärung aller Kerngrafiken zu schreiben;)
David Rönnqvist
es ist sehr schön! Glückwunsch! :)
holex
3
Dadurch wird der Farbverlauf linear gezeichnet, aber wie sieht es mit dem Zeichnen entlang des Pfades aus? Für kürzere Bögen hat dies diese Illusion, aber für einen Bogen, der fast kreisförmig ist, ist die Illusion gebrochen. Ein Ende der Kreislinie sollte colorA und das andere colorB sein. Ist das möglich?
Tettoffensive
4

Dies ist eine Swift 3- Version von Rob Mayoffs Antwort. Sehen Sie, wie viel effizienter diese Sprache ist! Dies kann der Inhalt einer MView.swift-Datei sein:

import UIKit

class MView: UIView {

    var size = CGSize.zero

    override init(frame: CGRect) {
    super.init(frame: frame)
    size = frame.size
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    var niceImage: UIImage {

        let kThickness = CGFloat(20)
        let kLineWidth = CGFloat(1)
        let kShadowWidth = CGFloat(8)

        UIGraphicsBeginImageContextWithOptions(size, false, 0)

            let gc = UIGraphicsGetCurrentContext()!
            gc.addArc(center: CGPoint(x: size.width/2, y: size.height/2),
                   radius: (size.width - kThickness - kLineWidth)/2,
                   startAngle: -45°,
                   endAngle: -135°,
                   clockwise: true)

            gc.setLineWidth(kThickness)
            gc.setLineCap(.butt)
            gc.replacePathWithStrokedPath()

            let path = gc.path!

            gc.setShadow(
                offset: CGSize(width: 0, height: kShadowWidth/2),
                blur: kShadowWidth/2,
                color: UIColor.gray.cgColor
            )

            gc.beginTransparencyLayer(auxiliaryInfo: nil)

                gc.saveGState()

                    let rgb = CGColorSpaceCreateDeviceRGB()

                    let gradient = CGGradient(
                        colorsSpace: rgb,
                        colors: [UIColor.gray.cgColor, UIColor.white.cgColor] as CFArray,
                        locations: [CGFloat(0), CGFloat(1)])!

                    let bbox = path.boundingBox
                    let startP = bbox.origin
                    var endP = CGPoint(x: bbox.maxX, y: bbox.maxY);
                    if (bbox.size.width > bbox.size.height) {
                        endP.y = startP.y
                    } else {
                        endP.x = startP.x
                    }

                    gc.clip()

                    gc.drawLinearGradient(gradient, start: startP, end: endP,
                                          options: CGGradientDrawingOptions(rawValue: 0))

                gc.restoreGState()

                gc.addPath(path)

                gc.setLineWidth(kLineWidth)
                gc.setLineJoin(.miter)
                UIColor.black.setStroke()
                gc.strokePath()

            gc.endTransparencyLayer()


        let image = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return image
    }

    override func draw(_ rect: CGRect) {
        niceImage.draw(at:.zero)
    }
}

Rufen Sie es von einem viewController wie folgt auf:

let vi = MView(frame: self.view.bounds)
self.view.addSubview(vi)

Um die Umrechnung von Grad in Bogenmaß vorzunehmen, habe ich den ° postfix-Operator erstellt. Sie können jetzt also zB 45 ° verwenden und dies bewirkt die Umrechnung von 45 Grad in Bogenmaß. Dieses Beispiel gilt für Ints. Erweitern Sie diese auch für die Float-Typen, wenn Sie Folgendes benötigen:

postfix operator °

protocol IntegerInitializable: ExpressibleByIntegerLiteral {
  init (_: Int)
}

extension Int: IntegerInitializable {
  postfix public static func °(lhs: Int) -> CGFloat {
    return CGFloat(lhs) * .pi / 180
  }
}

Fügen Sie diesen Code in eine schnelle Dienstprogrammdatei ein.

t1ser
quelle