Animated WebP support (#605)

* Updating to support animated WebP

* Fix a deadlock with display link

* Fix playhead issue.

* Fix up timing on iOS 10 and above

* Don't redraw the same frame over and over

* Clear out layer contents if we're an animated GIF on exit range

* Clear out cover image on exit of visible range

* Don't set cover image if we're no longer in display range.

* Don't clear out image if we're not an animated image

* Only set image if we're not already animating

* Get rid of changes to podfile

* Add CHANGELOG entry

* Update license

* Update PINRemoteImage

* Remove commented out lines in example
This commit is contained in:
Garrett Moon
2017-10-09 11:48:32 -07:00
committed by GitHub
parent 1705ec0140
commit 4dbb6441d7
9 changed files with 95 additions and 53 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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?

View File

@@ -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];
}
}

View File

@@ -24,14 +24,20 @@
#import <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/ASImageContainerProtocolCategories.h>
#if __has_include (<PINRemoteImage/PINAnimatedImage.h>)
#if __has_include (<PINRemoteImage/PINGIFAnimatedImage.h>)
#define PIN_ANIMATED_AVAILABLE 1
#import <PINRemoteImage/PINAnimatedImage.h>
#import <PINRemoteImage/PINCachedAnimatedImage.h>
#import <PINRemoteImage/PINAlternateRepresentationProvider.h>
#else
#define PIN_ANIMATED_AVAILABLE 0
#endif
#if __has_include(<webp/decode.h>)
#define PIN_WEBP_AVAILABLE 1
#else
#define PIN_WEBP_AVAILABLE 0
#endif
#import <PINRemoteImage/PINRemoteImageManager.h>
#import <PINRemoteImage/NSData+ImageDetectors.h>
#import <PINRemoteImage/PINRemoteImageCaching.h>
@@ -42,35 +48,23 @@
@end
@interface PINAnimatedImage (ASPINRemoteImageDownloader) <ASAnimatedImageProtocol>
@interface PINCachedAnimatedImage (ASPINRemoteImageDownloader) <ASAnimatedImageProtocol>
@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 <ASAnimatedImageProtocol>)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;
}

View File

@@ -26,6 +26,7 @@ extern NSString *const ASAnimatedImageDefaultRunLoopMode;
BOOL _animatedImagePaused;
NSString *_animatedImageRunLoopMode;
CADisplayLink *_displayLink;
NSUInteger _lastSuccessfulFrameIndex;
//accessed on main thread only
CFTimeInterval _playHead;

View File

@@ -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

View File

@@ -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;

View File

@@ -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