diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aa2cfd606..bbb407e1ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - [ASDisplayNode] Add attributed versions of a11y label, hint and value. [#554](https://github.com/TextureGroup/Texture/pull/554) [Alexander Hüllmandel](https://github.com/fruitcoder) - [ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. [Scott Goodson](https://github.com/appleguy) [#465](https://github.com/TextureGroup/Texture/pull/465) - [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. [Scott Goodson](https://github.com/appleguy) +- [Animated Image] Adds support for animated WebP as well as improves GIF handling. [#605](https://github.com/TextureGroup/Texture/pull/605) [Garrett Moon](https://github.com/garrettmoon) ## 2.5 diff --git a/Cartfile b/Cartfile index bdcfff60c9..aebf9308e8 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1,2 @@ -github "pinterest/PINRemoteImage" "3.0.0-beta.12" -github "pinterest/PINCache" "3.0.1-beta.5" +github "pinterest/PINRemoteImage" "3.0.0-beta.13" +github "pinterest/PINCache" diff --git a/Podfile b/Podfile index 0d8cb0fa26..0ebd8c98ca 100644 --- a/Podfile +++ b/Podfile @@ -8,7 +8,7 @@ target :'AsyncDisplayKitTests' do pod 'JGMethodSwizzler', :git => 'https://github.com/JonasGessner/JGMethodSwizzler', :branch => 'master' # Only for buck build - pod 'PINRemoteImage', '3.0.0-beta.10' + pod 'PINRemoteImage', '3.0.0-beta.13' end #TODO CocoaPods plugin instead? diff --git a/Source/ASImageNode+AnimatedImage.mm b/Source/ASImageNode+AnimatedImage.mm index 2ceed182f5..75e5d5f653 100644 --- a/Source/ASImageNode+AnimatedImage.mm +++ b/Source/ASImageNode+AnimatedImage.mm @@ -66,14 +66,17 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; }; } + animatedImage.playbackReadyCallback = ^{ + // In this case the lock is already gone we have to call the unlocked version therefore + [weakSelf setShouldAnimate:YES]; + }; if (animatedImage.playbackReady) { [self _locked_setShouldAnimate:YES]; - } else { - animatedImage.playbackReadyCallback = ^{ - // In this case the lock is already gone we have to call the unlocked version therefore - [weakSelf setShouldAnimate:YES]; - }; } + } else { + // Clean up after ourselves. + self.contents = nil; + [self setCoverImage:nil]; } [self animatedImageSet:_animatedImage previousAnimatedImage:previousAnimatedImage]; @@ -107,8 +110,10 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; - (void)setCoverImageCompleted:(UIImage *)coverImage { - ASDN::MutexLocker l(__instanceLock__); - [self _locked_setCoverImageCompleted:coverImage]; + if (ASInterfaceStateIncludesDisplay(self.interfaceState)) { + ASDN::MutexLocker l(__instanceLock__); + [self _locked_setCoverImageCompleted:coverImage]; + } } - (void)_locked_setCoverImageCompleted:(UIImage *)coverImage @@ -132,9 +137,12 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; { //If we're a network image node, we want to set the default image so //that it will correctly be restored if it exits the range. +#if ASAnimatedImageDebug + NSLog(@"setting cover image: %p", self); +#endif if ([self isKindOfClass:[ASNetworkImageNode class]]) { [(ASNetworkImageNode *)self _locked_setDefaultImage:coverImage]; - } else { + } else if (_displayLink == nil || _displayLink.paused == YES) { [self _locked_setImage:coverImage]; } } @@ -218,11 +226,14 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; NSLog(@"starting animation: %p", self); #endif + // Get frame interval before holding display link lock to avoid deadlock + NSUInteger frameInterval = self.animatedImage.frameInterval; ASDN::MutexLocker l(_displayLinkLock); if (_displayLink == nil) { _playHead = 0; _displayLink = [CADisplayLink displayLinkWithTarget:[ASWeakProxy weakProxyWithTarget:self] selector:@selector(displayLinkFired:)]; - _displayLink.frameInterval = self.animatedImage.frameInterval; + _displayLink.frameInterval = frameInterval; + _lastSuccessfulFrameIndex = NSUIntegerMax; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:_animatedImageRunLoopMode]; } else { @@ -263,7 +274,9 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; if (self.animatedImage.coverImageReady) { [self setCoverImage:self.animatedImage.coverImage]; } - [self startAnimating]; + if (self.animatedImage.playbackReady) { + [self startAnimating]; + } } - (void)didExitVisibleState @@ -274,6 +287,26 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; [self stopAnimating]; } +- (void)didExitDisplayState +{ + ASDisplayNodeAssertMainThread(); +#if ASAnimatedImageDebug + NSLog(@"exiting display state: %p", self); +#endif + + // Check to see if we're an animated image before calling super in case someone + // decides they want to clear out the animatedImage itself on exiting the display + // state + BOOL isAnimatedImage = self.animatedImage != nil; + [super didExitDisplayState]; + + // Also clear out the contents we've set to be good citizens, we'll put it back in when we become visible. + if (isAnimatedImage) { + self.contents = nil; + [self setCoverImage:nil]; + } +} + #pragma mark - Display Link Callbacks - (void)displayLinkFired:(CADisplayLink *)displayLink @@ -283,6 +316,8 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; CFTimeInterval timeBetweenLastFire; if (self.lastDisplayLinkFire == 0) { timeBetweenLastFire = 0; + } else if (AS_AT_LEAST_IOS10){ + timeBetweenLastFire = displayLink.targetTimestamp - displayLink.timestamp; } else { timeBetweenLastFire = CACurrentMediaTime() - self.lastDisplayLinkFire; } @@ -291,7 +326,8 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; _playHead += timeBetweenLastFire; while (_playHead > self.animatedImage.totalDuration) { - _playHead -= self.animatedImage.totalDuration; + // Set playhead to zero to keep from showing different frames on different playthroughs + _playHead = 0; _playedLoops++; } @@ -301,15 +337,18 @@ NSString *const ASAnimatedImageDefaultRunLoopMode = NSRunLoopCommonModes; } NSUInteger frameIndex = [self frameIndexAtPlayHeadPosition:_playHead]; + if (frameIndex == _lastSuccessfulFrameIndex) { + return; + } CGImageRef frameImage = [self.animatedImage imageAtIndex:frameIndex]; if (frameImage == nil) { - _playHead -= timeBetweenLastFire; //Pause the display link until we get a file ready notification displayLink.paused = YES; self.lastDisplayLinkFire = 0; } else { self.contents = (__bridge id)frameImage; + _lastSuccessfulFrameIndex = frameIndex; [self displayDidFinish]; } } diff --git a/Source/Details/ASPINRemoteImageDownloader.m b/Source/Details/ASPINRemoteImageDownloader.m index 601d009450..05107f8e00 100644 --- a/Source/Details/ASPINRemoteImageDownloader.m +++ b/Source/Details/ASPINRemoteImageDownloader.m @@ -24,14 +24,20 @@ #import #import -#if __has_include () +#if __has_include () #define PIN_ANIMATED_AVAILABLE 1 -#import +#import #import #else #define PIN_ANIMATED_AVAILABLE 0 #endif +#if __has_include() +#define PIN_WEBP_AVAILABLE 1 +#else +#define PIN_WEBP_AVAILABLE 0 +#endif + #import #import #import @@ -42,35 +48,23 @@ @end -@interface PINAnimatedImage (ASPINRemoteImageDownloader) +@interface PINCachedAnimatedImage (ASPINRemoteImageDownloader) @end -@implementation PINAnimatedImage (ASPINRemoteImageDownloader) - -- (void)setCoverImageReadyCallback:(void (^)(UIImage * _Nonnull))coverImageReadyCallback -{ - self.infoCompletion = coverImageReadyCallback; -} - -- (void (^)(UIImage * _Nonnull))coverImageReadyCallback -{ - return self.infoCompletion; -} - -- (void)setPlaybackReadyCallback:(dispatch_block_t)playbackReadyCallback -{ - self.fileReady = playbackReadyCallback; -} - -- (dispatch_block_t)playbackReadyCallback -{ - return self.fileReady; -} +@implementation PINCachedAnimatedImage (ASPINRemoteImageDownloader) - (BOOL)isDataSupported:(NSData *)data { - return [data pin_isGIF]; + if ([data pin_isGIF]) { + return YES; + } +#if PIN_WEBP_AVAILABLE + else if ([data pin_isAnimatedWebP]) { + return YES; + } +#endif + return NO; } @end @@ -187,7 +181,7 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; #if PIN_ANIMATED_AVAILABLE - (nullable id )animatedImageWithData:(NSData *)animatedImageData { - return [[PINAnimatedImage alloc] initWithAnimatedImageData:animatedImageData]; + return [[PINCachedAnimatedImage alloc] initWithAnimatedImageData:animatedImageData]; } #endif @@ -365,6 +359,12 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; if ([data pin_isGIF]) { return data; } +#if PIN_WEBP_AVAILABLE + else if ([data pin_isAnimatedWebP]) { + return data; + } +#endif + #endif return nil; } diff --git a/Source/Private/ASImageNode+AnimatedImagePrivate.h b/Source/Private/ASImageNode+AnimatedImagePrivate.h index 6e57d51dd1..bd2a4d3099 100644 --- a/Source/Private/ASImageNode+AnimatedImagePrivate.h +++ b/Source/Private/ASImageNode+AnimatedImagePrivate.h @@ -26,6 +26,7 @@ extern NSString *const ASAnimatedImageDefaultRunLoopMode; BOOL _animatedImagePaused; NSString *_animatedImageRunLoopMode; CADisplayLink *_displayLink; + NSUInteger _lastSuccessfulFrameIndex; //accessed on main thread only CFTimeInterval _playHead; diff --git a/Texture.podspec b/Texture.podspec index 5c353ed258..6bc7c16212 100644 --- a/Texture.podspec +++ b/Texture.podspec @@ -45,7 +45,7 @@ Pod::Spec.new do |spec| end spec.subspec 'PINRemoteImage' do |pin| - pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.12' + pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.13' pin.dependency 'PINRemoteImage/PINCache' pin.dependency 'Texture/Core' end diff --git a/examples/AnimatedGIF/ASAnimatedImage/ViewController.m b/examples/AnimatedGIF/ASAnimatedImage/ViewController.m index 90d88fbe27..b4286440f5 100644 --- a/examples/AnimatedGIF/ASAnimatedImage/ViewController.m +++ b/examples/AnimatedGIF/ASAnimatedImage/ViewController.m @@ -1,20 +1,18 @@ // // ViewController.m -// Sample -// -// Created by Garrett Moon on 3/22/16. +// Texture // // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // This source code is licensed under the BSD-style license found in the -// LICENSE file in the root directory of this source tree. An additional grant -// of patent rights can be found in the PATENTS file in the same directory. +// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. // -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 // #import "ViewController.h" @@ -33,6 +31,8 @@ ASNetworkImageNode *imageNode = [[ASNetworkImageNode alloc] init]; imageNode.URL = [NSURL URLWithString:@"https://s-media-cache-ak0.pinimg.com/originals/07/44/38/074438e7c75034df2dcf37ba1057803e.gif"]; + // Uncomment to see animated webp support + // imageNode.URL = [NSURL URLWithString:@"https://storage.googleapis.com/downloads.webmproject.org/webp/images/dancing_banana2.lossless.webp"]; imageNode.frame = self.view.bounds; imageNode.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; imageNode.contentMode = UIViewContentModeScaleAspectFit; diff --git a/examples/AnimatedGIF/Podfile b/examples/AnimatedGIF/Podfile index 922ff50ec1..e784c52d14 100644 --- a/examples/AnimatedGIF/Podfile +++ b/examples/AnimatedGIF/Podfile @@ -2,5 +2,6 @@ source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' target 'Sample' do pod 'Texture', :path => '../..' + pod 'PINRemoteImage/WebP' end