WBStatusLayout.m 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  1. //
  2. // WBFeedLayout.m
  3. // YYKitExample
  4. //
  5. // Created by ibireme on 15/9/5.
  6. // Copyright (c) 2015 ibireme. All rights reserved.
  7. //
  8. #import "WBStatusLayout.h"
  9. /*
  10. 将每行的 baseline 位置固定下来,不受不同字体的 ascent/descent 影响。
  11. 注意,Heiti SC 中, ascent + descent = font size,
  12. 但是在 PingFang SC 中,ascent + descent > font size。
  13. 所以这里统一用 Heiti SC (0.86 ascent, 0.14 descent) 作为顶部和底部标准,保证不同系统下的显示一致性。
  14. 间距仍然用字体默认
  15. */
  16. @implementation WBTextLinePositionModifier
  17. - (instancetype)init {
  18. self = [super init];
  19. if (kiOS9Later) {
  20. _lineHeightMultiple = 1.34; // for PingFang SC
  21. } else {
  22. _lineHeightMultiple = 1.3125; // for Heiti SC
  23. }
  24. return self;
  25. }
  26. - (void)modifyLines:(NSArray *)lines fromText:(NSAttributedString *)text inContainer:(YYTextContainer *)container {
  27. //CGFloat ascent = _font.ascender;
  28. CGFloat ascent = _font.pointSize * 0.86;
  29. CGFloat lineHeight = _font.pointSize * _lineHeightMultiple;
  30. for (YYTextLine *line in lines) {
  31. CGPoint position = line.position;
  32. position.y = _paddingTop + ascent + line.row * lineHeight;
  33. line.position = position;
  34. }
  35. }
  36. - (id)copyWithZone:(NSZone *)zone {
  37. WBTextLinePositionModifier *one = [self.class new];
  38. one->_font = _font;
  39. one->_paddingTop = _paddingTop;
  40. one->_paddingBottom = _paddingBottom;
  41. one->_lineHeightMultiple = _lineHeightMultiple;
  42. return one;
  43. }
  44. - (CGFloat)heightForLineCount:(NSUInteger)lineCount {
  45. if (lineCount == 0) return 0;
  46. // CGFloat ascent = _font.ascender;
  47. // CGFloat descent = -_font.descender;
  48. CGFloat ascent = _font.pointSize * 0.86;
  49. CGFloat descent = _font.pointSize * 0.14;
  50. CGFloat lineHeight = _font.pointSize * _lineHeightMultiple;
  51. return _paddingTop + _paddingBottom + ascent + descent + (lineCount - 1) * lineHeight;
  52. }
  53. @end
  54. /**
  55. 微博的文本中,某些嵌入的图片需要从网上下载,这里简单做个封装
  56. */
  57. @interface WBTextImageViewAttachment : YYTextAttachment
  58. @property (nonatomic, strong) NSURL *imageURL;
  59. @property (nonatomic, assign) CGSize size;
  60. @end
  61. @implementation WBTextImageViewAttachment {
  62. UIImageView *_imageView;
  63. }
  64. - (void)setContent:(id)content {
  65. _imageView = content;
  66. }
  67. - (id)content {
  68. /// UIImageView 只能在主线程访问
  69. if (pthread_main_np() == 0) return nil;
  70. if (_imageView) return _imageView;
  71. /// 第一次获取时 (应该是在文本渲染完成,需要添加附件视图时),初始化图片视图,并下载图片
  72. /// 这里改成 YYAnimatedImageView 就能支持 GIF/APNG/WebP 动画了
  73. _imageView = [UIImageView new];
  74. _imageView.size = _size;
  75. [_imageView setImageWithURL:_imageURL placeholder:nil];
  76. return _imageView;
  77. }
  78. @end
  79. @implementation WBStatusLayout
  80. - (instancetype)initWithStatus:(WBStatus *)status style:(WBLayoutStyle)style {
  81. if (!status || !status.user) return nil;
  82. self = [super init];
  83. _status = status;
  84. _style = style;
  85. [self layout];
  86. return self;
  87. }
  88. - (void)layout {
  89. [self _layout];
  90. }
  91. - (void)updateDate {
  92. [self _layoutSource];
  93. }
  94. - (void)_layout {
  95. _marginTop = kWBCellTopMargin;
  96. _titleHeight = 0;
  97. _profileHeight = 0;
  98. _textHeight = 0;
  99. _retweetHeight = 0;
  100. _retweetTextHeight = 0;
  101. _retweetPicHeight = 0;
  102. _retweetCardHeight = 0;
  103. _picHeight = 0;
  104. _cardHeight = 0;
  105. _toolbarHeight = kWBCellToolbarHeight;
  106. _marginBottom = kWBCellToolbarBottomMargin;
  107. // 文本排版,计算布局
  108. [self _layoutTitle];
  109. [self _layoutProfile];
  110. [self _layoutRetweet];
  111. if (_retweetHeight == 0) {
  112. [self _layoutPics];
  113. if (_picHeight == 0) {
  114. [self _layoutCard];
  115. }
  116. }
  117. [self _layoutText];
  118. [self _layoutTag];
  119. [self _layoutToolbar];
  120. // 计算高度
  121. _height = 0;
  122. _height += _marginTop;
  123. _height += _titleHeight;
  124. _height += _profileHeight;
  125. _height += _textHeight;
  126. if (_retweetHeight > 0) {
  127. _height += _retweetHeight;
  128. } else if (_picHeight > 0) {
  129. _height += _picHeight;
  130. } else if (_cardHeight > 0) {
  131. _height += _cardHeight;
  132. }
  133. if (_tagHeight > 0) {
  134. _height += _tagHeight;
  135. } else {
  136. if (_picHeight > 0 || _cardHeight > 0) {
  137. _height += kWBCellPadding;
  138. }
  139. }
  140. _height += _toolbarHeight;
  141. _height += _marginBottom;
  142. }
  143. - (void)_layoutTitle {
  144. _titleHeight = 0;
  145. _titleTextLayout = nil;
  146. WBStatusTitle *title = _status.title;
  147. if (title.text.length == 0) return;
  148. NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:title.text];
  149. if (title.iconURL) {
  150. NSAttributedString *icon = [self _attachmentWithFontSize:kWBCellTitlebarFontSize imageURL:title.iconURL shrink:NO];
  151. if (icon) {
  152. [text insertAttributedString:icon atIndex:0];
  153. }
  154. }
  155. text.color = kWBCellToolbarTitleColor;
  156. text.font = [UIFont systemFontOfSize:kWBCellTitlebarFontSize];
  157. YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(kScreenWidth - 100, kWBCellTitleHeight)];
  158. _titleTextLayout = [YYTextLayout layoutWithContainer:container text:text];
  159. _titleHeight = kWBCellTitleHeight;
  160. }
  161. - (void)_layoutProfile {
  162. [self _layoutName];
  163. [self _layoutSource];
  164. _profileHeight = kWBCellProfileHeight;
  165. }
  166. /// 名字
  167. - (void)_layoutName {
  168. WBUser *user = _status.user;
  169. NSString *nameStr = nil;
  170. if (user.remark.length) {
  171. nameStr = user.remark;
  172. } else if (user.screenName.length) {
  173. nameStr = user.screenName;
  174. } else {
  175. nameStr = user.name;
  176. }
  177. if (nameStr.length == 0) {
  178. _nameTextLayout = nil;
  179. return;
  180. }
  181. NSMutableAttributedString *nameText = [[NSMutableAttributedString alloc] initWithString:nameStr];
  182. // 蓝V
  183. if (user.userVerifyType == WBUserVerifyTypeOrganization) {
  184. UIImage *blueVImage = [WBStatusHelper imageNamed:@"avatar_enterprise_vip"];
  185. NSAttributedString *blueVText = [self _attachmentWithFontSize:kWBCellNameFontSize image:blueVImage shrink:NO];
  186. [nameText appendString:@" "];
  187. [nameText appendAttributedString:blueVText];
  188. }
  189. // VIP
  190. if (user.mbrank > 0) {
  191. UIImage *yelllowVImage = [WBStatusHelper imageNamed:[NSString stringWithFormat:@"common_icon_membership_level%d",user.mbrank]];
  192. if (!yelllowVImage) {
  193. yelllowVImage = [WBStatusHelper imageNamed:@"common_icon_membership"];
  194. }
  195. NSAttributedString *vipText = [self _attachmentWithFontSize:kWBCellNameFontSize image:yelllowVImage shrink:NO];
  196. [nameText appendString:@" "];
  197. [nameText appendAttributedString:vipText];
  198. }
  199. nameText.font = [UIFont systemFontOfSize:kWBCellNameFontSize];
  200. nameText.color = user.mbrank > 0 ? kWBCellNameOrangeColor : kWBCellNameNormalColor;
  201. nameText.lineBreakMode = NSLineBreakByCharWrapping;
  202. YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(kWBCellNameWidth, 9999)];
  203. container.maximumNumberOfRows = 1;
  204. _nameTextLayout = [YYTextLayout layoutWithContainer:container text:nameText];
  205. }
  206. /// 时间和来源
  207. - (void)_layoutSource {
  208. NSMutableAttributedString *sourceText = [NSMutableAttributedString new];
  209. NSString *createTime = [WBStatusHelper stringWithTimelineDate:_status.createdAt];
  210. // 时间
  211. if (createTime.length) {
  212. NSMutableAttributedString *timeText = [[NSMutableAttributedString alloc] initWithString:createTime];
  213. [timeText appendString:@" "];
  214. timeText.font = [UIFont systemFontOfSize:kWBCellSourceFontSize];
  215. timeText.color = kWBCellTimeNormalColor;
  216. [sourceText appendAttributedString:timeText];
  217. }
  218. // 来自 XXX
  219. if (_status.source.length) {
  220. // <a href="sinaweibo://customweibosource" rel="nofollow">iPhone 5siPhone 5s</a>
  221. static NSRegularExpression *hrefRegex, *textRegex;
  222. static dispatch_once_t onceToken;
  223. dispatch_once(&onceToken, ^{
  224. hrefRegex = [NSRegularExpression regularExpressionWithPattern:@"(?<=href=\").+(?=\" )" options:kNilOptions error:NULL];
  225. textRegex = [NSRegularExpression regularExpressionWithPattern:@"(?<=>).+(?=<)" options:kNilOptions error:NULL];
  226. });
  227. NSTextCheckingResult *hrefResult, *textResult;
  228. NSString *href = nil, *text = nil;
  229. hrefResult = [hrefRegex firstMatchInString:_status.source options:kNilOptions range:NSMakeRange(0, _status.source.length)];
  230. textResult = [textRegex firstMatchInString:_status.source options:kNilOptions range:NSMakeRange(0, _status.source.length)];
  231. if (hrefResult && textResult && hrefResult.range.location != NSNotFound && textResult.range.location != NSNotFound) {
  232. href = [_status.source substringWithRange:hrefResult.range];
  233. text = [_status.source substringWithRange:textResult.range];
  234. }
  235. if (href.length && text.length) {
  236. NSMutableAttributedString *from = [NSMutableAttributedString new];
  237. [from appendString:[NSString stringWithFormat:@"来自 %@", text]];
  238. from.font = [UIFont systemFontOfSize:kWBCellSourceFontSize];
  239. from.color = kWBCellTimeNormalColor;
  240. if (_status.sourceAllowClick > 0) {
  241. NSRange range = NSMakeRange(3, text.length);
  242. [from setColor:kWBCellTextHighlightColor range:range];
  243. YYTextBackedString *backed = [YYTextBackedString stringWithString:href];
  244. [from setTextBackedString:backed range:range];
  245. YYTextBorder *border = [YYTextBorder new];
  246. border.insets = UIEdgeInsetsMake(-2, 0, -2, 0);
  247. border.fillColor = kWBCellTextHighlightBackgroundColor;
  248. border.cornerRadius = 3;
  249. YYTextHighlight *highlight = [YYTextHighlight new];
  250. if (href) highlight.userInfo = @{kWBLinkHrefName : href};
  251. [highlight setBackgroundBorder:border];
  252. [from setTextHighlight:highlight range:range];
  253. }
  254. [sourceText appendAttributedString:from];
  255. }
  256. }
  257. if (sourceText.length == 0) {
  258. _sourceTextLayout = nil;
  259. } else {
  260. YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(kWBCellNameWidth, 9999)];
  261. container.maximumNumberOfRows = 1;
  262. _sourceTextLayout = [YYTextLayout layoutWithContainer:container text:sourceText];
  263. }
  264. }
  265. - (void)_layoutRetweet {
  266. _retweetHeight = 0;
  267. [self _layoutRetweetedText];
  268. [self _layoutRetweetPics];
  269. if (_retweetPicHeight == 0) {
  270. [self _layoutRetweetCard];
  271. }
  272. _retweetHeight = _retweetTextHeight;
  273. if (_retweetPicHeight > 0) {
  274. _retweetHeight += _retweetPicHeight;
  275. _retweetHeight += kWBCellPadding;
  276. } else if (_retweetCardHeight > 0) {
  277. _retweetHeight += _retweetCardHeight;
  278. _retweetHeight += kWBCellPadding;
  279. }
  280. }
  281. /// 文本
  282. - (void)_layoutText {
  283. _textHeight = 0;
  284. _textLayout = nil;
  285. NSMutableAttributedString *text = [self _textWithStatus:_status
  286. isRetweet:NO
  287. fontSize:kWBCellTextFontSize
  288. textColor:kWBCellTextNormalColor];
  289. if (text.length == 0) return;
  290. WBTextLinePositionModifier *modifier = [WBTextLinePositionModifier new];
  291. modifier.font = [UIFont fontWithName:@"Heiti SC" size:kWBCellTextFontSize];
  292. modifier.paddingTop = kWBCellPaddingText;
  293. modifier.paddingBottom = kWBCellPaddingText;
  294. YYTextContainer *container = [YYTextContainer new];
  295. container.size = CGSizeMake(kWBCellContentWidth, HUGE);
  296. container.linePositionModifier = modifier;
  297. _textLayout = [YYTextLayout layoutWithContainer:container text:text];
  298. if (!_textLayout) return;
  299. _textHeight = [modifier heightForLineCount:_textLayout.rowCount];
  300. }
  301. - (void)_layoutRetweetedText {
  302. _retweetHeight = 0;
  303. _retweetTextLayout = nil;
  304. NSMutableAttributedString *text = [self _textWithStatus:_status.retweetedStatus
  305. isRetweet:YES
  306. fontSize:kWBCellTextFontRetweetSize
  307. textColor:kWBCellTextSubTitleColor];
  308. if (text.length == 0) return;
  309. WBTextLinePositionModifier *modifier = [WBTextLinePositionModifier new];
  310. modifier.font = [UIFont fontWithName:@"Heiti SC" size:kWBCellTextFontRetweetSize];
  311. modifier.paddingTop = kWBCellPaddingText;
  312. modifier.paddingBottom = kWBCellPaddingText;
  313. YYTextContainer *container = [YYTextContainer new];
  314. container.size = CGSizeMake(kWBCellContentWidth, HUGE);
  315. container.linePositionModifier = modifier;
  316. _retweetTextLayout = [YYTextLayout layoutWithContainer:container text:text];
  317. if (!_retweetTextLayout) return;
  318. _retweetTextHeight = [modifier heightForLineCount:_retweetTextLayout.lines.count];
  319. }
  320. - (void)_layoutPics {
  321. [self _layoutPicsWithStatus:_status isRetweet:NO];
  322. }
  323. - (void)_layoutRetweetPics {
  324. [self _layoutPicsWithStatus:_status.retweetedStatus isRetweet:YES];
  325. }
  326. - (void)_layoutPicsWithStatus:(WBStatus *)status isRetweet:(BOOL)isRetweet {
  327. if (isRetweet) {
  328. _retweetPicSize = CGSizeZero;
  329. _retweetPicHeight = 0;
  330. } else {
  331. _picSize = CGSizeZero;
  332. _picHeight = 0;
  333. }
  334. if (status.pics.count == 0) return;
  335. CGSize picSize = CGSizeZero;
  336. CGFloat picHeight = 0;
  337. CGFloat len1_3 = (kWBCellContentWidth + kWBCellPaddingPic) / 3 - kWBCellPaddingPic;
  338. len1_3 = CGFloatPixelRound(len1_3);
  339. switch (status.pics.count) {
  340. case 1: {
  341. WBPicture *pic = _status.pics.firstObject;
  342. WBPictureMetadata *bmiddle = pic.bmiddle;
  343. if (pic.keepSize || bmiddle.width < 1 || bmiddle.height < 1) {
  344. CGFloat maxLen = kWBCellContentWidth / 2.0;
  345. maxLen = CGFloatPixelRound(maxLen);
  346. picSize = CGSizeMake(maxLen, maxLen);
  347. picHeight = maxLen;
  348. } else {
  349. CGFloat maxLen = len1_3 * 2 + kWBCellPaddingPic;
  350. if (bmiddle.width < bmiddle.height) {
  351. picSize.width = (float)bmiddle.width / (float)bmiddle.height * maxLen;
  352. picSize.height = maxLen;
  353. } else {
  354. picSize.width = maxLen;
  355. picSize.height = (float)bmiddle.height / (float)bmiddle.width * maxLen;
  356. }
  357. picSize = CGSizePixelRound(picSize);
  358. picHeight = picSize.height;
  359. }
  360. } break;
  361. case 2: case 3: {
  362. picSize = CGSizeMake(len1_3, len1_3);
  363. picHeight = len1_3;
  364. } break;
  365. case 4: case 5: case 6: {
  366. picSize = CGSizeMake(len1_3, len1_3);
  367. picHeight = len1_3 * 2 + kWBCellPaddingPic;
  368. } break;
  369. default: { // 7, 8, 9
  370. picSize = CGSizeMake(len1_3, len1_3);
  371. picHeight = len1_3 * 3 + kWBCellPaddingPic * 2;
  372. } break;
  373. }
  374. if (isRetweet) {
  375. _retweetPicSize = picSize;
  376. _retweetPicHeight = picHeight;
  377. } else {
  378. _picSize = picSize;
  379. _picHeight = picHeight;
  380. }
  381. }
  382. - (void)_layoutCard {
  383. [self _layoutCardWithStatus:_status isRetweet:NO];
  384. }
  385. - (void)_layoutRetweetCard {
  386. [self _layoutCardWithStatus:_status.retweetedStatus isRetweet:YES];
  387. }
  388. - (void)_layoutCardWithStatus:(WBStatus *)status isRetweet:(BOOL)isRetweet {
  389. if (isRetweet) {
  390. _retweetCardType = WBStatusCardTypeNone;
  391. _retweetCardHeight = 0;
  392. _retweetCardTextLayout = nil;
  393. _retweetCardTextRect = CGRectZero;
  394. } else {
  395. _cardType = WBStatusCardTypeNone;
  396. _cardHeight = 0;
  397. _cardTextLayout = nil;
  398. _cardTextRect = CGRectZero;
  399. }
  400. WBPageInfo *pageInfo = status.pageInfo;
  401. if (!pageInfo) return;
  402. WBStatusCardType cardType = WBStatusCardTypeNone;
  403. CGFloat cardHeight = 0;
  404. YYTextLayout *cardTextLayout = nil;
  405. CGRect textRect = CGRectZero;
  406. if ((pageInfo.type == 11) && [pageInfo.objectType isEqualToString:@"video"]) {
  407. // 视频,一个大图片,上面播放按钮
  408. if (pageInfo.pagePic) {
  409. cardType = WBStatusCardTypeVideo;
  410. cardHeight = (2 * kWBCellContentWidth - kWBCellPaddingPic) / 3.0;
  411. }
  412. } else {
  413. BOOL hasImage = pageInfo.pagePic != nil;
  414. BOOL hasBadge = pageInfo.typeIcon != nil;
  415. WBButtonLink *button = pageInfo.buttons.firstObject;
  416. BOOL hasButtom = button.pic && button.name;
  417. /*
  418. badge: 25,25 左上角 (42)
  419. image: 70,70 方形
  420. 100, 70 矩形
  421. btn: 60,70
  422. lineheight 20
  423. padding 10
  424. */
  425. textRect.size.height = 70;
  426. if (hasImage) {
  427. if (hasBadge) {
  428. textRect.origin.x = 100;
  429. } else {
  430. textRect.origin.x = 70;
  431. }
  432. } else {
  433. if (hasBadge) {
  434. textRect.origin.x = 42;
  435. }
  436. }
  437. textRect.origin.x += 10; //padding
  438. textRect.size.width = kWBCellContentWidth - textRect.origin.x;
  439. if (hasButtom) textRect.size.width -= 60;
  440. textRect.size.width -= 10; //padding
  441. NSMutableAttributedString *text = [NSMutableAttributedString new];
  442. if (pageInfo.pageTitle.length) {
  443. NSMutableAttributedString *title = [[NSMutableAttributedString alloc] initWithString:pageInfo.pageTitle];
  444. title.font = [UIFont systemFontOfSize:kWBCellCardTitleFontSize];
  445. title.color = kWBCellNameNormalColor;
  446. [text appendAttributedString:title];
  447. }
  448. if (pageInfo.pageDesc.length) {
  449. if (text.length) [text appendString:@"\n"];
  450. NSMutableAttributedString *desc = [[NSMutableAttributedString alloc] initWithString:pageInfo.pageDesc];
  451. desc.font = [UIFont systemFontOfSize:kWBCellCardDescFontSize];
  452. desc.color = kWBCellNameNormalColor;
  453. [text appendAttributedString:desc];
  454. } else if (pageInfo.content2.length) {
  455. if (text.length) [text appendString:@"\n"];
  456. NSMutableAttributedString *content3 = [[NSMutableAttributedString alloc] initWithString:pageInfo.content2];
  457. content3.font = [UIFont systemFontOfSize:kWBCellCardDescFontSize];
  458. content3.color = kWBCellTextSubTitleColor;
  459. [text appendAttributedString:content3];
  460. } else if (pageInfo.content3.length) {
  461. if (text.length) [text appendString:@"\n"];
  462. NSMutableAttributedString *content3 = [[NSMutableAttributedString alloc] initWithString:pageInfo.content3];
  463. content3.font = [UIFont systemFontOfSize:kWBCellCardDescFontSize];
  464. content3.color = kWBCellTextSubTitleColor;
  465. [text appendAttributedString:content3];
  466. }
  467. if (pageInfo.tips.length) {
  468. if (text.length) [text appendString:@"\n"];
  469. NSMutableAttributedString *tips = [[NSMutableAttributedString alloc] initWithString:pageInfo.tips];
  470. tips.font = [UIFont systemFontOfSize:kWBCellCardDescFontSize];
  471. tips.color = kWBCellTextSubTitleColor;
  472. [text appendAttributedString:tips];
  473. }
  474. if (text.length) {
  475. text.maximumLineHeight = 20;
  476. text.minimumLineHeight = 20;
  477. text.lineBreakMode = NSLineBreakByTruncatingTail;
  478. YYTextContainer *container = [YYTextContainer containerWithSize:textRect.size];
  479. container.maximumNumberOfRows = 3;
  480. cardTextLayout = [YYTextLayout layoutWithContainer:container text:text];
  481. }
  482. if (cardTextLayout) {
  483. cardType = WBStatusCardTypeNormal;
  484. cardHeight = 70;
  485. }
  486. }
  487. if (isRetweet) {
  488. _retweetCardType = cardType;
  489. _retweetCardHeight = cardHeight;
  490. _retweetCardTextLayout = cardTextLayout;
  491. _retweetCardTextRect = textRect;
  492. } else {
  493. _cardType = cardType;
  494. _cardHeight = cardHeight;
  495. _cardTextLayout = cardTextLayout;
  496. _cardTextRect = textRect;
  497. }
  498. }
  499. - (void)_layoutTag {
  500. _tagType = WBStatusTagTypeNone;
  501. _tagHeight = 0;
  502. WBTag *tag = _status.tagStruct.firstObject;
  503. if (tag.tagName.length == 0) return;
  504. NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:tag.tagName];
  505. if (tag.tagType == 1) {
  506. _tagType = WBStatusTagTypePlace;
  507. _tagHeight = 40;
  508. text.color = [UIColor colorWithWhite:0.217 alpha:1.000];
  509. } else {
  510. _tagType = WBStatusTagTypeNormal;
  511. _tagHeight = 32;
  512. if (tag.urlTypePic) {
  513. NSAttributedString *pic = [self _attachmentWithFontSize:kWBCellCardDescFontSize imageURL:tag.urlTypePic.absoluteString shrink:YES];
  514. [text insertAttributedString:pic atIndex:0];
  515. }
  516. // 高亮状态的背景
  517. YYTextBorder *highlightBorder = [YYTextBorder new];
  518. highlightBorder.insets = UIEdgeInsetsMake(-2, 0, -2, 0);
  519. highlightBorder.cornerRadius = 2;
  520. highlightBorder.fillColor = kWBCellTextHighlightBackgroundColor;
  521. [text setColor:kWBCellTextHighlightColor range:text.rangeOfAll];
  522. // 高亮状态
  523. YYTextHighlight *highlight = [YYTextHighlight new];
  524. [highlight setBackgroundBorder:highlightBorder];
  525. // 数据信息,用于稍后用户点击
  526. highlight.userInfo = @{kWBLinkTagName : tag};
  527. [text setTextHighlight:highlight range:text.rangeOfAll];
  528. }
  529. text.font = [UIFont systemFontOfSize:kWBCellCardDescFontSize];
  530. YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(9999, 9999)];
  531. _tagTextLayout = [YYTextLayout layoutWithContainer:container text:text];
  532. if (!_tagTextLayout) {
  533. _tagType = WBStatusTagTypeNone;
  534. _tagHeight = 0;
  535. }
  536. }
  537. - (void)_layoutToolbar {
  538. // should be localized
  539. UIFont *font = [UIFont systemFontOfSize:kWBCellToolbarFontSize];
  540. YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(kScreenWidth, kWBCellToolbarHeight)];
  541. container.maximumNumberOfRows = 1;
  542. NSMutableAttributedString *repostText = [[NSMutableAttributedString alloc] initWithString:_status.repostsCount <= 0 ? @"转发" : [WBStatusHelper shortedNumberDesc:_status.repostsCount]];
  543. repostText.font = font;
  544. repostText.color = kWBCellToolbarTitleColor;
  545. _toolbarRepostTextLayout = [YYTextLayout layoutWithContainer:container text:repostText];
  546. _toolbarRepostTextWidth = CGFloatPixelRound(_toolbarRepostTextLayout.textBoundingRect.size.width);
  547. NSMutableAttributedString *commentText = [[NSMutableAttributedString alloc] initWithString:_status.commentsCount <= 0 ? @"评论" : [WBStatusHelper shortedNumberDesc:_status.commentsCount]];
  548. commentText.font = font;
  549. commentText.color = kWBCellToolbarTitleColor;
  550. _toolbarCommentTextLayout = [YYTextLayout layoutWithContainer:container text:commentText];
  551. _toolbarCommentTextWidth = CGFloatPixelRound(_toolbarCommentTextLayout.textBoundingRect.size.width);
  552. NSMutableAttributedString *likeText = [[NSMutableAttributedString alloc] initWithString:_status.attitudesCount <= 0 ? @"赞" : [WBStatusHelper shortedNumberDesc:_status.attitudesCount]];
  553. likeText.font = font;
  554. likeText.color = _status.attitudesStatus ? kWBCellToolbarTitleHighlightColor : kWBCellToolbarTitleColor;
  555. _toolbarLikeTextLayout = [YYTextLayout layoutWithContainer:container text:likeText];
  556. _toolbarLikeTextWidth = CGFloatPixelRound(_toolbarLikeTextLayout.textBoundingRect.size.width);
  557. }
  558. - (NSMutableAttributedString *)_textWithStatus:(WBStatus *)status
  559. isRetweet:(BOOL)isRetweet
  560. fontSize:(CGFloat)fontSize
  561. textColor:(UIColor *)textColor {
  562. if (!status) return nil;
  563. NSMutableString *string = status.text.mutableCopy;
  564. if (string.length == 0) return nil;
  565. if (isRetweet) {
  566. NSString *name = status.user.name;
  567. if (name.length == 0) {
  568. name = status.user.screenName;
  569. }
  570. if (name) {
  571. NSString *insert = [NSString stringWithFormat:@"@%@:",name];
  572. [string insertString:insert atIndex:0];
  573. }
  574. }
  575. // 字体
  576. UIFont *font = [UIFont systemFontOfSize:fontSize];
  577. // 高亮状态的背景
  578. YYTextBorder *highlightBorder = [YYTextBorder new];
  579. highlightBorder.insets = UIEdgeInsetsMake(-2, 0, -2, 0);
  580. highlightBorder.cornerRadius = 3;
  581. highlightBorder.fillColor = kWBCellTextHighlightBackgroundColor;
  582. NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:string];
  583. text.font = font;
  584. text.color = textColor;
  585. // 根据 urlStruct 中每个 URL.shortURL 来匹配文本,将其替换为图标+友好描述
  586. for (WBURL *wburl in status.urlStruct) {
  587. if (wburl.shortURL.length == 0) continue;
  588. if (wburl.urlTitle.length == 0) continue;
  589. NSString *urlTitle = wburl.urlTitle;
  590. if (urlTitle.length > 27) {
  591. urlTitle = [[urlTitle substringToIndex:27] stringByAppendingString:YYTextTruncationToken];
  592. }
  593. NSRange searchRange = NSMakeRange(0, text.string.length);
  594. do {
  595. NSRange range = [text.string rangeOfString:wburl.shortURL options:kNilOptions range:searchRange];
  596. if (range.location == NSNotFound) break;
  597. if (range.location + range.length == text.length) {
  598. if (status.pageInfo.pageID && wburl.pageID &&
  599. [wburl.pageID isEqualToString:status.pageInfo.pageID]) {
  600. if ((!isRetweet && !status.retweetedStatus) || isRetweet) {
  601. if (status.pics.count == 0) {
  602. [text replaceCharactersInRange:range withString:@""];
  603. break; // cut the tail, show with card
  604. }
  605. }
  606. }
  607. }
  608. if ([text attribute:YYTextHighlightAttributeName atIndex:range.location] == nil) {
  609. // 替换的内容
  610. NSMutableAttributedString *replace = [[NSMutableAttributedString alloc] initWithString:urlTitle];
  611. if (wburl.urlTypePic.length) {
  612. // 链接头部有个图片附件 (要从网络获取)
  613. NSURL *picURL = [WBStatusHelper defaultURLForImageURL:wburl.urlTypePic];
  614. UIImage *image = [[YYImageCache sharedCache] getImageForKey:picURL.absoluteString];
  615. NSAttributedString *pic = (image && !wburl.pics.count) ? [self _attachmentWithFontSize:fontSize image:image shrink:YES] : [self _attachmentWithFontSize:fontSize imageURL:wburl.urlTypePic shrink:YES];
  616. [replace insertAttributedString:pic atIndex:0];
  617. }
  618. replace.font = font;
  619. replace.color = kWBCellTextHighlightColor;
  620. // 高亮状态
  621. YYTextHighlight *highlight = [YYTextHighlight new];
  622. [highlight setBackgroundBorder:highlightBorder];
  623. // 数据信息,用于稍后用户点击
  624. highlight.userInfo = @{kWBLinkURLName : wburl};
  625. [replace setTextHighlight:highlight range:NSMakeRange(0, replace.length)];
  626. // 添加被替换的原始字符串,用于复制
  627. YYTextBackedString *backed = [YYTextBackedString stringWithString:[text.string substringWithRange:range]];
  628. [replace setTextBackedString:backed range:NSMakeRange(0, replace.length)];
  629. // 替换
  630. [text replaceCharactersInRange:range withAttributedString:replace];
  631. searchRange.location = searchRange.location + (replace.length ? replace.length : 1);
  632. if (searchRange.location + 1 >= text.length) break;
  633. searchRange.length = text.length - searchRange.location;
  634. } else {
  635. searchRange.location = searchRange.location + (searchRange.length ? searchRange.length : 1);
  636. if (searchRange.location + 1>= text.length) break;
  637. searchRange.length = text.length - searchRange.location;
  638. }
  639. } while (1);
  640. }
  641. // 根据 topicStruct 中每个 Topic.topicTitle 来匹配文本,标记为话题
  642. for (WBTopic *topic in status.topicStruct) {
  643. if (topic.topicTitle.length == 0) continue;
  644. NSString *topicTitle = [NSString stringWithFormat:@"#%@#",topic.topicTitle];
  645. NSRange searchRange = NSMakeRange(0, text.string.length);
  646. do {
  647. NSRange range = [text.string rangeOfString:topicTitle options:kNilOptions range:searchRange];
  648. if (range.location == NSNotFound) break;
  649. if ([text attribute:YYTextHighlightAttributeName atIndex:range.location] == nil) {
  650. [text setColor:kWBCellTextHighlightColor range:range];
  651. // 高亮状态
  652. YYTextHighlight *highlight = [YYTextHighlight new];
  653. [highlight setBackgroundBorder:highlightBorder];
  654. // 数据信息,用于稍后用户点击
  655. highlight.userInfo = @{kWBLinkTopicName : topic};
  656. [text setTextHighlight:highlight range:range];
  657. }
  658. searchRange.location = searchRange.location + (searchRange.length ? searchRange.length : 1);
  659. if (searchRange.location + 1>= text.length) break;
  660. searchRange.length = text.length - searchRange.location;
  661. } while (1);
  662. }
  663. // 匹配 @用户名
  664. NSArray *atResults = [[WBStatusHelper regexAt] matchesInString:text.string options:kNilOptions range:text.rangeOfAll];
  665. for (NSTextCheckingResult *at in atResults) {
  666. if (at.range.location == NSNotFound && at.range.length <= 1) continue;
  667. if ([text attribute:YYTextHighlightAttributeName atIndex:at.range.location] == nil) {
  668. [text setColor:kWBCellTextHighlightColor range:at.range];
  669. // 高亮状态
  670. YYTextHighlight *highlight = [YYTextHighlight new];
  671. [highlight setBackgroundBorder:highlightBorder];
  672. // 数据信息,用于稍后用户点击
  673. highlight.userInfo = @{kWBLinkAtName : [text.string substringWithRange:NSMakeRange(at.range.location + 1, at.range.length - 1)]};
  674. [text setTextHighlight:highlight range:at.range];
  675. }
  676. }
  677. // 匹配 [表情]
  678. NSArray<NSTextCheckingResult *> *emoticonResults = [[WBStatusHelper regexEmoticon] matchesInString:text.string options:kNilOptions range:text.rangeOfAll];
  679. NSUInteger emoClipLength = 0;
  680. for (NSTextCheckingResult *emo in emoticonResults) {
  681. if (emo.range.location == NSNotFound && emo.range.length <= 1) continue;
  682. NSRange range = emo.range;
  683. range.location -= emoClipLength;
  684. if ([text attribute:YYTextHighlightAttributeName atIndex:range.location]) continue;
  685. if ([text attribute:YYTextAttachmentAttributeName atIndex:range.location]) continue;
  686. NSString *emoString = [text.string substringWithRange:range];
  687. NSString *imagePath = [WBStatusHelper emoticonDic][emoString];
  688. UIImage *image = [WBStatusHelper imageWithPath:imagePath];
  689. if (!image) continue;
  690. NSAttributedString *emoText = [NSAttributedString attachmentStringWithEmojiImage:image fontSize:fontSize];
  691. [text replaceCharactersInRange:range withAttributedString:emoText];
  692. emoClipLength += range.length - 1;
  693. }
  694. return text;
  695. }
  696. - (NSAttributedString *)_attachmentWithFontSize:(CGFloat)fontSize image:(UIImage *)image shrink:(BOOL)shrink {
  697. // CGFloat ascent = YYEmojiGetAscentWithFontSize(fontSize);
  698. // CGFloat descent = YYEmojiGetDescentWithFontSize(fontSize);
  699. // CGRect bounding = YYEmojiGetGlyphBoundingRectWithFontSize(fontSize);
  700. // Heiti SC 字体。。
  701. CGFloat ascent = fontSize * 0.86;
  702. CGFloat descent = fontSize * 0.14;
  703. CGRect bounding = CGRectMake(0, -0.14 * fontSize, fontSize, fontSize);
  704. UIEdgeInsets contentInsets = UIEdgeInsetsMake(ascent - (bounding.size.height + bounding.origin.y), 0, descent + bounding.origin.y, 0);
  705. YYTextRunDelegate *delegate = [YYTextRunDelegate new];
  706. delegate.ascent = ascent;
  707. delegate.descent = descent;
  708. delegate.width = bounding.size.width;
  709. YYTextAttachment *attachment = [YYTextAttachment new];
  710. attachment.contentMode = UIViewContentModeScaleAspectFit;
  711. attachment.contentInsets = contentInsets;
  712. attachment.content = image;
  713. if (shrink) {
  714. // 缩小~
  715. CGFloat scale = 1 / 10.0;
  716. contentInsets.top += fontSize * scale;
  717. contentInsets.bottom += fontSize * scale;
  718. contentInsets.left += fontSize * scale;
  719. contentInsets.right += fontSize * scale;
  720. contentInsets = UIEdgeInsetPixelFloor(contentInsets);
  721. attachment.contentInsets = contentInsets;
  722. }
  723. NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:YYTextAttachmentToken];
  724. [atr setTextAttachment:attachment range:NSMakeRange(0, atr.length)];
  725. CTRunDelegateRef ctDelegate = delegate.CTRunDelegate;
  726. [atr setRunDelegate:ctDelegate range:NSMakeRange(0, atr.length)];
  727. if (ctDelegate) CFRelease(ctDelegate);
  728. return atr;
  729. }
  730. - (NSAttributedString *)_attachmentWithFontSize:(CGFloat)fontSize imageURL:(NSString *)imageURL shrink:(BOOL)shrink {
  731. /*
  732. 微博 URL 嵌入的图片,比临近的字体要小一圈。。
  733. 这里模拟一下 Heiti SC 字体,然后把图片缩小一下。
  734. */
  735. CGFloat ascent = fontSize * 0.86;
  736. CGFloat descent = fontSize * 0.14;
  737. CGRect bounding = CGRectMake(0, -0.14 * fontSize, fontSize, fontSize);
  738. UIEdgeInsets contentInsets = UIEdgeInsetsMake(ascent - (bounding.size.height + bounding.origin.y), 0, descent + bounding.origin.y, 0);
  739. CGSize size = CGSizeMake(fontSize, fontSize);
  740. if (shrink) {
  741. // 缩小~
  742. CGFloat scale = 1 / 10.0;
  743. contentInsets.top += fontSize * scale;
  744. contentInsets.bottom += fontSize * scale;
  745. contentInsets.left += fontSize * scale;
  746. contentInsets.right += fontSize * scale;
  747. contentInsets = UIEdgeInsetPixelFloor(contentInsets);
  748. size = CGSizeMake(fontSize - fontSize * scale * 2, fontSize - fontSize * scale * 2);
  749. size = CGSizePixelRound(size);
  750. }
  751. YYTextRunDelegate *delegate = [YYTextRunDelegate new];
  752. delegate.ascent = ascent;
  753. delegate.descent = descent;
  754. delegate.width = bounding.size.width;
  755. WBTextImageViewAttachment *attachment = [WBTextImageViewAttachment new];
  756. attachment.contentMode = UIViewContentModeScaleAspectFit;
  757. attachment.contentInsets = contentInsets;
  758. attachment.size = size;
  759. attachment.imageURL = [WBStatusHelper defaultURLForImageURL:imageURL];
  760. NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:YYTextAttachmentToken];
  761. [atr setTextAttachment:attachment range:NSMakeRange(0, atr.length)];
  762. CTRunDelegateRef ctDelegate = delegate.CTRunDelegate;
  763. [atr setRunDelegate:ctDelegate range:NSMakeRange(0, atr.length)];
  764. if (ctDelegate) CFRelease(ctDelegate);
  765. return atr;
  766. }
  767. - (WBTextLinePositionModifier *)_textlineModifier {
  768. static WBTextLinePositionModifier *mod;
  769. static dispatch_once_t onceToken;
  770. dispatch_once(&onceToken, ^{
  771. mod = [WBTextLinePositionModifier new];
  772. mod.font = [UIFont fontWithName:@"Heiti SC" size:kWBCellTextFontSize];
  773. mod.paddingTop = 10;
  774. mod.paddingBottom = 10;
  775. });
  776. return mod;
  777. }
  778. @end