From 0b55df9649ee4dc15650efb5a1c77a73216c4781 Mon Sep 17 00:00:00 2001 From: Wendy Date: Thu, 21 Apr 2016 18:57:53 -0700 Subject: [PATCH] Add the ability for ASNetworkImageNodes to keep track of their progressive image quality --- AsyncDisplayKit.xcodeproj/project.pbxproj | 10 +++ AsyncDisplayKit/ASDisplayNode+Beta.h | 9 +++ AsyncDisplayKit/ASDisplayNode.mm | 33 +++++---- AsyncDisplayKit/ASMultiplexImageNode.mm | 2 +- AsyncDisplayKit/ASNetworkImageNode.h | 11 +++ AsyncDisplayKit/ASNetworkImageNode.mm | 67 +++++++++++++++---- AsyncDisplayKit/Details/ASImageProtocols.h | 2 +- .../Details/ASPINRemoteImageDownloader.m | 2 +- 8 files changed, 109 insertions(+), 27 deletions(-) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 53dafbe56b..3569f18eee 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -337,6 +337,10 @@ 9CDC18CC1B910E12004965E2 /* ASLayoutablePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9CDC18CD1B910E12004965E2 /* ASLayoutablePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */; }; + A2763D791CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = A2763D771CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h */; }; + A2763D7A1CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = A2763D771CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h */; }; + A2763D7B1CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = A2763D781CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.m */; }; + A2763D7C1CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = A2763D781CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.m */; }; A32FEDD51C501B6A004F642A /* ASTextKitFontSizeAdjuster.h in Headers */ = {isa = PBXBuildFile; fileRef = A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */; settings = {ATTRIBUTES = (Public, ); }; }; A373200F1C571B730011FC94 /* ASTextNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = A373200E1C571B050011FC94 /* ASTextNode+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; A37320101C571B740011FC94 /* ASTextNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = A373200E1C571B050011FC94 /* ASTextNode+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -808,6 +812,8 @@ 9C8898BA1C738B9800D6B02E /* ASTextKitFontSizeAdjuster.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASTextKitFontSizeAdjuster.mm; path = TextKit/ASTextKitFontSizeAdjuster.mm; sourceTree = ""; }; 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASLayoutablePrivate.h; path = AsyncDisplayKit/Layout/ASLayoutablePrivate.h; sourceTree = ""; }; 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASCollectionViewTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + A2763D771CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASPINRemoteImageDownloader.h; path = Details/ASPINRemoteImageDownloader.h; sourceTree = ""; }; + A2763D781CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASPINRemoteImageDownloader.m; path = Details/ASPINRemoteImageDownloader.m; sourceTree = ""; }; A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextKitFontSizeAdjuster.h; path = TextKit/ASTextKitFontSizeAdjuster.h; sourceTree = ""; }; A373200E1C571B050011FC94 /* ASTextNode+Beta.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASTextNode+Beta.h"; sourceTree = ""; }; AC026B571BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASStaticLayoutSpecSnapshotTests.m; sourceTree = ""; }; @@ -1064,6 +1070,8 @@ 055B9FA71A1C154B00035D6D /* ASNetworkImageNode.mm */, 25E327541C16819500A2170C /* ASPagerNode.h */, 25E327551C16819500A2170C /* ASPagerNode.m */, + A2763D771CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h */, + A2763D781CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.m */, D785F6601A74327E00291744 /* ASScrollNode.h */, D785F6611A74327E00291744 /* ASScrollNode.m */, B0F880581BEAEC7500D17647 /* ASTableNode.h */, @@ -1461,6 +1469,7 @@ 058D0A72195D05F800B7D73C /* _ASCoreAnimationExtras.h in Headers */, 7A06A73B1C35F08800FE8DAA /* ASRelativeLayoutSpec.h in Headers */, 68B8A4DC1CBD911D007E4543 /* ASImageNode+AnimatedImagePrivate.h in Headers */, + A2763D791CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h in Headers */, 058D0A53195D05DC00B7D73C /* _ASDisplayLayer.h in Headers */, 058D0A55195D05DC00B7D73C /* _ASDisplayView.h in Headers */, B13CA0F71C519E9400E031AB /* ASCollectionViewLayoutFacilitatorProtocol.h in Headers */, @@ -1620,6 +1629,7 @@ B35062581B010F070018CF92 /* ASAvailability.h in Headers */, DE84918D1C8FFF2B003D89E9 /* ASRunLoopQueue.h in Headers */, 254C6B731BF94DF4003EC431 /* ASTextKitCoreTextAdditions.h in Headers */, + A2763D7A1CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h in Headers */, 254C6B7A1BF94DF4003EC431 /* ASTextKitRenderer.h in Headers */, 69CB62AC1CB8165900024920 /* _ASDisplayViewAccessiblity.h in Headers */, 68355B3F1CB57A64001D4E68 /* ASPINRemoteImageDownloader.h in Headers */, diff --git a/AsyncDisplayKit/ASDisplayNode+Beta.h b/AsyncDisplayKit/ASDisplayNode+Beta.h index 4bb108b66a..7c894486ed 100644 --- a/AsyncDisplayKit/ASDisplayNode+Beta.h +++ b/AsyncDisplayKit/ASDisplayNode+Beta.h @@ -103,4 +103,13 @@ ASDISPLAYNODE_EXTERN_C_END */ - (void)cancelLayoutTransitionsInProgress; +/** + * @abstract Indicates that the receiver and all subnodes have finished displaying. May be called more than once, for example if the receiver has + * a network image node. This is called after the first display pass even if network image nodes have not downloaded anything (text would be done, + * and other nodes that are ready to do their final display). Each render of every progressive jpeg network node would cause this to be called, so + * this hook could be called up to 1 + (pJPEGcount * pJPEGrenderCount) times. The render count depends on how many times the downloader calls the + * progressImage block. + */ +- (void)hierarchyDisplayDidFinish; + @end diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 444e874c80..81375d39ff 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1722,19 +1722,23 @@ static NSInteger incrementIfFound(NSInteger i) { [_pendingDisplayNodes removeObject:node]; - if (_pendingDisplayNodes.count == 0 && _placeholderLayer.superlayer && ![self placeholderShouldPersist]) { - void (^cleanupBlock)() = ^{ - [_placeholderLayer removeFromSuperlayer]; - }; + if (_pendingDisplayNodes.count == 0) { + [self hierarchyDisplayDidFinish]; + + if (_placeholderLayer.superlayer && ![self placeholderShouldPersist]) { + void (^cleanupBlock)() = ^{ + [_placeholderLayer removeFromSuperlayer]; + }; - if (_placeholderFadeDuration > 0.0 && ASInterfaceStateIncludesVisible(self.interfaceState)) { - [CATransaction begin]; - [CATransaction setCompletionBlock:cleanupBlock]; - [CATransaction setAnimationDuration:_placeholderFadeDuration]; - _placeholderLayer.opacity = 0.0; - [CATransaction commit]; - } else { - cleanupBlock(); + if (_placeholderFadeDuration > 0.0 && ASInterfaceStateIncludesVisible(self.interfaceState)) { + [CATransaction begin]; + [CATransaction setCompletionBlock:cleanupBlock]; + [CATransaction setAnimationDuration:_placeholderFadeDuration]; + _placeholderLayer.opacity = 0.0; + [CATransaction commit]; + } else { + cleanupBlock(); + } } } } @@ -2390,6 +2394,11 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) }); } +- (void)hierarchyDisplayDidFinish +{ + // subclass hook +} + - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // subclass hook diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index 47e551bdb3..cb83bb1156 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -469,7 +469,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent ASImageDownloaderProgressImage progress = nil; if (ASInterfaceStateIncludesVisible(interfaceState)) { __weak __typeof__(self) weakSelf = self; - progress = ^(UIImage * _Nonnull progressImage, id _Nullable downloadIdentifier) { + progress = ^(UIImage * _Nonnull progressImage, CGFloat progress, id _Nullable downloadIdentifier) { __typeof__(self) strongSelf = weakSelf; if (strongSelf == nil) { return; diff --git a/AsyncDisplayKit/ASNetworkImageNode.h b/AsyncDisplayKit/ASNetworkImageNode.h index bb867f576c..ef3feba1e1 100644 --- a/AsyncDisplayKit/ASNetworkImageNode.h +++ b/AsyncDisplayKit/ASNetworkImageNode.h @@ -73,6 +73,17 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign, readwrite) BOOL shouldCacheImage; +/** + * The image quality of the current image. This is a number between 0 and 1 and can be used to track + * progressive progress. Calculated by dividing number of bytes / expected number of total bytes. + */ +@property (nonatomic, assign, readonly) CGFloat currentImageQuality; + +/** + * The image quality (value between 0 and 1) of the last image that completed displaying. + */ +@property (nonatomic, assign, readonly) CGFloat renderedImageQuality; + @end diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index 43f7c7cdb1..e282a84840 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -39,17 +39,19 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; id _downloadIdentifier; BOOL _imageLoaded; - + CGFloat _currentImageQuality; + CGFloat _renderedImageQuality; + BOOL _delegateSupportsDidStartFetchingData; BOOL _delegateSupportsDidFailWithError; BOOL _delegateSupportsImageNodeDidFinishDecoding; - + //set on init only BOOL _downloaderSupportsNewProtocol; BOOL _downloaderImplementsSetProgress; BOOL _downloaderImplementsSetPriority; BOOL _downloaderImplementsAnimatedImage; - + BOOL _cacheSupportsNewProtocol; BOOL _cacheSupportsClearing; BOOL _cacheSupportsSynchronousFetch; @@ -120,9 +122,13 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _URL = URL; - if (reset || _URL == nil) + if (reset || _URL == nil) { self.image = _defaultImage; - + ASPerformBlockOnMainThread(^{ + _currentImageQuality = 0.0; + }); + } + if (self.interfaceState & ASInterfaceStateFetchData) { [self fetchData]; } @@ -145,6 +151,9 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _defaultImage = defaultImage; if (!_imageLoaded) { + ASPerformBlockOnMainThread(^{ + _currentImageQuality = 0.0; + }); _lock.unlock(); // Locking: it is important to release _lock before entering setImage:, as it needs to release the lock before -invalidateCalculatedLayout. // If we continue to hold the lock here, it will still be locked until the next unlock() call, causing a possible deadlock with @@ -161,6 +170,30 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; return _defaultImage; } +- (void)setCurrentImageQuality:(CGFloat)currentImageQuality +{ + ASDN::MutexLocker l(_lock); + _currentImageQuality = currentImageQuality; +} + +- (CGFloat)currentImageQuality +{ + ASDN::MutexLocker l(_lock); + return _currentImageQuality; +} + +- (void)setRenderedImageQuality:(CGFloat)renderedImageQuality +{ + ASDN::MutexLocker l(_lock); + _renderedImageQuality = renderedImageQuality; +} + +- (CGFloat)renderedImageQuality +{ + ASDN::MutexLocker l(_lock); + return _renderedImageQuality; +} + - (void)setDelegate:(id)delegate { ASDN::MutexLocker l(_lock); @@ -196,6 +229,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (result) { self.image = result; _imageLoaded = YES; + _currentImageQuality = 1.0; } } } @@ -276,7 +310,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; ASImageDownloaderProgressImage progress = nil; if (ASInterfaceStateIncludesVisible(interfaceState)) { __weak __typeof__(self) weakSelf = self; - progress = ^(UIImage * _Nonnull progressImage, id _Nullable downloadIdentifier) { + progress = ^(UIImage * _Nonnull progressImage, CGFloat progress, id _Nullable downloadIdentifier) { __typeof__(self) strongSelf = weakSelf; if (strongSelf == nil) { return; @@ -288,6 +322,9 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; return; } strongSelf.image = progressImage; + ASPerformBlockOnMainThread(^{ + strongSelf->_currentImageQuality = progress; + }); }; } [_downloader setProgressImageBlock:progress callbackQueue:dispatch_get_main_queue() withDownloadIdentifier:_downloadIdentifier]; @@ -310,6 +347,9 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; self.animatedImage = nil; self.image = _defaultImage; _imageLoaded = NO; + ASPerformBlockOnMainThread(^{ + _currentImageQuality = 0.0; + }); } - (void)_cancelImageDownload @@ -393,6 +433,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; } _imageLoaded = YES; + self.currentImageQuality = 1.0; [_delegate imageNode:self didLoadImage:self.image]; }); } @@ -418,6 +459,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; } else { strongSelf.image = [imageContainer asdk_image]; } + strongSelf->_currentImageQuality = 1.0; } strongSelf->_downloadIdentifier = nil; @@ -472,13 +514,14 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; #pragma mark - ASDisplayNode+Subclasses -- (void)asyncdisplaykit_asyncTransactionContainerStateDidChange +- (void)displayDidFinish { - if (self.asyncdisplaykit_asyncTransactionContainerState == ASAsyncTransactionContainerStateNoTransactions) { - ASDN::MutexLocker l(_lock); - if (self.layer.contents != nil && _delegateSupportsImageNodeDidFinishDecoding) { - [self.delegate imageNodeDidFinishDecoding:self]; - } + [super displayDidFinish]; + + ASDN::MutexLocker l(_lock); + if (_delegateSupportsImageNodeDidFinishDecoding && self.layer.contents != nil) { + _renderedImageQuality = _currentImageQuality; + [self.delegate imageNodeDidFinishDecoding:self]; } } diff --git a/AsyncDisplayKit/Details/ASImageProtocols.h b/AsyncDisplayKit/Details/ASImageProtocols.h index c5079760d0..6b86fb97db 100644 --- a/AsyncDisplayKit/Details/ASImageProtocols.h +++ b/AsyncDisplayKit/Details/ASImageProtocols.h @@ -63,7 +63,7 @@ typedef void(^ASImageCacherCompletion)(id _Nullable i typedef void(^ASImageDownloaderCompletion)(id _Nullable image, NSError * _Nullable error, id _Nullable downloadIdentifier); typedef void(^ASImageDownloaderProgress)(CGFloat progress); -typedef void(^ASImageDownloaderProgressImage)(UIImage *progressImage, id _Nullable downloadIdentifier); +typedef void(^ASImageDownloaderProgressImage)(UIImage *progressImage, CGFloat progress, id _Nullable downloadIdentifier); typedef NS_ENUM(NSUInteger, ASImageDownloaderPriority) { ASImageDownloaderPriorityPreload = 0, diff --git a/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m b/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m index 1716006f5f..401211709c 100644 --- a/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m +++ b/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m @@ -174,7 +174,7 @@ if (progressBlock) { [[self sharedPINRemoteImageManager] setProgressImageCallback:^(PINRemoteImageManagerResult * _Nonnull result) { dispatch_async(callbackQueue, ^{ - progressBlock(result.image, result.UUID); + progressBlock(result.image, result.renderedImageQuality, result.UUID); }); } ofTaskWithUUID:downloadIdentifier]; } else {