Lazy load, eagerly unload & cache cell media.

// FREEBIE
This commit is contained in:
Matthew Chen 2017-10-16 10:39:52 -04:00
parent e77292c2a9
commit 65efa7f836
6 changed files with 202 additions and 125 deletions

View File

@ -15,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN
isIncoming:(BOOL)isIncoming
viewItem:(ConversationViewItem *)viewItem;
- (void)createContentsForSize:(CGSize)viewSize;
- (void)createContents;
+ (CGFloat)bubbleHeight;

View File

@ -193,34 +193,46 @@ NS_ASSUME_NONNULL_BEGIN
return (self.attachmentStream.isVoiceMessage || self.attachmentStream.sourceFilename.length < 1);
}
- (void)createContentsForSize:(CGSize)viewSize
- (void)createContents
{
UIColor *textColor = [self audioTextColor];
self.backgroundColor = self.bubbleBackgroundColor;
self.layoutMargins = UIEdgeInsetsZero;
// TODO: Verify that this layout works in RTL.
const CGFloat kBubbleTailWidth = 6.f;
CGRect contentFrame = CGRectMake(self.isIncoming ? kBubbleTailWidth : 0.f,
self.audioIconVMargin,
viewSize.width - kBubbleTailWidth - self.audioIconHMargin,
viewSize.height - self.audioIconVMargin * 2);
CGRect iconFrame = CGRectMake((CGFloat)round(contentFrame.origin.x + self.audioIconHMargin),
(CGFloat)round(contentFrame.origin.y + (contentFrame.size.height - self.iconSize) * 0.5f),
self.iconSize,
self.iconSize);
_audioPlayPauseButton = [[UIButton alloc] initWithFrame:iconFrame];
_audioPlayPauseButton.enabled = NO;
[self addSubview:_audioPlayPauseButton];
UIView *contentView = [UIView containerView];
[self addSubview:contentView];
[contentView autoPinLeadingToSuperviewWithMargin:self.isIncoming ? kBubbleTailWidth : 0.f];
[contentView autoPinTrailingToSuperviewWithMargin:self.isIncoming ? 0.f : kBubbleTailWidth];
[contentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.audioIconVMargin];
[contentView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.audioIconVMargin];
_audioPlayPauseButton = [UIButton buttonWithType:UIButtonTypeCustom];
self.audioPlayPauseButton.enabled = NO;
[contentView addSubview:self.audioPlayPauseButton];
[self.audioPlayPauseButton autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:self.audioIconHMargin];
[self.audioPlayPauseButton autoVCenterInSuperview];
[self.audioPlayPauseButton autoSetDimension:ALDimensionWidth toSize:self.iconSize];
[self.audioPlayPauseButton autoSetDimension:ALDimensionHeight toSize:self.iconSize];
const CGFloat kLabelHSpacing = self.audioIconHSpacing;
UIView *labelsView = [UIView containerView];
[contentView addSubview:labelsView];
[labelsView autoPinLeadingToTrailingOfView:self.audioPlayPauseButton margin:kLabelHSpacing];
[labelsView autoPinEdgeToSuperviewEdge:ALEdgeRight];
[labelsView autoVCenterInSuperview];
const CGFloat kLabelVSpacing = 2;
NSString *filename = self.attachmentStream.sourceFilename;
if (!filename) {
filename = [[self.attachmentStream filePath] lastPathComponent];
}
NSString *topText = [[filename stringByDeletingPathExtension]
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if (topText.length < 1) {
topText = [MIMETypeUtil fileExtensionForMIMEType:self.attachmentStream.contentType].uppercaseString;
}
@ -235,13 +247,19 @@ NS_ASSUME_NONNULL_BEGIN
topLabel.textColor = [textColor colorWithAlphaComponent:0.85f];
topLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
topLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)];
[topLabel sizeToFit];
[self addSubview:topLabel];
topLabel.textAlignment = NSTextAlignmentLeft;
[labelsView addSubview:topLabel];
[topLabel autoPinEdgeToSuperviewEdge:ALEdgeTop];
[topLabel autoPinWidthToSuperview];
const CGFloat kAudioProgressViewHeight = 12.f;
AudioProgressView *audioProgressView = [AudioProgressView new];
self.audioProgressView = audioProgressView;
[self updateAudioProgressView];
[self addSubview:audioProgressView];
[labelsView addSubview:audioProgressView];
[audioProgressView autoPinWidthToSuperview];
[audioProgressView autoSetDimension:ALDimensionHeight toSize:kAudioProgressViewHeight];
[audioProgressView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:topLabel withOffset:kLabelVSpacing];
UILabel *bottomLabel = [UILabel new];
self.audioBottomLabel = bottomLabel;
@ -249,25 +267,11 @@ NS_ASSUME_NONNULL_BEGIN
bottomLabel.textColor = [textColor colorWithAlphaComponent:0.85f];
bottomLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
bottomLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)];
[bottomLabel sizeToFit];
[self addSubview:bottomLabel];
const CGFloat topLabelHeight = (CGFloat)ceil(topLabel.font.lineHeight);
const CGFloat kAudioProgressViewHeight = 12.f;
const CGFloat bottomLabelHeight = (CGFloat)ceil(bottomLabel.font.lineHeight);
CGRect labelsBounds = CGRectZero;
labelsBounds.origin.x = (CGFloat)round(iconFrame.origin.x + iconFrame.size.width + kLabelHSpacing);
labelsBounds.size.width = contentFrame.origin.x + contentFrame.size.width - labelsBounds.origin.x;
labelsBounds.size.height = topLabelHeight + kAudioProgressViewHeight + bottomLabelHeight + kLabelVSpacing * 2;
labelsBounds.origin.y
= (CGFloat)round(contentFrame.origin.y + (contentFrame.size.height - labelsBounds.size.height) * 0.5f);
CGFloat y = labelsBounds.origin.y;
topLabel.frame = CGRectMake(labelsBounds.origin.x, labelsBounds.origin.y, labelsBounds.size.width, topLabelHeight);
y += topLabelHeight + kLabelVSpacing;
audioProgressView.frame = CGRectMake(labelsBounds.origin.x, y, labelsBounds.size.width, kAudioProgressViewHeight);
y += kAudioProgressViewHeight + kLabelVSpacing;
bottomLabel.frame = CGRectMake(labelsBounds.origin.x, y, labelsBounds.size.width, bottomLabelHeight);
bottomLabel.textAlignment = NSTextAlignmentLeft;
[labelsView addSubview:bottomLabel];
[bottomLabel autoPinWidthToSuperview];
[bottomLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:audioProgressView withOffset:kLabelVSpacing];
[bottomLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom];
[self updateContents];
}

View File

@ -10,7 +10,7 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachmentStream isIncoming:(BOOL)isIncoming;
- (void)createContentsForSize:(CGSize)viewSize;
- (void)createContents;
+ (CGFloat)bubbleHeight;

View File

@ -98,32 +98,43 @@ NS_ASSUME_NONNULL_BEGIN
return [self.textColor blendWithColor:self.bubbleBackgroundColor alpha:alpha];
}
- (void)createContentsForSize:(CGSize)viewSize
- (void)createContents
{
UIColor *textColor = (self.isIncoming ? [UIColor colorWithWhite:0.2 alpha:1.f] : [UIColor whiteColor]);
self.backgroundColor = self.bubbleBackgroundColor;
self.layoutMargins = UIEdgeInsetsZero;
// TODO: Verify that this layout works in RTL.
const CGFloat kBubbleTailWidth = 6.f;
CGRect contentFrame = CGRectMake(self.isIncoming ? kBubbleTailWidth : 0.f,
self.vMargin,
viewSize.width - kBubbleTailWidth - self.iconHMargin,
viewSize.height - self.vMargin * 2);
UIView *contentView = [UIView containerView];
[self addSubview:contentView];
[contentView autoPinLeadingToSuperviewWithMargin:self.isIncoming ? kBubbleTailWidth : 0.f];
[contentView autoPinTrailingToSuperviewWithMargin:self.isIncoming ? 0.f : kBubbleTailWidth];
[contentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.vMargin];
[contentView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.vMargin];
UIImage *image = [UIImage imageNamed:@"generic-attachment-small"];
OWSAssert(image);
UIImageView *imageView = [UIImageView new];
CGRect iconFrame = CGRectMake(round(contentFrame.origin.x + self.iconHMargin),
round(contentFrame.origin.y + (contentFrame.size.height - self.iconSize) * 0.5f),
self.iconSize,
self.iconSize);
imageView.frame = iconFrame;
imageView.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
imageView.tintColor = self.bubbleBackgroundColor;
imageView.backgroundColor
= (self.isIncoming ? [UIColor colorWithRGBHex:0x9e9e9e] : [self foregroundColorWithOpacity:0.15f]);
imageView.layer.cornerRadius = MIN(imageView.bounds.size.width, imageView.bounds.size.height) * 0.5f;
[self addSubview:imageView];
[contentView addSubview:imageView];
[imageView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:self.iconHMargin];
[imageView autoVCenterInSuperview];
[imageView autoSetDimension:ALDimensionWidth toSize:self.iconSize];
[imageView autoSetDimension:ALDimensionHeight toSize:self.iconSize];
const CGFloat kLabelHSpacing = self.iconHSpacing;
UIView *labelsView = [UIView containerView];
[contentView addSubview:labelsView];
[labelsView autoPinLeadingToTrailingOfView:imageView margin:kLabelHSpacing];
[labelsView autoPinEdgeToSuperviewEdge:ALEdgeRight];
[labelsView autoVCenterInSuperview];
NSString *filename = self.attachmentStream.sourceFilename;
if (!filename) {
@ -145,19 +156,11 @@ NS_ASSUME_NONNULL_BEGIN
fileTypeLabel.font = [UIFont ows_mediumFontWithSize:20.f];
fileTypeLabel.adjustsFontSizeToFitWidth = YES;
fileTypeLabel.textAlignment = NSTextAlignmentCenter;
CGRect fileTypeLabelFrame = CGRectZero;
fileTypeLabelFrame.size = [fileTypeLabel sizeThatFits:CGSizeZero];
// This dimension depends on the space within the icon boundaries.
fileTypeLabelFrame.size.width = 15.f;
// Center on icon.
fileTypeLabelFrame.origin.x
= round(iconFrame.origin.x + (iconFrame.size.width - fileTypeLabelFrame.size.width) * 0.5f);
fileTypeLabelFrame.origin.y
= round(iconFrame.origin.y + (iconFrame.size.height - fileTypeLabelFrame.size.height) * 0.5f);
fileTypeLabel.frame = fileTypeLabelFrame;
[self addSubview:fileTypeLabel];
[imageView addSubview:fileTypeLabel];
[imageView autoCenterInSuperview];
[imageView autoSetDimension:ALDimensionWidth toSize:15.f];
const CGFloat kLabelHSpacing = self.iconHSpacing;
const CGFloat kLabelVSpacing = 2;
NSString *topText =
[self.attachmentStream.sourceFilename stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
@ -172,8 +175,10 @@ NS_ASSUME_NONNULL_BEGIN
topLabel.textColor = textColor;
topLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
topLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(13.f, 15.f)];
[topLabel sizeToFit];
[self addSubview:topLabel];
topLabel.textAlignment = NSTextAlignmentLeft;
[labelsView addSubview:topLabel];
[topLabel autoPinEdgeToSuperviewEdge:ALEdgeTop];
[topLabel autoPinWidthToSuperview];
NSError *error;
unsigned long long fileSize =
@ -185,21 +190,10 @@ NS_ASSUME_NONNULL_BEGIN
bottomLabel.textColor = [textColor colorWithAlphaComponent:0.85f];
bottomLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
bottomLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)];
[bottomLabel sizeToFit];
[self addSubview:bottomLabel];
CGRect topLabelFrame = CGRectZero;
topLabelFrame.size = topLabel.bounds.size;
topLabelFrame.origin.x = round(iconFrame.origin.x + iconFrame.size.width + kLabelHSpacing);
topLabelFrame.origin.y = round(contentFrame.origin.y
+ (contentFrame.size.height - (topLabel.frame.size.height + bottomLabel.frame.size.height + kLabelVSpacing))
* 0.5f);
topLabelFrame.size.width = round((contentFrame.origin.x + contentFrame.size.width) - topLabelFrame.origin.x);
topLabel.frame = topLabelFrame;
CGRect bottomLabelFrame = topLabelFrame;
bottomLabelFrame.origin.y += topLabelFrame.size.height + kLabelVSpacing;
bottomLabel.frame = bottomLabelFrame;
[labelsView addSubview:bottomLabel];
[bottomLabel autoPinWidthToSuperview];
[bottomLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:topLabel withOffset:kLabelVSpacing];
[bottomLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom];
}
@end

View File

@ -243,7 +243,7 @@ NS_ASSUME_NONNULL_BEGIN
case OWSMessageCellType_GenericAttachment: {
self.attachmentView =
[[OWSGenericAttachmentView alloc] initWithAttachment:self.attachmentStream isIncoming:self.isIncoming];
[self.attachmentView createContentsForSize:self.bounds.size];
[self.attachmentView createContents];
[self replaceBubbleWithView:self.attachmentView];
[self addAttachmentUploadViewIfNecessary:self.attachmentView];
break;
@ -254,6 +254,8 @@ NS_ASSUME_NONNULL_BEGIN
}
}
[self ensureViewMediaState];
// dispatch_async(dispatch_get_main_queue(), ^{
// NSLog(@"---- %@", self.viewItem.interaction.debugDescription);
// NSLog(@"cell: %@", NSStringFromCGRect(self.frame));
@ -263,6 +265,116 @@ NS_ASSUME_NONNULL_BEGIN
// });
}
- (nullable id)tryToLoadCellMedia:(nullable id (^)())loadCellMediaBlock mediaView:(UIView *)mediaView
{
OWSAssert(self.attachmentStream);
OWSAssert(mediaView);
if (self.viewItem.didCellMediaFailToLoad) {
return nil;
}
id _Nullable cellMedia = self.viewItem.cachedCellMedia;
if (cellMedia) {
DDLogVerbose(@"%@ cell media cache hit", self.logTag);
return cellMedia;
}
cellMedia = loadCellMediaBlock();
if (cellMedia) {
DDLogVerbose(@"%@ cell media cache miss", self.logTag);
self.viewItem.cachedCellMedia = cellMedia;
} else {
DDLogError(@"%@ Failed to load cell media: %@", [self logTag], [self.attachmentStream mediaURL]);
self.viewItem.didCellMediaFailToLoad = YES;
[mediaView removeFromSuperview];
// TODO: We need to hide/remove the media view.
[self showAttachmentErrorView];
}
return cellMedia;
}
// We want to lazy-load expensive view contents and eagerly unload if the
// cell is no longer visible.
- (void)ensureViewMediaState
{
if (!self.isCellVisible) {
// Eagerly unload.
if (self.stillImageView.image || self.animatedImageView.image) {
DDLogError(@"%@ ---- ensureViewMediaState unloading[%zd]: %@",
self.logTag,
self.viewItem.row,
self.viewItem.interaction.description);
}
self.stillImageView.image = nil;
self.animatedImageView.image = nil;
return;
}
switch (self.cellType) {
case OWSMessageCellType_StillImage: {
if (self.stillImageView.image) {
return;
}
DDLogError(@"%@ ---- ensureViewMediaState loading[%zd]: %@",
self.logTag,
self.viewItem.row,
self.viewItem.interaction.description);
self.stillImageView.image = [self tryToLoadCellMedia:^{
OWSAssert([self.attachmentStream isImage]);
return self.attachmentStream.image;
}
mediaView:self.stillImageView];
break;
}
case OWSMessageCellType_AnimatedImage: {
if (self.animatedImageView.image) {
return;
}
DDLogError(@"%@ ---- ensureViewMediaState loading[%zd]: %@",
self.logTag,
self.viewItem.row,
self.viewItem.interaction.description);
self.animatedImageView.image = [self tryToLoadCellMedia:^{
OWSAssert([self.attachmentStream isAnimated]);
NSString *_Nullable filePath = [self.attachmentStream filePath];
YYImage *_Nullable animatedImage = nil;
if (filePath && [NSData ows_isValidImageAtPath:filePath]) {
animatedImage = [YYImage imageWithContentsOfFile:filePath];
}
return animatedImage;
}
mediaView:self.animatedImageView];
break;
}
case OWSMessageCellType_Audio:
// TODO: Lazy load audio length in audio cells.
// [self loadForAudioDisplay];
break;
case OWSMessageCellType_Video: {
if (self.stillImageView.image) {
return;
}
DDLogError(@"%@ ---- ensureViewMediaState loading[%zd]: %@",
self.logTag,
self.viewItem.row,
self.viewItem.interaction.description);
self.stillImageView.image = [self tryToLoadCellMedia:^{
OWSAssert([self.attachmentStream isVideo]);
return self.attachmentStream.image;
}
mediaView:self.stillImageView];
break;
}
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage:
case OWSMessageCellType_GenericAttachment:
case OWSMessageCellType_DownloadingAttachment:
// Inexpensive cell types don't need to lazy-load or eagerly-unload.
break;
}
}
- (void)updateDateHeader
{
OWSAssert(self.contentWidth > 0);
@ -454,14 +566,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(self.attachmentStream);
OWSAssert([self.attachmentStream isImage]);
UIImage *_Nullable image = self.attachmentStream.image;
if (!image) {
DDLogError(@"%@ Could not load image: %@", [self logTag], [self.attachmentStream mediaURL]);
[self showAttachmentErrorView];
return;
}
self.stillImageView = [[UIImageView alloc] initWithImage:image];
self.stillImageView = [UIImageView new];
// We need to specify a contentMode since the size of the image
// might not match the aspect ratio of the view.
self.stillImageView.contentMode = UIViewContentModeScaleAspectFill;
@ -478,19 +583,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(self.attachmentStream);
OWSAssert([self.attachmentStream isAnimated]);
NSString *_Nullable filePath = [self.attachmentStream filePath];
YYImage *_Nullable animatedImage = nil;
if (filePath && [NSData ows_isValidImageAtPath:filePath]) {
animatedImage = [YYImage imageWithContentsOfFile:filePath];
}
if (!animatedImage) {
DDLogError(@"%@ Could not load animated image: %@", [self logTag], [self.attachmentStream mediaURL]);
[self showAttachmentErrorView];
return;
}
self.animatedImageView = [[YYAnimatedImageView alloc] init];
self.animatedImageView.image = animatedImage;
// We need to specify a contentMode since the size of the image
// might not match the aspect ratio of the view.
self.animatedImageView.contentMode = UIViewContentModeScaleAspectFill;
@ -507,7 +600,7 @@ NS_ASSUME_NONNULL_BEGIN
isIncoming:self.isIncoming
viewItem:self.viewItem];
self.viewItem.lastAudioMessageView = self.audioMessageView;
[self.audioMessageView createContentsForSize:self.bounds.size];
[self.audioMessageView createContents];
[self replaceBubbleWithView:self.audioMessageView];
[self addAttachmentUploadViewIfNecessary:self.audioMessageView];
}
@ -517,16 +610,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(self.attachmentStream);
OWSAssert([self.attachmentStream isVideo]);
// CGSize size = [self mediaViewDisplaySize];
UIImage *_Nullable image = self.attachmentStream.image;
if (!image) {
DDLogError(@"%@ Could not load image: %@", [self logTag], [self.attachmentStream mediaURL]);
[self showAttachmentErrorView];
return;
}
self.stillImageView = [[UIImageView alloc] initWithImage:image];
self.stillImageView = [UIImageView new];
// We need to specify a contentMode since the size of the image
// might not match the aspect ratio of the view.
self.stillImageView.contentMode = UIViewContentModeScaleAspectFill;
@ -616,25 +700,11 @@ NS_ASSUME_NONNULL_BEGIN
[self.payloadView updateMask];
}
//// TODO:
//- (void)setFrame:(CGRect)frame {
// [super setFrame:frame];
//
// DDLogError(@"setFrame: %@ %@ %@", self.viewItem.interaction.uniqueId, self.viewItem.interaction.description,
// NSStringFromCGRect(frame));
//}
//
//// TODO:
//- (void)setBounds:(CGRect)bounds {
// [super setBounds:bounds];
//
// DDLogError(@"setBounds: %@ %@ %@", self.viewItem.interaction.uniqueId, self.viewItem.interaction.description,
// NSStringFromCGRect(bounds));
//}
- (void)showAttachmentErrorView
{
// TODO: We could do a better job of indicating that the image could not be loaded.
OWSAssert(!self.customView);
// TODO: We could do a better job of indicating that the media could not be loaded.
self.customView = [UIView new];
self.customView.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f];
self.customView.userInteractionEnabled = NO;
@ -862,6 +932,8 @@ NS_ASSUME_NONNULL_BEGIN
return;
}
[self ensureViewMediaState];
if (isCellVisible) {
if (self.message.shouldStartExpireTimer) {
[self.expirationTimerView ensureAnimations];

View File

@ -86,6 +86,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
- (nullable TSAttachmentPointer *)attachmentPointer;
- (CGSize)contentSize;
// A generic property that cells can use to cache their loaded
// media. This cache is volatile and will get evacuated based
// on scroll state, so that we only retain state for a sliding
// window of cells that are almost on-screen.
@property (nonatomic) id cachedCellMedia;
@property (nonatomic) BOOL didCellMediaFailToLoad;
// TODO:
//// Cells will request that this adapter clear its cached media views,
//// but the adapter should only honor requests from the last cell to