Last active
December 28, 2022 08:12
-
-
Save leilee/8edd3bbeec43e8b294fdd6072f55960b to your computer and use it in GitHub Desktop.
View to expand & zoom the photo. Inspired by https://github.com/muukii/ZoomImageView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import <UIKit/UIKit.h> | |
@interface OUPZoomImageView : UIScrollView | |
@property (nonatomic) UIImage* image; | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import "OUPZoomImageView.h" | |
@interface OUPZoomImageView ()<UIScrollViewDelegate> | |
@property (nonatomic) UIImageView* imageView; | |
@property (nonatomic) CGSize oldSize; | |
@end | |
@implementation OUPZoomImageView | |
#pragma mark - init | |
- (instancetype)initWithCoder:(NSCoder*)coder | |
{ | |
self = [super initWithCoder:coder]; | |
if (self) | |
{ | |
[self commonInit]; | |
} | |
return self; | |
} | |
- (instancetype)initWithFrame:(CGRect)frame | |
{ | |
self = [super initWithFrame:frame]; | |
if (self) | |
{ | |
[self commonInit]; | |
} | |
return self; | |
} | |
- (void)commonInit | |
{ | |
self.backgroundColor = UIColor.clearColor; | |
self.delegate = self; | |
self.showsVerticalScrollIndicator = NO; | |
self.showsHorizontalScrollIndicator = NO; | |
self.decelerationRate = UIScrollViewDecelerationRateFast; | |
_imageView = [[UIImageView alloc] init]; | |
_imageView.contentMode = UIViewContentModeScaleAspectFill; | |
[self addSubview:_imageView]; | |
auto doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)]; | |
doubleTap.numberOfTapsRequired = 2; | |
[self addGestureRecognizer:doubleTap]; | |
} | |
- (void)layoutSubviews | |
{ | |
[super layoutSubviews]; | |
auto bounds = self.bounds; | |
if (self.image != nil && _oldSize != bounds.size) | |
{ | |
[self updateImageView]; | |
_oldSize = bounds.size; | |
} | |
if (CGRectGetWidth(_imageView.frame) <= CGRectGetWidth(bounds)) | |
{ | |
auto center = _imageView.center; | |
center.x = CGRectGetWidth(bounds) * 0.5; | |
_imageView.center = center; | |
} | |
if (CGRectGetHeight(_imageView.frame) <= CGRectGetHeight(bounds)) | |
{ | |
auto center = _imageView.center; | |
center.y = CGRectGetHeight(bounds) * 0.5; | |
_imageView.center = center; | |
} | |
} | |
#pragma mark - action | |
- (void)handleDoubleTap:(UITapGestureRecognizer*)gr | |
{ | |
if (self.minimumZoomScale >= self.maximumZoomScale) | |
{ | |
return; | |
} | |
if (self.zoomScale < self.maximumZoomScale) | |
{ | |
[self zoomToMaxScaleWithGestureRecognizer:gr]; | |
} | |
else | |
{ | |
[self zoomToMinScale]; | |
} | |
} | |
#pragma mark - zoom | |
- (void)zoomToMaxScaleWithGestureRecognizer:(UITapGestureRecognizer*)gr | |
{ | |
auto zoomRect = [self zoomRectWithScale:self.maximumZoomScale | |
withCenter:[gr locationInView:gr.view]]; | |
[self zoomToRect:zoomRect animated:YES]; | |
} | |
- (void)zoomToMinScale | |
{ | |
[self setZoomScale:self.minimumZoomScale animated:YES]; | |
} | |
- (CGRect)zoomRectWithScale:(CGFloat)scale withCenter:(CGPoint)center | |
{ | |
center = [_imageView convertPoint:center fromView:self]; | |
auto imageSize = _imageView.bounds.size; | |
auto zoomSize = imageSize / scale; | |
auto zoomX = center.x - (zoomSize.width / 2.f); | |
auto zoomY = center.y - (zoomSize.height / 2.f); | |
return CGRect{.origin = {zoomX, zoomY}, .size = zoomSize}; | |
} | |
#pragma mark - update | |
- (void)updateImageView | |
{ | |
if (!self.image) | |
{ | |
return; | |
} | |
auto boundingSize = self.bounds.size; | |
auto imageSize = self.image.size; | |
auto widthRatio = boundingSize.width / imageSize.width; | |
auto heightRatio = boundingSize.height / imageSize.height; | |
if (widthRatio < heightRatio) | |
{ | |
boundingSize.height = boundingSize.width / imageSize.width * imageSize.height; | |
} | |
else if (heightRatio < widthRatio) | |
{ | |
boundingSize.width = boundingSize.height / imageSize.height * imageSize.width; | |
} | |
auto size = CGSizeMake(ceil(boundingSize.width), ceil(boundingSize.height)); | |
_imageView.bounds = {.origin = CGPointZero, .size = size}; | |
self.contentSize = size; | |
_imageView.center = [self contentCenterForBoundingSize:self.bounds.size contentSize:self.contentSize]; | |
self.zoomScale = 1; | |
self.minimumZoomScale = 1; | |
self.maximumZoomScale = 3; | |
} | |
#pragma mark - accessor | |
- (UIImage*)image | |
{ | |
return _imageView.image; | |
} | |
- (void)setImage:(UIImage*)image | |
{ | |
auto oldImage = _imageView.image; | |
_imageView.image = image; | |
if (oldImage.size != image.size) | |
{ | |
_oldSize = CGSizeZero; | |
[self setNeedsLayout]; | |
[self updateImageView]; | |
} | |
} | |
#pragma mark - UIScrollViewDelegate | |
- (void)scrollViewDidZoom:(UIScrollView*)scrollView | |
{ | |
_imageView.center = [self contentCenterForBoundingSize:self.bounds.size | |
contentSize:self.contentSize]; | |
} | |
- (UIView*)viewForZoomingInScrollView:(UIScrollView*)scrollView | |
{ | |
return _imageView; | |
} | |
#pragma mark - internal | |
- (CGPoint)contentCenterForBoundingSize:(CGSize)boundingSize contentSize:(CGSize)contentSize | |
{ | |
/// When the zoom scale changes i.e. the image is zoomed in or out, the hypothetical center | |
/// of content view changes too. But the default Apple implementation is keeping the last center | |
/// value which doesn't make much sense. If the image ratio is not matching the screen | |
/// ratio, there will be some empty space horizontaly or verticaly. This needs to be calculated | |
/// so that we can get the correct new center value. When these are added, edges of contentView | |
/// are aligned in realtime and always aligned with corners of scrollview. | |
auto horizontalOffest = (boundingSize.width > contentSize.width) ? ((boundingSize.width - contentSize.width) * 0.5) : 0.0; | |
auto verticalOffset = (boundingSize.height > contentSize.height) ? ((boundingSize.height - contentSize.height) * 0.5) : 0.0; | |
return CGPointMake(contentSize.width * 0.5 + horizontalOffest, contentSize.height * 0.5 + verticalOffset); | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment