2015年7月29日水曜日

2Dアニメ機能のライブラリを試作(本編1)

2Dアニメーションのライブラリを試作した話の続きです。UIViewのサブクラスという形を選択し、いろいろなViewと組み合わせて使えることを想定して作りました。

いよいよソースコードの紹介なのですが、クラス本体の紹介の前に、データ定義を紹介します。指定する項目が多すぎて、かなり長いSwiftコードになってしまいました。

 

最初に作ったデータ定義は、たった1つの構造体で、多くの項目が含まれていました。テストコードを作るのが書きづらく感じ、すぐに分割することにしました。

まず2つに分けようと思ったのですが、上手に分ける基準が見付かりません。分割の試行錯誤を重ねているうちに、3つに分けるのが適切ではないかと思うようになりました。表示する要素(文字列や画像など)に関する情報、アニメーションの共通情報(時間や繰り返し回数など)、2Dアニメーションで指定する情報(位置や回転や透明度など)の3つです。

3つに分けたのですが、構造体名の番号はゼロから始まります。3つめが2Dアニメーションの指定情報で、2番を割り当てられるからです。また、3Dアニメーションで指定する情報の構造体を追加したとき、3番が割り当てられるのも良い点だと考えました。

 

データ定義の構造体を紹介する前に、構造体の中に含まれる項目のうち、列挙型に適したものを選んで、最初に定義しました。次のように、3つあります。

// UI部品の種類
enum ZavType: Int {
    case ZtView = 0
    case ZtImg = 1
    case ZtText = 2
}
// アニメーションの種類
enum ZavAnimate: Int {
    case Za2D = 0 // 2Dアニメ
    case Za3D = 1 // 3Dアニメ
    case ZaEtc = 2 // 特殊アニメ(画像処理など)
}
// 動作速度変化(Timing Functions)の種類
enum ZavTF: Int {
    case ZpLinear = 0
    case ZpIn = 1
    case ZpOut = 2
    case ZpInOut = 3
}

最初は、アニメーションとして動かす表示要素の種類です。汎用的に使うUIVew、画像のUIImage、文字列のStringの3つだけです。UIImageもStringも、最終的にはUIViewの形で生成するのですが、もとの形のままでライブラリへ渡せたほうが使いやすいので、このように作ってみました。

2つめのアニメーションの種類は、今のところ、3種類を考えています。2Dと3Dとその他です。その他は、画像処理を加えながらのアニメーションを考えていますが、細かな内容はまだ未定です。3種類のうち、最初は2Dだけを作りました。

3つめの動作速度変化の種類は、iOSのAPIに用意されている設定項目「Timing Functions」です。時間の変化に比例して動くLinearのほかに、動き出しがゆっくりのIn、終わりがゆっくりのOutなどがあります。この項目が指定できると、アニメーションに速度の変化が付けられて、表現力が向上します。必要だと感じて入れました。

 

準備が終わったので、いよいよデータ定義の構造体です。3つあるので、順番に取り上げます。

最初の構造体は、表示する要素(文字列や画像など)の関する情報です。

struct ZavDef0 {
    var sType: ZavType
    var sAnim: ZavAnimate
    var sObj: AnyObject? = nil
    var sText: String = "dummy"
    var sTSize: CGFloat = 24
    var sTColor: UIColor = UIColor.blackColor()
    var sFuncI: ((UIView) -> Void)? = nil   // 表示部品に対する初期化時の加工処理:処理せずはnil
    init(sType:ZavType, sAnim:ZavAnimate, sObj:AnyObject?, sFuncI:((UIView) -> Void)?) {
        self.sType = sType
        self.sAnim = sAnim
        self.sObj = sObj
        self.sFuncI = sFuncI
    }
    init(sType:ZavType, sAnim:ZavAnimate, sObj:AnyObject?) {
        self.sType = sType
        self.sAnim = sAnim
        self.sObj = sObj
    }
    init(sAnim:ZavAnimate, sTSize:CGFloat, sText:String, sTColor:UIColor, sFuncI:((UIView) -> Void)?) {
        self.sType = .ZtText
        self.sAnim = sAnim
        self.sText = sText
        self.sTSize = sTSize
        self.sTColor = sTColor
        self.sFuncI = sFuncI
    }
    init(sAnim:ZavAnimate, sTSize:CGFloat, sText:String, sTColor:UIColor) {
        self.sType = .ZtText
        self.sAnim = sAnim
        self.sText = sText
        self.sTSize = sTSize
        self.sTColor = sTColor
    }
}

最初の2項目は、列挙型で定義した内容なので、説明不要でしょう。

続くsObjは、UIViewまたはUIImageを受け取るために使います。文字列では使いません。文字列のときにnilとなるので、Optional型にして、デフォルト値としてnilを入れてあります。

続く3項目は、文字列の場合の指定です。文字列、文字サイズ、文字色が含まれます。

最後は、ライブラリ内で生成した表示部品に対して、何かの加工を加えるために用意しました。文字列またはUIImageを指定したときも、ライブラリの内部ではUIViewの形で生成します。この生成されたUIViewに対して加工を加えるため、関数のデータ型を「((UIView) -> Void)?」としました。Optional型を選んだのは、nilのときは機能オフにしたいからです。

構造体の中身の後には、初期化の処理を並べてあります。最初の初期化は、画像またはUIViewで使います。関係する項目だけしか含まれてないので、余計なダミー値を指定する必要はありません。2つ目の初期化は、関数を外したバージョンで、関数なしのときに使います。関数なしの場合がほとんどなので、こちらを多く使うでしょう。3つめと4つめの初期化は、文字列の場合に使います。こちらも関数の有無で2つ用意しました。

今まで作ったデータ定義の構造体では、デフォルトで生成される初期化を使っていました。理由は、初期化の処理を書かなくて済むからです。しかし今回は、指定する項目数が多いので、減らす目的で初期化を多く付けてみました。実際に使ってみると、今回のほうが、格段に使いやすいです。過去に作ったライブラリでも、同じような初期化を追加したくなりました。後で追加しようと思っています。

 

続いては、2番目のデータ定義の構造体です。アニメーションの共通情報(時間や繰り返し回数など)を指定するために使います。

// 共通動作定義:各種アニメーションに共通する項目
struct ZavDef1 {
    var sRepeat: Float = 1      // 繰り返し回数
    var sReverse: Bool = false  // アニメ終了後に逆再生
    var sTimeDo: NSTimeInterval // アニメの実行時間(秒)
    var sDelay: NSTimeInterval  // アニメ開始までの遅延(秒)
    var sHid1: Bool = false     // アニメ開始前に隠す
    var sHid2: Bool = false     // アニメ終了後に隠す
    var sFunc1: ((UIView) -> Void)? = nil   // アニメ開始時に実行する処理:処理せずはnil
    var sFunc2: ((UIView) -> Void)? = nil   // アニメ終了時に実行する処理:処理せずはnil
    init(sRepeat:Float, sReverse:Bool, sTimeDo:NSTimeInterval, sDelay:NSTimeInterval, sHid1:Bool, sHid2:Bool, sFunc1:((UIView) -> Void)?, sFunc2:((UIView) -> Void)?) {
        self.sRepeat = sRepeat
        self.sReverse = sReverse
        self.sTimeDo = sTimeDo
        self.sDelay = sDelay
        self.sHid1 = sHid1
        self.sHid2 = sHid2
        self.sFunc1 = sFunc1
        self.sFunc2 = sFunc2
    }
    init(sRepeat:Float, sReverse:Bool, sTimeDo:NSTimeInterval, sDelay:NSTimeInterval, sHid1:Bool, sHid2:Bool) {
        self.sRepeat = sRepeat
        self.sReverse = sReverse
        self.sTimeDo = sTimeDo
        self.sDelay = sDelay
        self.sHid1 = sHid1
        self.sHid2 = sHid2
    }
}

それぞれの項目の役割はコメントとして付けてあるので、簡単に理解できると思います。補足が必要な項目だけ、追加した理由を中心に解説します。

アニメの開始前と終了後に隠す設定は、複雑な動きを実現するために用意しました。それぞれに表示部品に指定できるアニメーションは、回転や異動などを組み合わせても、1種類の動きしか指定できません。もし途中から違う動きをさせたい場合、もっと複雑な指定方法を組み込むことになります。そうせずに、複雑な動きを実現する方法として、アニメーションの前後に消す指定を用意しました。

複数の動きを連続して見せたいときは、次のように指定します。まず最初の動きを指定して、決まった時間だけ動かします。当然、終了時には隠す設定です。この続きは、別なアニメーション指定を追加して実現します。同じ表示部品を使ったアニメーション指定を、終了した時間に開始します。こちらは、開始前に隠す設定です。すると、2つのアニメーションは繋がって再生され、1つの表示部品が、連続した違う動きをしているように見えます。同様に指定すれば、3つ以上の動きを連続して指定できるというわけです。これが、アニメーションの前後に隠す設定を付けた理由です。

最後の項目である2つの関数は、アニメーションの開始と終了時に実行します。表示部品が渡されるので、表示状態を変更することもできます。また、アプリ側の処理を起動するのにも使えます。

 

最後は、3番目のデータ定義の構造体です。2Dアニメーションで指定する情報(位置や回転や透明度など)を入れています。

// アニメ定義:2Dアニメーションの指定項目
struct ZavDef2 {
    var sX1: CGFloat = 0.0      // 位置
    var sX2: CGFloat = 0.0
    var sY1: CGFloat = 0.0
    var sY2: CGFloat = 0.0
    var sZoom1: CGFloat = 1.0   // 拡大縮小
    var sZoom2: CGFloat = 1.0
    var sRoll1: CGFloat = 0.0   // 回転
    var sRoll2: CGFloat = 0.0
    var sOpa1: Float = 1.0      // 透明度
    var sOpa2: Float = 1.0
    var sTF: ZavTF = .ZpLinear  // 動作速度変化
    init(sX1:CGFloat, sX2:CGFloat,sY1:CGFloat, sY2:CGFloat, sZoom1:CGFloat, sZoom2:CGFloat, sRoll1:CGFloat, sRoll2:CGFloat, sOpa1:Float, sOpa2:Float, sTF:ZavTF) {
        self.sX1 = sX1
        self.sX2 = sX2
        self.sY1 = sY1
        self.sY2 = sY2
        self.sZoom1 = sZoom1
        self.sZoom2 = sZoom2
        self.sRoll1 = sRoll1
        self.sRoll2 = sRoll2
        self.sOpa1 = sOpa1
        self.sOpa2 = sOpa2
        self.sTF = sTF
    }
    init(sX1:CGFloat, sX2:CGFloat,sY1:CGFloat, sY2:CGFloat, sTF:ZavTF) {
        self.sX1 = sX1
        self.sX2 = sX2
        self.sY1 = sY1
        self.sY2 = sY2
        self.sTF = sTF
    }
    init(sX1:CGFloat, sX2:CGFloat,sY1:CGFloat, sY2:CGFloat, sZoom1:CGFloat, sZoom2:CGFloat, sTF:ZavTF) {
        self.sX1 = sX1
        self.sX2 = sX2
        self.sY1 = sY1
        self.sY2 = sY2
        self.sZoom1 = sZoom1
        self.sZoom2 = sZoom2
        self.sTF = sTF
    }
    init(sX1:CGFloat, sX2:CGFloat,sY1:CGFloat, sY2:CGFloat, sRoll1:CGFloat, sRoll2:CGFloat, sTF:ZavTF) {
        self.sX1 = sX1
        self.sX2 = sX2
        self.sY1 = sY1
        self.sY2 = sY2
        self.sRoll1 = sRoll1
        self.sRoll2 = sRoll2
        self.sTF = sTF
    }
    init(sX1:CGFloat, sX2:CGFloat,sY1:CGFloat, sY2:CGFloat, sOpa1:Float, sOpa2:Float, sTF:ZavTF) {
        self.sX1 = sX1
        self.sX2 = sX2
        self.sY1 = sY1
        self.sY2 = sY2
        self.sOpa1 = sOpa1
        self.sOpa2 = sOpa2
        self.sTF = sTF
    }
}

表示位置、拡大縮小、回転位置、透明度の4つの項目は、それぞれ開始時と終了時の値を指定します。

ちなみに今回の構造体では、開始が1で終了が2と、変数名を統一してあります。最後に1桁の数字を加える方法なら、変数名を邪魔しない形で、同じ変数名に違いを付けられます。

最後の項目となる動作速度変化だけは、開始と終了という2つの値を持たないので、1桁の数字は付きません。この項目は、2番目の構造体に入れようとも考えたのですが、表示部品の細かな動きを指定する項目なので、3番目の構造体に入れました。

この構造体でも、複数の初期化を用意してあります。単純な動きを指定する際、最小限の項目だけ記述すれば済むようにとの配慮です。まだまだ加えられるので、実際に使ってみて、使う機会の多い組み合わせを、後から追加すると思います。

 

データ定義の構造体を紹介するだけでも、かなり長くなってしまいました。ここで一旦、区切ります。続きは次回の投稿にて。

 

(使用開発ツール:Xcode 6.4, SDK iOS 8.4)

0 件のコメント:

コメントを投稿