web-dev-qa-db-ja.com

CAShapeLayerとUIBezierPathで滑らかな円を描く方法は?

CAShapeLayerを使用して円形のパスを設定することで、ストローク付きの円を描画しようとしています。ただし、このメソッドは、borderRadiusを使用したり、CGContextRefにパスを直接描画したりするよりも、画面にレンダリングしたときの一貫性が低くなります。

以下は、3つの方法すべての結果です。 enter image description here

特に上部と下部のストローク内では、3番目のレンダリングが不十分であることに注意してください。

I havecontentsScaleプロパティを[UIScreen mainScreen].scaleに設定します。

これらの3つの円の描画コードは次のとおりです。 CAShapeLayerをスムーズに描画するには何が欠けていますか?

@interface BCViewController ()

@end

@interface BCDrawingView : UIView

@end

@implementation BCDrawingView

- (id)initWithFrame:(CGRect)frame
{
    if ((self = [super initWithFrame:frame])) {
        self.backgroundColor = nil;
        self.opaque = YES;
    }

    return self;
}

- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];

    [[UIColor whiteColor] setFill];
    CGContextFillRect(UIGraphicsGetCurrentContext(), rect);

    CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), NULL);
    [[UIColor redColor] setStroke];
    CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 1);
    [[UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 4, 4)] stroke];
}

@end

@interface BCShapeView : UIView

@end

@implementation BCShapeView

+ (Class)layerClass
{
    return [CAShapeLayer class];
}

- (id)initWithFrame:(CGRect)frame
{
    if ((self = [super initWithFrame:frame])) {
        self.backgroundColor = nil;
        CAShapeLayer *layer = (id)self.layer;
        layer.lineWidth = 1;
        layer.fillColor = NULL;
        layer.path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 4, 4)].CGPath;
        layer.strokeColor = [UIColor redColor].CGColor;
        layer.contentsScale = [UIScreen mainScreen].scale;
        layer.shouldRasterize = NO;
    }

    return self;
}

@end


@implementation BCViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIView *borderView = [[UIView alloc] initWithFrame:CGRectMake(24, 104, 36, 36)];
    borderView.layer.borderColor = [UIColor redColor].CGColor;
    borderView.layer.borderWidth = 1;
    borderView.layer.cornerRadius = 18;
    [self.view addSubview:borderView];

    BCDrawingView *drawingView = [[BCDrawingView alloc] initWithFrame:CGRectMake(20, 40, 44, 44)];
    [self.view addSubview:drawingView];

    BCShapeView *shapeView = [[BCShapeView alloc] initWithFrame:CGRectMake(20, 160, 44, 44)];
    [self.view addSubview:shapeView];

    UILabel *borderLabel = [UILabel new];
    borderLabel.text = @"CALayer borderRadius";
    [borderLabel sizeToFit];
    borderLabel.center = CGPointMake(borderView.center.x + 26 + borderLabel.bounds.size.width/2.0, borderView.center.y);
    [self.view addSubview:borderLabel];

    UILabel *drawingLabel = [UILabel new];
    drawingLabel.text = @"drawRect: UIBezierPath";
    [drawingLabel sizeToFit];
    drawingLabel.center = CGPointMake(drawingView.center.x + 26 + drawingLabel.bounds.size.width/2.0, drawingView.center.y);
    [self.view addSubview:drawingLabel];

    UILabel *shapeLabel = [UILabel new];
    shapeLabel.text = @"CAShapeLayer UIBezierPath";
    [shapeLabel sizeToFit];
    shapeLabel.center = CGPointMake(shapeView.center.x + 26 + shapeLabel.bounds.size.width/2.0, shapeView.center.y);
    [self.view addSubview:shapeLabel];
}


@end

編集:違いを見ることができない人のために、私は互いの上に円を描き、ズームインしました:

ここでは、drawRect:で赤い円を描き、その上に緑色でdrawRect:で再び同じ円を描きました。赤の限定的なブリードに注意してください。これらの円は両方とも「滑らか」です(cornerRadius実装と同じです):

enter image description here

この2番目の例では、問題が表示されます。赤でCAShapeLayerを使用して一度描画し、同じパスのdrawRect:実装を上に重ねて描画しましたが、緑で描画しました。下にある赤い円からのにじみが多く、より多くの不整合が見られることに注意してください。それは明らかに、異なる(そしてより悪い)方法で描かれています。

enter image description here

72
bcherry

円を描く方法がたくさんあることを誰が知っていましたか?

TL; DR:CAShapeLayerを使用し、それでも滑らかな円を取得するには、shouldRasterizeを使用する必要があります。 rasterizationScale慎重に。

オリジナル

enter image description here

元のCAShapeLayerdrawRectバージョンからの差分があります。 Retina Displayを使用してiPad Miniからスクリーンショットを作成し、Photoshopでマッサージして、最大200%吹き飛ばしました。はっきりとわかるように、CAShapeLayerバージョンには、特に左右のエッジ(diffの最も暗いピクセル)で目に見える違いがあります。

画面スケールでラスタライズ

enter image description here

スクリーンスケールでラスタライズしましょう。2.0 Retinaデバイス。このコードを追加してください:

layer.rasterizationScale = [UIScreen mainScreen].scale;
layer.shouldRasterize = YES;

rasterizationScaleのデフォルトは1.0デフォルトの曖昧さを説明するRetinaデバイスでもshouldRasterize

円は少し滑らかになりましたが、不良ビット(差分の最も暗いピクセル)は上端と下端に移動しました。ラスタライズを行わないよりもかなり優れているわけではありません!

2倍の画面スケールでラスタライズ

enter image description here

layer.rasterizationScale = 2.0 * [UIScreen mainScreen].scale;
layer.shouldRasterize = YES;

これにより、パスが2倍のスクリーンスケールまたは最大4.0 Retinaデバイス。

円は見た目より滑らかになり、差分はより軽くなり、均等に広がります。

Instruments:Core Animationでもこれを実行しましたが、Core Animationのデバッグオプションに大きな違いは見られませんでした。ただし、画面にオフスクリーンビットマップをブリッティングするだけでなく、ダウンスケーリングするため、速度が遅くなる場合があります。また、一時的にshouldRasterize = NOアニメーション中。

機能しないもの

  • Set shouldRasterize = YES単独。Retinaデバイスでは、rasterizationScale != screenScale

  • Set contentScale = screenScaleCAShapeLayercontentsに描画されないため、ラスタライズされているかどうかに関係なく、レンディションには影響しません。

Humaan のジェイハリウッドへのクレジット、私に最初に指摘した鋭いグラフィックデザイナー。

67
Glen Low

ああ、私はしばらく前に同じ問題にぶつかりました(まだiOS 5でiircでした)。コードに次のコメントを書きました。

/*
    ShapeLayer
    ----------
    Fixed equivalent of CAShapeLayer. 
    CAShapeLayer is meant for animatable bezierpath 
    and also doesn't cache properly for retina display. 
    ShapeLayer converts its path into a pixelimage, 
    honoring any displayscaling required for retina. 
*/

円の下にある塗りつぶされた円は、塗りつぶし色を塗りつぶします。色に応じて、これは非常に顕著です。また、ユーザーの操作中にシェイプのレンダリングがさらに悪くなります。これは、シェープレイヤーは、アニメーションを目的とするため、レイヤーのスケールファクターに関係なく、常に1.0のスケールファクターでレンダリングされると結論付けます。

つまり、通常のレイヤープロパティを介してアニメート可能な他のプロパティではなく、ベジエパスの形状にアニメート可能な変更が特に必要な場合にのみCAShapeLayerを使用します。

最終的に、独自の結果をキャッシュする単純なShapeLayerを作成することにしましたが、displayLayer:またはdrawLayer:inContextを実装してみてください。

何かのようなもの:

- (void)displayLayer:(CALayer *)layer
{
    UIImage *image = nil;

    CGContextRef context = UIImageContextBegin(layer.bounds.size, NO, 0.0);
    if (context != nil)
    {
        [layer renderInContext:context];
        image = UIImageContextEnd();
    }

    layer.contents = image;
}

私はそれを試していませんが、結果を知ることは興味深いでしょう...

9
32Beat

これは古い質問であることは知っていますが、drawRectメソッドで作業しようとしていて、まだ問題がある場合は、正しいメソッドを使用してUIGraphicsContextを取得するのに非常に役立つ小さな調整が1つありました。デフォルトを使用:

let newSize = CGSize(width: 50, height: 50)
UIGraphicsBeginImageContext(newSize)
let context = UIGraphicsGetCurrentContext()

私が他の答えからどの提案に従ったとしても、ぼやけた円になります。最終的に私のためにしたことは、ImageContextを取得するためのデフォルトの方法がスケーリングを非網膜に設定することを認識していたことです。 RetinaディスプレイのImageContextを取得するには、このメソッドを使用する必要があります。

let newSize = CGSize(width: 50, height: 50)
UIGraphicsBeginImageContextWithOptions(newSize, false, 0)
let context = UIGraphicsGetCurrentContext()

そこから通常の描画方法を使用するとうまくいきました。最後のオプションを0に設定すると、デバイスのメイン画面のスケーリング係数を使用するようシステムに指示します。中央のオプションfalseは、不透明な画像(trueは画像が不透明であることを意味する)か、透明度にアルファチャネルを含める必要があるかをグラフィックコンテキストに伝えるために使用されます。適切なAppleコンテキストに関するドキュメント: https://developer.Apple.com/reference/uikit/1623912-uigraphicsbeginimagecontextwitho?language=objc

4
JiuJitsuCoder

CAShapeLayerは、その形状をレンダリングするよりパフォーマンスの高い方法に支えられており、いくつかのショートカットを取っていると思います。とにかくCAShapeLayerはメインスレッド上で少し遅くなる可能性があります。異なるパス間でアニメーション化する必要がない限り、バックグラウンドスレッドでUIImageに非同期的にレンダリングすることをお勧めします。

1
hfossli