You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

679 lines
23 KiB

//
// TYTextContainer.m
// TYAttributedLabelDemo
//
// Created by tanyang on 15/6/4.
// Copyright (c) 2015tanyang. All rights reserved.
//
#import "TYTextContainer.h"
#define kTextColor [UIColor colorWithRed:51/255.0 green:51/255.0 blue:51/255.0 alpha:1]
#define kLinkColor [UIColor colorWithRed:0/255.0 green:91/255.0 blue:255/255.0 alpha:1]
// this code quote TTTAttributedLabel
static inline CGSize CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(CTFramesetterRef framesetter, NSAttributedString *attributedString, CGSize size, NSUInteger numberOfLines) {
CFRange rangeToSize = CFRangeMake(0, (CFIndex)[attributedString length]);
CGSize constraints = CGSizeMake(size.width, MAXFLOAT);
if (numberOfLines > 0) {
// If the line count of the label more than 1, limit the range to size to the number of lines that have been set
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0.0f, 0.0f, constraints.width, MAXFLOAT));
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
CFArrayRef lines = CTFrameGetLines(frame);
if (CFArrayGetCount(lines) > 0) {
NSInteger lastVisibleLineIndex = MIN((CFIndex)numberOfLines, CFArrayGetCount(lines)) - 1;
CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex);
CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine);
rangeToSize = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length);
}
CFRelease(frame);
CFRelease(path);
}
CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, rangeToSize, NULL, constraints, NULL);
return CGSizeMake(ceil(suggestedSize.width), ceil(suggestedSize.height));
}
@interface TYTextContainer()
@property (nonatomic, strong) NSMutableArray *textStorageArray; // run数组
@property (nonatomic, strong) NSArray *textStorages; // run array copy
@property (nonatomic, strong) NSDictionary *drawRectDictionary;
@property (nonatomic, strong) NSDictionary *runRectDictionary; // runRect字典
@property (nonatomic, strong) NSDictionary *linkRectDictionary; // linkRect字典
@property (nonatomic, assign) NSInteger replaceStringNum; // 图片替换字符数
@property (nonatomic, strong) NSMutableAttributedString *attString;
@property (nonatomic, assign) CTFrameRef frameRef;
@property (nonatomic, assign) CGFloat textHeight;
@property (nonatomic, assign) CGFloat textWidth;
@end
@implementation TYTextContainer
- (instancetype)init
{
if (self = [super init]) {
[self setupProperty];
}
return self;
}
#pragma mark - getter
- (NSMutableArray *)textStorageArray
{
if (_textStorageArray == nil) {
_textStorageArray = [NSMutableArray array];
}
return _textStorageArray;
}
- (NSString *)text{
return _attString.string;
}
- (NSAttributedString *)attributedText
{
return [_attString copy];
}
- (NSAttributedString *)createAttributedString
{
[self addTextStoragesWithAtrributedString:_attString];
if (_attString == nil) {
_attString = [[NSMutableAttributedString alloc] init];
}
return [_attString copy];
}
#pragma mark - setter
- (void)setupProperty
{
_font = [UIFont systemFontOfSize:15];
_characterSpacing = 1;
_linesSpacing = 2;
_paragraphSpacing = 0;
#if __IPHONE_OS_VERSION_MAX_ALLOWED <= 9000
_textAlignment = kCTLeftTextAlignment;
#else
_textAlignment = kCTTextAlignmentLeft;
#endif
_lineBreakMode = kCTLineBreakByCharWrapping;
_textColor = kTextColor;
_linkColor = kLinkColor;
_replaceStringNum = 0;
}
- (void)resetAllAttributed
{
[self resetRectDictionary];
_textStorageArray = nil;
_textStorages = nil;
_replaceStringNum = 0;
}
- (void)resetRectDictionary
{
_drawRectDictionary = nil;
_linkRectDictionary = nil;
_runRectDictionary = nil;
}
- (void)resetFrameRef
{
if (_frameRef) {
CFRelease(_frameRef);
_frameRef = nil;
}
_textHeight = 0;
}
- (void)setText:(NSString *)text
{
_attString = [self createTextAttibuteStringWithText:text];
[self resetAllAttributed];
[self resetFrameRef];
}
- (void)setAttributedText:(NSAttributedString *)attributedText
{
if (attributedText == nil) {
_attString = [[NSMutableAttributedString alloc] init];
}else if ([attributedText isKindOfClass:[NSMutableAttributedString class]]) {
_attString = (NSMutableAttributedString *)attributedText;
}else {
_attString = [[NSMutableAttributedString alloc] initWithAttributedString:attributedText];
}
[self resetAllAttributed];
[self resetFrameRef];
}
- (void)setTextColor:(UIColor *)textColor
{
if (textColor && _textColor != textColor){
_textColor = textColor;
[_attString addAttributeTextColor:textColor];
[self resetFrameRef];
}
}
- (void)setFont:(UIFont *)font
{
if (font && _font != font){
_font = font;
[_attString addAttributeFont:font];
[self resetFrameRef];
}
}
- (void)setStrokeWidth:(unichar)strokeWidth
{
if (_strokeWidth != strokeWidth) {
_strokeWidth = strokeWidth;
[_attString addAttributeStrokeWidth:strokeWidth strokeColor:_strokeColor];
[self resetFrameRef];
}
}
- (void)setStrokeColor:(UIColor *)strokeColor
{
if (strokeColor && _strokeColor != strokeColor) {
_strokeColor = strokeColor;
[_attString addAttributeStrokeWidth:_strokeWidth strokeColor:strokeColor];
[self resetFrameRef];
}
}
- (void)setCharacterSpacing:(unichar)characterSpacing
{
if (_characterSpacing != characterSpacing) {
_characterSpacing = characterSpacing;
[_attString addAttributeCharacterSpacing:characterSpacing];
[self resetFrameRef];
}
}
- (void)setLinesSpacing:(CGFloat)linesSpacing
{
if (_linesSpacing != linesSpacing) {
_linesSpacing = linesSpacing;
[self addAttributeAlignmentStyle:_textAlignment lineSpaceStyle:linesSpacing paragraphSpaceStyle:_paragraphSpacing lineBreakStyle:_lineBreakMode];
[self resetFrameRef];
}
}
- (void)setParagraphSpacing:(CGFloat)paragraphSpacing
{
if (_paragraphSpacing != paragraphSpacing) {
_paragraphSpacing = paragraphSpacing;
[self addAttributeAlignmentStyle:_textAlignment lineSpaceStyle:_linesSpacing paragraphSpaceStyle:_paragraphSpacing lineBreakStyle:_lineBreakMode];
[self resetFrameRef];
}
}
- (void)setTextAlignment:(CTTextAlignment)textAlignment
{
if (_textAlignment != textAlignment) {
_textAlignment = textAlignment;
[self addAttributeAlignmentStyle:textAlignment lineSpaceStyle:_linesSpacing paragraphSpaceStyle:_paragraphSpacing lineBreakStyle:_lineBreakMode];
[self resetFrameRef];
}
}
- (void)setLineBreakMode:(CTLineBreakMode)lineBreakMode
{
if (_lineBreakMode != lineBreakMode) {
_lineBreakMode = lineBreakMode;
[self addAttributeAlignmentStyle:_textAlignment lineSpaceStyle:_linesSpacing paragraphSpaceStyle:_paragraphSpacing lineBreakStyle:_lineBreakMode];
[self resetFrameRef];
}
}
#pragma mark - create text attibuteString
- (NSMutableAttributedString *)createTextAttibuteStringWithText:(NSString *)text
{
if (text.length <= 0) {
return [[NSMutableAttributedString alloc] init];
}
// 创建属性文本
NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:text];
// 添加文本颜色 字体属性
[self addTextColorAndFontWithAtrributedString:attString];
// 添加文本段落样式
[self addTextParaphStyleWithAtrributedString:attString];
return attString;
}
// 添加文本颜色 字体属性
- (void)addTextColorAndFontWithAtrributedString:(NSMutableAttributedString *)attString
{
// 添加文本字体
[attString addAttributeFont:_font];
// 添加文本颜色
[attString addAttributeTextColor:_textColor];
// 添加空心字体
if (_strokeWidth > 0) {
[attString addAttributeStrokeWidth:_strokeWidth strokeColor:_strokeColor];
}
}
// 添加文本段落样式
- (void)addTextParaphStyleWithAtrributedString:(NSMutableAttributedString *)attString
{
// 字体间距
if (_characterSpacing)
{
[attString addAttributeCharacterSpacing:_characterSpacing];
}
// 添加文本段落样式
[self addAttributeAlignmentStyle:_textAlignment lineSpaceStyle:_linesSpacing paragraphSpaceStyle:_paragraphSpacing lineBreakStyle:_lineBreakMode];
}
- (void)addAttributeAlignmentStyle:(CTTextAlignment)textAlignment
lineSpaceStyle:(CGFloat)linesSpacing
paragraphSpaceStyle:(CGFloat)paragraphSpacing
lineBreakStyle:(CTLineBreakMode)lineBreakMode
{
if (lineBreakMode == kCTLineBreakByTruncatingTail)
{
lineBreakMode = _numberOfLines == 1 ? kCTLineBreakByCharWrapping : kCTLineBreakByWordWrapping;
}
[_attString addAttributeAlignmentStyle:_textAlignment lineSpaceStyle:_linesSpacing paragraphSpaceStyle:_paragraphSpacing lineBreakStyle:lineBreakMode];
}
#pragma mark - add text storage atrributed
- (void)addTextStoragesWithAtrributedString:(NSMutableAttributedString *)attString
{
if (attString && _textStorageArray.count > 0) {
// 排序range
[self sortTextStorageArray:_textStorageArray];
for (id<TYTextStorageProtocol> textStorage in _textStorageArray) {
// 修正图片替换字符来的误差
if ([textStorage conformsToProtocol:@protocol(TYDrawStorageProtocol) ]) {
continue;
}
if ([textStorage conformsToProtocol:@protocol(TYLinkStorageProtocol)]) {
if (!((id<TYLinkStorageProtocol>)textStorage).textColor) {
((id<TYLinkStorageProtocol>)textStorage).textColor = _linkColor;
}
}
// 验证范围
if (NSMaxRange(textStorage.range) <= attString.length) {
[textStorage addTextStorageWithAttributedString:attString];
}
}
for (id<TYTextStorageProtocol> textStorage in _textStorageArray) {
textStorage.realRange = NSMakeRange(textStorage.range.location-_replaceStringNum, textStorage.range.length);
if ([textStorage conformsToProtocol:@protocol(TYDrawStorageProtocol)]) {
id<TYDrawStorageProtocol> drawStorage = (id<TYDrawStorageProtocol>)textStorage;
NSInteger currentLenght = attString.length;
[drawStorage setTextfontAscent:_font.ascender descent:_font.descender];
[drawStorage currentReplacedStringNum:_replaceStringNum];
[drawStorage addTextStorageWithAttributedString:attString];
_replaceStringNum += currentLenght - attString.length;
}
}
_textStorages = [_textStorageArray copy];
[_textStorageArray removeAllObjects];
}
}
- (void)sortTextStorageArray:(NSMutableArray *)textStorageArray
{
[textStorageArray sortUsingComparator:^NSComparisonResult(id<TYTextStorageProtocol> obj1, id<TYTextStorageProtocol> obj2) {
if (obj1.range.location < obj2.range.location) {
return NSOrderedAscending;
} else if (obj1.range.location > obj2.range.location){
return NSOrderedDescending;
}else {
return obj1.range.length > obj2.range.length ? NSOrderedAscending:NSOrderedDescending;
}
}];
}
- (void)saveTextStorageRectWithFrame:(CTFrameRef)frame
{
if (!frame) {
return;
}
// 获取每行
CFArrayRef lines = CTFrameGetLines(frame);
CGPoint lineOrigins[CFArrayGetCount(lines)];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
CGFloat viewWidth = _textWidth;
NSInteger numberOfLines = _numberOfLines > 0 ? MIN(_numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines);
NSMutableDictionary *runRectDictionary = [NSMutableDictionary dictionary];
NSMutableDictionary *linkRectDictionary = [NSMutableDictionary dictionary];
NSMutableDictionary *drawRectDictionary = [NSMutableDictionary dictionary];
// 获取每行有多少run
for (int i = 0; i < numberOfLines; i++) {
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CGFloat lineAscent;
CGFloat lineDescent;
CGFloat lineLeading;
CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading);
CFArrayRef runs = CTLineGetGlyphRuns(line);
// 获得每行的run
for (int j = 0; j < CFArrayGetCount(runs); j++) {
CGFloat runAscent;
CGFloat runDescent;
CGPoint lineOrigin = lineOrigins[i];
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
// run的属性字典
NSDictionary* attributes = (NSDictionary*)CTRunGetAttributes(run);
id<TYTextStorageProtocol> textStorage = [attributes objectForKey:kTYTextRunAttributedName];
if (textStorage) {
CGFloat runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0,0), &runAscent, &runDescent, NULL);
if (viewWidth > 0 && runWidth > viewWidth) {
runWidth = viewWidth;
}
CGRect runRect = CGRectMake(lineOrigin.x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL), lineOrigin.y - runDescent, runWidth, runAscent + runDescent);
if ([textStorage conformsToProtocol:@protocol(TYDrawStorageProtocol)]) {
[drawRectDictionary setObject:textStorage forKey:[NSValue valueWithCGRect:runRect]];
} else if ([textStorage conformsToProtocol:@protocol(TYLinkStorageProtocol)]) {
[linkRectDictionary setObject:textStorage forKey:[NSValue valueWithCGRect:runRect]];
}
[runRectDictionary setObject:textStorage forKey:[NSValue valueWithCGRect:runRect]];
}
}
}
if (drawRectDictionary.count > 0) {
_drawRectDictionary = [drawRectDictionary copy];
}else {
_drawRectDictionary = nil;
}
if (runRectDictionary.count > 0) {
// 添加响应点击rect
[self addRunRectDictionary:[runRectDictionary copy]];
}
if (linkRectDictionary.count > 0) {
_linkRectDictionary = [linkRectDictionary copy];
}else {
_linkRectDictionary = nil;
}
}
// 添加响应点击rect
- (void)addRunRectDictionary:(NSDictionary *)runRectDictionary
{
if (runRectDictionary.count < _runRectDictionary.count) {
NSMutableArray *drawStorageArray = [[_runRectDictionary allValues]mutableCopy];
// 剔除已经画出来的
[drawStorageArray removeObjectsInArray:[runRectDictionary allValues]];
// 遍历不会画出来的
for (id<TYTextStorageProtocol>drawStorage in drawStorageArray) {
if ([drawStorage conformsToProtocol:@protocol(TYViewStorageProtocol)]) {
[(id<TYViewStorageProtocol>)drawStorage didNotDrawRun];
}
}
}
_runRectDictionary = runRectDictionary;
}
- (CGSize)getSuggestedSizeWithFramesetter:(CTFramesetterRef)framesetter width:(CGFloat)width
{
if (_attString == nil || width <= 0) {
return CGSizeZero;
}
if (_textHeight > 0) {
return CGSizeMake(_textWidth > 0 ? _textWidth : width, _textHeight);
}
// 是否需要更新frame
if (framesetter == nil) {
framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)[self createAttributedString]);
}else {
CFRetain(framesetter);
}
// 获得建议的size
CGSize suggestedSize = CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(framesetter, _attString, CGSizeMake(width,MAXFLOAT), _numberOfLines);
CFRelease(framesetter);
return CGSizeMake(_isWidthToFit ? suggestedSize.width : width, suggestedSize.height+1);
}
- (CGFloat)getHeightWithFramesetter:(CTFramesetterRef)framesetter width:(CGFloat)width
{
return [self getSuggestedSizeWithFramesetter:framesetter width:width].height;
}
- (CTFrameRef)createFrameRefWithFramesetter:(CTFramesetterRef)framesetter textSize:(CGSize)textSize
{
// 这里你需要创建一个用于绘制文本的路径区域,通过 self.bounds 使用整个视图矩形区域创建 CGPath 引用。
CGMutablePathRef path = CGPathCreateMutable();
CGFloat textHeight = [self getHeightWithFramesetter:framesetter width:textSize.width];
CGPathAddRect(path, NULL, CGRectMake(0, 0, textSize.width, MAX(textHeight, textSize.height)));
CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [_attString length]), path, NULL);
CFRelease(path);
return frameRef;
}
- (instancetype)createTextContainerWithTextWidth:(CGFloat)textWidth
{
return [self createTextContainerWithContentSize:CGSizeMake(textWidth, 0)];
}
- (instancetype)createTextContainerWithContentSize:(CGSize)contentSize
{
if (_frameRef) {
return self;
}
NSAttributedString *attStr = [self createAttributedString];
// 创建CTFramesetter
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attStr);
// 获得建议的size
CGSize size = [self getSuggestedSizeWithFramesetter:framesetter width:contentSize.width];
_textWidth = size.width;
_textHeight = size.height;
// 创建CTFrameRef
_frameRef = [self createFrameRefWithFramesetter:framesetter textSize:CGSizeMake(_textWidth, contentSize.height > 0 ? contentSize.height : _textHeight)];
// 释放内存
CFRelease(framesetter);
// 保存run rect
[self saveTextStorageRectWithFrame:_frameRef];
return self;
}
#pragma mark - enumerate runRect
- (BOOL)existRunRectDictionary
{
return _runRectDictionary.count != 0;
}
- (BOOL)existLinkRectDictionary
{
return _linkRectDictionary.count != 0;
}
- (BOOL)existDrawRectDictionary
{
return _drawRectDictionary.count != 0;
}
- (void)enumerateDrawRectDictionaryUsingBlock:(void (^)(id<TYDrawStorageProtocol> drawStorage, CGRect rect))block
{
[_drawRectDictionary enumerateKeysAndObjectsUsingBlock:^(NSValue *rectValue, id<TYDrawStorageProtocol> drawStorage, BOOL * stop) {
if (block) {
block(drawStorage,[rectValue CGRectValue]);
}
}];
}
- (BOOL)enumerateRunRectContainPoint:(CGPoint)point viewHeight:(CGFloat)viewHeight successBlock:(void (^)(id<TYTextStorageProtocol> textStorage))successBlock
{
return [self enumerateRunRect:_runRectDictionary ContainPoint:point viewHeight:viewHeight successBlock:successBlock];
}
- (BOOL)enumerateLinkRectContainPoint:(CGPoint)point viewHeight:(CGFloat)viewHeight successBlock:(void (^)(id<TYLinkStorageProtocol> textStorage))successBlock
{
return [self enumerateRunRect:_linkRectDictionary ContainPoint:point viewHeight:viewHeight successBlock:successBlock];
}
- (BOOL)enumerateRunRect:(NSDictionary *)runRectDic ContainPoint:(CGPoint)point viewHeight:(CGFloat)viewHeight successBlock:(void (^)(id textStorage))successBlock
{
if (runRectDic.count == 0) {
return NO;
}
// CoreText context coordinates are the opposite to UIKit so we flip the bounds
CGAffineTransform transform = CGAffineTransformScale(CGAffineTransformMakeTranslation(0, viewHeight), 1.f, -1.f);
__block BOOL find = NO;
// 遍历run位置字典
[runRectDic enumerateKeysAndObjectsUsingBlock:^(NSValue *keyRectValue, id<TYTextStorageProtocol> textStorage, BOOL *stop) {
CGRect imgRect = [keyRectValue CGRectValue];
CGRect rect = CGRectApplyAffineTransform(imgRect, transform);
if ([textStorage conformsToProtocol:@protocol(TYDrawStorageProtocol) ]) {
rect = UIEdgeInsetsInsetRect(rect,((id<TYDrawStorageProtocol>)textStorage).margin);
}
// point 是否在rect里
if(CGRectContainsPoint(rect, point)){
find = YES;
*stop = YES;
if (successBlock) {
successBlock(textStorage);
}
}
}];
return find;
}
- (void)dealloc{
[self resetFrameRef];
}
@end
#pragma mark - add textStorage
@implementation TYTextContainer (Add)
- (void)addTextStorage:(id<TYTextStorageProtocol>)textStorage
{
if (textStorage) {
[self.textStorageArray addObject:textStorage];
[self resetFrameRef];
}
}
- (void)addTextStorageArray:(NSArray *)textStorageArray
{
if (textStorageArray) {
for (id<TYTextStorageProtocol> textStorage in textStorageArray) {
if ([textStorage conformsToProtocol:@protocol(TYTextStorageProtocol)]) {
[self addTextStorage:textStorage];
}
}
}
}
@end
#pragma mark - append textStorage
@implementation TYTextContainer (Append)
- (void)appendText:(NSString *)text
{
NSAttributedString *attributedText = [self createTextAttibuteStringWithText:text];
[self appendTextAttributedString:attributedText];
[self resetFrameRef];
}
- (void)appendTextAttributedString:(NSAttributedString *)attributedText
{
if (attributedText == nil) {
return;
}
if (_attString == nil) {
_attString = [[NSMutableAttributedString alloc] init];
}
if ([attributedText isKindOfClass:[NSMutableAttributedString class]]) {
[self addTextParaphStyleWithAtrributedString:(NSMutableAttributedString *)attributedText];
}
[_attString appendAttributedString:attributedText];
[self resetFrameRef];
}
- (void)appendTextStorage:(id<TYAppendTextStorageProtocol>)textStorage
{
if (textStorage) {
if ([textStorage conformsToProtocol:@protocol(TYDrawStorageProtocol)]) {
[(id<TYDrawStorageProtocol>)textStorage setTextfontAscent:_font.ascender descent:_font.descender];
} else if ([textStorage conformsToProtocol:@protocol(TYLinkStorageProtocol)]) {
if (!((id<TYLinkStorageProtocol>)textStorage).textColor) {
((id<TYLinkStorageProtocol>)textStorage).textColor = _linkColor;
}
}
NSAttributedString *attAppendString = [textStorage appendTextStorageAttributedString];
textStorage.realRange = NSMakeRange(_attString.length, attAppendString.length);
[self appendTextAttributedString:attAppendString];
[self resetFrameRef];
}
}
- (void)appendTextStorageArray:(NSArray *)textStorageArray
{
if (textStorageArray) {
for (id<TYAppendTextStorageProtocol> textStorage in textStorageArray) {
if ([textStorage conformsToProtocol:@protocol(TYAppendTextStorageProtocol)]) {
[self appendTextStorage:textStorage];
}
}
}
}
@end