chromium/ios/chrome/browser/ui/scanner/scanner_view.mm

// Copyright 2019 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/scanner/scanner_view.h"

#import <numbers>

#import "base/apple/foundation_util.h"
#import "base/check.h"
#import "base/numerics/math_constants.h"
#import "ios/chrome/browser/shared/ui/symbols/chrome_icon.h"
#import "ios/chrome/browser/ui/scanner/preview_overlay_view.h"
#import "ios/chrome/browser/ui/scanner/video_preview_view.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"

namespace {

// Padding of the viewport caption, below the viewport.
const CGFloat kViewportCaptionVerticalPadding = 14.0;
// Padding of the viewport caption from the edges of the superview.
const CGFloat kViewportCaptionHorizontalPadding = 31.0;
// Shadow opacity of the viewport caption.
const CGFloat kViewportCaptionShadowOpacity = 1.0;
// Shadow radius of the viewport caption.
const CGFloat kViewportCaptionShadowRadius = 5.0;

// Duration of the flash animation played when a code is scanned.
const CGFloat kFlashDuration = 0.5;

}  // namespace

@interface ScannerView () {
  // A scrollview containing the viewport's caption.
  UIScrollView* _captionContainer;
  // A button to toggle the torch.
  UIBarButtonItem* _torchButton;
  // A view containing the preview layer for camera input.
  VideoPreviewView* _previewView;
  // A transparent overlay on top of the preview layer.
  PreviewOverlayView* _previewOverlay;
  // The constraint specifying that the preview overlay should be square.
  NSLayoutConstraint* _overlaySquareConstraint;
  // The constraint relating the size of the `_previewOverlay` to the width of
  // the ScannerView.
  NSLayoutConstraint* _overlayWidthConstraint;
  // The constraint relating the size of the `_previewOverlay` to the height of
  // te ScannerView.
  NSLayoutConstraint* _overlayHeightConstraint;
}

@end

@implementation ScannerView

#pragma mark - lifecycle

- (instancetype)initWithFrame:(CGRect)frame
                     delegate:(id<ScannerViewDelegate>)delegate {
  self = [super initWithFrame:frame];
  if (!self) {
    return nil;
  }
  DCHECK(delegate);
  _delegate = delegate;
  return self;
}

#pragma mark - UIView

// TODO(crbug.com/40478852): Replace the preview overlay with a UIView which is
// not resized.
- (void)layoutSubviews {
  [super layoutSubviews];
  [self setBackgroundColor:[UIColor blackColor]];
  if (CGRectEqualToRect([_previewView bounds], CGRectZero)) {
    [_previewView setBounds:self.bounds];
  }
  [_previewView setCenter:CGPointMake(CGRectGetMidX(self.bounds),
                                      CGRectGetMidY(self.bounds))];
}

- (void)willMoveToSuperview:(UIView*)superview {
  // Set up subviews if they don't already exist.
  if (superview && self.subviews.count == 0) {
    [self setupPreviewView];
    [self setupPreviewOverlayView];
    [self addSubviews];
  }
}

#pragma mark - public methods

- (AVCaptureVideoPreviewLayer*)previewLayer {
  return [_previewView previewLayer];
}

- (void)enableTorchButton:(BOOL)torchIsAvailable {
  [_torchButton setEnabled:torchIsAvailable];
  if (!torchIsAvailable) {
    [self setTorchButtonTo:NO];
  }
}

- (void)setTorchButtonTo:(BOOL)torchIsOn {
  DCHECK(_torchButton);
  UIImage* icon = nil;
  NSString* accessibilityValue = nil;
  if (torchIsOn) {
    icon = [self torchOnIcon];
    accessibilityValue =
        l10n_util::GetNSString(IDS_IOS_SCANNER_TORCH_ON_ACCESSIBILITY_VALUE);
  } else {
    icon = [self torchOffIcon];
    accessibilityValue =
        l10n_util::GetNSString(IDS_IOS_SCANNER_TORCH_OFF_ACCESSIBILITY_VALUE);
  }
  [_torchButton setImage:icon];
  [_torchButton setAccessibilityValue:accessibilityValue];
}

- (void)resetPreviewFrame:(CGSize)size {
  [_previewView setTransform:CGAffineTransformIdentity];
  [_previewView setFrame:CGRectMake(0, 0, size.width, size.height)];
}

- (void)rotatePreviewByAngle:(CGFloat)angle {
  [_previewView
      setTransform:CGAffineTransformRotate([_previewView transform], angle)];
}

- (void)finishPreviewRotation {
  CGAffineTransform rotation = [_previewView transform];
  // Check that the current transform is either an identity or a 90, -90, or 180
  // degree rotation.
  DCHECK(fabs(atan2f(rotation.b, rotation.a)) < 0.001 ||
         fabs(fabs(atan2f(rotation.b, rotation.a)) -
              std::numbers::pi_v<float>) < 0.001 ||
         fabs(fabs(atan2f(rotation.b, rotation.a)) -
              std::numbers::pi_v<float> / 2) < 0.001);
  rotation.a = round(rotation.a);
  rotation.b = round(rotation.b);
  rotation.c = round(rotation.c);
  rotation.d = round(rotation.d);
  [_previewView setTransform:rotation];
}

- (CGRect)viewportRegionOfInterest {
  return [_previewView viewportRegionOfInterest];
}

- (CGRect)viewportRectOfInterest {
  return [_previewView viewportRectOfInterest];
}

- (void)animateScanningResultWithCompletion:(void (^)(void))completion {
  UIView* whiteView = [[UIView alloc] init];
  whiteView.frame = self.bounds;
  [self addSubview:whiteView];
  whiteView.backgroundColor = [UIColor whiteColor];
  [UIView animateWithDuration:kFlashDuration
      animations:^{
        whiteView.alpha = 0.0;
      }
      completion:^void(BOOL finished) {
        [whiteView removeFromSuperview];
        if (completion) {
          completion();
        }
      }];
}

- (CGSize)viewportSize {
  return self.window.frame.size;
}

- (NSString*)caption {
  return @"";
}

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];
  _captionContainer.hidden =
      UIUserInterfaceSizeClassCompact == self.traitCollection.verticalSizeClass;
}

#pragma mark - private methods

// Creates an image with template rendering mode for use in icons.
- (UIImage*)templateImageWithName:(NSString*)name {
  UIImage* image = [[UIImage imageNamed:name]
      imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
  DCHECK(image);
  return image;
}

// Creates an icon for torch turned on.
- (UIImage*)torchOnIcon {
  UIImage* icon = [self templateImageWithName:@"scanner_torch_on"];
  return icon;
}

// Creates an icon for torch turned off.
- (UIImage*)torchOffIcon {
  UIImage* icon = [self templateImageWithName:@"scanner_torch_off"];
  return icon;
}

// Adds the subviews.
- (void)addSubviews {
  UIBarButtonItem* close =
      [[UIBarButtonItem alloc] initWithImage:[ChromeIcon closeIcon]
                                       style:UIBarButtonItemStylePlain
                                      target:_delegate
                                      action:@selector(dismissScannerView:)];
  close.accessibilityLabel = [[ChromeIcon closeIcon] accessibilityLabel];
  UIBarButtonItem* spacer = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
                           target:nil
                           action:nil];
  _torchButton =
      [[UIBarButtonItem alloc] initWithImage:[self torchOffIcon]
                                       style:UIBarButtonItemStylePlain
                                      target:_delegate
                                      action:@selector(toggleTorch:)];
  _torchButton.enabled = NO;
  _torchButton.accessibilityIdentifier = @"scanner_torch_button";
  _torchButton.accessibilityLabel =
      l10n_util::GetNSString(IDS_IOS_SCANNER_TORCH_BUTTON_ACCESSIBILITY_LABEL);
  _torchButton.accessibilityValue =
      l10n_util::GetNSString(IDS_IOS_SCANNER_TORCH_OFF_ACCESSIBILITY_VALUE);
  UIToolbar* toolbar = [[UIToolbar alloc] init];
  toolbar.items = @[ close, spacer, _torchButton ];
  toolbar.tintColor = UIColor.whiteColor;
  [toolbar setBackgroundImage:[[UIImage alloc] init]
           forToolbarPosition:UIToolbarPositionAny
                   barMetrics:UIBarMetricsDefault];
  [toolbar setShadowImage:[[UIImage alloc] init]
       forToolbarPosition:UIBarPositionAny];

  [toolbar setBackgroundColor:[UIColor clearColor]];
  toolbar.translatesAutoresizingMaskIntoConstraints = NO;
  [self addSubview:toolbar];

  AddSameConstraintsToSides(self, toolbar,
                            LayoutSides::kLeading | LayoutSides::kTrailing);
  [toolbar.bottomAnchor
      constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor]
      .active = YES;

  UILabel* viewportCaption = [[UILabel alloc] init];
  NSString* label = [self caption];
  [viewportCaption setText:label];
  [viewportCaption
      setFont:[UIFont preferredFontForTextStyle:UIFontTextStyleBody]];
  [viewportCaption setAdjustsFontForContentSizeCategory:YES];
  [viewportCaption setNumberOfLines:0];
  [viewportCaption setTextAlignment:NSTextAlignmentCenter];
  [viewportCaption setAccessibilityLabel:label];
  [viewportCaption setAccessibilityIdentifier:@"scanner_viewport_caption"];
  [viewportCaption setTextColor:[UIColor whiteColor]];
  [viewportCaption.layer setShadowColor:[UIColor blackColor].CGColor];
  [viewportCaption.layer setShadowOffset:CGSizeZero];
  [viewportCaption.layer setShadowRadius:kViewportCaptionShadowRadius];
  [viewportCaption.layer setShadowOpacity:kViewportCaptionShadowOpacity];
  [viewportCaption.layer setMasksToBounds:NO];
  [viewportCaption.layer setShouldRasterize:YES];

  _captionContainer = [[UIScrollView alloc] init];
  _captionContainer.showsVerticalScrollIndicator = NO;
  [self addSubview:_captionContainer];
  [_captionContainer addSubview:viewportCaption];

  // Constraints for viewportCaption.
  _captionContainer.translatesAutoresizingMaskIntoConstraints = NO;
  viewportCaption.translatesAutoresizingMaskIntoConstraints = NO;

  [NSLayoutConstraint activateConstraints:@[
    [_captionContainer.topAnchor
        constraintEqualToAnchor:self.centerYAnchor
                       constant:self.viewportSize.height / 2 +
                                kViewportCaptionVerticalPadding],
    [_captionContainer.bottomAnchor constraintEqualToAnchor:toolbar.topAnchor],
    [_captionContainer.centerXAnchor
        constraintEqualToAnchor:_previewOverlay.centerXAnchor],
    [_captionContainer.contentLayoutGuide.widthAnchor
        constraintEqualToAnchor:_captionContainer.widthAnchor],
    [_captionContainer.widthAnchor
        constraintLessThanOrEqualToAnchor:self.widthAnchor
                                 constant:-2 *
                                          kViewportCaptionHorizontalPadding],
    [_captionContainer.widthAnchor
        constraintLessThanOrEqualToAnchor:self.heightAnchor
                                 constant:-2 *
                                          kViewportCaptionHorizontalPadding],
  ]];
  AddSameConstraints(_captionContainer, viewportCaption);

  // There is no space for at least 1 line of text in compact height.
  _captionContainer.hidden =
      UIUserInterfaceSizeClassCompact == self.traitCollection.verticalSizeClass;
}

// Adds a preview view to `self` and configures its layout constraints.
- (void)setupPreviewView {
  DCHECK(!_previewView);
  _previewView = [[VideoPreviewView alloc] initWithFrame:self.frame
                                            viewportSize:[self viewportSize]];
  [self insertSubview:_previewView atIndex:0];
}

// Adds a transparent overlay with a viewport border to `self` and configures
// its layout constraints.
- (void)setupPreviewOverlayView {
  DCHECK(!_previewOverlay);
  _previewOverlay =
      [[PreviewOverlayView alloc] initWithFrame:CGRectZero
                                   viewportSize:[self viewportSize]];
  [self addSubview:_previewOverlay];

  // Add a multiplier of sqrt(2) to the width and height constraints to make
  // sure that the overlay covers the whole screen during rotation.
  _overlayWidthConstraint =
      [NSLayoutConstraint constraintWithItem:_previewOverlay
                                   attribute:NSLayoutAttributeWidth
                                   relatedBy:NSLayoutRelationGreaterThanOrEqual
                                      toItem:self
                                   attribute:NSLayoutAttributeWidth
                                  multiplier:sqrt(2)
                                    constant:0.0];

  _overlayHeightConstraint =
      [NSLayoutConstraint constraintWithItem:_previewOverlay
                                   attribute:NSLayoutAttributeHeight
                                   relatedBy:NSLayoutRelationGreaterThanOrEqual
                                      toItem:self
                                   attribute:NSLayoutAttributeHeight
                                  multiplier:sqrt(2)
                                    constant:0.0];

  _overlaySquareConstraint = [[_previewOverlay heightAnchor]
      constraintEqualToAnchor:[_previewOverlay widthAnchor]];

  // Constrains the preview overlay to be square, centered, with both width and
  // height greater than or equal to the width and height of the ScannerView.
  [_previewOverlay setTranslatesAutoresizingMaskIntoConstraints:NO];
  [NSLayoutConstraint activateConstraints:@[
    [[_previewOverlay centerXAnchor]
        constraintEqualToAnchor:[self centerXAnchor]],
    [[_previewOverlay centerYAnchor]
        constraintEqualToAnchor:[self centerYAnchor]],
    _overlaySquareConstraint, _overlayWidthConstraint, _overlayHeightConstraint
  ]];
}

@end