├── .gitignore ├── CHANGES ├── FLAnimatedImage.podspec ├── FLAnimatedImage.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── FLAnimatedImage.xcscheme ├── FLAnimatedImage.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── FLAnimatedImage ├── FLAnimatedImage.m ├── FLAnimatedImageView.m ├── Info.plist └── include │ ├── FLAnimatedImage.h │ └── FLAnimatedImageView.h ├── FLAnimatedImageDemo.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── FLAnimatedImageDemo ├── AppDelegate.h ├── AppDelegate.m ├── DebugView.h ├── DebugView.m ├── FLAnimatedImageDemo-Info.plist ├── FLAnimatedImageDemo-Prefix.pch ├── FrameCacheView.h ├── FrameCacheView.m ├── GraphView.h ├── GraphView.m ├── Images.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── ICON-1024.png │ │ ├── Icon-76.png │ │ ├── Icon-76@2x.png │ │ └── Icon-83.5@2x.png │ └── LaunchImage.launchimage │ │ └── Contents.json ├── PlayheadView.h ├── PlayheadView.m ├── RSPlayPauseButton.h ├── RSPlayPauseButton.m ├── RootViewController.h ├── RootViewController.m ├── en.lproj │ └── InfoPlist.strings ├── main.m └── test-gifs │ └── rock.gif ├── LICENSE ├── Package.swift ├── README.md └── images └── flanimatedimage-demo-player.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Global .gitignore 2 | # Files such as .DS_Store on OS X should be excluded system-wide: $ git config --global core.excludesfile "~/.gitignore_global_osx" 3 | 4 | # Xcode 5 | build/* 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | *.xcworkspacedata 18 | 19 | # CocoaPods 20 | # 21 | # We recommend against adding the Pods directory to your .gitignore. However 22 | # you should judge for yourself, the pros and cons are mentioned at: 23 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control? 24 | # 25 | # Pods/ 26 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | = 1.0.17 (2022-07-27) 2 | 3 | * Fix playback speed issue on 120Hz displays (Matt Pennig @pennig) 4 | 5 | = 1.0.16 (2021-04-21) 6 | 7 | * Add support for Swift Packet Manager (SPM) (Bennie Botha @bennie-atlassian) 8 | 9 | * Fix warnings and bump min version to iOS 9 (Raphael Schaad @raphaelschaad) 10 | 11 | = 1.0.15 (2021-01-30) 12 | 13 | * Release long-overdue new version including multiple small fixes (Raphael Schaad @raphaelschaad) 14 | 15 | = 1.0.14 (2017-09-22) 16 | 17 | * Allow FLAnimatedImageView instances to set the accessibilityIgnoresInvertColors property (Colin Caufield @cjcaufield) 18 | 19 | = 1.0.13 (2017-09-11) 20 | 21 | * Prevent FLAnimatedImageView from inverting on iOS 11 when Smart Invert Colors is enabled (Colin Caufield @cjcaufield) 22 | 23 | = 1.0.12 (2016-02-19) 24 | 25 | * Fix crash triggered by using certain FLAnimatedImageView initializers (Tim Johnsen @timonus) 26 | 27 | * Fix crash when running demo project on device (Tim Johnsen @timonus) 28 | 29 | = 1.0.11 (2016-02-17) 30 | 31 | * Allow for run loop mode customization (Ignacio Romero Zurbuchen @dzenbot) 32 | 33 | * Add initialization flags that allow for reduced memory consumption (Tim Johnsen @timonus) 34 | 35 | * Crash fix (Alec Geatches @mopsled) 36 | 37 | = 1.0.10 (2015-12-11) 38 | 39 | * Optimize performance by reducing CADisplayLink callbacks (Eric Jensen @ejensen) 40 | 41 | * Make FLAnimatedImage automatically pause when hidden (Daniel Amitay @danielamitay) 42 | 43 | * Decouple FLAnimatedImage from any particular logging solution (Tim Johnsen @timonus) 44 | 45 | = 1.0.9 (2015-12-01) 46 | 47 | * Support Carthage (Quanlong He @cybertk, Alexander Grebenyuk @kean) 48 | 49 | * Add loop completion block (Kevin Delannoy @delannoyk) 50 | 51 | * Improve resilience against GIFs with corrupted frames (Tim Johnsen @timonus) 52 | 53 | * Disable image source decoded image caching (Aditya KD @caughtinflux) 54 | 55 | * Update sample project to support iOS 9 TLS (Alexander Grebenyuk @kean) 56 | 57 | * Update sample project to cache remote GIFs (Tim Johnsen @timonus) 58 | 59 | * Turn on -Wundef warning flag (Tim Johnsen @timonus) 60 | 61 | = 1.0.8 (2015-01-16) 62 | 63 | * Fix occasional crash during memory warning (Chris Marcellino @chrismarcellino) 64 | 65 | * Drop iOS 5 compatibility code and bump minimum version in preparation for a bug fix using iOS 6+ API (Raphael Schaad @raphaelschaad) 66 | 67 | = 1.0.7 (2014-11-20) 68 | 69 | * CocoaLumberjack 2.x-beta compatibility (Raphael Schaad @raphaelschaad) 70 | 71 | = 1.0.6 (2014-11-20) 72 | 73 | * Decouple project and library CocoaLumberjack log levels (Raphael Schaad @raphaelschaad) 74 | 75 | = 1.0.5 (2014-11-20) 76 | 77 | * Fix regression from v1.0.2 that prevented animation on single-core devices iPhone 3GS and 4, iPod Touch 3rd and 4th gen, and iPad 1st gen (Raphael Schaad @raphaelschaad) 78 | 79 | = 1.0.4 (2014-11-01) 80 | 81 | * Add convenience initializer `+animatedImageWithGIFData:` and improve existing initialization semantics (Raphael Schaad @raphaelschaad) 82 | 83 | * Use custom CocoaLumberjack log level to avoid `ddLogLevel` re-definition (or lack of definition) issues (Raphael Schaad @raphaelschaad) 84 | 85 | = 1.0.3 (2014-10-29) 86 | 87 | * Move to non-const ddLogLevel for better configurability of log level (Raphael Schaad @raphaelschaad) 88 | 89 | * Allow FLLumberjackIntegrationEnabled and FLDebugLoggingEnabled to be configured externally (Vinicius Baggio Fuentes @vinibaggio) 90 | 91 | = 1.0.2 (2014-10-21) 92 | 93 | * Move to #import single header FLAnimatedImage.h (Raphael Schaad @raphaelschaad) 94 | 95 | * Fix potential crash during memory warning (Raphael Schaad @raphaelschaad, Ryan Olson @ryanolsonk) 96 | 97 | * Import UIKit in FLAnimatedImage to allow omitting the prefix header and enabling its usage as Swift module (Nolan Waite @nolanw) 98 | 99 | * Fix issue where an animated image view would go black on receiving a touch inside a collection view cell/getting highlighted (Nolan Waite @nolanw, Aditya KD @caughtinflux) 100 | 101 | * Support Auto Layout better by sizing the image view's correctly (Nolan Waite @nolanw) 102 | 103 | * Add CocoaLumberjack logging integration (Raphael Schaad @raphaelschaad) 104 | 105 | * Fix issue where setting a still image didn't display when an animated image previously was displayed in the same view (Nikolai Sander @nikolaiSa) 106 | 107 | * Enable continuous playback when scrolling in a UIScrollView (Raphael Schaad @raphaelschaad) 108 | 109 | * In the demo project, make debug views work with async image loading. (Tim Johnsen @tijoinc) 110 | 111 | = 1.0.1 (2014-08-13) 112 | 113 | * Fix issue where the minimum frame delay time of 0.02s sometimes wasn't supported because of float rounding (Andrej Mihajlov @pronebird) 114 | 115 | * Fix crash in `FLAnimatedImageView` when `-stopAnimating` is called from `-[UIImageView dealloc]` (Ryan Olson @ryanolsonk) 116 | 117 | * Fix build issue "Use of undeclared identifier 'BYTE_SIZE'" when compiling for device with Xcode 6 (Raphael Schaad @raphaelschaad) 118 | 119 | * Optimization by avoiding continued production of frames on a background thread when the image is going away (Raphael Schaad @raphaelschaad) 120 | 121 | * Allow tracking arbitrary debugging data for any instance of `FLAnimatedImage` (Raphael Schaad @raphaelschaad) 122 | 123 | * In the demo project, move loading of remote GIFs off the main thread (Raphael Schaad @raphaelschaad) 124 | 125 | = 1.0.0 (2014-05-29) 126 | 127 | * Initial commit (Raphael Schaad @raphaelschaad) 128 | -------------------------------------------------------------------------------- /FLAnimatedImage.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = "FLAnimatedImage" 3 | spec.version = "1.0.17" 4 | spec.summary = "Performant animated GIF engine for iOS" 5 | spec.description = <<-DESC 6 | - Plays multiple GIFs simultaneously with a playback speed comparable to desktop browsers 7 | - Honors variable frame delays 8 | - Behaves gracefully under memory pressure 9 | - Eliminates delays or blocking during the first playback loop 10 | - Interprets the frame delays of fast GIFs the same way modern browsers do 11 | 12 | It's a well-tested [component that powers all GIFs in Flipboard](http://engineering.flipboard.com/2014/05/animated-gif/). 13 | DESC 14 | 15 | spec.homepage = "https://github.com/Flipboard/FLAnimatedImage" 16 | spec.screenshots = "https://github.com/Flipboard/FLAnimatedImage/raw/master/images/flanimatedimage-demo-player.gif" 17 | spec.license = { :type => "MIT", :file => "LICENSE" } 18 | spec.author = { "Raphael Schaad" => "raphael.schaad@gmail.com" } 19 | spec.platform = :ios, "9.0" 20 | spec.source = { :git => "https://github.com/Flipboard/FLAnimatedImage.git", :tag => "1.0.17" } 21 | spec.source_files = "FLAnimatedImage/**/*.{h,m}" 22 | spec.frameworks = "QuartzCore", "ImageIO", "CoreGraphics" 23 | spec.requires_arc = true 24 | end 25 | -------------------------------------------------------------------------------- /FLAnimatedImage.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0C22DAED1BCABF4F006E1D3B /* FLAnimatedImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 0C22DAE81BCABF4F006E1D3B /* FLAnimatedImage.m */; }; 11 | 0C22DAEF1BCABF4F006E1D3B /* FLAnimatedImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0C22DAEA1BCABF4F006E1D3B /* FLAnimatedImageView.m */; }; 12 | 84C35646241D1B2D00052A01 /* FLAnimatedImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 84AB1DD9241D0A4F0039C1C7 /* FLAnimatedImage.h */; settings = {ATTRIBUTES = (Public, ); }; }; 13 | 84C35647241D1B2D00052A01 /* FLAnimatedImageView.h in Headers */ = {isa = PBXBuildFile; fileRef = 84AB1DDA241D0A4F0039C1C7 /* FLAnimatedImageView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 0C22DAE81BCABF4F006E1D3B /* FLAnimatedImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLAnimatedImage.m; sourceTree = ""; }; 18 | 0C22DAEA1BCABF4F006E1D3B /* FLAnimatedImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLAnimatedImageView.m; sourceTree = ""; }; 19 | 0C22DAEB1BCABF4F006E1D3B /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 20 | 84AB1DD9241D0A4F0039C1C7 /* FLAnimatedImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FLAnimatedImage.h; path = include/FLAnimatedImage.h; sourceTree = ""; }; 21 | 84AB1DDA241D0A4F0039C1C7 /* FLAnimatedImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FLAnimatedImageView.h; path = include/FLAnimatedImageView.h; sourceTree = ""; }; 22 | 92C9BC0C1B199DC500D79B06 /* FLAnimatedImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FLAnimatedImage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | /* End PBXFileReference section */ 24 | 25 | /* Begin PBXFrameworksBuildPhase section */ 26 | 92C9BC081B199DC500D79B06 /* Frameworks */ = { 27 | isa = PBXFrameworksBuildPhase; 28 | buildActionMask = 2147483647; 29 | files = ( 30 | ); 31 | runOnlyForDeploymentPostprocessing = 0; 32 | }; 33 | /* End PBXFrameworksBuildPhase section */ 34 | 35 | /* Begin PBXGroup section */ 36 | 0C22DAE61BCABF4F006E1D3B /* FLAnimatedImage */ = { 37 | isa = PBXGroup; 38 | children = ( 39 | 84AB1DD9241D0A4F0039C1C7 /* FLAnimatedImage.h */, 40 | 84AB1DDA241D0A4F0039C1C7 /* FLAnimatedImageView.h */, 41 | 0C22DAE81BCABF4F006E1D3B /* FLAnimatedImage.m */, 42 | 0C22DAEA1BCABF4F006E1D3B /* FLAnimatedImageView.m */, 43 | 0C22DAEB1BCABF4F006E1D3B /* Info.plist */, 44 | ); 45 | path = FLAnimatedImage; 46 | sourceTree = ""; 47 | }; 48 | 92C9BC021B199DC500D79B06 = { 49 | isa = PBXGroup; 50 | children = ( 51 | 0C22DAE61BCABF4F006E1D3B /* FLAnimatedImage */, 52 | 92C9BC0D1B199DC500D79B06 /* Products */, 53 | ); 54 | sourceTree = ""; 55 | }; 56 | 92C9BC0D1B199DC500D79B06 /* Products */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 92C9BC0C1B199DC500D79B06 /* FLAnimatedImage.framework */, 60 | ); 61 | name = Products; 62 | sourceTree = ""; 63 | }; 64 | /* End PBXGroup section */ 65 | 66 | /* Begin PBXHeadersBuildPhase section */ 67 | 92C9BC091B199DC500D79B06 /* Headers */ = { 68 | isa = PBXHeadersBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | 84C35646241D1B2D00052A01 /* FLAnimatedImage.h in Headers */, 72 | 84C35647241D1B2D00052A01 /* FLAnimatedImageView.h in Headers */, 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | /* End PBXHeadersBuildPhase section */ 77 | 78 | /* Begin PBXNativeTarget section */ 79 | 92C9BC0B1B199DC500D79B06 /* FLAnimatedImage */ = { 80 | isa = PBXNativeTarget; 81 | buildConfigurationList = 92C9BC221B199DC600D79B06 /* Build configuration list for PBXNativeTarget "FLAnimatedImage" */; 82 | buildPhases = ( 83 | 92C9BC071B199DC500D79B06 /* Sources */, 84 | 92C9BC081B199DC500D79B06 /* Frameworks */, 85 | 92C9BC091B199DC500D79B06 /* Headers */, 86 | 92C9BC0A1B199DC500D79B06 /* Resources */, 87 | ); 88 | buildRules = ( 89 | ); 90 | dependencies = ( 91 | ); 92 | name = FLAnimatedImage; 93 | productName = FLAnimatedImage; 94 | productReference = 92C9BC0C1B199DC500D79B06 /* FLAnimatedImage.framework */; 95 | productType = "com.apple.product-type.framework"; 96 | }; 97 | /* End PBXNativeTarget section */ 98 | 99 | /* Begin PBXProject section */ 100 | 92C9BC031B199DC500D79B06 /* Project object */ = { 101 | isa = PBXProject; 102 | attributes = { 103 | LastUpgradeCheck = 1240; 104 | ORGANIZATIONNAME = com.flipboard; 105 | TargetAttributes = { 106 | 92C9BC0B1B199DC500D79B06 = { 107 | CreatedOnToolsVersion = 6.3.1; 108 | }; 109 | }; 110 | }; 111 | buildConfigurationList = 92C9BC061B199DC500D79B06 /* Build configuration list for PBXProject "FLAnimatedImage" */; 112 | compatibilityVersion = "Xcode 3.2"; 113 | developmentRegion = en; 114 | hasScannedForEncodings = 0; 115 | knownRegions = ( 116 | en, 117 | Base, 118 | ); 119 | mainGroup = 92C9BC021B199DC500D79B06; 120 | productRefGroup = 92C9BC0D1B199DC500D79B06 /* Products */; 121 | projectDirPath = ""; 122 | projectRoot = ""; 123 | targets = ( 124 | 92C9BC0B1B199DC500D79B06 /* FLAnimatedImage */, 125 | ); 126 | }; 127 | /* End PBXProject section */ 128 | 129 | /* Begin PBXResourcesBuildPhase section */ 130 | 92C9BC0A1B199DC500D79B06 /* Resources */ = { 131 | isa = PBXResourcesBuildPhase; 132 | buildActionMask = 2147483647; 133 | files = ( 134 | ); 135 | runOnlyForDeploymentPostprocessing = 0; 136 | }; 137 | /* End PBXResourcesBuildPhase section */ 138 | 139 | /* Begin PBXSourcesBuildPhase section */ 140 | 92C9BC071B199DC500D79B06 /* Sources */ = { 141 | isa = PBXSourcesBuildPhase; 142 | buildActionMask = 2147483647; 143 | files = ( 144 | 0C22DAED1BCABF4F006E1D3B /* FLAnimatedImage.m in Sources */, 145 | 0C22DAEF1BCABF4F006E1D3B /* FLAnimatedImageView.m in Sources */, 146 | ); 147 | runOnlyForDeploymentPostprocessing = 0; 148 | }; 149 | /* End PBXSourcesBuildPhase section */ 150 | 151 | /* Begin XCBuildConfiguration section */ 152 | 92C9BC201B199DC600D79B06 /* Debug */ = { 153 | isa = XCBuildConfiguration; 154 | buildSettings = { 155 | ALWAYS_SEARCH_USER_PATHS = NO; 156 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 157 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 158 | CLANG_CXX_LIBRARY = "libc++"; 159 | CLANG_ENABLE_MODULES = YES; 160 | CLANG_ENABLE_OBJC_ARC = YES; 161 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 162 | CLANG_WARN_BOOL_CONVERSION = YES; 163 | CLANG_WARN_COMMA = YES; 164 | CLANG_WARN_CONSTANT_CONVERSION = YES; 165 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 166 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 167 | CLANG_WARN_EMPTY_BODY = YES; 168 | CLANG_WARN_ENUM_CONVERSION = YES; 169 | CLANG_WARN_INFINITE_RECURSION = YES; 170 | CLANG_WARN_INT_CONVERSION = YES; 171 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 172 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 173 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 174 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 175 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 176 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 177 | CLANG_WARN_STRICT_PROTOTYPES = YES; 178 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 179 | CLANG_WARN_UNREACHABLE_CODE = YES; 180 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 181 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 182 | COPY_PHASE_STRIP = NO; 183 | CURRENT_PROJECT_VERSION = 1; 184 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 185 | ENABLE_STRICT_OBJC_MSGSEND = YES; 186 | ENABLE_TESTABILITY = YES; 187 | GCC_C_LANGUAGE_STANDARD = gnu99; 188 | GCC_DYNAMIC_NO_PIC = NO; 189 | GCC_NO_COMMON_BLOCKS = YES; 190 | GCC_OPTIMIZATION_LEVEL = 0; 191 | GCC_PREPROCESSOR_DEFINITIONS = ( 192 | "DEBUG=1", 193 | "$(inherited)", 194 | ); 195 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 196 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 197 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 198 | GCC_WARN_UNDECLARED_SELECTOR = YES; 199 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 200 | GCC_WARN_UNUSED_FUNCTION = YES; 201 | GCC_WARN_UNUSED_VARIABLE = YES; 202 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 203 | MTL_ENABLE_DEBUG_INFO = YES; 204 | ONLY_ACTIVE_ARCH = YES; 205 | SDKROOT = iphoneos; 206 | TARGETED_DEVICE_FAMILY = "1,2"; 207 | VERSIONING_SYSTEM = "apple-generic"; 208 | VERSION_INFO_PREFIX = ""; 209 | }; 210 | name = Debug; 211 | }; 212 | 92C9BC211B199DC600D79B06 /* Release */ = { 213 | isa = XCBuildConfiguration; 214 | buildSettings = { 215 | ALWAYS_SEARCH_USER_PATHS = NO; 216 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 217 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 218 | CLANG_CXX_LIBRARY = "libc++"; 219 | CLANG_ENABLE_MODULES = YES; 220 | CLANG_ENABLE_OBJC_ARC = YES; 221 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 222 | CLANG_WARN_BOOL_CONVERSION = YES; 223 | CLANG_WARN_COMMA = YES; 224 | CLANG_WARN_CONSTANT_CONVERSION = YES; 225 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 226 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 227 | CLANG_WARN_EMPTY_BODY = YES; 228 | CLANG_WARN_ENUM_CONVERSION = YES; 229 | CLANG_WARN_INFINITE_RECURSION = YES; 230 | CLANG_WARN_INT_CONVERSION = YES; 231 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 232 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 233 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 234 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 235 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 236 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 237 | CLANG_WARN_STRICT_PROTOTYPES = YES; 238 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 239 | CLANG_WARN_UNREACHABLE_CODE = YES; 240 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 241 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 242 | COPY_PHASE_STRIP = NO; 243 | CURRENT_PROJECT_VERSION = 1; 244 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 245 | ENABLE_NS_ASSERTIONS = NO; 246 | ENABLE_STRICT_OBJC_MSGSEND = YES; 247 | GCC_C_LANGUAGE_STANDARD = gnu99; 248 | GCC_NO_COMMON_BLOCKS = YES; 249 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 250 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 251 | GCC_WARN_UNDECLARED_SELECTOR = YES; 252 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 253 | GCC_WARN_UNUSED_FUNCTION = YES; 254 | GCC_WARN_UNUSED_VARIABLE = YES; 255 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 256 | MTL_ENABLE_DEBUG_INFO = NO; 257 | SDKROOT = iphoneos; 258 | TARGETED_DEVICE_FAMILY = "1,2"; 259 | VALIDATE_PRODUCT = YES; 260 | VERSIONING_SYSTEM = "apple-generic"; 261 | VERSION_INFO_PREFIX = ""; 262 | }; 263 | name = Release; 264 | }; 265 | 92C9BC231B199DC600D79B06 /* Debug */ = { 266 | isa = XCBuildConfiguration; 267 | buildSettings = { 268 | APPLICATION_EXTENSION_API_ONLY = YES; 269 | DEFINES_MODULE = YES; 270 | DYLIB_COMPATIBILITY_VERSION = 1; 271 | DYLIB_CURRENT_VERSION = 1; 272 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 273 | INFOPLIST_FILE = "$(SRCROOT)/FLAnimatedImage/Info.plist"; 274 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 275 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 276 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 277 | PRODUCT_BUNDLE_IDENTIFIER = "com.flipboard.$(PRODUCT_NAME:rfc1034identifier)"; 278 | PRODUCT_NAME = "$(TARGET_NAME)"; 279 | SKIP_INSTALL = YES; 280 | }; 281 | name = Debug; 282 | }; 283 | 92C9BC241B199DC600D79B06 /* Release */ = { 284 | isa = XCBuildConfiguration; 285 | buildSettings = { 286 | APPLICATION_EXTENSION_API_ONLY = YES; 287 | DEFINES_MODULE = YES; 288 | DYLIB_COMPATIBILITY_VERSION = 1; 289 | DYLIB_CURRENT_VERSION = 1; 290 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 291 | INFOPLIST_FILE = "$(SRCROOT)/FLAnimatedImage/Info.plist"; 292 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 293 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 294 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 295 | PRODUCT_BUNDLE_IDENTIFIER = "com.flipboard.$(PRODUCT_NAME:rfc1034identifier)"; 296 | PRODUCT_NAME = "$(TARGET_NAME)"; 297 | SKIP_INSTALL = YES; 298 | }; 299 | name = Release; 300 | }; 301 | /* End XCBuildConfiguration section */ 302 | 303 | /* Begin XCConfigurationList section */ 304 | 92C9BC061B199DC500D79B06 /* Build configuration list for PBXProject "FLAnimatedImage" */ = { 305 | isa = XCConfigurationList; 306 | buildConfigurations = ( 307 | 92C9BC201B199DC600D79B06 /* Debug */, 308 | 92C9BC211B199DC600D79B06 /* Release */, 309 | ); 310 | defaultConfigurationIsVisible = 0; 311 | defaultConfigurationName = Release; 312 | }; 313 | 92C9BC221B199DC600D79B06 /* Build configuration list for PBXNativeTarget "FLAnimatedImage" */ = { 314 | isa = XCConfigurationList; 315 | buildConfigurations = ( 316 | 92C9BC231B199DC600D79B06 /* Debug */, 317 | 92C9BC241B199DC600D79B06 /* Release */, 318 | ); 319 | defaultConfigurationIsVisible = 0; 320 | defaultConfigurationName = Release; 321 | }; 322 | /* End XCConfigurationList section */ 323 | }; 324 | rootObject = 92C9BC031B199DC500D79B06 /* Project object */; 325 | } 326 | -------------------------------------------------------------------------------- /FLAnimatedImage.xcodeproj/xcshareddata/xcschemes/FLAnimatedImage.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 77 | 83 | 84 | 85 | 86 | 92 | 93 | 99 | 100 | 101 | 102 | 104 | 105 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /FLAnimatedImage.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /FLAnimatedImage.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FLAnimatedImage/FLAnimatedImage.m: -------------------------------------------------------------------------------- 1 | // 2 | // FLAnimatedImage.m 3 | // Flipboard 4 | // 5 | // Created by Raphael Schaad on 7/8/13. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import "FLAnimatedImage.h" 11 | #import 12 | #if __has_include() 13 | #import 14 | #else 15 | #import 16 | #endif 17 | 18 | 19 | // From vm_param.h, define for iOS 8.0 or higher to build on device. 20 | #ifndef BYTE_SIZE 21 | #define BYTE_SIZE 8 // byte size in bits 22 | #endif 23 | 24 | #define MEGABYTE (1024 * 1024) 25 | 26 | // This is how the fastest browsers do it as per 2012: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser-compatibility 27 | const NSTimeInterval kFLAnimatedImageDelayTimeIntervalMinimum = 0.02; 28 | 29 | // An animated image's data size (dimensions * frameCount) category; its value is the max allowed memory (in MB). 30 | // E.g.: A 100x200px GIF with 30 frames is ~2.3MB in our pixel format and would fall into the `FLAnimatedImageDataSizeCategoryAll` category. 31 | typedef NS_ENUM(NSUInteger, FLAnimatedImageDataSizeCategory) { 32 | FLAnimatedImageDataSizeCategoryAll = 10, // All frames permanently in memory (be nice to the CPU) 33 | FLAnimatedImageDataSizeCategoryDefault = 75, // A frame cache of default size in memory (usually real-time performance and keeping low memory profile) 34 | FLAnimatedImageDataSizeCategoryOnDemand = 250, // Only keep one frame at the time in memory (easier on memory, slowest performance) 35 | FLAnimatedImageDataSizeCategoryUnsupported // Even for one frame too large, computer says no. 36 | }; 37 | 38 | typedef NS_ENUM(NSUInteger, FLAnimatedImageFrameCacheSize) { 39 | FLAnimatedImageFrameCacheSizeNoLimit = 0, // 0 means no specific limit 40 | FLAnimatedImageFrameCacheSizeLowMemory = 1, // The minimum frame cache size; this will produce frames on-demand. 41 | FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning = 2, // If we can produce the frames faster than we consume, one frame ahead will already result in a stutter-free playback. 42 | FLAnimatedImageFrameCacheSizeDefault = 5 // Build up a comfy buffer window to cope with CPU hiccups etc. 43 | }; 44 | 45 | 46 | #if defined(DEBUG) && DEBUG 47 | @protocol FLAnimatedImageDebugDelegate 48 | @optional 49 | - (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didUpdateCachedFrames:(NSIndexSet *)indexesOfFramesInCache; 50 | - (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didRequestCachedFrame:(NSUInteger)index; 51 | - (CGFloat)debug_animatedImagePredrawingSlowdownFactor:(FLAnimatedImage *)animatedImage; 52 | @end 53 | #endif 54 | 55 | 56 | @interface FLAnimatedImage () 57 | 58 | @property (nonatomic, assign, readonly) NSUInteger frameCacheSizeOptimal; // The optimal number of frames to cache based on image size & number of frames; never changes 59 | @property (nonatomic, assign, readonly, getter=isPredrawingEnabled) BOOL predrawingEnabled; // Enables predrawing of images to improve performance. 60 | @property (nonatomic, assign) NSUInteger frameCacheSizeMaxInternal; // Allow to cap the cache size e.g. when memory warnings occur; 0 means no specific limit (default) 61 | @property (nonatomic, assign) NSUInteger requestedFrameIndex; // Most recently requested frame index 62 | @property (nonatomic, assign, readonly) NSUInteger posterImageFrameIndex; // Index of non-purgable poster image; never changes 63 | @property (nonatomic, strong, readonly) NSMutableDictionary *cachedFramesForIndexes; 64 | @property (nonatomic, strong, readonly) NSMutableIndexSet *cachedFrameIndexes; // Indexes of cached frames 65 | @property (nonatomic, strong, readonly) NSMutableIndexSet *requestedFrameIndexes; // Indexes of frames that are currently produced in the background 66 | @property (nonatomic, strong, readonly) NSIndexSet *allFramesIndexSet; // Default index set with the full range of indexes; never changes 67 | @property (nonatomic, assign) NSUInteger memoryWarningCount; 68 | @property (nonatomic, strong, readonly) dispatch_queue_t serialQueue; 69 | @property (nonatomic, strong, readonly) __attribute__((NSObject)) CGImageSourceRef imageSource; 70 | 71 | // The weak proxy is used to break retain cycles with delayed actions from memory warnings. 72 | // We are lying about the actual type here to gain static type checking and eliminate casts. 73 | // The actual type of the object is `FLWeakProxy`. 74 | @property (nonatomic, strong, readonly) FLAnimatedImage *weakProxy; 75 | 76 | #if defined(DEBUG) && DEBUG 77 | @property (nonatomic, weak) id debug_delegate; 78 | #endif 79 | 80 | @end 81 | 82 | 83 | // For custom dispatching of memory warnings to avoid deallocation races since NSNotificationCenter doesn't retain objects it is notifying. 84 | static NSHashTable *allAnimatedImagesWeak; 85 | 86 | @implementation FLAnimatedImage 87 | 88 | #pragma mark - Accessors 89 | #pragma mark Public 90 | 91 | // This is the definite value the frame cache needs to size itself to. 92 | - (NSUInteger)frameCacheSizeCurrent 93 | { 94 | NSUInteger frameCacheSizeCurrent = self.frameCacheSizeOptimal; 95 | 96 | // If set, respect the caps. 97 | if (self.frameCacheSizeMax > FLAnimatedImageFrameCacheSizeNoLimit) { 98 | frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMax); 99 | } 100 | 101 | if (self.frameCacheSizeMaxInternal > FLAnimatedImageFrameCacheSizeNoLimit) { 102 | frameCacheSizeCurrent = MIN(frameCacheSizeCurrent, self.frameCacheSizeMaxInternal); 103 | } 104 | 105 | return frameCacheSizeCurrent; 106 | } 107 | 108 | 109 | - (void)setFrameCacheSizeMax:(NSUInteger)frameCacheSizeMax 110 | { 111 | if (_frameCacheSizeMax != frameCacheSizeMax) { 112 | 113 | // Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed. 114 | const BOOL willFrameCacheSizeShrink = (frameCacheSizeMax < self.frameCacheSizeCurrent); 115 | 116 | // Update the value 117 | _frameCacheSizeMax = frameCacheSizeMax; 118 | 119 | if (willFrameCacheSizeShrink) { 120 | [self purgeFrameCacheIfNeeded]; 121 | } 122 | } 123 | } 124 | 125 | 126 | #pragma mark Private 127 | 128 | - (void)setFrameCacheSizeMaxInternal:(NSUInteger)frameCacheSizeMaxInternal 129 | { 130 | if (_frameCacheSizeMaxInternal != frameCacheSizeMaxInternal) { 131 | 132 | // Remember whether the new cap will cause the current cache size to shrink; then we'll make sure to purge from the cache if needed. 133 | BOOL willFrameCacheSizeShrink = (frameCacheSizeMaxInternal < self.frameCacheSizeCurrent); 134 | 135 | // Update the value 136 | _frameCacheSizeMaxInternal = frameCacheSizeMaxInternal; 137 | 138 | if (willFrameCacheSizeShrink) { 139 | [self purgeFrameCacheIfNeeded]; 140 | } 141 | } 142 | } 143 | 144 | 145 | #pragma mark - Life Cycle 146 | 147 | + (void)initialize 148 | { 149 | if (self == [FLAnimatedImage class]) { 150 | // UIKit memory warning notification handler shared by all of the instances 151 | allAnimatedImagesWeak = [NSHashTable weakObjectsHashTable]; 152 | 153 | [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(NSNotification *note) { 154 | // UIKit notifications are posted on the main thread. didReceiveMemoryWarning: is expecting the main run loop, and we don't lock on allAnimatedImagesWeak 155 | NSAssert([NSThread isMainThread], @"Received memory warning on non-main thread"); 156 | // Get a strong reference to all of the images. If an instance is returned in this array, it is still live and has not entered dealloc. 157 | // Note that FLAnimatedImages can be created on any thread, so the hash table must be locked. 158 | NSArray *images = nil; 159 | @synchronized(allAnimatedImagesWeak) { 160 | images = [[allAnimatedImagesWeak allObjects] copy]; 161 | } 162 | // Now issue notifications to all of the images while holding a strong reference to them 163 | [images makeObjectsPerformSelector:@selector(didReceiveMemoryWarning:) withObject:note]; 164 | }]; 165 | } 166 | } 167 | 168 | 169 | - (instancetype)init 170 | { 171 | FLAnimatedImage *_Nullable const animatedImage = [self initWithAnimatedGIFData:nil]; 172 | if (!animatedImage) { 173 | FLLog(FLLogLevelError, @"Use `-initWithAnimatedGIFData:` and supply the animated GIF data as an argument to initialize an object of type `FLAnimatedImage`."); 174 | } 175 | return animatedImage; 176 | } 177 | 178 | 179 | - (instancetype)initWithAnimatedGIFData:(NSData *)data 180 | { 181 | return [self initWithAnimatedGIFData:data optimalFrameCacheSize:0 predrawingEnabled:YES]; 182 | } 183 | 184 | - (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled 185 | { 186 | // Early return if no data supplied! 187 | const BOOL hasData = (data.length > 0); 188 | if (!hasData) { 189 | FLLog(FLLogLevelError, @"No animated GIF data supplied."); 190 | return nil; 191 | } 192 | 193 | self = [super init]; 194 | if (self) { 195 | // Do one-time initializations of `readonly` properties directly to ivar to prevent implicit actions and avoid need for private `readwrite` property overrides. 196 | 197 | // Keep a strong reference to `data` and expose it read-only publicly. 198 | // However, we will use the `_imageSource` as handler to the image data throughout our life cycle. 199 | _data = data; 200 | _predrawingEnabled = isPredrawingEnabled; 201 | 202 | // Initialize internal data structures 203 | _cachedFramesForIndexes = [[NSMutableDictionary alloc] init]; 204 | _cachedFrameIndexes = [[NSMutableIndexSet alloc] init]; 205 | _requestedFrameIndexes = [[NSMutableIndexSet alloc] init]; 206 | 207 | // Note: We could leverage `CGImageSourceCreateWithURL` too to add a second initializer `-initWithAnimatedGIFContentsOfURL:`. 208 | _imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, 209 | (__bridge CFDictionaryRef)@{(NSString *)kCGImageSourceShouldCache: @NO}); 210 | // Early return on failure! 211 | if (!_imageSource) { 212 | FLLog(FLLogLevelError, @"Failed to `CGImageSourceCreateWithData` for animated GIF data %@", data); 213 | return nil; 214 | } 215 | 216 | // Early return if not GIF! 217 | const CFStringRef _Nullable imageSourceContainerType = CGImageSourceGetType(_imageSource); 218 | const BOOL isGIFData = imageSourceContainerType ? UTTypeConformsTo(imageSourceContainerType, kUTTypeGIF) : NO; 219 | if (!isGIFData) { 220 | FLLog(FLLogLevelError, @"Supplied data is of type %@ and doesn't seem to be GIF data %@", imageSourceContainerType, data); 221 | return nil; 222 | } 223 | 224 | // Get `LoopCount` 225 | // Note: 0 means repeating the animation indefinitely. 226 | // Image properties example: 227 | // { 228 | // FileSize = 314446; 229 | // "{GIF}" = { 230 | // HasGlobalColorMap = 1; 231 | // LoopCount = 0; 232 | // }; 233 | // } 234 | NSDictionary *_Nullable const imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(_imageSource, NULL); 235 | _loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue]; 236 | 237 | // Iterate through frame images 238 | const size_t imageCount = CGImageSourceGetCount(_imageSource); 239 | NSUInteger skippedFrameCount = 0; 240 | NSMutableDictionary *const delayTimesForIndexesMutable = [NSMutableDictionary dictionaryWithCapacity:imageCount]; 241 | for (size_t i = 0; i < imageCount; i++) { 242 | @autoreleasepool { 243 | const CGImageRef _Nullable frameImageRef = CGImageSourceCreateImageAtIndex(_imageSource, i, NULL); 244 | if (frameImageRef) { 245 | UIImage *frameImage = [UIImage imageWithCGImage:frameImageRef]; 246 | // Check for valid `frameImage` before parsing its properties as frames can be corrupted (and `frameImage` even `nil` when `frameImageRef` was valid). 247 | if (frameImage) { 248 | // Set poster image 249 | if (!self.posterImage) { 250 | _posterImage = frameImage; 251 | // Set its size to proxy our size. 252 | _size = _posterImage.size; 253 | // Remember index of poster image so we never purge it; also add it to the cache. 254 | _posterImageFrameIndex = i; 255 | [self.cachedFramesForIndexes setObject:self.posterImage forKey:@(self.posterImageFrameIndex)]; 256 | [self.cachedFrameIndexes addIndex:self.posterImageFrameIndex]; 257 | } 258 | 259 | // Get `DelayTime` 260 | // Note: It's not in (1/100) of a second like still falsely described in the documentation as per iOS 8 (rdar://19507384) but in seconds stored as `kCFNumberFloat32Type`. 261 | // Frame properties example: 262 | // { 263 | // ColorModel = RGB; 264 | // Depth = 8; 265 | // PixelHeight = 960; 266 | // PixelWidth = 640; 267 | // "{GIF}" = { 268 | // DelayTime = "0.4"; 269 | // UnclampedDelayTime = "0.4"; 270 | // }; 271 | // } 272 | 273 | NSDictionary *_Nullable const frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(_imageSource, i, NULL); 274 | NSDictionary *_Nullable const framePropertiesGIF = [frameProperties objectForKey:(id)kCGImagePropertyGIFDictionary]; 275 | 276 | // Try to use the unclamped delay time; fall back to the normal delay time. 277 | NSNumber *_Nullable delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFUnclampedDelayTime]; 278 | if (delayTime == nil) { 279 | delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFDelayTime]; 280 | } 281 | // If we don't get a delay time from the properties, fall back to `kDelayTimeIntervalDefault` or carry over the preceding frame's value. 282 | const NSTimeInterval kDelayTimeIntervalDefault = 0.1; 283 | if (delayTime == nil) { 284 | if (i == 0) { 285 | FLLog(FLLogLevelInfo, @"Falling back to default delay time for first frame %@ because none found in GIF properties %@", frameImage, frameProperties); 286 | delayTime = @(kDelayTimeIntervalDefault); 287 | } else { 288 | FLLog(FLLogLevelInfo, @"Falling back to preceding delay time for frame %zu %@ because none found in GIF properties %@", i, frameImage, frameProperties); 289 | delayTime = delayTimesForIndexesMutable[@(i - 1)]; 290 | } 291 | } 292 | // Support frame delays as low as `kFLAnimatedImageDelayTimeIntervalMinimum`, with anything below being rounded up to `kDelayTimeIntervalDefault` for legacy compatibility. 293 | // To support the minimum even when rounding errors occur, use an epsilon when comparing. We downcast to float because that's what we get for delayTime from ImageIO. 294 | if ([delayTime floatValue] < ((float)kFLAnimatedImageDelayTimeIntervalMinimum - FLT_EPSILON)) { 295 | FLLog(FLLogLevelInfo, @"Rounding frame %zu's `delayTime` from %f up to default %f (minimum supported: %f).", i, [delayTime floatValue], kDelayTimeIntervalDefault, kFLAnimatedImageDelayTimeIntervalMinimum); 296 | delayTime = @(kDelayTimeIntervalDefault); 297 | } 298 | delayTimesForIndexesMutable[@(i)] = delayTime; 299 | } else { 300 | skippedFrameCount++; 301 | FLLog(FLLogLevelInfo, @"Dropping frame %zu because valid `CGImageRef` %@ did result in `nil`-`UIImage`.", i, frameImageRef); 302 | } 303 | CFRelease(frameImageRef); 304 | } else { 305 | skippedFrameCount++; 306 | FLLog(FLLogLevelInfo, @"Dropping frame %zu because failed to `CGImageSourceCreateImageAtIndex` with image source %@", i, self->_imageSource); 307 | } 308 | } 309 | } 310 | _delayTimesForIndexes = [delayTimesForIndexesMutable copy]; 311 | _frameCount = imageCount; 312 | 313 | if (self.frameCount == 0) { 314 | FLLog(FLLogLevelInfo, @"Failed to create any valid frames for GIF with properties %@", imageProperties); 315 | return nil; 316 | } else if (self.frameCount == 1) { 317 | // Warn when we only have a single frame but return a valid GIF. 318 | FLLog(FLLogLevelInfo, @"Created valid GIF but with only a single frame. Image properties: %@", imageProperties); 319 | } else { 320 | // We have multiple frames, rock on! 321 | } 322 | 323 | // If no value is provided, select a default based on the GIF. 324 | if (optimalFrameCacheSize == 0) { 325 | // Calculate the optimal frame cache size: try choosing a larger buffer window depending on the predicted image size. 326 | // It's only dependent on the image size & number of frames and never changes. 327 | const CGFloat animatedImageDataSize = (CGFloat)CGImageGetBytesPerRow(self.posterImage.CGImage) * self.size.height * (CGFloat)(self.frameCount - skippedFrameCount) / (CGFloat)MEGABYTE; 328 | if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryAll) { 329 | _frameCacheSizeOptimal = self.frameCount; 330 | } else if (animatedImageDataSize <= FLAnimatedImageDataSizeCategoryDefault) { 331 | // This value doesn't depend on device memory much because if we're not keeping all frames in memory we will always be decoding 1 frame up ahead per 1 frame that gets played and at this point we might as well just keep a small buffer just large enough to keep from running out of frames. 332 | _frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeDefault; 333 | } else { 334 | // The predicted size exceeds the limits to build up a cache and we go into low memory mode from the beginning. 335 | _frameCacheSizeOptimal = FLAnimatedImageFrameCacheSizeLowMemory; 336 | } 337 | } else { 338 | // Use the provided value. 339 | _frameCacheSizeOptimal = optimalFrameCacheSize; 340 | } 341 | // In any case, cap the optimal cache size at the frame count. 342 | _frameCacheSizeOptimal = MIN(_frameCacheSizeOptimal, self.frameCount); 343 | 344 | // Convenience/minor performance optimization; keep an index set handy with the full range to return in `-frameIndexesToCache`. 345 | _allFramesIndexSet = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, self.frameCount)]; 346 | 347 | // See the property declarations for descriptions. 348 | _weakProxy = (id)[FLWeakProxy weakProxyForObject:self]; 349 | 350 | // Register this instance in the weak table for memory notifications. The NSHashTable will clean up after itself when we're gone. 351 | // Note that FLAnimatedImages can be created on any thread, so the hash table must be locked. 352 | @synchronized(allAnimatedImagesWeak) { 353 | [allAnimatedImagesWeak addObject:self]; 354 | } 355 | } 356 | return self; 357 | } 358 | 359 | 360 | + (instancetype)animatedImageWithGIFData:(NSData *)data 361 | { 362 | FLAnimatedImage *const animatedImage = [[FLAnimatedImage alloc] initWithAnimatedGIFData:data]; 363 | return animatedImage; 364 | } 365 | 366 | 367 | - (void)dealloc 368 | { 369 | if (_weakProxy) { 370 | [NSObject cancelPreviousPerformRequestsWithTarget:_weakProxy]; 371 | } 372 | 373 | if (_imageSource) { 374 | CFRelease(_imageSource); 375 | } 376 | } 377 | 378 | 379 | #pragma mark - Public Methods 380 | 381 | // See header for more details. 382 | // Note: both consumer and producer are throttled: consumer by frame timings and producer by the available memory (max buffer window size). 383 | - (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index 384 | { 385 | // Early return if the requested index is beyond bounds. 386 | // Note: We're comparing an index with a count and need to bail on greater than or equal to. 387 | if (index >= self.frameCount) { 388 | FLLog(FLLogLevelWarn, @"Skipping requested frame %lu beyond bounds (total frame count: %lu) for animated image: %@", (unsigned long)index, (unsigned long)self.frameCount, self); 389 | return nil; 390 | } 391 | 392 | // Remember requested frame index, this influences what we should cache next. 393 | self.requestedFrameIndex = index; 394 | #if defined(DEBUG) && DEBUG 395 | if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImage:didRequestCachedFrame:)]) { 396 | [self.debug_delegate debug_animatedImage:self didRequestCachedFrame:index]; 397 | } 398 | #endif 399 | 400 | // Quick check to avoid doing any work if we already have all possible frames cached, a common case. 401 | if ([self.cachedFrameIndexes count] < self.frameCount) { 402 | // If we have frames that should be cached but aren't and aren't requested yet, request them. 403 | // Exclude existing cached frames, frames already requested, and specially cached poster image. 404 | NSMutableIndexSet *frameIndexesToAddToCacheMutable = [self frameIndexesToCache]; 405 | [frameIndexesToAddToCacheMutable removeIndexes:self.cachedFrameIndexes]; 406 | [frameIndexesToAddToCacheMutable removeIndexes:self.requestedFrameIndexes]; 407 | [frameIndexesToAddToCacheMutable removeIndex:self.posterImageFrameIndex]; 408 | NSIndexSet *frameIndexesToAddToCache = [frameIndexesToAddToCacheMutable copy]; 409 | 410 | // Asynchronously add frames to our cache. 411 | if ([frameIndexesToAddToCache count] > 0) { 412 | [self addFrameIndexesToCache:frameIndexesToAddToCache]; 413 | } 414 | } 415 | 416 | // Get the specified image. 417 | UIImage *const image = self.cachedFramesForIndexes[@(index)]; 418 | 419 | // Purge if needed based on the current playhead position. 420 | [self purgeFrameCacheIfNeeded]; 421 | 422 | return image; 423 | } 424 | 425 | 426 | // Only called once from `-imageLazilyCachedAtIndex` but factored into its own method for logical grouping. 427 | - (void)addFrameIndexesToCache:(NSIndexSet *)frameIndexesToAddToCache 428 | { 429 | // Order matters. First, iterate over the indexes starting from the requested frame index. 430 | // Then, if there are any indexes before the requested frame index, do those. 431 | const NSRange firstRange = NSMakeRange(self.requestedFrameIndex, self.frameCount - self.requestedFrameIndex); 432 | const NSRange secondRange = NSMakeRange(0, self.requestedFrameIndex); 433 | if (firstRange.length + secondRange.length != self.frameCount) { 434 | FLLog(FLLogLevelWarn, @"Two-part frame cache range doesn't equal full range."); 435 | } 436 | 437 | // Add to the requested list before we actually kick them off, so they don't get into the queue twice. 438 | [self.requestedFrameIndexes addIndexes:frameIndexesToAddToCache]; 439 | 440 | // Lazily create dedicated isolation queue. 441 | if (!self.serialQueue) { 442 | _serialQueue = dispatch_queue_create("com.flipboard.framecachingqueue", DISPATCH_QUEUE_SERIAL); 443 | } 444 | 445 | // Start streaming requested frames in the background into the cache. 446 | // Avoid capturing self in the block as there's no reason to keep doing work if the animated image went away. 447 | __weak __typeof(self) weakSelf = self; 448 | dispatch_async(self.serialQueue, ^{ 449 | // Produce and cache next needed frame. 450 | void (^frameRangeBlock)(NSRange, BOOL *) = ^(NSRange range, BOOL *stop) { 451 | // Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`. 452 | for (NSUInteger i = range.location; i < NSMaxRange(range); i++) { 453 | #if defined(DEBUG) && DEBUG 454 | const CFTimeInterval predrawBeginTime = CACurrentMediaTime(); 455 | #endif 456 | UIImage *const image = [weakSelf imageAtIndex:i]; 457 | #if defined(DEBUG) && DEBUG 458 | const CFTimeInterval predrawDuration = CACurrentMediaTime() - predrawBeginTime; 459 | CFTimeInterval slowdownDuration = 0.0; 460 | if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImagePredrawingSlowdownFactor:)]) { 461 | CGFloat predrawingSlowdownFactor = [self.debug_delegate debug_animatedImagePredrawingSlowdownFactor:self]; 462 | slowdownDuration = predrawDuration * predrawingSlowdownFactor - predrawDuration; 463 | [NSThread sleepForTimeInterval:slowdownDuration]; 464 | } 465 | FLLog(FLLogLevelVerbose, @"Predrew frame %lu in %f ms for animated image: %@", (unsigned long)i, (predrawDuration + slowdownDuration) * 1000, self); 466 | #endif 467 | // The results get returned one by one as soon as they're ready (and not in batch). 468 | // The benefits of having the first frames as quick as possible outweigh building up a buffer to cope with potential hiccups when the CPU suddenly gets busy. 469 | if (image && weakSelf) { 470 | dispatch_async(dispatch_get_main_queue(), ^{ 471 | weakSelf.cachedFramesForIndexes[@(i)] = image; 472 | [weakSelf.cachedFrameIndexes addIndex:i]; 473 | [weakSelf.requestedFrameIndexes removeIndex:i]; 474 | #if defined(DEBUG) && DEBUG 475 | if ([weakSelf.debug_delegate respondsToSelector:@selector(debug_animatedImage:didUpdateCachedFrames:)]) { 476 | [weakSelf.debug_delegate debug_animatedImage:weakSelf didUpdateCachedFrames:weakSelf.cachedFrameIndexes]; 477 | } 478 | #endif 479 | }); 480 | } 481 | } 482 | }; 483 | 484 | [frameIndexesToAddToCache enumerateRangesInRange:firstRange options:0 usingBlock:frameRangeBlock]; 485 | [frameIndexesToAddToCache enumerateRangesInRange:secondRange options:0 usingBlock:frameRangeBlock]; 486 | }); 487 | } 488 | 489 | 490 | + (CGSize)sizeForImage:(id)image 491 | { 492 | CGSize imageSize = CGSizeZero; 493 | 494 | // Early return for nil 495 | if (!image) { 496 | return imageSize; 497 | } 498 | 499 | if ([image isKindOfClass:[UIImage class]]) { 500 | UIImage *const uiImage = (UIImage *)image; 501 | imageSize = uiImage.size; 502 | } else if ([image isKindOfClass:[FLAnimatedImage class]]) { 503 | FLAnimatedImage *const animatedImage = (FLAnimatedImage *)image; 504 | imageSize = animatedImage.size; 505 | } else { 506 | // Bear trap to capture bad images; we have seen crashers cropping up on iOS 7. 507 | FLLog(FLLogLevelError, @"`image` isn't of expected types `UIImage` or `FLAnimatedImage`: %@", image); 508 | } 509 | 510 | return imageSize; 511 | } 512 | 513 | 514 | #pragma mark - Private Methods 515 | #pragma mark Frame Loading 516 | 517 | - (UIImage *)imageAtIndex:(NSUInteger)index 518 | { 519 | // It's very important to use the cached `_imageSource` since the random access to a frame with `CGImageSourceCreateImageAtIndex` turns from an O(1) into an O(n) operation when re-initializing the image source every time. 520 | const CGImageRef _Nullable imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL); 521 | 522 | // Early return for nil 523 | if (!imageRef) { 524 | return nil; 525 | } 526 | 527 | UIImage *image = [UIImage imageWithCGImage:imageRef]; 528 | CFRelease(imageRef); 529 | 530 | // Loading in the image object is only half the work, the displaying image view would still have to synchronosly wait and decode the image, so we go ahead and do that here on the background thread. 531 | if (self.isPredrawingEnabled) { 532 | image = [[self class] predrawnImageFromImage:image]; 533 | } 534 | 535 | return image; 536 | } 537 | 538 | 539 | #pragma mark Frame Caching 540 | 541 | - (NSMutableIndexSet *)frameIndexesToCache 542 | { 543 | NSMutableIndexSet *indexesToCache = nil; 544 | // Quick check to avoid building the index set if the number of frames to cache equals the total frame count. 545 | if (self.frameCacheSizeCurrent == self.frameCount) { 546 | indexesToCache = [self.allFramesIndexSet mutableCopy]; 547 | } else { 548 | indexesToCache = [[NSMutableIndexSet alloc] init]; 549 | 550 | // Add indexes to the set in two separate blocks- the first starting from the requested frame index, up to the limit or the end. 551 | // The second, if needed, the remaining number of frames beginning at index zero. 552 | const NSUInteger firstLength = MIN(self.frameCacheSizeCurrent, self.frameCount - self.requestedFrameIndex); 553 | const NSRange firstRange = NSMakeRange(self.requestedFrameIndex, firstLength); 554 | [indexesToCache addIndexesInRange:firstRange]; 555 | const NSUInteger secondLength = self.frameCacheSizeCurrent - firstLength; 556 | if (secondLength > 0) { 557 | NSRange secondRange = NSMakeRange(0, secondLength); 558 | [indexesToCache addIndexesInRange:secondRange]; 559 | } 560 | // Double check our math, before we add the poster image index which may increase it by one. 561 | if ([indexesToCache count] != self.frameCacheSizeCurrent) { 562 | FLLog(FLLogLevelWarn, @"Number of frames to cache doesn't equal expected cache size."); 563 | } 564 | 565 | [indexesToCache addIndex:self.posterImageFrameIndex]; 566 | } 567 | 568 | return indexesToCache; 569 | } 570 | 571 | 572 | - (void)purgeFrameCacheIfNeeded 573 | { 574 | // Purge frames that are currently cached but don't need to be. 575 | // But not if we're still under the number of frames to cache. 576 | // This way, if all frames are allowed to be cached (the common case), we can skip all the `NSIndexSet` math below. 577 | if ([self.cachedFrameIndexes count] > self.frameCacheSizeCurrent) { 578 | NSMutableIndexSet *indexesToPurge = [self.cachedFrameIndexes mutableCopy]; 579 | [indexesToPurge removeIndexes:[self frameIndexesToCache]]; 580 | [indexesToPurge enumerateRangesUsingBlock:^(NSRange range, BOOL *stop) { 581 | // Iterate through contiguous indexes; can be faster than `enumerateIndexesInRange:options:usingBlock:`. 582 | for (NSUInteger i = range.location; i < NSMaxRange(range); i++) { 583 | [self.cachedFrameIndexes removeIndex:i]; 584 | [self.cachedFramesForIndexes removeObjectForKey:@(i)]; 585 | // Note: Don't `CGImageSourceRemoveCacheAtIndex` on the image source for frames that we don't want cached any longer to maintain O(1) time access. 586 | #if defined(DEBUG) && DEBUG 587 | if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImage:didUpdateCachedFrames:)]) { 588 | dispatch_async(dispatch_get_main_queue(), ^{ 589 | [self.debug_delegate debug_animatedImage:self didUpdateCachedFrames:self.cachedFrameIndexes]; 590 | }); 591 | } 592 | #endif 593 | } 594 | }]; 595 | } 596 | } 597 | 598 | 599 | - (void)growFrameCacheSizeAfterMemoryWarning:(NSNumber *)frameCacheSize 600 | { 601 | self.frameCacheSizeMaxInternal = [frameCacheSize unsignedIntegerValue]; 602 | FLLog(FLLogLevelDebug, @"Grew frame cache size max to %lu after memory warning for animated image: %@", (unsigned long)self.frameCacheSizeMaxInternal, self); 603 | 604 | // Schedule resetting the frame cache size max completely after a while. 605 | const NSTimeInterval kResetDelay = 3.0; 606 | [self.weakProxy performSelector:@selector(resetFrameCacheSizeMaxInternal) withObject:nil afterDelay:kResetDelay]; 607 | } 608 | 609 | 610 | - (void)resetFrameCacheSizeMaxInternal 611 | { 612 | self.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeNoLimit; 613 | FLLog(FLLogLevelDebug, @"Reset frame cache size max (current frame cache size: %lu) for animated image: %@", (unsigned long)self.frameCacheSizeCurrent, self); 614 | } 615 | 616 | 617 | #pragma mark System Memory Warnings Notification Handler 618 | 619 | - (void)didReceiveMemoryWarning:(NSNotification *)notification 620 | { 621 | self.memoryWarningCount++; 622 | 623 | // If we were about to grow larger, but got rapped on our knuckles by the system again, cancel. 624 | [NSObject cancelPreviousPerformRequestsWithTarget:self.weakProxy selector:@selector(growFrameCacheSizeAfterMemoryWarning:) object:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning)]; 625 | [NSObject cancelPreviousPerformRequestsWithTarget:self.weakProxy selector:@selector(resetFrameCacheSizeMaxInternal) object:nil]; 626 | 627 | // Go down to the minimum and by that implicitly immediately purge from the cache if needed to not get jettisoned by the system and start producing frames on-demand. 628 | FLLog(FLLogLevelDebug, @"Attempt setting frame cache size max to %lu (previous was %lu) after memory warning #%lu for animated image: %@", (unsigned long)FLAnimatedImageFrameCacheSizeLowMemory, (unsigned long)self.frameCacheSizeMaxInternal, (unsigned long)self.memoryWarningCount, self); 629 | self.frameCacheSizeMaxInternal = FLAnimatedImageFrameCacheSizeLowMemory; 630 | 631 | // Schedule growing larger again after a while, but cap our attempts to prevent a periodic sawtooth wave (ramps upward and then sharply drops) of memory usage. 632 | // 633 | // [mem]^ (2) (5) (6) 1) Loading frames for the first time 634 | // (*)| , , , 2) Mem warning #1; purge cache 635 | // | /| (4)/| /| 3) Grow cache size a bit after a while, if no mem warning occurs 636 | // | / | _/ | _/ | 4) Try to grow cache size back to optimum after a while, if no mem warning occurs 637 | // |(1)/ |_/ |/ |__(7) 5) Mem warning #2; purge cache 638 | // |__/ (3) 6) After repetition of (3) and (4), mem warning #3; purge cache 639 | // +----------------------> 7) After 3 mem warnings, stay at minimum cache size 640 | // [t] 641 | // *) The mem high water mark before we get warned might change for every cycle. 642 | // 643 | const NSUInteger kGrowAttemptsMax = 2; 644 | const NSTimeInterval kGrowDelay = 2.0; 645 | if ((self.memoryWarningCount - 1) <= kGrowAttemptsMax) { 646 | [self.weakProxy performSelector:@selector(growFrameCacheSizeAfterMemoryWarning:) withObject:@(FLAnimatedImageFrameCacheSizeGrowAfterMemoryWarning) afterDelay:kGrowDelay]; 647 | } 648 | 649 | // Note: It's not possible to get the level of a memory warning with a public API: http://stackoverflow.com/questions/2915247/iphone-os-memory-warnings-what-do-the-different-levels-mean/2915477#2915477 650 | } 651 | 652 | 653 | #pragma mark Image Decoding 654 | 655 | // Decodes the image's data and draws it off-screen fully in memory; it's thread-safe and hence can be called on a background thread. 656 | // On success, the returned object is a new `UIImage` instance with the same content as the one passed in. 657 | // On failure, the returned object is the unchanged passed in one; the data will not be predrawn in memory though and an error will be logged. 658 | // First inspired by & good Karma to: https://gist.github.com/steipete/1144242 659 | + (UIImage *)predrawnImageFromImage:(UIImage *)imageToPredraw 660 | { 661 | // Always use a device RGB color space for simplicity and predictability what will be going on. 662 | const CGColorSpaceRef _Nullable colorSpaceDeviceRGBRef = CGColorSpaceCreateDeviceRGB(); 663 | // Early return on failure! 664 | if (!colorSpaceDeviceRGBRef) { 665 | FLLog(FLLogLevelError, @"Failed to `CGColorSpaceCreateDeviceRGB` for image %@", imageToPredraw); 666 | return imageToPredraw; 667 | } 668 | 669 | // Even when the image doesn't have transparency, we have to add the extra channel because Quartz doesn't support other pixel formats than 32 bpp/8 bpc for RGB: 670 | // kCGImageAlphaNoneSkipFirst, kCGImageAlphaNoneSkipLast, kCGImageAlphaPremultipliedFirst, kCGImageAlphaPremultipliedLast 671 | // (source: docs "Quartz 2D Programming Guide > Graphics Contexts > Table 2-1 Pixel formats supported for bitmap graphics contexts") 672 | const size_t numberOfComponents = CGColorSpaceGetNumberOfComponents(colorSpaceDeviceRGBRef) + 1; // 4: RGB + A 673 | 674 | // "In iOS 4.0 and later, and OS X v10.6 and later, you can pass NULL if you want Quartz to allocate memory for the bitmap." (source: docs) 675 | void *_Nullable data = NULL; 676 | const size_t width = imageToPredraw.size.width; 677 | const size_t height = imageToPredraw.size.height; 678 | const size_t bitsPerComponent = CHAR_BIT; 679 | 680 | const size_t bitsPerPixel = (bitsPerComponent * numberOfComponents); 681 | const size_t bytesPerPixel = (bitsPerPixel / BYTE_SIZE); 682 | const size_t bytesPerRow = (bytesPerPixel * width); 683 | 684 | CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault; 685 | 686 | CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageToPredraw.CGImage); 687 | // If the alpha info doesn't match to one of the supported formats (see above), pick a reasonable supported one. 688 | // "For bitmaps created in iOS 3.2 and later, the drawing environment uses the premultiplied ARGB format to store the bitmap data." (source: docs) 689 | if (alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaOnly) { 690 | alphaInfo = kCGImageAlphaNoneSkipFirst; 691 | } else if (alphaInfo == kCGImageAlphaFirst) { 692 | alphaInfo = kCGImageAlphaPremultipliedFirst; 693 | } else if (alphaInfo == kCGImageAlphaLast) { 694 | alphaInfo = kCGImageAlphaPremultipliedLast; 695 | } 696 | // "The constants for specifying the alpha channel information are declared with the `CGImageAlphaInfo` type but can be passed to this parameter safely." (source: docs) 697 | bitmapInfo |= alphaInfo; 698 | 699 | // Create our own graphics context to draw to; `UIGraphicsGetCurrentContext`/`UIGraphicsBeginImageContextWithOptions` doesn't create a new context but returns the current one which isn't thread-safe (e.g. main thread could use it at the same time). 700 | // Note: It's not worth caching the bitmap context for multiple frames ("unique key" would be `width`, `height` and `hasAlpha`), it's ~50% slower. Time spent in libRIP's `CGSBlendBGRA8888toARGB8888` suddenly shoots up -- not sure why. 701 | const CGContextRef _Nullable bitmapContextRef = CGBitmapContextCreate(data, width, height, bitsPerComponent, bytesPerRow, colorSpaceDeviceRGBRef, bitmapInfo); 702 | CGColorSpaceRelease(colorSpaceDeviceRGBRef); 703 | // Early return on failure! 704 | if (!bitmapContextRef) { 705 | FLLog(FLLogLevelError, @"Failed to `CGBitmapContextCreate` with color space %@ and parameters (width: %zu height: %zu bitsPerComponent: %zu bytesPerRow: %zu) for image %@", colorSpaceDeviceRGBRef, width, height, bitsPerComponent, bytesPerRow, imageToPredraw); 706 | return imageToPredraw; 707 | } 708 | 709 | // Draw image in bitmap context and create image by preserving receiver's properties. 710 | CGContextDrawImage(bitmapContextRef, CGRectMake(0.0, 0.0, imageToPredraw.size.width, imageToPredraw.size.height), imageToPredraw.CGImage); 711 | const CGImageRef _Nullable predrawnImageRef = CGBitmapContextCreateImage(bitmapContextRef); 712 | UIImage *_Nullable predrawnImage = predrawnImageRef ? [UIImage imageWithCGImage:predrawnImageRef scale:imageToPredraw.scale orientation:imageToPredraw.imageOrientation] : nil; 713 | CGImageRelease(predrawnImageRef); 714 | CGContextRelease(bitmapContextRef); 715 | 716 | // Early return on failure! 717 | if (!predrawnImage) { 718 | FLLog(FLLogLevelError, @"Failed to `imageWithCGImage:scale:orientation:` with image ref %@ created with color space %@ and bitmap context %@ and properties and properties (scale: %f orientation: %ld) for image %@", predrawnImageRef, colorSpaceDeviceRGBRef, bitmapContextRef, imageToPredraw.scale, (long)imageToPredraw.imageOrientation, imageToPredraw); 719 | return imageToPredraw; 720 | } 721 | 722 | return predrawnImage; 723 | } 724 | 725 | 726 | #pragma mark - Description 727 | 728 | - (NSString *)description 729 | { 730 | NSString *description = [super description]; 731 | 732 | description = [description stringByAppendingFormat:@" size=%@", NSStringFromCGSize(self.size)]; 733 | description = [description stringByAppendingFormat:@" frameCount=%lu", (unsigned long)self.frameCount]; 734 | 735 | return description; 736 | } 737 | 738 | 739 | @end 740 | 741 | #pragma mark - Logging 742 | 743 | @implementation FLAnimatedImage (Logging) 744 | 745 | static void (^_logBlock)(NSString *logString, FLLogLevel logLevel) = nil; 746 | static FLLogLevel _logLevel; 747 | 748 | + (void)setLogBlock:(void (^_Nullable)(NSString *logString, FLLogLevel logLevel))logBlock logLevel:(FLLogLevel)logLevel 749 | { 750 | _logBlock = [logBlock copy]; 751 | _logLevel = logLevel; 752 | } 753 | 754 | + (void)logStringFromBlock:(NSString *(^_Nullable)(void))stringBlock withLevel:(FLLogLevel)level 755 | { 756 | if (level <= _logLevel && _logBlock && stringBlock) { 757 | _logBlock(stringBlock(), level); 758 | } 759 | } 760 | 761 | @end 762 | 763 | 764 | #pragma mark - FLWeakProxy 765 | 766 | @interface FLWeakProxy () 767 | 768 | @property (nonatomic, weak) id target; 769 | 770 | @end 771 | 772 | 773 | @implementation FLWeakProxy 774 | 775 | #pragma mark Life Cycle 776 | 777 | // This is the designated creation method of an `FLWeakProxy` and 778 | // as a subclass of `NSProxy` it doesn't respond to or need `-init`. 779 | + (instancetype)weakProxyForObject:(id)targetObject 780 | { 781 | FLWeakProxy *weakProxy = [FLWeakProxy alloc]; 782 | weakProxy.target = targetObject; 783 | return weakProxy; 784 | } 785 | 786 | 787 | #pragma mark Forwarding Messages 788 | 789 | - (id)forwardingTargetForSelector:(SEL)selector 790 | { 791 | // Keep it lightweight: access the ivar directly 792 | return _target; 793 | } 794 | 795 | 796 | #pragma mark - NSWeakProxy Method Overrides 797 | #pragma mark Handling Unimplemented Methods 798 | 799 | - (void)forwardInvocation:(NSInvocation *)invocation 800 | { 801 | // Fallback for when target is nil. Don't do anything, just return 0/NULL/nil. 802 | // The method signature we've received to get here is just a dummy to keep `doesNotRecognizeSelector:` from firing. 803 | // We can't really handle struct return types here because we don't know the length. 804 | void *_Nullable nullPointer = NULL; 805 | [invocation setReturnValue:&nullPointer]; 806 | } 807 | 808 | 809 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector 810 | { 811 | // We only get here if `forwardingTargetForSelector:` returns nil. 812 | // In that case, our weak target has been reclaimed. Return a dummy method signature to keep `doesNotRecognizeSelector:` from firing. 813 | // We'll emulate the Obj-c messaging nil behavior by setting the return value to nil in `forwardInvocation:`, but we'll assume that the return value is `sizeof(void *)`. 814 | // Other libraries handle this situation by making use of a global method signature cache, but that seems heavier than necessary and has issues as well. 815 | // See https://www.mikeash.com/pyblog/friday-qa-2010-02-26-futures.html and https://github.com/steipete/PSTDelegateProxy/issues/1 for examples of using a method signature cache. 816 | return [NSObject instanceMethodSignatureForSelector:@selector(init)]; 817 | } 818 | 819 | 820 | @end 821 | -------------------------------------------------------------------------------- /FLAnimatedImage/FLAnimatedImageView.m: -------------------------------------------------------------------------------- 1 | // 2 | // FLAnimatedImageView.h 3 | // Flipboard 4 | // 5 | // Created by Raphael Schaad on 7/8/13. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import "FLAnimatedImageView.h" 11 | #import "FLAnimatedImage.h" 12 | #import 13 | 14 | 15 | #if defined(DEBUG) && DEBUG 16 | @protocol FLAnimatedImageViewDebugDelegate 17 | @optional 18 | - (void)debug_animatedImageView:(FLAnimatedImageView *)animatedImageView waitingForFrame:(NSUInteger)index duration:(NSTimeInterval)duration; 19 | @end 20 | #endif 21 | 22 | 23 | @interface FLAnimatedImageView () 24 | 25 | // Override of public `readonly` properties as private `readwrite` 26 | @property (nonatomic, strong, readwrite) UIImage *currentFrame; 27 | @property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex; 28 | 29 | @property (nonatomic, assign) NSUInteger loopCountdown; 30 | @property (nonatomic, assign) NSTimeInterval accumulator; 31 | @property (nonatomic, strong) CADisplayLink *displayLink; 32 | 33 | @property (nonatomic, assign) BOOL shouldAnimate; // Before checking this value, call `-updateShouldAnimate` whenever the animated image or visibility (window, superview, hidden, alpha) has changed. 34 | @property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable; 35 | 36 | #if defined(DEBUG) && DEBUG 37 | @property (nonatomic, weak) id debug_delegate; 38 | #endif 39 | 40 | @end 41 | 42 | 43 | @implementation FLAnimatedImageView 44 | @synthesize runLoopMode = _runLoopMode; 45 | 46 | #pragma mark - Initializers 47 | 48 | // -initWithImage: isn't documented as a designated initializer of UIImageView, but it actually seems to be. 49 | // Using -initWithImage: doesn't call any of the other designated initializers. 50 | - (instancetype)initWithImage:(UIImage *)image 51 | { 52 | self = [super initWithImage:image]; 53 | if (self) { 54 | [self commonInit]; 55 | } 56 | return self; 57 | } 58 | 59 | // -initWithImage:highlightedImage: also isn't documented as a designated initializer of UIImageView, but it doesn't call any other designated initializers. 60 | - (instancetype)initWithImage:(UIImage *)image highlightedImage:(UIImage *)highlightedImage 61 | { 62 | self = [super initWithImage:image highlightedImage:highlightedImage]; 63 | if (self) { 64 | [self commonInit]; 65 | } 66 | return self; 67 | } 68 | 69 | - (instancetype)initWithFrame:(CGRect)frame 70 | { 71 | self = [super initWithFrame:frame]; 72 | if (self) { 73 | [self commonInit]; 74 | } 75 | return self; 76 | } 77 | 78 | - (instancetype)initWithCoder:(NSCoder *)aDecoder 79 | { 80 | self = [super initWithCoder:aDecoder]; 81 | if (self) { 82 | [self commonInit]; 83 | } 84 | return self; 85 | } 86 | 87 | - (void)commonInit 88 | { 89 | self.runLoopMode = [[self class] defaultRunLoopMode]; 90 | 91 | if (@available(iOS 11.0, *)) { 92 | self.accessibilityIgnoresInvertColors = YES; 93 | } 94 | } 95 | 96 | 97 | #pragma mark - Accessors 98 | #pragma mark Public 99 | 100 | - (void)setAnimatedImage:(FLAnimatedImage *)animatedImage 101 | { 102 | if (![_animatedImage isEqual:animatedImage]) { 103 | if (animatedImage) { 104 | if (super.image) { 105 | // UIImageView's `setImage:` will internally call its layer's `setContentsTransform:` based on the `image.imageOrientation`. 106 | // The `contentsTransform` will affect layer rendering rotation because the CGImage's bitmap buffer does not actually take rotation. 107 | // However, when calling `setImage:nil`, this `contentsTransform` will not be reset to identity. 108 | // Further animation frame will be rendered as rotated. So we must set it to the poster image to clear the previous state. 109 | // See more here: https://github.com/Flipboard/FLAnimatedImage/issues/100 110 | super.image = animatedImage.posterImage; 111 | // Clear out the image. 112 | super.image = nil; 113 | } 114 | // Ensure disabled highlighting; it's not supported (see `-setHighlighted:`). 115 | super.highlighted = NO; 116 | // UIImageView seems to bypass some accessors when calculating its intrinsic content size, so this ensures its intrinsic content size comes from the animated image. 117 | [self invalidateIntrinsicContentSize]; 118 | } else { 119 | // Stop animating before the animated image gets cleared out. 120 | [self stopAnimating]; 121 | } 122 | 123 | _animatedImage = animatedImage; 124 | 125 | self.currentFrame = animatedImage.posterImage; 126 | self.currentFrameIndex = 0; 127 | if (animatedImage.loopCount > 0) { 128 | self.loopCountdown = animatedImage.loopCount; 129 | } else { 130 | self.loopCountdown = NSUIntegerMax; 131 | } 132 | self.accumulator = 0.0; 133 | 134 | // Start animating after the new animated image has been set. 135 | [self updateShouldAnimate]; 136 | if (self.shouldAnimate) { 137 | [self startAnimating]; 138 | } 139 | 140 | [self.layer setNeedsDisplay]; 141 | } 142 | } 143 | 144 | 145 | #pragma mark - Life Cycle 146 | 147 | - (void)dealloc 148 | { 149 | // Removes the display link from all run loop modes. 150 | [_displayLink invalidate]; 151 | } 152 | 153 | 154 | #pragma mark - UIView Method Overrides 155 | #pragma mark Observing View-Related Changes 156 | 157 | - (void)didMoveToSuperview 158 | { 159 | [super didMoveToSuperview]; 160 | 161 | [self updateShouldAnimate]; 162 | if (self.shouldAnimate) { 163 | [self startAnimating]; 164 | } else { 165 | [self stopAnimating]; 166 | } 167 | } 168 | 169 | 170 | - (void)didMoveToWindow 171 | { 172 | [super didMoveToWindow]; 173 | 174 | [self updateShouldAnimate]; 175 | if (self.shouldAnimate) { 176 | [self startAnimating]; 177 | } else { 178 | [self stopAnimating]; 179 | } 180 | } 181 | 182 | - (void)setAlpha:(CGFloat)alpha 183 | { 184 | [super setAlpha:alpha]; 185 | 186 | [self updateShouldAnimate]; 187 | if (self.shouldAnimate) { 188 | [self startAnimating]; 189 | } else { 190 | [self stopAnimating]; 191 | } 192 | } 193 | 194 | - (void)setHidden:(BOOL)hidden 195 | { 196 | [super setHidden:hidden]; 197 | 198 | [self updateShouldAnimate]; 199 | if (self.shouldAnimate) { 200 | [self startAnimating]; 201 | } else { 202 | [self stopAnimating]; 203 | } 204 | } 205 | 206 | 207 | #pragma mark Auto Layout 208 | 209 | - (CGSize)intrinsicContentSize 210 | { 211 | // Default to let UIImageView handle the sizing of its image, and anything else it might consider. 212 | CGSize intrinsicContentSize = [super intrinsicContentSize]; 213 | 214 | // If we have have an animated image, use its image size. 215 | // UIImageView's intrinsic content size seems to be the size of its image. The obvious approach, simply calling `-invalidateIntrinsicContentSize` when setting an animated image, results in UIImageView steadfastly returning `{UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric}` for its intrinsicContentSize. 216 | // (Perhaps UIImageView bypasses its `-image` getter in its implementation of `-intrinsicContentSize`, as `-image` is not called after calling `-invalidateIntrinsicContentSize`.) 217 | if (self.animatedImage) { 218 | intrinsicContentSize = self.image.size; 219 | } 220 | 221 | return intrinsicContentSize; 222 | } 223 | 224 | 225 | #pragma mark - UIImageView Method Overrides 226 | #pragma mark Image Data 227 | 228 | - (UIImage *)image 229 | { 230 | UIImage *image = nil; 231 | if (self.animatedImage) { 232 | // Initially set to the poster image. 233 | image = self.currentFrame; 234 | } else { 235 | image = super.image; 236 | } 237 | return image; 238 | } 239 | 240 | 241 | - (void)setImage:(UIImage *)image 242 | { 243 | if (image) { 244 | // Clear out the animated image and implicitly pause animation playback. 245 | self.animatedImage = nil; 246 | } 247 | 248 | super.image = image; 249 | } 250 | 251 | 252 | #pragma mark Animating Images 253 | 254 | - (NSTimeInterval)frameDelayGreatestCommonDivisor 255 | { 256 | // Presision is set to half of the `kFLAnimatedImageDelayTimeIntervalMinimum` in order to minimize frame dropping. 257 | const NSTimeInterval kGreatestCommonDivisorPrecision = 2.0 / kFLAnimatedImageDelayTimeIntervalMinimum; 258 | 259 | NSArray *const delays = self.animatedImage.delayTimesForIndexes.allValues; 260 | 261 | // Scales the frame delays by `kGreatestCommonDivisorPrecision` 262 | // then converts it to an UInteger for in order to calculate the GCD. 263 | NSUInteger scaledGCD = lrint([delays.firstObject floatValue] * kGreatestCommonDivisorPrecision); 264 | for (NSNumber *value in delays) { 265 | scaledGCD = gcd(lrint([value floatValue] * kGreatestCommonDivisorPrecision), scaledGCD); 266 | } 267 | 268 | // Reverse to scale to get the value back into seconds. 269 | return (double)scaledGCD / kGreatestCommonDivisorPrecision; 270 | } 271 | 272 | 273 | static NSUInteger gcd(NSUInteger a, NSUInteger b) 274 | { 275 | // http://en.wikipedia.org/wiki/Greatest_common_divisor 276 | if (a < b) { 277 | return gcd(b, a); 278 | } else if (a == b) { 279 | return b; 280 | } 281 | 282 | while (true) { 283 | const NSUInteger remainder = a % b; 284 | if (remainder == 0) { 285 | return b; 286 | } 287 | a = b; 288 | b = remainder; 289 | } 290 | } 291 | 292 | 293 | - (void)startAnimating 294 | { 295 | if (self.animatedImage) { 296 | // Lazily create the display link. 297 | if (!self.displayLink) { 298 | // It is important to note the use of a weak proxy here to avoid a retain cycle. `-displayLinkWithTarget:selector:` 299 | // will retain its target until it is invalidated. We use a weak proxy so that the image view will get deallocated 300 | // independent of the display link's lifetime. Upon image view deallocation, we invalidate the display 301 | // link which will lead to the deallocation of both the display link and the weak proxy. 302 | FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self]; 303 | self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)]; 304 | 305 | [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode]; 306 | } 307 | 308 | if (@available(iOS 10, *)) { 309 | // Adjusting preferredFramesPerSecond allows us to skip unnecessary calls to displayDidRefresh: when showing GIFs 310 | // that don't animate quickly. Use ceil to err on the side of too many FPS so we don't miss a frame transition moment. 311 | self.displayLink.preferredFramesPerSecond = ceil(1.0 / [self frameDelayGreatestCommonDivisor]); 312 | } else { 313 | const NSTimeInterval kDisplayRefreshRate = 60.0; // 60Hz 314 | self.displayLink.frameInterval = MAX([self frameDelayGreatestCommonDivisor] * kDisplayRefreshRate, 1); 315 | } 316 | self.displayLink.paused = NO; 317 | } else { 318 | [super startAnimating]; 319 | } 320 | } 321 | 322 | - (void)setRunLoopMode:(NSRunLoopMode)runLoopMode 323 | { 324 | if (![@[NSDefaultRunLoopMode, NSRunLoopCommonModes] containsObject:runLoopMode]) { 325 | NSAssert(NO, @"Invalid run loop mode: %@", runLoopMode); 326 | _runLoopMode = [[self class] defaultRunLoopMode]; 327 | } else { 328 | _runLoopMode = runLoopMode; 329 | } 330 | } 331 | 332 | - (void)stopAnimating 333 | { 334 | if (self.animatedImage) { 335 | self.displayLink.paused = YES; 336 | } else { 337 | [super stopAnimating]; 338 | } 339 | } 340 | 341 | 342 | - (BOOL)isAnimating 343 | { 344 | BOOL isAnimating = NO; 345 | if (self.animatedImage) { 346 | isAnimating = self.displayLink && !self.displayLink.isPaused; 347 | } else { 348 | isAnimating = [super isAnimating]; 349 | } 350 | return isAnimating; 351 | } 352 | 353 | 354 | #pragma mark Highlighted Image Unsupport 355 | 356 | - (void)setHighlighted:(BOOL)highlighted 357 | { 358 | // Highlighted image is unsupported for animated images, but implementing it breaks the image view when embedded in a UICollectionViewCell. 359 | if (!self.animatedImage) { 360 | [super setHighlighted:highlighted]; 361 | } 362 | } 363 | 364 | 365 | #pragma mark - Private Methods 366 | #pragma mark Animation 367 | 368 | // Don't repeatedly check our window & superview in `-displayDidRefresh:` for performance reasons. 369 | // Just update our cached value whenever the animated image or visibility (window, superview, hidden, alpha) is changed. 370 | - (void)updateShouldAnimate 371 | { 372 | const BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0; 373 | self.shouldAnimate = self.animatedImage && isVisible; 374 | } 375 | 376 | 377 | - (void)displayDidRefresh:(CADisplayLink *)displayLink 378 | { 379 | // If for some reason a wild call makes it through when we shouldn't be animating, bail. 380 | // Early return! 381 | if (!self.shouldAnimate) { 382 | FLLog(FLLogLevelWarn, @"Trying to animate image when we shouldn't: %@", self); 383 | return; 384 | } 385 | 386 | NSNumber *_Nullable const delayTimeNumber = [self.animatedImage.delayTimesForIndexes objectForKey:@(self.currentFrameIndex)]; 387 | // If we don't have a frame delay (e.g. corrupt frame), don't update the view but skip the playhead to the next frame (in else-block). 388 | if (delayTimeNumber != nil) { 389 | const NSTimeInterval delayTime = [delayTimeNumber floatValue]; 390 | // If we have a nil image (e.g. waiting for frame), don't update the view nor playhead. 391 | UIImage *_Nullable const image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex]; 392 | if (image) { 393 | FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage); 394 | self.currentFrame = image; 395 | if (self.needsDisplayWhenImageBecomesAvailable) { 396 | [self.layer setNeedsDisplay]; 397 | self.needsDisplayWhenImageBecomesAvailable = NO; 398 | } 399 | 400 | if (@available(iOS 10, *)) { 401 | self.accumulator += displayLink.targetTimestamp - CACurrentMediaTime(); 402 | } else { 403 | self.accumulator += displayLink.duration * (NSTimeInterval)displayLink.frameInterval; 404 | } 405 | 406 | // While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m 407 | while (self.accumulator >= delayTime) { 408 | self.accumulator -= delayTime; 409 | self.currentFrameIndex++; 410 | if (self.currentFrameIndex >= self.animatedImage.frameCount) { 411 | // If we've looped the number of times that this animated image describes, stop looping. 412 | self.loopCountdown--; 413 | if (self.loopCompletionBlock) { 414 | self.loopCompletionBlock(self.loopCountdown); 415 | } 416 | 417 | if (self.loopCountdown == 0) { 418 | [self stopAnimating]; 419 | return; 420 | } 421 | self.currentFrameIndex = 0; 422 | } 423 | // Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to. 424 | // Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded. 425 | self.needsDisplayWhenImageBecomesAvailable = YES; 426 | } 427 | } else { 428 | FLLog(FLLogLevelDebug, @"Waiting for frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage); 429 | #if defined(DEBUG) && DEBUG 430 | if ([self.debug_delegate respondsToSelector:@selector(debug_animatedImageView:waitingForFrame:duration:)]) { 431 | if (@available(iOS 10, *)) { 432 | [self.debug_delegate debug_animatedImageView:self waitingForFrame:self.currentFrameIndex duration:displayLink.targetTimestamp - CACurrentMediaTime()]; 433 | } else { 434 | [self.debug_delegate debug_animatedImageView:self waitingForFrame:self.currentFrameIndex duration:displayLink.duration * (NSTimeInterval)displayLink.frameInterval]; 435 | } 436 | } 437 | #endif 438 | } 439 | } else { 440 | self.currentFrameIndex++; 441 | } 442 | } 443 | 444 | + (NSRunLoopMode)defaultRunLoopMode 445 | { 446 | // Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations. 447 | return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode; 448 | } 449 | 450 | 451 | #pragma mark - CALayerDelegate (Informal) 452 | #pragma mark Providing the Layer's Content 453 | 454 | - (void)displayLayer:(CALayer *)layer 455 | { 456 | layer.contents = (__bridge id)self.image.CGImage; 457 | } 458 | 459 | 460 | @end 461 | -------------------------------------------------------------------------------- /FLAnimatedImage/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /FLAnimatedImage/include/FLAnimatedImage.h: -------------------------------------------------------------------------------- 1 | // 2 | // FLAnimatedImage.h 3 | // Flipboard 4 | // 5 | // Created by Raphael Schaad on 7/8/13. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import 11 | 12 | // Allow user classes conveniently just importing one header. 13 | #import "FLAnimatedImageView.h" 14 | 15 | #ifndef NS_DESIGNATED_INITIALIZER 16 | #if __has_attribute(objc_designated_initializer) 17 | #define NS_DESIGNATED_INITIALIZER __attribute((objc_designated_initializer)) 18 | #else 19 | #define NS_DESIGNATED_INITIALIZER 20 | #endif 21 | #endif 22 | 23 | extern const NSTimeInterval kFLAnimatedImageDelayTimeIntervalMinimum; 24 | 25 | // 26 | // An `FLAnimatedImage`'s job is to deliver frames in a highly performant way and works in conjunction with `FLAnimatedImageView`. 27 | // It subclasses `NSObject` and not `UIImage` because it's only an "image" in the sense that a sea lion is a lion. 28 | // It tries to intelligently choose the frame cache size depending on the image and memory situation with the goal to lower CPU usage for smaller ones, lower memory usage for larger ones and always deliver frames for high performant play-back. 29 | // Note: `posterImage`, `size`, `loopCount`, `delayTimes` and `frameCount` don't change after successful initialization. 30 | // 31 | @interface FLAnimatedImage : NSObject 32 | 33 | @property (nonatomic, strong, readonly) UIImage *posterImage; // Guaranteed to be loaded; usually equivalent to `-imageLazilyCachedAtIndex:0` 34 | @property (nonatomic, assign, readonly) CGSize size; // The `.posterImage`'s `.size` 35 | 36 | @property (nonatomic, assign, readonly) NSUInteger loopCount; // "The number of times to repeat an animated sequence." according to ImageIO (note the slightly different definition to Netscape 2.0 Loop Extension); 0 means repeating the animation forever 37 | @property (nonatomic, strong, readonly) NSDictionary *delayTimesForIndexes; // Of type `NSTimeInterval` boxed in `NSNumber`s 38 | @property (nonatomic, assign, readonly) NSUInteger frameCount; // Number of valid frames; equal to `[.delayTimes count]` 39 | 40 | @property (nonatomic, assign, readonly) NSUInteger frameCacheSizeCurrent; // Current size of intelligently chosen buffer window; can range in the interval [1..frameCount] 41 | @property (nonatomic, assign) NSUInteger frameCacheSizeMax; // Allow to cap the cache size; 0 means no specific limit (default) 42 | 43 | // Intended to be called from main thread synchronously; will return immediately. 44 | // If the result isn't cached, will return `nil`; the caller should then pause playback, not increment frame counter and keep polling. 45 | // After an initial loading time, depending on `frameCacheSize`, frames should be available immediately from the cache. 46 | - (UIImage *)imageLazilyCachedAtIndex:(NSUInteger)index; 47 | 48 | // Pass either a `UIImage` or an `FLAnimatedImage` and get back its size 49 | + (CGSize)sizeForImage:(id)image; 50 | 51 | // On success, the initializers return an `FLAnimatedImage` with all fields initialized, on failure they return `nil` and an error will be logged. 52 | - (instancetype)initWithAnimatedGIFData:(NSData *)data; 53 | // Pass 0 for optimalFrameCacheSize to get the default, predrawing is enabled by default. 54 | - (instancetype)initWithAnimatedGIFData:(NSData *)data optimalFrameCacheSize:(NSUInteger)optimalFrameCacheSize predrawingEnabled:(BOOL)isPredrawingEnabled NS_DESIGNATED_INITIALIZER; 55 | + (instancetype)animatedImageWithGIFData:(NSData *)data; 56 | 57 | @property (nonatomic, strong, readonly) NSData *data; // The data the receiver was initialized with; read-only 58 | 59 | @end 60 | 61 | typedef NS_ENUM(NSUInteger, FLLogLevel) { 62 | FLLogLevelNone = 0, 63 | FLLogLevelError, 64 | FLLogLevelWarn, 65 | FLLogLevelInfo, 66 | FLLogLevelDebug, 67 | FLLogLevelVerbose 68 | }; 69 | 70 | @interface FLAnimatedImage (Logging) 71 | 72 | + (void)setLogBlock:(void (^)(NSString *logString, FLLogLevel logLevel))logBlock logLevel:(FLLogLevel)logLevel; 73 | + (void)logStringFromBlock:(NSString *(^)(void))stringBlock withLevel:(FLLogLevel)level; 74 | 75 | @end 76 | 77 | #define FLLog(logLevel, format, ...) [FLAnimatedImage logStringFromBlock:^NSString *{ return [NSString stringWithFormat:(format), ## __VA_ARGS__]; } withLevel:(logLevel)] 78 | 79 | @interface FLWeakProxy : NSProxy 80 | 81 | + (instancetype)weakProxyForObject:(id)targetObject; 82 | 83 | @end 84 | -------------------------------------------------------------------------------- /FLAnimatedImage/include/FLAnimatedImageView.h: -------------------------------------------------------------------------------- 1 | // 2 | // FLAnimatedImageView.h 3 | // Flipboard 4 | // 5 | // Created by Raphael Schaad on 7/8/13. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import 11 | 12 | @class FLAnimatedImage; 13 | @protocol FLAnimatedImageViewDebugDelegate; 14 | 15 | 16 | // 17 | // An `FLAnimatedImageView` can take an `FLAnimatedImage` and plays it automatically when in view hierarchy and stops when removed. 18 | // The animation can also be controlled with the `UIImageView` methods `-start/stop/isAnimating`. 19 | // It is a fully compatible `UIImageView` subclass and can be used as a drop-in component to work with existing code paths expecting to display a `UIImage`. 20 | // Under the hood it uses a `CADisplayLink` for playback, which can be inspected with `currentFrame` & `currentFrameIndex`. 21 | // 22 | @interface FLAnimatedImageView : UIImageView 23 | 24 | // Setting `[UIImageView.image]` to a non-`nil` value clears out existing `animatedImage`. 25 | // And vice versa, setting `animatedImage` will initially populate the `[UIImageView.image]` to its `posterImage` and then start animating and hold `currentFrame`. 26 | @property (nonatomic, strong) FLAnimatedImage *animatedImage; 27 | @property (nonatomic, copy) void(^loopCompletionBlock)(NSUInteger loopCountRemaining); 28 | 29 | @property (nonatomic, strong, readonly) UIImage *currentFrame; 30 | @property (nonatomic, assign, readonly) NSUInteger currentFrameIndex; 31 | 32 | // The animation runloop mode. Enables playback during scrolling by allowing timer events (i.e. animation) with NSRunLoopCommonModes. 33 | // To keep scrolling smooth on single-core devices such as iPhone 3GS/4 and iPod Touch 4th gen, the default run loop mode is NSDefaultRunLoopMode. Otherwise, the default is NSDefaultRunLoopMode. 34 | @property (nonatomic, copy) NSRunLoopMode runLoopMode; 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 229D6DFF1C77D121000C59E6 /* FLAnimatedImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 229D6DFE1C77D10D000C59E6 /* FLAnimatedImage.framework */; }; 11 | 229D6E001C77D121000C59E6 /* FLAnimatedImage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 229D6DFE1C77D10D000C59E6 /* FLAnimatedImage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 12 | 870D79D51936EBFC009D1BCE /* rock.gif in Resources */ = {isa = PBXBuildFile; fileRef = 870D79D41936EBFC009D1BCE /* rock.gif */; }; 13 | 872EBE70178B825500B7531B /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 872EBE6F178B825500B7531B /* UIKit.framework */; }; 14 | 872EBE72178B825500B7531B /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 872EBE71178B825500B7531B /* Foundation.framework */; }; 15 | 872EBE74178B825500B7531B /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 872EBE73178B825500B7531B /* CoreGraphics.framework */; }; 16 | 872EBEB3178B89DF00B7531B /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 872EBEB2178B89DF00B7531B /* ImageIO.framework */; }; 17 | 872EBEBD178B8FA000B7531B /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 872EBEBC178B8FA000B7531B /* QuartzCore.framework */; }; 18 | 8790E1171935ABDA00459BC1 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8790E1161935ABDA00459BC1 /* Images.xcassets */; }; 19 | 87CB59F01935A7BB00620C16 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 87CB59ED1935A7BB00620C16 /* AppDelegate.m */; }; 20 | 87CB59F11935A7BB00620C16 /* RootViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 87CB59EF1935A7BB00620C16 /* RootViewController.m */; }; 21 | 87CB5A001935A83900620C16 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 87CB59FE1935A83900620C16 /* InfoPlist.strings */; }; 22 | 87CB5A031935A84900620C16 /* DebugView.m in Sources */ = {isa = PBXBuildFile; fileRef = 87CB5A021935A84900620C16 /* DebugView.m */; }; 23 | 87CB5A111935A8C600620C16 /* FrameCacheView.m in Sources */ = {isa = PBXBuildFile; fileRef = 87CB5A091935A8C600620C16 /* FrameCacheView.m */; }; 24 | 87CB5A121935A8C600620C16 /* GraphView.m in Sources */ = {isa = PBXBuildFile; fileRef = 87CB5A0B1935A8C600620C16 /* GraphView.m */; }; 25 | 87CB5A131935A8C600620C16 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 87CB5A0C1935A8C600620C16 /* main.m */; }; 26 | 87CB5A141935A8C600620C16 /* PlayheadView.m in Sources */ = {isa = PBXBuildFile; fileRef = 87CB5A0E1935A8C600620C16 /* PlayheadView.m */; }; 27 | 87CB5A151935A8C600620C16 /* RSPlayPauseButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 87CB5A101935A8C600620C16 /* RSPlayPauseButton.m */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXContainerItemProxy section */ 31 | 229D6DFD1C77D10D000C59E6 /* PBXContainerItemProxy */ = { 32 | isa = PBXContainerItemProxy; 33 | containerPortal = 229D6DF91C77D10D000C59E6 /* FLAnimatedImage.xcodeproj */; 34 | proxyType = 2; 35 | remoteGlobalIDString = 92C9BC0C1B199DC500D79B06; 36 | remoteInfo = FLAnimatedImage; 37 | }; 38 | 229D6E011C77D121000C59E6 /* PBXContainerItemProxy */ = { 39 | isa = PBXContainerItemProxy; 40 | containerPortal = 229D6DF91C77D10D000C59E6 /* FLAnimatedImage.xcodeproj */; 41 | proxyType = 1; 42 | remoteGlobalIDString = 92C9BC0B1B199DC500D79B06; 43 | remoteInfo = FLAnimatedImage; 44 | }; 45 | /* End PBXContainerItemProxy section */ 46 | 47 | /* Begin PBXCopyFilesBuildPhase section */ 48 | 229D6E031C77D121000C59E6 /* Embed Frameworks */ = { 49 | isa = PBXCopyFilesBuildPhase; 50 | buildActionMask = 2147483647; 51 | dstPath = ""; 52 | dstSubfolderSpec = 10; 53 | files = ( 54 | 229D6E001C77D121000C59E6 /* FLAnimatedImage.framework in Embed Frameworks */, 55 | ); 56 | name = "Embed Frameworks"; 57 | runOnlyForDeploymentPostprocessing = 0; 58 | }; 59 | /* End PBXCopyFilesBuildPhase section */ 60 | 61 | /* Begin PBXFileReference section */ 62 | 229D6DF91C77D10D000C59E6 /* FLAnimatedImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = FLAnimatedImage.xcodeproj; sourceTree = ""; }; 63 | 870D79D41936EBFC009D1BCE /* rock.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; name = rock.gif; path = "FLAnimatedImageDemo/test-gifs/rock.gif"; sourceTree = SOURCE_ROOT; }; 64 | 872EBE6C178B825500B7531B /* FLAnimatedImageDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FLAnimatedImageDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 872EBE6F178B825500B7531B /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 66 | 872EBE71178B825500B7531B /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 67 | 872EBE73178B825500B7531B /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 68 | 872EBEB2178B89DF00B7531B /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; }; 69 | 872EBEBA178B8F9000B7531B /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; 70 | 872EBEBC178B8FA000B7531B /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 71 | 8790E1161935ABDA00459BC1 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = FLAnimatedImageDemo/Images.xcassets; sourceTree = ""; }; 72 | 87CB59EC1935A7BB00620C16 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = FLAnimatedImageDemo/AppDelegate.h; sourceTree = ""; }; 73 | 87CB59ED1935A7BB00620C16 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = FLAnimatedImageDemo/AppDelegate.m; sourceTree = ""; }; 74 | 87CB59EE1935A7BB00620C16 /* RootViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RootViewController.h; path = FLAnimatedImageDemo/RootViewController.h; sourceTree = ""; }; 75 | 87CB59EF1935A7BB00620C16 /* RootViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RootViewController.m; path = FLAnimatedImageDemo/RootViewController.m; sourceTree = ""; }; 76 | 87CB59FF1935A83900620C16 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = FLAnimatedImageDemo/en.lproj/InfoPlist.strings; sourceTree = ""; }; 77 | 87CB5A011935A84900620C16 /* DebugView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DebugView.h; path = FLAnimatedImageDemo/DebugView.h; sourceTree = ""; }; 78 | 87CB5A021935A84900620C16 /* DebugView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DebugView.m; path = FLAnimatedImageDemo/DebugView.m; sourceTree = ""; }; 79 | 87CB5A041935A8A600620C16 /* FLAnimatedImageDemo-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "FLAnimatedImageDemo-Info.plist"; path = "FLAnimatedImageDemo/FLAnimatedImageDemo-Info.plist"; sourceTree = ""; }; 80 | 87CB5A081935A8C600620C16 /* FrameCacheView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FrameCacheView.h; path = FLAnimatedImageDemo/FrameCacheView.h; sourceTree = ""; }; 81 | 87CB5A091935A8C600620C16 /* FrameCacheView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FrameCacheView.m; path = FLAnimatedImageDemo/FrameCacheView.m; sourceTree = ""; }; 82 | 87CB5A0A1935A8C600620C16 /* GraphView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GraphView.h; path = FLAnimatedImageDemo/GraphView.h; sourceTree = ""; }; 83 | 87CB5A0B1935A8C600620C16 /* GraphView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GraphView.m; path = FLAnimatedImageDemo/GraphView.m; sourceTree = ""; }; 84 | 87CB5A0C1935A8C600620C16 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = FLAnimatedImageDemo/main.m; sourceTree = ""; }; 85 | 87CB5A0D1935A8C600620C16 /* PlayheadView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PlayheadView.h; path = FLAnimatedImageDemo/PlayheadView.h; sourceTree = ""; }; 86 | 87CB5A0E1935A8C600620C16 /* PlayheadView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PlayheadView.m; path = FLAnimatedImageDemo/PlayheadView.m; sourceTree = ""; }; 87 | 87CB5A0F1935A8C600620C16 /* RSPlayPauseButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RSPlayPauseButton.h; path = FLAnimatedImageDemo/RSPlayPauseButton.h; sourceTree = ""; }; 88 | 87CB5A101935A8C600620C16 /* RSPlayPauseButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RSPlayPauseButton.m; path = FLAnimatedImageDemo/RSPlayPauseButton.m; sourceTree = ""; }; 89 | 87CB5A171935A93F00620C16 /* FLAnimatedImageDemo-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FLAnimatedImageDemo-Prefix.pch"; path = "FLAnimatedImageDemo/FLAnimatedImageDemo-Prefix.pch"; sourceTree = ""; }; 90 | /* End PBXFileReference section */ 91 | 92 | /* Begin PBXFrameworksBuildPhase section */ 93 | 872EBE69178B825500B7531B /* Frameworks */ = { 94 | isa = PBXFrameworksBuildPhase; 95 | buildActionMask = 2147483647; 96 | files = ( 97 | 872EBE72178B825500B7531B /* Foundation.framework in Frameworks */, 98 | 872EBE74178B825500B7531B /* CoreGraphics.framework in Frameworks */, 99 | 872EBE70178B825500B7531B /* UIKit.framework in Frameworks */, 100 | 872EBEBD178B8FA000B7531B /* QuartzCore.framework in Frameworks */, 101 | 229D6DFF1C77D121000C59E6 /* FLAnimatedImage.framework in Frameworks */, 102 | 872EBEB3178B89DF00B7531B /* ImageIO.framework in Frameworks */, 103 | ); 104 | runOnlyForDeploymentPostprocessing = 0; 105 | }; 106 | /* End PBXFrameworksBuildPhase section */ 107 | 108 | /* Begin PBXGroup section */ 109 | 229D6DFA1C77D10D000C59E6 /* Products */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 229D6DFE1C77D10D000C59E6 /* FLAnimatedImage.framework */, 113 | ); 114 | name = Products; 115 | sourceTree = ""; 116 | }; 117 | 872EBE63178B825500B7531B = { 118 | isa = PBXGroup; 119 | children = ( 120 | 872EBE75178B825500B7531B /* FLAnimatedImageDemo */, 121 | 229D6DF91C77D10D000C59E6 /* FLAnimatedImage.xcodeproj */, 122 | 872EBE6E178B825500B7531B /* Frameworks */, 123 | 872EBE6D178B825500B7531B /* Products */, 124 | ); 125 | sourceTree = ""; 126 | }; 127 | 872EBE6D178B825500B7531B /* Products */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 872EBE6C178B825500B7531B /* FLAnimatedImageDemo.app */, 131 | ); 132 | name = Products; 133 | sourceTree = ""; 134 | }; 135 | 872EBE6E178B825500B7531B /* Frameworks */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 872EBE71178B825500B7531B /* Foundation.framework */, 139 | 872EBE73178B825500B7531B /* CoreGraphics.framework */, 140 | 872EBE6F178B825500B7531B /* UIKit.framework */, 141 | 872EBEBC178B8FA000B7531B /* QuartzCore.framework */, 142 | 872EBEB2178B89DF00B7531B /* ImageIO.framework */, 143 | 872EBEBA178B8F9000B7531B /* MobileCoreServices.framework */, 144 | ); 145 | name = Frameworks; 146 | sourceTree = ""; 147 | }; 148 | 872EBE75178B825500B7531B /* FLAnimatedImageDemo */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 87CB59EC1935A7BB00620C16 /* AppDelegate.h */, 152 | 87CB59ED1935A7BB00620C16 /* AppDelegate.m */, 153 | 87CB59EE1935A7BB00620C16 /* RootViewController.h */, 154 | 87CB59EF1935A7BB00620C16 /* RootViewController.m */, 155 | 8791BCBF191B2AAE00A846A5 /* test-gifs */, 156 | 8790E1161935ABDA00459BC1 /* Images.xcassets */, 157 | 872EBE76178B825500B7531B /* Supporting Files */, 158 | ); 159 | name = FLAnimatedImageDemo; 160 | sourceTree = ""; 161 | }; 162 | 872EBE76178B825500B7531B /* Supporting Files */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 87CB5A041935A8A600620C16 /* FLAnimatedImageDemo-Info.plist */, 166 | 87CB59FE1935A83900620C16 /* InfoPlist.strings */, 167 | 87CB5A171935A93F00620C16 /* FLAnimatedImageDemo-Prefix.pch */, 168 | 87CB5A0C1935A8C600620C16 /* main.m */, 169 | 87CB5A011935A84900620C16 /* DebugView.h */, 170 | 87CB5A021935A84900620C16 /* DebugView.m */, 171 | 87CB5A0A1935A8C600620C16 /* GraphView.h */, 172 | 87CB5A0B1935A8C600620C16 /* GraphView.m */, 173 | 87CB5A081935A8C600620C16 /* FrameCacheView.h */, 174 | 87CB5A091935A8C600620C16 /* FrameCacheView.m */, 175 | 87CB5A0D1935A8C600620C16 /* PlayheadView.h */, 176 | 87CB5A0E1935A8C600620C16 /* PlayheadView.m */, 177 | 87CB5A0F1935A8C600620C16 /* RSPlayPauseButton.h */, 178 | 87CB5A101935A8C600620C16 /* RSPlayPauseButton.m */, 179 | ); 180 | name = "Supporting Files"; 181 | sourceTree = ""; 182 | }; 183 | 8791BCBF191B2AAE00A846A5 /* test-gifs */ = { 184 | isa = PBXGroup; 185 | children = ( 186 | 870D79D41936EBFC009D1BCE /* rock.gif */, 187 | ); 188 | name = "test-gifs"; 189 | path = "AnimatedImageDemo/animated-gifs"; 190 | sourceTree = SOURCE_ROOT; 191 | }; 192 | /* End PBXGroup section */ 193 | 194 | /* Begin PBXNativeTarget section */ 195 | 872EBE6B178B825500B7531B /* FLAnimatedImageDemo */ = { 196 | isa = PBXNativeTarget; 197 | buildConfigurationList = 872EBEA7178B825500B7531B /* Build configuration list for PBXNativeTarget "FLAnimatedImageDemo" */; 198 | buildPhases = ( 199 | 872EBE68178B825500B7531B /* Sources */, 200 | 872EBE69178B825500B7531B /* Frameworks */, 201 | 872EBE6A178B825500B7531B /* Resources */, 202 | 229D6E031C77D121000C59E6 /* Embed Frameworks */, 203 | ); 204 | buildRules = ( 205 | ); 206 | dependencies = ( 207 | 229D6E021C77D121000C59E6 /* PBXTargetDependency */, 208 | ); 209 | name = FLAnimatedImageDemo; 210 | productName = AnimatedImageDemo; 211 | productReference = 872EBE6C178B825500B7531B /* FLAnimatedImageDemo.app */; 212 | productType = "com.apple.product-type.application"; 213 | }; 214 | /* End PBXNativeTarget section */ 215 | 216 | /* Begin PBXProject section */ 217 | 872EBE64178B825500B7531B /* Project object */ = { 218 | isa = PBXProject; 219 | attributes = { 220 | CLASSPREFIX = FL; 221 | LastUpgradeCheck = 1240; 222 | ORGANIZATIONNAME = Flipboard; 223 | }; 224 | buildConfigurationList = 872EBE67178B825500B7531B /* Build configuration list for PBXProject "FLAnimatedImageDemo" */; 225 | compatibilityVersion = "Xcode 3.2"; 226 | developmentRegion = en; 227 | hasScannedForEncodings = 0; 228 | knownRegions = ( 229 | en, 230 | Base, 231 | ); 232 | mainGroup = 872EBE63178B825500B7531B; 233 | productRefGroup = 872EBE6D178B825500B7531B /* Products */; 234 | projectDirPath = ""; 235 | projectReferences = ( 236 | { 237 | ProductGroup = 229D6DFA1C77D10D000C59E6 /* Products */; 238 | ProjectRef = 229D6DF91C77D10D000C59E6 /* FLAnimatedImage.xcodeproj */; 239 | }, 240 | ); 241 | projectRoot = ""; 242 | targets = ( 243 | 872EBE6B178B825500B7531B /* FLAnimatedImageDemo */, 244 | ); 245 | }; 246 | /* End PBXProject section */ 247 | 248 | /* Begin PBXReferenceProxy section */ 249 | 229D6DFE1C77D10D000C59E6 /* FLAnimatedImage.framework */ = { 250 | isa = PBXReferenceProxy; 251 | fileType = wrapper.framework; 252 | path = FLAnimatedImage.framework; 253 | remoteRef = 229D6DFD1C77D10D000C59E6 /* PBXContainerItemProxy */; 254 | sourceTree = BUILT_PRODUCTS_DIR; 255 | }; 256 | /* End PBXReferenceProxy section */ 257 | 258 | /* Begin PBXResourcesBuildPhase section */ 259 | 872EBE6A178B825500B7531B /* Resources */ = { 260 | isa = PBXResourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | 8790E1171935ABDA00459BC1 /* Images.xcassets in Resources */, 264 | 870D79D51936EBFC009D1BCE /* rock.gif in Resources */, 265 | 87CB5A001935A83900620C16 /* InfoPlist.strings in Resources */, 266 | ); 267 | runOnlyForDeploymentPostprocessing = 0; 268 | }; 269 | /* End PBXResourcesBuildPhase section */ 270 | 271 | /* Begin PBXSourcesBuildPhase section */ 272 | 872EBE68178B825500B7531B /* Sources */ = { 273 | isa = PBXSourcesBuildPhase; 274 | buildActionMask = 2147483647; 275 | files = ( 276 | 87CB59F01935A7BB00620C16 /* AppDelegate.m in Sources */, 277 | 87CB5A151935A8C600620C16 /* RSPlayPauseButton.m in Sources */, 278 | 87CB59F11935A7BB00620C16 /* RootViewController.m in Sources */, 279 | 87CB5A141935A8C600620C16 /* PlayheadView.m in Sources */, 280 | 87CB5A031935A84900620C16 /* DebugView.m in Sources */, 281 | 87CB5A121935A8C600620C16 /* GraphView.m in Sources */, 282 | 87CB5A131935A8C600620C16 /* main.m in Sources */, 283 | 87CB5A111935A8C600620C16 /* FrameCacheView.m in Sources */, 284 | ); 285 | runOnlyForDeploymentPostprocessing = 0; 286 | }; 287 | /* End PBXSourcesBuildPhase section */ 288 | 289 | /* Begin PBXTargetDependency section */ 290 | 229D6E021C77D121000C59E6 /* PBXTargetDependency */ = { 291 | isa = PBXTargetDependency; 292 | name = FLAnimatedImage; 293 | targetProxy = 229D6E011C77D121000C59E6 /* PBXContainerItemProxy */; 294 | }; 295 | /* End PBXTargetDependency section */ 296 | 297 | /* Begin PBXVariantGroup section */ 298 | 87CB59FE1935A83900620C16 /* InfoPlist.strings */ = { 299 | isa = PBXVariantGroup; 300 | children = ( 301 | 87CB59FF1935A83900620C16 /* en */, 302 | ); 303 | name = InfoPlist.strings; 304 | sourceTree = ""; 305 | }; 306 | /* End PBXVariantGroup section */ 307 | 308 | /* Begin XCBuildConfiguration section */ 309 | 872EBEA5178B825500B7531B /* Debug */ = { 310 | isa = XCBuildConfiguration; 311 | buildSettings = { 312 | ALWAYS_SEARCH_USER_PATHS = NO; 313 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 314 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 315 | CLANG_CXX_LIBRARY = "libc++"; 316 | CLANG_ENABLE_OBJC_ARC = YES; 317 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 318 | CLANG_WARN_BOOL_CONVERSION = YES; 319 | CLANG_WARN_COMMA = YES; 320 | CLANG_WARN_CONSTANT_CONVERSION = YES; 321 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 322 | CLANG_WARN_EMPTY_BODY = YES; 323 | CLANG_WARN_ENUM_CONVERSION = YES; 324 | CLANG_WARN_INFINITE_RECURSION = YES; 325 | CLANG_WARN_INT_CONVERSION = YES; 326 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 328 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 329 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 330 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 331 | CLANG_WARN_STRICT_PROTOTYPES = YES; 332 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 333 | CLANG_WARN_UNREACHABLE_CODE = YES; 334 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 335 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 336 | COPY_PHASE_STRIP = NO; 337 | ENABLE_STRICT_OBJC_MSGSEND = YES; 338 | ENABLE_TESTABILITY = YES; 339 | GCC_C_LANGUAGE_STANDARD = gnu99; 340 | GCC_DYNAMIC_NO_PIC = NO; 341 | GCC_NO_COMMON_BLOCKS = YES; 342 | GCC_OPTIMIZATION_LEVEL = 0; 343 | GCC_PREPROCESSOR_DEFINITIONS = ( 344 | "DEBUG=1", 345 | "$(inherited)", 346 | ); 347 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 348 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 349 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 350 | GCC_WARN_UNDECLARED_SELECTOR = YES; 351 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 352 | GCC_WARN_UNUSED_FUNCTION = YES; 353 | GCC_WARN_UNUSED_VARIABLE = YES; 354 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 355 | ONLY_ACTIVE_ARCH = YES; 356 | SDKROOT = iphoneos; 357 | TARGETED_DEVICE_FAMILY = "1,2"; 358 | }; 359 | name = Debug; 360 | }; 361 | 872EBEA6178B825500B7531B /* Release */ = { 362 | isa = XCBuildConfiguration; 363 | buildSettings = { 364 | ALWAYS_SEARCH_USER_PATHS = NO; 365 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 366 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 367 | CLANG_CXX_LIBRARY = "libc++"; 368 | CLANG_ENABLE_OBJC_ARC = YES; 369 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 370 | CLANG_WARN_BOOL_CONVERSION = YES; 371 | CLANG_WARN_COMMA = YES; 372 | CLANG_WARN_CONSTANT_CONVERSION = YES; 373 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 374 | CLANG_WARN_EMPTY_BODY = YES; 375 | CLANG_WARN_ENUM_CONVERSION = YES; 376 | CLANG_WARN_INFINITE_RECURSION = YES; 377 | CLANG_WARN_INT_CONVERSION = YES; 378 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 379 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 380 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 381 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 382 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 383 | CLANG_WARN_STRICT_PROTOTYPES = YES; 384 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 385 | CLANG_WARN_UNREACHABLE_CODE = YES; 386 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 387 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 388 | COPY_PHASE_STRIP = YES; 389 | ENABLE_STRICT_OBJC_MSGSEND = YES; 390 | GCC_C_LANGUAGE_STANDARD = gnu99; 391 | GCC_NO_COMMON_BLOCKS = YES; 392 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 393 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 394 | GCC_WARN_UNDECLARED_SELECTOR = YES; 395 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 396 | GCC_WARN_UNUSED_FUNCTION = YES; 397 | GCC_WARN_UNUSED_VARIABLE = YES; 398 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 399 | OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; 400 | SDKROOT = iphoneos; 401 | TARGETED_DEVICE_FAMILY = "1,2"; 402 | VALIDATE_PRODUCT = YES; 403 | }; 404 | name = Release; 405 | }; 406 | 872EBEA8178B825500B7531B /* Debug */ = { 407 | isa = XCBuildConfiguration; 408 | buildSettings = { 409 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 410 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 411 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 412 | GCC_PREFIX_HEADER = "FLAnimatedImageDemo/FLAnimatedImageDemo-Prefix.pch"; 413 | INFOPLIST_FILE = "FLAnimatedImageDemo/FLAnimatedImageDemo-Info.plist"; 414 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 415 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 416 | PRODUCT_BUNDLE_IDENTIFIER = "com.flipboard.${PRODUCT_NAME:rfc1034identifier}"; 417 | PRODUCT_NAME = FLAnimatedImageDemo; 418 | TARGETED_DEVICE_FAMILY = 2; 419 | USER_HEADER_SEARCH_PATHS = ""; 420 | WARNING_CFLAGS = "-Wundef"; 421 | WRAPPER_EXTENSION = app; 422 | }; 423 | name = Debug; 424 | }; 425 | 872EBEA9178B825500B7531B /* Release */ = { 426 | isa = XCBuildConfiguration; 427 | buildSettings = { 428 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 429 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 430 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 431 | GCC_PREFIX_HEADER = "FLAnimatedImageDemo/FLAnimatedImageDemo-Prefix.pch"; 432 | INFOPLIST_FILE = "FLAnimatedImageDemo/FLAnimatedImageDemo-Info.plist"; 433 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 434 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 435 | PRODUCT_BUNDLE_IDENTIFIER = "com.flipboard.${PRODUCT_NAME:rfc1034identifier}"; 436 | PRODUCT_NAME = FLAnimatedImageDemo; 437 | TARGETED_DEVICE_FAMILY = 2; 438 | USER_HEADER_SEARCH_PATHS = ""; 439 | WARNING_CFLAGS = "-Wundef"; 440 | WRAPPER_EXTENSION = app; 441 | }; 442 | name = Release; 443 | }; 444 | /* End XCBuildConfiguration section */ 445 | 446 | /* Begin XCConfigurationList section */ 447 | 872EBE67178B825500B7531B /* Build configuration list for PBXProject "FLAnimatedImageDemo" */ = { 448 | isa = XCConfigurationList; 449 | buildConfigurations = ( 450 | 872EBEA5178B825500B7531B /* Debug */, 451 | 872EBEA6178B825500B7531B /* Release */, 452 | ); 453 | defaultConfigurationIsVisible = 0; 454 | defaultConfigurationName = Release; 455 | }; 456 | 872EBEA7178B825500B7531B /* Build configuration list for PBXNativeTarget "FLAnimatedImageDemo" */ = { 457 | isa = XCConfigurationList; 458 | buildConfigurations = ( 459 | 872EBEA8178B825500B7531B /* Debug */, 460 | 872EBEA9178B825500B7531B /* Release */, 461 | ); 462 | defaultConfigurationIsVisible = 0; 463 | defaultConfigurationName = Release; 464 | }; 465 | /* End XCConfigurationList section */ 466 | }; 467 | rootObject = 872EBE64178B825500B7531B /* Project object */; 468 | } 469 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // FLAnimatedImageDemo 4 | // 5 | // Created by Raphael Schaad on 4/1/14. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import 11 | 12 | 13 | @interface AppDelegate : UIResponder 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // FLAnimatedImageDemo 4 | // 5 | // Created by Raphael Schaad on 4/1/14. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import "AppDelegate.h" 11 | #import "RootViewController.h" 12 | 13 | 14 | @interface AppDelegate () 15 | 16 | 17 | @property (nonatomic, strong) RootViewController *viewController; 18 | 19 | @end 20 | 21 | 22 | @implementation AppDelegate 23 | 24 | @synthesize window = _window; 25 | 26 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 27 | { 28 | self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 29 | self.viewController = [[RootViewController alloc] init]; 30 | self.window.rootViewController = self.viewController; 31 | [self.window makeKeyAndVisible]; 32 | return YES; 33 | } 34 | 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/DebugView.h: -------------------------------------------------------------------------------- 1 | // 2 | // DebugView.h 3 | // FLAnimatedImageDemo 4 | // 5 | // Created by Raphael Schaad on 4/1/14. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import 11 | #import 12 | 13 | typedef NS_ENUM(NSUInteger, DebugViewStyle) { 14 | DebugViewStyleDefault, 15 | DebugViewStyleCondensed 16 | }; 17 | 18 | 19 | // Conforms to private FLAnimatedImageDebugDelegate and FLAnimatedImageViewDebugDelegate protocols, used in sample project. 20 | @interface DebugView : UIView 21 | 22 | @property (nonatomic, weak) FLAnimatedImage *image; 23 | @property (nonatomic, weak) FLAnimatedImageView *imageView; 24 | @property (nonatomic, assign) DebugViewStyle style; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/DebugView.m: -------------------------------------------------------------------------------- 1 | // 2 | // DebugView.m 3 | // FLAnimatedImageDemo 4 | // 5 | // Created by Raphael Schaad on 4/1/14. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import "DebugView.h" 11 | #import "GraphView.h" 12 | #import "FrameCacheView.h" 13 | #import "PlayheadView.h" 14 | #import "RSPlayPauseButton.h" 15 | 16 | 17 | @interface DebugView () 18 | 19 | @property (nonatomic, strong) CAGradientLayer *gradientLayer; 20 | @property (nonatomic, strong) GraphView *memoryUsageView; 21 | @property (nonatomic, strong) GraphView *frameDelayView; 22 | @property (nonatomic, assign) NSTimeInterval currentFrameDelay; 23 | @property (nonatomic, strong) RSPlayPauseButton *playPauseButton; 24 | @property (nonatomic, strong) FrameCacheView *frameCacheView; 25 | @property (nonatomic, strong) PlayheadView *playheadView; 26 | 27 | @end 28 | 29 | 30 | @implementation DebugView 31 | 32 | - (instancetype)initWithFrame:(CGRect)frame 33 | { 34 | self = [super initWithFrame:frame]; 35 | if (self) { 36 | self.style = DebugViewStyleDefault; 37 | } 38 | return self; 39 | } 40 | 41 | 42 | - (void)setStyle:(DebugViewStyle)style 43 | { 44 | if (_style != style) { 45 | _style = style; 46 | [self setNeedsLayout]; 47 | } 48 | } 49 | 50 | 51 | - (void)layoutSubviews 52 | { 53 | [super layoutSubviews]; 54 | 55 | const CGFloat kMargin = 10.0; 56 | 57 | if (!self.gradientLayer) { 58 | self.gradientLayer = [CAGradientLayer layer]; 59 | self.gradientLayer.colors = @[(__bridge id)[UIColor colorWithWhite:0.0 alpha:0.85].CGColor, (__bridge id)[UIColor colorWithWhite:0.0 alpha:0.0].CGColor, (__bridge id)[UIColor colorWithWhite:0.0 alpha:0.0].CGColor, (__bridge id)[UIColor colorWithWhite:0.0 alpha:0.85].CGColor]; 60 | self.gradientLayer.locations = @[@0.0, @0.22, @0.78, @1.0]; 61 | [self.layer addSublayer:self.gradientLayer]; 62 | } 63 | self.gradientLayer.frame = self.bounds; 64 | 65 | if (!self.memoryUsageView) { 66 | self.memoryUsageView = [[GraphView alloc] init]; 67 | [self addSubview:self.memoryUsageView]; 68 | } 69 | self.memoryUsageView.numberOfDisplayedDataPoints = self.image.frameCount * 3; 70 | CGFloat memoryUsage = self.image.size.width * self.image.size.height * 4 * self.image.frameCount / 1024 / 1024; 71 | self.memoryUsageView.maxDataPoint = memoryUsage; 72 | self.memoryUsageView.shouldShowDescription = self.style == DebugViewStyleDefault; 73 | CGFloat memoryUsageViewWidth = self.style == DebugViewStyleDefault ? 212.0 : 117.0; 74 | self.memoryUsageView.frame = CGRectMake(kMargin, kMargin, memoryUsageViewWidth, 50.0); 75 | 76 | if (!self.frameDelayView) { 77 | self.frameDelayView = [[GraphView alloc] init]; 78 | self.frameDelayView.style = GraphViewStyleFrameDelay; 79 | [self addSubview:self.frameDelayView]; 80 | } 81 | self.frameDelayView.numberOfDisplayedDataPoints = self.image.frameCount * 3; 82 | self.frameDelayView.shouldShowDescription = self.style == DebugViewStyleDefault; 83 | CGFloat graphViewsSpacing = self.style == DebugViewStyleDefault ? 50.0 : 30.0; 84 | CGFloat frameDelayViewWidth = self.style == DebugViewStyleDefault ? 204.0 : 126.0; 85 | self.frameDelayView.frame = CGRectMake(CGRectGetMaxX(self.memoryUsageView.frame) + graphViewsSpacing, kMargin, frameDelayViewWidth, 50.0); 86 | 87 | if (!self.playPauseButton) { 88 | self.playPauseButton = [[RSPlayPauseButton alloc] init]; 89 | self.playPauseButton.paused = NO; 90 | CGRect frame = self.playPauseButton.frame; 91 | frame.origin = CGPointMake(CGRectGetMaxX(self.bounds) - frame.size.width - kMargin, CGRectGetMaxY(self.bounds) - frame.size.height - kMargin); 92 | self.playPauseButton.frame = frame; 93 | self.playPauseButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin; 94 | self.playPauseButton.color = [UIColor colorWithWhite:0.8 alpha:1.0]; 95 | [self.playPauseButton addTarget:self action:@selector(playPauseButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; 96 | [self addSubview:self.playPauseButton]; 97 | } 98 | 99 | if (!self.frameCacheView) { 100 | self.frameCacheView = [[FrameCacheView alloc] init]; 101 | [self addSubview:self.frameCacheView]; 102 | } 103 | self.frameCacheView.frame = CGRectMake(kMargin, self.playPauseButton.frame.origin.y, self.playPauseButton.frame.origin.x - 2 * kMargin, self.playPauseButton.frame.size.height); 104 | self.frameCacheView.image = self.image; 105 | 106 | if (!self.playheadView) { 107 | const CGFloat kSize = 10.0; 108 | self.playheadView = [[PlayheadView alloc] initWithFrame:CGRectMake(0.0, 0.0, kSize, kSize)]; 109 | [self addSubview:self.playheadView]; 110 | } 111 | self.playheadView.center = CGPointMake(self.frameCacheView.frame.origin.x, self.frameCacheView.frame.origin.y - floor(self.playheadView.bounds.size.height / 2) - 3.0); 112 | } 113 | 114 | 115 | #pragma mark - Play/Pause Action 116 | 117 | - (void)playPauseButtonPressed:(RSPlayPauseButton *)playPauseButton 118 | { 119 | if (self.playPauseButton.isPaused) { 120 | [self.playPauseButton setPaused:NO animated:YES]; 121 | [self.imageView startAnimating]; 122 | } else { 123 | [self.playPauseButton setPaused:YES animated:YES]; 124 | [self.imageView stopAnimating]; 125 | } 126 | } 127 | 128 | 129 | #if defined(DEBUG) && DEBUG 130 | #pragma mark - FLAnimatedImageDebugDelegate 131 | 132 | - (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didUpdateCachedFrames:(NSIndexSet *)indexesOfFramesInCache 133 | { 134 | self.frameCacheView.framesInCache = indexesOfFramesInCache; 135 | } 136 | #endif 137 | 138 | 139 | - (void)debug_animatedImage:(FLAnimatedImage *)animatedImage didRequestCachedFrame:(NSUInteger)index 140 | { 141 | if (self.frameCacheView.requestedFrameIndex != index) { 142 | self.frameCacheView.requestedFrameIndex = index; 143 | 144 | NSTimeInterval delayTime = [self.image.delayTimesForIndexes[@(index)] doubleValue]; 145 | CGRect frameRect = ((UIView *)self.frameCacheView.subviews[index]).frame; 146 | 147 | CGPoint playheadStartCenter = CGPointMake(self.frameCacheView.frame.origin.x + frameRect.origin.x, self.playheadView.center.y); 148 | self.playheadView.center = playheadStartCenter; 149 | [UIView animateWithDuration:delayTime delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{ 150 | CGPoint playheadEndCenter = CGPointMake(playheadStartCenter.x + frameRect.size.width, playheadStartCenter.y); 151 | self.playheadView.center = playheadEndCenter; 152 | } completion:NULL]; 153 | 154 | CGFloat memoryUsage = animatedImage.size.width * animatedImage.size.height * 4 * animatedImage.frameCacheSizeCurrent / 1024 / 1024; 155 | [self.memoryUsageView addDataPoint:memoryUsage]; 156 | 157 | [self.frameDelayView addDataPoint:self.currentFrameDelay]; 158 | self.currentFrameDelay = 0.0; 159 | } 160 | } 161 | 162 | 163 | #pragma mark - FLAnimatedImageViewDebugDelegate 164 | 165 | - (void)debug_animatedImageView:(FLAnimatedImageView *)animatedImageView waitingForFrame:(NSUInteger)index duration:(NSTimeInterval)duration 166 | { 167 | self.currentFrameDelay += duration; 168 | } 169 | 170 | 171 | @end 172 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/FLAnimatedImageDemo-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | FLAnimImage 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIcons 12 | 13 | CFBundleIcons~ipad 14 | 15 | CFBundleIdentifier 16 | $(PRODUCT_BUNDLE_IDENTIFIER) 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleName 20 | ${PRODUCT_NAME} 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | 1.0 25 | CFBundleSignature 26 | ???? 27 | CFBundleVersion 28 | 1.0 29 | LSRequiresIPhoneOS 30 | 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UIStatusBarHidden 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | 46 | UIViewControllerBasedStatusBarAppearance 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/FLAnimatedImageDemo-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'AnimatedImageDemo' target in the 'AnimatedImageDemo' project 3 | // 4 | 5 | 6 | #import 7 | 8 | 9 | #ifndef __IPHONE_7_0 10 | #warning "This project uses features only available in iOS SDK 7.0 and later." 11 | #endif 12 | 13 | #ifdef __OBJC__ 14 | #import 15 | #import 16 | #endif 17 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/FrameCacheView.h: -------------------------------------------------------------------------------- 1 | // 2 | // FrameCacheView.h 3 | // FLAnimatedImageDemo 4 | // 5 | // Created by Raphael Schaad on 4/1/14. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import 11 | 12 | @class FLAnimatedImage; 13 | 14 | 15 | @interface FrameCacheView : UIView 16 | 17 | @property (nonatomic, strong) FLAnimatedImage *image; 18 | @property (nonatomic, strong) NSIndexSet *framesInCache; 19 | @property (nonatomic, assign) NSUInteger requestedFrameIndex; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/FrameCacheView.m: -------------------------------------------------------------------------------- 1 | // 2 | // FrameCacheView.m 3 | // FLAnimatedImageDemo 4 | // 5 | // Created by Raphael Schaad on 4/1/14. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import "FrameCacheView.h" 11 | #import 12 | 13 | 14 | @implementation FrameCacheView 15 | 16 | - (void)setImage:(FLAnimatedImage *)image 17 | { 18 | if (![_image isEqual:image]) { 19 | _image = image; 20 | 21 | for (UIView *subview in self.subviews) { 22 | [subview removeFromSuperview]; 23 | } 24 | 25 | for (NSUInteger i = 0; i < _image.frameCount; i++) { 26 | UIView *frameView = [[UIView alloc] init]; 27 | frameView.layer.borderWidth = 1.0; 28 | frameView.layer.borderColor = [UIColor colorWithWhite:0.8 alpha:1.0].CGColor; 29 | [self addSubview:frameView]; 30 | } 31 | 32 | [self updateSubviewFrames]; 33 | 34 | [self setNeedsLayout]; 35 | } 36 | } 37 | 38 | 39 | - (void)setFrame:(CGRect)frame 40 | { 41 | if (!CGRectEqualToRect(self.frame, frame)) { 42 | super.frame = frame; 43 | [self updateSubviewFrames]; 44 | [self setNeedsLayout]; 45 | } 46 | } 47 | 48 | 49 | - (void)setFramesInCache:(NSIndexSet *)framesInCache 50 | { 51 | if (![_framesInCache isEqual:framesInCache]) { 52 | _framesInCache = framesInCache; 53 | [self setNeedsLayout]; 54 | } 55 | } 56 | 57 | 58 | - (void)setRequestedFrameIndex:(NSUInteger)requestedFrameIndex 59 | { 60 | if (_requestedFrameIndex != requestedFrameIndex) { 61 | _requestedFrameIndex = requestedFrameIndex; 62 | [self setNeedsLayout]; 63 | } 64 | } 65 | 66 | 67 | - (instancetype)initWithFrame:(CGRect)frame 68 | { 69 | self = [super initWithFrame:frame]; 70 | if (self) { 71 | self.requestedFrameIndex = NSNotFound; 72 | self.backgroundColor = [UIColor clearColor]; 73 | } 74 | return self; 75 | } 76 | 77 | 78 | - (void)layoutSubviews 79 | { 80 | [super layoutSubviews]; 81 | 82 | NSUInteger i = 0; 83 | for (UIView *subview in self.subviews) { 84 | BOOL isRequestedFrame = (i == self.requestedFrameIndex); 85 | BOOL isCached = [self.framesInCache containsIndex:i]; 86 | UIColor *fillColor = [UIColor clearColor]; 87 | if (isCached) { 88 | fillColor = [UIColor colorWithWhite:1.0 alpha:0.5]; 89 | } else if (isRequestedFrame) { 90 | fillColor = [UIColor colorWithRed:0.8 green:0.15 blue:0.15 alpha:0.6]; 91 | } 92 | subview.backgroundColor = fillColor; 93 | 94 | i++; 95 | } 96 | } 97 | 98 | 99 | - (void)updateSubviewFrames 100 | { 101 | NSTimeInterval delayTimesTotal = [[[self.image.delayTimesForIndexes allValues] valueForKeyPath:@"@sum.self"] doubleValue]; 102 | CGFloat x = 0.0; 103 | NSUInteger i = 0; 104 | for (UIView *subview in self.subviews) { 105 | CGFloat width = self.bounds.size.width * [self.image.delayTimesForIndexes[@(i)] doubleValue] / delayTimesTotal + subview.layer.borderWidth; 106 | CGRect frame = CGRectMake(x, 0.0, width, self.bounds.size.height); 107 | 108 | subview.frame = frame; 109 | 110 | x += width - subview.layer.borderWidth; 111 | i++; 112 | } 113 | } 114 | 115 | 116 | @end 117 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/GraphView.h: -------------------------------------------------------------------------------- 1 | // 2 | // GraphView.h 3 | // FLAnimatedImageDemo 4 | // 5 | // Created by Raphael Schaad on 4/1/14. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import 11 | 12 | typedef NS_ENUM(NSUInteger, GraphViewStyle) { 13 | GraphViewStyleMemoryUsage, 14 | GraphViewStyleFrameDelay 15 | }; 16 | 17 | 18 | @interface GraphView : UIView 19 | 20 | @property (nonatomic, assign) GraphViewStyle style; // Default is `GraphViewStyleMemoryUsage` 21 | @property (nonatomic, assign) BOOL shouldShowDescription; // Default is YES 22 | @property (nonatomic, assign) NSUInteger numberOfDisplayedDataPoints; // Default is 50 23 | @property (nonatomic, assign) CGFloat maxDataPoint; // Default is the max of all data points added so far 24 | 25 | - (void)addDataPoint:(CGFloat)dataPoint; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/GraphView.m: -------------------------------------------------------------------------------- 1 | // 2 | // GraphView.m 3 | // FLAnimatedImageDemo 4 | // 5 | // Created by Raphael Schaad on 4/1/14. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import "GraphView.h" 11 | 12 | 13 | @interface GraphView () 14 | 15 | @property (nonatomic, strong) NSMutableArray *dataPoints; 16 | @property (nonatomic, strong) CAShapeLayer *graphLayer; 17 | @property (nonatomic, strong) UILabel *topYAxisLabel; 18 | @property (nonatomic, strong) UILabel *bottomYAxisLabel; 19 | @property (nonatomic, strong) UILabel *descriptionLabel; 20 | 21 | @end 22 | 23 | 24 | @implementation GraphView 25 | 26 | - (void)setStyle:(GraphViewStyle)style 27 | { 28 | if (_style != style) { 29 | _style = style; 30 | [self setNeedsLayout]; 31 | } 32 | } 33 | 34 | 35 | - (void)setShouldShowDescription:(BOOL)shouldShowDescription 36 | { 37 | if (_shouldShowDescription != shouldShowDescription) { 38 | _shouldShowDescription = shouldShowDescription; 39 | [self setNeedsLayout]; 40 | } 41 | } 42 | 43 | 44 | - (CGFloat)maxDataPoint 45 | { 46 | CGFloat maxDataPoint = _maxDataPoint; 47 | if (maxDataPoint <= 0.0) { 48 | maxDataPoint = [[self.dataPoints valueForKeyPath:@"@max.self"] doubleValue];; 49 | } 50 | return maxDataPoint; 51 | } 52 | 53 | 54 | - (id)initWithFrame:(CGRect)frame 55 | { 56 | self = [super initWithFrame:frame]; 57 | if (self) { 58 | self.style = GraphViewStyleMemoryUsage; 59 | self.shouldShowDescription = YES; 60 | self.numberOfDisplayedDataPoints = 50; 61 | self.dataPoints = [NSMutableArray array]; 62 | self.opaque = NO; 63 | } 64 | return self; 65 | } 66 | 67 | 68 | - (void)addDataPoint:(CGFloat)dataPoint 69 | { 70 | if ([self.dataPoints count] >= self.numberOfDisplayedDataPoints) { 71 | [self.dataPoints removeObjectAtIndex:0]; 72 | } 73 | [self.dataPoints addObject:@(dataPoint)]; 74 | 75 | [self setNeedsLayout]; 76 | } 77 | 78 | 79 | - (void)layoutSubviews 80 | { 81 | [super layoutSubviews]; 82 | 83 | CGFloat rightEdge = CGRectGetMaxX(self.bounds); 84 | 85 | if (self.shouldShowDescription) { 86 | if (!self.descriptionLabel) { 87 | self.descriptionLabel = [[UILabel alloc] init]; 88 | self.descriptionLabel.numberOfLines = 0; 89 | self.descriptionLabel.backgroundColor = [UIColor clearColor]; 90 | self.descriptionLabel.textColor = [UIColor colorWithWhite:0.8 alpha:1.0]; 91 | self.descriptionLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:13.0]; 92 | [self addSubview:self.descriptionLabel]; 93 | } 94 | self.descriptionLabel.text = self.style == GraphViewStyleMemoryUsage ? @"Memory usage\n(in MB)" : @"Frame delay\n(in ms)"; 95 | [self.descriptionLabel sizeToFit]; 96 | self.descriptionLabel.frame = CGRectMake(rightEdge - self.descriptionLabel.bounds.size.width, self.bounds.origin.y, self.descriptionLabel.bounds.size.width, self.bounds.size.height); 97 | rightEdge = self.descriptionLabel.frame.origin.x - 8.0; 98 | } 99 | self.descriptionLabel.hidden = !self.shouldShowDescription; 100 | 101 | if (!self.bottomYAxisLabel) { 102 | self.bottomYAxisLabel = [[UILabel alloc] init]; 103 | self.bottomYAxisLabel.backgroundColor = [UIColor clearColor]; 104 | self.bottomYAxisLabel.textColor = [UIColor colorWithWhite:0.8 alpha:1.0]; 105 | self.bottomYAxisLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:13.0]; 106 | self.bottomYAxisLabel.text = @"0"; 107 | [self.bottomYAxisLabel sizeToFit]; 108 | [self addSubview:self.bottomYAxisLabel]; 109 | } 110 | self.bottomYAxisLabel.frame = CGRectMake(rightEdge - self.topYAxisLabel.bounds.size.width, CGRectGetMaxY(self.bounds) - floor(self.bottomYAxisLabel.bounds.size.height / 2), self.bottomYAxisLabel.bounds.size.width, self.bottomYAxisLabel.bounds.size.height); 111 | 112 | BOOL shouldShowTopYAxisLabel = self.maxDataPoint > 0.0; 113 | if (shouldShowTopYAxisLabel) { 114 | if (!self.topYAxisLabel) { 115 | self.topYAxisLabel = [[UILabel alloc] init]; 116 | self.topYAxisLabel.backgroundColor = [UIColor clearColor]; 117 | self.topYAxisLabel.textColor = [UIColor colorWithWhite:0.8 alpha:1.0]; 118 | self.topYAxisLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:13.0]; 119 | [self addSubview:self.topYAxisLabel]; 120 | } 121 | CGFloat topYAxisDataPoint = self.maxDataPoint; 122 | if (self.style == GraphViewStyleFrameDelay) { 123 | topYAxisDataPoint *= 1000; 124 | } 125 | topYAxisDataPoint *= 3.0 / 2.0; 126 | self.topYAxisLabel.text = [NSString stringWithFormat:@"%.1f", topYAxisDataPoint]; 127 | [self.topYAxisLabel sizeToFit]; 128 | self.topYAxisLabel.frame = CGRectMake(self.bottomYAxisLabel.frame.origin.x, self.bounds.origin.y - floor(self.topYAxisLabel.bounds.size.height / 2), self.topYAxisLabel.bounds.size.width, self.bottomYAxisLabel.bounds.size.height); 129 | } 130 | self.topYAxisLabel.hidden = !shouldShowTopYAxisLabel; 131 | 132 | if (!self.graphLayer) { 133 | self.graphLayer = [CAShapeLayer layer]; 134 | self.graphLayer.masksToBounds = YES; 135 | self.graphLayer.borderWidth = 1.0; 136 | self.graphLayer.borderColor = [UIColor colorWithWhite:0.8 alpha:1.0].CGColor; 137 | [self.layer addSublayer:self.graphLayer]; 138 | } 139 | self.graphLayer.fillColor = self.style == GraphViewStyleMemoryUsage ? [UIColor colorWithWhite:1.0 alpha:0.5].CGColor : [UIColor colorWithRed:0.8 green:0.15 blue:0.15 alpha:0.6].CGColor; 140 | CGRect graphRect = UIEdgeInsetsInsetRect(self.bounds, UIEdgeInsetsMake(0.0, 0.0, 0.0, CGRectGetMaxX(self.bounds) - CGRectGetMinX(self.bottomYAxisLabel.frame) + 4.0)); 141 | self.graphLayer.frame = graphRect; 142 | 143 | const CGFloat kGraphStepWidth = CGRectGetWidth(graphRect) / (self.numberOfDisplayedDataPoints - 1); 144 | CGFloat scaleFactor = 0.0; 145 | if (self.maxDataPoint > 0.0) { 146 | CGFloat maxHeightScaleFactor = self.style == GraphViewStyleMemoryUsage ? 0.9 : 0.7; 147 | scaleFactor = CGRectGetHeight(graphRect) * maxHeightScaleFactor / self.maxDataPoint; 148 | } 149 | CGFloat currentGraphX = CGRectGetMinX(graphRect); 150 | UIBezierPath *path = [UIBezierPath bezierPath]; 151 | [path moveToPoint:CGPointMake(currentGraphX, CGRectGetMaxY(graphRect))]; 152 | for (NSUInteger i = 0; i < self.numberOfDisplayedDataPoints; i++) { 153 | CGFloat graphHeight = 1.0; 154 | if (i < [self.dataPoints count]) { 155 | graphHeight += MAX(2.0, [self.dataPoints[i] doubleValue] * scaleFactor); 156 | } 157 | CGFloat graphY = CGRectGetMaxY(graphRect) - graphHeight; 158 | [path addLineToPoint:CGPointMake(currentGraphX, graphY)]; 159 | currentGraphX += kGraphStepWidth; 160 | } 161 | [path addLineToPoint:CGPointMake(currentGraphX, CGRectGetHeight(graphRect))]; 162 | [path closePath]; 163 | 164 | self.graphLayer.path = path.CGPath; 165 | } 166 | 167 | 168 | @end 169 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "ipad", 5 | "scale" : "1x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "ipad", 10 | "scale" : "2x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "ipad", 15 | "scale" : "1x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "scale" : "2x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "scale" : "1x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "ipad", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "Icon-76.png", 35 | "idiom" : "ipad", 36 | "scale" : "1x", 37 | "size" : "76x76" 38 | }, 39 | { 40 | "filename" : "Icon-76@2x.png", 41 | "idiom" : "ipad", 42 | "scale" : "2x", 43 | "size" : "76x76" 44 | }, 45 | { 46 | "filename" : "Icon-83.5@2x.png", 47 | "idiom" : "ipad", 48 | "scale" : "2x", 49 | "size" : "83.5x83.5" 50 | }, 51 | { 52 | "filename" : "Icon-1024.png", 53 | "idiom" : "ios-marketing", 54 | "scale" : "1x", 55 | "size" : "1024x1024" 56 | } 57 | ], 58 | "info" : { 59 | "author" : "xcode", 60 | "version" : 1 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/Images.xcassets/AppIcon.appiconset/ICON-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flipboard/FLAnimatedImage/d4f07b6f164d53c1212c3e54d6460738b1981e9f/FLAnimatedImageDemo/Images.xcassets/AppIcon.appiconset/ICON-1024.png -------------------------------------------------------------------------------- /FLAnimatedImageDemo/Images.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flipboard/FLAnimatedImage/d4f07b6f164d53c1212c3e54d6460738b1981e9f/FLAnimatedImageDemo/Images.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /FLAnimatedImageDemo/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flipboard/FLAnimatedImage/d4f07b6f164d53c1212c3e54d6460738b1981e9f/FLAnimatedImageDemo/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png -------------------------------------------------------------------------------- /FLAnimatedImageDemo/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flipboard/FLAnimatedImage/d4f07b6f164d53c1212c3e54d6460738b1981e9f/FLAnimatedImageDemo/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png -------------------------------------------------------------------------------- /FLAnimatedImageDemo/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "landscape", 5 | "idiom" : "ipad", 6 | "minimum-system-version" : "7.0", 7 | "extent" : "full-screen", 8 | "scale" : "1x" 9 | }, 10 | { 11 | "orientation" : "landscape", 12 | "idiom" : "ipad", 13 | "minimum-system-version" : "7.0", 14 | "extent" : "full-screen", 15 | "scale" : "2x" 16 | }, 17 | { 18 | "orientation" : "portrait", 19 | "idiom" : "ipad", 20 | "minimum-system-version" : "7.0", 21 | "extent" : "full-screen", 22 | "scale" : "1x" 23 | }, 24 | { 25 | "orientation" : "portrait", 26 | "idiom" : "ipad", 27 | "minimum-system-version" : "7.0", 28 | "extent" : "full-screen", 29 | "scale" : "2x" 30 | } 31 | ], 32 | "info" : { 33 | "version" : 1, 34 | "author" : "xcode" 35 | } 36 | } -------------------------------------------------------------------------------- /FLAnimatedImageDemo/PlayheadView.h: -------------------------------------------------------------------------------- 1 | // 2 | // PlayheadView.h 3 | // FLAnimatedImageDemo 4 | // 5 | // Created by Raphael Schaad on 4/1/14. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import 11 | 12 | 13 | @interface PlayheadView : UIView 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/PlayheadView.m: -------------------------------------------------------------------------------- 1 | // 2 | // PlayheadView.m 3 | // FLAnimatedImageDemo 4 | // 5 | // Created by Raphael Schaad on 4/1/14. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import "PlayheadView.h" 11 | 12 | 13 | @implementation PlayheadView 14 | 15 | - (instancetype)initWithFrame:(CGRect)frame 16 | { 17 | self = [super initWithFrame:frame]; 18 | if (self) { 19 | self.opaque = NO; 20 | } 21 | return self; 22 | } 23 | 24 | 25 | - (void)drawRect:(CGRect)rect 26 | { 27 | UIBezierPath *path = [UIBezierPath bezierPath]; 28 | [path moveToPoint:rect.origin]; 29 | [path addLineToPoint:CGPointMake(CGRectGetMaxX(rect), rect.origin.y)]; 30 | [path addLineToPoint:CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect))]; 31 | [path closePath]; 32 | [[UIColor colorWithWhite:0.8 alpha:1.0] setFill]; 33 | [path fill]; 34 | } 35 | 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/RSPlayPauseButton.h: -------------------------------------------------------------------------------- 1 | // 2 | // RSPlayPauseButton.h 3 | // 4 | // Created by Raphael Schaad on 2014-03-22. 5 | // This is free and unencumbered software released into the public domain. 6 | // 7 | 8 | 9 | #import 10 | 11 | 12 | typedef NS_ENUM(NSUInteger, RSPlayPauseButtonAnimationStyle) { 13 | RSPlayPauseButtonAnimationStyleSplit, 14 | RSPlayPauseButtonAnimationStyleSplitAndRotate // Default 15 | }; 16 | 17 | 18 | // 19 | // Displays a ⃝ with either the ► (play) or ❚❚ (pause) icon and nicely morphs between the two states. 20 | // 21 | @interface RSPlayPauseButton : UIControl 22 | 23 | // State 24 | @property (nonatomic, assign, getter = isPaused) BOOL paused; // Default is `YES`; changing this way is not animated 25 | - (void)setPaused:(BOOL)paused animated:(BOOL)animated; 26 | 27 | // Style 28 | @property (nonatomic, strong) UIColor *color; // Default is 96% black 29 | @property (nonatomic, assign) RSPlayPauseButtonAnimationStyle animationStyle; // Default is `RSPlayPauseButtonAnimationStyleSplitAndRotate` 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/RSPlayPauseButton.m: -------------------------------------------------------------------------------- 1 | // 2 | // RSPlayPauseButton.m 3 | // 4 | // Created by Raphael Schaad https://github.com/raphaelschaad on 2014-03-22. 5 | // This is free and unencumbered software released into the public domain. 6 | // 7 | 8 | 9 | #import "RSPlayPauseButton.h" 10 | #include // type generic math, yo: http://en.wikipedia.org/wiki/Tgmath.h#tgmath.h 11 | 12 | 13 | static const CGFloat kScale = 1.0; 14 | static const CGFloat kBorderSize = 32.0 * kScale; 15 | static const CGFloat kBorderWidth = 3.0 * kScale; 16 | static const CGFloat kSize = kBorderSize + kBorderWidth; // The total size is the border size + 2x half the border width. 17 | static const CGFloat kPauseLineWidth = 4.0 * kScale; 18 | static const CGFloat kPauseLineHeight = 15.0 * kScale; 19 | static const CGFloat kPauseLinesSpace = 4.0 * kScale; 20 | static const CGFloat kPlayTriangleOffsetX = 1.0 * kScale; 21 | static const CGFloat kPlayTriangleTipOffsetX = 2.0 * kScale; 22 | 23 | static const CGPoint p1 = {0.0, 0.0}; // line 1, top left 24 | static const CGPoint p2 = {kPauseLineWidth, 0.0}; // line 1, top right 25 | static const CGPoint p3 = {kPauseLineWidth, kPauseLineHeight}; // line 1, bottom right 26 | static const CGPoint p4 = {0.0, kPauseLineHeight}; // line 1, bottom left 27 | 28 | static const CGPoint p5 = {kPauseLineWidth + kPauseLinesSpace, 0.0}; // line 2, top left 29 | static const CGPoint p6 = {kPauseLineWidth + kPauseLinesSpace + kPauseLineWidth, 0.0}; // line 2, top right 30 | static const CGPoint p7 = {kPauseLineWidth + kPauseLinesSpace + kPauseLineWidth, kPauseLineHeight}; // line 2, bottom right 31 | static const CGPoint p8 = {kPauseLineWidth + kPauseLinesSpace, kPauseLineHeight}; // line 2, bottom left 32 | 33 | 34 | @interface RSPlayPauseButton () 35 | 36 | @property (nonatomic, strong) CAShapeLayer *borderShapeLayer; 37 | @property (nonatomic, strong) CAShapeLayer *playPauseShapeLayer; 38 | @property (nonatomic, strong, readonly) UIBezierPath *pauseBezierPath; 39 | @property (nonatomic, strong, readonly) UIBezierPath *pauseRotateBezierPath; 40 | @property (nonatomic, strong, readonly) UIBezierPath *playBezierPath; 41 | @property (nonatomic, strong, readonly) UIBezierPath *playRotateBezierPath; 42 | 43 | @end 44 | 45 | 46 | @implementation RSPlayPauseButton 47 | 48 | #pragma mark - Accessors 49 | #pragma mark Public 50 | 51 | - (void)setPaused:(BOOL)paused 52 | { 53 | if (_paused != paused) { 54 | [self setPaused:paused animated:NO]; 55 | } 56 | } 57 | 58 | - (void)setColor:(UIColor *)color 59 | { 60 | if (![_color isEqual:color]) { 61 | _color = color; 62 | 63 | [self setNeedsLayout]; 64 | } 65 | } 66 | 67 | 68 | #pragma mark Private 69 | 70 | @synthesize pauseBezierPath = _pauseBezierPath; 71 | 72 | - (UIBezierPath *)pauseBezierPath 73 | { 74 | if (!_pauseBezierPath) { 75 | _pauseBezierPath = [UIBezierPath bezierPath]; 76 | 77 | // Subpath for 1. line 78 | [_pauseBezierPath moveToPoint:p1]; 79 | [_pauseBezierPath addLineToPoint:p2]; 80 | [_pauseBezierPath addLineToPoint:p3]; 81 | [_pauseBezierPath addLineToPoint:p4]; 82 | [_pauseBezierPath closePath]; 83 | 84 | // Subpath for 2. line 85 | [_pauseBezierPath moveToPoint:p5]; 86 | [_pauseBezierPath addLineToPoint:p6]; 87 | [_pauseBezierPath addLineToPoint:p7]; 88 | [_pauseBezierPath addLineToPoint:p8]; 89 | [_pauseBezierPath closePath]; 90 | } 91 | 92 | return _pauseBezierPath; 93 | } 94 | 95 | 96 | @synthesize pauseRotateBezierPath = _pauseRotateBezierPath; 97 | 98 | - (UIBezierPath *)pauseRotateBezierPath 99 | { 100 | if (!_pauseRotateBezierPath) { 101 | _pauseRotateBezierPath = [UIBezierPath bezierPath]; 102 | 103 | // Subpath for 1. line 104 | [_pauseRotateBezierPath moveToPoint:p7]; 105 | [_pauseRotateBezierPath addLineToPoint:p8]; 106 | [_pauseRotateBezierPath addLineToPoint:p5]; 107 | [_pauseRotateBezierPath addLineToPoint:p6]; 108 | [_pauseRotateBezierPath closePath]; 109 | 110 | // Subpath for 2. line 111 | [_pauseRotateBezierPath moveToPoint:p3]; 112 | [_pauseRotateBezierPath addLineToPoint:p4]; 113 | [_pauseRotateBezierPath addLineToPoint:p1]; 114 | [_pauseRotateBezierPath addLineToPoint:p2]; 115 | [_pauseRotateBezierPath closePath]; 116 | } 117 | 118 | return _pauseRotateBezierPath; 119 | } 120 | 121 | 122 | @synthesize playBezierPath = _playBezierPath; 123 | 124 | - (UIBezierPath *)playBezierPath 125 | { 126 | if (!_playBezierPath) { 127 | _playBezierPath = [UIBezierPath bezierPath]; 128 | 129 | const CGFloat kPauseLinesHalfSpace = floor(kPauseLinesSpace / 2); 130 | const CGFloat kPauseLineHalfHeight = floor(kPauseLineHeight / 2); 131 | 132 | CGPoint _p1 = CGPointMake(p1.x + kPlayTriangleOffsetX, p1.y); 133 | CGPoint _p2 = CGPointMake(p2.x + kPauseLinesHalfSpace, p2.y); 134 | CGPoint _p3 = CGPointMake(p3.x + kPauseLinesHalfSpace, p3.y); 135 | CGPoint _p4 = CGPointMake(p4.x + kPlayTriangleOffsetX, p4.y); 136 | 137 | CGPoint _p5 = CGPointMake(p5.x - kPauseLinesHalfSpace, p5.y); 138 | CGPoint _p6 = CGPointMake(p6.x + kPlayTriangleTipOffsetX, p6.y); 139 | CGPoint _p7 = CGPointMake(p7.x + kPlayTriangleTipOffsetX, p7.y); 140 | CGPoint _p8 = CGPointMake(p8.x - kPauseLinesHalfSpace, p8.y); 141 | 142 | const CGFloat kPlayTriangleWidth = _p6.x - _p1.x; 143 | 144 | _p2.y += kPauseLineHalfHeight * (_p2.x - kPlayTriangleOffsetX) / kPlayTriangleWidth; 145 | _p3.y -= kPauseLineHalfHeight * (_p3.x - kPlayTriangleOffsetX) / kPlayTriangleWidth; 146 | 147 | _p5.y += kPauseLineHalfHeight * (_p5.x - kPlayTriangleOffsetX) / kPlayTriangleWidth; 148 | 149 | _p6.y = kPauseLineHalfHeight; 150 | _p7.y = kPauseLineHalfHeight; 151 | 152 | _p8.y -= kPauseLineHalfHeight * (_p8.x - kPlayTriangleOffsetX) / kPlayTriangleWidth; 153 | 154 | [_playBezierPath moveToPoint:_p1]; 155 | [_playBezierPath addLineToPoint:_p2]; 156 | [_playBezierPath addLineToPoint:_p3]; 157 | [_playBezierPath addLineToPoint:_p4]; 158 | [_playBezierPath closePath]; 159 | 160 | [_playBezierPath moveToPoint:_p5]; 161 | [_playBezierPath addLineToPoint:_p6]; 162 | [_playBezierPath addLineToPoint:_p7]; 163 | [_playBezierPath addLineToPoint:_p8]; 164 | [_playBezierPath closePath]; 165 | } 166 | 167 | return _playBezierPath; 168 | } 169 | 170 | 171 | @synthesize playRotateBezierPath = _playRotateBezierPath; 172 | 173 | - (UIBezierPath *)playRotateBezierPath 174 | { 175 | if (!_playRotateBezierPath) { 176 | _playRotateBezierPath = [UIBezierPath bezierPath]; 177 | 178 | const CGFloat kPauseLineHalfHeight = floor(kPauseLineHeight / 2); 179 | 180 | CGPoint _p1, _p2, _p3, _p4, _p5, _p6, _p7, _p8; 181 | _p1 = _p2 = _p5 = _p6 = CGPointMake(p6.x + kPlayTriangleTipOffsetX, kPauseLineHalfHeight); 182 | _p3 = _p8 = CGPointMake(p1.x + kPlayTriangleOffsetX, kPauseLineHalfHeight); 183 | _p4 = CGPointMake(p1.x + kPlayTriangleOffsetX, p1.y); 184 | _p7 = CGPointMake(p4.x + kPlayTriangleOffsetX, p4.y); 185 | 186 | [_playRotateBezierPath moveToPoint:_p1]; 187 | [_playRotateBezierPath addLineToPoint:_p2]; 188 | [_playRotateBezierPath addLineToPoint:_p3]; 189 | [_playRotateBezierPath addLineToPoint:_p4]; 190 | [_playRotateBezierPath closePath]; 191 | 192 | [_playRotateBezierPath moveToPoint:_p5]; 193 | [_playRotateBezierPath addLineToPoint:_p6]; 194 | [_playRotateBezierPath addLineToPoint:_p7]; 195 | [_playRotateBezierPath addLineToPoint:_p8]; 196 | [_playRotateBezierPath closePath]; 197 | } 198 | 199 | return _playRotateBezierPath; 200 | } 201 | 202 | 203 | #pragma mark - Life Cycle 204 | 205 | - (id)initWithFrame:(CGRect)frame 206 | { 207 | self = [super initWithFrame:frame]; 208 | if (self) { 209 | _paused = YES; 210 | _color = [UIColor colorWithWhite:0.04 alpha:1.0]; 211 | _animationStyle = RSPlayPauseButtonAnimationStyleSplitAndRotate; 212 | 213 | [self sizeToFit]; 214 | } 215 | return self; 216 | } 217 | 218 | 219 | #pragma mark - UIView Method Overrides 220 | #pragma mark Configuring the Resizing Behavior 221 | 222 | - (CGSize)sizeThatFits:(CGSize)size 223 | { 224 | // Ignore the current size/new size by super, and instead use our default size. 225 | return CGSizeMake(kSize, kSize); 226 | } 227 | 228 | 229 | #pragma mark Laying out Subviews 230 | 231 | - (void)layoutSubviews 232 | { 233 | [super layoutSubviews]; 234 | 235 | if (!self.borderShapeLayer) { 236 | self.borderShapeLayer = [[CAShapeLayer alloc] init]; 237 | // Adjust for line width. 238 | CGRect borderRect = CGRectInset(self.bounds, ceil(kBorderWidth / 2), ceil(kBorderWidth / 2)); 239 | self.borderShapeLayer.path = [UIBezierPath bezierPathWithOvalInRect:borderRect].CGPath; 240 | self.borderShapeLayer.lineWidth = kBorderWidth; 241 | self.borderShapeLayer.fillColor = [UIColor clearColor].CGColor; 242 | [self.layer addSublayer:self.borderShapeLayer]; 243 | } 244 | self.borderShapeLayer.strokeColor = self.color.CGColor; 245 | 246 | if (!self.playPauseShapeLayer) { 247 | self.playPauseShapeLayer = [[CAShapeLayer alloc] init]; 248 | CGRect playPauseRect = CGRectZero; 249 | playPauseRect.origin.x = floor(((self.bounds.size.width) - (kPauseLineWidth + kPauseLinesSpace + kPauseLineWidth)) / 2); 250 | playPauseRect.origin.y = floor(((self.bounds.size.height) - (kPauseLineHeight)) / 2); 251 | playPauseRect.size.width = kPauseLineWidth + kPauseLinesSpace + kPauseLineWidth + kPlayTriangleTipOffsetX; 252 | playPauseRect.size.height = kPauseLineHeight; 253 | self.playPauseShapeLayer.frame = playPauseRect; 254 | UIBezierPath *path = self.isPaused ? self.playRotateBezierPath : self.pauseBezierPath; 255 | self.playPauseShapeLayer.path = path.CGPath; 256 | [self.layer addSublayer:self.playPauseShapeLayer]; 257 | } 258 | self.playPauseShapeLayer.fillColor = self.color.CGColor; 259 | } 260 | 261 | 262 | #pragma mark - Public Methods 263 | 264 | - (void)setPaused:(BOOL)paused animated:(BOOL)animated 265 | { 266 | if (_paused != paused) { 267 | _paused = paused; 268 | 269 | UIBezierPath *fromPath = nil; 270 | UIBezierPath *toPath = nil; 271 | if (self.animationStyle == RSPlayPauseButtonAnimationStyleSplit) { 272 | fromPath = self.isPaused ? self.pauseBezierPath : self.playBezierPath; 273 | toPath = self.isPaused ? self.playBezierPath : self.pauseBezierPath; 274 | } else if (self.animationStyle == RSPlayPauseButtonAnimationStyleSplitAndRotate) { 275 | fromPath = self.isPaused ? self.pauseBezierPath : self.playRotateBezierPath; 276 | toPath = self.isPaused ? self.playRotateBezierPath : self.pauseRotateBezierPath; 277 | } else { 278 | // Unsupported animation style 279 | } 280 | 281 | if (animated) { 282 | // Morph between the two states. 283 | CABasicAnimation *morphAnimation = [CABasicAnimation animationWithKeyPath:@"path"]; 284 | 285 | CAMediaTimingFunction *timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; 286 | [morphAnimation setTimingFunction:timingFunction]; 287 | 288 | // Make the new state stick. 289 | [morphAnimation setRemovedOnCompletion:NO]; 290 | [morphAnimation setFillMode:kCAFillModeForwards]; 291 | 292 | morphAnimation.duration = 0.3; 293 | morphAnimation.fromValue = (__bridge id)fromPath.CGPath; 294 | morphAnimation.toValue = (__bridge id)toPath.CGPath; 295 | 296 | [self.playPauseShapeLayer addAnimation:morphAnimation forKey:nil]; 297 | } else { 298 | self.playPauseShapeLayer.path = toPath.CGPath; 299 | } 300 | } 301 | } 302 | 303 | 304 | @end 305 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/RootViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // RootViewController.h 3 | // FLAnimatedImageDemo 4 | // 5 | // Created by Raphael Schaad on 4/1/14. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import 11 | 12 | 13 | @interface RootViewController : UIViewController 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/RootViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // RootViewController.m 3 | // FLAnimatedImageDemo 4 | // 5 | // Created by Raphael Schaad on 4/1/14. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import "RootViewController.h" 11 | #import 12 | #import "DebugView.h" 13 | 14 | 15 | @interface RootViewController () 16 | 17 | @property (nonatomic, strong) UILabel *titleLabel; 18 | @property (nonatomic, strong) UILabel *subtitleLabel; 19 | @property (nonatomic, strong) UIButton *memoryWarningButton; 20 | 21 | @property (nonatomic, strong) FLAnimatedImageView *imageView1; 22 | @property (nonatomic, strong) FLAnimatedImageView *imageView2; 23 | @property (nonatomic, strong) FLAnimatedImageView *imageView3; 24 | 25 | // Views for the debug overlay UI 26 | @property (nonatomic, strong) DebugView *debugView1; 27 | @property (nonatomic, strong) DebugView *debugView2; 28 | @property (nonatomic, strong) DebugView *debugView3; 29 | 30 | @end 31 | 32 | // Internal properties on FLAnimatedImage and FLAnimatedImageView, only availabe in debug and used exclusively for the sample project. 33 | #if defined(DEBUG) && DEBUG 34 | 35 | @interface FLAnimatedImage (Private) 36 | @property (nonatomic, weak) id debug_delegate; 37 | @end 38 | 39 | @implementation FLAnimatedImage (Private) 40 | @dynamic debug_delegate; 41 | @end 42 | 43 | @interface FLAnimatedImageView (Private) 44 | @property (nonatomic, weak) id debug_delegate; 45 | @end 46 | 47 | @implementation FLAnimatedImageView (Private) 48 | @dynamic debug_delegate; 49 | @end 50 | 51 | #endif 52 | 53 | 54 | @implementation RootViewController 55 | 56 | - (void)viewWillAppear:(BOOL)animated 57 | { 58 | [super viewWillAppear:animated]; 59 | 60 | self.view.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0]; 61 | 62 | self.titleLabel.frame = CGRectMake(18.0, 27.0, self.titleLabel.bounds.size.width, self.titleLabel.bounds.size.height); 63 | self.subtitleLabel.frame = CGRectMake(20.0, 74.0, self.subtitleLabel.bounds.size.width, self.subtitleLabel.bounds.size.height); 64 | self.memoryWarningButton.frame = CGRectMake(544.0, 69.0, self.memoryWarningButton.bounds.size.width, self.memoryWarningButton.bounds.size.height); 65 | 66 | 67 | 68 | // Setup the three `FLAnimatedImageView`s and load GIFs into them: 69 | 70 | // 1 71 | if (!self.imageView1) { 72 | self.imageView1 = [[FLAnimatedImageView alloc] init]; 73 | self.imageView1.contentMode = UIViewContentModeScaleAspectFill; 74 | self.imageView1.clipsToBounds = YES; 75 | } 76 | [self.view addSubview:self.imageView1]; 77 | self.imageView1.frame = CGRectMake(0.0, 120.0, self.view.bounds.size.width, 447.0); 78 | 79 | NSURL *url1 = [[NSBundle mainBundle] URLForResource:@"rock" withExtension:@"gif"]; 80 | NSData *data1 = [NSData dataWithContentsOfURL:url1]; 81 | FLAnimatedImage *animatedImage1 = [FLAnimatedImage animatedImageWithGIFData:data1]; 82 | self.imageView1.animatedImage = animatedImage1; 83 | 84 | // 2 85 | if (!self.imageView2) { 86 | self.imageView2 = [[FLAnimatedImageView alloc] init]; 87 | self.imageView2.contentMode = UIViewContentModeScaleAspectFill; 88 | self.imageView2.clipsToBounds = YES; 89 | } 90 | [self.view addSubview:self.imageView2]; 91 | self.imageView2.frame = CGRectMake(0.0, 577.0, 379.0, 447.0); 92 | 93 | NSURL *url2 = [NSURL URLWithString:@"https://cloud.githubusercontent.com/assets/1567433/10417835/1c97e436-7052-11e5-8fb5-69373072a5a0.gif"]; 94 | [self loadAnimatedImageWithURL:url2 completion:^(FLAnimatedImage *animatedImage) { 95 | self.imageView2.animatedImage = animatedImage; 96 | 97 | // Set up debug UI for image 2 98 | #if defined(DEBUG) && DEBUG 99 | self.imageView2.debug_delegate = self.debugView2; 100 | animatedImage.debug_delegate = self.debugView2; 101 | #endif 102 | self.debugView2.imageView = self.imageView2; 103 | self.debugView2.image = animatedImage; 104 | self.imageView2.userInteractionEnabled = YES; 105 | }]; 106 | 107 | // 3 108 | if (!self.imageView3) { 109 | self.imageView3 = [[FLAnimatedImageView alloc] init]; 110 | self.imageView3.contentMode = UIViewContentModeScaleAspectFill; 111 | self.imageView3.clipsToBounds = YES; 112 | } 113 | [self.view addSubview:self.imageView3]; 114 | self.imageView3.frame = CGRectMake(389.0, 577.0, 379.0, 447.0); 115 | 116 | NSURL *url3 = [NSURL URLWithString:@"https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif"]; 117 | [self loadAnimatedImageWithURL:url3 completion:^(FLAnimatedImage *animatedImage) { 118 | self.imageView3.animatedImage = animatedImage; 119 | 120 | // Set up debug UI for image 3 121 | #if defined(DEBUG) && DEBUG 122 | self.imageView3.debug_delegate = self.debugView3; 123 | animatedImage.debug_delegate = self.debugView3; 124 | #endif 125 | self.debugView3.imageView = self.imageView3; 126 | self.debugView3.image = animatedImage; 127 | self.imageView3.userInteractionEnabled = YES; 128 | }]; 129 | 130 | // ... that's it! 131 | 132 | 133 | 134 | // Setting the delegates is for the debug UI in this demo only and is usually not needed. 135 | #if defined(DEBUG) && DEBUG 136 | self.imageView1.debug_delegate = self.debugView1; 137 | animatedImage1.debug_delegate = self.debugView1; 138 | #endif 139 | self.debugView1.imageView = self.imageView1; 140 | self.debugView1.image = animatedImage1; 141 | self.imageView1.userInteractionEnabled = YES; 142 | } 143 | 144 | 145 | #pragma mark - 146 | 147 | - (UILabel *)titleLabel 148 | { 149 | if (!_titleLabel) { 150 | _titleLabel = [[UILabel alloc] init]; 151 | _titleLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:31.0]; 152 | _titleLabel.textColor = [UIColor colorWithWhite:0.05 alpha:1.0]; 153 | _titleLabel.text = @"FLAnimatedImage Demo Player"; 154 | [_titleLabel sizeToFit]; 155 | } 156 | _titleLabel.backgroundColor = self.view.backgroundColor; 157 | [self.view addSubview:_titleLabel]; 158 | 159 | return _titleLabel; 160 | } 161 | 162 | 163 | - (UILabel *)subtitleLabel 164 | { 165 | if (!_subtitleLabel) { 166 | _subtitleLabel = [[UILabel alloc] init]; 167 | _subtitleLabel.font = [UIFont systemFontOfSize:17.0]; 168 | _subtitleLabel.textColor = [UIColor colorWithWhite:0.05 alpha:1.0]; 169 | _subtitleLabel.text = @"Cache sizes are optimized individually for each image."; 170 | [_subtitleLabel sizeToFit]; 171 | } 172 | _subtitleLabel.backgroundColor = self.view.backgroundColor; 173 | [self.view addSubview:_subtitleLabel]; 174 | 175 | return _subtitleLabel; 176 | } 177 | 178 | 179 | - (UIButton *)memoryWarningButton 180 | { 181 | if (!_memoryWarningButton) { 182 | _memoryWarningButton = [UIButton buttonWithType:UIButtonTypeSystem]; 183 | _memoryWarningButton.titleLabel.font = [UIFont systemFontOfSize:17.0]; 184 | _memoryWarningButton.tintColor = [UIColor colorWithRed:0.8 green:0.15 blue:0.15 alpha:1.0]; 185 | [_memoryWarningButton setTitle:@"Simulate Memory Warning" forState:UIControlStateNormal]; 186 | #pragma clang diagnostic push 187 | #pragma clang diagnostic ignored "-Wundeclared-selector" 188 | [_memoryWarningButton addTarget:[UIApplication sharedApplication] action:@selector(_performMemoryWarning) forControlEvents:UIControlEventTouchUpInside]; 189 | #pragma clang diagnostic pop 190 | [_memoryWarningButton sizeToFit]; 191 | } 192 | [self.view addSubview:_memoryWarningButton]; 193 | 194 | return _memoryWarningButton; 195 | } 196 | 197 | 198 | - (DebugView *)debugView1 199 | { 200 | if (!_debugView1) { 201 | _debugView1 = [[DebugView alloc] init]; 202 | } 203 | [self.imageView1 addSubview:_debugView1]; 204 | _debugView1.frame = self.imageView1.bounds; 205 | 206 | return _debugView1; 207 | } 208 | 209 | 210 | - (DebugView *)debugView2 211 | { 212 | if (!_debugView2) { 213 | _debugView2 = [[DebugView alloc] init]; 214 | _debugView2.style = DebugViewStyleCondensed; 215 | } 216 | [self.imageView2 addSubview:_debugView2]; 217 | _debugView2.frame = self.imageView2.bounds; 218 | 219 | return _debugView2; 220 | } 221 | 222 | 223 | - (DebugView *)debugView3 224 | { 225 | if (!_debugView3) { 226 | _debugView3 = [[DebugView alloc] init]; 227 | _debugView3.style = DebugViewStyleCondensed; 228 | } 229 | [self.imageView3 addSubview:_debugView3]; 230 | _debugView3.frame = self.imageView3.bounds; 231 | 232 | return _debugView3; 233 | } 234 | 235 | /// Even though NSURLCache *may* cache the results for remote images, it doesn't guarantee it. 236 | /// Cache control headers or internal parts of NSURLCache's implementation may cause these images to become uncache. 237 | /// Here we enfore strict disk caching so we're sure the images stay around. 238 | - (void)loadAnimatedImageWithURL:(NSURL *const)url completion:(void (^)(FLAnimatedImage *animatedImage))completion 239 | { 240 | NSString *const filename = url.lastPathComponent; 241 | NSString *const diskPath = [NSHomeDirectory() stringByAppendingPathComponent:filename]; 242 | 243 | NSData * __block animatedImageData = [[NSFileManager defaultManager] contentsAtPath:diskPath]; 244 | FLAnimatedImage * __block animatedImage = [[FLAnimatedImage alloc] initWithAnimatedGIFData:animatedImageData]; 245 | 246 | if (animatedImage) { 247 | if (completion) { 248 | completion(animatedImage); 249 | } 250 | } else { 251 | [[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 252 | animatedImageData = data; 253 | animatedImage = [[FLAnimatedImage alloc] initWithAnimatedGIFData:animatedImageData]; 254 | if (animatedImage) { 255 | if (completion) { 256 | dispatch_async(dispatch_get_main_queue(), ^{ 257 | completion(animatedImage); 258 | }); 259 | } 260 | [data writeToFile:diskPath atomically:YES]; 261 | } 262 | }] resume]; 263 | } 264 | } 265 | 266 | 267 | @end 268 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // FLAnimatedImageDemo 4 | // 5 | // Created by Raphael Schaad on 4/1/14. 6 | // Copyright (c) Flipboard. All rights reserved. 7 | // 8 | 9 | 10 | #import "AppDelegate.h" 11 | #import 12 | 13 | 14 | int main(int argc, char *argv[]) 15 | { 16 | @autoreleasepool { 17 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /FLAnimatedImageDemo/test-gifs/rock.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flipboard/FLAnimatedImage/d4f07b6f164d53c1212c3e54d6460738b1981e9f/FLAnimatedImageDemo/test-gifs/rock.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 Flipboard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "FLAnimatedImage", 6 | platforms: [ 7 | .iOS(.v9) 8 | ], 9 | products: [ 10 | .library(name: "FLAnimatedImage", targets: ["FLAnimatedImage"]), 11 | ], 12 | targets: [ 13 | .target( 14 | name: "FLAnimatedImage", 15 | path: "FLAnimatedImage", 16 | exclude: [ "Info.plist" ], 17 | sources: [ "FLAnimatedImageView.m", "FLAnimatedImage.m" ], 18 | publicHeadersPath: "include", 19 | cSettings: [ 20 | .headerSearchPath("include") 21 | ] 22 | ) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Flipboard/FLAnimatedImage/blob/master/LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/Flipboard/FLAnimatedImage/pulls) 2 | 3 | FLAnimatedImage is a performant animated GIF engine for iOS: 4 | 5 | - Plays multiple GIFs simultaneously with a playback speed comparable to desktop browsers 6 | - Honors variable frame delays 7 | - Behaves gracefully under memory pressure 8 | - Eliminates delays or blocking during the first playback loop 9 | - Interprets the frame delays of fast GIFs the same way modern browsers do 10 | 11 | It's a well-tested [component that powers all GIFs in Flipboard](http://engineering.flipboard.com/2014/05/animated-gif). To understand its behavior it comes with an interactive demo: 12 | 13 | ![Flipboard playing multiple GIFs](https://github.com/Flipboard/FLAnimatedImage/raw/master/images/flanimatedimage-demo-player.gif) 14 | 15 | ## Who is this for? 16 | 17 | - Apps that don't support animated GIFs yet 18 | - Apps that already support animated GIFs but want a higher performance solution 19 | - People who want to tinker with the code ([the corresponding blog post](http://engineering.flipboard.com/2014/05/animated-gif/) is a great place to start; also see the *To Do* section below) 20 | 21 | ## Installation & Usage 22 | 23 | FLAnimatedImage is a well-encapsulated drop-in component. Simply replace your `UIImageView` instances with instances of `FLAnimatedImageView` to get animated GIF support. There is no central cache or state to manage. 24 | 25 | If using CocoaPods, the quickest way to try it out is to type this on the command line: 26 | 27 | ```shell 28 | $ pod try FLAnimatedImage 29 | ``` 30 | 31 | To add it to your app, copy the two classes `FLAnimatedImage.h/.m` and `FLAnimatedImageView.h/.m` into your Xcode project or add via [CocoaPods](http://cocoapods.org) by adding this to your Podfile: 32 | 33 | ```ruby 34 | pod 'FLAnimatedImage', '~> 1.0' 35 | ``` 36 | 37 | If using [Carthage](https://github.com/Carthage/Carthage), add the following line into your `Cartfile` 38 | 39 | ``` 40 | github "Flipboard/FLAnimatedImage" 41 | ``` 42 | 43 | If using [Swift Package Manager](https://github.com/apple/swift-package-manager), add the following to your `Package.swift` or add via XCode: 44 | 45 | ```swift 46 | dependencies: [ 47 | .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", .upToNextMajor(from: "1.0.16")) 48 | ], 49 | targets: [ 50 | .target(name: "TestProject", dependencies: ["FLAnimatedImage""]) 51 | ] 52 | ``` 53 | 54 | In your code, `#import "FLAnimatedImage.h"`, create an image from an animated GIF, and setup the image view to display it: 55 | 56 | ```objective-c 57 | FLAnimatedImage *image = [FLAnimatedImage animatedImageWithGIFData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif"]]]; 58 | FLAnimatedImageView *imageView = [[FLAnimatedImageView alloc] init]; 59 | imageView.animatedImage = image; 60 | imageView.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); 61 | [self.view addSubview:imageView]; 62 | ``` 63 | 64 | It's flexible to integrate in your custom image loading stack and backwards compatible to iOS 9. 65 | 66 | It uses ARC and the Apple frameworks `QuartzCore`, `ImageIO`, `MobileCoreServices`, and `CoreGraphics`. 67 | 68 | It is capable of fine-grained logging. A block can be set on `FLAnimatedImage` that's invoked when logging occurs with various log levels via the `+setLogBlock:logLevel:` method. For example: 69 | 70 | ```objective-c 71 | // Set up FLAnimatedImage logging. 72 | [FLAnimatedImage setLogBlock:^(NSString *logString, FLLogLevel logLevel) { 73 | // Using NSLog 74 | NSLog(@"%@", logString); 75 | 76 | // ...or CocoaLumberjackLogger only logging warnings and errors 77 | if (logLevel == FLLogLevelError) { 78 | DDLogError(@"%@", logString); 79 | } else if (logLevel == FLLogLevelWarn) { 80 | DDLogWarn(@"%@", logString); 81 | } 82 | } logLevel:FLLogLevelWarn]; 83 | ``` 84 | 85 | Since FLAnimatedImage is licensed under MIT, it's compatible with the terms of using it for any app on the App Store. 86 | 87 | ## Release process 88 | 1. Bump version in `FLAnimatedImage.podspec`, update CHANGES, and commit. 89 | 2. Tag commit with `> git tag -a -m ""` and `> git push --tags`. 90 | 3. [Submit Podspec to Trunk with](https://guides.cocoapods.org/making/specs-and-specs-repo.html#how-do-i-update-an-existing-pod) `> pod trunk push FLAnimatedImage.podspec` ([ensure you're auth'ed](https://guides.cocoapods.org/making/getting-setup-with-trunk.html#getting-started)). 91 | ## To Do 92 | - Support other animated image formats such as APNG or WebP (WebP support implemented [here](https://github.com/Flipboard/FLAnimatedImage/pull/86)) 93 | - Integration into network libraries and image caches 94 | - Investigate whether `FLAnimatedImage` should become a `UIImage` subclass 95 | - Smarter buffering 96 | - Bring demo app to iPhone 97 | 98 | This code has successfully shipped to many people as is, but please do come with your questions, issues and pull requests! 99 | 100 | ## Select apps using FLAnimatedImage 101 | (alphabetically) 102 | 103 | - [Close-up](http://closeu.pe) 104 | - [Design Shots](https://itunes.apple.com/app/id792517951) 105 | - [Dropbox](https://www.dropbox.com) 106 | - [Dumpert](http://dumpert.nl) 107 | - [Ello](https://ello.co/) 108 | - [Facebook](https://facebook.com) 109 | - [Flipboard](https://flipboard.com) 110 | - [getGIF](https://itunes.apple.com/app/id964784701) 111 | - [Gifalicious](https://itunes.apple.com/us/app/gifalicious-see-your-gifs/id965346708?mt=8) 112 | - [HashPhotos](https://itunes.apple.com/app/id685784609) 113 | - [Instagram](https://www.instagram.com/) 114 | - [LiveBooth](http://www.liveboothapp.com) 115 | - [lWlVl Festival](http://lwlvl.com) 116 | - [Medium](https://medium.com) 117 | - [Pinterest](https://pinterest.com) 118 | - [Slack](https://slack.com/) 119 | - [Telegram](https://telegram.org/) 120 | - [Zip Code Finder](https://itunes.apple.com/app/id893031254) 121 | 122 | If you're using FLAnimatedImage in your app, please open a PR to add it to this list! 123 | -------------------------------------------------------------------------------- /images/flanimatedimage-demo-player.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flipboard/FLAnimatedImage/d4f07b6f164d53c1212c3e54d6460738b1981e9f/images/flanimatedimage-demo-player.gif --------------------------------------------------------------------------------