目录
主要参照 YYKit
YYKit 博大精深,就像少林武功
Async View
为了异步 + runloop 空闲时绘制,
因为苹果的 UILabel 性能不够 6
Async Layer
思路: UI 操作,必须放在主线程,
然而图形处理,可以放在子线程,
( 开辟图形上下文,进行绘制,取出图片 )
最后一步,放在主线程,就好了
layer.contents = image
Custom View 中, layer 类,重新制定为异步 layer
+ (Class)layerClass { return YYAsyncLayer.class; }
建立绘制任务
创建一个绘制任务,YYAsyncLayerDisplayTask
关键是里面的绘制方法 display
拿到异步图层 layer 创建的图形上下文,
调一下坐标系,( Core Text 的原点,在左下方 )
文本 map 为富文本,
富文本 map 为一帧,
一帧拆分为好多 CTLine,
一行一行地展示
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask { // capture current state to display task NSString *text = _text; UIFont *fontX = _font; YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new]; CGFloat h_h = self.bounds.size.height; CGFloat w_w = self.bounds.size.width; task.display = ^(CGContextRef context, CGSize size, BOOL(^isCancelled)(void)) { if (isCancelled()) return; //在这里由于绘制文字会颠倒 [[NSOperationQueue mainQueue] addOperationWithBlock:^{ CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, h_h); CGContextScaleCTM(context, 1.0, -1.0); }]; NSAttributedString* str = [[NSAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName: fontX, NSForegroundColorAttributeName: UIColor.blueColor}]; CTFramesetterRef ref = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)str); CGPathRef path = CGPathCreateWithRect(CGRectMake(0, 0, w_w, 3000), nil); CTFrameRef pic = CTFramesetterCreateFrame(ref, CFRangeMake(0, 0), path, nil); CFArrayRef arr = CTFrameGetLines(pic); NSArray *array = (__bridge NSArray*)arr; int i = 0; int cnt = (int)array.count; CGPoint originsArray[cnt]; CTFrameGetLineOrigins(pic, CFRangeMake(0, 0), originsArray); CGFloat y_y = h_h - 60; while (i < cnt) { NSLog(@"%f", originsArray[i].y); CTLineRef line = (__bridge CTLineRef)(array[i]); CGContextSetTextPosition(context, 0, y_y - i * 30); CTLineDraw(line, context); i += 1; } }; return task; }
Async Layer 中, 启动绘制任务,
先处理下继承关系,
再执行上文提到的绘制任务
- (void)display { super.contents = super.contents; [self _displayAsync]; }
执行绘制任务,
拿到任务,没有绘制内容,就算了
再判断,自身的大小 ( size ), 合不合规
大小 CGSize(1, 1)
, 就继续,
子线程,先开辟图形上下文,
再处理背景色,
如果顺利,执行上文的绘制步骤,
从图形上下文中,取出 image, 交给 layer.contents
- (void)_displayAsync{ __strong id<YYAsyncLayerDelegate> delegate = (id)self.delegate; YYAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask]; if (!task.display) { self.contents = nil; return; } CGSize size = self.bounds.size; BOOL opaque = self.opaque; CGFloat scale = self.contentsScale; CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL; if (size.width < 1 || size.height < 1) { CGImageRef image = (__bridge_retained CGImageRef)(self.contents); self.contents = nil; if (image) { dispatch_async(YYAsyncLayerGetReleaseQueue(), ^{ CFRelease(image); }); } CGColorRelease(backgroundColor); return; } dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{ if (isCancelled()) { CGColorRelease(backgroundColor); return; } UIGraphicsBeginImageContextWithOptions(size, opaque, scale); CGContextRef context = UIGraphicsGetCurrentContext(); if (opaque) { CGContextSaveGState(context); { if (!backgroundColor || CGColorGetAlpha(backgroundColor) < 1) { CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor); CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale)); CGContextFillPath(context); } if (backgroundColor) { CGContextSetFillColorWithColor(context, backgroundColor); CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale)); CGContextFillPath(context); } } CGContextRestoreGState(context); CGColorRelease(backgroundColor); } task.display(context, size, isCancelled); if (isCancelled()) { UIGraphicsEndImageContext(); return; } UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); if (isCancelled()) { return; } dispatch_async(dispatch_get_main_queue(), ^{ if (isCancelled() == NO) { self.contents = (__bridge id)(image.CGImage); } }); }); }
RunLoop
触发
设置样式后,不会立即触发,重绘
先保存起来
- (void)setText:(NSString *)text { _text = text.copy; [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit]; }
调用异步图层的绘制任务
- (void)contentsNeedUpdated { // do update [self.layer setNeedsDisplay]; }
事件的保存
先把方法调用的 target 和 action, 保存为对象
YYTransactionSetup();
单例方法,初始化
把方法调用的对象,添加到集合
@implementation YYTransaction + (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector{ if (!target || !selector) return nil; YYTransaction *t = [YYTransaction new]; t.target = target; t.selector = selector; return t; } - (void)commit { if (!_target || !_selector) return; YYTransactionSetup(); [transactionSet addObject:self]; }
空闲的时候,把事情给办了,不影响帧率
下面的单例方法,初始化事件任务集合,
run loop 回调中,执行
不干涉, 主 runloop
static NSMutableSet *transactionSet = nil; static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { if (transactionSet.count == 0) return; NSSet *currentSet = transactionSet; transactionSet = [NSMutableSet new]; [currentSet enumerateObjectsUsingBlock:^(YYTransaction *transaction, BOOL *stop) { [transaction.target performSelector:transaction.selector]; }]; } static void YYTransactionSetup() { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ transactionSet = [NSMutableSet new]; CFRunLoopRef runloop = CFRunLoopGetMain(); CFRunLoopObserverRef observer; observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting | kCFRunLoopExit, true, 0xFFFFFF, YYRunLoopObserverCallBack, NULL); CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes); CFRelease(observer); }); }
YYLabel
功能相当强大的渲染工具,
在上文异步渲染的基础上
支持各种样式
增加了一层抽象 YYTextLayout
YYLabel
中的绘制任务,
如果需要更新,就创建新的布局 layout ,
如果居中 / 底部对其,处理下 layout 布局
然后 layout 绘制
@implementation YYLabel - (YYTextAsyncLayerDisplayTask *)newAsyncDisplayTask { // create display task YYTextAsyncLayerDisplayTask *task = [YYTextAsyncLayerDisplayTask new]; task.display = ^(CGContextRef context, CGSize size, BOOL (^isCancelled)(void)) { if (isCancelled()) return; if (text.length == 0) return; YYTextLayout *drawLayout = layout; if (layoutNeedUpdate) { layout = [YYTextLayout layoutWithContainer:container text:text]; shrinkLayout = [YYLabel _shrinkLayoutWithLayout:layout]; if (isCancelled()) return; layoutUpdated = YES; drawLayout = shrinkLayout ? shrinkLayout : layout; } CGSize boundingSize = drawLayout.textBoundingSize; CGPoint point = CGPointZero; if (verticalAlignment == YYTextVerticalAlignmentCenter) { if (drawLayout.container.isVerticalForm) { point.x = -(size.width - boundingSize.width) * 0.5; } else { point.y = (size.height - boundingSize.height) * 0.5; } } else if (verticalAlignment == YYTextVerticalAlignmentBottom) { if (drawLayout.container.isVerticalForm) { point.x = -(size.width - boundingSize.width); } else { point.y = (size.height - boundingSize.height); } } point = YYTextCGPointPixelRound(point); [drawLayout drawInContext:context size:size point:point view:nil layer:nil debug:debug cancel:isCancelled]; }; return task; } @end
绘制各种
先绘制背景,
其次画阴影,
下划线,
文字,
图片
边框
@implementation YYTextLayout - (void)drawInContext:(CGContextRef)context size:(CGSize)size point:(CGPoint)point view:(UIView *)view layer:(CALayer *)layer debug:(YYTextDebugOption *)debug cancel:(BOOL (^)(void))cancel{ @autoreleasepool { if (self.needDrawBlockBorder && context) { if (cancel && cancel()) return; YYTextDrawBlockBorder(self, context, size, point, cancel); } if (self.needDrawBackgroundBorder && context) { if (cancel && cancel()) return; YYTextDrawBorder(self, context, size, point, YYTextBorderTypeBackgound, cancel); } if (self.needDrawShadow && context) { if (cancel && cancel()) return; YYTextDrawShadow(self, context, size, point, cancel); } if (self.needDrawUnderline && context) { if (cancel && cancel()) return; YYTextDrawDecoration(self, context, size, point, YYTextDecorationTypeUnderline, cancel); } if (self.needDrawText && context) { if (cancel && cancel()) return; YYTextDrawText(self, context, size, point, cancel); } if (self.needDrawAttachment && (context || view || layer)) { if (cancel && cancel()) return; YYTextDrawAttachment(self, context, size, point, view, layer, cancel); } if (self.needDrawInnerShadow && context) { if (cancel && cancel()) return; YYTextDrawInnerShadow(self, context, size, point, cancel); } if (self.needDrawStrikethrough && context) { if (cancel && cancel()) return; YYTextDrawDecoration(self, context, size, point, YYTextDecorationTypeStrikethrough, cancel); } if (self.needDrawBorder && context) { if (cancel && cancel()) return; YYTextDrawBorder(self, context, size, point, YYTextBorderTypeNormal, cancel); } if (debug.needDrawDebug && context) { if (cancel && cancel()) return; YYTextDrawDebug(self, context, size, point, debug); } } }
进入绘制文字
还有图片
这里的绘制粒度,比较上文,
粒度更加的细
上文是 CTLine,
这里是 CTRun
// 注意条件判断, // 与保存 / 恢复图形上下文 static void YYTextDrawAttachment(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, UIView *targetView, CALayer *targetLayer, BOOL (^cancel)(void)) { BOOL isVertical = layout.container.verticalForm; CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; for (NSUInteger i = 0, max = layout.attachments.count; i < max; i++) { YYTextAttachment *a = layout.attachments[i]; if (!a.content) continue; UIImage *image = nil; UIView *view = nil; CALayer *layer = nil; if ([a.content isKindOfClass:[UIImage class]]) { image = a.content; } else if ([a.content isKindOfClass:[UIView class]]) { view = a.content; } else if ([a.content isKindOfClass:[CALayer class]]) { layer = a.content; } if (!image && !view && !layer) continue; if (image && !context) continue; if (view && !targetView) continue; if (layer && !targetLayer) continue; if (cancel && cancel()) break; CGSize asize = image ? image.size : view ? view.frame.size : layer.frame.size; CGRect rect = ((NSValue *)layout.attachmentRects[i]).CGRectValue; if (isVertical) { rect = UIEdgeInsetsInsetRect(rect, UIEdgeInsetRotateVertical(a.contentInsets)); } else { rect = UIEdgeInsetsInsetRect(rect, a.contentInsets); } rect = YYTextCGRectFitWithContentMode(rect, asize, a.contentMode); rect = YYTextCGRectPixelRound(rect); rect = CGRectStandardize(rect); rect.origin.x += point.x + verticalOffset; rect.origin.y += point.y; if (image) { CGImageRef ref = image.CGImage; if (ref) { CGContextSaveGState(context); CGContextTranslateCTM(context, 0, CGRectGetMaxY(rect) + CGRectGetMinY(rect)); CGContextScaleCTM(context, 1, -1); CGContextDrawImage(context, rect, ref); CGContextRestoreGState(context); } } else if (view) { view.frame = rect; [targetView addSubview:view]; } else if (layer) { layer.frame = rect; [targetLayer addSublayer:layer]; } } }
本文,最后一个问题:
上面 layout 的绘制信息,怎么取得的?
先拿文本,创建 CTFrame, CTFrame 中拿到 CTLine 数组
然后遍历每一行,与计算
@implementation YYTextLayout + (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range { // ... ctSetter = CTFramesetterCreateWithAttributedString((CFTypeRef)text); if (!ctSetter) goto fail; ctFrame = CTFramesetterCreateFrame(ctSetter, YYTextCFRangeFromNSRange(range), cgPath, (CFTypeRef)frameAttrs); if (!ctFrame) goto fail; lines = [NSMutableArray new]; ctLines = CTFrameGetLines(ctFrame); // ... for (NSUInteger i = 0, max = lines.count; i < max; i++) { YYTextLine *line = lines[i]; if (truncatedLine && line.index == truncatedLine.index) line = truncatedLine; if (line.attachments.count > 0) { [attachments addObjectsFromArray:line.attachments]; [attachmentRanges addObjectsFromArray:line.attachmentRanges]; [attachmentRects addObjectsFromArray:line.attachmentRects]; for (YYTextAttachment *attachment in line.attachments) { if (attachment.content) { [attachmentContentsSet addObject:attachment.content]; } } } } // ... }