YBIBImageCell.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. //
  2. // YBIBImageCell.m
  3. // YBImageBrowserDemo
  4. //
  5. // Created by 波儿菜 on 2019/6/5.
  6. // Copyright © 2019 波儿菜. All rights reserved.
  7. //
  8. #import "YBIBImageCell.h"
  9. #import "YBIBImageData.h"
  10. #import "YBIBIconManager.h"
  11. #import "YBIBImageCell+Internal.h"
  12. #import "YBIBImageData+Internal.h"
  13. #import "YBIBCopywriter.h"
  14. #import "YBIBUtilities.h"
  15. @interface YBIBImageCell () <YBIBImageDataDelegate, UIScrollViewDelegate, UIGestureRecognizerDelegate>
  16. @end
  17. @implementation YBIBImageCell {
  18. CGPoint _interactStartPoint;
  19. BOOL _interacting;
  20. }
  21. #pragma mark - life cycle
  22. - (instancetype)initWithFrame:(CGRect)frame {
  23. self = [super initWithFrame:frame];
  24. if (self) {
  25. [self initValue];
  26. [self.contentView addSubview:self.imageScrollView];
  27. [self addGesture];
  28. }
  29. return self;
  30. }
  31. - (void)layoutSubviews {
  32. [super layoutSubviews];
  33. self.imageScrollView.frame = self.bounds;
  34. }
  35. - (void)initValue {
  36. _interactStartPoint = CGPointZero;
  37. _interacting = NO;
  38. }
  39. - (void)prepareForReuse {
  40. ((YBIBImageData *)self.yb_cellData).delegate = nil;
  41. [self.imageScrollView reset];
  42. [self hideTailoringImageView];
  43. [self hideAuxiliaryView];
  44. [super prepareForReuse];
  45. }
  46. #pragma mark - <YBIBCellProtocol>
  47. @synthesize yb_currentOrientation = _yb_currentOrientation;
  48. @synthesize yb_containerSize = _yb_containerSize;
  49. @synthesize yb_backView = _yb_backView;
  50. @synthesize yb_collectionView = _yb_collectionView;
  51. @synthesize yb_isTransitioning = _yb_isTransitioning;
  52. @synthesize yb_isRotating = _yb_isRotating;
  53. @synthesize yb_auxiliaryViewHandler = _yb_auxiliaryViewHandler;
  54. @synthesize yb_hideStatusBar = _yb_hideStatusBar;
  55. @synthesize yb_hideBrowser = _yb_hideBrowser;
  56. @synthesize yb_hideToolViews = _yb_hideToolViews;
  57. @synthesize yb_cellData = _yb_cellData;
  58. @synthesize yb_cellIsInCenter = _yb_cellIsInCenter;
  59. @synthesize yb_selfPage = _yb_selfPage;
  60. @synthesize yb_currentPage = _yb_currentPage;
  61. - (void)setYb_cellData:(id<YBIBDataProtocol>)yb_cellData {
  62. _yb_cellData = yb_cellData;
  63. ((YBIBImageData *)yb_cellData).delegate = self;
  64. }
  65. - (UIView *)yb_foregroundView {
  66. return self.imageScrollView.imageView;
  67. }
  68. - (void)yb_orientationWillChangeWithExpectOrientation:(UIDeviceOrientation)orientation {
  69. [self hideTailoringImageView];
  70. }
  71. - (void)yb_orientationChangeAnimationWithExpectOrientation:(UIDeviceOrientation)orientation {
  72. [self updateImageLayoutWithOrientation:orientation previousImageSize:self.imageScrollView.imageView.image.size];
  73. }
  74. #pragma mark - private
  75. - (CGSize)contentSizeWithContainerSize:(CGSize)containerSize imageViewFrame:(CGRect)imageViewFrame {
  76. return CGSizeMake(MAX(containerSize.width, imageViewFrame.size.width), MAX(containerSize.height, imageViewFrame.size.height));
  77. }
  78. - (void)updateImageLayoutWithOrientation:(UIDeviceOrientation)orientation previousImageSize:(CGSize)previousImageSize {
  79. if (_interacting) [self restoreInteractionWithDuration:0];
  80. YBIBImageData *data = self.yb_cellData;
  81. CGSize imageSize;
  82. UIImage *image = self.imageScrollView.imageView.image;
  83. YBIBScrollImageType imageType = self.imageScrollView.imageType;
  84. if (imageType == YBIBScrollImageTypeCompressed) {
  85. imageSize = data.originImage ? data.originImage.size : image.size;
  86. } else {
  87. imageSize = image.size;
  88. }
  89. CGSize containerSize = self.yb_containerSize(orientation);
  90. CGRect imageViewFrame = [data.layout yb_imageViewFrameWithContainerSize:containerSize imageSize:imageSize orientation:orientation];
  91. CGSize contentSize = [self contentSizeWithContainerSize:containerSize imageViewFrame:imageViewFrame];
  92. CGFloat maxZoomScale = imageType == YBIBScrollImageTypeThumb ? 1 : [data.layout yb_maximumZoomScaleWithContainerSize:containerSize imageSize:imageSize orientation:orientation];
  93. // 'zoomScale' must set before 'contentSize' and 'imageView.frame'.
  94. self.imageScrollView.zoomScale = 1;
  95. self.imageScrollView.contentSize = contentSize;
  96. self.imageScrollView.minimumZoomScale = 1;
  97. self.imageScrollView.maximumZoomScale = maxZoomScale;
  98. CGFloat scale;
  99. if (previousImageSize.width > 0 && previousImageSize.height > 0) {
  100. scale = imageSize.width / imageSize.height - previousImageSize.width / previousImageSize.height;
  101. } else {
  102. scale = 0;
  103. }
  104. // '0.001' is admissible error.
  105. if (ABS(scale) <= 0.001) {
  106. self.imageScrollView.imageView.frame = imageViewFrame;
  107. } else {
  108. [UIView animateWithDuration:0.25 animations:^{
  109. self.imageScrollView.imageView.frame = imageViewFrame;
  110. }];
  111. }
  112. }
  113. - (void)cuttingImage {
  114. // This method has been delayed called, so 'browser' may be in transit now.
  115. if (self.yb_isTransitioning()) return;
  116. if (_interacting) return;
  117. YBIBImageData *data = self.yb_cellData;
  118. if (!data.originImage) return;
  119. if (self.imageScrollView.zoomScale < data.cuttingZoomScale) return;
  120. if ([data shouldCompress]) {
  121. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(cuttingImage_) object:nil];
  122. [self performSelector:@selector(cuttingImage_) withObject:nil afterDelay:0.15];
  123. }
  124. }
  125. - (void)cuttingImage_ {
  126. YBIBImageData *data = self.yb_cellData;
  127. if (!data.originImage) return;
  128. CGFloat scale = data.originImage.size.width / self.imageScrollView.contentSize.width;
  129. CGFloat x = self.imageScrollView.contentOffset.x * scale,
  130. y = self.imageScrollView.contentOffset.y * scale,
  131. width = self.imageScrollView.bounds.size.width * scale,
  132. height = self.imageScrollView.bounds.size.height * scale;
  133. __weak typeof(self) wSelf = self;
  134. [data cuttingImageToRect:CGRectMake(x, y, width, height) complete:^(UIImage *image) {
  135. if (!image) return;
  136. YBIB_DISPATCH_ASYNC_MAIN(^{
  137. __strong typeof(wSelf) self = wSelf;
  138. if (!self) return;
  139. if (data == self.yb_cellData && !self.imageScrollView.isDragging && !self->_interacting && !self.yb_isTransitioning()) {
  140. [self showTailoringImageView:image];
  141. }
  142. })
  143. }];
  144. }
  145. - (void)showTailoringImageView:(UIImage *)image {
  146. CGSize containerSize = self.yb_containerSize(self.yb_currentOrientation());
  147. if (!self.tailoringImageView.superview) {
  148. [self.contentView addSubview:self.tailoringImageView];
  149. }
  150. self.tailoringImageView.frame = CGRectMake(0, 0, containerSize.width, containerSize.height);
  151. self.tailoringImageView.hidden = NO;
  152. self.tailoringImageView.image = image;
  153. }
  154. - (void)hideTailoringImageView {
  155. // Don't use 'getter' method, because it's according to the need to load.
  156. if (_tailoringImageView) {
  157. self.tailoringImageView.hidden = YES;
  158. }
  159. }
  160. - (void)hideAuxiliaryView {
  161. [self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
  162. [self.yb_auxiliaryViewHandler() yb_hideToastWithContainer:self];
  163. }
  164. - (void)hideBrowser {
  165. ((YBIBImageData *)self.yb_cellData).delegate = nil;
  166. [self hideTailoringImageView];
  167. [self hideAuxiliaryView];
  168. self.yb_hideBrowser();
  169. _interacting = NO;
  170. }
  171. #pragma mark - <YBIBImageDataDelegate>
  172. - (void)yb_imageData:(YBIBImageData *)data startLoadingWithStatus:(YBIBImageLoadingStatus)status {
  173. switch (status) {
  174. case YBIBImageLoadingStatusDecoding: {
  175. if (!self.imageScrollView.imageView.image) {
  176. [self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self];
  177. }
  178. }
  179. break;
  180. case YBIBImageLoadingStatusProcessing: {
  181. if (!self.imageScrollView.imageView.image) {
  182. [self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self];
  183. }
  184. }
  185. break;
  186. case YBIBImageLoadingStatusCompressing: {
  187. if (!self.imageScrollView.imageView.image) {
  188. [self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self];
  189. }
  190. }
  191. break;
  192. case YBIBImageLoadingStatusReadingPHAsset: {
  193. if (!self.imageScrollView.imageView.image) {
  194. [self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self];
  195. }
  196. }
  197. break;
  198. case YBIBImageLoadingStatusNone: {
  199. [self hideAuxiliaryView];
  200. }
  201. break;
  202. default:
  203. break;
  204. }
  205. }
  206. - (void)yb_imageData:(YBIBImageData *)data readyForImage:(__kindof UIImage *)image {
  207. [self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
  208. if (self.imageScrollView.imageView.image == image) return;
  209. CGSize size = self.imageScrollView.imageView.image.size;
  210. [self.imageScrollView setImage:image type:YBIBScrollImageTypeOriginal];
  211. [self updateImageLayoutWithOrientation:self.yb_currentOrientation() previousImageSize:size];
  212. }
  213. - (void)yb_imageData:(YBIBImageData *)data readyForCompressedImage:(__kindof UIImage *)image {
  214. [self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
  215. if (self.imageScrollView.imageView.image == image) return;
  216. CGSize size = self.imageScrollView.imageView.image.size;
  217. [self.imageScrollView setImage:image type:YBIBScrollImageTypeCompressed];
  218. [self updateImageLayoutWithOrientation:self.yb_currentOrientation() previousImageSize:size];
  219. }
  220. - (void)yb_imageData:(YBIBImageData *)data readyForThumbImage:(__kindof UIImage *)image {
  221. if (self.imageScrollView.imageView.image) return;
  222. [self.imageScrollView setImage:image type:YBIBScrollImageTypeThumb];
  223. [self updateImageLayoutWithOrientation:self.yb_currentOrientation() previousImageSize:image.size];
  224. }
  225. - (void)yb_imageIsInvalidForData:(YBIBImageData *)data {
  226. [self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
  227. NSString *imageIsInvalid = [YBIBCopywriter sharedCopywriter].imageIsInvalid;
  228. if (self.imageScrollView.imageView.image) {
  229. [self.yb_auxiliaryViewHandler() yb_showIncorrectToastWithContainer:self text:imageIsInvalid];
  230. } else {
  231. [self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self text:imageIsInvalid];
  232. }
  233. }
  234. - (void)yb_imageData:(YBIBImageData *)data downloadProgress:(CGFloat)progress {
  235. [self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self progress:progress];
  236. }
  237. - (void)yb_imageDownloadFailedForData:(YBIBImageData *)data {
  238. if (self.imageScrollView.imageView.image) {
  239. [self.yb_auxiliaryViewHandler() yb_hideLoadingWithContainer:self];
  240. [self.yb_auxiliaryViewHandler() yb_showIncorrectToastWithContainer:self text:[YBIBCopywriter sharedCopywriter].downloadFailed];
  241. } else {
  242. [self.yb_auxiliaryViewHandler() yb_showLoadingWithContainer:self text:[YBIBCopywriter sharedCopywriter].downloadFailed];
  243. }
  244. }
  245. #pragma mark - gesture
  246. - (void)addGesture {
  247. UITapGestureRecognizer *tapSingle = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(respondsToTapSingle:)];
  248. tapSingle.numberOfTapsRequired = 1;
  249. UITapGestureRecognizer *tapDouble = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(respondsToTapDouble:)];
  250. tapDouble.numberOfTapsRequired = 2;
  251. UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(respondsToPan:)];
  252. pan.maximumNumberOfTouches = 1;
  253. pan.delegate = self;
  254. [tapSingle requireGestureRecognizerToFail:tapDouble];
  255. [tapSingle requireGestureRecognizerToFail:pan];
  256. [tapDouble requireGestureRecognizerToFail:pan];
  257. [self addGestureRecognizer:tapSingle];
  258. [self addGestureRecognizer:tapDouble];
  259. [self addGestureRecognizer:pan];
  260. }
  261. - (void)respondsToTapSingle:(UITapGestureRecognizer *)tap {
  262. if (self.yb_isRotating()) return;
  263. YBIBImageData *data = self.yb_cellData;
  264. if (data.singleTouchBlock) {
  265. data.singleTouchBlock(data);
  266. } else {
  267. [self hideTailoringImageView];
  268. [self hideAuxiliaryView];
  269. self.yb_hideBrowser();
  270. }
  271. }
  272. - (void)respondsToTapDouble:(UITapGestureRecognizer *)tap {
  273. if (self.yb_isRotating()) return;
  274. [self hideTailoringImageView];
  275. UIScrollView *scrollView = self.imageScrollView;
  276. UIView *zoomView = [self viewForZoomingInScrollView:scrollView];
  277. CGPoint point = [tap locationInView:zoomView];
  278. if (!CGRectContainsPoint(zoomView.bounds, point)) return;
  279. if (scrollView.zoomScale == scrollView.maximumZoomScale) {
  280. [scrollView setZoomScale:1 animated:YES];
  281. } else {
  282. [scrollView zoomToRect:CGRectMake(point.x, point.y, 1, 1) animated:YES];
  283. }
  284. }
  285. - (void)respondsToPan:(UIPanGestureRecognizer *)pan {
  286. if (self.yb_isRotating()) return;
  287. YBIBInteractionProfile *profile = ((YBIBImageData *)self.yb_cellData).interactionProfile;
  288. if (profile.disable) return;
  289. if ((CGRectIsEmpty(self.imageScrollView.imageView.frame) || !self.imageScrollView.imageView.image)) return;
  290. CGPoint point = [pan locationInView:self];
  291. CGSize containerSize = self.yb_containerSize(self.yb_currentOrientation());
  292. if (pan.state == UIGestureRecognizerStateBegan) {
  293. _interactStartPoint = point;
  294. } else if (pan.state == UIGestureRecognizerStateCancelled || pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateRecognized || pan.state == UIGestureRecognizerStateFailed) {
  295. // End.
  296. if (_interacting) {
  297. CGPoint velocity = [pan velocityInView:self.imageScrollView];
  298. BOOL velocityArrive = ABS(velocity.y) > profile.dismissVelocityY;
  299. BOOL distanceArrive = ABS(point.y - _interactStartPoint.y) > containerSize.height * profile.dismissScale;
  300. BOOL shouldDismiss = distanceArrive || velocityArrive;
  301. if (shouldDismiss) {
  302. [self hideBrowser];
  303. } else {
  304. [self restoreInteractionWithDuration:profile.restoreDuration];
  305. }
  306. }
  307. } else if (pan.state == UIGestureRecognizerStateChanged) {
  308. if (_interacting) {
  309. // Change.
  310. self.imageScrollView.center = point;
  311. CGFloat scale = 1 - ABS(point.y - _interactStartPoint.y) / (containerSize.height * 1.2);
  312. if (scale > 1) scale = 1;
  313. if (scale < 0.35) scale = 0.35;
  314. self.imageScrollView.transform = CGAffineTransformMakeScale(scale, scale);
  315. CGFloat alpha = 1 - ABS(point.y - _interactStartPoint.y) / (containerSize.height * 0.7);
  316. if (alpha > 1) alpha = 1;
  317. if (alpha < 0) alpha = 0;
  318. self.yb_backView.backgroundColor = [self.yb_backView.backgroundColor colorWithAlphaComponent:alpha];
  319. } else {
  320. // Start.
  321. if (CGPointEqualToPoint(_interactStartPoint, CGPointZero) || self.yb_currentPage() != self.yb_selfPage() || !self.yb_cellIsInCenter() || self.imageScrollView.isZooming) return;
  322. CGPoint velocity = [pan velocityInView:self.imageScrollView];
  323. CGFloat triggerDistance = profile.triggerDistance;
  324. CGFloat offsetY = self.imageScrollView.contentOffset.y, height = self.imageScrollView.bounds.size.height;
  325. BOOL distanceArrive = ABS(point.x - _interactStartPoint.x) < triggerDistance && ABS(velocity.x) < 500;
  326. BOOL upArrive = point.y - _interactStartPoint.y > triggerDistance && offsetY <= 1;
  327. BOOL downArrive = point.y - _interactStartPoint.y < -triggerDistance && offsetY + height >= MAX(self.imageScrollView.contentSize.height, height) - 1;
  328. BOOL shouldStart = (upArrive || downArrive) && distanceArrive;
  329. if (!shouldStart) return;
  330. _interactStartPoint = point;
  331. CGRect startFrame = self.imageScrollView.frame;
  332. CGFloat anchorX = point.x / startFrame.size.width, anchorY = point.y / startFrame.size.height;
  333. self.imageScrollView.layer.anchorPoint = CGPointMake(anchorX, anchorY);
  334. self.imageScrollView.userInteractionEnabled = NO;
  335. self.imageScrollView.scrollEnabled = NO;
  336. self.imageScrollView.center = point;
  337. self.yb_hideToolViews(YES);
  338. self.yb_hideStatusBar(NO);
  339. self.yb_collectionView().scrollEnabled = NO;
  340. [self hideTailoringImageView];
  341. _interacting = YES;
  342. }
  343. }
  344. }
  345. - (void)restoreInteractionWithDuration:(NSTimeInterval)duration {
  346. CGSize containerSize = self.yb_containerSize(self.yb_currentOrientation());
  347. void (^animations)(void) = ^{
  348. self.yb_backView.backgroundColor = [self.yb_backView.backgroundColor colorWithAlphaComponent:1];
  349. CGPoint anchorPoint = self.imageScrollView.layer.anchorPoint;
  350. self.imageScrollView.center = CGPointMake(containerSize.width * anchorPoint.x, containerSize.height * anchorPoint.y);
  351. self.imageScrollView.transform = CGAffineTransformIdentity;
  352. };
  353. void (^completion)(BOOL finished) = ^(BOOL finished){
  354. self.imageScrollView.layer.anchorPoint = CGPointMake(0.5, 0.5);
  355. self.imageScrollView.center = CGPointMake(containerSize.width * 0.5, containerSize.height * 0.5);
  356. self.imageScrollView.userInteractionEnabled = YES;
  357. self.imageScrollView.scrollEnabled = YES;
  358. self.yb_hideToolViews(NO);
  359. self.yb_hideStatusBar(YES);
  360. self.yb_collectionView().scrollEnabled = YES;
  361. [self cuttingImage];
  362. self->_interactStartPoint = CGPointZero;
  363. self->_interacting = NO;
  364. };
  365. if (duration <= 0) {
  366. animations();
  367. completion(NO);
  368. } else {
  369. [UIView animateWithDuration:duration animations:animations completion:completion];
  370. }
  371. }
  372. #pragma mark - <UIScrollViewDelegate>
  373. - (void)scrollViewDidZoom:(UIScrollView *)scrollView {
  374. YBIBImageData *data = self.yb_cellData;
  375. if (data.imageDidZoomBlock) {
  376. data.imageDidZoomBlock(data, scrollView);
  377. }
  378. CGRect imageViewFrame = self.imageScrollView.imageView.frame;
  379. CGFloat width = imageViewFrame.size.width,
  380. height = imageViewFrame.size.height,
  381. sHeight = scrollView.bounds.size.height,
  382. sWidth = scrollView.bounds.size.width;
  383. if (height > sHeight) {
  384. imageViewFrame.origin.y = 0;
  385. } else {
  386. imageViewFrame.origin.y = (sHeight - height) / 2.0;
  387. }
  388. if (width > sWidth) {
  389. imageViewFrame.origin.x = 0;
  390. } else {
  391. imageViewFrame.origin.x = (sWidth - width) / 2.0;
  392. }
  393. self.imageScrollView.imageView.frame = imageViewFrame;
  394. }
  395. - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
  396. return self.imageScrollView.imageView;
  397. }
  398. - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  399. YBIBImageData *data = self.yb_cellData;
  400. if (data.imageDidScrollBlock) {
  401. data.imageDidScrollBlock(data, scrollView);
  402. }
  403. [self cuttingImage];
  404. }
  405. - (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view {
  406. [self hideTailoringImageView];
  407. }
  408. - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
  409. [self hideTailoringImageView];
  410. }
  411. #pragma mark - <UIGestureRecognizerDelegate>
  412. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
  413. return YES;
  414. }
  415. #pragma mark - getters
  416. - (YBIBImageScrollView *)imageScrollView {
  417. if (!_imageScrollView) {
  418. _imageScrollView = [YBIBImageScrollView new];
  419. _imageScrollView.delegate = self;
  420. }
  421. return _imageScrollView;
  422. }
  423. - (UIImageView *)tailoringImageView {
  424. if (!_tailoringImageView) {
  425. _tailoringImageView = [UIImageView new];
  426. _tailoringImageView.contentMode = UIViewContentModeScaleAspectFit;
  427. }
  428. return _tailoringImageView;
  429. }
  430. @end