| OLD | NEW |
| 1 // Copyright 2017 The Chromium Authors. All rights reserved. | 1 // Copyright 2017 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_ite
m.h" | 5 #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_ite
m.h" |
| 6 | 6 |
| 7 #include "base/time/time.h" | 7 #include "base/time/time.h" |
| 8 #import "ios/chrome/browser/ui/colors/MDCPalette+CrAdditions.h" | 8 #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_cel
l.h" |
| 9 #import "ios/chrome/browser/ui/content_suggestions/identifier/content_suggestion
_identifier.h" | 9 #import "ios/chrome/browser/ui/content_suggestions/identifier/content_suggestion
_identifier.h" |
| 10 #import "ios/chrome/browser/ui/favicon/favicon_attributes.h" | 10 #import "ios/chrome/browser/ui/favicon/favicon_attributes.h" |
| 11 #import "ios/chrome/browser/ui/favicon/favicon_view.h" | 11 #import "ios/chrome/browser/ui/favicon/favicon_view.h" |
| 12 #import "ios/chrome/browser/ui/uikit_ui_util.h" | |
| 13 #import "ios/chrome/browser/ui/util/i18n_string.h" | |
| 14 #import "ios/third_party/material_components_ios/src/components/Palettes/src/Mat
erialPalettes.h" | |
| 15 #import "ios/third_party/material_components_ios/src/components/Typography/src/M
aterialTypography.h" | |
| 16 #include "url/gurl.h" | 12 #include "url/gurl.h" |
| 17 | 13 |
| 18 #if !defined(__has_feature) || !__has_feature(objc_arc) | 14 #if !defined(__has_feature) || !__has_feature(objc_arc) |
| 19 #error "This file requires ARC support." | 15 #error "This file requires ARC support." |
| 20 #endif | 16 #endif |
| 21 | 17 |
| 22 namespace { | |
| 23 const CGFloat kImageSize = 72; | |
| 24 const CGFloat kStandardSpacing = 16; | |
| 25 const CGFloat kSmallSpacing = 8; | |
| 26 | |
| 27 // Name of the icon displayed when a suggestion is available offline. | |
| 28 NSString* const kOfflineIconName = @"content_suggestions_offline"; | |
| 29 // Size of the icon displayed when a suggestion is available offline. | |
| 30 const CGFloat kOfflineIconSize = 24; | |
| 31 | |
| 32 // Size of the favicon view. | |
| 33 const CGFloat kFaviconSize = 16; | |
| 34 // Size of the icon displayed when there is not image but one should be | |
| 35 // displayed. | |
| 36 const CGFloat kIconSize = 24; | |
| 37 // Name of the icon displayed when there is not image but one should be | |
| 38 // displayed. | |
| 39 NSString* const kNoImageIconName = @"content_suggestions_no_image"; | |
| 40 // No image icon percentage of white. | |
| 41 const CGFloat kNoImageIconWhite = 0.38; | |
| 42 // No image background percentage of white. | |
| 43 const CGFloat kNoImageBackgroundWhite = 0.95; | |
| 44 // Duration of the animation to display the image. | |
| 45 const CGFloat kAnimationDuration = 0.3; | |
| 46 } | |
| 47 | |
| 48 @interface ContentSuggestionsItem () | 18 @interface ContentSuggestionsItem () |
| 49 | 19 |
| 50 @property(nonatomic, copy) NSString* subtitle; | 20 @property(nonatomic, copy) NSString* subtitle; |
| 51 // Used to check if the image has already been fetched. There is no way to | 21 // Used to check if the image has already been fetched. There is no way to |
| 52 // discriminate between failed image download and nonexitent image. The | 22 // discriminate between failed image download and nonexitent image. The |
| 53 // suggestion tries to download the image only once. | 23 // suggestion tries to download the image only once. |
| 54 @property(nonatomic, assign) BOOL imageFetched; | 24 @property(nonatomic, assign) BOOL imageFetched; |
| 55 | 25 |
| 56 @end | 26 @end |
| 57 | 27 |
| (...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 91 if (self.hasImage && !self.imageFetched) { | 61 if (self.hasImage && !self.imageFetched) { |
| 92 self.imageFetched = YES; | 62 self.imageFetched = YES; |
| 93 // Fetch the image. During the fetch the cell's image should still be set. | 63 // Fetch the image. During the fetch the cell's image should still be set. |
| 94 [self.delegate loadImageForSuggestedItem:self]; | 64 [self.delegate loadImageForSuggestedItem:self]; |
| 95 } | 65 } |
| 96 [cell.faviconView configureWithAttributes:self.attributes]; | 66 [cell.faviconView configureWithAttributes:self.attributes]; |
| 97 cell.titleLabel.text = self.title; | 67 cell.titleLabel.text = self.title; |
| 98 [cell setSubtitleText:self.subtitle]; | 68 [cell setSubtitleText:self.subtitle]; |
| 99 cell.displayImage = self.hasImage; | 69 cell.displayImage = self.hasImage; |
| 100 [cell setContentImage:self.image]; | 70 [cell setContentImage:self.image]; |
| 71 NSDate* date = |
| 72 [NSDate dateWithTimeIntervalSince1970:self.publishDate.ToDoubleT()]; |
| 101 [cell setAdditionalInformationWithPublisherName:self.publisher | 73 [cell setAdditionalInformationWithPublisherName:self.publisher |
| 102 date:self.publishDate | 74 date:date |
| 103 offlineAvailability:self.availableOffline]; | 75 offlineAvailability:self.availableOffline]; |
| 104 } | 76 } |
| 105 | 77 |
| 106 @end | 78 @end |
| 107 | |
| 108 #pragma mark - ContentSuggestionsCell | |
| 109 | |
| 110 @interface ContentSuggestionsCell () | |
| 111 | |
| 112 @property(nonatomic, strong) UILabel* additionalInformationLabel; | |
| 113 // Contains the no-image icon or the image. | |
| 114 @property(nonatomic, strong) UIView* imageContainer; | |
| 115 // The no-image icon displayed when there is no image. | |
| 116 @property(nonatomic, strong) UIImageView* noImageIcon; | |
| 117 // Displays the image associated with this suggestion. It is added to the | |
| 118 // imageContainer only if there is an image to display, hiding the no-image | |
| 119 // icon. | |
| 120 @property(nonatomic, strong) UIImageView* contentImageView; | |
| 121 // Constraint for the size of the image. | |
| 122 @property(nonatomic, strong) NSLayoutConstraint* imageSize; | |
| 123 // Constraint for the distance between the texts and the image. | |
| 124 @property(nonatomic, strong) NSLayoutConstraint* imageTitleSpacing; | |
| 125 // Constraint for the vertical distance between the title and the subtitle. | |
| 126 @property(nonatomic, strong) NSLayoutConstraint* titleSubtitleSpacing; | |
| 127 // Label for the subtitle. | |
| 128 @property(nonatomic, readonly, strong) UILabel* subtitleLabel; | |
| 129 | |
| 130 // Applies the constraints on the elements. Called in the init. | |
| 131 - (void)applyConstraints; | |
| 132 | |
| 133 @end | |
| 134 | |
| 135 @implementation ContentSuggestionsCell | |
| 136 | |
| 137 @synthesize titleLabel = _titleLabel; | |
| 138 @synthesize subtitleLabel = _subtitleLabel; | |
| 139 @synthesize imageContainer = _imageContainer; | |
| 140 @synthesize noImageIcon = _noImageIcon; | |
| 141 @synthesize additionalInformationLabel = _additionalInformationLabel; | |
| 142 @synthesize contentImageView = _contentImageView; | |
| 143 @synthesize faviconView = _faviconView; | |
| 144 @synthesize imageSize = _imageSize; | |
| 145 @synthesize imageTitleSpacing = _imageTitleSpacing; | |
| 146 @synthesize displayImage = _displayImage; | |
| 147 @synthesize titleSubtitleSpacing = _titleSubtitleSpacing; | |
| 148 | |
| 149 - (instancetype)initWithFrame:(CGRect)frame { | |
| 150 self = [super initWithFrame:frame]; | |
| 151 if (self) { | |
| 152 _titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; | |
| 153 _subtitleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; | |
| 154 _imageContainer = [[UIView alloc] initWithFrame:CGRectZero]; | |
| 155 _noImageIcon = [[UIImageView alloc] initWithFrame:CGRectZero]; | |
| 156 _additionalInformationLabel = [[UILabel alloc] initWithFrame:CGRectZero]; | |
| 157 _contentImageView = [[UIImageView alloc] initWithFrame:CGRectZero]; | |
| 158 _faviconView = [[FaviconViewNew alloc] init]; | |
| 159 | |
| 160 _titleLabel.numberOfLines = 2; | |
| 161 _subtitleLabel.numberOfLines = 2; | |
| 162 [_subtitleLabel setContentHuggingPriority:UILayoutPriorityDefaultHigh | |
| 163 forAxis:UILayoutConstraintAxisVertical]; | |
| 164 [_titleLabel setContentHuggingPriority:UILayoutPriorityDefaultHigh | |
| 165 forAxis:UILayoutConstraintAxisVertical]; | |
| 166 | |
| 167 _contentImageView.contentMode = UIViewContentModeScaleAspectFill; | |
| 168 _contentImageView.clipsToBounds = YES; | |
| 169 _contentImageView.hidden = YES; | |
| 170 | |
| 171 _imageContainer.translatesAutoresizingMaskIntoConstraints = NO; | |
| 172 _noImageIcon.translatesAutoresizingMaskIntoConstraints = NO; | |
| 173 _titleLabel.translatesAutoresizingMaskIntoConstraints = NO; | |
| 174 _subtitleLabel.translatesAutoresizingMaskIntoConstraints = NO; | |
| 175 _additionalInformationLabel.translatesAutoresizingMaskIntoConstraints = NO; | |
| 176 _contentImageView.translatesAutoresizingMaskIntoConstraints = NO; | |
| 177 _faviconView.translatesAutoresizingMaskIntoConstraints = NO; | |
| 178 | |
| 179 [self.contentView addSubview:_imageContainer]; | |
| 180 [self.contentView addSubview:_titleLabel]; | |
| 181 [self.contentView addSubview:_subtitleLabel]; | |
| 182 [self.contentView addSubview:_additionalInformationLabel]; | |
| 183 [self.contentView addSubview:_faviconView]; | |
| 184 | |
| 185 [_imageContainer addSubview:_noImageIcon]; | |
| 186 [_imageContainer addSubview:_contentImageView]; | |
| 187 | |
| 188 _imageContainer.backgroundColor = | |
| 189 [UIColor colorWithWhite:kNoImageBackgroundWhite alpha:1]; | |
| 190 _noImageIcon.image = [[UIImage imageNamed:kNoImageIconName] | |
| 191 imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; | |
| 192 [_noImageIcon | |
| 193 setTintColor:[UIColor colorWithWhite:kNoImageIconWhite alpha:1]]; | |
| 194 | |
| 195 _titleLabel.font = [MDCTypography subheadFont]; | |
| 196 _subtitleLabel.font = [MDCTypography body1Font]; | |
| 197 _additionalInformationLabel.font = [MDCTypography captionFont]; | |
| 198 _faviconView.font = [[MDCTypography fontLoader] mediumFontOfSize:10]; | |
| 199 | |
| 200 _subtitleLabel.textColor = [[MDCPalette greyPalette] tint700]; | |
| 201 _additionalInformationLabel.textColor = [[MDCPalette greyPalette] tint700]; | |
| 202 | |
| 203 [self applyConstraints]; | |
| 204 } | |
| 205 return self; | |
| 206 } | |
| 207 | |
| 208 - (void)setContentImage:(UIImage*)image { | |
| 209 if (!image) { | |
| 210 self.contentImageView.hidden = YES; | |
| 211 return; | |
| 212 } | |
| 213 | |
| 214 self.contentImageView.image = image; | |
| 215 | |
| 216 self.contentImageView.alpha = 0; | |
| 217 self.contentImageView.hidden = NO; | |
| 218 | |
| 219 [UIView animateWithDuration:kAnimationDuration | |
| 220 animations:^{ | |
| 221 self.contentImageView.alpha = 1; | |
| 222 }]; | |
| 223 } | |
| 224 | |
| 225 - (void)setAdditionalInformationWithPublisherName:(NSString*)publisherName | |
| 226 date:(base::Time)publishDate | |
| 227 offlineAvailability:(BOOL)availableOffline { | |
| 228 NSDate* date = [NSDate dateWithTimeIntervalSince1970:publishDate.ToDoubleT()]; | |
| 229 NSString* dateString = | |
| 230 [NSDateFormatter localizedStringFromDate:date | |
| 231 dateStyle:NSDateFormatterMediumStyle | |
| 232 timeStyle:NSDateFormatterNoStyle]; | |
| 233 | |
| 234 NSString* publisherString = AdjustStringForLocaleDirection( | |
| 235 [NSString stringWithFormat:@"%@ - %@ ", publisherName, dateString]); | |
| 236 | |
| 237 NSMutableAttributedString* additionInformation = | |
| 238 [[NSMutableAttributedString alloc] initWithString:publisherString | |
| 239 attributes:nil]; | |
| 240 | |
| 241 if (availableOffline) { | |
| 242 NSTextAttachment* offlineIcon = [[NSTextAttachment alloc] init]; | |
| 243 offlineIcon.image = [[UIImage imageNamed:kOfflineIconName] | |
| 244 imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; | |
| 245 offlineIcon.bounds = CGRectMake( | |
| 246 0, (_additionalInformationLabel.font.xHeight - kOfflineIconSize) / 2, | |
| 247 kOfflineIconSize, kOfflineIconSize); | |
| 248 | |
| 249 [additionInformation | |
| 250 appendAttributedString:[NSAttributedString | |
| 251 attributedStringWithAttachment:offlineIcon]]; | |
| 252 } | |
| 253 | |
| 254 self.additionalInformationLabel.attributedText = additionInformation; | |
| 255 } | |
| 256 | |
| 257 - (void)setSubtitleText:(NSString*)subtitle { | |
| 258 self.subtitleLabel.text = subtitle; | |
| 259 if (subtitle.length > 0) { | |
| 260 self.titleSubtitleSpacing.constant = kSmallSpacing; | |
| 261 } else { | |
| 262 self.titleSubtitleSpacing.constant = 0; | |
| 263 } | |
| 264 } | |
| 265 | |
| 266 - (void)setDisplayImage:(BOOL)displayImage { | |
| 267 if (displayImage) { | |
| 268 self.imageTitleSpacing.constant = kStandardSpacing; | |
| 269 self.imageSize.constant = kImageSize; | |
| 270 self.imageContainer.hidden = NO; | |
| 271 } else { | |
| 272 self.imageTitleSpacing.constant = 0; | |
| 273 self.imageSize.constant = 0; | |
| 274 self.imageContainer.hidden = YES; | |
| 275 } | |
| 276 _displayImage = displayImage; | |
| 277 } | |
| 278 | |
| 279 #pragma mark - UICollectionViewCell | |
| 280 | |
| 281 - (void)prepareForReuse { | |
| 282 self.contentImageView.hidden = YES; | |
| 283 } | |
| 284 | |
| 285 #pragma mark - UIView | |
| 286 | |
| 287 // Implements -layoutSubviews as per instructions in documentation for | |
| 288 // +[MDCCollectionViewCell cr_preferredHeightForWidth:forItem:]. | |
| 289 - (void)layoutSubviews { | |
| 290 [super layoutSubviews]; | |
| 291 | |
| 292 // Adjust the text label preferredMaxLayoutWidth when the parent's width | |
| 293 // changes, for instance on screen rotation. | |
| 294 CGFloat parentWidth = CGRectGetWidth(self.contentView.bounds); | |
| 295 | |
| 296 CGFloat offset = 0; | |
| 297 if (self.displayImage) { | |
| 298 offset = kImageSize + kStandardSpacing; | |
| 299 } | |
| 300 | |
| 301 self.titleLabel.preferredMaxLayoutWidth = | |
| 302 parentWidth - 2 * kStandardSpacing - offset; | |
| 303 self.subtitleLabel.preferredMaxLayoutWidth = | |
| 304 parentWidth - 2 * kStandardSpacing - offset; | |
| 305 self.additionalInformationLabel.preferredMaxLayoutWidth = | |
| 306 parentWidth - kFaviconSize - kSmallSpacing - 2 * kStandardSpacing; | |
| 307 | |
| 308 // Re-layout with the new preferred width to allow the label to adjust its | |
| 309 // height. | |
| 310 [super layoutSubviews]; | |
| 311 } | |
| 312 | |
| 313 #pragma mark - Private | |
| 314 | |
| 315 - (void)applyConstraints { | |
| 316 _imageSize = | |
| 317 [_imageContainer.heightAnchor constraintEqualToConstant:kImageSize]; | |
| 318 _imageTitleSpacing = [_imageContainer.leadingAnchor | |
| 319 constraintEqualToAnchor:_titleLabel.trailingAnchor | |
| 320 constant:kStandardSpacing]; | |
| 321 _titleSubtitleSpacing = | |
| 322 [_subtitleLabel.topAnchor constraintEqualToAnchor:_titleLabel.bottomAnchor | |
| 323 constant:kSmallSpacing]; | |
| 324 | |
| 325 [NSLayoutConstraint activateConstraints:@[ | |
| 326 // Image. | |
| 327 _imageSize, | |
| 328 [_imageContainer.widthAnchor | |
| 329 constraintEqualToAnchor:_imageContainer.heightAnchor], | |
| 330 [_imageContainer.topAnchor constraintEqualToAnchor:_titleLabel.topAnchor], | |
| 331 | |
| 332 // Text. | |
| 333 _imageTitleSpacing, _titleSubtitleSpacing, | |
| 334 [_titleLabel.trailingAnchor | |
| 335 constraintEqualToAnchor:_subtitleLabel.trailingAnchor], | |
| 336 | |
| 337 // Additional Information. | |
| 338 [_additionalInformationLabel.topAnchor | |
| 339 constraintGreaterThanOrEqualToAnchor:_imageContainer.bottomAnchor | |
| 340 constant:kStandardSpacing], | |
| 341 [_additionalInformationLabel.topAnchor | |
| 342 constraintGreaterThanOrEqualToAnchor:_subtitleLabel.bottomAnchor | |
| 343 constant:kStandardSpacing], | |
| 344 [_additionalInformationLabel.bottomAnchor | |
| 345 constraintLessThanOrEqualToAnchor:self.contentView.bottomAnchor | |
| 346 constant:-kStandardSpacing], | |
| 347 | |
| 348 // Favicon. | |
| 349 [_faviconView.topAnchor | |
| 350 constraintGreaterThanOrEqualToAnchor:_imageContainer.bottomAnchor | |
| 351 constant:kStandardSpacing], | |
| 352 [_faviconView.topAnchor | |
| 353 constraintGreaterThanOrEqualToAnchor:_subtitleLabel.bottomAnchor | |
| 354 constant:kStandardSpacing], | |
| 355 [_faviconView.centerYAnchor | |
| 356 constraintEqualToAnchor:_additionalInformationLabel.centerYAnchor], | |
| 357 [_faviconView.bottomAnchor | |
| 358 constraintLessThanOrEqualToAnchor:self.contentView.bottomAnchor | |
| 359 constant:-kStandardSpacing], | |
| 360 [_faviconView.heightAnchor constraintEqualToConstant:kFaviconSize], | |
| 361 [_faviconView.widthAnchor | |
| 362 constraintEqualToAnchor:_faviconView.heightAnchor], | |
| 363 | |
| 364 // No image icon. | |
| 365 [_noImageIcon.centerXAnchor | |
| 366 constraintEqualToAnchor:_imageContainer.centerXAnchor], | |
| 367 [_noImageIcon.centerYAnchor | |
| 368 constraintEqualToAnchor:_imageContainer.centerYAnchor], | |
| 369 [_noImageIcon.widthAnchor constraintEqualToConstant:kIconSize], | |
| 370 [_noImageIcon.heightAnchor constraintEqualToAnchor:_noImageIcon.widthAnchor] | |
| 371 ]]; | |
| 372 | |
| 373 AddSameConstraints(_contentImageView, _imageContainer); | |
| 374 | |
| 375 ApplyVisualConstraintsWithMetrics( | |
| 376 @[ | |
| 377 @"H:|-(space)-[title]", | |
| 378 @"H:[image]-(space)-|", | |
| 379 @"H:|-(space)-[text]", | |
| 380 @"V:|-(space)-[title]", | |
| 381 @"H:|-(space)-[favicon]-(small)-[additional]-(space)-|", | |
| 382 ], | |
| 383 @{ | |
| 384 @"image" : _imageContainer, | |
| 385 @"title" : _titleLabel, | |
| 386 @"text" : _subtitleLabel, | |
| 387 @"additional" : _additionalInformationLabel, | |
| 388 @"favicon" : _faviconView, | |
| 389 }, | |
| 390 @{ @"space" : @(kStandardSpacing), | |
| 391 @"small" : @(kSmallSpacing) }); | |
| 392 } | |
| 393 | |
| 394 @end | |
| OLD | NEW |