如何优雅的为UIView添加圆角?

一.在解决UIView的圆角问题之前,我们先看一下UITableView的卡顿问题。

1.在解决关于添加圆角的问题之前,大家先看一下这个页面。下面的截图是我之前做的项目中的一个页面。大家可以看到这个页面中有大量的圆角,包括头像圆角,view圆角等,另外这个TableViewCell的高度是不确定的。在开发这个页面的时候,整体采用的是自动布局的方法,页面在滚动的过程中很是卡顿,下面是对这个页面一步一步的优化方法。


WechatIMG2.jpeg
  • 关于TableView的卡顿,无外乎是CPU或者GPU导致的,首先查看代码看有没有在cell的reload方法中做一些复杂的操作,果然犯了一个很SB的错误,在cell的reload方法中,做了一个这个的操作,这个方法会多次的调用,多多次为ImageView添加手势,会造成太多cpu去处理手势的相应。解决方法,把这段代码挪到其他地方。
@weakify
[self.askerAvaterImageView bk_whenTap:^{
       @strongify(self)
}];
  • 通过上面的修改之后,发现页面还是不流畅,然后用Instrument工具跑了一下,查找发现高度计算,存在一定的耗时,考虑可能是高度引起的,通过检查代码,发现高度计算我们使用的是
 UITableView+FDTemplateLayoutCell 这个框架中的这个方法,  
return [tableView fd_heightForCellWithIdentifier:[SNSelectedTableViewCell identifier] configuration:^(SNSelectedTableViewCell *cell) {
    }];

跳到头文件发现在这个类里面有一个构造方法。
This method does what "-fd_heightForCellWithIdentifier:configuration" does, and calculated height will be cached by its index path, returns a cached height when needed. Therefore lots of extra height calculations could be saved.

-(CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration;

这个方法意思是用indexPath作为缓存。通过替换方法发现,在第一滑动的时候和刚才差不多,但是在往回滚动的时候,比刚才好多了,由于高度被缓存 了,所以不用再重新计算高度。这样就好多了。

  • 在修改了计算高度的方法之后,其实还不是很放心,就干脆把高度写死了,然后发现最后的效果和使用FDTemplateLayoutCell 在往回滚动的卡顿成都差不多。

二、关于圆角的绘制

1.上面主要解决的是cpu导致的卡顿问题,但是并没有太大的改善,大家可能注意到,在这个页面中有好多的圆角头像,考虑到可能是圆角导致的,所以把加载圆角头像的collectionView先移除。发现此时的页面已经非常流畅了,这时再次把问题定位到多个圆角的绘制上。通过instruments 中的timeprofiler 发现drawRecr方法耗时比较多。然后就把问题再一次定位到圆角的绘制上。查看代码,是这样写的。

self.askerAvatarImageV.layer.cornerRadius = 10.f;
self.askerAvatarImageV.layer.masksToBounds = YES;

这样写的坏处,不用我多说大家都知道,一是耗时,二是会造成离屏渲染问题。因此把绘制圆角的方法改成下面的方法进行手动绘制。

- (UIImage*)cirleImage
{
 // NO代表透明
 UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0);

 // 获得上下文
 CGContextRef ctx = UIGraphicsGetCurrentContext();
 // 添加一个圆
 CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
 CGContextAddEllipseInRect(ctx, rect);
 // 裁剪
 CGContextClip(ctx);
 // 将图片画上去
 [self drawInRect:rect];
 UIImage *cirleImage = UIGraphicsGetImageFromCurrentImageContext();
 UIGraphicsEndImageContext();
 return cirleImage;
}
改成这种写法之后,页面再次滚动,比刚才好的太多了。而且离屏渲染的问题也没有了。

2.其实到这只算是我们的第一步优化。通常我们使用SDWebImag的时候,我们知道由于SD的缓存机制,图片下载之后会被缓存到磁盘和内存中一份,所以当我们再去取的时候,就直接从磁盘或者内存中操作,速度比较快,因此就萌生了为什么不能给圆角头像添加缓存的功能呢,所干就干,下面的代码就产生了。

- (void)setRoundImageWithURL:(NSURL *)url placeHoder:(UIImage *)placeHoder cornerRadius:(CGFloat)radius
{
    NSString *urlStirng = [url absoluteString];
    NSString *imagenName = [NSString stringWithFormat:@"%@%@",urlStirng,@"roundImage"];
    __block NSString *path = [self base64String:imagenName];
    UIImage *image = [[SDImageCache sharedImageCache] imageFromMemoryCacheForKey:path];
    if (image)
    {
        [self setImage:image];
        return;
    }
    else if (placeHoder)
    {
        [self setImage:placeHoder];
    }
    __weak typeof(self)weakSelf = self;
    if (url)
    {
        [[SDWebImageManager sharedManager] downloadImageWithURL:url options:SDWebImageRetryFailed|SDWebImageLowPriority progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong typeof(weakSelf)stongSelf = weakSelf;
            if (!finished) return;
            if (!image) return;
            @synchronized(self) {
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
                    UIImage *cirleImage =  [image cirleImage];
                    [[SDImageCache sharedImageCache]storeImage:cirleImage forKey:path];
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [stongSelf performSelector:@selector(reloadImageData:) withObject:cirleImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
                    });
                });
            }
        }];
    }
}

- (NSString*)base64String:(NSString*)str
{
    NSData* originData = [str dataUsingEncoding:NSASCIIStringEncoding];
    NSString* encodeResult = [originData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    return [NSString stringWithFormat:@"%@.png",encodeResult];
}

- (void)reloadImageData:(UIImage*)image
{
    [self setImage:image];
}

在这端代码中[image cirleImage] 这个方法就是为UIImage写的一个分类就是上面用来处理圆角的方法。
我们把加载头像的方法替换成这个方法。瞬间感觉又流畅了好多。嘚瑟一下。

3.对于通过这种方法处理后的结果自己还是挺满意的,但是同事突然间来了一句你为什么不能通过一个空心的矩形盖在上面呢。这样就不用处理圆角了。我去,他说的貌似很有道理的样子,我竟无言以对。刚开始想着是要不让UI给出一张这样的空心矩形,但是想了想,用到头像的地方比较多,每个地方都要切,多麻烦UI妹子,我多心疼啊。然后就自己想着自己画一个吧,说搞就搞,刚开始没有思路,网上也没有这样的方法,然后再stackoverflow上面无意间看到了这样一个问题,然后把上面贴的代码复制粘贴一发。结果一运行,发现不好使,顿时很是失望。没办法。自己改,经过我的不懈努力。总算是显示对了,绘制空心矩形的代码如下:

- (void)drawRect:(CGRect)rect
{
 [super drawRect:rect];
 CAShapeLayer *maskLayer = [CAShapeLayer layer];
 maskLayer.frame = self.bounds;
 // 画圆
 CGFloat radius = self.bounds.size.width / 2;
 CGRect tempRect = CGRectMake(CGRectGetMidX(self.frame) - radius, CGRectGetMidY(self.frame) - radius, 2 * radius,2 * radius);
 UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.bounds];
 maskLayer.fillRule = kCAFillRuleEvenOdd;
 maskLayer.fillColor = [UIColor whiteColor].CGColor;
 [path appendPath:[UIBezierPath bezierPathWithOvalInRect:tempRect]];
 maskLayer.path = path.CGPath;
 self.layer.mask = maskLayer;
}

4.完成上面的空心矩形的绘制,其实绘制空心圆角的已经完成一半了,完整的代码如下,稍后会把代码放到github上。主要是给UIView加了一个分类。

头文件如下:
@interface UIView (SNRoundCorner)
/**
 *  圆角的颜色-这个颜色需要和背景颜色设置一致
 */
@property (nonatomic, strong)  UIColor *sn_roundCornerColor;
/**
 *  圆角的半径
 */
@property (nonatomic, assign)  CGFloat sn_roundCornerRadius;
@end

.m文件实现如下
@interface UIView()

@property (nonatomic, strong) CALayer *sn_maskLayer;

@end

@implementation UIView (SNRoundCorner)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        SEL selectors[] = {
            @selector(layoutSubviews),
            @selector(setFrame:)
        };
        
        for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) {
            SEL originalSelector = selectors[index];
            SEL swizzledSelector = NSSelectorFromString([@"sn_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
            
            Method originalMethod = class_getInstanceMethod(self, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
            
            BOOL addedSuccess = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
            if (addedSuccess)
            {
                class_replaceMethod(self, originalSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
            }
            else
            {
                method_exchangeImplementations(originalMethod, swizzledMethod);
            }
        }
    });
}

- (void)sn_layoutSubviews
{
    if (self.sn_roundCornerColor && self.sn_roundCornerRadius > 0 && !self.sn_maskLayer)
    {
        [self addRoundCorner];
    }
    [self sn_layoutSubviews];
}

- (void)sn_setFrame:(CGRect)frame
{
    [self sn_setFrame:frame];
    if (self.sn_maskLayer)
    {
        if (!CGSizeEqualToSize(frame.size, self.sn_maskLayer.frame.size))
        {
            [self.sn_maskLayer removeFromSuperlayer];
            self.sn_maskLayer = nil;
        }
    }
}

#pragma mark - 添加分类属性
- (UIColor *)sn_roundCornerColor
{
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setSn_roundCornerColor:(UIColor *)sn_roundCornerColor
{
    objc_setAssociatedObject(self, @selector(sn_roundCornerColor), sn_roundCornerColor, OBJC_ASSOCIATION_RETAIN);
}

- (CGFloat)sn_roundCornerRadius
{
    return [objc_getAssociatedObject(self, _cmd) floatValue];
}

- (void)setSn_roundCornerRadius:(CGFloat)sn_roundCornerRadius
{
    objc_setAssociatedObject(self, @selector(sn_roundCornerRadius), @(sn_roundCornerRadius), OBJC_ASSOCIATION_RETAIN);
}

- (CALayer *)sn_maskLayer
{
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setSn_maskLayer:(CALayer *)sn_maskLayer
{
    objc_setAssociatedObject(self, @selector(sn_maskLayer), sn_maskLayer, OBJC_ASSOCIATION_RETAIN);
}

/**
 *  根据当前图片当前返回一个经过处理之后的圆角图片
 *  @return UIImage
 */
- (void)addRoundCorner
{
    // 这段代码的作用是保证只添加一次
    if (self.sn_maskLayer)
    {
        NSString *reason = @"这个属性只允许设置一次,再次设置不会生效";
        @throw [NSException exceptionWithName:NSGenericException
                                       reason:reason
                                     userInfo:nil];
    }
    else
    {
        [self.layer addSublayer:[self createMaskImageView]];
    }
}

- (CALayer *)createMaskImageView
{

    NSString *imageCacheKey = [NSString stringWithFormat:@"%@%@%@",@"snob_mass_roundImage",NSStringFromCGSize(self.bounds.size),[self rgbStringWithColor:self.sn_roundCornerColor]];
    UIImage *image = [[SDImageCache sharedImageCache] imageFromMemoryCacheForKey:imageCacheKey];
    if (image)
    {
        self.sn_maskLayer = [CALayer layer];
        self.sn_maskLayer.frame = self.bounds;
        // 解决离屏渲染问题
        self.sn_maskLayer.shouldRasterize = YES;
        self.sn_maskLayer.rasterizationScale = [UIScreen mainScreen].scale;
        self.sn_maskLayer.contents = (id)image.CGImage;
        return self.sn_maskLayer;
    }
    else
    {
        CALayer *roundLayer = [CALayer layer];
        roundLayer.frame = self.bounds;
        roundLayer.backgroundColor = self.sn_roundCornerColor.CGColor;
        CAShapeLayer *maskLayer = [CAShapeLayer layer];
        maskLayer.frame = self.bounds;
        
        // 画圆
        UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.bounds];
        maskLayer.fillRule =  kCAFillRuleEvenOdd;
        [path appendPath:[UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:self.sn_roundCornerRadius]];
        maskLayer.path = path.CGPath;
        roundLayer.mask = maskLayer;
        
        CGSize size = roundLayer.bounds.size;
        // 下面方法,第一个参数表示区域大小。第二个参数表示是否是非透明的。如果需要显示半透明效果,需要传NO,否则传YES。第三个参数就是屏幕密度了
        UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
        [roundLayer renderInContext:UIGraphicsGetCurrentContext()];
        UIImage *tempImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        
        // 缓存图片
        [[SDImageCache sharedImageCache] storeImage:tempImage forKey:imageCacheKey toDisk:NO];
        self.sn_maskLayer = roundLayer;
        return self.sn_maskLayer;
    }
}

#pragma mark - 获取当前颜色的rgb值
- (NSString *)rgbStringWithColor:(UIColor *)color
{
    CGColorRef colorRef = color.CGColor;
    NSInteger numComponents = CGColorGetNumberOfComponents(colorRef);
    NSMutableString *stringM = [NSMutableString string];
    for (int i = 0; i < numComponents; i++)
    {
        const CGFloat *components = CGColorGetComponents(colorRef);
        [stringM appendString:@(components[i]).stringValue];
    }
    return [stringM copy];
}

5.需要重点说明的是,目前github上有很多解决圆角的代码,但是都不支持自动布局,所有我才觉得写这个代码很重要。今天就写到这吧,这是我的第一篇简书博客,后续会继续……

放出地址 (https://github.com/zwcshy/SNRoundCorner.git)

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,457评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,019评论 4 62
  • o法法师打发是否是否是否ssf 是打发是否是ff
    IT小生123阅读 110评论 2 0
  • 性格是持续不断的习惯造就的。 之前跟程教授说过我在高中的时候很在意一个同学对我的态度。教授说是因为她身上有某种特质...
    六月花阅读 154评论 0 0