session-ios/Signal/src/view controllers/FullImageViewController.m
Michael Kirk 9db3b0db27 Consistent and efficient media Delete/Copy/Save UX
copy/save/delete is accessed via longpress for all media messages, just
like for simple text messages.

Notes
-----
We don't support saving audio attachments as it's not clear where they should go.
(I don't think users expect them to end up in their iTunes library.)

There is still no UX for "pasting" media into Signal.

Removed the now redundant (and confusing) "share" button interface.

//FREEBIE
2016-07-18 09:20:48 -07:00

366 lines
13 KiB
Objective-C

//
// FullImageViewController.m
// Signal
//
// Created by Dylan Bourgeois on 11/11/14.
// Animated GIF support added by Mike Okner (@mikeokner) on 11/27/15.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
//
#import "DJWActionSheet+OWS.h"
#import "FLAnimatedImage.h"
#import "FullImageViewController.h"
#import "UIUtil.h"
#define kImageViewCornerRadius 5.0f
#define kMinZoomScale 1.0f
#define kMaxZoomScale 8.0f
#define kTargetDoubleTapZoom 3.0f
#define kBackgroundAlpha 0.6f
@interface FullImageViewController () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
@property (nonatomic, strong) UIView *backgroundView;
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, strong) UITapGestureRecognizer *singleTap;
@property (nonatomic, strong) UITapGestureRecognizer *doubleTap;
@property (nonatomic, strong) UIButton *shareButton;
@property CGRect originRect;
@property BOOL isPresenting;
@property BOOL isAnimated;
@property NSData *fileData;
@property TSAttachmentStream *attachment;
@property TSInteraction *interaction;
@end
@implementation FullImageViewController
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment
fromRect:(CGRect)rect
forInteraction:(TSInteraction *)interaction
isAnimated:(BOOL)animated {
self = [super initWithNibName:nil bundle:nil];
if (self) {
self.attachment = attachment;
self.originRect = rect;
self.interaction = interaction;
self.isAnimated = animated;
self.fileData = [NSData dataWithContentsOfURL:[attachment mediaURL]];
}
return self;
}
- (UIImage *)image {
return self.attachment.image;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self initializeBackground];
[self initializeScrollView];
[self initializeImageView];
[self initializeGestureRecognizers];
[self populateImageView:self.image];
}
#pragma mark - Initializers
- (void)initializeBackground {
self.imageView.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha];
self.view.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha];
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.backgroundView = [[UIView alloc] initWithFrame:CGRectInset(self.view.bounds, -512, -512)];
self.backgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha];
[self.view addSubview:self.backgroundView];
}
- (void)initializeScrollView {
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
self.scrollView.delegate = self;
self.scrollView.zoomScale = 1.0f;
self.scrollView.maximumZoomScale = kMaxZoomScale;
self.scrollView.scrollEnabled = NO;
[self.view addSubview:self.scrollView];
}
- (void)initializeImageView {
if (self.isAnimated) {
// Present the animated image using Flipboard/FLAnimatedImage
FLAnimatedImage *animatedGif = [FLAnimatedImage animatedImageWithGIFData:self.fileData];
FLAnimatedImageView *imageView = [[FLAnimatedImageView alloc] init];
imageView.animatedImage = animatedGif;
imageView.frame = self.originRect;
imageView.contentMode = UIViewContentModeScaleAspectFill;
imageView.clipsToBounds = YES;
self.imageView = imageView;
} else {
// Present the static image using standard UIImageView
self.imageView = [[UIImageView alloc] initWithFrame:self.originRect];
self.imageView.layer.cornerRadius = kImageViewCornerRadius;
self.imageView.contentMode = UIViewContentModeScaleAspectFill;
self.imageView.userInteractionEnabled = YES;
self.imageView.clipsToBounds = YES;
self.imageView.layer.allowsEdgeAntialiasing = YES;
}
[self.scrollView addSubview:self.imageView];
}
- (void)populateImageView:(UIImage *)image {
if (image && !self.isAnimated) {
self.imageView.image = image;
}
}
- (void)initializeGestureRecognizers {
self.doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(imageDoubleTapped:)];
self.doubleTap.numberOfTapsRequired = 2;
self.singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(imageSingleTapped:)];
[self.singleTap requireGestureRecognizerToFail:self.doubleTap];
self.singleTap.delegate = self;
self.doubleTap.delegate = self;
[self.view addGestureRecognizer:self.singleTap];
[self.view addGestureRecognizer:self.doubleTap];
}
#pragma mark - Gesture Recognizers
- (void)imageDoubleTapped:(UITapGestureRecognizer *)doubleTap {
CGPoint tap = [doubleTap locationInView:doubleTap.view];
CGPoint convertCoord = [self.scrollView convertPoint:tap fromView:doubleTap.view];
CGRect targetZoomRect;
UIEdgeInsets targetInsets;
CGSize zoom;
if (self.scrollView.zoomScale == 1.0f) {
zoom = CGSizeMake(self.view.bounds.size.width / kTargetDoubleTapZoom,
self.view.bounds.size.height / kTargetDoubleTapZoom);
targetZoomRect = CGRectMake(
convertCoord.x - (zoom.width / 2.0f), convertCoord.y - (zoom.height / 2.0f), zoom.width, zoom.height);
targetInsets = [self contentInsetForScrollView:kTargetDoubleTapZoom];
} else {
zoom = CGSizeMake(self.view.bounds.size.width * self.scrollView.zoomScale,
self.view.bounds.size.height * self.scrollView.zoomScale);
targetZoomRect = CGRectMake(
convertCoord.x - (zoom.width / 2.0f), convertCoord.y - (zoom.height / 2.0f), zoom.width, zoom.height);
targetInsets = [self contentInsetForScrollView:1.0f];
}
self.view.userInteractionEnabled = NO;
[CATransaction begin];
[CATransaction setCompletionBlock:^{
self.scrollView.contentInset = targetInsets;
self.view.userInteractionEnabled = YES;
}];
[self.scrollView zoomToRect:targetZoomRect animated:YES];
[CATransaction commit];
}
- (void)imageSingleTapped:(UITapGestureRecognizer *)singleTap {
[self dismiss];
}
#pragma mark - Presentation
- (void)presentFromViewController:(UIViewController *)viewController {
_isPresenting = YES;
self.view.userInteractionEnabled = NO;
[self.view addSubview:self.imageView];
self.modalPresentationStyle = UIModalPresentationOverCurrentContext;
self.view.alpha = 0;
[viewController
presentViewController:self
animated:NO
completion:^{
[UIView animateWithDuration:0.4f
delay:0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut
animations:^() {
self.view.alpha = 1.0f;
self.imageView.frame = [self resizedFrameForImageView:self.image.size];
self.imageView.center =
CGPointMake(self.view.bounds.size.width / 2.0f, self.view.bounds.size.height / 2.0f);
}
completion:^(BOOL completed) {
self.scrollView.frame = self.view.bounds;
[self.scrollView addSubview:self.imageView];
[self updateLayouts];
self.view.userInteractionEnabled = YES;
_isPresenting = NO;
}];
[UIUtil modalCompletionBlock]();
}];
}
- (void)dismiss {
self.view.userInteractionEnabled = NO;
[UIView animateWithDuration:0.4f
delay:0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut
animations:^() {
self.backgroundView.backgroundColor = [UIColor clearColor];
self.scrollView.alpha = 0;
self.view.alpha = 0;
}
completion:^(BOOL completed) {
[self.presentingViewController dismissViewControllerAnimated:NO completion:nil];
}];
}
#pragma mark - Update Layout
- (void)viewDidLayoutSubviews {
[self updateLayouts];
}
- (void)updateLayouts {
if (_isPresenting) {
return;
}
self.scrollView.frame = self.view.bounds;
self.imageView.frame = [self resizedFrameForImageView:self.image.size];
self.scrollView.contentSize = self.imageView.frame.size;
self.scrollView.contentInset = [self contentInsetForScrollView:self.scrollView.zoomScale];
}
#pragma mark - Resizing
- (CGRect)resizedFrameForImageView:(CGSize)imageSize {
CGRect frame = self.view.bounds;
CGSize screenSize =
CGSizeMake(frame.size.width * self.scrollView.zoomScale, frame.size.height * self.scrollView.zoomScale);
CGSize targetSize = screenSize;
if ([self isImagePortrait]) {
if ([self getAspectRatioForCGSize:screenSize] < [self getAspectRatioForCGSize:imageSize]) {
targetSize.width = screenSize.height / [self getAspectRatioForCGSize:imageSize];
} else {
targetSize.height = screenSize.width * [self getAspectRatioForCGSize:imageSize];
}
} else {
if ([self getAspectRatioForCGSize:screenSize] > [self getAspectRatioForCGSize:imageSize]) {
targetSize.height = screenSize.width * [self getAspectRatioForCGSize:imageSize];
} else {
targetSize.width = screenSize.height / [self getAspectRatioForCGSize:imageSize];
}
}
frame.size = targetSize;
frame.origin = CGPointMake(0, 0);
return frame;
}
- (UIEdgeInsets)contentInsetForScrollView:(CGFloat)targetZoomScale {
UIEdgeInsets inset = UIEdgeInsetsZero;
CGSize boundsSize = self.scrollView.bounds.size;
CGSize contentSize = self.image.size;
CGSize minSize;
if ([self isImagePortrait]) {
if ([self getAspectRatioForCGSize:boundsSize] < [self getAspectRatioForCGSize:contentSize]) {
minSize.height = boundsSize.height;
minSize.width = minSize.height / [self getAspectRatioForCGSize:contentSize];
} else {
minSize.width = boundsSize.width;
minSize.height = minSize.width * [self getAspectRatioForCGSize:contentSize];
}
} else {
if ([self getAspectRatioForCGSize:boundsSize] > [self getAspectRatioForCGSize:contentSize]) {
minSize.width = boundsSize.width;
minSize.height = minSize.width * [self getAspectRatioForCGSize:contentSize];
} else {
minSize.height = boundsSize.height;
minSize.width = minSize.height / [self getAspectRatioForCGSize:contentSize];
}
}
CGSize finalSize = self.view.bounds.size;
minSize.width *= targetZoomScale;
minSize.height *= targetZoomScale;
if (minSize.height > finalSize.height && minSize.width > finalSize.width) {
inset = UIEdgeInsetsZero;
} else {
CGFloat dy = boundsSize.height - minSize.height;
CGFloat dx = boundsSize.width - minSize.width;
dy = (dy > 0) ? dy : 0;
dx = (dx > 0) ? dx : 0;
inset.top = dy / 2.0f;
inset.bottom = dy / 2.0f;
inset.left = dx / 2.0f;
inset.right = dx / 2.0f;
}
return inset;
}
#pragma mark - UIScrollViewDelegate
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.imageView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
scrollView.contentInset = [self contentInsetForScrollView:scrollView.zoomScale];
if (self.scrollView.scrollEnabled == NO) {
self.scrollView.scrollEnabled = YES;
}
}
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
self.scrollView.scrollEnabled = (scale > 1);
self.scrollView.contentInset = [self contentInsetForScrollView:scale];
}
#pragma mark - Utility
- (BOOL)isImagePortrait {
return ([self getAspectRatioForCGSize:self.image.size] > 1.0f);
}
- (CGFloat)getAspectRatioForCGSize:(CGSize)size {
return size.height / size.width;
}
#pragma mark - Saving images to Camera Roll
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo {
if (error) {
DDLogWarn(@"There was a problem saving <%@> to camera roll from %s ",
error.localizedDescription,
__PRETTY_FUNCTION__);
}
}
@end