webentwicklung-frage-antwort-db.com.de

Zeichnen Sie Segmente aus einem Kreis oder einem Ring

Ich habe versucht, eine Möglichkeit zum Zeichnen von Segmenten zu finden, wie in der folgenden Abbildung dargestellt:

enter image description here

Ich möchte gerne:

  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 und ähnlichen Aufrufen zu tun, bin aber nicht sehr weit gekommen.

Kann jemand helfen ?

71
Chris Baxter

Ihre Frage besteht aus vielen Teilen.

Den Weg finden

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

Path to be stroked

Um den Weg zu streicheln, brauchen wir ihn zuerst. Das ist im Grunde genommen so einfach wie Verschieben nach den Startpunkt und Zeichnen eines Bogens um die Mitte vom aktuellen Winkel zum 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 gleich weit 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: Core Graphics in drawRect: oder formen Sie Ebenen mit Core Animation. Mit Core Graphics erhalten Sie das leistungsstärkere Zeichnen, mit Core Animation jedoch die bessere Animationsleistung. Da es sich um Pfade handelt, funktioniert reine Cora Animation nicht. Sie werden mit seltsamen Artefakten enden. Wir können jedoch eine Kombination aus Ebenen und Kerngrafiken verwenden, indem Sie den Grafikkontext der Ebene zeichnen.

Segment füllen und streicheln

Wir haben bereits die Grundform, aber bevor wir Farbverläufe und Schatten hinzufügen, mache ich eine Grundfüllung und einen Strich (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);

Dadurch wird so etwas auf dem Bildschirm angezeigt

Filled and stroked shape

Schatten hinzufügen

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

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

enter image description here

Verlauf zeichnen

Für den Farbverlauf benötigen wir eine Verlaufsebene. Ich mache hier einen sehr einfachen zweifarbigen Farbverlauf, aber Sie können ihn beliebig anpassen. Um den Farbverlauf zu erstellen, müssen wir die Farben und den geeigneten Farbraum ermitteln. Dann können wir den Farbverlauf über die Füllung zeichnen (aber vor dem Strich). Wir müssen auch den Farbverlauf auf den gleichen Pfad wie zuvor maskieren. Dazu beschneiden wir den Pfad.

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 über dieses Ergebnis verfügen

Masked gradient

Animation

Wenn es um die Animation der Form geht, hat es alles wurde zuvor geschrieben: Animieren von Kreisschnitten mit einem benutzerdefinierten CALayer . Wenn Sie versuchen, das Zeichnen durch einfaches Animieren der Pfadeigenschaft durchzuführen, werden Sie während der Animation einige wirklich irre Verzerrungen des Pfades bemerken. Der Schatten und der Farbverlauf wurden zur Veranschaulichung im Bild unten intakt gelassen.

Funky warping of path

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 bekommen, 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. Wie auch immer Sie die Eigenschaft shadowPath festlegen sollten, um die Leistung zu verbessern.

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

Verlauf zeichnen

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äre er über der Form und nicht darin. Nein, wir können die Form nicht mit einem Farbverlauf füllen (ich weiß, dass Sie darüber nachgedacht haben). Wir müssen den Farbverlauf maskieren, damit er außerhalb des Segments verläuft. Dazu erstellen wir noch eine Ebene, um die Maske dieses Segments zu sein. Es muss sein eine andere Ebene sein, der Dokumentation ist klar, dass das Verhalten "undefiniert" ist, wenn die Maske Teil der Ebenenhierarchie ist. Da das Koordinatensystem der Maske dem Koordinatensystem der Unterebenen des Verlaufs entspricht, müssen Sie die Segmentform vor dem Festlegen verschieben.

CAShapeLayer *mask = [CAShapeLayer layer];
CGAffineTransform translation = CGAffineTransformMakeTranslation(-CGRectGetMinX(gradient.frame),
                                                                 -CGRectGetMinY(gradient.frame));
mask.path = CGPathCreateCopyByTransformingPath(segment.path,
                                               &translation);
gradient.mask = mask;
180

Alles, was Sie brauchen, finden Sie im Quartz 2D Programming Guide . Ich schlage vor, Sie schauen sich das an.

Es kann jedoch schwierig sein, alles zusammenzustellen. Wir schreiben eine Funktion, die eine Größe annimmt und ein Bild zurückgibt, das in etwa einem Ihrer Segmente ähnelt:

arc with outline, gradient, and shadow

Wir starten die Funktionsdefinition wie folgt:

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 setze eine linke Klammer an das Ende dieser Zeile, weil mir eine zusätzliche Einrückungsebene gefällt, die mich daran erinnert, UIGraphicsEndImageContext später aufzurufen.

Da es sich bei vielen Funktionen, die wir aufrufen müssen, um Core Graphics-Funktionen (auch bekannt als Quartz 2D) handelt, und nicht um UIKit-Funktionen, müssen wir CGContext abrufen:

        CGContextRef gc = UIGraphicsGetCurrentContext();

Jetzt können wir richtig loslegen. 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 Linien-Cap-Stil auf "butt", damit die Enden abgeflacht sind :

        CGContextSetLineCap(gc, kCGLineCapButt);

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

        CGContextReplacePathWithStrokedPath(gc);

Um diesen Pfad mit einem linearen Farbverlauf zu füllen, müssen Sie Core Graphics anweisen, alle Operationen auf das Innere des Pfads zu beschränken. Dadurch wird der Pfad von Core Graphics zurückgesetzt, aber der Pfad wird später benötigt, um die schwarze Linie um den Rand herum zu zeichnen. Also kopieren wir den Pfad hier:

        CGPathRef path = CGContextCopyPath(gc);

Da das Segment einen Schatten werfen soll, werden die Schattenparameter vor dem Zeichnen festgelegt:

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

Wir werden beide das Segment füllen (mit einem Farbverlauf) und es streichen (um den schwarzen Umriss zu zeichnen). Wir wollen einen einzigen Schatten für beide Operationen. Wir sagen Core Graphics, dass mit dem Beginn einer Transparenzebene:

        CGContextBeginTransparencyLayer(gc, 0); {

Ich setze eine linke Klammer an das Ende dieser Zeile, weil ich eine zusätzliche Einrückungsebene haben möchte, um mich daran zu erinnern, CGContextEndTransparencyLayer später aufzurufen.

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

            CGContextSaveGState(gc); {

Ich setze eine linke Klammer an das Ende dieser Zeile, weil ich eine zusätzliche Einrückungsebene haben möchte, um mich daran zu erinnern, CGContextRestoreGState später aufzurufen.

Um den Pfad mit einem Farbverlauf zu füllen, müssen Sie ein Farbverlaufsobjekt 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 Farbverlauf 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 Wert 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 beschneiden wir 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 CGContextClip aufgerufen haben, hat Core Graphics den Pfad des Kontexts zurückgesetzt. 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 wieder in den Kontext zu setzen:

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

Jetzt können wir den Pfad streicheln, 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. Dies wird einen Blick auf das, was wir gezeichnet haben, werfen und den Schatten darunter hinzufügen:

        } CGContextEndTransparencyLayer(gc);

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

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

Sie können den Code alle zusammen finden in dieser Übersicht .

39
rob mayoff

Dies ist eine Swift Version von Rob Mayoffs Antwort. Mal sehen, wie viel effizienter diese Sprache ist! Dies könnte 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ß durchzuführen, habe ich den Operator ° postfix erstellt. So können Sie jetzt z. 45 ° 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 Dienstprogrammdatei Swift.

4
t1ser