CAShapeLayer 및 UIBezierPath로 부드러운 원을 그리는 방법은 무엇입니까?


78

CAShapeLayer를 사용하고 그 위에 원형 경로를 설정하여 획이있는 원을 그리려고합니다. 그러나이 방법은 borderRadius를 사용하거나 CGContextRef에서 직접 경로를 그리는 것보다 화면에 렌더링 할 때 일관성이 떨어집니다.

다음은 세 가지 방법 모두의 결과입니다. 여기에 이미지 설명 입력

세 번째는 특히 상단과 하단의 획 내부에서 제대로 렌더링되지 않습니다.

나는 설정된 contentsScale에 속성을 [UIScreen mainScreen].scale.

이 세 개의 원에 대한 제 그리기 코드는 다음과 같습니다. 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구현 과 동일합니다 .

여기에 이미지 설명 입력

이 두 번째 예에서 문제를 볼 수 있습니다. 한 번은 CAShapeLayer빨간색을 사용하여 그렸고 drawRect:, 동일한 경로를 구현 하여 다시 맨 위에 녹색 으로 그렸습니다 . 아래의 빨간색 원에서 더 많은 블리드로 인해 훨씬 ​​더 많은 불일치를 볼 수 있습니다. 그것은 분명히 다른 (그리고 더 나쁜) 방식으로 그려지고 있습니다.

여기에 이미지 설명 입력


1
이미지 캡처와 관련이 있는지 확실하지 않지만 위의 세 원은 적어도 내 눈에는 정확히 동일하게 나타납니다.
Max MacLeod

이 라인 : [UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 4, 4)]self.bound너무 기술적으로가 아닌 망막 크기의 곡선을 만드는 포인트 (하지 픽셀)를 보유하고 있습니다. 이 크기에 [UIScreen mainScreen].scale값 을 곱하면 타원이 망막 화면에서 완벽하게 부드러워집니다.
holex 2014-06-26

@MaxMacLeod는 내 편집에서 차이점을 확인하십시오.
bcherry 2014-06-26

@holex 그것은 단지 더 작은 원을 만듭니다. 두 구현 모두 동일한 경로를 사용하고 있으며 하나는 좋아 보이고 다른 하나는 그렇지 않습니다.
bcherry 2014-06-26

문서에서 래스터 화는 품질보다는 속도를 선호한다고 말합니다. 부정확 한 보간 / 앤티 앨리어싱의 아티팩트를보고있을 수 있습니다.
Leo Natan

답변:


70

원을 그리는 방법이 너무 많다는 것을 누가 알았습니까?

TL은, DR은 : 당신이 사용하려는 경우 CAShapeLayer여전히 부드럽게 원을 얻을, 당신은 사용해야 shouldRasterize하고 rasterizationScale신중하게.

실물

여기에 이미지 설명 입력

여기에 원본 CAShapeLayerdrawRect버전 과의 차이점이 있습니다 . 레티 나 디스플레이가 장착 된 iPad Mini에서 스크린 샷을 만든 다음 Photoshop에서 마사지하여 최대 200 %까지 날 렸습니다. 명확하게 볼 수 있듯이 CAShapeLayer버전에는 특히 왼쪽과 오른쪽 가장자리에 눈에 띄는 차이가 있습니다 (차이에서 가장 어두운 픽셀).

화면 크기로 래스터 화

여기에 이미지 설명 입력

2.0레티 나 장치에 있어야하는 화면 크기로 래스터 화하겠습니다 . 이 코드를 추가하십시오.

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

참고 rasterizationScale기본값으로 1.0도 기본의 흐릿한 부분을 차지하는 망막 장치에 shouldRasterize.

이제 원이 조금 더 부드러워졌지만 불량 비트 (diff에서 가장 어두운 픽셀)가 위쪽 및 아래쪽 가장자리로 이동했습니다. 래스터 화하지 않는 것보다 눈에 띄게 낫지 않습니다!

2x 화면 스케일로 래스터 화

여기에 이미지 설명 입력

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

이것은 2x 화면 스케일 또는 최대 4.0레티 나 장치 에서 경로를 래스터 화합니다 .

이제 원이 눈에 띄게 부드러워지고 diff가 훨씬 더 가볍고 고르게 퍼집니다.

나는 또한 이것을 Instruments : Core Animation에서 실행했고 Core Animation Debug Options에서 큰 차이점을 보지 못했습니다. 그러나 오프 스크린 비트 맵을 화면에 블리 팅하는 것이 아니라 축소되기 때문에 속도가 느려질 수 있습니다. shouldRasterize = NO애니메이션하는 동안 일시적으로 설정해야 할 수도 있습니다 .

작동하지 않는 것

  • shouldRasterize = YES스스로 설정 합니다. 레티 나 기기에서는 rasterizationScale != screenScale.

  • 설정합니다 contentScale = screenScale. 이후 CAShapeLayer에 그려하지 않습니다 contents, 그것은 래스터 여부와 상관없이이는 연주에 영향을주지 않습니다.

저에게 처음으로 그것을 지적한 날카로운 그래픽 디자이너 Humaan의 Jay Hollywood에게 감사 드립니다.


답변 해주셔서 감사합니다! 이것은 확실히 일부 경우에 적절한 해결 방법을 제공하는 것 같습니다. @ 32Beat가 정확하다는 것을 알고 있습니까? CAShapeLayer는 애니메이션 최적화로 인해 낮은 충실도의 사본을 렌더링합니다. 직접 그리기가 2x 배율로 올바른 것을 그리는 것처럼 보이므로 배율을 높이면 문제가 해결되지만 궁극적으로 CAShapeLayer의 근본적인 문제는 해결되지 않습니다.
bcherry

2
CAShapeLayer빠른 그리기와 애니메이션에 최적화되어 있다고 확신 합니다. 그러나 여전히 내가 선호하는 다른 장점이 있습니다 drawRect. 해상도 독립성 (애플이 4x 레티 나 디스플레이를 생산할 때까지 기다리십시오), 메모리 감소, 왜곡없이 변형 가능, 유연한 래스터 화 등
Glen Low

Re : 유연한 래스터 화. 래스터 화할지 여부를 임시로 결정할 수 있으며, 예를 들어 여러 CAShapeLayer.
Glen Low

나는 애플이 CAShapeLayer렌 디션을 위해 높은 평탄도를 사용한다고 생각한다 . 때때로 비 레티 나 디스플레이에서 볼 수 있습니다. 변환은 다각형처럼 보입니다. 수정은 직접 평평하게하는 것입니다. 예 : ps.missouri.edu/ps2/support/tutorialfolder/flatness/index.html
Glen Low

9

아, 얼마 전에 같은 문제가 발생했습니다 (여전히 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. 
*/

원 모양 아래에 채워진 원은 채움 색상을 번질 것입니다. 색상에 따라 이것은 매우 눈에.니다. 그리고 사용자 상호 작용 중에 모양이 더 나빠질 것입니다. 즉, 애니메이션 목적을위한 것이기 때문에 shapelayer는 레이어 스케일 팩터에 관계없이 항상 스케일 팩터 1.0으로 렌더링됩니다.

즉 , 일반적인 레이어 속성을 통해 애니메이션 할 수있는 다른 속성이 아니라 bezierpath 의 모양 에 대한 애니메이션 가능한 변경이 필요한 경우에만 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;
}

나는 그것을 시도하지 않았지만 결과를 아는 것이 흥미로울 것입니다.


이것은 많은 의미가 있습니다. 나는 contentsScale을 무엇으로 설정했는지는 중요하지 않지만 여전히 똑같이 보입니다. displayLayer 구현을 시도했습니다. 안타깝게도 차이가 없었습니다. CAShapeLayer에 대한 애니메이션 중심의 그리기 최적화에 대해 당신이 맞다고 생각합니다. 애니메이션이 적용되지 않은 레이어에 대해 사용자 정의 드로잉을 계속 사용할 것입니다.
bcherry

4

나는 이것이 오래된 질문이라는 것을 알고 있지만 drawRect 메서드에서 작업을 시도하고 여전히 문제가있는 사람들에게는 UIGraphicsContext를 가져 오는 데 올바른 메서드를 사용하는 것이 크게 도움이 된 작은 조정이었습니다. 기본값 사용 :

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

다른 답변에서 어떤 제안을 따랐는지에 관계없이 흐릿한 원이 생깁니다. 마침내 나를 위해 한 것은 ImageContext를 얻는 기본 방법이 스케일링을 비 망막으로 설정한다는 것을 깨달았습니다. 레티 나 디스플레이에 대한 ImageContext를 얻으려면 다음 메서드를 사용해야합니다.

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

거기에서 정상적인 그리기 방법을 사용하면 잘 작동했습니다. 마지막 옵션을로 설정하면 0시스템이 장치 기본 화면의 배율을 사용하도록 지시합니다. 중간 옵션 false은 불투명 한 이미지 ( true이미지가 불투명 함을 의미)를 그릴 것인지 또는 투명 용으로 포함 된 알파 채널이 필요한 것인지를 그래픽 컨텍스트에 알리는 데 사용됩니다 . 자세한 내용은 다음과 같습니다. https://developer.apple.com/reference/uikit/1623912-uigraphicsbeginimagecontextwitho?language=objc


1

내가 추측은 CAShapeLayer그것의 모양을 렌더링보다 성능이 좋은 방법으로 백업하고 일부 바로 가기를 걸립니다. 어쨌든 CAShapeLayer메인 스레드에서 약간 느릴 수 있습니다. 다른 경로간에 애니메이션을 적용해야하는 경우가 아니라면 UIImage백그라운드 스레드에서 비동기 적으로 렌더링하는 것이 좋습니다 .


-1

이 메서드를 사용하여 UIBezierPath를 그립니다.

/*********draw circle where double tapped******/
- (UIBezierPath *)makeCircleAtLocation:(CGPoint)location radius:(CGFloat)radius
{
    self.circleCenter = location;
    self.circleRadius = radius;

    UIBezierPath *path = [UIBezierPath bezierPath];
    [path addArcWithCenter:self.circleCenter
                    radius:self.circleRadius
                startAngle:0.0
                  endAngle:M_PI * 2.0
                 clockwise:YES];

    return path;
}

그리고 이렇게 그려

CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.path = [[self makeCircleAtLocation:location radius:50.0] CGPath];
    shapeLayer.strokeColor = [[UIColor whiteColor] CGColor];
    shapeLayer.fillColor = nil;
    shapeLayer.lineWidth = 3.0;

    // Add CAShapeLayer to our view

    [gesture.view.layer addSublayer:shapeLayer];

나는 원형을 만들고 둥근 직사각형 경로를 사용하는이 방법을 시도했습니다. 둘 다 CAShapeLayer로 그린 경로의 가장자리를 따라 픽셀 화 문제를 해결하지 못합니다.
bcherry
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.