blob: 4c351480912ecd724b164c70a7894a0329a52eb7 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// 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/qr_scanner/qr_scanner_view.h"
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/numerics/math_constants.h"
#include "ios/chrome/browser/ui/icons/chrome_icon.h"
#include "ios/chrome/browser/ui/util/ui_util.h"
#import "ios/chrome/common/ui_util/constraints_ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Width and height of the QR scanner viewport.
const CGFloat kViewportSize_iPhone = 250.0;
const CGFloat kViewportSize_iPad = 300.0;
// Length of the viewport borders, starting from the corner.
const CGFloat kViewportBorderCornerWidth_iPhone = 25.0;
const CGFloat kViewportBorderCornerWidth_iPad = 30.0;
// Opacity of the preview overlay.
const CGFloat kPreviewOverlayOpacity = 0.5;
// Corner radius of the border around the viewport.
const CGFloat kViewportBorderCornerRadius = 2.0;
// Line width of the viewport border.
const CGFloat kViewportBorderLineWidth = 4.0;
// Shadow opacity of the viewport border.
const CGFloat kViewportBorderShadowOpacity = 1.0;
// Shadow radius of the viewport border.
const CGFloat kViewportBorderShadowRadius = 10.0;
// 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;
// Returns a square of size |rectSize| centered inside |frameSize|.
CGRect CenteredRectForViewport(CGSize frameSize, CGFloat rectSize) {
CGFloat rectX = AlignValueToPixel((frameSize.width - rectSize) / 2);
CGFloat rectY = AlignValueToPixel((frameSize.height - rectSize) / 2);
return CGRectMake(rectX, rectY, rectSize, rectSize);
}
// Returns the size of the viewport based on the device type.
CGFloat GetViewportSize() {
return IsIPadIdiom() ? kViewportSize_iPad : kViewportSize_iPhone;
}
} // namespace
// A subclass of UIView with the layerClass property set to
// AVCaptureVideoPreviewLayer. Contains the video preview for the QR scanner.
@interface VideoPreviewView : UIView
// Returns the VideoPreviewView's layer cast to AVCaptureVideoPreviewLayer.
- (AVCaptureVideoPreviewLayer*)previewLayer;
// Returns the rectangle in camera coordinates in which codes should be
// recognized.
- (CGRect)viewportRectOfInterest;
@end
@implementation VideoPreviewView
+ (Class)layerClass {
return [AVCaptureVideoPreviewLayer class];
}
- (AVCaptureVideoPreviewLayer*)previewLayer {
return base::mac::ObjCCastStrict<AVCaptureVideoPreviewLayer>([self layer]);
}
- (CGRect)viewportRectOfInterest {
DCHECK(CGPointEqualToPoint(self.frame.origin, CGPointZero));
CGRect viewportRect =
CenteredRectForViewport(self.frame.size, GetViewportSize());
AVCaptureVideoPreviewLayer* layer = [self previewLayer];
// If the layer does not have a connection,
// |metadataOutputRectOfInterestForRect:| does not return the right value.
DCHECK(layer.connection);
return [layer metadataOutputRectOfInterestForRect:viewportRect];
}
@end
// A subclass of UIView containing the preview overlay. It is responsible for
// redrawing the preview overlay and the viewport border every time the size
// of the preview changes. This UIView should always be square, with its width
// and height being the maximum of the width and height of its parent.
@interface PreviewOverlayView : UIView {
// Creates a transparent preview overlay. The overlay is a sublayer of the
// PreviewOverlayView's view to keep the opacity of the view's layer 1.0,
// otherwise the viewport border would inherit the opacity of the overlay.
CALayer* _previewOverlay;
// A container for the viewport border to draw a shadow under the border.
// Sublayer of PreviewOverlayView's layer.
CALayer* _viewportBorderContainer;
// The preview viewport border. Sublayer of |_viewportBorderContainer|.
CAShapeLayer* _viewportBorder;
}
// Creates a square mask for the overlay to keep the viewport transparent.
- (CAShapeLayer*)getViewportMaskWithFrameSize:(CGSize)frameSize
viewportSize:(CGFloat)viewportSize;
// Creates a mask to only draw the corners of the viewport border.
- (CAShapeLayer*)getViewportBorderMaskWithFrameSize:(CGSize)frameSize
viewportSize:(CGFloat)viewportSize;
@end
@implementation PreviewOverlayView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (!self) {
return nil;
}
_previewOverlay = [[CALayer alloc] init];
[_previewOverlay setBackgroundColor:[[UIColor blackColor] CGColor]];
[_previewOverlay setOpacity:kPreviewOverlayOpacity];
[[self layer] addSublayer:_previewOverlay];
_viewportBorderContainer = [[CALayer alloc] init];
[_viewportBorderContainer setShadowColor:[[UIColor blackColor] CGColor]];
[_viewportBorderContainer setShadowOffset:CGSizeZero];
[_viewportBorderContainer setShadowRadius:kViewportBorderShadowRadius];
[_viewportBorderContainer setShadowOpacity:kViewportBorderShadowOpacity];
[_viewportBorderContainer setShouldRasterize:YES];
[_viewportBorderContainer
setRasterizationScale:[[UIScreen mainScreen] scale]];
_viewportBorder = [[CAShapeLayer alloc] init];
[_viewportBorder setStrokeColor:[[UIColor whiteColor] CGColor]];
[_viewportBorder setFillColor:nil];
[_viewportBorder setOpacity:1.0];
[_viewportBorder setLineWidth:kViewportBorderLineWidth];
[_viewportBorderContainer addSublayer:_viewportBorder];
[[self layer] addSublayer:_viewportBorderContainer];
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGSize frameSize = self.frame.size;
CGFloat viewportSize = GetViewportSize();
[_previewOverlay
setFrame:CGRectMake(0, 0, frameSize.width, frameSize.height)];
[_previewOverlay setMask:[self getViewportMaskWithFrameSize:frameSize
viewportSize:viewportSize]];
CGRect borderRect = CenteredRectForViewport(
frameSize, viewportSize + kViewportBorderLineWidth);
UIBezierPath* borderPath =
[UIBezierPath bezierPathWithRoundedRect:borderRect
cornerRadius:kViewportBorderCornerRadius];
[_viewportBorder setPath:[borderPath CGPath]];
[_viewportBorder
setMask:[self getViewportBorderMaskWithFrameSize:frameSize
viewportSize:viewportSize]];
}
- (CAShapeLayer*)getViewportMaskWithFrameSize:(CGSize)frameSize
viewportSize:(CGFloat)viewportSize {
CGRect frameRect = CGRectMake(0, 0, frameSize.width, frameSize.height);
CGRect viewportRect = CenteredRectForViewport(frameSize, viewportSize);
UIBezierPath* maskPath = [UIBezierPath bezierPathWithRect:frameRect];
[maskPath appendPath:[UIBezierPath bezierPathWithRect:viewportRect]];
CAShapeLayer* mask = [[CAShapeLayer alloc] init];
[mask setFillColor:[[UIColor blackColor] CGColor]];
[mask setFillRule:kCAFillRuleEvenOdd];
[mask setFrame:frameRect];
[mask setPath:maskPath.CGPath];
return mask;
}
- (CAShapeLayer*)getViewportBorderMaskWithFrameSize:(CGSize)frameSize
viewportSize:(CGFloat)viewportSize {
CGFloat viewportBorderCornerWidth = IsIPadIdiom()
? kViewportBorderCornerWidth_iPad
: kViewportBorderCornerWidth_iPhone;
CGRect maskRect = CenteredRectForViewport(
frameSize, viewportSize - 2 * viewportBorderCornerWidth);
CGFloat sizeX = maskRect.origin.x;
CGFloat sizeY = maskRect.origin.y;
CGFloat offsetX = sizeX + maskRect.size.width;
CGFloat offsetY = sizeY + maskRect.size.height;
UIBezierPath* path =
[UIBezierPath bezierPathWithRect:CGRectMake(0, 0, sizeX, sizeY)];
[path appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(0, offsetY,
sizeX, sizeY)]];
[path appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(offsetY, 0,
sizeX, sizeY)]];
[path appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(offsetX, offsetY,
sizeX, sizeY)]];
CAShapeLayer* mask = [[CAShapeLayer alloc] init];
[mask setFillColor:[[UIColor blackColor] CGColor]];
[mask setFrame:CGRectMake(0, 0, frameSize.width, frameSize.height)];
[mask setPath:path.CGPath];
return mask;
}
@end
@interface QRScannerView () {
// 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 QRScannerView.
NSLayoutConstraint* _overlayWidthConstraint;
// The constraint relating the size of the |_previewOverlay| to the height of
// te QRScannerView.
NSLayoutConstraint* _overlayHeightConstraint;
}
@end
@implementation QRScannerView
#pragma mark lifecycle
- (instancetype)initWithFrame:(CGRect)frame
delegate:(id<QRScannerViewDelegate>)delegate {
self = [super initWithFrame:frame];
if (!self) {
return nil;
}
DCHECK(delegate);
_delegate = delegate;
[self setupPreviewView];
[self setupPreviewOverlayView];
[self addSubviews];
return self;
}
#pragma mark UIView
// TODO(crbug.com/633577): 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))];
}
#pragma mark public methods
- (AVCaptureVideoPreviewLayer*)getPreviewLayer {
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_QR_SCANNER_TORCH_ON_ACCESSIBILITY_VALUE);
} else {
icon = [self torchOffIcon];
accessibilityValue = l10n_util::GetNSString(
IDS_IOS_QR_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)) - base::kPiFloat) < 0.001 ||
fabs(fabs(atan2f(rotation.b, rotation.a)) - base::kPiFloat / 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)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();
}
}];
}
#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:@"qr_scanner_torch_on"];
return icon;
}
// Creates an icon for torch turned off.
- (UIImage*)torchOffIcon {
UIImage* icon = [self templateImageWithName:@"qr_scanner_torch_off"];
return icon;
}
// Adds the subviews.
- (void)addSubviews {
UIBarButtonItem* close =
[[UIBarButtonItem alloc] initWithImage:[ChromeIcon closeIcon]
style:UIBarButtonItemStylePlain
target:_delegate
action:@selector(dismissQRScannerView:)];
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 = @"qr_scanner_torch_button";
_torchButton.accessibilityLabel = l10n_util::GetNSString(
IDS_IOS_QR_SCANNER_TORCH_BUTTON_ACCESSIBILITY_LABEL);
_torchButton.accessibilityValue =
l10n_util::GetNSString(IDS_IOS_QR_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 = l10n_util::GetNSString(IDS_IOS_QR_SCANNER_VIEWPORT_CAPTION);
[viewportCaption setText:label];
[viewportCaption
setFont:[UIFont preferredFontForTextStyle:UIFontTextStyleBody]];
[viewportCaption setAdjustsFontForContentSizeCategory:YES];
[viewportCaption setNumberOfLines:0];
[viewportCaption setTextAlignment:NSTextAlignmentCenter];
[viewportCaption setAccessibilityLabel:label];
[viewportCaption setAccessibilityIdentifier:@"qr_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];
UIScrollView* scrollView = [[UIScrollView alloc] init];
scrollView.showsVerticalScrollIndicator = NO;
[self addSubview:scrollView];
[scrollView addSubview:viewportCaption];
// Constraints for viewportCaption.
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
viewportCaption.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[scrollView.topAnchor
constraintEqualToAnchor:self.centerYAnchor
constant:GetViewportSize() / 2 +
kViewportCaptionVerticalPadding],
[scrollView.bottomAnchor constraintEqualToAnchor:toolbar.topAnchor],
[scrollView.leadingAnchor
constraintEqualToAnchor:self.leadingAnchor
constant:kViewportCaptionHorizontalPadding],
[viewportCaption.leadingAnchor
constraintEqualToAnchor:self.leadingAnchor
constant:kViewportCaptionHorizontalPadding],
[scrollView.trailingAnchor
constraintEqualToAnchor:self.trailingAnchor
constant:-kViewportCaptionHorizontalPadding],
[viewportCaption.trailingAnchor
constraintEqualToAnchor:self.trailingAnchor
constant:-kViewportCaptionHorizontalPadding],
]];
AddSameConstraints(scrollView, viewportCaption);
}
// Adds a preview view to |self| and configures its layout constraints.
- (void)setupPreviewView {
DCHECK(!_previewView);
_previewView = [[VideoPreviewView alloc] initWithFrame:self.frame];
[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];
[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 QRScannerView.
[_previewOverlay setTranslatesAutoresizingMaskIntoConstraints:NO];
[NSLayoutConstraint activateConstraints:@[
[[_previewOverlay centerXAnchor]
constraintEqualToAnchor:[self centerXAnchor]],
[[_previewOverlay centerYAnchor]
constraintEqualToAnchor:[self centerYAnchor]],
_overlaySquareConstraint, _overlayWidthConstraint, _overlayHeightConstraint
]];
}
@end