// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/content_suggestions/set_up_list/set_up_list_item_icon.h"
#import "base/task/sequenced_task_runner.h"
#import "base/time/time.h"
#import "ios/chrome/browser/ntp/model/set_up_list_item_type.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
namespace {
// Constants related to icon sizing.
constexpr CGFloat kMagicStackIconSize = 30;
constexpr CGFloat kCompactIconSize = 26;
constexpr CGFloat kSymbolPointSize = 18;
constexpr CGFloat kSparkleSize = 60;
constexpr CGFloat kCompactSparkleSize = 52;
constexpr CGFloat kSparkleOffset = (kSparkleSize - kMagicStackIconSize) / 2;
constexpr CGFloat kCompactSparkleOffset =
(kCompactSparkleSize - kCompactIconSize) / 2;
constexpr CGFloat kIconSquareContainerRadius = 7.0f;
// The amount of rotation for the icons, during the animation.
constexpr CGFloat kAnimationRotation = 90 * M_PI / 180;
// Constants related to the sparkle animation frame images.
constexpr NSString* const kAnimationSparkleFrames = @"set_up_list_sparkle%d";
constexpr int kAnimationSparkleFrameCount = 36;
// Returns the size that the icons should be.
CGFloat IconSize(BOOL compact_layout) {
if (compact_layout) {
return kCompactIconSize;
}
return kMagicStackIconSize;
}
// Returns a UIImageView for the given SF Symbol, and with a color named
// `colorName`.
UIImageView* IconForSymbol(NSString* symbol,
BOOL compact_layout,
NSArray<UIColor*>* color_palette = nil) {
UIImageSymbolConfiguration* config = [UIImageSymbolConfiguration
configurationWithWeight:UIImageSymbolWeightLight];
if (color_palette) {
UIImageSymbolConfiguration* colorConfig = [UIImageSymbolConfiguration
configurationWithPaletteColors:color_palette];
config = [config configurationByApplyingConfiguration:colorConfig];
}
UIImage* image = DefaultSymbolWithConfiguration(symbol, config);
UIImageView* icon = [[UIImageView alloc] initWithImage:image];
icon.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[icon.widthAnchor constraintEqualToConstant:IconSize(compact_layout)],
[icon.heightAnchor constraintEqualToAnchor:icon.widthAnchor],
]];
return icon;
}
UIView* IconInSquareContainer(UIImageView* icon, NSString* color) {
UIView* square_view = [[UIView alloc] init];
square_view.translatesAutoresizingMaskIntoConstraints = NO;
square_view.layer.cornerRadius = kIconSquareContainerRadius;
square_view.backgroundColor = [UIColor colorNamed:color];
[square_view addSubview:icon];
AddSameCenterConstraints(icon, square_view);
[NSLayoutConstraint activateConstraints:@[
[square_view.widthAnchor constraintEqualToConstant:IconSize(NO)],
[square_view.heightAnchor constraintEqualToAnchor:square_view.widthAnchor],
]];
return square_view;
}
UIImageView* DefaultBrowserIcon(BOOL compact_layout) {
#if BUILDFLAG(IOS_USE_BRANDED_SYMBOLS)
UIImage* image = MakeSymbolMulticolor(
CustomSymbolWithPointSize(kMulticolorChromeballSymbol, kSymbolPointSize));
UIImageView* icon = [[UIImageView alloc] initWithImage:image];
icon.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[icon.widthAnchor constraintEqualToConstant:IconSize(compact_layout)],
[icon.heightAnchor constraintEqualToAnchor:icon.widthAnchor],
]];
return icon;
#else
return IconForSymbol(kDefaultBrowserSymbol, compact_layout);
#endif
}
// Returns a UIImageView containing the given symbol enclosed in a filled
// circle. The circle's color will be the color named `circleColorName`.
// Note: this was necessary because there was no symbol exactly matching
// what was needed for the Autofill icon.
UIImageView* IconInCircle(NSString* symbol,
BOOL compact_layout,
NSString* circle_color_name) {
UIImageView* circle_view = IconForSymbol(kCircleFillSymbol, compact_layout);
circle_view.tintColor = [UIColor colorNamed:circle_color_name];
UIImageConfiguration* compactImageConfiguration = [UIImageSymbolConfiguration
configurationWithPointSize:kSymbolPointSize
weight:UIImageSymbolWeightLight
scale:UIImageSymbolScaleSmall];
UIImage* symbol_image =
compact_layout
? DefaultSymbolWithConfiguration(symbol, compactImageConfiguration)
: DefaultSymbolWithPointSize(symbol, kSymbolPointSize);
CHECK(symbol_image);
UIImageView* symbol_view = [[UIImageView alloc] initWithImage:symbol_image];
symbol_view.tintColor = [UIColor whiteColor];
symbol_view.translatesAutoresizingMaskIntoConstraints = NO;
[circle_view addSubview:symbol_view];
AddSameCenterConstraints(symbol_view, circle_view);
return circle_view;
}
UIView* IconInSquare(NSString* symbol,
BOOL compact_layout,
NSString* color_name) {
UIImageConfiguration* compactImageConfiguration = [UIImageSymbolConfiguration
configurationWithPointSize:kSymbolPointSize
weight:UIImageSymbolWeightLight
scale:UIImageSymbolScaleSmall];
UIView* square_view = [[UIView alloc] init];
square_view.translatesAutoresizingMaskIntoConstraints = NO;
square_view.layer.cornerRadius = kIconSquareContainerRadius;
square_view.backgroundColor = [UIColor colorNamed:color_name];
UIImage* symbol_image =
compact_layout
? DefaultSymbolWithConfiguration(symbol, compactImageConfiguration)
: DefaultSymbolWithPointSize(symbol, kSymbolPointSize);
CHECK(symbol_image);
UIImageView* symbol_view = [[UIImageView alloc] initWithImage:symbol_image];
symbol_view.tintColor = [UIColor whiteColor];
symbol_view.translatesAutoresizingMaskIntoConstraints = NO;
[square_view addSubview:symbol_view];
AddSameCenterConstraints(symbol_view, square_view);
[NSLayoutConstraint activateConstraints:@[
[square_view.widthAnchor constraintEqualToConstant:IconSize(NO)],
[square_view.heightAnchor constraintEqualToAnchor:square_view.widthAnchor],
]];
return square_view;
}
} // namespace
@implementation SetUpListItemIcon {
SetUpListItemType _type;
BOOL _complete;
UIView* _typeIcon;
UIImageView* _checkmark;
UIImageView* _sparkle;
// YES if this view should configure itself with a compacted layout.
BOOL _compactLayout;
// YES if this view should place the icon in a square shape.
BOOL _inSquare;
}
- (instancetype)initWithType:(SetUpListItemType)type
complete:(BOOL)complete
compactLayout:(BOOL)compactLayout
inSquare:(BOOL)inSquare {
self = [super init];
if (self) {
_type = type;
_complete = complete;
_compactLayout = compactLayout;
_inSquare = inSquare;
}
return self;
}
#pragma mark - UIView
- (void)willMoveToSuperview:(UIView*)newSuperview {
[super willMoveToSuperview:newSuperview];
[self createSubviews];
}
#pragma mark - Public methods
- (void)markComplete {
if (_complete) {
return;
}
_complete = YES;
_checkmark.transform = CGAffineTransformMakeRotation(0);
_typeIcon.transform = CGAffineTransformMakeRotation(kAnimationRotation);
_checkmark.alpha = 1;
_typeIcon.alpha = 0;
}
- (void)playSparkleWithDuration:(base::TimeDelta)duration
delay:(base::TimeDelta)delay {
// Load the sparkle animation frame images.
NSMutableArray<UIImage*>* images = [[NSMutableArray alloc] init];
for (int i = 0; i < kAnimationSparkleFrameCount; i++) {
NSString* name = [NSString stringWithFormat:kAnimationSparkleFrames, i];
[images addObject:[UIImage imageNamed:name]];
}
_sparkle.animationImages = images;
_sparkle.animationDuration = duration.InSecondsF();
_sparkle.animationRepeatCount = 1;
// Start animating after the given `delay`.
__weak __typeof(_sparkle) weakSparkle = _sparkle;
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, base::BindOnce(^{
[weakSparkle startAnimating];
}),
delay);
}
#pragma mark - Private methods
// Creates all the subviews: the type-specific icon, the checkmark, and the
// sparkle image view.
- (void)createSubviews {
// Return if the subviews have already been created and added.
if (!(self.subviews.count == 0)) {
return;
}
self.tintAdjustmentMode = UIViewTintAdjustmentModeNormal;
_typeIcon = [self createTypeIcon];
_checkmark = IconForSymbol(
kCheckmarkCircleFillSymbol, _compactLayout,
@[ [UIColor whiteColor], [UIColor colorNamed:kBlue500Color] ]);
_sparkle = [self createSparkle];
[self addSubview:_typeIcon];
[self addSubview:_checkmark];
[self addSubview:_sparkle];
AddSameConstraints(_checkmark, _typeIcon);
AddSameConstraints(self, _typeIcon);
if (_complete) {
_typeIcon.alpha = 0;
} else {
_checkmark.alpha = 0;
_checkmark.transform = CGAffineTransformMakeRotation(-kAnimationRotation);
}
}
// Creates the type-specific icon for this item.
- (UIView*)createTypeIcon {
switch (_type) {
case SetUpListItemType::kSignInSync: {
return _inSquare ? IconInSquare(kPersonCropCircleSymbol, _compactLayout,
kGreen500Color)
: IconInCircle(kPersonCropCircleSymbol, _compactLayout,
kGreen500Color);
}
case SetUpListItemType::kDefaultBrowser: {
UIImageView* iconImage = DefaultBrowserIcon(_compactLayout || _inSquare);
if (_inSquare) {
return IconInSquareContainer(iconImage, kBackgroundColor);
}
return iconImage;
}
case SetUpListItemType::kAutofill: {
return _inSquare
? IconInSquare(kEllipsisRectangleSymbol, NO, kBlue500Color)
: IconInCircle(kEllipsisRectangleSymbol, _compactLayout,
kBlue500Color);
}
case SetUpListItemType::kNotifications: {
return _inSquare ? IconInSquare(kBellBadgeSymbol, NO, kPink500Color)
: IconInCircle(kBellBadgeSymbol, _compactLayout,
kPink500Color);
}
case SetUpListItemType::kAllSet: {
return IconForSymbol(
kCheckmarkSealFillSymbol, _compactLayout,
@[ [UIColor whiteColor], [UIColor colorNamed:kBlue500Color] ]);
}
case SetUpListItemType::kFollow:
// TODO(crbug.com/40262090): Add a Follow item to the Set Up List.
NOTREACHED_IN_MIGRATION();
return nil;
}
}
// Creates the image view that plays the "sparkle" animation.
- (UIImageView*)createSparkle {
// This image view does not initially have an image. The animation frames
// are loaded on demand.
UIImageView* imageView = [[UIImageView alloc] initWithImage:nil];
CGFloat offset = _compactLayout ? kCompactSparkleOffset : kSparkleOffset;
CGFloat size = _compactLayout ? kCompactSparkleSize : kSparkleSize;
imageView.frame = CGRectMake(-offset, -offset, size, size);
return imageView;
}
@end