YYAnimatedImageView 源碼淺析

YYAnimatedImageViewUIImageView的子類,如果imagehighlightedImage屬性繼承YYAnimatedImage協(xié)議, 則可以顯示多幀動(dòng)畫圖像,可以使startAnimationstopAnimation控制動(dòng)畫.YYAnimatedImageView會(huì)緩存一些幀,來減小CPU消耗.緩存的大小會(huì)根據(jù)可用的內(nèi)存改變.

YYAnimatedImageView接口

@interface YYAnimatedImageView : UIImageView

//如果圖片數(shù)據(jù)有多幀,設(shè)置為`YES`時(shí),當(dāng)view 可見/消失時(shí)會(huì)自動(dòng)播放/停止動(dòng)畫
@property (nonatomic) BOOL autoPlayAnimatedImage;

//當(dāng)前顯示的幀( 0 based),設(shè)置它會(huì)立即顯示某一幀,支持KVO
@property (nonatomic) NSUInteger currentAnimatedImageIndex;

//是否是播放狀態(tài),支持KVO
@property (nonatomic, readonly) BOOL currentIsPlayingAnimation;

//動(dòng)畫timer的運(yùn)行模式,默認(rèn)NSRunLoopCommonModes
@property (nonatomic, copy) NSString *runloopMode;

//緩存的最大限制,設(shè)置為0會(huì)根據(jù)系統(tǒng)可用內(nèi)存動(dòng)態(tài)計(jì)算
@property (nonatomic) NSUInteger maxBufferSize;

@end

YYAnimatedImage協(xié)議

@protocol YYAnimatedImage <NSObject>
@required
//動(dòng)畫的總幀數(shù),小于1則其他的方法將被忽略
- (NSUInteger)animatedImageFrameCount;

//動(dòng)畫循環(huán)次數(shù),0是無限循環(huán)
- (NSUInteger)animatedImageLoopCount;

//每幀圖片的大小,用來決定緩存buffer大小
- (NSUInteger)animatedImageBytesPerFrame;

//使用索引獲取某一幀,可能會(huì)在后臺(tái)線程調(diào)用
- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index;

//某一幀的動(dòng)畫時(shí)長
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;

@optional
//圖片坐標(biāo)系的子區(qū)域,用來顯示 sprite animation
- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index;
@end

設(shè)置image

- (void)setImage:(UIImage *)image {
    //與當(dāng)前圖片相同,不做任何操作直接返回
    if (self.image == image) return;
    //調(diào)用私有方法
    [self setImage:image withType:YYAnimatedImageTypeImage];
}

//私有設(shè)置圖片方法
- (void)setImage:(id)image withType:(YYAnimatedImageType)type {
    //停止動(dòng)畫
    [self stopAnimating];
    //重新設(shè)置動(dòng)畫
    if (_link) [self resetAnimated];
    //清空當(dāng)前幀數(shù)據(jù)
    _curFrame = nil;
    //解析圖片類型調(diào)用不同的父類方法
    switch (type) {
        case YYAnimatedImageTypeNone: break;
        case YYAnimatedImageTypeImage: super.image = image; break;
        case YYAnimatedImageTypeHighlightedImage: super.highlightedImage = image; break;
        case YYAnimatedImageTypeImages: super.animationImages = image; break;
        case YYAnimatedImageTypeHighlightedImages: super.highlightedAnimationImages = image; break;
    }
    //調(diào)用圖片發(fā)生改變
    [self imageChanged];
}

- (void)imageChanged {
    //重新獲取真正的類型
    YYAnimatedImageType newType = [self currentImageType];
    //取出對(duì)應(yīng)類型下的圖片
    id newVisibleImage = [self imageForType:newType];
    NSUInteger newImageFrameCount = 0;
    BOOL hasContentsRect = NO;
    //獲取到的是UIImage類型,并且繼承了YYAnimatedImage 說明可能是動(dòng)圖
    if ([newVisibleImage isKindOfClass:[UIImage class]] &&
        [newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) {
        //獲取圖片幀數(shù)
        newImageFrameCount = ((UIImage<YYAnimatedImage> *) newVisibleImage).animatedImageFrameCount;
        if (newImageFrameCount > 1) {
            //是否設(shè)置了 animatedImageContentsRect
            hasContentsRect = [((UIImage<YYAnimatedImage> *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)];
        }
    }
    //沒有設(shè)置contentRect則恢復(fù)默認(rèn)設(shè)置
    if (!hasContentsRect && _curImageHasContentsRect) {
        if (!CGRectEqualToRect(self.layer.contentsRect, CGRectMake(0, 0, 1, 1)) ) {
            [CATransaction begin];
            [CATransaction setDisableActions:YES];
            self.layer.contentsRect = CGRectMake(0, 0, 1, 1);
            [CATransaction commit];
        }
    }
    _curImageHasContentsRect = hasContentsRect;
    //設(shè)置了contentRect
    if (hasContentsRect) {
        //獲取第一幀的animatedImageContentsRect
        CGRect rect = [((UIImage<YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:0];
        //設(shè)置animatedImageContentsRect
        [self setContentsRect:rect forImage:newVisibleImage];
    }
    
    //幀數(shù)大于1時(shí)
    if (newImageFrameCount > 1) {
        //重新設(shè)置動(dòng)畫
        [self resetAnimated];
        //保存當(dāng)前動(dòng)畫幀
        _curAnimatedImage = newVisibleImage;
        //保存當(dāng)前幀
        _curFrame = newVisibleImage;
        //保存循環(huán)次數(shù)
        _totalLoop = _curAnimatedImage.animatedImageLoopCount;
        //保存幀數(shù)
        _totalFrameCount = _curAnimatedImage.animatedImageFrameCount;
        //計(jì)算緩存大小
        [self calcMaxBufferCount];
    }
    //標(biāo)記重繪
    [self setNeedsDisplay];
    //更新動(dòng)畫狀態(tài)
    [self didMoved];
}

在修改image屬性后,主要要處理三件事:1.停止當(dāng)前動(dòng)畫, 2. 解析圖片是否是動(dòng)圖,更新與圖片相關(guān)數(shù)據(jù),重新計(jì)算緩存大小 3.根據(jù)設(shè)置開啟動(dòng)畫.

動(dòng)畫相關(guān)方法

- (void)stopAnimating {
    //調(diào)用super方法
    [super stopAnimating];
    //取消隊(duì)列中的所有任務(wù)
    [_requestQueue cancelAllOperations];
    //暫停CADisplayLink
    _link.paused = YES;
    //更新狀態(tài)
    self.currentIsPlayingAnimation = NO;
}

// 清空動(dòng)畫參數(shù)
- (void)resetAnimated {
    dispatch_once(&_onceToken, ^{
        //初始化鎖
        _lock = dispatch_semaphore_create(1);
        //初始化緩沖區(qū)
        _buffer = [NSMutableDictionary new];
        //初始化數(shù)據(jù)請(qǐng)求隊(duì)列
        _requestQueue = [[NSOperationQueue alloc] init];
        _requestQueue.maxConcurrentOperationCount = 1;
        //初始化timer CADisplayLink
        _link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(step:)];
        if (_runloopMode) {
            [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode];
        }
        _link.paused = YES;
        //監(jiān)聽內(nèi)存警告
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
        //監(jiān)聽程序進(jìn)入后臺(tái)
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
    });
    //數(shù)據(jù)請(qǐng)求隊(duì)列取消所有任務(wù)
    [_requestQueue cancelAllOperations];
    LOCK(
         //清空緩存
         if (_buffer.count) {
             NSMutableDictionary *holder = _buffer;
             _buffer = [NSMutableDictionary new];
             dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
                 // Capture the dictionary to global queue,
                 // release these images in background to avoid blocking UI thread.
                 [holder class];
             });
         }
    );
    //暫停CADisplaylink
    _link.paused = YES;
    _time = 0;
    //currentAnimatedImageIndex對(duì)生成KVO通知
    if (_curIndex != 0) {
        [self willChangeValueForKey:@"currentAnimatedImageIndex"];
        _curIndex = 0;
        [self didChangeValueForKey:@"currentAnimatedImageIndex"];
    }
    //清空當(dāng)前動(dòng)畫的幀
    _curAnimatedImage = nil;
    //清空當(dāng)前幀
    _curFrame = nil;
    //清空幀索引
    _curLoop = 0;
    //清空循環(huán)總數(shù)
    _totalLoop = 0;
    //設(shè)置所有幀數(shù)為1
    _totalFrameCount = 1;
    //清空循環(huán)結(jié)束標(biāo)記
    _loopEnd = NO;
    //清空緩存miss標(biāo)記
    _bufferMiss = NO;
    //清空buffer中實(shí)際的緩存?zhèn)€數(shù)
    _incrBufferCount = 0;
}

//開始動(dòng)畫方法
- (void)startAnimating {
    //獲取當(dāng)前的圖片類型
    YYAnimatedImageType type = [self currentImageType];
    //如果是多幀圖片的類型
    if (type == YYAnimatedImageTypeImages || type == YYAnimatedImageTypeHighlightedImages) {
        //獲取多幀圖片數(shù)據(jù)
        NSArray *images = [self imageForType:type];
        if (images.count > 0) {
            //調(diào)用父類開始方法
            [super startAnimating];
            //更新動(dòng)畫狀態(tài)
            self.currentIsPlayingAnimation = YES;
        }
    } else {
        //不是多幀圖片數(shù)據(jù)
        if (_curAnimatedImage && _link.paused) {
            //清空循環(huán)索引
            _curLoop = 0;
            //清空循環(huán)結(jié)束標(biāo)記
            _loopEnd = NO;
             //開始CADisplayLink
            _link.paused = NO;
            //設(shè)置動(dòng)畫播放狀態(tài)
            self.currentIsPlayingAnimation = YES;
        }
    }
}

CADisplay 刷新方法

- (void)step:(CADisplayLink *)link {
    //獲取當(dāng)前動(dòng)畫圖片
    UIImage <YYAnimatedImage> *image = _curAnimatedImage;
    NSMutableDictionary *buffer = _buffer;
    UIImage *bufferedImage = nil;
    //計(jì)算索引值
    NSUInteger nextIndex = (_curIndex + 1) % _totalFrameCount;
    BOOL bufferIsFull = NO;
    //動(dòng)畫幀為空,直接返回
    if (!image) return;
    if (_loopEnd) { // view will keep in last frame
        //循環(huán)結(jié)束則停止動(dòng)畫
        [self stopAnimating];
        return;
    }
    
    NSTimeInterval delay = 0;
    //如果之前是否緩存命中,第1幀已經(jīng)展示
    if (!_bufferMiss) {
        //當(dāng)前的時(shí)間區(qū)間
        _time += link.duration;
        //獲取當(dāng)前幀的持續(xù)時(shí)長
        delay = [image animatedImageDurationAtIndex:_curIndex];
        //當(dāng)前幀沒有顯示結(jié)束 直接return
        if (_time < delay) return;
        //在本displayLink周期內(nèi)計(jì)算本幀剩余時(shí)長
        _time -= delay;
        if (nextIndex == 0) { //是一次新的循環(huán)
            _curLoop++;
            //播放循環(huán)結(jié)束時(shí)
            if (_curLoop >= _totalLoop && _totalLoop != 0) {
                _loopEnd = YES;
                [self stopAnimating];
                [self.layer setNeedsDisplay]; //let system call `displayLayer:` before runloop sleep
                return; // stop at last frame
            }
        }
        //獲取下幀動(dòng)畫時(shí)長
        delay = [image animatedImageDurationAtIndex:nextIndex];
        //本周期內(nèi)剩余時(shí)長大于下一幀的持續(xù)時(shí)長,_time變?yōu)橄乱粠臅r(shí)長
        if (_time > delay) _time = delay; // do not jump over frame
    }
    LOCK(
         //從緩存中取圖片
         bufferedImage = buffer[@(nextIndex)];
         if (bufferedImage) {
             //當(dāng)緩存區(qū)最大個(gè)數(shù)小于幀的總數(shù)時(shí),要移除一個(gè)
             if ((int)_incrBufferCount < _totalFrameCount) {
                 [buffer removeObjectForKey:@(nextIndex)];
             }
             //生成currentAnimatedImageIndexKVO通知
             [self willChangeValueForKey:@"currentAnimatedImageIndex"];
             //更新當(dāng)前幀索引
             _curIndex = nextIndex;
             [self didChangeValueForKey:@"currentAnimatedImageIndex"];
             //更新當(dāng)前幀數(shù)據(jù)
             _curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage;
             
             if (_curImageHasContentsRect) {
                 _curContentsRect = [image animatedImageContentsRectAtIndex:_curIndex];
                 //為layer設(shè)置contentsRect
                 [self setContentsRect:_curContentsRect forImage:_curFrame];
             }
             //移動(dòng)到下一幀幀
             nextIndex = (_curIndex + 1) % _totalFrameCount;
             //標(biāo)記緩存命中
             _bufferMiss = NO;
             //緩存中是否已經(jīng)包含了所有幀
             if (buffer.count == _totalFrameCount) {
                 bufferIsFull = YES;
             }
         } else { //標(biāo)記緩存未命中
             _bufferMiss = YES;
         }
    )//LOCK
    
    //緩存命中則更新layer
    if (!_bufferMiss) {
        [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
    }
    
    //buffer沒有滿并且沒有加載任務(wù),則創(chuàng)建加載圖片任務(wù)放到請(qǐng)求隊(duì)列中執(zhí)行
    if (!bufferIsFull && _requestQueue.operationCount == 0) { // if some work not finished, wait for next opportunity
        _YYAnimatedImageViewFetchOperation *operation = [_YYAnimatedImageViewFetchOperation new];
        operation.view = self;
        operation.nextIndex = nextIndex;
        operation.curImage = image;
        [_requestQueue addOperation:operation];
    }
}

在每一次CADisplay的回調(diào)方法中,根據(jù)CADisplay迭代周期_display.duration計(jì)算時(shí)間區(qū)間_time,當(dāng)時(shí)間區(qū)間足夠顯示當(dāng)前幀(_time > 當(dāng)前幀的duration)時(shí),從緩存獲取幀,進(jìn)行layer內(nèi)容的更新,但是通過源代碼得知,如果緩存沒有圖片,需要異步等待緩存中獲取到圖片之后才能更新layer,這個(gè)時(shí)間最少是一個(gè)CADisplay.duration

layer更新內(nèi)容的方法

- (void)displayLayer:(CALayer *)layer {
    if (_curFrame) {
       //直接用_curFrame顯示
        layer.contents = (__bridge id)_curFrame.CGImage;
    }
}

圖片加載任務(wù)_YYAnimatedImageViewFetchOperation

_YYAnimatedImageViewFetchOperation 是 NSOpeation的子類,封裝了圖片子線程加載過程,主要功能時(shí)是從當(dāng)前幀索引開始向后加載_incrBufferCount個(gè)幀圖片,然后放到緩存YYAnimatedImageView->_buffer中.

- (void)main {
    __strong YYAnimatedImageView *view = _view;
    if (!view) return;
    if ([self isCancelled]) return;
    view->_incrBufferCount++;
    if (view->_incrBufferCount == 0) [view calcMaxBufferCount];
    if (view->_incrBufferCount > (NSInteger)view->_maxBufferCount) {
        view->_incrBufferCount = view->_maxBufferCount;
    }
    NSUInteger idx = _nextIndex;
    NSUInteger max = view->_incrBufferCount < 1 ? 1 : view->_incrBufferCount;
    NSUInteger total = view->_totalFrameCount;
    view = nil;
   
    for (int i = 0; i < max; i++, idx++) {
        @autoreleasepool {
            if (idx >= total) idx = 0;
            if ([self isCancelled]) break;
            __strong YYAnimatedImageView *view = _view;
            if (!view) break;
            LOCK_VIEW(BOOL miss = (view->_buffer[@(idx)] == nil));
            if (miss) {
                UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
                //子線程解碼圖片
                img = img.imageByDecoded;
                if ([self isCancelled]) break;
                //設(shè)置到緩存中
                LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]);
                //解除強(qiáng)引用
                view = nil;
            }
        }
    }
}

收到內(nèi)存警告

- (void)didReceiveMemoryWarning:(NSNotification *)notification {
    //取消所有圖片請(qǐng)求任務(wù)
    [_requestQueue cancelAllOperations];
    //后臺(tái)線程清除緩存
    [_requestQueue addOperationWithBlock: ^{
        //???
        _incrBufferCount = -60 - (int)(arc4random() % 120); // about 1~3 seconds to grow back..
        NSNumber *next = @((_curIndex + 1) % _totalFrameCount);
        LOCK(
             //保留下一幀圖片,清除緩存中的其它幀,使重新顯示時(shí)避免出現(xiàn)跳幀現(xiàn)象
             NSArray * keys = _buffer.allKeys;
             for (NSNumber * key in keys) {
                 if (![key isEqualToNumber:next]) { // keep the next frame for smoothly animation
                     [_buffer removeObjectForKey:key];
                 }
             }
        )//LOCK
    }];
}

進(jìn)入后臺(tái)時(shí)

進(jìn)入后臺(tái)時(shí)與收到內(nèi)存警告時(shí)的操作基本一致

- (void)didEnterBackground:(NSNotification *)notification {
    [_requestQueue cancelAllOperations];
    NSNumber *next = @((_curIndex + 1) % _totalFrameCount);
    LOCK(
         NSArray * keys = _buffer.allKeys;
         for (NSNumber * key in keys) {
             if (![key isEqualToNumber:next]) { // keep the next frame for smoothly animation
                 [_buffer removeObjectForKey:key];
             }
         }
     )//LOCK
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市核蘸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌律杠,老刑警劉巖栈暇,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異嗤朴,居然都是意外死亡娱仔,警方通過查閱死者的電腦和手機(jī)沐飘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來牲迫,“玉大人耐朴,你說我怎么就攤上這事《鹘Γ” “怎么了隔箍?”我有些...
    開封第一講書人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長脚乡。 經(jīng)常有香客問我蜒滩,道長,這世上最難降的妖魔是什么奶稠? 我笑而不...
    開封第一講書人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任俯艰,我火速辦了婚禮,結(jié)果婚禮上锌订,老公的妹妹穿的比我還像新娘竹握。我一直安慰自己,他們只是感情好辆飘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開白布啦辐。 她就那樣靜靜地躺著,像睡著了一般蜈项。 火紅的嫁衣襯著肌膚如雪芹关。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,573評(píng)論 1 305
  • 那天紧卒,我揣著相機(jī)與錄音侥衬,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛轴总,可吹牛的內(nèi)容都是我干的直颅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼怀樟,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼功偿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起漂佩,我...
    開封第一講書人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤脖含,失蹤者是張志新(化名)和其女友劉穎罪塔,沒想到半個(gè)月后投蝉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡征堪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年瘩缆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片佃蚜。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡庸娱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谐算,到底是詐尸還是另有隱情熟尉,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布洲脂,位于F島的核電站斤儿,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏恐锦。R本人自食惡果不足惜往果,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望一铅。 院中可真熱鬧陕贮,春花似錦、人聲如沸潘飘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卜录。三九已至戈擒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間暴凑,已是汗流浹背峦甩。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人凯傲。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓犬辰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親冰单。 傳聞我的和親對(duì)象是個(gè)殘疾皇子幌缝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,101評(píng)論 1 32
  • 1.自定義控件 a.繼承某個(gè)控件 b.重寫initWithFrame方法可以設(shè)置一些它的屬性 c.在layouts...
    圍繞的城閱讀 3,389評(píng)論 2 4
  • 在iOS實(shí)際開發(fā)中常用的動(dòng)畫無非是以下四種:UIView動(dòng)畫,核心動(dòng)畫诫欠,幀動(dòng)畫涵卵,自定義轉(zhuǎn)場動(dòng)畫。 1.UIView...
    請(qǐng)叫我周小帥閱讀 3,097評(píng)論 1 23
  • 在iOS實(shí)際開發(fā)中常用的動(dòng)畫無非是以下四種:UIView動(dòng)畫荒叼,核心動(dòng)畫轿偎,幀動(dòng)畫,自定義轉(zhuǎn)場動(dòng)畫被廓。下面我們逐個(gè)介紹坏晦。...
    4b5cb36a2ee2閱讀 355評(píng)論 0 0
  • 現(xiàn)在分析到Y(jié)YImage 首先看文件 YYImage YYFrameImage YYSpriteSheetImag...
    充滿活力的早晨閱讀 2,539評(píng)論 0 3