如何在IOS中将可调整大小的UIView的一个角落圆角化?

6

我正在使用下面的代码来圆角化我的UIView的一个角:

UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
    self.view.bounds byRoundingCorners:(UIRectCornerTopLeft) cornerRadii:
    CGSizeMake(10.0, 10.0)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.view.bounds;
maskLayer.path = maskPath.CGPath;
self.view.layer.mask = maskLayer;
self.view.layer.masksToBounds = NO;

只要我不调整视图大小,这段代码就可以工作。如果我将视图放大,新区域不会出现,因为它超出了遮罩层的边界(这个遮罩层不会自动随着视图调整大小)。我可以让遮罩层尽可能大,但在iPad上可能会全屏显示,所以我担心使用如此大的遮罩会影响性能(我的UI中会有多个这样的元素)。此外,一个超大的遮罩无法满足我需要单独将右上角圆角化的情况。

有没有更简单、更容易实现的方法呢?

更新:我想要实现的效果如下: http://i.imgur.com/W2AfRBd.png (我想要的圆角在这里用绿色圈圈标出)。

我已经通过重写 viewDidLayoutSubviews 的方式,在UINavigationController 的子类中实现了这个效果。

- (void)viewDidLayoutSubviews {
    CGRect rect = self.view.bounds;
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:rect 
        byRoundingCorners:UIRectCornerTopLeft cornerRadii:CGSizeMake(8.0, 8.0)];
    self.maskLayer = [CAShapeLayer layer];
    self.maskLayer.frame = rect;
    self.maskLayer.path = maskPath.CGPath;
    self.view.layer.mask = self.maskLayer;
}

我使用我的视图控制器实例化了一个UINavigationController子类,然后将导航控制器视图的框架向下偏移20像素(y)以显示状态栏并留出44像素高的导航栏(如图片所示)。
这段代码可以正常工作,但它不能很好地处理旋转。当应用程序旋转时,viewDidLayoutSubviews在旋转之前就被调用,而我的代码则在旋转后创建了一个适合视图的遮罩层。这会导致旋转时一些本该被隐藏的部分暴露出来,从而产生不良的块状效果。而且,在没有遮罩的情况下,应用程序的旋转是非常平滑的,但在有了遮罩���后,旋转会变得明显卡顿和缓慢。
iPad应用程序Evomail也有像这样的圆角,并且他们的应用程序也遇到了同样的问题。

1
你能贴出现有解决方案的图片吗?我很难想象你想要什么样的。 - Warren Burton
@JackWu:是的,一个比较可行的解决方案是一开始创建一个带有圆角左上角和1024高度和宽度的遮罩。问题在于这只是一个例子 - 设计师实际上希望所有四个角都是圆角的。 - MusiGenesis
@MusiGenesis 等等……如果设计师想要四个角都是圆角,为什么不直接使用图层的cornerRadius属性呢?或者他们想要任意一个角落都可以单独设置成圆角吗? - Jack
任意角落 - 最多一次只有三个(左上,右上和左下),有时只有两个或一个。 - MusiGenesis
我理解背景是一张图片。背景中是否有动画内容? - TomSwift
显示剩余4条评论
9个回答

5
问题是,CoreAnimation属性在UIKit动画块中无法实现动画效果。您需要创建一个单独的动画,其曲线和持续时间与UIKit动画相同。
我在viewDidLoad中创建了蒙版层。当视图即将布局时,我只修改蒙版层的path属性。
您不知道布局回调方法中的旋转持续时间,但在旋转之前(并在触发布局之前),您会知道它,因此可以在那里保留它。
以下代码运行良好。
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    //Keep duration for next layout.
    _duration = duration;
}

-(void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];

    UIBezierPath* maskPath = [UIBezierPath bezierPathWithRoundedRect:self.view.bounds byRoundingCorners:UIRectCornerTopLeft cornerRadii:CGSizeMake(10, 10)];

    CABasicAnimation* animation;

    if(_duration > 0)
    {
        animation = [CABasicAnimation animationWithKeyPath:@"path"];
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

        [animation setDuration:_duration];
        //Set old value
        [animation setFromValue:(id)((CAShapeLayer*)self.view.layer.mask).path];
        //Set new value
        [animation setToValue:(id)maskPath.CGPath];
    }
    ((CAShapeLayer*)self.view.layer.mask).path = maskPath.CGPath;

    if(_duration > 0)
    {
        [self.view.layer.mask addAnimation:animation forKey:@"path"];
    }

    //Zero duration for next layout.
    _duration = 0;
}

@MusiGenesis 你测试过我的方法了吗? - Léo Natan
抱歉,现在还没有,我正在解决其他问题。 - MusiGenesis
由于此赏金即将结束,您现在应该真正测试一下。 - erdekhayser
抱歉,我正在度假。同时,我认为最佳答案会自动被选为悬赏答案。 - MusiGenesis

2

我知道这是一个相当hack的方法,但你不可以在角落上方添加一个png图形吗?

虽然这看起来有点难看,但它不会影响性能,如果它是一个分视图,旋转是没问题的,用户也不会注意到。


我们将在下面放置一张相当复杂的图片。如果背景只是纯黑色,那么你的建议可能行得通。 - MusiGenesis
这实际上是我最终不得不做的事情。 - MusiGenesis
我喜欢你因为提出唯一有效建议而被踩的样子。 - MusiGenesis

1
你可以创建一个继承自你正在使用的视图的子类,并重写“layoutSubviews”方法。每当你的视图尺寸改变时,都会调用这个方法。
即使“self.view”(在你的代码中引用)是你的视图控制器的视图,你仍然可以在故事板中将此视图设置为自定义类。以下是子类的修改后代码:
- (void)layoutSubviews {
    [super layoutSubviews];

    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
                          self.bounds byRoundingCorners:(UIRectCornerTopLeft) cornerRadii:
                          CGSizeMake(10.0, 10.0)];
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = self.bounds;
    maskLayer.path = maskPath.CGPath;
    self.layer.mask = maskLayer;
    self.layer.masksToBounds = NO;
}

请看我上面的编辑。这基本上就是我正在做的事情,尽管是通过子类化UINavigationController并覆盖viewDidLayoutSubviews来实现的。问题在于,在动画或旋转之前,layoutSubviewsviewDidLayoutSubviews只会被调用一次(而不是连续调用),从而产生尴尬、抖动的效果。 - MusiGenesis
要让它们持续调用,您需要重写drawRect:就像我在我的答案中建议的那样。 - tanz

1
我认为你应该创建一个自定义视图,每当需要更新时它会自动更新,也就是说,每当调用setNeedsDisplay时。
我的建议是创建一个自定义UIView子类,实现如下:
// OneRoundedCornerUIView.h

#import <UIKit/UIKit.h>

@interface OneRoundedCornerUIView : UIView //Subclass of UIView

@end


// OneRoundedCornerUIView.m

#import "OneRoundedCornerUIView.h"

@implementation OneRoundedCornerUIView

- (void) setFrame:(CGRect)frame
{
    [super setFrame:frame];
    [self setNeedsDisplay];
}

// Override drawRect as follows.
- (void)drawRect:(CGRect)rect
{
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
                          self.bounds byRoundingCorners:(UIRectCornerTopLeft) cornerRadii:
                          CGSizeMake(10.0, 10.0)];
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = self.bounds;
    maskLayer.path = maskPath.CGPath;
    self.layer.mask = maskLayer;
    self.layer.masksToBounds = NO;
}
@end

一旦您这样做了,您只需要将视图更改为OneRoundedCornerUIView实例,而不是UIView实例,每次调整大小或更改其框架时,视图都会平滑更新。我刚刚进行了一些测试,看起来完美无缺。 此解决方案也可以轻松定制,以便从您的视图控制器中轻松设置哪些角应该存在,哪些角应该不存在。实施如下:
// OneRoundedCornerUIView.h

#import <UIKit/UIKit.h>

@interface OneRoundedCornerUIView : UIView //Subclass of UIView

// This properties are declared in the public API so that you can setup from your ViewController (it also works if you decide to add/remove corners at any time as the setter of each of these properties will call setNeedsDisplay - as shown in the implementation file)
@property (nonatomic, getter = isTopLeftCornerOn) BOOL topLeftCornerOn;
@property (nonatomic, getter = isTopRightCornerOn) BOOL topRightCornerOn;
@property (nonatomic, getter = isBottomLeftCornerOn) BOOL bottomLeftCornerOn;
@property (nonatomic, getter = isBottomRightCornerOn) BOOL bottomRightCornerOn;

@end


// OneRoundedCornerUIView.m

#import "OneRoundedCornerUIView.h"

@implementation OneRoundedCornerUIView

- (void) setFrame:(CGRect)frame
{
    [super setFrame:frame];
    [self setNeedsDisplay];
}

- (void) setTopLeftCornerOn:(BOOL)topLeftCornerOn
{
    _topLeftCornerOn = topLeftCornerOn;
    [self setNeedsDisplay];
}

- (void) setTopRightCornerOn:(BOOL)topRightCornerOn
{
    _topRightCornerOn = topRightCornerOn;
    [self setNeedsDisplay];
}

-(void) setBottomLeftCornerOn:(BOOL)bottomLeftCornerOn
{
    _bottomLeftCornerOn = bottomLeftCornerOn;
    [self setNeedsDisplay];
}

-(void) setBottomRightCornerOn:(BOOL)bottomRightCornerOn
{
    _bottomRightCornerOn = bottomRightCornerOn;
    [self setNeedsDisplay];
}

// Override drawRect as follows.
- (void)drawRect:(CGRect)rect
{
    UIRectCorner topLeftCorner = 0;
    UIRectCorner topRightCorner = 0;
    UIRectCorner bottomLeftCorner = 0;
    UIRectCorner bottomRightCorner = 0;

    if (self.isTopLeftCornerOn) topLeftCorner = UIRectCornerTopLeft;
    if (self.isTopRightCornerOn) topRightCorner = UIRectCornerTopRight;
    if (self.isBottomLeftCornerOn) bottomLeftCorner = UIRectCornerBottomLeft;
    if (self.isBottomRightCornerOn) bottomRightCorner = UIRectCornerBottomRight;

    UIRectCorner corners = topLeftCorner | topRightCorner | bottomLeftCorner | bottomRightCorner;

    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:
                          self.bounds byRoundingCorners:(corners) cornerRadii:
                          CGSizeMake(10.0, 10.0)];
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = self.bounds;
    maskLayer.path = maskPath.CGPath;
    self.layer.mask = maskLayer;
    self.layer.masksToBounds = NO;
}

@end

拥有如此复杂的 drawRect: 永远不是一个好主意,特别是当可以用不同的方法来完成时。 - Léo Natan
我同意当不是绝对必要时,drawRect 不应该被覆盖 - 但我认为这是其中一种有意义且最佳的方法。我们希望视图在任何情况下都能以圆角绘制自己 - 这就是 drawRect 的作用。为什么我们需要引入复杂的解决方案来处理动画,而不是只需要求视图在需要时绘制自己呢?我认为这种方法更加线性和清晰。此外,在 drawRect 中只有几行代码 - 在某些情况下,您可能需要比那更复杂的绘图。 - tanz
因为drawRect:可能会在不希望重新应用遮罩的情况下被调用。因为在动画期间,应用新的遮罩会严重影响性能。因为已经提供了优化的API来处理动画。之所以不在视图的绘制代码中实现动画,是有原因的。 - Léo Natan

1

我喜欢做@Martin建议的事情。 只要圆角后面没有动画内容,即使在需要圆角的最前面视图显示位图图像的情况下,您也可以完成此操作。

我创建了一个示例项目来模仿您的屏幕截图。 魔术发生在名为TSRoundedCornerViewUIView子类中。 您可以将此视图放置在任何位置 - 在您想要显示圆角的视图上方,设置一个属性以指定要圆角的角(通过调整视图大小来调整半径),并设置一个作为“背景视图”的属性,您希望在角落中可见。

这是示例的存储库:https://github.com/TomSwift/testRoundedCorner

这里是TSRoundedCornerView的绘图魔法。 基本上,我们使用带有圆角的反向剪辑路径,然后绘制背景。

- (void) drawRect:(CGRect)rect
{
    CGContextRef gc = UIGraphicsGetCurrentContext();

    CGContextSaveGState(gc);
    {
        // create an inverted clip path
        // (thanks rob mayoff: http://stackoverflow.com/questions/9042725/drawrect-how-do-i-do-an-inverted-clip)
        UIBezierPath* bp = [UIBezierPath bezierPathWithRoundedRect: self.bounds
                                                 byRoundingCorners: self.corner // e.g. UIRectCornerTopLeft
                                                       cornerRadii: self.bounds.size];
        CGContextAddPath(gc, bp.CGPath);
        CGContextAddRect(gc, CGRectInfinite);
        CGContextEOClip(gc);

        // self.backgroundView is the view we want to show peering out behind the rounded corner
        // this works well enough if there's only one layer to render and not a view hierarchy!
        [self.backgroundView.layer renderInContext: gc];

//$ the iOS7 way of rendering the contents of a view.  It works, but only if the UIImageView has already painted...  I think.
//$ if you try this, be sure to setNeedsDisplay on this view from your view controller's viewDidAppear: method.
//        CGRect r = self.backgroundView.bounds;
//        r.origin = [self.backgroundView convertPoint: CGPointZero toView: self];
//        [self.backgroundView drawViewHierarchyInRect: r
//                                  afterScreenUpdates: YES];
    }
    CGContextRestoreGState(gc);
}

有趣,我也会尝试一下。背景是一个渐变,但没有任何动画效果。 - MusiGenesis
进一步的优化可能是将背景+角落渲染成一个UIImage,然后从drawRect绘制该图像,或通过UIImageView绘制。这取决于您的需求/用途。 - TomSwift

1

两个想法:

  • 当视图大小调整时,调整蒙版的大小。虽然不能像自动调整子视图那样自动调整子层的大小,但仍然会获得一个事件,因此可以手动调整子层的大小。

  • 或者... 如果这是一个由您负责绘制和显示的视图,则将圆角处理作为首次绘制视图的一部分(通过裁剪)。这实际上是最有效的方法。


我认为在这里裁剪不起作用。另一个复杂的问题是,这个视图在一个导航控制器中,顶部有44像素的导航栏(比如,它有20像素的黑色状态栏,然后是44像素的半透明导航栏),x坐标大约为300。我通过将导航控制器视图的框架设置为y=20来实现这一点,它运行良好,但它创建的尖角看起来很糟糕。我认为没有办法使用裁剪来圆角导航控制器视图的角落。但如果可以的话,我很乐意被证明是错误的! :) - MusiGenesis
2
顺便说一句,我试图说服设计师和我的同事程序员,这将是难以实现的,但是他们嘲笑了我。 - MusiGenesis
啊,UINavigationController 有一个方法 layoutSublayersForLayer:。看起来这可能是放置此代码的地方。 - MusiGenesis

0

试试这个。希望能对你有所帮助。

UIView* parent = [[UIView alloc] initWithFrame:CGRectMake(10,10,100,100)];
parent.clipsToBounds = YES;

UIView* child = [[UIView alloc] new];
child.clipsToBounds = YES;
child.layer.cornerRadius = 3.0f;
child.backgroundColor = [UIColor redColor];
child.frame = CGRectOffset(parent.bounds, +4, -4);


[parent addSubView:child];

0
如果您想在Swift中实现它,我建议您使用UIView的扩展。这样做可以使所有子类都能够使用以下方法:
import QuartzCore

extension UIView {
    func roundCorner(corners: UIRectCorner, radius: CGFloat) {
        let maskPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSizeMake(radius, radius))
        var maskLayer = CAShapeLayer()
        maskLayer.frame = self.bounds;
        maskLayer.path = maskPath.CGPath;
        self.layer.mask = maskLayer;
    }
}

self.anImageView.roundCorner(UIRectCorner.TopRight, radius: 10)

是的,这里的问题在于对角线使用了遮罩路径,无论您是否使用Swift。在静态情况下,这样做通常是有效的,但它会使旋转动画变得不流畅和抖动。 - MusiGenesis
你说得对,旋转动画确实有点棘手。恐怕我们需要采用更复杂的解决方案。 - Kevin Delord

0

我再次思考了一下,我认为有一个更简单的解决方案。我更新了我的示例以展示两种解决方案。

新的解决方案是创建一个容器视图,该视图具有4个圆角(通过CALayer cornerRadius实现)。您可以调整该视图的大小,使屏幕上仅显示您感兴趣的角落。如果您需要3个角落或对角线上的两个相反角落,则此解决方案效果不佳。我认为它在大多数其他情况下都有效,包括您在问题和截图中描述的情况。

这是示例的存储库:https://github.com/TomSwift/testRoundedCorner


我也考虑过这样做,但是要将其整合到现有的UI结构中太困难了,需要进行一些重大的重构。最终,我只创建了圆角位图并将它们添加到导航控制器的视图中。 - MusiGenesis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接