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