1. ホーム
  2. ios

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

2023-08-06 09:27:01

質問

私はCAShapeLayerを使用し、その上に円形のパスを設定することによって、ストロークした円を描画しようとしています。 しかし、この方法は、borderRadiusを使用したり、CGContextRefで直接パスを描画するよりも、画面にレンダリングされたときに一貫して精度が低いです。

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

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

I ある を設定します。 contentsScale プロパティに [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: と書かれた同じ円を描き、その上に再び緑色で描きました。赤のにじみが少ないことに注意してください。これらの円は両方とも、quot;smooth"(そして cornerRadius の実装と同じです)。

この2つ目の例では、問題があることがわかります。を使って一度描画しています。 CAShapeLayer を赤で、その上にもう一度 drawRect: の実装を緑色で表示しています。下の赤い円からのにじみで、より多くの矛盾が見られることに注意してください。これは明らかに異なる (そして悪い) 方法で描かれています。

どのように解決するのですか?

円を描くのにこんなにたくさんの方法があるなんて。

TL;DR。 もし CAShapeLayer を使って、なおかつ滑らかな円を得たいのであれば shouldRasterizerasterizationScale を注意深く見てください。

オリジナル

以下は、あなたのオリジナルの CAShapeLayer との差分です。 drawRect のバージョンです。iPad Mini with Retina Displayでスクリーンショットを作成し、Photoshopでマスキングして、200%にブローアップしています。明らかにわかるように CAShapeLayer バージョンでは、特に左右の端 (差分では最も暗いピクセル) に目に見える違いがあります。

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

画面スケールでラスタライズしてみましょう。 2.0 であるべきです。このコードを追加します。

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

なお rasterizationScale はデフォルトで 1.0 であるため、デフォルトの shouldRasterize .

円は少し滑らかになりましたが、悪い部分 (差分の最も暗いピクセル) が上下の端に移動しています。ラスタライズを行わない場合よりも、はるかに良いとは言えません!

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

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

これは、パスを2倍の画面スケールでラスタライズします。 4.0 まで拡大します。

円は目に見えて滑らかになり、差分はより軽く、均等に広がるようになりました。

また、Instruments.Core Animation でこれを実行しました。Core Animation Debug Options では、大きな違いは見られませんでした。しかし、オフスクリーンのビットマップを画面にブライトするだけでなく、ダウンスケールしているため、より遅くなる可能性があります。また、一時的に shouldRasterize = NO を一時的に設定する必要があるかもしれません。

動作しないもの

  • 設定 shouldRasterize = YES を単体で設定します。 Retinaデバイスでは、これがぼやけて見えるのは rasterizationScale != screenScale .

  • 設定 contentScale = screenScale . 以降 CAShapeLayer には描画されません。 contents に描画しないので、ラスタライズしていてもしていなくても、描画には影響しません。

Jay Hollywood のクレジット フマーン のジェイ・ハリウッド (Jay Hollywood) が指摘してくれました。