如何在 IOS 中修整可调整大小的 UIView 的一个角?

Posted

技术标签:

【中文标题】如何在 IOS 中修整可调整大小的 UIView 的一个角?【英文标题】:How to round off one corner of a resizable UIView in IOS? 【发布时间】:2014-01-23 02:45:10 【问题描述】:

我正在使用此代码将我的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 上是全屏的,所以我担心这么大的面具的性能(我会在我的用户界面)。此外,超大尺寸的蒙版不适用于我需要将右上角(单独)修圆的右上角

有没有更简单、更容易的方法来实现这一点?

更新:这是我想要实现的目标:http://i.imgur.com/W2AfRBd.png(我想要的圆角在此处用绿色圈出)。

我已经实现了一个工作版本,使用UINavigationController 的子类并覆盖viewDidLayoutSubviews,如下所示:

- (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 子类,然后我将导航控制器视图的框架偏移 20px (y) 以暴露状态栏并留下一个 44 像素高的导航栏,如图所示图片。

代码可以正常工作,只是它根本不能很好地处理旋转。当应用程序旋转时,viewDidLayoutSubviews 被调用 before 旋转,我的代码创建了一个适合视图 after 旋转的掩码;这会对旋转产生不希望的阻塞,在旋转过程中应该隐藏的位会暴露出来。此外,虽然没有此蒙版,应用的旋转非常平滑,但创建蒙版后,旋转变得明显不平稳且缓慢。

iPad 应用 Evomail 也有这样的圆角,他们的应用也存在同样的问题。

【问题讨论】:

你能贴一张现有解决方案的图片吗,我无法想象你想要什么 @WarrenBurton:看看我的编辑,谢谢。 我感受到了你的痛苦……当设计师想要一些看起来很简单的东西时……我可以这样做是使用cornerRadius,然后将其他3个角隐藏在屏幕外。始终将框架设置为比可见量大得多。如果您需要显示整个视图,可以尝试用圆角视图后面的视图来假装其他 3 个角......把它扔在这里 @JackWu:是的,一种可行的解决方案是在开始时创建一个蒙版,左上角为圆形,高度和宽度为 1024。问题是这只是一个例子 - 设计师实际上希望所有四个角都是圆形的。 @MusiGenesis 查看我对您问题的解决方案。效果很好。 【参考方案1】:

问题是,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 你测试过我的方法了吗? 还没有,抱歉。目前正在扑灭其他火灾。 因为这个赏金即将结束,你现在真的应该测试一下 对不起,我正在度假。另外,我认为最佳答案会自动被选为赏金。【参考方案2】:

我知道这是一种非常老套的做法,但你不能在角落的顶部添加一个 png 吗?

丑我知道,但不会影响性能,如果它是子视图并且用户不会注意到,旋转会很好。

【讨论】:

我们将在下面有一个相当复杂的图像。如果背景只是纯黑色,我们可能会接受您的建议。 这实际上是我最终不得不做的。 我喜欢你因为提出唯一有效的建议而被否决。【参考方案3】:

两个想法:

调整视图大小时调整掩码大小。您不会像自动调整子视图大小那样自动调整子图层的大小,但您仍然会收到一个事件,因此您可以手动调整子图层的大小。

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

【讨论】:

我不认为剪辑会在这里工作。一个复杂的问题是这个视图在一个导航控制器中,顶部有 44px 的导航栏(比如,它有 20px 的黑色状态栏,然后是 44px 的半透明导航栏),x 坐标约为 300。我这样做通过将导航控制器视图的框架设置为 y=20,它可以正常工作,但是它创建的尖角看起来很糟糕。我认为没有任何方法可以使用剪辑来绕过导航控制器视图的角落。但如果它有效,我很想被证明是错误的! :) 顺便说一句,我试图说服设计师和我的程序员同事这将很难实现,但我被嘲笑了。 啊,UINavigationController 有一个方法layoutSublayersForLayer:。这似乎是放置这段代码的地方。【参考方案4】:

您可以对正在使用的视图进行子类化并覆盖“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 在动画或旋转(而不是连续)之前只调用一次,从而产生尴尬、生涩的效果。 要连续调用它们,您需要覆盖 drawRect: 正如我刚刚在回答中建议的那样。【参考方案5】:

我认为您应该创建一个自定义视图,该视图可以在需要时随时更新,这意味着任何时候调用 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: 绝不是一个好主意,尤其是当这可以通过不同方式完成时。 我同意 drawRect 在不是绝对必要的情况下不应该被过度使用 - 但我认为这是有意义的情况之一,它是最好的方法。我们希望视图在任何情况下都以圆角绘制自身——这就是 drawRect 的用途。当我们可以要求视图在需要时自行绘制时,为什么我们需要引入复杂的解决方法来处理动画?我发现这种方法更加线性和干净。此外,drawRect 中只有几行代码 - 在某些情况下,您可能需要比这更复杂的绘图。 因为drawRect: 可以在您不想重新应用掩码的情况下调用。因为在动画期间,应用新蒙版会极大地影响性能。因为已经为动画提供和优化了 API。在视图的绘制代码中没有实现动画是有原因的。【参考方案6】:

我喜欢按照@Martin 的建议去做。只要圆角后面没有动画内容,您就可以取消它 - 即使最前面的视图后面显示的位图图像需要圆角。

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

这里是示例的 repo:https://github.com/TomSwift/testRoundedCorner

这是TSRoundedCornerView 的绘图魔法。基本上我们用圆角创建一个倒置的剪辑路径,然后绘制背景。

- (void) drawRect:(CGRect)rect

    CGContextRef gc = UIGraphicsGetCurrentContext();

    CGContextSaveGState(gc);
    
        // create an inverted clip path
        // (thanks rob mayoff: http://***.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);

【讨论】:

有趣,我也试试。背景是渐变的,但没有任何动画效果。 进一步的优化可能是将背景+角只渲染一次到 UIImage 中,然后从 drawRect 或通过 UIImageView 绘制该图像。取决于您的需求/使用情况。【参考方案7】:

我又想到了这个问题,我认为有一个更简单的解决方案。我更新了我的示例以展示这两种解决方案。

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

这里是示例的 repo:https://github.com/TomSwift/testRoundedCorner

【讨论】:

我也想过这样做,但是如果不进行一些重大重构,将其折叠到现有的 UI 结构中太难了。我最终只是创建了圆角位图并将它们添加到导航控制器的视图中。【参考方案8】:

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

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

【讨论】:

【参考方案9】:

如果您想在 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。这样做通常适用于静态情况,但它会使旋转动画变得不可接受且不流畅。 您对旋转动画的看法是正确的。恐怕你/我们将不得不寻求更复杂的解决方案

以上是关于如何在 IOS 中修整可调整大小的 UIView 的一个角?的主要内容,如果未能解决你的问题,请参考以下文章

可调整大小的居中圆形 UIView

UIView 和 TableView 可调整大小

iOS - 可调整大小的图像的固定部分设置?

iOS中的自定义“可调整大小的图像”绘图

在 iOS 中创建可调整大小的图像/矩形 - Objective C

模拟可调整大小的播放显示