// // WBFeedLayout.m // YYKitExample // // Created by ibireme on 15/9/5. // Copyright (c) 2015 ibireme. All rights reserved. // #import "WBStatusLayout.h" /* 将每行的 baseline 位置固定下来,不受不同字体的 ascent/descent 影响。 注意,Heiti SC 中, ascent + descent = font size, 但是在 PingFang SC 中,ascent + descent > font size。 所以这里统一用 Heiti SC (0.86 ascent, 0.14 descent) 作为顶部和底部标准,保证不同系统下的显示一致性。 间距仍然用字体默认 */ @implementation WBTextLinePositionModifier - (instancetype)init { self = [super init]; if (kiOS9Later) { _lineHeightMultiple = 1.34; // for PingFang SC } else { _lineHeightMultiple = 1.3125; // for Heiti SC } return self; } - (void)modifyLines:(NSArray *)lines fromText:(NSAttributedString *)text inContainer:(YYTextContainer *)container { //CGFloat ascent = _font.ascender; CGFloat ascent = _font.pointSize * 0.86; CGFloat lineHeight = _font.pointSize * _lineHeightMultiple; for (YYTextLine *line in lines) { CGPoint position = line.position; position.y = _paddingTop + ascent + line.row * lineHeight; line.position = position; } } - (id)copyWithZone:(NSZone *)zone { WBTextLinePositionModifier *one = [self.class new]; one->_font = _font; one->_paddingTop = _paddingTop; one->_paddingBottom = _paddingBottom; one->_lineHeightMultiple = _lineHeightMultiple; return one; } - (CGFloat)heightForLineCount:(NSUInteger)lineCount { if (lineCount == 0) return 0; // CGFloat ascent = _font.ascender; // CGFloat descent = -_font.descender; CGFloat ascent = _font.pointSize * 0.86; CGFloat descent = _font.pointSize * 0.14; CGFloat lineHeight = _font.pointSize * _lineHeightMultiple; return _paddingTop + _paddingBottom + ascent + descent + (lineCount - 1) * lineHeight; } @end /** 微博的文本中,某些嵌入的图片需要从网上下载,这里简单做个封装 */ @interface WBTextImageViewAttachment : YYTextAttachment @property (nonatomic, strong) NSURL *imageURL; @property (nonatomic, assign) CGSize size; @end @implementation WBTextImageViewAttachment { UIImageView *_imageView; } - (void)setContent:(id)content { _imageView = content; } - (id)content { /// UIImageView 只能在主线程访问 if (pthread_main_np() == 0) return nil; if (_imageView) return _imageView; /// 第一次获取时 (应该是在文本渲染完成,需要添加附件视图时),初始化图片视图,并下载图片 /// 这里改成 YYAnimatedImageView 就能支持 GIF/APNG/WebP 动画了 _imageView = [UIImageView new]; _imageView.size = _size; [_imageView setImageWithURL:_imageURL placeholder:nil]; return _imageView; } @end @implementation WBStatusLayout - (instancetype)initWithStatus:(WBStatus *)status style:(WBLayoutStyle)style { if (!status || !status.user) return nil; self = [super init]; _status = status; _style = style; [self layout]; return self; } - (void)layout { [self _layout]; } - (void)updateDate { [self _layoutSource]; } - (void)_layout { _marginTop = kWBCellTopMargin; _titleHeight = 0; _profileHeight = 0; _textHeight = 0; _retweetHeight = 0; _retweetTextHeight = 0; _retweetPicHeight = 0; _retweetCardHeight = 0; _picHeight = 0; _cardHeight = 0; _toolbarHeight = kWBCellToolbarHeight; _marginBottom = kWBCellToolbarBottomMargin; // 文本排版,计算布局 [self _layoutTitle]; [self _layoutProfile]; [self _layoutRetweet]; if (_retweetHeight == 0) { [self _layoutPics]; if (_picHeight == 0) { [self _layoutCard]; } } [self _layoutText]; [self _layoutTag]; [self _layoutToolbar]; // 计算高度 _height = 0; _height += _marginTop; _height += _titleHeight; _height += _profileHeight; _height += _textHeight; if (_retweetHeight > 0) { _height += _retweetHeight; } else if (_picHeight > 0) { _height += _picHeight; } else if (_cardHeight > 0) { _height += _cardHeight; } if (_tagHeight > 0) { _height += _tagHeight; } else { if (_picHeight > 0 || _cardHeight > 0) { _height += kWBCellPadding; } } _height += _toolbarHeight; _height += _marginBottom; } - (void)_layoutTitle { _titleHeight = 0; _titleTextLayout = nil; WBStatusTitle *title = _status.title; if (title.text.length == 0) return; NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:title.text]; if (title.iconURL) { NSAttributedString *icon = [self _attachmentWithFontSize:kWBCellTitlebarFontSize imageURL:title.iconURL shrink:NO]; if (icon) { [text insertAttributedString:icon atIndex:0]; } } text.color = kWBCellToolbarTitleColor; text.font = [UIFont systemFontOfSize:kWBCellTitlebarFontSize]; YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(kScreenWidth - 100, kWBCellTitleHeight)]; _titleTextLayout = [YYTextLayout layoutWithContainer:container text:text]; _titleHeight = kWBCellTitleHeight; } - (void)_layoutProfile { [self _layoutName]; [self _layoutSource]; _profileHeight = kWBCellProfileHeight; } /// 名字 - (void)_layoutName { WBUser *user = _status.user; NSString *nameStr = nil; if (user.remark.length) { nameStr = user.remark; } else if (user.screenName.length) { nameStr = user.screenName; } else { nameStr = user.name; } if (nameStr.length == 0) { _nameTextLayout = nil; return; } NSMutableAttributedString *nameText = [[NSMutableAttributedString alloc] initWithString:nameStr]; // 蓝V if (user.userVerifyType == WBUserVerifyTypeOrganization) { UIImage *blueVImage = [WBStatusHelper imageNamed:@"avatar_enterprise_vip"]; NSAttributedString *blueVText = [self _attachmentWithFontSize:kWBCellNameFontSize image:blueVImage shrink:NO]; [nameText appendString:@" "]; [nameText appendAttributedString:blueVText]; } // VIP if (user.mbrank > 0) { UIImage *yelllowVImage = [WBStatusHelper imageNamed:[NSString stringWithFormat:@"common_icon_membership_level%d",user.mbrank]]; if (!yelllowVImage) { yelllowVImage = [WBStatusHelper imageNamed:@"common_icon_membership"]; } NSAttributedString *vipText = [self _attachmentWithFontSize:kWBCellNameFontSize image:yelllowVImage shrink:NO]; [nameText appendString:@" "]; [nameText appendAttributedString:vipText]; } nameText.font = [UIFont systemFontOfSize:kWBCellNameFontSize]; nameText.color = user.mbrank > 0 ? kWBCellNameOrangeColor : kWBCellNameNormalColor; nameText.lineBreakMode = NSLineBreakByCharWrapping; YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(kWBCellNameWidth, 9999)]; container.maximumNumberOfRows = 1; _nameTextLayout = [YYTextLayout layoutWithContainer:container text:nameText]; } /// 时间和来源 - (void)_layoutSource { NSMutableAttributedString *sourceText = [NSMutableAttributedString new]; NSString *createTime = [WBStatusHelper stringWithTimelineDate:_status.createdAt]; // 时间 if (createTime.length) { NSMutableAttributedString *timeText = [[NSMutableAttributedString alloc] initWithString:createTime]; [timeText appendString:@" "]; timeText.font = [UIFont systemFontOfSize:kWBCellSourceFontSize]; timeText.color = kWBCellTimeNormalColor; [sourceText appendAttributedString:timeText]; } // 来自 XXX if (_status.source.length) { // iPhone 5siPhone 5s static NSRegularExpression *hrefRegex, *textRegex; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ hrefRegex = [NSRegularExpression regularExpressionWithPattern:@"(?<=href=\").+(?=\" )" options:kNilOptions error:NULL]; textRegex = [NSRegularExpression regularExpressionWithPattern:@"(?<=>).+(?=<)" options:kNilOptions error:NULL]; }); NSTextCheckingResult *hrefResult, *textResult; NSString *href = nil, *text = nil; hrefResult = [hrefRegex firstMatchInString:_status.source options:kNilOptions range:NSMakeRange(0, _status.source.length)]; textResult = [textRegex firstMatchInString:_status.source options:kNilOptions range:NSMakeRange(0, _status.source.length)]; if (hrefResult && textResult && hrefResult.range.location != NSNotFound && textResult.range.location != NSNotFound) { href = [_status.source substringWithRange:hrefResult.range]; text = [_status.source substringWithRange:textResult.range]; } if (href.length && text.length) { NSMutableAttributedString *from = [NSMutableAttributedString new]; [from appendString:[NSString stringWithFormat:@"来自 %@", text]]; from.font = [UIFont systemFontOfSize:kWBCellSourceFontSize]; from.color = kWBCellTimeNormalColor; if (_status.sourceAllowClick > 0) { NSRange range = NSMakeRange(3, text.length); [from setColor:kWBCellTextHighlightColor range:range]; YYTextBackedString *backed = [YYTextBackedString stringWithString:href]; [from setTextBackedString:backed range:range]; YYTextBorder *border = [YYTextBorder new]; border.insets = UIEdgeInsetsMake(-2, 0, -2, 0); border.fillColor = kWBCellTextHighlightBackgroundColor; border.cornerRadius = 3; YYTextHighlight *highlight = [YYTextHighlight new]; if (href) highlight.userInfo = @{kWBLinkHrefName : href}; [highlight setBackgroundBorder:border]; [from setTextHighlight:highlight range:range]; } [sourceText appendAttributedString:from]; } } if (sourceText.length == 0) { _sourceTextLayout = nil; } else { YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(kWBCellNameWidth, 9999)]; container.maximumNumberOfRows = 1; _sourceTextLayout = [YYTextLayout layoutWithContainer:container text:sourceText]; } } - (void)_layoutRetweet { _retweetHeight = 0; [self _layoutRetweetedText]; [self _layoutRetweetPics]; if (_retweetPicHeight == 0) { [self _layoutRetweetCard]; } _retweetHeight = _retweetTextHeight; if (_retweetPicHeight > 0) { _retweetHeight += _retweetPicHeight; _retweetHeight += kWBCellPadding; } else if (_retweetCardHeight > 0) { _retweetHeight += _retweetCardHeight; _retweetHeight += kWBCellPadding; } } /// 文本 - (void)_layoutText { _textHeight = 0; _textLayout = nil; NSMutableAttributedString *text = [self _textWithStatus:_status isRetweet:NO fontSize:kWBCellTextFontSize textColor:kWBCellTextNormalColor]; if (text.length == 0) return; WBTextLinePositionModifier *modifier = [WBTextLinePositionModifier new]; modifier.font = [UIFont fontWithName:@"Heiti SC" size:kWBCellTextFontSize]; modifier.paddingTop = kWBCellPaddingText; modifier.paddingBottom = kWBCellPaddingText; YYTextContainer *container = [YYTextContainer new]; container.size = CGSizeMake(kWBCellContentWidth, HUGE); container.linePositionModifier = modifier; _textLayout = [YYTextLayout layoutWithContainer:container text:text]; if (!_textLayout) return; _textHeight = [modifier heightForLineCount:_textLayout.rowCount]; } - (void)_layoutRetweetedText { _retweetHeight = 0; _retweetTextLayout = nil; NSMutableAttributedString *text = [self _textWithStatus:_status.retweetedStatus isRetweet:YES fontSize:kWBCellTextFontRetweetSize textColor:kWBCellTextSubTitleColor]; if (text.length == 0) return; WBTextLinePositionModifier *modifier = [WBTextLinePositionModifier new]; modifier.font = [UIFont fontWithName:@"Heiti SC" size:kWBCellTextFontRetweetSize]; modifier.paddingTop = kWBCellPaddingText; modifier.paddingBottom = kWBCellPaddingText; YYTextContainer *container = [YYTextContainer new]; container.size = CGSizeMake(kWBCellContentWidth, HUGE); container.linePositionModifier = modifier; _retweetTextLayout = [YYTextLayout layoutWithContainer:container text:text]; if (!_retweetTextLayout) return; _retweetTextHeight = [modifier heightForLineCount:_retweetTextLayout.lines.count]; } - (void)_layoutPics { [self _layoutPicsWithStatus:_status isRetweet:NO]; } - (void)_layoutRetweetPics { [self _layoutPicsWithStatus:_status.retweetedStatus isRetweet:YES]; } - (void)_layoutPicsWithStatus:(WBStatus *)status isRetweet:(BOOL)isRetweet { if (isRetweet) { _retweetPicSize = CGSizeZero; _retweetPicHeight = 0; } else { _picSize = CGSizeZero; _picHeight = 0; } if (status.pics.count == 0) return; CGSize picSize = CGSizeZero; CGFloat picHeight = 0; CGFloat len1_3 = (kWBCellContentWidth + kWBCellPaddingPic) / 3 - kWBCellPaddingPic; len1_3 = CGFloatPixelRound(len1_3); switch (status.pics.count) { case 1: { WBPicture *pic = _status.pics.firstObject; WBPictureMetadata *bmiddle = pic.bmiddle; if (pic.keepSize || bmiddle.width < 1 || bmiddle.height < 1) { CGFloat maxLen = kWBCellContentWidth / 2.0; maxLen = CGFloatPixelRound(maxLen); picSize = CGSizeMake(maxLen, maxLen); picHeight = maxLen; } else { CGFloat maxLen = len1_3 * 2 + kWBCellPaddingPic; if (bmiddle.width < bmiddle.height) { picSize.width = (float)bmiddle.width / (float)bmiddle.height * maxLen; picSize.height = maxLen; } else { picSize.width = maxLen; picSize.height = (float)bmiddle.height / (float)bmiddle.width * maxLen; } picSize = CGSizePixelRound(picSize); picHeight = picSize.height; } } break; case 2: case 3: { picSize = CGSizeMake(len1_3, len1_3); picHeight = len1_3; } break; case 4: case 5: case 6: { picSize = CGSizeMake(len1_3, len1_3); picHeight = len1_3 * 2 + kWBCellPaddingPic; } break; default: { // 7, 8, 9 picSize = CGSizeMake(len1_3, len1_3); picHeight = len1_3 * 3 + kWBCellPaddingPic * 2; } break; } if (isRetweet) { _retweetPicSize = picSize; _retweetPicHeight = picHeight; } else { _picSize = picSize; _picHeight = picHeight; } } - (void)_layoutCard { [self _layoutCardWithStatus:_status isRetweet:NO]; } - (void)_layoutRetweetCard { [self _layoutCardWithStatus:_status.retweetedStatus isRetweet:YES]; } - (void)_layoutCardWithStatus:(WBStatus *)status isRetweet:(BOOL)isRetweet { if (isRetweet) { _retweetCardType = WBStatusCardTypeNone; _retweetCardHeight = 0; _retweetCardTextLayout = nil; _retweetCardTextRect = CGRectZero; } else { _cardType = WBStatusCardTypeNone; _cardHeight = 0; _cardTextLayout = nil; _cardTextRect = CGRectZero; } WBPageInfo *pageInfo = status.pageInfo; if (!pageInfo) return; WBStatusCardType cardType = WBStatusCardTypeNone; CGFloat cardHeight = 0; YYTextLayout *cardTextLayout = nil; CGRect textRect = CGRectZero; if ((pageInfo.type == 11) && [pageInfo.objectType isEqualToString:@"video"]) { // 视频,一个大图片,上面播放按钮 if (pageInfo.pagePic) { cardType = WBStatusCardTypeVideo; cardHeight = (2 * kWBCellContentWidth - kWBCellPaddingPic) / 3.0; } } else { BOOL hasImage = pageInfo.pagePic != nil; BOOL hasBadge = pageInfo.typeIcon != nil; WBButtonLink *button = pageInfo.buttons.firstObject; BOOL hasButtom = button.pic && button.name; /* badge: 25,25 左上角 (42) image: 70,70 方形 100, 70 矩形 btn: 60,70 lineheight 20 padding 10 */ textRect.size.height = 70; if (hasImage) { if (hasBadge) { textRect.origin.x = 100; } else { textRect.origin.x = 70; } } else { if (hasBadge) { textRect.origin.x = 42; } } textRect.origin.x += 10; //padding textRect.size.width = kWBCellContentWidth - textRect.origin.x; if (hasButtom) textRect.size.width -= 60; textRect.size.width -= 10; //padding NSMutableAttributedString *text = [NSMutableAttributedString new]; if (pageInfo.pageTitle.length) { NSMutableAttributedString *title = [[NSMutableAttributedString alloc] initWithString:pageInfo.pageTitle]; title.font = [UIFont systemFontOfSize:kWBCellCardTitleFontSize]; title.color = kWBCellNameNormalColor; [text appendAttributedString:title]; } if (pageInfo.pageDesc.length) { if (text.length) [text appendString:@"\n"]; NSMutableAttributedString *desc = [[NSMutableAttributedString alloc] initWithString:pageInfo.pageDesc]; desc.font = [UIFont systemFontOfSize:kWBCellCardDescFontSize]; desc.color = kWBCellNameNormalColor; [text appendAttributedString:desc]; } else if (pageInfo.content2.length) { if (text.length) [text appendString:@"\n"]; NSMutableAttributedString *content3 = [[NSMutableAttributedString alloc] initWithString:pageInfo.content2]; content3.font = [UIFont systemFontOfSize:kWBCellCardDescFontSize]; content3.color = kWBCellTextSubTitleColor; [text appendAttributedString:content3]; } else if (pageInfo.content3.length) { if (text.length) [text appendString:@"\n"]; NSMutableAttributedString *content3 = [[NSMutableAttributedString alloc] initWithString:pageInfo.content3]; content3.font = [UIFont systemFontOfSize:kWBCellCardDescFontSize]; content3.color = kWBCellTextSubTitleColor; [text appendAttributedString:content3]; } if (pageInfo.tips.length) { if (text.length) [text appendString:@"\n"]; NSMutableAttributedString *tips = [[NSMutableAttributedString alloc] initWithString:pageInfo.tips]; tips.font = [UIFont systemFontOfSize:kWBCellCardDescFontSize]; tips.color = kWBCellTextSubTitleColor; [text appendAttributedString:tips]; } if (text.length) { text.maximumLineHeight = 20; text.minimumLineHeight = 20; text.lineBreakMode = NSLineBreakByTruncatingTail; YYTextContainer *container = [YYTextContainer containerWithSize:textRect.size]; container.maximumNumberOfRows = 3; cardTextLayout = [YYTextLayout layoutWithContainer:container text:text]; } if (cardTextLayout) { cardType = WBStatusCardTypeNormal; cardHeight = 70; } } if (isRetweet) { _retweetCardType = cardType; _retweetCardHeight = cardHeight; _retweetCardTextLayout = cardTextLayout; _retweetCardTextRect = textRect; } else { _cardType = cardType; _cardHeight = cardHeight; _cardTextLayout = cardTextLayout; _cardTextRect = textRect; } } - (void)_layoutTag { _tagType = WBStatusTagTypeNone; _tagHeight = 0; WBTag *tag = _status.tagStruct.firstObject; if (tag.tagName.length == 0) return; NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:tag.tagName]; if (tag.tagType == 1) { _tagType = WBStatusTagTypePlace; _tagHeight = 40; text.color = [UIColor colorWithWhite:0.217 alpha:1.000]; } else { _tagType = WBStatusTagTypeNormal; _tagHeight = 32; if (tag.urlTypePic) { NSAttributedString *pic = [self _attachmentWithFontSize:kWBCellCardDescFontSize imageURL:tag.urlTypePic.absoluteString shrink:YES]; [text insertAttributedString:pic atIndex:0]; } // 高亮状态的背景 YYTextBorder *highlightBorder = [YYTextBorder new]; highlightBorder.insets = UIEdgeInsetsMake(-2, 0, -2, 0); highlightBorder.cornerRadius = 2; highlightBorder.fillColor = kWBCellTextHighlightBackgroundColor; [text setColor:kWBCellTextHighlightColor range:text.rangeOfAll]; // 高亮状态 YYTextHighlight *highlight = [YYTextHighlight new]; [highlight setBackgroundBorder:highlightBorder]; // 数据信息,用于稍后用户点击 highlight.userInfo = @{kWBLinkTagName : tag}; [text setTextHighlight:highlight range:text.rangeOfAll]; } text.font = [UIFont systemFontOfSize:kWBCellCardDescFontSize]; YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(9999, 9999)]; _tagTextLayout = [YYTextLayout layoutWithContainer:container text:text]; if (!_tagTextLayout) { _tagType = WBStatusTagTypeNone; _tagHeight = 0; } } - (void)_layoutToolbar { // should be localized UIFont *font = [UIFont systemFontOfSize:kWBCellToolbarFontSize]; YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(kScreenWidth, kWBCellToolbarHeight)]; container.maximumNumberOfRows = 1; NSMutableAttributedString *repostText = [[NSMutableAttributedString alloc] initWithString:_status.repostsCount <= 0 ? @"转发" : [WBStatusHelper shortedNumberDesc:_status.repostsCount]]; repostText.font = font; repostText.color = kWBCellToolbarTitleColor; _toolbarRepostTextLayout = [YYTextLayout layoutWithContainer:container text:repostText]; _toolbarRepostTextWidth = CGFloatPixelRound(_toolbarRepostTextLayout.textBoundingRect.size.width); NSMutableAttributedString *commentText = [[NSMutableAttributedString alloc] initWithString:_status.commentsCount <= 0 ? @"评论" : [WBStatusHelper shortedNumberDesc:_status.commentsCount]]; commentText.font = font; commentText.color = kWBCellToolbarTitleColor; _toolbarCommentTextLayout = [YYTextLayout layoutWithContainer:container text:commentText]; _toolbarCommentTextWidth = CGFloatPixelRound(_toolbarCommentTextLayout.textBoundingRect.size.width); NSMutableAttributedString *likeText = [[NSMutableAttributedString alloc] initWithString:_status.attitudesCount <= 0 ? @"赞" : [WBStatusHelper shortedNumberDesc:_status.attitudesCount]]; likeText.font = font; likeText.color = _status.attitudesStatus ? kWBCellToolbarTitleHighlightColor : kWBCellToolbarTitleColor; _toolbarLikeTextLayout = [YYTextLayout layoutWithContainer:container text:likeText]; _toolbarLikeTextWidth = CGFloatPixelRound(_toolbarLikeTextLayout.textBoundingRect.size.width); } - (NSMutableAttributedString *)_textWithStatus:(WBStatus *)status isRetweet:(BOOL)isRetweet fontSize:(CGFloat)fontSize textColor:(UIColor *)textColor { if (!status) return nil; NSMutableString *string = status.text.mutableCopy; if (string.length == 0) return nil; if (isRetweet) { NSString *name = status.user.name; if (name.length == 0) { name = status.user.screenName; } if (name) { NSString *insert = [NSString stringWithFormat:@"@%@:",name]; [string insertString:insert atIndex:0]; } } // 字体 UIFont *font = [UIFont systemFontOfSize:fontSize]; // 高亮状态的背景 YYTextBorder *highlightBorder = [YYTextBorder new]; highlightBorder.insets = UIEdgeInsetsMake(-2, 0, -2, 0); highlightBorder.cornerRadius = 3; highlightBorder.fillColor = kWBCellTextHighlightBackgroundColor; NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:string]; text.font = font; text.color = textColor; // 根据 urlStruct 中每个 URL.shortURL 来匹配文本,将其替换为图标+友好描述 for (WBURL *wburl in status.urlStruct) { if (wburl.shortURL.length == 0) continue; if (wburl.urlTitle.length == 0) continue; NSString *urlTitle = wburl.urlTitle; if (urlTitle.length > 27) { urlTitle = [[urlTitle substringToIndex:27] stringByAppendingString:YYTextTruncationToken]; } NSRange searchRange = NSMakeRange(0, text.string.length); do { NSRange range = [text.string rangeOfString:wburl.shortURL options:kNilOptions range:searchRange]; if (range.location == NSNotFound) break; if (range.location + range.length == text.length) { if (status.pageInfo.pageID && wburl.pageID && [wburl.pageID isEqualToString:status.pageInfo.pageID]) { if ((!isRetweet && !status.retweetedStatus) || isRetweet) { if (status.pics.count == 0) { [text replaceCharactersInRange:range withString:@""]; break; // cut the tail, show with card } } } } if ([text attribute:YYTextHighlightAttributeName atIndex:range.location] == nil) { // 替换的内容 NSMutableAttributedString *replace = [[NSMutableAttributedString alloc] initWithString:urlTitle]; if (wburl.urlTypePic.length) { // 链接头部有个图片附件 (要从网络获取) NSURL *picURL = [WBStatusHelper defaultURLForImageURL:wburl.urlTypePic]; UIImage *image = [[YYImageCache sharedCache] getImageForKey:picURL.absoluteString]; NSAttributedString *pic = (image && !wburl.pics.count) ? [self _attachmentWithFontSize:fontSize image:image shrink:YES] : [self _attachmentWithFontSize:fontSize imageURL:wburl.urlTypePic shrink:YES]; [replace insertAttributedString:pic atIndex:0]; } replace.font = font; replace.color = kWBCellTextHighlightColor; // 高亮状态 YYTextHighlight *highlight = [YYTextHighlight new]; [highlight setBackgroundBorder:highlightBorder]; // 数据信息,用于稍后用户点击 highlight.userInfo = @{kWBLinkURLName : wburl}; [replace setTextHighlight:highlight range:NSMakeRange(0, replace.length)]; // 添加被替换的原始字符串,用于复制 YYTextBackedString *backed = [YYTextBackedString stringWithString:[text.string substringWithRange:range]]; [replace setTextBackedString:backed range:NSMakeRange(0, replace.length)]; // 替换 [text replaceCharactersInRange:range withAttributedString:replace]; searchRange.location = searchRange.location + (replace.length ? replace.length : 1); if (searchRange.location + 1 >= text.length) break; searchRange.length = text.length - searchRange.location; } else { searchRange.location = searchRange.location + (searchRange.length ? searchRange.length : 1); if (searchRange.location + 1>= text.length) break; searchRange.length = text.length - searchRange.location; } } while (1); } // 根据 topicStruct 中每个 Topic.topicTitle 来匹配文本,标记为话题 for (WBTopic *topic in status.topicStruct) { if (topic.topicTitle.length == 0) continue; NSString *topicTitle = [NSString stringWithFormat:@"#%@#",topic.topicTitle]; NSRange searchRange = NSMakeRange(0, text.string.length); do { NSRange range = [text.string rangeOfString:topicTitle options:kNilOptions range:searchRange]; if (range.location == NSNotFound) break; if ([text attribute:YYTextHighlightAttributeName atIndex:range.location] == nil) { [text setColor:kWBCellTextHighlightColor range:range]; // 高亮状态 YYTextHighlight *highlight = [YYTextHighlight new]; [highlight setBackgroundBorder:highlightBorder]; // 数据信息,用于稍后用户点击 highlight.userInfo = @{kWBLinkTopicName : topic}; [text setTextHighlight:highlight range:range]; } searchRange.location = searchRange.location + (searchRange.length ? searchRange.length : 1); if (searchRange.location + 1>= text.length) break; searchRange.length = text.length - searchRange.location; } while (1); } // 匹配 @用户名 NSArray *atResults = [[WBStatusHelper regexAt] matchesInString:text.string options:kNilOptions range:text.rangeOfAll]; for (NSTextCheckingResult *at in atResults) { if (at.range.location == NSNotFound && at.range.length <= 1) continue; if ([text attribute:YYTextHighlightAttributeName atIndex:at.range.location] == nil) { [text setColor:kWBCellTextHighlightColor range:at.range]; // 高亮状态 YYTextHighlight *highlight = [YYTextHighlight new]; [highlight setBackgroundBorder:highlightBorder]; // 数据信息,用于稍后用户点击 highlight.userInfo = @{kWBLinkAtName : [text.string substringWithRange:NSMakeRange(at.range.location + 1, at.range.length - 1)]}; [text setTextHighlight:highlight range:at.range]; } } // 匹配 [表情] NSArray *emoticonResults = [[WBStatusHelper regexEmoticon] matchesInString:text.string options:kNilOptions range:text.rangeOfAll]; NSUInteger emoClipLength = 0; for (NSTextCheckingResult *emo in emoticonResults) { if (emo.range.location == NSNotFound && emo.range.length <= 1) continue; NSRange range = emo.range; range.location -= emoClipLength; if ([text attribute:YYTextHighlightAttributeName atIndex:range.location]) continue; if ([text attribute:YYTextAttachmentAttributeName atIndex:range.location]) continue; NSString *emoString = [text.string substringWithRange:range]; NSString *imagePath = [WBStatusHelper emoticonDic][emoString]; UIImage *image = [WBStatusHelper imageWithPath:imagePath]; if (!image) continue; NSAttributedString *emoText = [NSAttributedString attachmentStringWithEmojiImage:image fontSize:fontSize]; [text replaceCharactersInRange:range withAttributedString:emoText]; emoClipLength += range.length - 1; } return text; } - (NSAttributedString *)_attachmentWithFontSize:(CGFloat)fontSize image:(UIImage *)image shrink:(BOOL)shrink { // CGFloat ascent = YYEmojiGetAscentWithFontSize(fontSize); // CGFloat descent = YYEmojiGetDescentWithFontSize(fontSize); // CGRect bounding = YYEmojiGetGlyphBoundingRectWithFontSize(fontSize); // Heiti SC 字体。。 CGFloat ascent = fontSize * 0.86; CGFloat descent = fontSize * 0.14; CGRect bounding = CGRectMake(0, -0.14 * fontSize, fontSize, fontSize); UIEdgeInsets contentInsets = UIEdgeInsetsMake(ascent - (bounding.size.height + bounding.origin.y), 0, descent + bounding.origin.y, 0); YYTextRunDelegate *delegate = [YYTextRunDelegate new]; delegate.ascent = ascent; delegate.descent = descent; delegate.width = bounding.size.width; YYTextAttachment *attachment = [YYTextAttachment new]; attachment.contentMode = UIViewContentModeScaleAspectFit; attachment.contentInsets = contentInsets; attachment.content = image; if (shrink) { // 缩小~ CGFloat scale = 1 / 10.0; contentInsets.top += fontSize * scale; contentInsets.bottom += fontSize * scale; contentInsets.left += fontSize * scale; contentInsets.right += fontSize * scale; contentInsets = UIEdgeInsetPixelFloor(contentInsets); attachment.contentInsets = contentInsets; } NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:YYTextAttachmentToken]; [atr setTextAttachment:attachment range:NSMakeRange(0, atr.length)]; CTRunDelegateRef ctDelegate = delegate.CTRunDelegate; [atr setRunDelegate:ctDelegate range:NSMakeRange(0, atr.length)]; if (ctDelegate) CFRelease(ctDelegate); return atr; } - (NSAttributedString *)_attachmentWithFontSize:(CGFloat)fontSize imageURL:(NSString *)imageURL shrink:(BOOL)shrink { /* 微博 URL 嵌入的图片,比临近的字体要小一圈。。 这里模拟一下 Heiti SC 字体,然后把图片缩小一下。 */ CGFloat ascent = fontSize * 0.86; CGFloat descent = fontSize * 0.14; CGRect bounding = CGRectMake(0, -0.14 * fontSize, fontSize, fontSize); UIEdgeInsets contentInsets = UIEdgeInsetsMake(ascent - (bounding.size.height + bounding.origin.y), 0, descent + bounding.origin.y, 0); CGSize size = CGSizeMake(fontSize, fontSize); if (shrink) { // 缩小~ CGFloat scale = 1 / 10.0; contentInsets.top += fontSize * scale; contentInsets.bottom += fontSize * scale; contentInsets.left += fontSize * scale; contentInsets.right += fontSize * scale; contentInsets = UIEdgeInsetPixelFloor(contentInsets); size = CGSizeMake(fontSize - fontSize * scale * 2, fontSize - fontSize * scale * 2); size = CGSizePixelRound(size); } YYTextRunDelegate *delegate = [YYTextRunDelegate new]; delegate.ascent = ascent; delegate.descent = descent; delegate.width = bounding.size.width; WBTextImageViewAttachment *attachment = [WBTextImageViewAttachment new]; attachment.contentMode = UIViewContentModeScaleAspectFit; attachment.contentInsets = contentInsets; attachment.size = size; attachment.imageURL = [WBStatusHelper defaultURLForImageURL:imageURL]; NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:YYTextAttachmentToken]; [atr setTextAttachment:attachment range:NSMakeRange(0, atr.length)]; CTRunDelegateRef ctDelegate = delegate.CTRunDelegate; [atr setRunDelegate:ctDelegate range:NSMakeRange(0, atr.length)]; if (ctDelegate) CFRelease(ctDelegate); return atr; } - (WBTextLinePositionModifier *)_textlineModifier { static WBTextLinePositionModifier *mod; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ mod = [WBTextLinePositionModifier new]; mod.font = [UIFont fontWithName:@"Heiti SC" size:kWBCellTextFontSize]; mod.paddingTop = 10; mod.paddingBottom = 10; }); return mod; } @end