その後のその後

iOSエンジニア 堤 修一のブログ github.com/shu223

Core Image の遷移エフェクトを使う

Core Image のフィルタ (CIFilter) には、CICategoryTransition というカテゴリーがあり、次のような遷移(トランジション)エフェクトが用意されています。


(2015.10.5更新)

  • CIBarsSwipeTransition
  • CICopyMachineTransition
  • CIDisintegrateWithMaskTransition
  • CIDissolveTransition
  • CIFlashTransition
  • CIModTransition
  • CISwipeTransition
  • CIAccordionFoldTransition(iOS 8)
  • CIPageCurlTransition(iOS 9)
  • CIPageCurlWithShadowTransition(iOS 9)
  • CIRippleTransition(iOS 9)


次のような一風変わった遷移エフェクトを実現できます。


サンプルコード『CoreImageTransition』

Githubにサンプルをアップしてあります。iOS 9 で追加された3種類を含む、全9種類の遷移エフェクトを試すことができます。

※GPUを利用するためシミュレータではなく実機でご確認ください


以下実装手順です。

宣言と初期化処理の実装

使い方の基本的な部分は CIFilter の他のフィルタと同じなのですが、遷移エフェクトなので、遷移前と遷移後の2つの画像を用意します。

@interface TransitionView ()
{
    NSTimeInterval  base;
    CGRect imageRect;
}
@property (nonatomic, strong) CIImage *image1;
@property (nonatomic, strong) CIImage *image2;
@property (nonatomic, strong) CIFilter *transition;
@end
// 遷移前後の画像を生成
UIImage *uiImage1 = [UIImage imageNamed:@"sample1.jpg"];
UIImage *uiImage2 = [UIImage imageNamed:@"sample2.jpg"];
self.image1 = [CIImage imageWithCGImage:uiImage1.CGImage];
self.image2 = [CIImage imageWithCGImage:uiImage2.CGImage];

// マスク画像を生成
UIImage *uiMaskImage = [UIImage imageNamed:@"mask.jpg"];
CIImage *maskImage = [[CIImage alloc] initWithCGImage:uiMaskImage.CGImage];

// CIFilterオブジェクトを生成
self.transition = [CIFilter filterWithName: @"CIDisintegrateWithMaskTransition"
                             keysAndValues:
                   @"inputMaskImage", maskImage,
                   nil];

// 表示領域を示す矩形(CGRect型)
imageRect = CGRectMake(0, 0, uiImage1.size.width / 2, uiImage1.size.height / 2);

// 遷移アニメーション制御の基準となる時刻
base = [NSDate timeIntervalSinceReferenceDate];    

// 遷移アニメーションを制御するタイマー
[NSTimer scheduledTimerWithTimeInterval:1.0/30.0
                                 target:self
                               selector:@selector(onTimer:)
                               userInfo:nil
                                repeats:YES];


初期化処理では各種 CIImage オブジェクトの生成、CIFilter オブジェクトの生成と、表示領域を示す矩形、遷移アニメーション制御に必要な時刻とタイマーの生成を行います。*1


CIDisintegrateWithMaskTransition はマスクを使用するエフェクトなので、マスク画像の CIImage オブジェクトを生成し、@"inputMaskImage" キーの値に設定しています。

フィルタ処理の実装

遷移アニメーション中の各フレームで行うフィルタ処理を実装します。

- (CIImage *)imageForTransitionAtTime:(float)time
{
    // 遷移前後の画像をtimeによって切り替える
    if (fmodf(time, 2.0) < 1.0f)
    {
        [self.transition setValue:self.image1 forKey:@"inputImage"];
        [self.transition setValue:self.image2 forKey:@"inputTargetImage"];
    }
    else
    {
        [self.transition setValue:self.image2 forKey:@"inputImage"];
        [self.transition setValue:self.image1 forKey:@"inputTargetImage"];
    }

    // 遷移アニメーションの時間を指定
    [self.transition setValue:@(fmodf(time, 1.0f)) forKey:@"inputTime"];
    
    // フィルタ処理実行
    CIImage *transitionImage = [self.transition valueForKey:@"outputImage"];
        
    return transitionImage;
}


遷移前後の画像をそれぞれ @"inputImage" キーと @"inputTargetImage" キーに指定し、遷移アニメーションの時間(範囲は0〜1)を @"inputTime" キーに指定します。

アニメーション制御

初期化時に生成したタイマーのハンドラでは setNeedsDisplay をコールし、

- (void)onTimer:(NSTimer *)timer {

    [self setNeedsDisplay];
}


それにより呼ばれるようになる UIView の drawRect: で次のように処理を行います。

- (void)drawRect:(CGRect)rect {

    CGFloat time = 0.4 * ([NSDate timeIntervalSinceReferenceDate] - base);

    CIImage *image = [self imageForTransitionAtTime:time];
    UIImage *uiImage = [UIImage imageWithCIImage:image];
    
    [uiImage drawInRect:imageRect];
}


先に実装した imageForTransitionAtTime: をコールし、受け取ったフィルタ処理結果の画像を描画しています。

GLKView を用いた高速化

スムーズにアニメーションさせるには描画が追いつかないので、GLKView に描画します。(サンプルは GLKit を利用した実装になっています。また拙著にも解説を書きました)


*1:※実際にはNSTimer より CADisplayLink を使うべき