阅读VVeboTableView有感

2017-04-10  本文已影响135人  李昭宏

最近公司项目重构,并且小组也分享了UITableView优化的一些方法,结合查看了VVeboTableView源码,学到了很多东西。

首先查看了一下全局的category文件,发现了很多新东西,作者比较注重版本的管理,所以在获取系统的变量的时候,也会根据不同版本来获取不同版本的所推荐的系统变量,这点我自身做的不好,都到了iOS10的时代,还在用着iOS8的东西,下面我们看一些比较有亮点的代码 首先我们了解一下整个项目的架构

优化思路图

UIScreen+Additions

//UIScreen+Additions.h

#import <UIKit/UIKit.h>

@interface UIScreen (Additions)

//获取屏幕宽度
+ (float)screenWidth;
//获取屏幕高度
+ (float)screenHeight;
//判断是否是retina屏幕
+ (BOOL)isRetina;
//获取屏幕scale
+ (float)scale;

@end
//
//  UIScreen+Additions.m
//  Additions
//
//  Created by Johnil on 13-6-15.
//  Copyright (c) 2013年 Johnil. All rights reserved.
//

#import "UIScreen+Additions.h"

@implementation UIScreen (Additions)

//作者在iOS8以上 均适用nativeScale,nativeBounds,而在iOS8以下,继续沿用bounds,scale这些,这个是我之前没有了解过的知识,学习到了新东西
+ (float)screenWidth{
    if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
        if ([[UIDevice currentDevice].systemVersion floatValue]>=8.0) {
            return [UIScreen mainScreen].nativeBounds.size.height/[UIScreen mainScreen].nativeScale;
        } else {
            return [UIScreen mainScreen].bounds.size.height;
        }
    } else {
        if ([[UIDevice currentDevice].systemVersion floatValue]>=8.0) {
            return [UIScreen mainScreen].nativeBounds.size.width/[UIScreen mainScreen].nativeScale;
        } else {
            return [UIScreen mainScreen].bounds.size.width;
        }
    }
}

+ (float)screenHeight{
    if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
        if ([[UIDevice currentDevice].systemVersion floatValue]>=8.0) {
            if ([UIApplication sharedApplication].statusBarFrame.size.width>20) {
                return [UIScreen mainScreen].nativeBounds.size.width/[UIScreen mainScreen].nativeScale-20;
            }
            return [UIScreen mainScreen].nativeBounds.size.width/[UIScreen mainScreen].nativeScale;
        } else {
            if ([UIApplication sharedApplication].statusBarFrame.size.width>20) {
                return [UIScreen mainScreen].bounds.size.width-20;
            }
            return [UIScreen mainScreen].bounds.size.width;
        }
    } else {
        if ([[UIDevice currentDevice].systemVersion floatValue]>=8.0) {
            if ([UIApplication sharedApplication].statusBarFrame.size.height>20) {
                return [UIScreen mainScreen].nativeBounds.size.height/[UIScreen mainScreen].nativeScale-20;
            }
            return [UIScreen mainScreen].nativeBounds.size.height/[UIScreen mainScreen].nativeScale;
        } else {
            if ([UIApplication sharedApplication].statusBarFrame.size.height>20) {
                return [UIScreen mainScreen].bounds.size.height-20;
            }
            return [UIScreen mainScreen].bounds.size.height;
        }
    }
}

+ (BOOL)isRetina{
    if ([[UIDevice currentDevice].systemVersion floatValue]>=8.0) {
        return [UIScreen mainScreen].nativeScale>=2;
    } else {
        return [UIScreen mainScreen].scale>=2;
    }
}

+ (float)scale{
    if ([[UIDevice currentDevice].systemVersion floatValue]>=8.0) {
        return [UIScreen mainScreen].nativeScale;
    } else {
        return [UIScreen mainScreen].scale;
    }
}

@end

NSString+Additions(核心函数)

//计算字体的宽高数据
- (CGSize)sizeWithConstrainedToWidth:(float)width fromFont:(UIFont *)font1 lineSpace:(float)lineSpace;
- (CGSize)sizeWithConstrainedToSize:(CGSize)size fromFont:(UIFont *)font1 lineSpace:(float)lineSpace;
//使用CoreText绘制文本
- (void)drawInContext:(CGContextRef)context withPosition:(CGPoint)p andFont:(UIFont *)font andTextColor:(UIColor *)color andHeight:(float)height andWidth:(float)width;
- (void)drawInContext:(CGContextRef)context withPosition:(CGPoint)p andFont:(UIFont *)font andTextColor:(UIColor *)color andHeight:(float)height;
- (CGSize)sizeWithConstrainedToWidth:(float)width fromFont:(UIFont *)font1 lineSpace:(float)lineSpace{
    return [self sizeWithConstrainedToSize:CGSizeMake(width, CGFLOAT_MAX) fromFont:font1 lineSpace:lineSpace];
}

- (CGSize)sizeWithConstrainedToSize:(CGSize)size fromFont:(UIFont *)font1 lineSpace:(float)lineSpace{
    CGFloat minimumLineHeight = font1.pointSize,
    maximumLineHeight = minimumLineHeight,
    linespace = lineSpace;
    //生成CTFont
    CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)font1.fontName,font1.pointSize,NULL);
    //设置linkBreakMode
    CTLineBreakMode lineBreakMode = kCTLineBreakByWordWrapping;
    //Apply paragraph settings,设置文本排列方式
    CTTextAlignment alignment = kCTLeftTextAlignment;
    //配置段落style的数据
    CTParagraphStyleRef style = CTParagraphStyleCreate((CTParagraphStyleSetting[6]){
        {kCTParagraphStyleSpecifierAlignment, sizeof(alignment), &alignment},
        {kCTParagraphStyleSpecifierMinimumLineHeight,sizeof(minimumLineHeight),&minimumLineHeight},
        {kCTParagraphStyleSpecifierMaximumLineHeight,sizeof(maximumLineHeight),&maximumLineHeight},
        {kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(linespace), &linespace},
        {kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(linespace), &linespace},
        {kCTParagraphStyleSpecifierLineBreakMode,sizeof(CTLineBreakMode),&lineBreakMode}
    },6);
    //配置字符串属性字典
    NSDictionary* attributes = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)font,(NSString*)kCTFontAttributeName,(__bridge id)style,(NSString*)kCTParagraphStyleAttributeName,nil];
    NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:self attributes:attributes];
    //生成属性字符串
    CFAttributedStringRef attributedString = (__bridge CFAttributedStringRef)string;
    //生成属性字符串的frame
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
    //调用系统函数,来获取推荐的框架大小
    CGSize result = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, [string length]), NULL, size, NULL);
    //因为不是对象类型,所以需要释放资源
    CFRelease(framesetter);
    CFRelease(font);
    CFRelease(style);
    string = nil;
    attributes = nil;
    return result;
}

- (void)drawInContext:(CGContextRef)context withPosition:(CGPoint)p andFont:(UIFont *)font andTextColor:(UIColor *)color andHeight:(float)height andWidth:(float)width{
    CGSize size = CGSizeMake(width, font.pointSize+10);
    //设置文本矩阵
    CGContextSetTextMatrix(context,CGAffineTransformIdentity);
    //移动坐标系,所有点的y增加height
    CGContextTranslateCTM(context,0,height);
    //缩放坐标系,所有的点的x乘以1.0,所有的点的y乘以-1.0(等于翻转坐标系)
    CGContextScaleCTM(context,1.0,-1.0);
    
    //Determine default text color(设置文本颜色)
    UIColor* textColor = color;
    //Set line height, font, color and break mode(配置字体font)
    CTFontRef font1 = CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize,NULL);
    //Apply paragraph settings(设置行高lineHeight)
    CGFloat minimumLineHeight = font.pointSize,maximumLineHeight = minimumLineHeight+10, linespace = 5;
    //设置breakMode(文本截断显示方式)
    CTLineBreakMode lineBreakMode = kCTLineBreakByTruncatingTail;
    //设置文本排列方式
    CTTextAlignment alignment = kCTLeftTextAlignment;
    //设置段落的style
    CTParagraphStyleRef style = CTParagraphStyleCreate((CTParagraphStyleSetting[6]){
        {kCTParagraphStyleSpecifierAlignment, sizeof(alignment), &alignment},
        {kCTParagraphStyleSpecifierMinimumLineHeight,sizeof(minimumLineHeight),&minimumLineHeight},
        {kCTParagraphStyleSpecifierMaximumLineHeight,sizeof(maximumLineHeight),&maximumLineHeight},
        {kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(linespace), &linespace},
        {kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(linespace), &linespace},
        {kCTParagraphStyleSpecifierLineBreakMode,sizeof(CTLineBreakMode),&lineBreakMode}
    },6);
    //设置字符串属性的字典
    NSDictionary* attributes = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)font1,(NSString*)kCTFontAttributeName,
                                textColor.CGColor,kCTForegroundColorAttributeName,
                                style,kCTParagraphStyleAttributeName,
                                nil];
    //Create path to work with a frame with applied margins(创建路径)
    CGMutablePathRef path = CGPathCreateMutable();
    //设置路径的frame大小
    CGPathAddRect(path,NULL,CGRectMake(p.x, height-p.y-size.height,(size.width),(size.height)));
    
    //Create attributed string, with applied syntax highlighting(创建属性字符串)
    NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc] initWithString:self attributes:attributes];
    CFAttributedStringRef attributedString = (__bridge CFAttributedStringRef)attributedStr;
    
    //Draw the frame(创建frame的setter)
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
     //创建frame
    CTFrameRef ctframe = CTFramesetterCreateFrame(framesetter, CFRangeMake(0,CFAttributedStringGetLength(attributedString)),path,NULL);
     //开始绘制
    CTFrameDraw(ctframe,context);
     //关闭路径
    CGPathRelease(path);
     //因为不是对象类型,需要释放资源
    CFRelease(font1);
    CFRelease(framesetter);
    CFRelease(ctframe);
    [[attributedStr mutableString] setString:@""];

    //重置为初始化的状态
    CGContextSetTextMatrix(context,CGAffineTransformIdentity);
    CGContextTranslateCTM(context,0, height);
    CGContextScaleCTM(context,1.0,-1.0);
}

- (void)drawInContext:(CGContextRef)context withPosition:(CGPoint)p andFont:(UIFont *)font andTextColor:(UIColor *)color andHeight:(float)height{
    [self drawInContext:context withPosition:p andFont:font andTextColor:color andHeight:height andWidth:CGFLOAT_MAX];
}

介绍完了一些category,我们现在来按照之前划分的模块来好好看一下他是如何优化的

减少CPU/GPU计算量

cell的重用机制

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

    //cell重用
    VVeboTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];

    if (cell==nil) {
        cell = [[VVeboTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }

    //绘制
    [self drawCell:cell withIndexPath:indexPath];
    return cell;
}

cell高度和 cell里的控件的frame缓存在model里(在拿到数据的同时,计算好高度)

  for (NSDictionary *dict in temp) {
        NSDictionary *user = dict[@"user"];
        NSMutableDictionary *data = [NSMutableDictionary dictionary];
        data[@"avatarUrl"] = user[@"avatar_large"];
        data[@"name"] = user[@"screen_name"];
        NSString *from = [dict valueForKey:@"source"];
        if (from.length>6) {
            NSInteger start = [from indexOf:@"\">"]+2;
            NSInteger end = [from indexOf:@"</a>"];
            from = [from substringFromIndex:start toIndex:end];
        } else {
            from = @"未知";
        }
        data[@"time"] = @"2015-05-25";
        data[@"from"] = from;
        [self setCommentsFrom:dict toData:data];
        [self setRepostsFrom:dict toData:data];
        data[@"text"] = dict[@"text"];
        
        NSDictionary *retweet = [dict valueForKey:@"retweeted_status"];
        if (retweet) {
            NSMutableDictionary *subData = [NSMutableDictionary dictionary];
            NSDictionary *user = retweet[@"user"];
            subData[@"avatarUrl"] = user[@"avatar_large"];
            subData[@"name"] = user[@"screen_name"];
            subData[@"text"] = [NSString stringWithFormat:@"@%@: %@", subData[@"name"], retweet[@"text"]];
            [self setPicUrlsFrom:retweet toData:subData];
            
            {
                float width = [UIScreen screenWidth]-SIZE_GAP_LEFT*2;
                CGSize size = [subData[@"text"] sizeWithConstrainedToWidth:width fromFont:FontWithSize(SIZE_FONT_SUBCONTENT) lineSpace:5];
                NSInteger sizeHeight = (size.height+.5);
                subData[@"textRect"] = [NSValue valueWithCGRect:CGRectMake(SIZE_GAP_LEFT, SIZE_GAP_BIG, width, sizeHeight)];
                sizeHeight += SIZE_GAP_BIG;
                if (subData[@"pic_urls"] && [subData[@"pic_urls"] count]>0) {
                    sizeHeight += (SIZE_GAP_IMG+SIZE_IMAGE+SIZE_GAP_IMG);
                }
                sizeHeight += SIZE_GAP_BIG;
                subData[@"frame"] = [NSValue valueWithCGRect:CGRectMake(0, 0, [UIScreen screenWidth], sizeHeight)];
            }
            data[@"subData"] = subData;
        } else {
            [self setPicUrlsFrom:dict toData:data];
        }
        
        {
            float width = [UIScreen screenWidth]-SIZE_GAP_LEFT*2;
            CGSize size = [data[@"text"] sizeWithConstrainedToWidth:width fromFont:FontWithSize(SIZE_FONT_CONTENT) lineSpace:5];
            NSInteger sizeHeight = (size.height+.5);
            data[@"textRect"] = [NSValue valueWithCGRect:CGRectMake(SIZE_GAP_LEFT, SIZE_GAP_TOP+SIZE_AVATAR+SIZE_GAP_BIG, width, sizeHeight)];
            sizeHeight += SIZE_GAP_TOP+SIZE_AVATAR+SIZE_GAP_BIG;
            if (data[@"pic_urls"] && [data[@"pic_urls"] count]>0) {
                sizeHeight += (SIZE_GAP_IMG+SIZE_IMAGE+SIZE_GAP_IMG);
            }
            
            NSMutableDictionary *subData = [data valueForKey:@"subData"];
            if (subData) {
                sizeHeight += SIZE_GAP_BIG;
                CGRect frame = [subData[@"frame"] CGRectValue];
                CGRect textRect = [subData[@"textRect"] CGRectValue];
                frame.origin.y = sizeHeight;
                subData[@"frame"] = [NSValue valueWithCGRect:frame];
                textRect.origin.y = frame.origin.y+SIZE_GAP_BIG;
                subData[@"textRect"] = [NSValue valueWithCGRect:textRect];
                sizeHeight += frame.size.height;
                data[@"subData"] = subData;
            }
            
            sizeHeight += 30;
            data[@"frame"] = [NSValue valueWithCGRect:CGRectMake(0, 0, [UIScreen screenWidth], sizeHeight)];
        }
        
        [datas addObject:data];
    }

减少cell内部控件的层级

正常开发想到的布局

大部分人会将上中下这三个部分各自封装成一个view,再通过每个view来管理各自的子view。但是这个框架的作者却将它们都排列到一层上。这样可以减少子view的层级,有助于减少cpu对各种约束的计算。这在子view的数量,层级都很多的情况下对cpu的压力会减轻很多(这个也是我们平时会忽略的地方)

通过覆盖圆角图片来实现头像的圆角效果

    //头像,frame固定
    avatarView = [UIButton buttonWithType:UIButtonTypeCustom];//[[VVeboAvatarView alloc] initWithFrame:avatarRect];
    avatarView.frame = CGRectMake(SIZE_GAP_LEFT, SIZE_GAP_TOP, SIZE_AVATAR, SIZE_AVATAR);
    avatarView.backgroundColor = [UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1];
    avatarView.hidden = NO;
    avatarView.tag = NSIntegerMax;
    avatarView.clipsToBounds = YES;
    [self.contentView addSubview:avatarView];

    //覆盖在头像上面的图片,制造圆角效果:frame
    cornerImage = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, SIZE_AVATAR+5, SIZE_AVATAR+5)];
    cornerImage.center = avatarView.center;
    cornerImage.image = [UIImage imageNamed:@"corner_circle@2x.png"];
    cornerImage.tag = NSIntegerMax;
    [self.contentView addSubview:cornerImage];

在这里作者没有使用什么复杂的技术来实现图片的圆角,而是使用一张和cell的背景颜色一样的图片来实现类似圆角的效果,但是这种在不同app主题的时候,就无法实现了,不是很推荐,其实使用UIGraphics绘图就可以实现比较好的圆角效果了

使用数组来保存时刻需要刷新的cell

在cellForRow:中加载可见cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    ...
    [self drawCell:cell withIndexPath:indexPath];
    ...
}

- (void)drawCell:(VVeboTableViewCell *)cell withIndexPath:(NSIndexPath *)indexPath{

    NSDictionary *data = [datas objectAtIndex:indexPath.row];

    ...
    cell.data = data;

    //当前的cell的indexPath不在needLoadArr里面,不用绘制
    if (needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
        [cell clear];
        return;
    }

    //将要滚动到顶部,不绘制
    if (scrollToToping) {
        return;
    }

    //真正绘制cell的代码
    [cell draw];
}
监听tableview的滚动,保存目标滚动范围的前后三行的索引
//按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行加载。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{

    //targetContentOffset : 停止后的contentOffset
    NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];

    //当前可见第一行row的index
    NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];

    //设置最小跨度,当滑动的速度很快,超过这个跨度时候执行按需加载
    NSInteger skipCount = 8;

    //快速滑动(跨度超过了8个cell)
    if (labs(cip.row-ip.row)>skipCount) {

        //某个区域里的单元格的indexPath
        NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
        NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];

        if (velocity.y<0) {

            //向上滚动
            NSIndexPath *indexPath = [temp lastObject];

            //超过倒数第3个
            if (indexPath.row+3<datas.count) {
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];
            }

        } else {

            //向下滚动
            NSIndexPath *indexPath = [temp firstObject];
            //超过正数第3个
            if (indexPath.row>3) {
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
            }
        }
        //添加arr里的内容到needLoadArr的末尾
        [needLoadArr addObjectsFromArray:arr];
    }
}
移除元素NSIndexPath
//用户触摸时第一时间加载内容
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{

    if (!scrollToToping) {
        [needLoadArr removeAllObjects];
        [self loadContent];
    }
    return [super hitTest:point withEvent:event];
}


- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    [needLoadArr removeAllObjects];
}

//将要滚动到顶部
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView{
    scrollToToping = YES;
    return YES;
}

//停止滚动
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
    scrollToToping = NO;
    [self loadContent];
}

//滚动到了顶部
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView{
    scrollToToping = NO;
    [self loadContent];
}

我们可以看到,当手指触碰到tableview时 和 开始拖动tableview的时候就要清理这个数组。 而且在tableview停止滚动后就会执行loadContent方法,用来加载可见区域的cell。

loadContent方法的具体实现
- (void)loadContent{

    //正在滚动到顶部
    if (scrollToToping) {
        return;
    }

    //可见cell数
    if (self.indexPathsForVisibleRows.count<=0) {
        return;
    }

    //触摸的时候刷新可见cell(tableview的visibleCells属性是可见的cell的数组)
    if (self.visibleCells&&self.visibleCells.count>0) {
        for (id temp in [self.visibleCells copy]) {
            VVeboTableViewCell *cell = (VVeboTableViewCell *)temp;
            [cell draw];
        }
    }
}

异步处理cell

核心控件 控件命名

在postBgView上面需要异步显示的内容分为四种: UIImageView:本地图片(comments, more,reposts)。 UIView:背景,分割线(topLine)。 NSString:name,from字符串。 Label:原贴的detailLabel 和 当前贴的 label。

draw函数
//将主要内容绘制到图片上
- (void)draw{
    //首先我们要知道绘制是异步的,为了防止多次绘制,我们会设置一个标识符来判断是否在绘制中
    if (drawed) {
        return;
    }
    
    NSInteger flag = drawColorFlag;
    drawed = YES;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //获取绘制区域
        CGRect rect = [_data[@"frame"] CGRectValue];
        //准备开始绘制
        UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0);
        //获取context
        CGContextRef context = UIGraphicsGetCurrentContext();
        //设置背景颜色
        [[UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set];
        //填充背景颜色
        CGContextFillRect(context, rect);
        //因为有原帖(所以当前是转发贴)
        if ([_data valueForKey:@"subData"]) {
            //绘制转发贴的背景颜色
            [[UIColor colorWithRed:243/255.0 green:243/255.0 blue:243/255.0 alpha:1] set];
            CGRect subFrame = [_data[@"subData"][@"frame"] CGRectValue];
            CGContextFillRect(context, subFrame);

            //绘制分割线
            [[UIColor colorWithRed:200/255.0 green:200/255.0 blue:200/255.0 alpha:1] set];
            CGContextFillRect(context, CGRectMake(0, subFrame.origin.y, rect.size.width, .5));
        }
        
        {
            //计算宽高
            float leftX = SIZE_GAP_LEFT+SIZE_AVATAR+SIZE_GAP_BIG;
            float x = leftX;
            float y = (SIZE_AVATAR-(SIZE_FONT_NAME+SIZE_FONT_SUBTITLE+6))/2-2+SIZE_GAP_TOP+SIZE_GAP_SMALL-5;
            //这个是之前说的字符串绘制的函数,绘制name 用户姓名
            [_data[@"name"] drawInContext:context withPosition:CGPointMake(x, y) andFont:FontWithSize(SIZE_FONT_NAME)
                             andTextColor:[UIColor colorWithRed:106/255.0 green:140/255.0 blue:181/255.0 alpha:1]
                                andHeight:rect.size.height];
            y += SIZE_FONT_NAME+5;
            float fromX = leftX;
            float size = [UIScreen screenWidth]-leftX;
            //绘制发布时间和来自
            NSString *from = [NSString stringWithFormat:@"%@  %@", _data[@"time"], _data[@"from"]];
            [from drawInContext:context withPosition:CGPointMake(fromX, y) andFont:FontWithSize(SIZE_FONT_SUBTITLE)
                   andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:1]
                      andHeight:rect.size.height andWidth:size];
        }
        
        {
            //绘制底部的栏
            CGRect countRect = CGRectMake(0, rect.size.height-30, [UIScreen screenWidth], 30);
            [[UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set];
            CGContextFillRect(context, countRect);
            float alpha = 1;
            
            float x = [UIScreen screenWidth]-SIZE_GAP_LEFT-10;
            NSString *comments = _data[@"comments"];
            if (comments) {
                CGSize size = [comments sizeWithConstrainedToSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) fromFont:FontWithSize(SIZE_FONT_SUBTITLE) lineSpace:5];
                
                x -= size.width;
                //绘制评论数
                [comments drawInContext:context withPosition:CGPointMake(x, 8+countRect.origin.y)
                                andFont:FontWithSize(12)
                           andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:1]
                              andHeight:rect.size.height];
                //绘制评论图片
                [[UIImage imageNamed:@"t_comments.png"] drawInRect:CGRectMake(x-5, 10.5+countRect.origin.y, 10, 9) blendMode:kCGBlendModeNormal alpha:alpha];
                commentsRect = CGRectMake(x-5, self.height-50, [UIScreen screenWidth]-x+5, 50);
                x -= 20;
            }
            
            NSString *reposts = _data[@"reposts"];
            if (reposts) {
                CGSize size = [reposts sizeWithConstrainedToSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) fromFont:FontWithSize(SIZE_FONT_SUBTITLE) lineSpace:5];
                
                x -= MAX(size.width, 5)+SIZE_GAP_BIG;
                //绘制转发数
                [reposts drawInContext:context withPosition:CGPointMake(x, 8+countRect.origin.y)
                                andFont:FontWithSize(12)
                           andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:1]
                              andHeight:rect.size.height];
                //绘制转发图片
                [[UIImage imageNamed:@"t_repost.png"] drawInRect:CGRectMake(x-5, 11+countRect.origin.y, 10, 9) blendMode:kCGBlendModeNormal alpha:alpha];
                repostsRect = CGRectMake(x-5, self.height-50, commentsRect.origin.x-x, 50);
                x -= 20;
            }
            
            //绘制•••
            [@"•••" drawInContext:context
                     withPosition:CGPointMake(SIZE_GAP_LEFT, 8+countRect.origin.y)
                          andFont:FontWithSize(11)
                     andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:.5]
                        andHeight:rect.size.height];
            
            if ([_data valueForKey:@"subData"]) {
                //绘制分割线
                [[UIColor colorWithRed:200/255.0 green:200/255.0 blue:200/255.0 alpha:1] set];
                CGContextFillRect(context, CGRectMake(0, rect.size.height-30.5, rect.size.width, .5));
            }
        }
        
        //保存当前截图(context)
        UIImage *temp = UIGraphicsGetImageFromCurrentImageContext();
        //关闭绘制
        UIGraphicsEndImageContext();
        dispatch_async(dispatch_get_main_queue(), ^{
            if (flag==drawColorFlag) {
                postBGView.frame = rect;
                postBGView.image = nil;
                postBGView.image = temp;
            }
        });
    });
     //准备绘制文本
    [self drawText];
    //准备加载网络图片
    [self loadThumb];
}
异步加载网络图片(使用SDWebImage)
- (void)setData:(NSDictionary *)data{
    _data = data;
    [avatarView setBackgroundImage:nil forState:UIControlStateNormal];
    if ([data valueForKey:@"avatarUrl"]) {
        NSURL *url = [NSURL URLWithString:[data valueForKey:@"avatarUrl"]];
        [avatarView sd_setBackgroundImageWithURL:url forState:UIControlStateNormal placeholderImage:nil options:SDWebImageLowPriority];
    }
}
异步加载本地图片(这个使用系统函数即可)
[[UIImage imageNamed:@"t_comments.png"] drawInRect:CGRectMake(x-5, 10.5+countRect.origin.y, 10, 9) blendMode:kCGBlendModeNormal alpha:alpha];
异步绘制UIView(设置好背景颜色和frame即可)
//背景颜色
[[UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set];

//通过rect填充背景颜色
CGContextFillRect(context, rect);
异步绘制NSString(设置好内容,frame, 字体属性,lineHeight行高,breakMode展示方式,文本排列textAlignment 可以看上面的category,具体说了内部实现的方式)
//绘制名字
[_data[@"name"] drawInContext:context withPosition:CGPointMake(x, y) andFont:FontWithSize(SIZE_FONT_NAME)
                 andTextColor:[UIColor colorWithRed:106/255.0 green:140/255.0 blue:181/255.0 alpha:1]
             andHeight:rect.size.height];
异步绘制UILabel(通过用hidden来设置展示与否,并且作者自己封装了VVeboLabel)

VVeboLabel

1.继承于UIView,可以响应用户点击,在初始化之后,textAlignment文本排列方式,textColor文字颜色,font字体大小,lineSpace属性都会被初始化。 2.使用Core Text绘制文字。 3.持有两种UIImageView,用来显示默认状态和高亮状态的图片(将字符串绘制成图片,这个很强)。 4.保存了四种特殊文字的颜色,用正则表达式识别以后,给其着色

setText方法(使用CoreText绘制字符串)
//使用coretext将文本绘制到图片。
- (void)setText:(NSString *)text{
    if (text==nil || text.length<=0) {
        labelImageView.image = nil;
        highlightImageView.image = nil;
        return;
    }
    if ([text isEqualToString:_text]) {
        if (!highlighting || currentRange.location==-1) {
            return;
        }
    }
    if (highlighting&&labelImageView.image==nil) {
        return;
    }
    if (!highlighting) {
        [framesDict removeAllObjects];
        currentRange = NSMakeRange(-1, -1);
    }
    //绘制标记,初始化时赋一个随机值;clear之后更新一个随机值
    NSInteger flag = drawFlag;
    //是否处于高亮的状态
    BOOL isHighlight = highlighting;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSString *temp = text;
        _text = text;
        CGSize size = self.frame.size;
        size.height += 10;

        //开始准备绘制
        UIGraphicsBeginImageContextWithOptions(size, ![self.backgroundColor isEqual:[UIColor clearColor]], 0);
        //获取当前的context
        CGContextRef context = UIGraphicsGetCurrentContext();
        if (context==NULL) {
            return;
        }
        if (![self.backgroundColor isEqual:[UIColor clearColor]]) {
            //设置背景颜色
            [self.backgroundColor set];
            CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height));
        }
        //设置绘画矩阵
        CGContextSetTextMatrix(context,CGAffineTransformIdentity);
        //设置偏移量(x -> x , y -> y + size.height)
        CGContextTranslateCTM(context,0,size.height);
        //偏转坐标系(x -> x, y -> -y)
        CGContextScaleCTM(context,1.0,-1.0);
        
        //Determine default text color
        UIColor* textColor = self.textColor;
        
        //Set line height, font, color and break mode
        //配置行高
        CGFloat minimumLineHeight = self.font.pointSize,maximumLineHeight = minimumLineHeight, linespace = self.lineSpace;
        //配置字体大小
        CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)self.font.fontName, self.font.pointSize,NULL);
        //配置文本段落显示方式
        CTLineBreakMode lineBreakMode = kCTLineBreakByWordWrapping;
        //配置文本排列方式
        CTTextAlignment alignment = CTTextAlignmentFromUITextAlignment(self.textAlignment);
        //Apply paragraph settings
        //配置段落style
        CTParagraphStyleRef style = CTParagraphStyleCreate((CTParagraphStyleSetting[6]){
            {kCTParagraphStyleSpecifierAlignment, sizeof(alignment), &alignment},
            {kCTParagraphStyleSpecifierMinimumLineHeight,sizeof(minimumLineHeight),&minimumLineHeight},
            {kCTParagraphStyleSpecifierMaximumLineHeight,sizeof(maximumLineHeight),&maximumLineHeight},
            {kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(linespace), &linespace},
            {kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(linespace), &linespace},
            {kCTParagraphStyleSpecifierLineBreakMode,sizeof(CTLineBreakMode),&lineBreakMode}
        },6);
        //字体属性字典
        NSDictionary* attributes = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)font,(NSString*)kCTFontAttributeName,
                                    textColor.CGColor,kCTForegroundColorAttributeName,
                                    style,kCTParagraphStyleAttributeName,
                                    nil];
        
        //Create attributed string, with applied syntax highlighting
        NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc] initWithString:text attributes:attributes];
        CFAttributedStringRef attributedString = (__bridge CFAttributedStringRef)[self highlightText:attributedStr];
        
        //Draw the frame
        //生成frame setter
        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
        
        CGRect rect = CGRectMake(0, 5,(size.width),(size.height-5));
        
        if ([temp isEqualToString:text]) {
            //绘制属性字符串
            [self drawFramesetter:framesetter attributedString:attributedStr textRange:CFRangeMake(0, text.length) inRect:rect context:context];
             //重新设置文本矩阵
            CGContextSetTextMatrix(context,CGAffineTransformIdentity);
            //偏移坐标的点
            CGContextTranslateCTM(context,0,size.height);
            //偏移坐标系
            CGContextScaleCTM(context,1.0,-1.0);
            //截屏,将当前的context保存成图片
            UIImage *screenShotimage = UIGraphicsGetImageFromCurrentImageContext();
            //关闭绘图
            UIGraphicsEndImageContext();
            dispatch_async(dispatch_get_main_queue(), ^{
                //释放资源
                CFRelease(font);
                CFRelease(framesetter);
                [[attributedStr mutableString] setString:@""];
                
                if (drawFlag==flag) {
                    if (isHighlight) {
                        if (highlighting) {
                            highlightImageView.image = nil;
                            if (highlightImageView.width!=screenShotimage.size.width) {
                                highlightImageView.width = screenShotimage.size.width;
                            }
                            if (highlightImageView.height!=screenShotimage.size.height) {
                                highlightImageView.height = screenShotimage.size.height;
                            }
                            highlightImageView.image = screenShotimage;
                        }
                    } else {
                        if ([temp isEqualToString:text]) {
                            if (labelImageView.width!=screenShotimage.size.width) {
                                labelImageView.width = screenShotimage.size.width;
                            }
                            if (labelImageView.height!=screenShotimage.size.height) {
                                labelImageView.height = screenShotimage.size.height;
                            }
                            highlightImageView.image = nil;
                            labelImageView.image = nil;
                            labelImageView.image = screenShotimage;
                        }
                    }
//                    [self debugDraw];//绘制可触摸区域
                }
            });
        }
    });
}
drawFramesetter(核心绘制函数)
//确保行高一致,计算所需触摸区域
- (void)drawFramesetter:(CTFramesetterRef)framesetter
       attributedString:(NSAttributedString *)attributedString
              textRange:(CFRange)textRange
                 inRect:(CGRect)rect
                context:(CGContextRef)c
{
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, rect);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, textRange, path, NULL);
    
    CFArrayRef lines = CTFrameGetLines(frame);
    NSInteger numberOfLines = CFArrayGetCount(lines);
    BOOL truncateLastLine = NO;//tailMode
    
    CGPoint lineOrigins[numberOfLines];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, numberOfLines), lineOrigins);
    for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
        CGPoint lineOrigin = lineOrigins[lineIndex];
        lineOrigin = CGPointMake(CGFloat_ceil(lineOrigin.x), CGFloat_ceil(lineOrigin.y));
        
        CGContextSetTextPosition(c, lineOrigin.x, lineOrigin.y);
        CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
        
        CGFloat descent = 0.0f;
        CGFloat ascent = 0.0f;
        CGFloat lineLeading;
        CTLineGetTypographicBounds((CTLineRef)line, &ascent, &descent, &lineLeading);
        
        // Adjust pen offset for flush depending on text alignment
        CGFloat flushFactor = NSTextAlignmentLeft;
        CGFloat penOffset;
        CGFloat y;
        if (lineIndex == numberOfLines - 1 && truncateLastLine) {
            // Check if the range of text in the last line reaches the end of the full attributed string
            CFRange lastLineRange = CTLineGetStringRange(line);
            
            if (!(lastLineRange.length == 0 && lastLineRange.location == 0) && lastLineRange.location + lastLineRange.length < textRange.location + textRange.length) {
                // Get correct truncationType and attribute position
                CTLineTruncationType truncationType = kCTLineTruncationEnd;
                CFIndex truncationAttributePosition = lastLineRange.location;
                
                NSString *truncationTokenString = @"\u2026";
                
                NSDictionary *truncationTokenStringAttributes = [attributedString attributesAtIndex:(NSUInteger)truncationAttributePosition effectiveRange:NULL];
                
                NSAttributedString *attributedTokenString = [[NSAttributedString alloc] initWithString:truncationTokenString attributes:truncationTokenStringAttributes];
                CTLineRef truncationToken = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attributedTokenString);
                
                // Append truncationToken to the string
                // because if string isn't too long, CT wont add the truncationToken on it's own
                // There is no change of a double truncationToken because CT only add the token if it removes characters (and the one we add will go first)
                NSMutableAttributedString *truncationString = [[attributedString attributedSubstringFromRange:NSMakeRange((NSUInteger)lastLineRange.location, (NSUInteger)lastLineRange.length)] mutableCopy];
                if (lastLineRange.length > 0) {
                    // Remove any newline at the end (we don't want newline space between the text and the truncation token). There can only be one, because the second would be on the next line.
                    unichar lastCharacter = [[truncationString string] characterAtIndex:(NSUInteger)(lastLineRange.length - 1)];
                    if ([[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter]) {
                        [truncationString deleteCharactersInRange:NSMakeRange((NSUInteger)(lastLineRange.length - 1), 1)];
                    }
                }
                [truncationString appendAttributedString:attributedTokenString];
                CTLineRef truncationLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)truncationString);
                
                // Truncate the line in case it is too long.
                CTLineRef truncatedLine = CTLineCreateTruncatedLine(truncationLine, rect.size.width, truncationType, truncationToken);
                if (!truncatedLine) {
                    // If the line is not as wide as the truncationToken, truncatedLine is NULL
                    truncatedLine = CFRetain(truncationToken);
                }
                
                penOffset = (CGFloat)CTLineGetPenOffsetForFlush(truncatedLine, flushFactor, rect.size.width);
                y = lineOrigin.y - descent - self.font.descender;
                CGContextSetTextPosition(c, penOffset, y);
                
                CTLineDraw(truncatedLine, c);
                
                CFRelease(truncatedLine);
                CFRelease(truncationLine);
                CFRelease(truncationToken);
            } else {
                penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, rect.size.width);
                y = lineOrigin.y - descent - self.font.descender;
                CGContextSetTextPosition(c, penOffset, y);
                CTLineDraw(line, c);
            }
        } else {
            penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, rect.size.width);
            y = lineOrigin.y - descent - self.font.descender;
            CGContextSetTextPosition(c, penOffset, y);
            CTLineDraw(line, c);
        }
        if ((!highlighting&&self.superview!=nil)) {
            CFArrayRef runs = CTLineGetGlyphRuns(line);
            for (int j = 0; j < CFArrayGetCount(runs); j++) {
                CGFloat runAscent;
                CGFloat runDescent;
                CTRunRef run = CFArrayGetValueAtIndex(runs, j);
                NSDictionary* attributes = (__bridge NSDictionary*)CTRunGetAttributes(run);
                if (!CGColorEqualToColor((__bridge CGColorRef)([attributes valueForKey:@"CTForegroundColor"]), self.textColor.CGColor)
                    && framesDict!=nil) {
                    CFRange range = CTRunGetStringRange(run);
                    CGRect runRect;
                    runRect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0,0), &runAscent, &runDescent, NULL);
                    float offset = CTLineGetOffsetForStringIndex(line, range.location, NULL);
                    float height = runAscent;
                    runRect=CGRectMake(lineOrigin.x + offset, (self.height+5)-y-height+runDescent/2, runRect.size.width, height);
                    NSRange nRange = NSMakeRange(range.location, range.length);
                    [framesDict setValue:[NSValue valueWithCGRect:runRect] forKey:NSStringFromRange(nRange)];
                }
            }
        }
    }
    
    CFRelease(frame);
    CFRelease(path);
}

总结

作者大部分的思维都很好,让我们在优化UITableView方面有很多新的思路,其中最让我佩服的就是使用CoreText来实现文本等一系列的优化,对于CoreText我并不是很擅长,之前没有做过类似的项目,打算最近好好学习,争取弄透CoreText,并且用CoreText做一个比较满意的东西出来

上一篇下一篇

猜你喜欢

热点阅读