1// Copyright 2019 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/scanner/scanner_view.h" 6 7#include "base/check.h" 8#include "base/mac/foundation_util.h" 9#include "base/numerics/math_constants.h" 10#include "ios/chrome/browser/ui/icons/chrome_icon.h" 11#import "ios/chrome/browser/ui/scanner/preview_overlay_view.h" 12#import "ios/chrome/browser/ui/scanner/video_preview_view.h" 13#include "ios/chrome/browser/ui/util/ui_util.h" 14#import "ios/chrome/common/ui/util/constraints_ui_util.h" 15#include "ios/chrome/grit/ios_strings.h" 16#include "ui/base/l10n/l10n_util.h" 17#include "ui/base/l10n/l10n_util_mac.h" 18 19#if !defined(__has_feature) || !__has_feature(objc_arc) 20#error "This file requires ARC support." 21#endif 22 23namespace { 24 25// Padding of the viewport caption, below the viewport. 26const CGFloat kViewportCaptionVerticalPadding = 14.0; 27// Padding of the viewport caption from the edges of the superview. 28const CGFloat kViewportCaptionHorizontalPadding = 31.0; 29// Shadow opacity of the viewport caption. 30const CGFloat kViewportCaptionShadowOpacity = 1.0; 31// Shadow radius of the viewport caption. 32const CGFloat kViewportCaptionShadowRadius = 5.0; 33 34// Duration of the flash animation played when a code is scanned. 35const CGFloat kFlashDuration = 0.5; 36 37} // namespace 38 39@interface ScannerView () { 40 // A button to toggle the torch. 41 UIBarButtonItem* _torchButton; 42 // A view containing the preview layer for camera input. 43 VideoPreviewView* _previewView; 44 // A transparent overlay on top of the preview layer. 45 PreviewOverlayView* _previewOverlay; 46 // The constraint specifying that the preview overlay should be square. 47 NSLayoutConstraint* _overlaySquareConstraint; 48 // The constraint relating the size of the |_previewOverlay| to the width of 49 // the ScannerView. 50 NSLayoutConstraint* _overlayWidthConstraint; 51 // The constraint relating the size of the |_previewOverlay| to the height of 52 // te ScannerView. 53 NSLayoutConstraint* _overlayHeightConstraint; 54} 55 56@end 57 58@implementation ScannerView 59 60#pragma mark - lifecycle 61 62- (instancetype)initWithFrame:(CGRect)frame 63 delegate:(id<ScannerViewDelegate>)delegate { 64 self = [super initWithFrame:frame]; 65 if (!self) { 66 return nil; 67 } 68 DCHECK(delegate); 69 _delegate = delegate; 70 return self; 71} 72 73#pragma mark - UIView 74 75// TODO(crbug.com/633577): Replace the preview overlay with a UIView which is 76// not resized. 77- (void)layoutSubviews { 78 [super layoutSubviews]; 79 [self setBackgroundColor:[UIColor blackColor]]; 80 if (CGRectEqualToRect([_previewView bounds], CGRectZero)) { 81 [_previewView setBounds:self.bounds]; 82 } 83 [_previewView setCenter:CGPointMake(CGRectGetMidX(self.bounds), 84 CGRectGetMidY(self.bounds))]; 85} 86 87- (void)willMoveToSuperview:(UIView*)superview { 88 // Set up subviews if they don't already exist. 89 if (superview && self.subviews.count == 0) { 90 [self setupPreviewView]; 91 [self setupPreviewOverlayView]; 92 [self addSubviews]; 93 } 94} 95 96#pragma mark - public methods 97 98- (AVCaptureVideoPreviewLayer*)previewLayer { 99 return [_previewView previewLayer]; 100} 101 102- (void)enableTorchButton:(BOOL)torchIsAvailable { 103 [_torchButton setEnabled:torchIsAvailable]; 104 if (!torchIsAvailable) { 105 [self setTorchButtonTo:NO]; 106 } 107} 108 109- (void)setTorchButtonTo:(BOOL)torchIsOn { 110 DCHECK(_torchButton); 111 UIImage* icon = nil; 112 NSString* accessibilityValue = nil; 113 if (torchIsOn) { 114 icon = [self torchOnIcon]; 115 accessibilityValue = 116 l10n_util::GetNSString(IDS_IOS_SCANNER_TORCH_ON_ACCESSIBILITY_VALUE); 117 } else { 118 icon = [self torchOffIcon]; 119 accessibilityValue = 120 l10n_util::GetNSString(IDS_IOS_SCANNER_TORCH_OFF_ACCESSIBILITY_VALUE); 121 } 122 [_torchButton setImage:icon]; 123 [_torchButton setAccessibilityValue:accessibilityValue]; 124} 125 126- (void)resetPreviewFrame:(CGSize)size { 127 [_previewView setTransform:CGAffineTransformIdentity]; 128 [_previewView setFrame:CGRectMake(0, 0, size.width, size.height)]; 129} 130 131- (void)rotatePreviewByAngle:(CGFloat)angle { 132 [_previewView 133 setTransform:CGAffineTransformRotate([_previewView transform], angle)]; 134} 135 136- (void)finishPreviewRotation { 137 CGAffineTransform rotation = [_previewView transform]; 138 // Check that the current transform is either an identity or a 90, -90, or 180 139 // degree rotation. 140 DCHECK(fabs(atan2f(rotation.b, rotation.a)) < 0.001 || 141 fabs(fabs(atan2f(rotation.b, rotation.a)) - base::kPiFloat) < 0.001 || 142 fabs(fabs(atan2f(rotation.b, rotation.a)) - base::kPiFloat / 2) < 143 0.001); 144 rotation.a = round(rotation.a); 145 rotation.b = round(rotation.b); 146 rotation.c = round(rotation.c); 147 rotation.d = round(rotation.d); 148 [_previewView setTransform:rotation]; 149} 150 151- (CGRect)viewportRegionOfInterest { 152 return [_previewView viewportRegionOfInterest]; 153} 154 155- (CGRect)viewportRectOfInterest { 156 return [_previewView viewportRectOfInterest]; 157} 158 159- (void)animateScanningResultWithCompletion:(void (^)(void))completion { 160 UIView* whiteView = [[UIView alloc] init]; 161 whiteView.frame = self.bounds; 162 [self addSubview:whiteView]; 163 whiteView.backgroundColor = [UIColor whiteColor]; 164 [UIView animateWithDuration:kFlashDuration 165 animations:^{ 166 whiteView.alpha = 0.0; 167 } 168 completion:^void(BOOL finished) { 169 [whiteView removeFromSuperview]; 170 if (completion) { 171 completion(); 172 } 173 }]; 174} 175 176- (CGSize)viewportSize { 177 return self.window.frame.size; 178} 179 180- (NSString*)caption { 181 return @""; 182} 183 184#pragma mark - private methods 185 186// Creates an image with template rendering mode for use in icons. 187- (UIImage*)templateImageWithName:(NSString*)name { 188 UIImage* image = [[UIImage imageNamed:name] 189 imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; 190 DCHECK(image); 191 return image; 192} 193 194// Creates an icon for torch turned on. 195- (UIImage*)torchOnIcon { 196 UIImage* icon = [self templateImageWithName:@"scanner_torch_on"]; 197 return icon; 198} 199 200// Creates an icon for torch turned off. 201- (UIImage*)torchOffIcon { 202 UIImage* icon = [self templateImageWithName:@"scanner_torch_off"]; 203 return icon; 204} 205 206// Adds the subviews. 207- (void)addSubviews { 208 UIBarButtonItem* close = 209 [[UIBarButtonItem alloc] initWithImage:[ChromeIcon closeIcon] 210 style:UIBarButtonItemStylePlain 211 target:_delegate 212 action:@selector(dismissScannerView:)]; 213 close.accessibilityLabel = [[ChromeIcon closeIcon] accessibilityLabel]; 214 UIBarButtonItem* spacer = [[UIBarButtonItem alloc] 215 initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace 216 target:nil 217 action:nil]; 218 _torchButton = 219 [[UIBarButtonItem alloc] initWithImage:[self torchOffIcon] 220 style:UIBarButtonItemStylePlain 221 target:_delegate 222 action:@selector(toggleTorch:)]; 223 _torchButton.enabled = NO; 224 _torchButton.accessibilityIdentifier = @"scanner_torch_button"; 225 _torchButton.accessibilityLabel = 226 l10n_util::GetNSString(IDS_IOS_SCANNER_TORCH_BUTTON_ACCESSIBILITY_LABEL); 227 _torchButton.accessibilityValue = 228 l10n_util::GetNSString(IDS_IOS_SCANNER_TORCH_OFF_ACCESSIBILITY_VALUE); 229 UIToolbar* toolbar = [[UIToolbar alloc] init]; 230 toolbar.items = @[ close, spacer, _torchButton ]; 231 toolbar.tintColor = UIColor.whiteColor; 232 [toolbar setBackgroundImage:[[UIImage alloc] init] 233 forToolbarPosition:UIToolbarPositionAny 234 barMetrics:UIBarMetricsDefault]; 235 [toolbar setShadowImage:[[UIImage alloc] init] 236 forToolbarPosition:UIBarPositionAny]; 237 238 [toolbar setBackgroundColor:[UIColor clearColor]]; 239 toolbar.translatesAutoresizingMaskIntoConstraints = NO; 240 [self addSubview:toolbar]; 241 242 AddSameConstraintsToSides(self, toolbar, 243 LayoutSides::kLeading | LayoutSides::kTrailing); 244 [toolbar.bottomAnchor 245 constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor] 246 .active = YES; 247 248 UILabel* viewportCaption = [[UILabel alloc] init]; 249 NSString* label = [self caption]; 250 [viewportCaption setText:label]; 251 [viewportCaption 252 setFont:[UIFont preferredFontForTextStyle:UIFontTextStyleBody]]; 253 [viewportCaption setAdjustsFontForContentSizeCategory:YES]; 254 [viewportCaption setNumberOfLines:0]; 255 [viewportCaption setTextAlignment:NSTextAlignmentCenter]; 256 [viewportCaption setAccessibilityLabel:label]; 257 [viewportCaption setAccessibilityIdentifier:@"scanner_viewport_caption"]; 258 [viewportCaption setTextColor:[UIColor whiteColor]]; 259 [viewportCaption.layer setShadowColor:[UIColor blackColor].CGColor]; 260 [viewportCaption.layer setShadowOffset:CGSizeZero]; 261 [viewportCaption.layer setShadowRadius:kViewportCaptionShadowRadius]; 262 [viewportCaption.layer setShadowOpacity:kViewportCaptionShadowOpacity]; 263 [viewportCaption.layer setMasksToBounds:NO]; 264 [viewportCaption.layer setShouldRasterize:YES]; 265 266 UIScrollView* scrollView = [[UIScrollView alloc] init]; 267 scrollView.showsVerticalScrollIndicator = NO; 268 [self addSubview:scrollView]; 269 [scrollView addSubview:viewportCaption]; 270 271 // Constraints for viewportCaption. 272 scrollView.translatesAutoresizingMaskIntoConstraints = NO; 273 viewportCaption.translatesAutoresizingMaskIntoConstraints = NO; 274 275 [NSLayoutConstraint activateConstraints:@[ 276 [scrollView.topAnchor 277 constraintEqualToAnchor:self.centerYAnchor 278 constant:[self viewportSize].height / 2 + 279 kViewportCaptionVerticalPadding], 280 [scrollView.bottomAnchor constraintEqualToAnchor:toolbar.topAnchor], 281 [scrollView.leadingAnchor 282 constraintEqualToAnchor:self.leadingAnchor 283 constant:kViewportCaptionHorizontalPadding], 284 [viewportCaption.leadingAnchor 285 constraintEqualToAnchor:self.leadingAnchor 286 constant:kViewportCaptionHorizontalPadding], 287 [scrollView.trailingAnchor 288 constraintEqualToAnchor:self.trailingAnchor 289 constant:-kViewportCaptionHorizontalPadding], 290 [viewportCaption.trailingAnchor 291 constraintEqualToAnchor:self.trailingAnchor 292 constant:-kViewportCaptionHorizontalPadding], 293 ]]; 294 AddSameConstraints(scrollView, viewportCaption); 295} 296 297// Adds a preview view to |self| and configures its layout constraints. 298- (void)setupPreviewView { 299 DCHECK(!_previewView); 300 _previewView = [[VideoPreviewView alloc] initWithFrame:self.frame 301 viewportSize:[self viewportSize]]; 302 [self insertSubview:_previewView atIndex:0]; 303} 304 305// Adds a transparent overlay with a viewport border to |self| and configures 306// its layout constraints. 307- (void)setupPreviewOverlayView { 308 DCHECK(!_previewOverlay); 309 _previewOverlay = 310 [[PreviewOverlayView alloc] initWithFrame:CGRectZero 311 viewportSize:[self viewportSize]]; 312 [self addSubview:_previewOverlay]; 313 314 // Add a multiplier of sqrt(2) to the width and height constraints to make 315 // sure that the overlay covers the whole screen during rotation. 316 _overlayWidthConstraint = 317 [NSLayoutConstraint constraintWithItem:_previewOverlay 318 attribute:NSLayoutAttributeWidth 319 relatedBy:NSLayoutRelationGreaterThanOrEqual 320 toItem:self 321 attribute:NSLayoutAttributeWidth 322 multiplier:sqrt(2) 323 constant:0.0]; 324 325 _overlayHeightConstraint = 326 [NSLayoutConstraint constraintWithItem:_previewOverlay 327 attribute:NSLayoutAttributeHeight 328 relatedBy:NSLayoutRelationGreaterThanOrEqual 329 toItem:self 330 attribute:NSLayoutAttributeHeight 331 multiplier:sqrt(2) 332 constant:0.0]; 333 334 _overlaySquareConstraint = [[_previewOverlay heightAnchor] 335 constraintEqualToAnchor:[_previewOverlay widthAnchor]]; 336 337 // Constrains the preview overlay to be square, centered, with both width and 338 // height greater than or equal to the width and height of the ScannerView. 339 [_previewOverlay setTranslatesAutoresizingMaskIntoConstraints:NO]; 340 [NSLayoutConstraint activateConstraints:@[ 341 [[_previewOverlay centerXAnchor] 342 constraintEqualToAnchor:[self centerXAnchor]], 343 [[_previewOverlay centerYAnchor] 344 constraintEqualToAnchor:[self centerYAnchor]], 345 _overlaySquareConstraint, _overlayWidthConstraint, _overlayHeightConstraint 346 ]]; 347} 348 349@end 350