iOS 视图圆角(任意角任意大小)

最近要做一个新项目,产品需求刚过完,UI的效果图也就随之而出了,拿到效果图之后,看到首页就让我大吃一惊了,因为里面出现好多不同大小和个数的圆角,这让我着实头疼,大家可以看看UI效果图。


首页.png

可以看到页面确实很炫,但是实现也比较费劲。

之前圆角方案

1.使用layer的cornerRadius属性进行设置
self.view.layer.cornerRadius = 5.f; 
self.view.layer.masksToBounds = YES;
缺点:

1).当图片数量比较多的时候,这种添加圆角方式特别消耗性能,比如在UITableViewCell
添加过多圆角的话,甚至会带来视觉可见的卡顿.
2).无法配置圆角数量(只能添加view的四个角全为圆角),无法配置某个圆角大小.
第一个问题实际上是由于数量太多的情况下,系统会频繁的调用GPU的离屏渲染(Offscreen Rendering)机制,导致内存损耗严重.

解决:

第一个问题: 采取预先生成圆角图片,并缓存起来这个方法才是比较好的手段。预处理圆角图片可以在后台处理,处理完毕后缓存起来,再在主线程显示,这就避免了不必要的离屏渲染了,更多关于离屏渲染的详解,大家可以看这里,本文不多赘述.

self.view.layer.cornerRadius = 5.f;
self.view.layer.masksToBounds = YES; // 裁剪
self.view.layer.shouldRasterize = YES; // 缓存
self.view.layer.rasterizationScale = [UIScreen mainScreen].scale;

当shouldRasterize设成true时,layer被渲染成一个bitmap,并缓存起来,等下次使用时不会再重新去渲染了。实现圆角本 身就是在做颜色混合(blending),如果每次页面出来时都blending,消耗太大,这时shouldRasterize = yes,下次就只是简单的从渲染引擎的cache里读取那张bitmap,节约系统资源。

2.使用UIBezierPath进行切圆角

这种方案可以完美的解决方案一中第二个问题不能实现配置任意圆角数量。
实现过程

UIView * view = [[UIView alloc] initWithFrame:CGRectMake(0, 100, 100, 50)];
view.backgroundColor = [UIColor redColor];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:UIRectCornerTopLeft | UIRectCornerBottomLeft cornerRadii:CGSizeMake(25, 0)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = view.bounds;
maskLayer.path = maskPath.CGPath;
view.layer.mask = maskLayer;
[self.view addSubview:view];

其中想配置不同数量圆角可以设置UIRectCorner枚举属性进行配置。

UIRectCornerTopLeft     = 1 << 0,//顶左
UIRectCornerTopRight    = 1 << 1,//顶右
UIRectCornerBottomLeft  = 1 << 2,//底左
UIRectCornerBottomRight = 1 << 3,//底右
UIRectCornerAllCorners  = ~0UL//所有
缺点:
  1. 通过UIBezierPath虽然解决了配置不同数量圆角,但是还是没有能解决配置不同大小的圆角
解决:

下面就到了主角亮相的时刻了。

插曲

与第二种方案很类似的一个是在iOS11.0之后对于圆角出来了一个新特性

@property CACornerMask maskedCorners
  CA_AVAILABLE_STARTING (10.13, 11.0, 11.0, 4.0);

就是在layer新增了一个maskedCorners属性,其中CACornerMask是一个结构体

typedef NS_OPTIONS (NSUInteger, CACornerMask)
{
  kCALayerMinXMinYCorner = 1U << 0,
  kCALayerMaxXMinYCorner = 1U << 1,
  kCALayerMinXMaxYCorner = 1U << 2,
  kCALayerMaxXMaxYCorner = 1U << 3,
};

这个属性用于设置不同位置的圆角

UIView * view = [[UIView alloc] initWithFrame:CGRectMake(0, 100, 100, 50)];
view.backgroundColor = [UIColor redColor];
if (@available(iOS 11.0, *)) {
    view.layer.maskedCorners = kCALayerMaxXMaxYCorner |kCALayerMaxXMinYCorner;
} else {
     // Fallback on earlier versions
}
view.layer.cornerRadius = 25.f;
view.layer.masksToBounds = YES;
[self.view addSubview:view];

但是他跟方案二有相同的缺点

问题解决思路

看到效果图之后,我首先想到了第二种方案,在模糊的记忆中首先想到它可以配置 不同数量的圆角,然后就是想看看它能不能同时实现配置不同大小的圆角,找了很久也看了layer的一些属性没有找到,相关属性,网上也找了很多文章(可能没有找对关键字)也没有找到,很多都是方案一方案二的方法,没有具体解决不同大小的圆角方案,后面我一度以为iOS中不能实现(可能我太菜了),在我陷入迷茫的时候,我的同事突然提醒了我,说reactnative 中可以实现配置任意数量圆角并同时配置不同大小的方式(到这块可能了解RN工作原理的人已经大概知道该怎么去做了,RN的项目其实最终展示的控件还是原生控件,他们内部通过js来实现原生交互,将RN代码创建的控件转化成原生的控件,在RN中的任何组件,在原生都可以找到对应的原生组件,既然RN可以实现,那代表着原生也能实现),我之前也用过RN,写代码很快(用熟了之后)很方便,像效果图中的那个效果,很快就能实现。

1.于是我赶紧找RN的原生代码
1.png

这个主要是RN的在原生中的代码库,我们最主要的是看红框中的内容


2.png

因为主要是view及它的子类有这种属性,所有我们只需要在view对应的那个文件中找就可以

- (void)updateClippingForLayer:(CALayer *)layer
{
  CALayer *mask = nil;
  CGFloat cornerRadius = 0;

  if (self.clipsToBounds) {

    const RCTCornerRadii cornerRadii = [self cornerRadii];
    if (RCTCornerRadiiAreEqual(cornerRadii)) {

      cornerRadius = cornerRadii.topLeft;

    } else {
      //切圆角的代码
      CAShapeLayer *shapeLayer = [CAShapeLayer layer];
      CGPathRef path = RCTPathCreateWithRoundedRect(self.bounds, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL);
      shapeLayer.path = path;
      CGPathRelease(path);
      mask = shapeLayer;
    }
  }

  layer.cornerRadius = cornerRadius;
  layer.mask = mask;
}

这是它里面的一段代码,主要的切圆角方式再标注的地方,不难看出还是用了类似方案二中添加路径的方式去切圆角只是和UIBezierPath不一样,点击RCTPathCreateWithRoundedRect这个方法,我们就可以看到具体切圆角的过程了。

CGPathRef RCTPathCreateWithRoundedRect(CGRect bounds,
                                       RCTCornerInsets cornerInsets,
                                       const CGAffineTransform *transform)
{
  const CGFloat minX = CGRectGetMinX(bounds);
  const CGFloat minY = CGRectGetMinY(bounds);
  const CGFloat maxX = CGRectGetMaxX(bounds);
  const CGFloat maxY = CGRectGetMaxY(bounds);

  const CGSize topLeft = {
    MAX(0, MIN(cornerInsets.topLeft.width, bounds.size.width - cornerInsets.topRight.width)),
    MAX(0, MIN(cornerInsets.topLeft.height, bounds.size.height - cornerInsets.bottomLeft.height)),
  };
  const CGSize topRight = {
    MAX(0, MIN(cornerInsets.topRight.width, bounds.size.width - cornerInsets.topLeft.width)),
    MAX(0, MIN(cornerInsets.topRight.height, bounds.size.height - cornerInsets.bottomRight.height)),
  };
  const CGSize bottomLeft = {
    MAX(0, MIN(cornerInsets.bottomLeft.width, bounds.size.width - cornerInsets.bottomRight.width)),
    MAX(0, MIN(cornerInsets.bottomLeft.height, bounds.size.height - cornerInsets.topLeft.height)),
  };
  const CGSize bottomRight = {
    MAX(0, MIN(cornerInsets.bottomRight.width, bounds.size.width - cornerInsets.bottomLeft.width)),
    MAX(0, MIN(cornerInsets.bottomRight.height, bounds.size.height - cornerInsets.topRight.height)),
  };

  CGMutablePathRef path = CGPathCreateMutable();
  RCTPathAddEllipticArc(path, transform, (CGPoint){
    minX + topLeft.width, minY + topLeft.height
  }, topLeft, M_PI, 3 * M_PI_2, NO);
  RCTPathAddEllipticArc(path, transform, (CGPoint){
    maxX - topRight.width, minY + topRight.height
  }, topRight, 3 * M_PI_2, 0, NO);
  RCTPathAddEllipticArc(path, transform, (CGPoint){
    maxX - bottomRight.width, maxY - bottomRight.height
  }, bottomRight, 0, M_PI_2, NO);
  RCTPathAddEllipticArc(path, transform, (CGPoint){
    minX + bottomLeft.width, maxY - bottomLeft.height
  }, bottomLeft, M_PI_2, M_PI, NO);
  CGPathCloseSubpath(path);
  return path;
}
static void RCTPathAddEllipticArc(CGMutablePathRef path,
                                  const CGAffineTransform *m,
                                  CGPoint origin,
                                  CGSize size,
                                  CGFloat startAngle,
                                  CGFloat endAngle,
                                  BOOL clockwise)
{
  CGFloat xScale = 1, yScale = 1, radius = 0;
  if (size.width != 0) {
    xScale = 1;
    yScale = size.height / size.width;
    radius = size.width;
  } else if (size.height != 0) {
    xScale = size.width / size.height;
    yScale = 1;
    radius = size.height;
  }

  CGAffineTransform t = CGAffineTransformMakeTranslation(origin.x, origin.y);
  t = CGAffineTransformScale(t, xScale, yScale);
  if (m != NULL) {
    t = CGAffineTransformConcat(t, *m);
  }

  CGPathAddArc(path, &t, 0, 0, radius, startAngle, endAngle, clockwise);
}

上面就是RN中视图切圆角的具体代码,主要执行过程大家可以自己看源码
主要文件名和路径看下图:


3.png

主要是这两个文件,最主要的代码就是这句

/* Add an arc of a circle to `path', possibly preceded by a straight line
   segment. The arc is approximated by a sequence of Bézier curves. `(x, y)'
   is the center of the arc; `radius' is its radius; `startAngle' is the
   angle to the first endpoint of the arc; `endAngle' is the angle to the
   second endpoint of the arc; and `clockwise' is true if the arc is to be
   drawn clockwise, false otherwise. `startAngle' and `endAngle' are
   measured in radians. If `m' is non-NULL, then the constructed Bézier
   curves representing the arc will be transformed by `m' before they are
   added to `path'.

   Note that using values very near 2π can be problematic. For example,
   setting `startAngle' to 0, `endAngle' to 2π, and `clockwise' to true will
   draw nothing. (It's easy to see this by considering, instead of 0 and 2π,
   the values ε and 2π - ε, where ε is very small.) Due to round-off error,
   however, it's possible that passing the value `2 * M_PI' to approximate
   2π will numerically equal to 2π + δ, for some small δ; this will cause a
   full circle to be drawn.

   If you want a full circle to be drawn clockwise, you should set
   `startAngle' to 2π, `endAngle' to 0, and `clockwise' to true. This avoids
   the instability problems discussed above. */

/*
      path : 路径
      m : 变换
      x  y : 画圆的圆心点
      radius : 圆的半径
      startAngle : 起始角度
      endAngle : 结束角度
      clockwise : 是否是顺时针
      void CGPathAddArc(CGMutablePathRef cg_nullable path,
      const CGAffineTransform * __nullable m,
      CGFloat x, CGFloat y, CGFloat radius, CGFloat startAngle, CGFloat endAngle,
      bool clockwise)
      */
CG_EXTERN void CGPathAddArc(CGMutablePathRef cg_nullable path,
    const CGAffineTransform * __nullable m,
    CGFloat x, CGFloat y, CGFloat radius, CGFloat startAngle, CGFloat endAngle,
    bool clockwise)
    CG_AVAILABLE_STARTING(__MAC_10_2, __IPHONE_2_0);

注:再API中写到clockwise默认是YES,即是顺时针,但是再iOS中的UIView中,这实际是逆时针,所有有时候看代码中写的感觉有问题,但运行出来确实正确的,不知道为什么。

2.开始参考RN的代码写自己圆角代码

通过了解和熟悉整个RN的代码实现过程,我就开始写这块的代码,具体代码如下:

CornerRadii CornerRadiiMake(CGFloat topLeft,CGFloat topRight,CGFloat bottomLeft,CGFloat bottomRight){
     return (CornerRadii){
          topLeft,
          topRight,
          bottomLeft,
          bottomRight,
     };
}
//切圆角函数
CGPathRef CYPathCreateWithRoundedRect(CGRect bounds,
                                      CornerRadii cornerRadii)
{
     const CGFloat minX = CGRectGetMinX(bounds);
     const CGFloat minY = CGRectGetMinY(bounds);
     const CGFloat maxX = CGRectGetMaxX(bounds);
     const CGFloat maxY = CGRectGetMaxY(bounds);
     
     const CGFloat topLeftCenterX = minX +  cornerRadii.topLeft;
     const CGFloat topLeftCenterY = minY + cornerRadii.topLeft;
     
     const CGFloat topRightCenterX = maxX - cornerRadii.topRight;
     const CGFloat topRightCenterY = minY + cornerRadii.topRight;
     
     const CGFloat bottomLeftCenterX = minX +  cornerRadii.bottomLeft;
     const CGFloat bottomLeftCenterY = maxY - cornerRadii.bottomLeft;
     
     const CGFloat bottomRightCenterX = maxX -  cornerRadii.bottomRight;
     const CGFloat bottomRightCenterY = maxY - cornerRadii.bottomRight;
     //虽然顺时针参数是YES,在iOS中的UIView中,这里实际是逆时针
     
     CGMutablePathRef path = CGPathCreateMutable();
     //顶 左
     CGPathAddArc(path, NULL, topLeftCenterX, topLeftCenterY,cornerRadii.topLeft, M_PI, 3 * M_PI_2, NO);
     //顶 右
     CGPathAddArc(path, NULL, topRightCenterX , topRightCenterY, cornerRadii.topRight, 3 * M_PI_2, 0, NO);
     //底 右
     CGPathAddArc(path, NULL, bottomRightCenterX, bottomRightCenterY, cornerRadii.bottomRight,0, M_PI_2, NO);
     //底 左
     CGPathAddArc(path, NULL, bottomLeftCenterX, bottomLeftCenterY, cornerRadii.bottomLeft, M_PI_2,M_PI, NO);
     CGPathCloseSubpath(path);
     return path;
}

具体应用部分代码:

//切圆角
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    self.cornerRadii = CornerRadiiMake(self.borderTopLeftRadius, self.borderTopRightRadius, self.borderBottomLeftRadius, self.borderBottomRightRadius);
    CGPathRef path = CYPathCreateWithRoundedRect(self.bounds,self.cornerRadii);
    shapeLayer.path = path;
    CGPathRelease(path);
    self.layer.mask = shapeLayer;

最中解决了这个问题可以实现配置不同数量不同大小的圆角,对于性能这块,它采用的也是路径去处理,跟方案二很类似,所以大体差不多,具体的性能对比还没有详细比较过,后面我也实现了效果图中的效果

4.png

其中的颜色渐变边框我才用的是路径绘制渐变线的方式实现的,这块的代码比较多,写的比较繁琐(还不知道有什么简单的处理方式),如果有比较好的解决思路可以评论留言交流交流。
具体代码这里,可以具体看看实现过程。

总结

通过这次切不同大小圆角的问题,发现了之前自己真的是只做业务层,做最简单的东西,对于稍微深入一点的东西,一点都不知道,可能有些知道,但也仅仅限于这是个什么,但要用它做什么东西就不知道,上层的东西很方便很快捷但是局限性太大,要想做一些很炫很酷的效果还是得深入底层了解每个属性每个方法是什么?有什么用?主要使用在哪些方面?,这些概念的了解是最基本的,最最重要的还是我们怎么能把一个具体的事物转换成我们代码需要实现的抽象的事物,并且运用我们所知道的所有东西,用最好的方式将它转化成代码,这整个过程是最值得我们深究的,我们需要有思路,需要哪个地方该用什么怎么实现等等,我觉得这可能是我(也可能是和我有同感的程序员)认为最最重要的东西了

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,027评论 8 265
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 家有一只名叫咖啡的泰迪,调皮,捣蛋,坏事一箩筐。疑惑的是别人家的泰迪都那么温顺,柔和,我们家的却是泰迪的外表...
    优优麻麻阅读 135评论 0 2
  • 3月3日,今天的武术课在温习拍打和八段锦基础上加了两个拍打新手法。跟杨老师学了这么多节课,每次课程都安排新的内容和...
    运动健康吧阅读 238评论 0 1