Fix borked frame preloading
This commit is contained in:
parent
47ef2e2008
commit
aa2e6a11c5
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
},
|
||||||
|
"data" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "earth.gif"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 13 MiB |
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="8191" systemVersion="14F27" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="vXZ-lx-hvc">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10116" systemVersion="15E65" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="vXZ-lx-hvc">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8154"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<scenes>
|
<scenes>
|
||||||
<!--Gifu-->
|
<!--Gifu-->
|
||||||
|
|
|
@ -3,58 +3,70 @@ import ImageIO
|
||||||
@testable import Gifu
|
@testable import Gifu
|
||||||
|
|
||||||
private let imageData = testImageDataNamed("mugen.gif")
|
private let imageData = testImageDataNamed("mugen.gif")
|
||||||
private let staticImage = UIImage(data: imageData!)
|
private let staticImage = UIImage(data: imageData)!
|
||||||
|
private let preloadFrameCount = 20
|
||||||
|
|
||||||
class GifuTests: XCTestCase {
|
class GifuTests: XCTestCase {
|
||||||
var animator: Animator?
|
var animator: Animator!
|
||||||
var originalFrameCount: Int?
|
var originalFrameCount: Int!
|
||||||
var preloadedFrameCount: Int?
|
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
animator = Animator(data: imageData!, size: CGSizeZero, contentMode: .ScaleToFill, framePreloadCount: 20)
|
animator = Animator(data: imageData, size: CGSizeZero, contentMode: .ScaleToFill, framePreloadCount: preloadFrameCount)
|
||||||
animator!.prepareFrames()
|
originalFrameCount = Int(CGImageSourceGetCount(animator.imageSource))
|
||||||
originalFrameCount = Int(CGImageSourceGetCount(animator!.imageSource))
|
|
||||||
preloadedFrameCount = animator!.animatedFrames.count
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tearDown() {
|
|
||||||
super.tearDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testIsAnimatable() {
|
func testIsAnimatable() {
|
||||||
XCTAssertTrue(animator!.isAnimatable)
|
XCTAssertTrue(animator.isAnimatable)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFramePreload() {
|
func testFramePreload() {
|
||||||
XCTAssertLessThanOrEqual(preloadedFrameCount!, originalFrameCount!)
|
let expectation = expectationWithDescription("frameDuration")
|
||||||
|
|
||||||
|
animator.prepareFrames {
|
||||||
|
let animatedFrameCount = self.animator.animatedFrames.count
|
||||||
|
XCTAssertEqual(animatedFrameCount, self.originalFrameCount)
|
||||||
|
XCTAssertNotNil(self.animator.frameAtIndex(preloadFrameCount - 1))
|
||||||
|
XCTAssertNil(self.animator.frameAtIndex(preloadFrameCount + 1)?.images)
|
||||||
|
XCTAssertEqual(self.animator.currentFrameIndex, 0)
|
||||||
|
|
||||||
|
self.animator.shouldChangeFrame(1.0) { hasNewFrame in
|
||||||
|
XCTAssertTrue(hasNewFrame)
|
||||||
|
XCTAssertEqual(self.animator.currentFrameIndex, 1)
|
||||||
|
expectation.fulfill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForExpectationsWithTimeout(1.0) { error in
|
||||||
|
if let error = error {
|
||||||
|
print("Error: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFrameAtIndex() {
|
func testFrameInfo() {
|
||||||
XCTAssertNotNil(animator!.frameAtIndex(preloadedFrameCount! - 1))
|
let expectation = expectationWithDescription("testFrameInfoIsAccurate")
|
||||||
}
|
|
||||||
|
|
||||||
func testFrameDurationPrecision() {
|
animator.prepareFrames {
|
||||||
let image = animator!.frameAtIndex(5)
|
let frameDuration = self.animator.frameAtIndex(5)?.duration ?? 0
|
||||||
XCTAssertTrue((image!.duration - 0.05) < 0.00001)
|
XCTAssertTrue((frameDuration - 0.05) < 0.00001)
|
||||||
}
|
|
||||||
|
|
||||||
func testFrameSize() {
|
let imageSize = self.animator.frameAtIndex(5)?.size ?? CGSizeZero
|
||||||
let image = animator!.frameAtIndex(5)
|
XCTAssertEqual(imageSize, staticImage.size)
|
||||||
XCTAssertEqual(image!.size, staticImage!.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPrepareFramesPerformance() {
|
expectation.fulfill()
|
||||||
let tempAnimator = Animator(data: imageData!, size: CGSizeZero, contentMode: .ScaleToFill, framePreloadCount: 50)
|
}
|
||||||
|
|
||||||
self.measureBlock() {
|
waitForExpectationsWithTimeout(1.0) { error in
|
||||||
tempAnimator.prepareFrames()
|
if let error = error {
|
||||||
|
print("Error: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func testImageDataNamed(name: String) -> NSData? {
|
private func testImageDataNamed(name: String) -> NSData {
|
||||||
let testBundle = NSBundle(forClass: GifuTests.self)
|
let testBundle = NSBundle(forClass: GifuTests.self)
|
||||||
let imagePath = testBundle.bundleURL.URLByAppendingPathComponent(name)
|
let imagePath = testBundle.bundleURL.URLByAppendingPathComponent(name)
|
||||||
return NSData(contentsOfURL: imagePath)
|
return NSData(contentsOfURL: imagePath)!
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ public class AnimatableImageView: UIImageView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func onScreenUpdate() {
|
@objc func onScreenUpdate() {
|
||||||
target?.updateFrame()
|
target?.updateFrameIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ public class AnimatableImageView: UIImageView {
|
||||||
|
|
||||||
/// Updates the `image` property of the image view if necessary. This method should not be called manually.
|
/// Updates the `image` property of the image view if necessary. This method should not be called manually.
|
||||||
override public func displayLayer(layer: CALayer) {
|
override public func displayLayer(layer: CALayer) {
|
||||||
image = animator?.currentFrame
|
image = animator?.currentFrameImage ?? image
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts the image view animation.
|
/// Starts the image view animation.
|
||||||
|
@ -100,16 +100,17 @@ public class AnimatableImageView: UIImageView {
|
||||||
displayLink.paused = true
|
displayLink.paused = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset the image view values
|
/// Reset the image view values.
|
||||||
public func prepareForReuse() {
|
public func prepareForReuse() {
|
||||||
stopAnimatingGIF()
|
stopAnimatingGIF()
|
||||||
animator = nil
|
animator = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the current frame with the displayLink duration
|
/// Update the current frame if needed.
|
||||||
func updateFrame() {
|
func updateFrameIfNeeded() {
|
||||||
if animator?.updateCurrentFrame(displayLink.duration) ?? false {
|
guard let animator = animator else { return }
|
||||||
layer.setNeedsDisplay()
|
animator.shouldChangeFrame(displayLink.duration) { hasNewFrame in
|
||||||
|
if hasNewFrame { self.layer.setNeedsDisplay() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,27 @@
|
||||||
/// Keeps a reference to an `UIImage` instance and its duration as a GIF frame.
|
/// Keeps a reference to an `UIImage` instance and its duration as a GIF frame.
|
||||||
struct AnimatedFrame {
|
struct AnimatedFrame {
|
||||||
|
/// The image that should be used for this frame.
|
||||||
let image: UIImage?
|
let image: UIImage?
|
||||||
|
/// The duration that the frame image should be displayed.
|
||||||
let duration: NSTimeInterval
|
let duration: NSTimeInterval
|
||||||
|
|
||||||
static func null() -> AnimatedFrame {
|
/// A placeholder frame with no image assigned.
|
||||||
return AnimatedFrame(image: .None, duration: 0)
|
/// Used to replace frames that are no longer needed in the animation.
|
||||||
|
var placeholderFrame: AnimatedFrame {
|
||||||
|
return AnimatedFrame(image: nil, duration: duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the AnimatedFrame instance contains an image or not.
|
||||||
|
var isPlaceholder: Bool {
|
||||||
|
return image == .None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes an optional image and returns an non-placeholder `AnimatedFrame`.
|
||||||
|
///
|
||||||
|
/// - parameter image: An optional `UIImage` instance to be assigned to the new frame.
|
||||||
|
/// - returns: A non-placeholder `AnimatedFrame` instance.
|
||||||
|
func frameWithImage(image: UIImage?) -> AnimatedFrame {
|
||||||
|
return AnimatedFrame(image: image, duration: duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,26 +12,45 @@ class Animator {
|
||||||
/// The content mode to use when resizing
|
/// The content mode to use when resizing
|
||||||
let contentMode: UIViewContentMode
|
let contentMode: UIViewContentMode
|
||||||
/// Maximum number of frames to load at once
|
/// Maximum number of frames to load at once
|
||||||
let maxFrameCount: Int
|
let preloadFrameCount: Int
|
||||||
/// The total number of frames in the GIF.
|
/// The total number of frames in the GIF.
|
||||||
var frameCount = 0
|
var frameCount = 0
|
||||||
/// A reference to the original image source.
|
/// A reference to the original image source.
|
||||||
var imageSource: CGImageSourceRef
|
var imageSource: CGImageSourceRef
|
||||||
|
|
||||||
/// The index of the current GIF frame.
|
/// The index of the current GIF frame.
|
||||||
var currentFrameIndex = 0
|
var currentFrameIndex = 0 {
|
||||||
/// The index of the current GIF frame from the source.
|
didSet {
|
||||||
var currentPreloadIndex = 0
|
previousFrameIndex = oldValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The index of the previous GIF frame.
|
||||||
|
var previousFrameIndex = 0 {
|
||||||
|
didSet {
|
||||||
|
dispatch_async(preloadFrameQueue) {
|
||||||
|
self.updatePreloadedFrames()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// Time elapsed since the last frame change. Used to determine when the frame should be updated.
|
/// Time elapsed since the last frame change. Used to determine when the frame should be updated.
|
||||||
var timeSinceLastFrameChange: NSTimeInterval = 0.0
|
var timeSinceLastFrameChange: NSTimeInterval = 0.0
|
||||||
/// Specifies whether GIF frames should be pre-scaled.
|
/// Specifies whether GIF frames should be pre-scaled.
|
||||||
/// - seealso: `needsPrescaling` in AnimatableImageView.
|
/// - seealso: `needsPrescaling` in AnimatableImageView.
|
||||||
var needsPrescaling = true
|
var needsPrescaling = true
|
||||||
|
/// Dispatch queue used for preloading images.
|
||||||
|
private lazy var preloadFrameQueue = dispatch_queue_create("co.kaishin.Gifu.preloadQueue", DISPATCH_QUEUE_SERIAL)
|
||||||
|
|
||||||
/// The current image frame to show.
|
/// The current image frame to show.
|
||||||
var currentFrame: UIImage? {
|
var currentFrameImage: UIImage? {
|
||||||
return frameAtIndex(currentFrameIndex)
|
return frameAtIndex(currentFrameIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The current frame duration
|
||||||
|
var currentFrameDuration: NSTimeInterval {
|
||||||
|
return durationAtIndex(currentFrameIndex)
|
||||||
|
}
|
||||||
|
|
||||||
/// Is this image animatable?
|
/// Is this image animatable?
|
||||||
var isAnimatable: Bool {
|
var isAnimatable: Bool {
|
||||||
return imageSource.isAnimatedGIF
|
return imageSource.isAnimatedGIF
|
||||||
|
@ -46,34 +65,70 @@ class Animator {
|
||||||
self.imageSource = CGImageSourceCreateWithData(data, options) ?? CGImageSourceCreateIncremental(options)
|
self.imageSource = CGImageSourceCreateWithData(data, options) ?? CGImageSourceCreateIncremental(options)
|
||||||
self.size = size
|
self.size = size
|
||||||
self.contentMode = contentMode
|
self.contentMode = contentMode
|
||||||
self.maxFrameCount = framePreloadCount
|
self.preloadFrameCount = framePreloadCount
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Frames
|
// MARK: - Frames
|
||||||
/// Loads the frames from an image source, resizes them, then caches them in `animatedFrames`.
|
/// Loads the frames from an image source, resizes them, then caches them in `animatedFrames`.
|
||||||
func prepareFrames() {
|
func prepareFrames(completionHandler: (Void -> Void)? = .None) {
|
||||||
frameCount = Int(CGImageSourceGetCount(imageSource))
|
frameCount = Int(CGImageSourceGetCount(imageSource))
|
||||||
let framesToProcess = min(frameCount, maxFrameCount)
|
animatedFrames.reserveCapacity(frameCount)
|
||||||
animatedFrames.reserveCapacity(framesToProcess)
|
dispatch_async(preloadFrameQueue) {
|
||||||
animatedFrames = (0..<framesToProcess).reduce([]) { $0 + [prepareFrame($1)] }
|
self.setupAnimatedFrames()
|
||||||
currentPreloadIndex = framesToProcess
|
if let handler = completionHandler { handler() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads a single frame from an image source, resizes it, then returns an `AnimatedFrame`.
|
/// Returns the frame at a particular index.
|
||||||
///
|
///
|
||||||
/// - parameter index: The index of the GIF image source to prepare
|
/// - parameter index: The index of the frame.
|
||||||
/// - returns: An AnimatedFrame object
|
/// - returns: An optional image at a given frame.
|
||||||
func prepareFrame(index: Int) -> AnimatedFrame {
|
func frameAtIndex(index: Int) -> UIImage? {
|
||||||
guard let frameImageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil) else {
|
return animatedFrames[safe: index]?.image
|
||||||
return AnimatedFrame.null()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let frameDuration = CGImageSourceGIFFrameDuration(imageSource, index: index)
|
/// Returns the duration at a particular index.
|
||||||
let image = UIImage(CGImage: frameImageRef)
|
///
|
||||||
|
/// - parameter index: The index of the duration.
|
||||||
|
/// - returns: The duration of the given frame.
|
||||||
|
func durationAtIndex(index: Int) -> NSTimeInterval {
|
||||||
|
return animatedFrames[index].duration
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether the frame should be changed and calls a handler with the results.
|
||||||
|
///
|
||||||
|
/// - parameter duration: A `CFTimeInterval` value that will be used to determine whether frame should be changed.
|
||||||
|
/// - parameter handler: A function that takes a `Bool` and returns nothing. It will be called with the frame change result.
|
||||||
|
func shouldChangeFrame(duration: CFTimeInterval, handler: Bool -> Void) {
|
||||||
|
incrementTimeSinceLastFrameChangeWithDuration(duration)
|
||||||
|
|
||||||
|
if currentFrameDuration > timeSinceLastFrameChange {
|
||||||
|
handler(false)
|
||||||
|
} else {
|
||||||
|
resetTimeSinceLastFrameChange()
|
||||||
|
incrementCurrentFrameIndex()
|
||||||
|
handler(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Animator {
|
||||||
|
/// Whether preloading is needed or not.
|
||||||
|
var preloadingIsNeeded: Bool {
|
||||||
|
return preloadFrameCount < frameCount - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optionally loads a single frame from an image source, resizes it if requierd, then returns an `UIImage`.
|
||||||
|
///
|
||||||
|
/// - parameter index: The index of the frame to load.
|
||||||
|
/// - returns: An optional `UIImage` instance.
|
||||||
|
func loadFrameAtIndex(index: Int) -> UIImage? {
|
||||||
|
guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, index, nil) else { return .None }
|
||||||
|
let image = UIImage(CGImage: imageRef)
|
||||||
let scaledImage: UIImage?
|
let scaledImage: UIImage?
|
||||||
|
|
||||||
if needsPrescaling == true {
|
if needsPrescaling {
|
||||||
switch contentMode {
|
switch self.contentMode {
|
||||||
case .ScaleAspectFit: scaledImage = image.resizeAspectFit(size)
|
case .ScaleAspectFit: scaledImage = image.resizeAspectFit(size)
|
||||||
case .ScaleAspectFill: scaledImage = image.resizeAspectFill(size)
|
case .ScaleAspectFill: scaledImage = image.resizeAspectFill(size)
|
||||||
default: scaledImage = image.resize(size)
|
default: scaledImage = image.resize(size)
|
||||||
|
@ -82,35 +137,77 @@ class Animator {
|
||||||
scaledImage = image
|
scaledImage = image
|
||||||
}
|
}
|
||||||
|
|
||||||
return AnimatedFrame(image: scaledImage, duration: frameDuration)
|
return scaledImage
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the frame at a particular index.
|
/// Updates the frames by preloading new ones and replacing the previous frame with a placeholder.
|
||||||
///
|
func updatePreloadedFrames() {
|
||||||
/// - parameter index: The index of the frame.
|
if !preloadingIsNeeded { return }
|
||||||
/// - returns: An optional image at a given frame.
|
animatedFrames[previousFrameIndex] = animatedFrames[previousFrameIndex].placeholderFrame
|
||||||
func frameAtIndex(index: Int) -> UIImage? {
|
|
||||||
return animatedFrames[index].image
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the current frame if necessary using the frame timer and the duration of each frame in `animatedFrames`.
|
preloadIndexesWithStartingIndex(currentFrameIndex).forEach { index in
|
||||||
///
|
let currentAnimatedFrame = animatedFrames[index]
|
||||||
/// - returns: An optional image at a given frame.
|
if !currentAnimatedFrame.isPlaceholder { return }
|
||||||
func updateCurrentFrame(duration: CFTimeInterval) -> Bool {
|
animatedFrames[index] = currentAnimatedFrame.frameWithImage(loadFrameAtIndex(index))
|
||||||
timeSinceLastFrameChange += min(maxTimeStep, duration)
|
|
||||||
guard let frameDuration = animatedFrames[safe:currentFrameIndex]?.duration where
|
|
||||||
frameDuration <= timeSinceLastFrameChange else { return false }
|
|
||||||
|
|
||||||
timeSinceLastFrameChange -= frameDuration
|
|
||||||
let lastFrameIndex = currentFrameIndex
|
|
||||||
currentFrameIndex = (currentFrameIndex + 1) % animatedFrames.count
|
|
||||||
|
|
||||||
// Loads the next needed frame for progressive loading
|
|
||||||
if animatedFrames.count < frameCount {
|
|
||||||
animatedFrames[lastFrameIndex] = prepareFrame(currentPreloadIndex)
|
|
||||||
currentPreloadIndex = (currentFrameIndex + 1) % frameCount
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
/// Increments the `timeSinceLastFrameChange` property with a given duration.
|
||||||
|
///
|
||||||
|
/// - parameter duration: An `NSTimeInterval` value to increment the `timeSinceLastFrameChange` property with.
|
||||||
|
func incrementTimeSinceLastFrameChangeWithDuration(duration: NSTimeInterval) {
|
||||||
|
timeSinceLastFrameChange += min(maxTimeStep, duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensures that `timeSinceLastFrameChange` remains accurate after each frame change by substracting the `currentFrameDuration`.
|
||||||
|
func resetTimeSinceLastFrameChange() {
|
||||||
|
timeSinceLastFrameChange -= currentFrameDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increments the `currentFrameIndex` property.
|
||||||
|
func incrementCurrentFrameIndex() {
|
||||||
|
currentFrameIndex = incrementFrameIndex(currentFrameIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increments a given frame index, taking into account the `frameCount` and looping when necessary.
|
||||||
|
///
|
||||||
|
/// - parameter index: The `Int` value to increment.
|
||||||
|
/// - parameter byValue: The `Int` value to increment with.
|
||||||
|
/// - returns: A new `Int` value.
|
||||||
|
func incrementFrameIndex(index: Int, byValue value: Int = 1) -> Int {
|
||||||
|
return (index + value) % frameCount
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the indexes of the frames to preload based on a starting frame index.
|
||||||
|
///
|
||||||
|
/// - parameter index: Starting index.
|
||||||
|
/// - returns: An array of indexes to preload.
|
||||||
|
func preloadIndexesWithStartingIndex(index: Int) -> [Int] {
|
||||||
|
let nextIndex = incrementFrameIndex(index)
|
||||||
|
let lastIndex = incrementFrameIndex(index, byValue: preloadFrameCount)
|
||||||
|
|
||||||
|
if lastIndex >= nextIndex {
|
||||||
|
return [Int](nextIndex...lastIndex)
|
||||||
|
} else {
|
||||||
|
return [Int](nextIndex..<frameCount) + [Int](0...lastIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set up animated frames after resetting them if necessary.
|
||||||
|
func setupAnimatedFrames() {
|
||||||
|
resetAnimatedFrames()
|
||||||
|
|
||||||
|
(0..<frameCount).forEach { index in
|
||||||
|
let frameDuration = CGImageSourceGIFFrameDuration(imageSource, index: index)
|
||||||
|
animatedFrames += [AnimatedFrame(image: .None, duration: frameDuration)]
|
||||||
|
|
||||||
|
if index > preloadFrameCount { return }
|
||||||
|
animatedFrames[index] = animatedFrames[index].frameWithImage(loadFrameAtIndex(index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset animated frames.
|
||||||
|
func resetAnimatedFrames() {
|
||||||
|
animatedFrames = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>91</string>
|
<string>94</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string></string>
|
<string></string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|
Loading…
Reference in New Issue