如何在 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
。问题是 layoutSubviews
和 viewDidLayoutSubviews
在动画或旋转(而不是连续)之前只调用一次,从而产生尴尬、生涩的效果。
要连续调用它们,您需要覆盖 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 的建议去做。只要圆角后面没有动画内容,您就可以取消它 - 即使最前面的视图后面显示的位图图像需要圆角。
我创建了一个示例项目来模仿您的屏幕截图。魔法发生在一个名为TSRoundedCornerView
的UIView
子类中。您可以将此视图放置在您想要的任何位置 - 在您想要显示圆角的视图上方,设置一个属性来说明要圆的角(通过调整视图的大小来调整半径),并设置一个属性是您希望在角落可见的“背景视图”。
这里是示例的 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 的一个角?的主要内容,如果未能解决你的问题,请参考以下文章