WBStatusLayout.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  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. #import "WBUserInfoModel.h"
  10. /*
  11. 将每行的 baseline 位置固定下来,不受不同字体的 ascent/descent 影响。
  12. 注意,Heiti SC 中, ascent + descent = font size,
  13. 但是在 PingFang SC 中,ascent + descent > font size。
  14. 所以这里统一用 Heiti SC (0.86 ascent, 0.14 descent) 作为顶部和底部标准,保证不同系统下的显示一致性。
  15. 间距仍然用字体默认
  16. */
  17. @implementation WBTextLinePositionModifier
  18. - (instancetype)init {
  19. self = [super init];
  20. if (kiOS9Later) {
  21. _lineHeightMultiple = 1.34; // for PingFang SC
  22. } else {
  23. _lineHeightMultiple = 1.3125; // for Heiti SC
  24. }
  25. return self;
  26. }
  27. - (void)modifyLines:(NSArray *)lines fromText:(NSAttributedString *)text inContainer:(YYTextContainer *)container {
  28. //CGFloat ascent = _font.ascender;
  29. CGFloat ascent = _font.pointSize * 0.86;
  30. CGFloat lineHeight = _font.pointSize * _lineHeightMultiple;
  31. for (YYTextLine *line in lines) {
  32. CGPoint position = line.position;
  33. position.y = _paddingTop + ascent + line.row * lineHeight;
  34. line.position = position;
  35. }
  36. }
  37. - (id)copyWithZone:(NSZone *)zone {
  38. WBTextLinePositionModifier *one = [self.class new];
  39. one->_font = _font;
  40. one->_paddingTop = _paddingTop;
  41. one->_paddingBottom = _paddingBottom;
  42. one->_lineHeightMultiple = _lineHeightMultiple;
  43. return one;
  44. }
  45. - (CGFloat)heightForLineCount:(NSUInteger)lineCount {
  46. if (lineCount == 0) return 0;
  47. // CGFloat ascent = _font.ascender;
  48. // CGFloat descent = -_font.descender;
  49. CGFloat ascent = _font.pointSize * 0.86;
  50. CGFloat descent = _font.pointSize * 0.14;
  51. CGFloat lineHeight = _font.pointSize * _lineHeightMultiple;
  52. return _paddingTop + _paddingBottom + ascent + descent + (lineCount - 1) * lineHeight;
  53. }
  54. @end
  55. /**
  56. 微博的文本中,某些嵌入的图片需要从网上下载,这里简单做个封装
  57. */
  58. @interface WBTextImageViewAttachment : YYTextAttachment
  59. @property (nonatomic, strong) NSURL *imageURL;
  60. @property (nonatomic, assign) CGSize size;
  61. @end
  62. @implementation WBTextImageViewAttachment {
  63. UIImageView *_imageView;
  64. }
  65. - (void)setContent:(id)content {
  66. _imageView = content;
  67. }
  68. - (id)content {
  69. /// UIImageView 只能在主线程访问
  70. if (pthread_main_np() == 0) return nil;
  71. if (_imageView) return _imageView;
  72. /// 第一次获取时 (应该是在文本渲染完成,需要添加附件视图时),初始化图片视图,并下载图片
  73. /// 这里改成 YYAnimatedImageView 就能支持 GIF/APNG/WebP 动画了
  74. _imageView = [UIImageView new];
  75. _imageView.size = _size;
  76. [_imageView setImageWithURL:_imageURL placeholder:nil];
  77. return _imageView;
  78. }
  79. @end
  80. @implementation WBStatusLayout
  81. - (instancetype)initWithStatus:(WBModel *)status style:(WBLayoutStyle)style {
  82. if (!status) return nil;
  83. self = [super init];
  84. _status = status;
  85. _style = style;
  86. [self layout];
  87. return self;
  88. }
  89. - (void)layout {
  90. [self _layout];
  91. }
  92. - (void)updateDate {
  93. [self _layoutTime];
  94. }
  95. - (void)_layout {
  96. _marginTop = 0;
  97. _titleHeight = 0;
  98. _profileHeight = 0;
  99. _textHeight = 0;
  100. _retweetHeight = 0;
  101. _retweetTextHeight = 0;
  102. _retweetPicHeight = 0;
  103. _retweetCardHeight = 0;
  104. _picHeight = 0;
  105. _cardHeight = 0;
  106. _toolbarHeight = kWBCellToolbarHeight;
  107. _marginBottom = -5;
  108. // 文本排版,计算布局
  109. [self _layoutTitle];
  110. [self _layoutProfile];
  111. [self _layoutRetweet];
  112. [self _layoutPics];
  113. [self _layoutCard];
  114. [self _layoutLocation];
  115. [self _layoutText];
  116. [self _layoutTag];
  117. [self _layoutToolbar];
  118. // 计算高度
  119. _height = 0;
  120. _height += _marginTop;
  121. _height += _titleHeight;
  122. _height += _profileHeight;
  123. _height += _textHeight;
  124. _height += self.status.audio.length ? 50 : 0;
  125. if (_retweetHeight > 0) {
  126. _height += _retweetHeight;
  127. } else if (_picHeight > 0) {
  128. _height += _picHeight;
  129. }
  130. // else
  131. if (_cardHeight > 0) {
  132. _height += _cardHeight;
  133. _height -= _picHeight;
  134. }
  135. if (_tagHeight > 0) {
  136. _height += _tagHeight;
  137. } else {
  138. if (_picHeight > 0 || _cardHeight > 0) {
  139. _height += kWBCellPadding;
  140. }
  141. }
  142. if (_locationHeight) {
  143. _height += _locationHeight + kWBCellPadding;
  144. }
  145. _height += _toolbarHeight;
  146. _height += 30;//时间
  147. _height += _marginBottom;
  148. }
  149. - (void)_layoutTitle {
  150. _titleHeight = 0;
  151. _titleTextLayout = nil;
  152. }
  153. - (void)_layoutProfile {
  154. [self _layoutName];
  155. [self _layoutTime];
  156. _profileHeight = kWBCellProfileHeight;
  157. }
  158. /// 名字
  159. - (void)_layoutName {
  160. // WBUserInfoModel *user = _status.userInfo;
  161. NSString *nameStr = _status.nick_name;
  162. if (_status.no_name.intValue == 1 && ![_status.uid isEqualToString:[BGIMLoginManager sharedInstance].loginParam.identifier]) {
  163. nameStr = ASLocalizedString(@"匿名用户");
  164. }
  165. // user.user_nickname;
  166. if (nameStr.length == 0) {
  167. _nameTextLayout = nil;
  168. return;
  169. }
  170. NSMutableAttributedString *nameText = [[NSMutableAttributedString alloc] initWithString:nameStr];
  171. nameText.font =[UIFont systemFontOfSize:14 weight:UIFontWeightMedium];// [UIFont systemFontOfSize:kWBCellNameFontSize];
  172. nameText.color = kWBCellNameNormalColor;
  173. nameText.lineBreakMode = NSLineBreakByCharWrapping;
  174. YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(kWBCellNameWidth, 9999)];
  175. container.maximumNumberOfRows = 1;
  176. _nameTextLayout = [YYTextLayout layoutWithContainer:container text:nameText];
  177. }
  178. /// 时间和来源
  179. - (void)_layoutTime {
  180. NSMutableAttributedString *sourceText = [NSMutableAttributedString new];
  181. NSString *createTime = [WBStatusHelper stringWithTimelineDate:[NSDate dateWithTimeIntervalSince1970:_status.addtime.integerValue]];
  182. // 时间
  183. if (createTime.length) {
  184. NSMutableAttributedString *timeText = [[NSMutableAttributedString alloc] initWithString:createTime];
  185. [timeText appendString:@" "];
  186. timeText.font = [UIFont systemFontOfSize:11];//13
  187. timeText.color = [UIColor colorWithHexString:@"#AAAAAA"];//@"#666666"
  188. [sourceText appendAttributedString:timeText];
  189. }
  190. if (sourceText.length == 0) {
  191. _sourceTextLayout = nil;
  192. } else {
  193. YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(kWBCellNameWidth, 9999)];
  194. container.maximumNumberOfRows = 1;
  195. _sourceTextLayout = [YYTextLayout layoutWithContainer:container text:sourceText];
  196. }
  197. }
  198. - (void)_layoutRetweet {
  199. _retweetHeight = 0;
  200. }
  201. /// 文本
  202. - (void)_layoutText {
  203. _textHeight = 0;
  204. _textLayout = nil;
  205. NSMutableAttributedString *text = [self _textWithStatus:_status
  206. isRetweet:NO
  207. fontSize: 13
  208. textColor:kWBCellTextNormalColor];//;kWBCellTextFontSize
  209. if (text.length == 0) return;
  210. WBTextLinePositionModifier *modifier = [WBTextLinePositionModifier new];
  211. modifier.font =[UIFont systemFontOfSize:13];// [UIFont fontWithName:@"Heiti SC" size:kWBCellTextFontSize];
  212. modifier.paddingTop = kWBCellPaddingText;
  213. modifier.paddingBottom = kWBCellPaddingText;
  214. YYTextContainer *container = [YYTextContainer new];
  215. container.size = CGSizeMake(kWBCellContentWidth, HUGE);
  216. container.linePositionModifier = modifier;
  217. _textLayout = [YYTextLayout layoutWithContainer:container text:text];
  218. if (!_textLayout) return;
  219. _textHeight = [modifier heightForLineCount:_textLayout.rowCount];
  220. }
  221. - (void)_layoutRetweetedText {
  222. _retweetHeight = 0;
  223. _retweetTextLayout = nil;
  224. }
  225. - (void)_layoutPics {
  226. [self _layoutPicsWithStatus:_status isRetweet:NO];
  227. }
  228. - (void)_layoutRetweetPics {
  229. }
  230. - (void)_layoutPicsWithStatus:(WBModel *)status isRetweet:(BOOL)isRetweet {
  231. NSArray *pics = status.picUrls;
  232. if (pics.count == 0) return;
  233. CGSize picSize = CGSizeZero;
  234. CGFloat picHeight = 0;
  235. CGFloat len1_3 = (kWBCellContentWidth + kWBCellPaddingPic) / 3 - kWBCellPaddingPic;
  236. len1_3 = CGFloatPixelRound(len1_3);
  237. switch (pics.count) {
  238. case 1: {
  239. picSize = CGSizeMake(len1_3, len1_3);
  240. picHeight = len1_3;
  241. } break;
  242. case 2: case 3: {
  243. picSize = CGSizeMake(len1_3, len1_3);
  244. picHeight = len1_3;
  245. } break;
  246. case 4: case 5: case 6: {
  247. picSize = CGSizeMake(len1_3, len1_3);
  248. picHeight = len1_3 * 2 + kWBCellPaddingPic;
  249. } break;
  250. default: { // 7, 8, 9
  251. picSize = CGSizeMake(len1_3, len1_3);
  252. picHeight = len1_3 * 3 + kWBCellPaddingPic * 2;
  253. } break;
  254. }
  255. _picSize = picSize;
  256. _picHeight = picHeight;
  257. NSLog(@"_picHeight:%f",_picHeight);
  258. }
  259. - (void)_layoutCard {
  260. [self _layoutCardWithStatus:_status isRetweet:NO];
  261. }
  262. - (void)_layoutLocation {
  263. _locationHeight = 0;
  264. // if (_status.address.length) {
  265. // _locationHeight = 30;
  266. // }
  267. }
  268. - (void)_layoutRetweetCard {
  269. }
  270. - (void)_layoutCardWithStatus:(WBModel *)status isRetweet:(BOOL)isRetweet {
  271. _cardType = WBStatusCardTypeNone;
  272. _cardHeight = 0;
  273. _cardTextLayout = nil;
  274. _cardTextRect = CGRectZero;
  275. WBStatusCardType cardType = WBStatusCardTypeNone;
  276. CGFloat cardHeight = 0;
  277. YYTextLayout *cardTextLayout = nil;
  278. CGRect textRect = CGRectZero;
  279. if (StrValid(status.video)) {
  280. // 视频,一个大图片,上面播放按钮
  281. cardType = WBStatusCardTypeVideo;
  282. CGFloat len1_3 = (kWBCellContentWidth + kWBCellPaddingPic) / 3 - kWBCellPaddingPic;
  283. len1_3 = CGFloatPixelRound(len1_3);
  284. cardHeight = 184;// kScreenW * 16 / 18;
  285. // 135 184
  286. }
  287. _cardType = cardType;
  288. _cardHeight = cardHeight;
  289. _cardTextLayout = cardTextLayout;
  290. _cardTextRect = textRect;
  291. }
  292. - (void)_layoutTag {
  293. _tagType = WBStatusTagTypeNone;
  294. _tagHeight = 0;
  295. }
  296. - (void)_layoutToolbar {
  297. // should be localized
  298. }
  299. - (NSMutableAttributedString *)_textWithStatus:(WBModel *)status
  300. isRetweet:(BOOL)isRetweet
  301. fontSize:(CGFloat)fontSize
  302. textColor:(UIColor *)textColor {
  303. if (!status) return nil;
  304. NSMutableString *string;
  305. if (status.theme.length) {
  306. string = [NSString stringWithFormat:@"#%@#%@",status.theme,status.content].mutableCopy;
  307. }else{
  308. string = status.content.mutableCopy;
  309. }
  310. if (string.length == 0) return nil;
  311. if (isRetweet) {
  312. }
  313. // 字体
  314. UIFont *font = [UIFont systemFontOfSize:fontSize];
  315. // 高亮状态的背景
  316. YYTextBorder *highlightBorder = [YYTextBorder new];
  317. highlightBorder.insets = UIEdgeInsetsMake(-2, 0, -2, 0);
  318. highlightBorder.cornerRadius = 3;
  319. highlightBorder.fillColor = kWBCellTextHighlightBackgroundColor;
  320. NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:string];
  321. text.font = font;
  322. text.color = textColor;
  323. // 根据 topicStruct 中每个 Topic.topicTitle 来匹配文本,标记为话题
  324. if (status.theme.length) {
  325. NSString *topicTitle = [NSString stringWithFormat:@"#%@#",status.theme];
  326. NSRange searchRange = NSMakeRange(0, text.string.length);
  327. do {
  328. NSRange range = [text.string rangeOfString:topicTitle options:kNilOptions range:searchRange];
  329. if (range.location == NSNotFound) break;
  330. if ([text attribute:YYTextHighlightAttributeName atIndex:range.location] == nil) {
  331. [text setColor:kWBCellTextHighlightColor range:range];
  332. // 高亮状态
  333. YYTextHighlight *highlight = [YYTextHighlight new];
  334. [highlight setBackgroundBorder:highlightBorder];
  335. // 数据信息,用于稍后用户点击
  336. highlight.userInfo = @{kWBLinkTopicName : status.theme};
  337. [text setTextHighlight:highlight range:range];
  338. }
  339. searchRange.location = searchRange.location + (searchRange.length ? searchRange.length : 1);
  340. if (searchRange.location + 1>= text.length) break;
  341. searchRange.length = text.length - searchRange.location;
  342. } while (1);
  343. }
  344. return text;
  345. }
  346. - (NSAttributedString *)_attachmentWithFontSize:(CGFloat)fontSize image:(UIImage *)image shrink:(BOOL)shrink {
  347. // CGFloat ascent = YYEmojiGetAscentWithFontSize(fontSize);
  348. // CGFloat descent = YYEmojiGetDescentWithFontSize(fontSize);
  349. // CGRect bounding = YYEmojiGetGlyphBoundingRectWithFontSize(fontSize);
  350. // Heiti SC 字体。。
  351. CGFloat ascent = fontSize * 0.86;
  352. CGFloat descent = fontSize * 0.14;
  353. CGRect bounding = CGRectMake(0, -0.14 * fontSize, fontSize, fontSize);
  354. UIEdgeInsets contentInsets = UIEdgeInsetsMake(ascent - (bounding.size.height + bounding.origin.y), 0, descent + bounding.origin.y, 0);
  355. YYTextRunDelegate *delegate = [YYTextRunDelegate new];
  356. delegate.ascent = ascent;
  357. delegate.descent = descent;
  358. delegate.width = bounding.size.width;
  359. YYTextAttachment *attachment = [YYTextAttachment new];
  360. attachment.contentMode = UIViewContentModeScaleAspectFit;
  361. attachment.contentInsets = contentInsets;
  362. attachment.content = image;
  363. if (shrink) {
  364. // 缩小~
  365. CGFloat scale = 1 / 10.0;
  366. contentInsets.top += fontSize * scale;
  367. contentInsets.bottom += fontSize * scale;
  368. contentInsets.left += fontSize * scale;
  369. contentInsets.right += fontSize * scale;
  370. contentInsets = UIEdgeInsetPixelFloor(contentInsets);
  371. attachment.contentInsets = contentInsets;
  372. }
  373. NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:YYTextAttachmentToken];
  374. [atr setTextAttachment:attachment range:NSMakeRange(0, atr.length)];
  375. CTRunDelegateRef ctDelegate = delegate.CTRunDelegate;
  376. [atr setRunDelegate:ctDelegate range:NSMakeRange(0, atr.length)];
  377. if (ctDelegate) CFRelease(ctDelegate);
  378. return atr;
  379. }
  380. - (NSAttributedString *)_attachmentWithFontSize:(CGFloat)fontSize imageURL:(NSString *)imageURL shrink:(BOOL)shrink {
  381. /*
  382. 微博 URL 嵌入的图片,比临近的字体要小一圈。。
  383. 这里模拟一下 Heiti SC 字体,然后把图片缩小一下。
  384. */
  385. CGFloat ascent = fontSize * 0.86;
  386. CGFloat descent = fontSize * 0.14;
  387. CGRect bounding = CGRectMake(0, -0.14 * fontSize, fontSize, fontSize);
  388. UIEdgeInsets contentInsets = UIEdgeInsetsMake(ascent - (bounding.size.height + bounding.origin.y), 0, descent + bounding.origin.y, 0);
  389. CGSize size = CGSizeMake(fontSize, fontSize);
  390. if (shrink) {
  391. // 缩小~
  392. CGFloat scale = 1 / 10.0;
  393. contentInsets.top += fontSize * scale;
  394. contentInsets.bottom += fontSize * scale;
  395. contentInsets.left += fontSize * scale;
  396. contentInsets.right += fontSize * scale;
  397. contentInsets = UIEdgeInsetPixelFloor(contentInsets);
  398. size = CGSizeMake(fontSize - fontSize * scale * 2, fontSize - fontSize * scale * 2);
  399. size = CGSizePixelRound(size);
  400. }
  401. YYTextRunDelegate *delegate = [YYTextRunDelegate new];
  402. delegate.ascent = ascent;
  403. delegate.descent = descent;
  404. delegate.width = bounding.size.width;
  405. WBTextImageViewAttachment *attachment = [WBTextImageViewAttachment new];
  406. attachment.contentMode = UIViewContentModeScaleAspectFit;
  407. attachment.contentInsets = contentInsets;
  408. attachment.size = size;
  409. attachment.imageURL = [WBStatusHelper defaultURLForImageURL:imageURL];
  410. NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:YYTextAttachmentToken];
  411. [atr setTextAttachment:attachment range:NSMakeRange(0, atr.length)];
  412. CTRunDelegateRef ctDelegate = delegate.CTRunDelegate;
  413. [atr setRunDelegate:ctDelegate range:NSMakeRange(0, atr.length)];
  414. if (ctDelegate) CFRelease(ctDelegate);
  415. return atr;
  416. }
  417. - (WBTextLinePositionModifier *)_textlineModifier {
  418. static WBTextLinePositionModifier *mod;
  419. static dispatch_once_t onceToken;
  420. dispatch_once(&onceToken, ^{
  421. mod = [WBTextLinePositionModifier new];
  422. mod.font = [UIFont fontWithName:@"Heiti SC" size:kWBCellTextFontSize];
  423. mod.paddingTop = 10;
  424. mod.paddingBottom = 10;
  425. });
  426. return mod;
  427. }
  428. @end