TUIVideoMessageCell.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. //
  2. // TUIVideoMessageCell.m
  3. // UIKit
  4. //
  5. // Created by annidyfeng on 2019/5/30.
  6. // Copyright © 2023 Tencent. All rights reserved.
  7. //
  8. #import "TUIVideoMessageCell.h"
  9. #import <TIMCommon/TIMDefine.h>
  10. #import "TUICircleLodingView.h"
  11. #import "TUIMessageProgressManager.h"
  12. @interface TUIVideoMessageCell () <TUIMessageProgressManagerDelegate>
  13. @property(nonatomic, strong) UIView *animateHighlightView;
  14. @property(nonatomic, strong) TUICircleLodingView *animateCircleView;
  15. @property(nonatomic, strong) UIImageView *downloadImage;
  16. @end
  17. @implementation TUIVideoMessageCell
  18. - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
  19. self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
  20. if (self) {
  21. _thumb = [[UIImageView alloc] init];
  22. _thumb.layer.cornerRadius = 5.0;
  23. [_thumb.layer setMasksToBounds:YES];
  24. _thumb.contentMode = UIViewContentModeScaleAspectFill;
  25. _thumb.backgroundColor = [UIColor clearColor];
  26. [self.bubbleView addSubview:_thumb];
  27. _thumb.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  28. CGSize playSize = TVideoMessageCell_Play_Size;
  29. _play = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, playSize.width, playSize.height)];
  30. _play.contentMode = UIViewContentModeScaleAspectFit;
  31. _play.image = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"play_normal")];
  32. _play.hidden = YES;
  33. [_thumb addSubview:_play];
  34. _downloadImage = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, playSize.width, playSize.height)];
  35. _downloadImage.contentMode = UIViewContentModeScaleAspectFit;
  36. _downloadImage.image = [[TUIImageCache sharedInstance] getResourceFromCache:TUIChatImagePath(@"download")];
  37. _downloadImage.hidden = YES;
  38. [_thumb addSubview:_downloadImage];
  39. _duration = [[UILabel alloc] init];
  40. _duration.textColor = [UIColor whiteColor];
  41. _duration.font = [UIFont systemFontOfSize:12];
  42. [_thumb addSubview:_duration];
  43. _animateCircleView = [[TUICircleLodingView alloc] initWithFrame:CGRectMake(0, 0, kScale390(40), kScale390(40))];
  44. _animateCircleView.progress = 0;
  45. [_thumb addSubview:_animateCircleView];
  46. _progress = [[UILabel alloc] init];
  47. _progress.textColor = [UIColor whiteColor];
  48. _progress.font = [UIFont systemFontOfSize:15];
  49. _progress.textAlignment = NSTextAlignmentCenter;
  50. _progress.layer.cornerRadius = 5.0;
  51. _progress.hidden = YES;
  52. _progress.backgroundColor = TVideoMessageCell_Progress_Color;
  53. [_progress.layer setMasksToBounds:YES];
  54. [self.container addSubview:_progress];
  55. _progress.mm_fill();
  56. _progress.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  57. [TUIMessageProgressManager.shareManager addDelegate:self];
  58. }
  59. return self;
  60. }
  61. - (void)fillWithData:(TUIVideoMessageCellData *)data;
  62. {
  63. // set data
  64. [super fillWithData:data];
  65. self.videoData = data;
  66. _thumb.image = nil;
  67. BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
  68. if (hasRiskContent) {
  69. self.thumb.image = TIMCommonBundleThemeImage(@"", @"icon_security_strike");
  70. self.securityStrikeView.textLabel.text = TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrikeImage);
  71. self.duration.text = @"";
  72. self.play.hidden = YES;
  73. self.downloadImage.hidden = YES;
  74. self.indicator.hidden = YES;
  75. self.animateCircleView.hidden = YES;
  76. return;
  77. }
  78. if (data.thumbImage == nil) {
  79. [data downloadThumb];
  80. }
  81. if (data.isPlaceHolderCellData) {
  82. //show placeHolder
  83. _thumb.backgroundColor = [UIColor grayColor];
  84. _animateCircleView.progress = (data.videoTranscodingProgress *100);
  85. self.duration.text = @"";
  86. self.play.hidden = YES;
  87. self.downloadImage.hidden = YES;
  88. self.indicator.hidden = YES;
  89. self.animateCircleView.hidden = NO;
  90. @weakify(self);
  91. [[RACObserve(data, videoTranscodingProgress) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(NSNumber *x) {
  92. // The transcoded animation can display up to 30% at maximum,
  93. // and the upload progress increases from 30% to 100%.
  94. @strongify(self);
  95. double progress = [x doubleValue];
  96. double factor = 0.3;
  97. double resultProgress = (progress *100) * factor;
  98. self.animateCircleView.progress = resultProgress;
  99. }];
  100. if (data.thumbImage) {
  101. self.thumb.image = data.thumbImage;
  102. }
  103. // tell constraints they need updating
  104. [self setNeedsUpdateConstraints];
  105. // update constraints now so we can animate the change
  106. [self updateConstraintsIfNeeded];
  107. [self layoutIfNeeded];
  108. return;
  109. }
  110. @weakify(self);
  111. [[RACObserve(data, thumbImage) takeUntil:self.rac_prepareForReuseSignal] subscribeNext:^(UIImage *thumbImage) {
  112. @strongify(self);
  113. if (thumbImage) {
  114. self.thumb.image = thumbImage;
  115. }
  116. }];
  117. _duration.text = [NSString stringWithFormat:@"%02ld:%02ld", (long)data.videoItem.duration / 60, (long)data.videoItem.duration % 60];
  118. self.play.hidden = YES;
  119. self.downloadImage.hidden = YES;
  120. self.indicator.hidden = YES;
  121. if (data.direction == MsgDirectionIncoming) {
  122. [[[RACObserve(data, thumbProgress) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSNumber *x) {
  123. @strongify(self);
  124. // Cover download progress callback
  125. int progress = [x intValue];
  126. self.progress.text = [NSString stringWithFormat:@"%d%%", progress];
  127. self.progress.hidden = (progress >= 100 || progress == 0);
  128. self.animateCircleView.progress = progress;
  129. if (progress >= 100 || progress == 0) {
  130. // The progress of cover download is called back and the download video icon is displayed when the cover progress is 100.
  131. if ([data isVideoExist]) {
  132. self.play.hidden = NO;
  133. } else {
  134. self.downloadImage.hidden = NO;
  135. }
  136. } else {
  137. self.play.hidden = YES;
  138. self.downloadImage.hidden = YES;
  139. }
  140. }];
  141. // Video resource download progress callback
  142. [[[RACObserve(data, videoProgress) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSNumber *x) {
  143. @strongify(self);
  144. int progress = [x intValue];
  145. self.animateCircleView.progress = progress;
  146. if (progress >= 100 || progress == 0) {
  147. self.play.hidden = NO;
  148. self.animateCircleView.hidden = YES;
  149. } else {
  150. self.play.hidden = YES;
  151. self.downloadImage.hidden = YES;
  152. self.animateCircleView.hidden = NO;
  153. }
  154. }];
  155. } else {
  156. if ([data isVideoExist]) {
  157. [[[RACObserve(data, uploadProgress) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSNumber *x) {
  158. @strongify(self);
  159. int progress = [x intValue];
  160. if (data.placeHolderCellData.videoTranscodingProgress > 0) {
  161. progress = MAX(progress, 30);//the upload progress increases from 30% to 100%.
  162. }
  163. self.animateCircleView.progress = progress;
  164. if (progress >= 100 || progress == 0) {
  165. [self.indicator stopAnimating];
  166. self.play.hidden = NO;
  167. self.animateCircleView.hidden = YES;
  168. } else {
  169. [self.indicator startAnimating];
  170. self.play.hidden = YES;
  171. self.animateCircleView.hidden = NO;
  172. }
  173. }];
  174. } else {
  175. [[[RACObserve(data, thumbProgress) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSNumber *x) {
  176. @strongify(self);
  177. // Cover download progress callback
  178. int progress = [x intValue];
  179. self.progress.text = [NSString stringWithFormat:@"%d%%", progress];
  180. self.progress.hidden = (progress >= 100 || progress == 0);
  181. self.animateCircleView.progress = progress;
  182. if (progress >= 100 || progress == 0) {
  183. // The download video icon is displayed when the cover progress reaches 100
  184. if ([data isVideoExist]) {
  185. self.play.hidden = NO;
  186. } else {
  187. self.downloadImage.hidden = NO;
  188. }
  189. } else {
  190. self.play.hidden = YES;
  191. self.downloadImage.hidden = YES;
  192. }
  193. }];
  194. // Video resource download progress callback
  195. [[[RACObserve(data, videoProgress) takeUntil:self.rac_prepareForReuseSignal] distinctUntilChanged] subscribeNext:^(NSNumber *x) {
  196. @strongify(self);
  197. int progress = [x intValue];
  198. self.animateCircleView.progress = progress;
  199. if (progress >= 100 || progress == 0) {
  200. self.play.hidden = NO;
  201. self.animateCircleView.hidden = YES;
  202. } else {
  203. self.play.hidden = YES;
  204. self.downloadImage.hidden = YES;
  205. self.animateCircleView.hidden = NO;
  206. }
  207. }];
  208. }
  209. }
  210. // tell constraints they need updating
  211. [self setNeedsUpdateConstraints];
  212. // update constraints now so we can animate the change
  213. [self updateConstraintsIfNeeded];
  214. [self layoutIfNeeded];
  215. }
  216. + (BOOL)requiresConstraintBasedLayout {
  217. return YES;
  218. }
  219. // this is Apple's recommended place for adding/updating constraints
  220. - (void)updateConstraints {
  221. [super updateConstraints];
  222. if (self.messageData.messageContainerAppendSize.height > 0) {
  223. CGFloat topMargin = 10;
  224. CGFloat tagViewTopMargin = 6;
  225. CGFloat thumbHeight = self.bubbleView.mm_h - topMargin - self.messageData.messageContainerAppendSize.height - tagViewTopMargin;
  226. CGSize size = [self.class getContentSize:self.messageData];
  227. [self.thumb mas_remakeConstraints:^(MASConstraintMaker *make) {
  228. make.height.mas_equalTo(thumbHeight);
  229. make.width.mas_equalTo(size.width);
  230. make.centerX.mas_equalTo(self.bubbleView);
  231. make.top.mas_equalTo(self.container).mas_offset(topMargin);
  232. }];
  233. [self.duration mas_remakeConstraints:^(MASConstraintMaker *make) {
  234. make.trailing.mas_equalTo(self.thumb.mas_trailing).mas_offset(-2);
  235. make.width.mas_greaterThanOrEqualTo(20);
  236. make.height.mas_equalTo(20);
  237. make.bottom.mas_equalTo(self.thumb.mas_bottom);
  238. }];
  239. } else {
  240. [self.thumb mas_remakeConstraints:^(MASConstraintMaker *make) {
  241. make.top.mas_equalTo(self.bubbleView).mas_offset(self.messageData.cellLayout.bubbleInsets.top);
  242. make.bottom.mas_equalTo(self.bubbleView).mas_offset(- self.messageData.cellLayout.bubbleInsets.bottom);
  243. make.leading.mas_equalTo(self.bubbleView).mas_offset(self.messageData.cellLayout.bubbleInsets.left);
  244. make.trailing.mas_equalTo(self.bubbleView).mas_offset(- self.messageData.cellLayout.bubbleInsets.right);
  245. }];
  246. [self.duration mas_remakeConstraints:^(MASConstraintMaker *make) {
  247. make.trailing.mas_equalTo(self.thumb.mas_trailing).mas_offset(-2);
  248. make.width.mas_greaterThanOrEqualTo(20);
  249. make.height.mas_equalTo(20);
  250. make.bottom.mas_equalTo(self.thumb.mas_bottom);
  251. }];
  252. }
  253. BOOL hasRiskContent = self.messageData.innerMessage.hasRiskContent;
  254. if (hasRiskContent ) {
  255. [self.thumb mas_remakeConstraints:^(MASConstraintMaker *make) {
  256. make.top.mas_equalTo(self.bubbleView).mas_offset(12);
  257. make.size.mas_equalTo(CGSizeMake(150, 150));
  258. make.centerX.mas_equalTo(self.bubbleView);
  259. }];
  260. [self.securityStrikeView mas_remakeConstraints:^(MASConstraintMaker *make) {
  261. make.top.mas_equalTo(self.thumb.mas_bottom);
  262. make.width.mas_equalTo(self.bubbleView);
  263. if(self.messageData.messageContainerAppendSize.height>0) {
  264. make.bottom.mas_equalTo(self.container).mas_offset(-self.messageData.messageContainerAppendSize.height);
  265. }
  266. else {
  267. make.bottom.mas_equalTo(self.container).mas_offset(-12);
  268. }
  269. }];
  270. }
  271. [self.play mas_remakeConstraints:^(MASConstraintMaker *make) {
  272. make.size.mas_equalTo(TVideoMessageCell_Play_Size);
  273. make.center.mas_equalTo(self.thumb);
  274. }];
  275. [self.downloadImage mas_remakeConstraints:^(MASConstraintMaker *make) {
  276. make.size.mas_equalTo(TVideoMessageCell_Play_Size);
  277. make.center.mas_equalTo(self.thumb);
  278. }];
  279. [self.animateCircleView mas_remakeConstraints:^(MASConstraintMaker *make) {
  280. make.center.mas_equalTo(self.thumb);
  281. make.size.mas_equalTo(CGSizeMake(kScale390(40), kScale390(40)));
  282. }];
  283. }
  284. - (void)layoutSubviews {
  285. [super layoutSubviews];
  286. }
  287. - (void)highlightWhenMatchKeyword:(NSString *)keyword {
  288. if (keyword) {
  289. if (self.highlightAnimating) {
  290. return;
  291. }
  292. [self animate:3];
  293. }
  294. }
  295. - (void)animate:(int)times {
  296. times--;
  297. if (times < 0) {
  298. [self.animateHighlightView removeFromSuperview];
  299. self.highlightAnimating = NO;
  300. return;
  301. }
  302. self.highlightAnimating = YES;
  303. self.animateHighlightView.frame = self.container.bounds;
  304. self.animateHighlightView.alpha = 0.1;
  305. [self.container addSubview:self.animateHighlightView];
  306. [UIView animateWithDuration:0.25
  307. animations:^{
  308. self.animateHighlightView.alpha = 0.5;
  309. }
  310. completion:^(BOOL finished) {
  311. [UIView animateWithDuration:0.25
  312. animations:^{
  313. self.animateHighlightView.alpha = 0.1;
  314. }
  315. completion:^(BOOL finished) {
  316. if (!self.videoData.highlightKeyword) {
  317. [self animate:0];
  318. return;
  319. }
  320. [self animate:times];
  321. }];
  322. }];
  323. }
  324. - (UIView *)animateHighlightView {
  325. if (_animateHighlightView == nil) {
  326. _animateHighlightView = [[UIView alloc] init];
  327. _animateHighlightView.backgroundColor = [UIColor orangeColor];
  328. }
  329. return _animateHighlightView;
  330. }
  331. #pragma mark - TUIMessageProgressManagerDelegate
  332. - (void)onUploadProgress:(NSString *)msgID progress:(NSInteger)progress {
  333. if (![msgID isEqualToString:self.videoData.msgID]) {
  334. return;
  335. }
  336. if (self.videoData.direction == MsgDirectionOutgoing) {
  337. self.videoData.uploadProgress = progress;
  338. }
  339. }
  340. #pragma mark - TUIMessageCellProtocol
  341. + (CGSize)getContentSize:(TUIMessageCellData *)data {
  342. NSAssert([data isKindOfClass:TUIVideoMessageCellData.class], @"data must be kind of TUIVideoMessageCellData");
  343. TUIVideoMessageCellData *videoCellData = (TUIVideoMessageCellData *)data;
  344. CGSize size = CGSizeZero;
  345. BOOL isDir = NO;
  346. if (![videoCellData.snapshotPath isEqualToString:@""] && [[NSFileManager defaultManager] fileExistsAtPath:videoCellData.snapshotPath isDirectory:&isDir]) {
  347. if (!isDir) {
  348. size = [UIImage imageWithContentsOfFile:videoCellData.snapshotPath].size;
  349. }
  350. } else {
  351. size = videoCellData.snapshotItem.size;
  352. }
  353. if (CGSizeEqualToSize(size, CGSizeZero)) {
  354. return size;
  355. }
  356. if (size.height > size.width) {
  357. size.width = size.width / size.height * TVideoMessageCell_Image_Height_Max;
  358. size.height = TVideoMessageCell_Image_Height_Max;
  359. } else {
  360. size.height = size.height / size.width * TVideoMessageCell_Image_Width_Max;
  361. size.width = TVideoMessageCell_Image_Width_Max;
  362. }
  363. BOOL hasRiskContent = videoCellData.innerMessage.hasRiskContent;
  364. if (hasRiskContent) {
  365. CGFloat bubbleTopMargin = 12;
  366. CGFloat bubbleBottomMargin = 12;
  367. size.height = MAX(size.height, 150);// width must more than TIMCommonBundleThemeImage(@"", @"icon_security_strike");
  368. size.width = MAX(size.width, 200);// width must more than TIMCommonLocalizableString(TUIKitMessageTypeSecurityStrike)
  369. size.height += bubbleTopMargin;
  370. size.height += kTUISecurityStrikeViewTopLineMargin;
  371. size.height += kTUISecurityStrikeViewTopLineToBottom;
  372. size.height += bubbleBottomMargin;
  373. }
  374. return size;
  375. }
  376. @end