1// Copyright 2015 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "ios/chrome/browser/ui/first_run/welcome_to_chrome_view.h" 6 7#import <MaterialComponents/MaterialTypography.h> 8 9#include "base/check_op.h" 10#include "base/i18n/rtl.h" 11#include "base/notreached.h" 12#include "base/strings/sys_string_conversions.h" 13#import "ios/chrome/browser/ui/elements/text_view_selection_disabled.h" 14#include "ios/chrome/browser/ui/fancy_ui/primary_action_button.h" 15#include "ios/chrome/browser/ui/first_run/first_run_util.h" 16#import "ios/chrome/browser/ui/util/CRUILabel+AttributeUtils.h" 17#import "ios/chrome/browser/ui/util/label_link_controller.h" 18#import "ios/chrome/browser/ui/util/label_observer.h" 19#include "ios/chrome/browser/ui/util/ui_util.h" 20#import "ios/chrome/browser/ui/util/uikit_ui_util.h" 21#include "ios/chrome/common/string_util.h" 22#import "ios/chrome/common/ui/colors/semantic_color_names.h" 23#include "ios/chrome/grit/ios_chromium_strings.h" 24#include "ios/chrome/grit/ios_strings.h" 25#include "ui/base/l10n/l10n_util.h" 26#include "url/gurl.h" 27 28#if !defined(__has_feature) || !__has_feature(objc_arc) 29#error "This file requires ARC support." 30#endif 31 32namespace { 33 34// An enum type to describe size classes. 35typedef NS_ENUM(NSInteger, SizeClassIdiom) { 36 COMPACT = 0, 37 REGULAR, 38 UNSPECIFIED, 39 SIZE_CLASS_COUNT = UNSPECIFIED, 40}; 41 42// Returns the SizeClassIdiom corresponding with |size_class|. 43SizeClassIdiom GetSizeClassIdiom(UIUserInterfaceSizeClass size_class) { 44 switch (size_class) { 45 case UIUserInterfaceSizeClassCompact: 46 return COMPACT; 47 case UIUserInterfaceSizeClassRegular: 48 return REGULAR; 49 case UIUserInterfaceSizeClassUnspecified: 50 return UNSPECIFIED; 51 } 52} 53 54// Accessibility identifier for the checkbox button. 55NSString* const kUMAMetricsButtonAccessibilityIdentifier = 56 @"UMAMetricsButtonAccessibilityIdentifier"; 57 58// The width of the container view for a REGULAR width size class. 59const CGFloat kContainerViewRegularWidth = 510.0; 60 61// The percentage of the view's width taken up by the container view for a 62// COMPACT width size class. 63const CGFloat kContainerViewCompactWidthPercentage = 0.8; 64 65// Layout constants. 66const CGFloat kImageTopPadding[SIZE_CLASS_COUNT] = {32.0, 50.0}; 67const CGFloat kTOSTextViewTopPadding[SIZE_CLASS_COUNT] = {34.0, 40.0}; 68const CGFloat kOptInLabelPadding[SIZE_CLASS_COUNT] = {10.0, 14.0}; 69const CGFloat kCheckBoxPadding[SIZE_CLASS_COUNT] = {10.0, 16.0}; 70const CGFloat kOKButtonBottomPadding[SIZE_CLASS_COUNT] = {32.0, 32.0}; 71const CGFloat kOKButtonHeight[SIZE_CLASS_COUNT] = {36.0, 54.0}; 72// Multiplier matches that used in LaunchScreen.xib to determine size of logo. 73const CGFloat kAppLogoProportionMultiplier = 0.381966; 74 75// Font sizes. 76const CGFloat kTitleLabelFontSize[SIZE_CLASS_COUNT] = {24.0, 36.0}; 77const CGFloat kTOSTOSTextViewFontSize[SIZE_CLASS_COUNT] = {14.0, 21.0}; 78const CGFloat kLegacyTOSLabelLineHeight[SIZE_CLASS_COUNT] = {20.0, 32.0}; 79const CGFloat kOptInLabelFontSize[SIZE_CLASS_COUNT] = {13.0, 19.0}; 80const CGFloat kOptInLabelLineHeight[SIZE_CLASS_COUNT] = {18.0, 26.0}; 81const CGFloat kOKButtonTitleLabelFontSize[SIZE_CLASS_COUNT] = {14.0, 20.0}; 82 83// Animation constants 84const CGFloat kAnimationDuration = .4; 85// Delay animation to avoid interaction with launch screen fadeout. 86const CGFloat kAnimationDelay = .5; 87 88// Image names. 89NSString* const kAppLogoImageName = @"launchscreen_app_logo"; 90NSString* const kCheckBoxImageName = @"checkbox"; 91NSString* const kCheckBoxCheckedImageName = @"checkbox_checked"; 92 93// Constant for the Terms of Service URL in the first run experience. 94const char kTermsOfServiceUrl[] = "internal://terms-of-service"; 95 96} // namespace 97 98@interface WelcomeToChromeView () <UITextViewDelegate> { 99 UIView* _containerView; 100 UILabel* _titleLabel; 101 UIImageView* _imageView; 102 UIButton* _checkBoxButton; 103 UILabel* _optInLabel; 104 PrimaryActionButton* _OKButton; 105 106 // Used for iOS 12 compatibility. 107 UILabel* _legacyTOSLabel; 108 LabelLinkController* _legacyTOSLabelLinkController; 109} 110 111// Subview properties are lazily instantiated upon their first use. 112 113// The "Terms of Service" legacy label used for iOS 12 compatibility. 114@property(strong, nonatomic, readonly) UILabel* legacyTOSLabel; 115// Legacy observer for setting the size of the TOSLabel with cr_lineHeight used 116// for iOS 12 compatibility. 117@property(strong, nonatomic) LabelObserver* legacyTOSObserver; 118 119// A container view used to layout and center subviews. 120@property(strong, nonatomic, readonly) UIView* containerView; 121// The "Welcome to Chrome" label that appears at the top of the view. 122@property(strong, nonatomic, readonly) UILabel* titleLabel; 123// The Chrome logo image view. 124@property(strong, nonatomic, readonly) UIImageView* imageView; 125// The "Terms of Service" text view. 126@property(strong, nonatomic) TextViewSelectionDisabled* TOSTextView; 127// The stats reporting opt-in label. 128@property(strong, nonatomic, readonly) UILabel* optInLabel; 129// Observer for setting the size of the optInLabel with cr_lineHeight. 130@property(strong, nonatomic) LabelObserver* optInObserver; 131// The stats reporting opt-in checkbox button. 132@property(strong, nonatomic, readonly) UIButton* checkBoxButton; 133// The "Accept & Continue" button. 134@property(strong, nonatomic, readonly) PrimaryActionButton* OKButton; 135 136// Subview layout methods. They must be called in the order declared here, as 137// subsequent subview layouts depend on the layouts that precede them. 138- (void)layoutTitleLabel; 139- (void)layoutImageView; 140- (void)layoutTOSTextView; 141- (void)layoutOptInLabel; 142- (void)layoutCheckBoxButton; 143- (void)layoutContainerView; 144- (void)layoutOKButton; 145 146// Calls the subview configuration selectors below. 147- (void)configureSubviews; 148 149// Subview configuration methods. 150- (void)configureTitleLabel; 151- (void)configureImageView; 152- (void)configureTOSTextView; 153- (void)configureOptInLabel; 154- (void)configureContainerView; 155- (void)configureOKButton; 156 157// Action triggered by the check box button. 158- (void)checkBoxButtonWasTapped; 159 160// Action triggered by the ok button. 161- (void)OKButtonWasTapped; 162 163@end 164 165@implementation WelcomeToChromeView 166 167- (instancetype)initWithFrame:(CGRect)frame { 168 self = [super initWithFrame:frame]; 169 if (self) { 170 self.backgroundColor = [UIColor colorNamed:kBackgroundColor]; 171 self.autoresizingMask = 172 UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 173 } 174 return self; 175} 176 177- (void)runLaunchAnimation { 178 // Prepare for animation by making views (except for the logo) transparent 179 // and finding the initial and final location of the logo. 180 self.titleLabel.alpha = 0.0; 181 self.TOSTextView.alpha = 0.0; 182 self.legacyTOSLabel.alpha = 0.0; 183 self.optInLabel.alpha = 0.0; 184 self.checkBoxButton.alpha = 0.0; 185 self.OKButton.alpha = 0.0; 186 187 // Get final location of logo based on result from previously run 188 // layoutSubviews. 189 CGRect finalLogoFrame = self.imageView.frame; 190 // Ensure that frame position is valid and that layoutSubviews ran 191 // before this method. 192 DCHECK(finalLogoFrame.origin.x >= 0 && finalLogoFrame.origin.y >= 0); 193 self.imageView.center = CGPointMake(CGRectGetMidX(self.containerView.bounds), 194 CGRectGetMidY(self.containerView.bounds)); 195 196 __weak WelcomeToChromeView* weakSelf = self; 197 [UIView animateWithDuration:kAnimationDuration 198 delay:kAnimationDelay 199 options:UIViewAnimationCurveEaseInOut 200 animations:^{ 201 [weakSelf imageView].frame = finalLogoFrame; 202 [weakSelf titleLabel].alpha = 1.0; 203 [weakSelf TOSTextView].alpha = 1.0; 204 [weakSelf legacyTOSLabel].alpha = 1.0; 205 [weakSelf optInLabel].alpha = 1.0; 206 [weakSelf checkBoxButton].alpha = 1.0; 207 [weakSelf OKButton].alpha = 1.0; 208 } 209 completion:nil]; 210} 211 212- (void)dealloc { 213 [self.legacyTOSObserver stopObserving]; 214 215 [self.optInObserver stopObserving]; 216} 217 218#pragma mark - Accessors 219 220- (BOOL)isCheckBoxSelected { 221 return self.checkBoxButton.selected; 222} 223 224- (void)setCheckBoxSelected:(BOOL)checkBoxSelected { 225 if (checkBoxSelected != self.checkBoxButton.selected) 226 [self checkBoxButtonWasTapped]; 227} 228 229- (UIView*)containerView { 230 if (!_containerView) { 231 _containerView = [[UIView alloc] initWithFrame:CGRectZero]; 232 } 233 return _containerView; 234} 235 236- (UILabel*)titleLabel { 237 if (!_titleLabel) { 238 _titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 239 [_titleLabel setNumberOfLines:0]; 240 [_titleLabel setLineBreakMode:NSLineBreakByWordWrapping]; 241 [_titleLabel setBaselineAdjustment:UIBaselineAdjustmentAlignBaselines]; 242 [_titleLabel 243 setText:l10n_util::GetNSString(IDS_IOS_FIRSTRUN_WELCOME_TO_CHROME)]; 244 } 245 return _titleLabel; 246} 247 248- (UIImageView*)imageView { 249 if (!_imageView) { 250 UIImage* image = [UIImage imageNamed:kAppLogoImageName]; 251 _imageView = [[UIImageView alloc] initWithImage:image]; 252 } 253 return _imageView; 254} 255 256- (TextViewSelectionDisabled*)TOSTextView { 257 if (!_TOSTextView) { 258 _TOSTextView = [[TextViewSelectionDisabled alloc] initWithFrame:CGRectZero]; 259 } 260 return _TOSTextView; 261} 262 263- (UILabel*)legacyTOSLabel { 264 if (!_legacyTOSLabel) { 265 _legacyTOSLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 266 // Add an observer to the label to be able to keep the cr_lineHeight. 267 self.legacyTOSObserver = [LabelObserver observerForLabel:_legacyTOSLabel]; 268 [self.legacyTOSObserver startObserving]; 269 270 [_legacyTOSLabel setNumberOfLines:0]; 271 [_legacyTOSLabel setTextAlignment:NSTextAlignmentCenter]; 272 } 273 return _legacyTOSLabel; 274} 275 276- (UILabel*)optInLabel { 277 if (!_optInLabel) { 278 _optInLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 279 // Add an observer to the label to be able to keep the cr_lineHeight. 280 self.optInObserver = [LabelObserver observerForLabel:_optInLabel]; 281 [self.optInObserver startObserving]; 282 283 [_optInLabel setNumberOfLines:0]; 284 [_optInLabel 285 setText:l10n_util::GetNSString(IDS_IOS_FIRSTRUN_NEW_OPT_IN_LABEL)]; 286 [_optInLabel setTextAlignment:NSTextAlignmentNatural]; 287 } 288 return _optInLabel; 289} 290 291- (UIButton*)checkBoxButton { 292 if (!_checkBoxButton) { 293 _checkBoxButton = [[UIButton alloc] initWithFrame:CGRectZero]; 294 [_checkBoxButton setBackgroundColor:[UIColor clearColor]]; 295 [_checkBoxButton addTarget:self 296 action:@selector(checkBoxButtonWasTapped) 297 forControlEvents:UIControlEventTouchUpInside]; 298 SetA11yLabelAndUiAutomationName(_checkBoxButton, 299 IDS_IOS_FIRSTRUN_NEW_OPT_IN_LABEL, 300 kUMAMetricsButtonAccessibilityIdentifier); 301 [_checkBoxButton 302 setAccessibilityValue:l10n_util::GetNSString(IDS_IOS_SETTING_OFF)]; 303 [_checkBoxButton setImage:[UIImage imageNamed:kCheckBoxImageName] 304 forState:UIControlStateNormal]; 305 UIImage* selectedImage = [[UIImage imageNamed:kCheckBoxCheckedImageName] 306 imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; 307 [_checkBoxButton setImage:selectedImage forState:UIControlStateSelected]; 308 } 309 return _checkBoxButton; 310} 311 312- (PrimaryActionButton*)OKButton { 313 if (!_OKButton) { 314 _OKButton = [[PrimaryActionButton alloc] initWithFrame:CGRectZero]; 315 [_OKButton addTarget:self 316 action:@selector(OKButtonWasTapped) 317 forControlEvents:UIControlEventTouchUpInside]; 318 NSString* acceptAndContinue = 319 l10n_util::GetNSString(IDS_IOS_FIRSTRUN_OPT_IN_ACCEPT_BUTTON); 320 [_OKButton setTitle:acceptAndContinue forState:UIControlStateNormal]; 321 [_OKButton setTitle:acceptAndContinue forState:UIControlStateHighlighted]; 322 // UIAutomation tests look for the Accept button to skip through the 323 // First Run UI when it shows up. 324 SetA11yLabelAndUiAutomationName( 325 _OKButton, IDS_IOS_FIRSTRUN_OPT_IN_ACCEPT_BUTTON, @"Accept & Continue"); 326 } 327 return _OKButton; 328} 329 330#pragma mark - Layout 331 332- (void)willMoveToSuperview:(nullable UIView*)newSuperview { 333 [super willMoveToSuperview:newSuperview]; 334 335 // Early return if the view hierarchy is already built. 336 if (self.containerView.superview) { 337 DCHECK_EQ(self, self.containerView.superview); 338 return; 339 } 340 341 [self addSubview:self.containerView]; 342 [self.containerView addSubview:self.titleLabel]; 343 [self.containerView addSubview:self.imageView]; 344 if (@available(iOS 13.5, *)) { 345 [self.containerView addSubview:self.TOSTextView]; 346 } else { 347 [self.containerView addSubview:self.legacyTOSLabel]; 348 } 349 [self.containerView addSubview:self.optInLabel]; 350 [self.containerView addSubview:self.checkBoxButton]; 351 [self addSubview:self.OKButton]; 352 [self configureSubviews]; 353} 354 355- (void)safeAreaInsetsDidChange { 356 [super safeAreaInsetsDidChange]; 357 [self layoutOKButtonAndContainerView]; 358} 359 360- (void)layoutSubviews { 361 [super layoutSubviews]; 362 [self layoutTitleLabel]; 363 [self layoutImageView]; 364 if (@available(iOS 13.5, *)) { 365 [self layoutTOSTextView]; 366 } else { 367 [self layoutLegacyTOSLabel]; 368 } 369 [self layoutOptInLabel]; 370 [self layoutCheckBoxButton]; 371 [self layoutOKButtonAndContainerView]; 372} 373 374- (void)layoutOKButtonAndContainerView { 375 // The OK Button must be laid out before the container view so that the 376 // container view can take its position into account. 377 [self layoutOKButton]; 378 [self layoutContainerView]; 379} 380 381- (void)layoutTitleLabel { 382 // The label is centered and top-aligned with the container view. 383 CGSize containerSize = self.containerView.bounds.size; 384 containerSize.height = CGFLOAT_MAX; 385 CGSize titleLabelSize = [self.titleLabel sizeThatFits:containerSize]; 386 self.titleLabel.frame = AlignRectOriginAndSizeToPixels( 387 CGRectMake((containerSize.width - titleLabelSize.width) / 2.0, 0.0, 388 titleLabelSize.width, titleLabelSize.height)); 389} 390 391- (void)layoutImageView { 392 // The image is centered and laid out below |titleLabel| as specified by 393 // kImageTopPadding. 394 CGSize imageViewSize = self.imageView.bounds.size; 395 CGFloat imageViewTopPadding = kImageTopPadding[[self heightSizeClassIdiom]]; 396 self.imageView.frame = AlignRectOriginAndSizeToPixels(CGRectMake( 397 (CGRectGetWidth(self.containerView.bounds) - imageViewSize.width) / 2.0, 398 CGRectGetMaxY(self.titleLabel.frame) + imageViewTopPadding, 399 imageViewSize.width, imageViewSize.height)); 400} 401 402- (void)layoutTOSTextView { 403 // The TOSTextView is centered and laid out below |imageView| as specified by 404 // kTOSTextViewTopPadding. 405 CGSize containerSize = self.containerView.bounds.size; 406 containerSize.height = CGFLOAT_MAX; 407 CGSize TOSTextViewSize = [self.TOSTextView sizeThatFits:containerSize]; 408 CGFloat TOSTextViewTopPadding = 409 kTOSTextViewTopPadding[[self heightSizeClassIdiom]]; 410 CGRect frame = 411 CGRectMake((containerSize.width - TOSTextViewSize.width) / 2.0, 412 CGRectGetMaxY(self.imageView.frame) + TOSTextViewTopPadding, 413 TOSTextViewSize.width, TOSTextViewSize.height); 414 self.TOSTextView.frame = AlignRectOriginAndSizeToPixels(frame); 415} 416 417- (void)layoutLegacyTOSLabel { 418 // The TOS label is centered and laid out below |imageView| as specified by 419 // kTOSLabelTopPadding. 420 CGSize containerSize = self.containerView.bounds.size; 421 containerSize.height = CGFLOAT_MAX; 422 self.legacyTOSLabel.frame = {CGPointZero, containerSize}; 423 NSString* TOSText = l10n_util::GetNSString(IDS_IOS_FIRSTRUN_AGREE_TO_TERMS); 424 NSRange tosLinkTextRange = NSMakeRange(NSNotFound, 0); 425 TOSText = ParseStringWithTag(TOSText, &tosLinkTextRange, 426 @"BEGIN_LINK_TOS[ \t]*", @"[ \t]*END_LINK_TOS"); 427 428 DCHECK_NE(NSNotFound, static_cast<NSInteger>(tosLinkTextRange.location)); 429 DCHECK_NE(0u, tosLinkTextRange.length); 430 431 self.legacyTOSLabel.text = TOSText; 432 433 __weak WelcomeToChromeView* weakSelf = self; 434 ProceduralBlockWithURL action = ^(const GURL& url) { 435 WelcomeToChromeView* strongSelf = weakSelf; 436 if (!strongSelf) 437 return; 438 if (url == kTermsOfServiceUrl) { 439 [[strongSelf delegate] welcomeToChromeViewDidTapTOSLink]; 440 } else { 441 NOTREACHED(); 442 } 443 }; 444 445 _legacyTOSLabelLinkController = 446 [[LabelLinkController alloc] initWithLabel:_legacyTOSLabel action:action]; 447 [_legacyTOSLabelLinkController addLinkWithRange:tosLinkTextRange 448 url:GURL(kTermsOfServiceUrl)]; 449 [_legacyTOSLabelLinkController setLinkColor:[UIColor colorNamed:kBlueColor]]; 450 451 CGSize TOSLabelSize = [self.legacyTOSLabel sizeThatFits:containerSize]; 452 CGFloat TOSLabelTopPadding = 453 kTOSTextViewTopPadding[[self heightSizeClassIdiom]]; 454 self.legacyTOSLabel.frame = AlignRectOriginAndSizeToPixels( 455 CGRectMake((containerSize.width - TOSLabelSize.width) / 2.0, 456 CGRectGetMaxY(self.imageView.frame) + TOSLabelTopPadding, 457 TOSLabelSize.width, TOSLabelSize.height)); 458} 459 460- (void)layoutOptInLabel { 461 // The opt in label is laid out to the right (or left in RTL) of the check box 462 // button and below |TOSLabel| as specified by kOptInLabelPadding. 463 CGSize checkBoxSize = 464 [self.checkBoxButton imageForState:self.checkBoxButton.state].size; 465 CGFloat checkBoxPadding = kCheckBoxPadding[[self widthSizeClassIdiom]]; 466 CGFloat optInLabelSidePadding = checkBoxSize.width + 2.0 * checkBoxPadding; 467 CGSize optInLabelSize = [self.optInLabel 468 sizeThatFits:CGSizeMake(CGRectGetWidth(self.containerView.bounds) - 469 optInLabelSidePadding, 470 CGFLOAT_MAX)]; 471 CGFloat optInLabelTopPadding = 472 kOptInLabelPadding[[self heightSizeClassIdiom]]; 473 CGFloat optInLabelOriginX = 474 base::i18n::IsRTL() ? 0.0f : optInLabelSidePadding; 475 if (@available(iOS 13.5, *)) { 476 self.optInLabel.frame = AlignRectOriginAndSizeToPixels( 477 CGRectMake(optInLabelOriginX, 478 CGRectGetMaxY(self.TOSTextView.frame) + optInLabelTopPadding, 479 optInLabelSize.width, optInLabelSize.height)); 480 } else { 481 self.optInLabel.frame = AlignRectOriginAndSizeToPixels(CGRectMake( 482 optInLabelOriginX, 483 CGRectGetMaxY(self.legacyTOSLabel.frame) + optInLabelTopPadding, 484 optInLabelSize.width, optInLabelSize.height)); 485 } 486} 487 488- (void)layoutCheckBoxButton { 489 // The checkBoxButton is laid out to the left of |optInLabel|. The view 490 // itself is sized so that it covers the label, and the image insets are 491 // chosen such that the check box image is centered vertically with 492 // |optInLabel|. 493 CGSize checkBoxSize = 494 [self.checkBoxButton imageForState:self.checkBoxButton.state].size; 495 CGFloat checkBoxPadding = kCheckBoxPadding[[self widthSizeClassIdiom]]; 496 CGSize checkBoxButtonSize = 497 CGSizeMake(CGRectGetWidth(self.optInLabel.frame) + checkBoxSize.width + 498 2.0 * checkBoxPadding, 499 std::max(CGRectGetHeight(self.optInLabel.frame), 500 checkBoxSize.height + 2.0f * checkBoxPadding)); 501 self.checkBoxButton.frame = AlignRectOriginAndSizeToPixels(CGRectMake( 502 0.0f, 503 CGRectGetMidY(self.optInLabel.frame) - checkBoxButtonSize.height / 2.0, 504 checkBoxButtonSize.width, checkBoxButtonSize.height)); 505 CGFloat largeHorizontalInset = 506 checkBoxButtonSize.width - checkBoxSize.width - checkBoxPadding; 507 CGFloat smallHorizontalInset = checkBoxPadding; 508 self.checkBoxButton.imageEdgeInsets = UIEdgeInsetsMake( 509 (checkBoxButtonSize.height - checkBoxSize.height) / 2.0, 510 base::i18n::IsRTL() ? largeHorizontalInset : smallHorizontalInset, 511 (checkBoxButtonSize.height - checkBoxSize.height) / 2.0, 512 base::i18n::IsRTL() ? smallHorizontalInset : largeHorizontalInset); 513} 514 515- (void)layoutContainerView { 516 // The container view is resized according to the final layout of 517 // |checkBoxButton|, which is its lowest subview. The resized view is then 518 // centered horizontally and vertically. If necessary, it is shifted up to 519 // allow |kOptInLabelPadding| between |optInLabel| and |OKButton|. 520 CGSize containerViewSize = self.containerView.bounds.size; 521 containerViewSize.height = CGRectGetMaxY(self.checkBoxButton.frame); 522 523 CGFloat padding = kOptInLabelPadding[[self heightSizeClassIdiom]]; 524 CGFloat originY = fmin( 525 (CGRectGetHeight(self.bounds) - containerViewSize.height) / 2.0, 526 CGRectGetMinY(self.OKButton.frame) - padding - containerViewSize.height); 527 528 self.containerView.frame = AlignRectOriginAndSizeToPixels(CGRectMake( 529 (CGRectGetWidth(self.bounds) - containerViewSize.width) / 2.0, originY, 530 containerViewSize.width, CGRectGetMaxY(self.checkBoxButton.frame))); 531} 532 533- (void)layoutOKButton { 534 // The OK button is laid out at the bottom of the view as specified by 535 // kOKButtonBottomPadding. 536 CGFloat OKButtonBottomPadding = 537 kOKButtonBottomPadding[[self widthSizeClassIdiom]]; 538 CGSize OKButtonSize = self.OKButton.bounds.size; 539 CGFloat bottomSafeArea = self.safeAreaInsets.bottom; 540 self.OKButton.frame = AlignRectOriginAndSizeToPixels( 541 CGRectMake((CGRectGetWidth(self.bounds) - OKButtonSize.width) / 2.0, 542 CGRectGetMaxY(self.bounds) - OKButtonSize.height - 543 OKButtonBottomPadding - bottomSafeArea, 544 OKButtonSize.width, OKButtonSize.height)); 545} 546 547- (void)traitCollectionDidChange: 548 (nullable UITraitCollection*)previousTraitCollection { 549 [super traitCollectionDidChange:previousTraitCollection]; 550 [self configureSubviews]; 551} 552 553- (void)configureSubviews { 554 [self configureContainerView]; 555 [self configureTitleLabel]; 556 [self configureImageView]; 557 if (@available(iOS 13.5, *)) { 558 [self configureTOSTextView]; 559 } else { 560 [self configureLegacyTOSLabel]; 561 } 562 [self configureOptInLabel]; 563 [self configureOKButton]; 564 [self setNeedsLayout]; 565} 566 567- (void)configureTitleLabel { 568 self.titleLabel.font = [[MDCTypography fontLoader] 569 regularFontOfSize:kTitleLabelFontSize[[self widthSizeClassIdiom]]]; 570} 571 572- (void)configureImageView { 573 CGFloat sideLength = self.imageView.image.size.width; 574 if ([self widthSizeClassIdiom] == COMPACT) { 575 sideLength = self.bounds.size.width * kAppLogoProportionMultiplier; 576 } else if ([self heightSizeClassIdiom] == COMPACT) { 577 sideLength = self.bounds.size.height * kAppLogoProportionMultiplier; 578 } 579 self.imageView.bounds = AlignRectOriginAndSizeToPixels( 580 CGRectMake(self.imageView.bounds.origin.x, self.imageView.bounds.origin.y, 581 sideLength, sideLength)); 582} 583 584- (void)configureTOSTextView { 585 self.TOSTextView.scrollEnabled = NO; 586 self.TOSTextView.editable = NO; 587 self.TOSTextView.adjustsFontForContentSizeCategory = YES; 588 self.TOSTextView.delegate = self; 589 self.TOSTextView.backgroundColor = UIColor.clearColor; 590 self.TOSTextView.linkTextAttributes = 591 @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]}; 592 593 NSString* TOSText = l10n_util::GetNSString(IDS_IOS_FIRSTRUN_AGREE_TO_TERMS); 594 NSRange tosLinkTextRange = NSMakeRange(NSNotFound, 0); 595 TOSText = ParseStringWithTag(TOSText, &tosLinkTextRange, 596 @"BEGIN_LINK_TOS[ \t]*", @"[ \t]*END_LINK_TOS"); 597 598 DCHECK_NE(NSNotFound, static_cast<NSInteger>(tosLinkTextRange.location)); 599 DCHECK_NE(0u, tosLinkTextRange.length); 600 601 NSRange fullRange = NSMakeRange(0, TOSText.length); 602 NSURL* URL = 603 [NSURL URLWithString:base::SysUTF8ToNSString(kTermsOfServiceUrl)]; 604 UIFont* font = [[MDCTypography fontLoader] 605 regularFontOfSize:kTOSTOSTextViewFontSize[[self widthSizeClassIdiom]]]; 606 NSMutableParagraphStyle* style = 607 [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; 608 style.alignment = NSTextAlignmentCenter; 609 610 NSMutableAttributedString* attributedText = 611 [[NSMutableAttributedString alloc] initWithString:TOSText]; 612 [attributedText addAttributes:@{ 613 NSForegroundColorAttributeName : [UIColor colorNamed:kTextPrimaryColor], 614 NSParagraphStyleAttributeName : style, 615 NSFontAttributeName : font 616 } 617 range:fullRange]; 618 [attributedText addAttribute:NSLinkAttributeName 619 value:URL 620 range:tosLinkTextRange]; 621 622 self.TOSTextView.attributedText = attributedText; 623} 624 625- (void)configureOptInLabel { 626 self.optInLabel.font = [[MDCTypography fontLoader] 627 regularFontOfSize:kOptInLabelFontSize[[self widthSizeClassIdiom]]]; 628 self.optInLabel.cr_lineHeight = 629 kOptInLabelLineHeight[[self widthSizeClassIdiom]]; 630} 631 632- (void)configureLegacyTOSLabel { 633 self.legacyTOSLabel.font = [[MDCTypography fontLoader] 634 regularFontOfSize:kTOSTOSTextViewFontSize[[self widthSizeClassIdiom]]]; 635 self.legacyTOSLabel.cr_lineHeight = 636 kLegacyTOSLabelLineHeight[[self widthSizeClassIdiom]]; 637} 638 639- (void)configureContainerView { 640 CGFloat containerViewWidth = 641 [self widthSizeClassIdiom] == COMPACT 642 ? kContainerViewCompactWidthPercentage * CGRectGetWidth(self.bounds) 643 : kContainerViewRegularWidth; 644 self.containerView.frame = 645 CGRectMake(0.0, 0.0, containerViewWidth, CGFLOAT_MAX); 646} 647 648- (void)configureOKButton { 649 UIFont* font = [[MDCTypography fontLoader] 650 mediumFontOfSize:kOKButtonTitleLabelFontSize[[self widthSizeClassIdiom]]]; 651 [self.OKButton setTitleFont:font forState:UIControlStateNormal]; 652 CGSize size = [self.OKButton 653 sizeThatFits:CGSizeMake(CGFLOAT_MAX, 654 kOKButtonHeight[[self widthSizeClassIdiom]])]; 655 [self.OKButton 656 setBounds:CGRectMake(0, 0, size.width, 657 kOKButtonHeight[[self widthSizeClassIdiom]])]; 658} 659 660- (SizeClassIdiom)widthSizeClassIdiom { 661 UIWindow* keyWindow = [UIApplication sharedApplication].keyWindow; 662 UIUserInterfaceSizeClass sizeClass = self.traitCollection.horizontalSizeClass; 663 if (sizeClass == UIUserInterfaceSizeClassUnspecified) 664 sizeClass = keyWindow.traitCollection.horizontalSizeClass; 665 return GetSizeClassIdiom(sizeClass); 666} 667 668- (SizeClassIdiom)heightSizeClassIdiom { 669 UIWindow* keyWindow = [UIApplication sharedApplication].keyWindow; 670 UIUserInterfaceSizeClass sizeClass = self.traitCollection.verticalSizeClass; 671 if (sizeClass == UIUserInterfaceSizeClassUnspecified) 672 sizeClass = keyWindow.traitCollection.verticalSizeClass; 673 return GetSizeClassIdiom(sizeClass); 674} 675 676#pragma mark - 677 678- (void)checkBoxButtonWasTapped { 679 self.checkBoxButton.selected = !self.checkBoxButton.selected; 680 self.checkBoxButton.accessibilityValue = 681 self.checkBoxButton.selected 682 ? l10n_util::GetNSString(IDS_IOS_SETTING_ON) 683 : l10n_util::GetNSString(IDS_IOS_SETTING_OFF); 684} 685 686- (void)OKButtonWasTapped { 687 [self.delegate welcomeToChromeViewDidTapOKButton:self]; 688} 689 690#pragma mark - UITextViewDelegate 691 692- (BOOL)textView:(TextViewSelectionDisabled*)textView 693 shouldInteractWithURL:(NSURL*)URL 694 inRange:(NSRange)characterRange 695 interaction:(UITextItemInteraction)interaction { 696 DCHECK(textView == self.TOSTextView); 697 DCHECK(GURL(base::SysNSStringToUTF8(URL.absoluteString)) == 698 kTermsOfServiceUrl); 699 [self.delegate welcomeToChromeViewDidTapTOSLink]; 700 // Returns NO as the app is handling the opening of the URL. 701 return NO; 702} 703 704@end 705