@ -1,44 +1,60 @@
/ /
/ / 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 .
/ / Copyright ( c ) 2017 Open Whisper Systems . All rights reserved .
/ /
#import "FLAnimatedImage . h "
#import "FullImageViewController . h "
#import "UIUtil . h "
#define kImageViewCornerRadius 5.0 f
#import "UIView + OWS . h "
#import "TSPhotoAdapter . h "
#import "TSMessageAdapter . h "
#import "TSAnimatedAdapter . h "
#define kMinZoomScale 1.0 f
#define kMaxZoomScale 8.0 f
#define kTargetDoubleTapZoom 3.0 f
#define kBackgroundAlpha 0.6 f
@ interface FullImageViewController ( ) < UIScrollViewDelegate , UIGestureRecognizerDelegate >
/ / In order to use UIMenuController , the view from which it is
/ / presented must have certain custom behaviors .
@ interface AttachmentMenuView : UIView
@ property ( nonatomic , strong ) UIView * backgroundView ;
@ end
@ property ( nonatomic , strong ) UIScrollView * scrollView ;
#pragma mark -
@ property ( nonatomic , strong ) UIImageView * imageView ;
@ implementation AttachmentMenuView
@ property ( nonatomic , strong ) UITapGestureRecognizer * singleTap ;
@ property ( nonatomic , strong ) UITapGestureRecognizer * doubleTap ;
- ( BOOL ) canBecomeFirstResponder {
return YES ;
}
@ property ( nonatomic , strong ) UIButton * shareButton ;
/ / We only use custom actions in UIMenuController .
- ( BOOL ) canPerformAction : ( SEL ) action
withSender : ( id ) sender
{
return NO ;
}
@ end
@ property CGRect originRect ;
@ property BOOL isPresenting ;
@ property BOOL isAnimated ;
@ property NSData * fileData ;
#pragma mark -
@ property TSAttachmentStream * attachment ;
@ property TSInteraction * interaction ;
@ interface FullImageViewController ( ) < UIScrollViewDelegate , UIGestureRecognizerDelegate >
@ property ( nonatomic ) UIView * backgroundView ;
@ property ( nonatomic ) UIScrollView * scrollView ;
@ property ( nonatomic ) UIImageView * imageView ;
@ property ( nonatomic ) UIButton * shareButton ;
@ property ( nonatomic ) CGRect originRect ;
@ property ( nonatomic ) BOOL isPresenting ;
@ property ( nonatomic ) BOOL isAnimated ;
@ property ( nonatomic ) NSData * fileData ;
@ property ( nonatomic ) TSAttachmentStream * attachment ;
@ property ( nonatomic ) TSInteraction * interaction ;
@ property ( nonatomic ) id < OWSMessageData > messageItem ;
@ end
@ -48,6 +64,7 @@
- ( instancetype ) initWithAttachment : ( TSAttachmentStream * ) attachment
fromRect : ( CGRect ) rect
forInteraction : ( TSInteraction * ) interaction
messageItem : ( id < OWSMessageData > ) messageItem
isAnimated : ( BOOL ) animated {
self = [ super initWithNibName : nil bundle : nil ] ;
@ -55,6 +72,7 @@
self . attachment = attachment ;
self . originRect = rect ;
self . interaction = interaction ;
self . messageItem = messageItem ;
self . isAnimated = animated ;
self . fileData = [ NSData dataWithContentsOfURL : [ attachment mediaURL ] ] ;
}
@ -66,6 +84,12 @@
return self . attachment . image ;
}
- ( void ) loadView {
self . view = [ AttachmentMenuView new ] ;
self . view . backgroundColor = [ UIColor colorWithWhite : 0 alpha : kBackgroundAlpha ] ;
self . view . autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight ;
}
- ( void ) viewDidLoad {
[ super viewDidLoad ] ;
@ -77,16 +101,24 @@
[ self populateImageView : self . image ] ;
}
- ( void ) viewWillDisappear : ( BOOL ) animated {
[ super viewWillDisappear : animated ] ;
if ( [ UIMenuController sharedMenuController ] . isMenuVisible ) {
[ [ UIMenuController sharedMenuController ] setMenuVisible : NO
animated : NO ] ;
}
}
#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 = [ UIView new ] ;
self . backgroundView . backgroundColor = [ UIColor colorWithWhite : 0 alpha : kBackgroundAlpha ] ;
[ self . view addSubview : self . backgroundView ] ;
[ self . backgroundView autoPinEdgesToSuperviewEdges ] ;
}
- ( void ) initializeScrollView {
@ -111,7 +143,6 @@
} 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 ;
@ -128,56 +159,106 @@
}
- ( 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 ;
UITapGestureRecognizer * singleTap = [ [ UITapGestureRecognizer alloc ] initWithTarget : self
action : @ selector ( imageDismissGesture : ) ] ;
singleTap . delegate = self ;
[ self . view addGestureRecognizer : singleTap ] ;
UITapGestureRecognizer * doubleTap = [ [ UITapGestureRecognizer alloc ] initWithTarget : self
action : @ selector ( imageDismissGesture : ) ] ;
doubleTap . numberOfTapsRequired = 2 ;
doubleTap . delegate = self ;
[ self . view addGestureRecognizer : doubleTap ] ;
/ / UISwipeGestureRecognizer supposedly supports multiple directions ,
/ / but in practice it works better if you use a separate GR for each
/ / direction .
for ( NSNumber * direction in @ [
@ ( UISwipeGestureRecognizerDirectionRight ) ,
@ ( UISwipeGestureRecognizerDirectionLeft ) ,
@ ( UISwipeGestureRecognizerDirectionUp ) ,
@ ( UISwipeGestureRecognizerDirectionDown ) ,
] ) {
UISwipeGestureRecognizer * swipe = [ [ UISwipeGestureRecognizer alloc ] initWithTarget : self
action : @ selector ( imageDismissGesture : ) ] ;
swipe . direction = ( UISwipeGestureRecognizerDirection ) direction . integerValue ;
swipe . delegate = self ;
[ self . view addGestureRecognizer : swipe ] ;
}
[ self . view addGestureRecognizer : self . singleTap ] ;
[ self . view addGestureRecognizer : self . doubleTap ] ;
UILongPressGestureRecognizer * longPress = [ [ UILongPressGestureRecognizer alloc ] initWithTarget : self
action : @ selector ( longPressGesture : ) ] ;
longPress . delegate = self ;
[ self . view addGestureRecognizer : longPress ] ;
}
#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 ;
- ( void ) imageD ismissGesture: ( UIGestureRecognizer * ) sender {
if ( sender . state == UIGestureRecognizerStateRecognized ) {
[ self dismiss ] ;
}
}
CGSize zoom ;
- ( void ) longPressGesture : ( UIGestureRecognizer * ) sender {
/ / We "eagerly " respond when the long press begins , not when it ends .
if ( sender . state == UIGestureRecognizerStateBegan ) {
if ( self . scrollView . zoomScale == 1.0 f ) {
zoom = CGSizeMake ( self . view . bounds . size . width / kTargetDoubleTapZoom ,
self . view . bounds . size . height / kTargetDoubleTapZoom ) ;
targetZoomRect = CGRectMake (
convertCoord . x - ( zoom . width / 2.0 f ) , convertCoord . y - ( zoom . height / 2.0 f ) , 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.0 f ) , convertCoord . y - ( zoom . height / 2.0 f ) , zoom . width , zoom . height ) ;
targetInsets = [ self contentInsetForScrollView : 1.0 f ] ;
[ self . view becomeFirstResponder ] ;
if ( [ UIMenuController sharedMenuController ] . isMenuVisible ) {
[ [ UIMenuController sharedMenuController ] setMenuVisible : NO
animated : NO ] ;
}
NSArray * menuItems = @ [
[ [ UIMenuItem alloc ] initWithTitle : NSLocalizedString ( @ "ATTACHMENT_VIEW_COPY_ACTION ", @ "Short name for edit menu item to copy contents of media message . ")
action : @ selector ( copyAttachment : ) ] ,
[ [ UIMenuItem alloc ] initWithTitle : NSLocalizedString ( @ "ATTACHMENT_VIEW_SAVE_ACTION ", @ "Short name for edit menu item to save contents of media message . ")
action : @ selector ( saveAttachment : ) ] ,
/ / TODO : We should implement sharing .
/ / [ [ UIMenuItem alloc ] initWithTitle : NSLocalizedString ( @ "ATTACHMENT_VIEW_SHARE_ACTION ", @ "Short name for edit menu item to share contents of media message . ")
/ / action : @ selector ( shareAttachment : ) ] ,
] ;
[ UIMenuController sharedMenuController ] . menuItems = menuItems ;
CGPoint location = [ sender locationInView : self . view ] ;
CGRect targetRect = CGRectMake ( location . x ,
location . y ,
1 , 1 ) ;
[ [ UIMenuController sharedMenuController ] setTargetRect : targetRect
inView : self . view ] ;
[ [ UIMenuController sharedMenuController ] setMenuVisible : YES
animated : YES ] ;
}
}
self . view . userInteractionEnabled = NO ;
- ( void ) performEditingActionWithSelector : ( SEL ) selector {
OWSAssert ( self . messageItem . messageType == TSIncomingMessageAdapter ||
self . messageItem . messageType == TSOutgoingMessageAdapter ) ;
OWSAssert ( [ self . messageItem isMediaMessage ] ) ;
OWSAssert ( [ self . messageItem isKindOfClass : [ TSMessageAdapter class ] ] ) ;
OWSAssert ( [ self . messageItem conformsToProtocol : @ protocol ( OWSMessageEditing ) ] ) ;
OWSAssert ( [ [ self . messageItem media ] isKindOfClass : [ TSPhotoAdapter class ] ] ||
[ [ self . messageItem media ] isKindOfClass : [ TSAnimatedAdapter class ] ] ) ;
id < OWSMessageEditing > messageEditing = ( id < OWSMessageEditing > ) self . messageItem . media ;
OWSAssert ( [ messageEditing canPerformEditingAction : selector ] ) ;
[ messageEditing performEditingAction : selector ] ;
}
[ CATransaction begin ] ;
[ CATransaction setCompletionBlock : ^{
self . scrollView . contentInset = targetInsets ;
self . view . userInteractionEnabled = YES ;
} ] ;
[ self . scrollView zoomToRect : targetZoomRect animated : YES ] ;
[ CATransaction commit ] ;
- ( void ) copyAttachment : ( id ) sender {
[ self performEditingActionWithSelector : NSSelectorFromString ( @ "copy : ") ] ;
}
- ( void ) imageSingleTapped : ( UITapGestureRecognizer * ) singleTap {
[ self dismiss ] ;
- ( void ) saveAttachment : ( id ) sender {
[ self performEditingActionWithSelector : NSSelectorFromString ( @ "save : ") ] ;
}
- ( void ) shareAttachment : ( id ) sender {
/ / TODO : We should implement sharing with UIActivityViewController .
/ /
/ / It seems that loading of the contents of the attachment is done
/ / with TSAttachment and TSAttachmentStream .
}
#pragma mark - Presentation
@ -193,14 +274,19 @@
presentViewController : self
animated : NO
completion : ^{
[ UIView animateWithDuration : 0.4 f
UIWindow * window = [ UIApplication sharedApplication ] . keyWindow ;
self . imageView . frame = [ self . view convertRect : self . originRect
fromView : window ] ;
[ UIView animateWithDuration : 0.25 f
delay : 0
options : UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut
options : UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEase Out
animations : ^( ) {
self . view . alpha = 1.0 f ;
self . imageView . frame = [ self resizedFrameForImageView : self . image . size ] ;
self . imageView . center =
CGPointMake ( self . view . bounds . size . width / 2.0 f , self . view . bounds . size . height / 2.0 f ) ;
CGPointMake ( self . view . bounds . size . width / 2.0 f ,
self . view . bounds . size . height / 2.0 f ) ;
}
completion : ^( BOOL completed ) {
self . scrollView . frame = self . view . bounds ;
@ -215,9 +301,9 @@
- ( void ) dismiss {
self . view . userInteractionEnabled = NO ;
[ UIView animateWithDuration : 0. 4 f
[ UIView animateWithDuration : 0. 25 f
delay : 0
options : UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurve EaseInOut
options : UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurve Linear
animations : ^( ) {
self . backgroundView . backgroundColor = [ UIColor clearColor ] ;
self . scrollView . alpha = 0 ;
@ -234,7 +320,6 @@
[ self updateLayouts ] ;
}
- ( void ) updateLayouts {
if ( _isPresenting ) {
return ;
@ -246,7 +331,6 @@
self . scrollView . contentInset = [ self contentInsetForScrollView : self . scrollView . zoomScale ] ;
}
#pragma mark - Resizing
- ( CGRect ) resizedFrameForImageView : ( CGSize ) imageSize {