web-dev-qa-db-ja.com

円またはドーナツからセグメントを描く

次の図に示すように、セグメントを描画する方法を見つけようとしました。

enter image description here

そうしたいです:

  1. セグメントを描く
  2. グラデーションを含める
  3. 影を含める
  4. 図面を0からnの角度でアニメーション化します

私はCGContextAddArcおよび同様の呼び出しでこれを行おうとしましたが、それほど遠くには行きませんでした。

誰でも助けることができますか?

71
Chris Baxter

あなたの質問には多くの部分があります。

パスを取得する

このようなセグメントのパスを作成するのはそれほど難しくありません。 2つの円弧と2つの直線があります。 以前にそのようなパスを分解する方法を説明した なので、ここではそれをしません。代わりに、私は空想になり、別のパスをstrでてパスを作成します。もちろん、内訳を読んで、自分でパスを作成できます。ストロークについて説明しているアークは、灰色の破線の最終結果内のオレンジのアークです。

Path to be stroked

パスをストロークするには、最初に必要です。基本的には、開始点に移動し、中心の周りに円弧を描くと同じくらい簡単ですセグメントをカバーしたい。

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

次に、そのパス(単一の円弧)がある場合、特定の幅でストロークすることで新しいセグメントを作成できます。結果のパスには、2つの直線と2つの円弧が含まれます。ストロークは、中心から内側と外側に等距離で発生します。

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

次は描画です。通常、主に2つの選択肢があります。drawRect:またはCore Animationを含むシェイプレイヤー。 Core Graphicsはより強力な描画を提供しますが、Core Animationはより優れたアニメーション性能を提供します。パスが含まれているため、純粋なCora Animationは機能しません。あなたは奇妙なアーティファクトになってしまいます。ただし、レイヤーのグラフィックスコンテキストを描画することにより、レイヤーとコアグラフィックスの組み合わせを使用できます。

セグメントの塗りつぶしとストローク

すでに基本的な形状がありますが、グラデーションとシャドウを追加する前に、基本的な塗りとストロークを実行します(画像に黒いストロークがあります)。

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

これは画面にこのようなものを置きます

Filled and stroked shape

影を追加する

順序を変更し、グラデーションの前に影を付けます。影を描画するには、コンテキストの影を構成し、図形を塗りつぶして影で描画する必要があります。次に、コンテキストを(シャドウの前に)復元し、シェイプを再度ストロークする必要があります。

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

この時点で、結果は次のようになります

enter image description here

グラデーションの描画

グラデーションには、グラデーションレイヤーが必要です。ここでは非常に単純な2色のグラデーションを行っていますが、必要に応じてカスタマイズできます。グラデーションを作成するには、色と適切な色空間を取得する必要があります。次に、塗りの上に(ただし、ストロークの前に)グラデーションを描画できます。また、グラデーションを以前と同じパスにマスクする必要があります。これを行うには、パスをクリップします。

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

現在、この結果が得られているため、これで描画が終了します

Masked gradient

アニメーション

シェイプのアニメーションになると、 すべて以前に記述された:カスタムCALayerを使用したパイスライスのアニメーション化 です。パスプロパティを単純にアニメートして描画を試みると、アニメーション中にパスの本当にファンキーなゆがみが見られます。影とグラデーションは、下の画像の説明のためにそのまま残されています。

Funky warping of path

この回答に掲載した描画コードを使用して、その記事のアニメーションコードに採用することをお勧めします。そして、あなたはあなたが求めているもので終わるはずです。


参考:コアアニメーションを使用した同じ図面

無地

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];

影を追加する

レイヤーには、カスタマイズするのはあなた次第であるいくつかのシャドウ関連のプロパティがあります。 ただし、パフォーマンスを向上させるには、shadowPathプロパティを設定する必要があります

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

グラデーションの描画

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

ここでグラデーションを描いた場合、図形の内側ではなく図形の上になります。いいえ、シェイプをグラデーションで塗りつぶすことはできません(あなたが考えていたのは知っています)。セグメントの外に出るようにグラデーションをマスクする必要があります。そのために、そのセグメントのマスクとなるanotherレイヤーを作成します。それは別の層でなければなりません、マスクが層階層の一部である場合の動作は「未定義」であることはドキュメントから明らかです。マスクの座標系はグラデーションのサブレイヤーの座標系と同じになるため、セグメントの形状を設定する前に変換する必要があります。

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

必要なものはすべて Quartz 2D Programming Guide で説明されています。私はあなたがそれを通して見ることをお勧めします。

ただし、すべてをまとめるのは難しい場合があるため、説明します。サイズを取り、おおよそセグメントの1つに似た画像を返す関数を作成します。

arc with outline, gradient, and shadow

次のように関数定義を開始します。

static UIImage *imageWithSize(CGSize size) {

セグメントの厚さには定数が必要です。

    static CGFloat const kThickness = 20;

セグメントの輪郭を描く線の幅の定数:

    static CGFloat const kLineWidth = 1;

影のサイズの定数:

    static CGFloat const kShadowWidth = 8;

次に、描画するイメージコンテキストを作成する必要があります。

    UIGraphicsBeginImageContextWithOptions(size, NO, 0); {

後でUIGraphicsEndImageContextを呼び出すことを思い出させるために余分なレベルのインデントが必要なので、その行の最後に左中かっこを配置します。

呼び出す必要がある関数の多くは、UIKit関数ではなく、Core Graphics(別名Quartz 2D)関数であるため、CGContextを取得する必要があります。

        CGContextRef gc = UIGraphicsGetCurrentContext();

これで、本当に始める準備ができました。まず、パスに円弧を追加します。弧は、描画したいセグメントの中心に沿って走ります:

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

次に、Core Graphicsに、パスの輪郭を描く「ストローク」バージョンでパスを置き換えるように依頼します。まず、線の太さをセグメントに設定したい太さに設定します。

        CGContextSetLineWidth(gc, kThickness);

ラインキャップスタイルを「突き合わせ」に設定して、四角い端を作成します

        CGContextSetLineCap(gc, kCGLineCapButt);

その後、Core Graphicsにパスをストロークバージョンに置き換えるように依頼できます。

        CGContextReplacePathWithStrokedPath(gc);

このパスを線形グラデーションで塗りつぶすには、Core Graphicsにすべての操作をパスの内部にクリップするように指示する必要があります。そうすると、Core Graphicsはパスをリセットしますが、後でEdgeの周りに黒い線を描くためにパスが必要になります。そこで、ここにパスをコピーします。

        CGPathRef path = CGContextCopyPath(gc);

セグメントに影を落とすため、描画を行う前に影のパラメーターを設定します。

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

セグメントの塗りつぶし(グラデーション)とストローク(黒のアウトラインの描画)の両方を行います。両方の操作に単一のシャドウが必要です。透明レイヤーを開始することにより、Core Graphicsに次のことを伝えます。

        CGContextBeginTransparencyLayer(gc, 0); {

後でCGContextEndTransparencyLayerを呼び出すことを思い出させるために、追加のレベルのインデントが必要なので、その行の最後に左中かっこを配置します。

塗りつぶしのためにコンテキストのクリップ領域を変更しますが、後でアウトラインをストロークするときにクリップしたくないので、グラフィックの状態を保存する必要があります。

            CGContextSaveGState(gc); {

後でCGContextRestoreGStateを呼び出すことを思い出させるために、追加のレベルのインデントが必要なので、その行の最後に左中かっこを配置します。

パスをグラデーションで塗りつぶすには、グラデーションオブジェクトを作成する必要があります。

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

また、グラデーションの開始点と終了点を把握する必要があります。パス境界ボックスを使用します。

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

グラデーションを水平または垂直のどちらか長い方に強制的に描画します。

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

これで、グラデーションを描画するために必要なものがすべて揃いました。まず、パスにクリップします。

                CGContextClip(gc);

次に、グラデーションを描画します。

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

その後、グラデーションを解放し、保存されたグラフィックス状態を復元できます。

                CGGradientRelease(gradient);
            } CGContextRestoreGState(gc);

CGContextClipを呼び出すと、Core Graphicsはコンテキストのパスをリセットしました。パスは保存されたグラフィックス状態の一部ではありません。そのため、以前にコピーを作成しました。今度は、そのコピーを使用して、コンテキスト内のパスを再度設定します。

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

これで、パスをストロークして、セグメントの黒い輪郭を描くことができます。

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

次に、透明度レイヤーを終了するようにCore Graphicsに指示します。これにより、描画したものが見えるようになり、下に影が追加されます。

        } CGContextEndTransparencyLayer(gc);

これで描画は完了です。 UIKitに画像コンテキストからUIImageを作成し、コンテキストを破棄して画像を返すように依頼します。

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

コードはすべて一緒に見つけることができます この要点で

39
rob mayoff

これはSwift Rob Mayoffの回答のバージョンです。この言語がどれほど効率的であるかを見てください!これは、MView.Swiftファイルの内容です。

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

次のようなviewControllerから呼び出します。

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

度からラジアンへの変換を行うために、°後置演算子を作成しました。これで、たとえば45°これは、45度からラジアンへの変換を行います。この例はInt向けですが、必要に応じてこれらをFloat型にも拡張します。

postfix operator °

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

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

このコードをユーティリティSwiftファイルに入れます。

4
t1ser