2014年12月21日日曜日

オフスクリーン描画のライブラリ化(拡張編)

このブログは、私の友人も見ています。先日紹介し終わった描画ライブラリを見て、さっそく意見をもらいました。使う方法が少し面倒でも構わないから、もっと機能を増やしたバージョンも作って紹介したらどうかと。

突っ込んで尋ねると、友人自身は既に自分で改良し、使い始めているとのこと。だったら、紹介文を書いてほしいと依頼しましたが、文章を書くのが苦手なのに加え、説明が下手なので勘弁してほしいとのことでした。

まあ、あまり手間をかけずに改良できそうなので、試しに作ってみました。改良前と異なる部分だけ、簡単に紹介します。

 

まずは、友人が示した要望を列挙します。言われたのは、次のようなことです。1つのインスタンスで何度も描画できること。つまり、描画の開始と終了を何度でも繰り返せること。何回か描画を繰り返す場合、登録した色を継続して使えること。逆に、登録した色をクリアーして、新しく描き始めることもできること。描画途中の状態で、何度でもUIImageを取り出せること。つまり、UIImageを取り出すときに描画を終了しないこと。

この要望を上手に整理すれば、少しの手直しだけで、要望を実現できそうです。一番大事なのは、色の登録と描画の開始終了を、別々に行なえることでしょう。

まずは、色の登録です。描画の開始終了と分離するわけですから、開始時点に色数を指定した方法が使えなくなります。また、途中でのクリアー処理も必要となります。メソッド数は増やしたくないので、クリアー処理を追加し、残りは既存の色数追加メソッドを用いることにしました。クリアーした時点では色数がゼロになり、必要な色数だけ追加メソッドで加えれば問題ありません。

続いて、描画の開始と終了です。何度も繰り返すわけですから、それぞれメソッドを用意します。現状では、インスタンス生成時に描画を開始しています。これをどうするかです。仕様をスッキリさせるために、インスタンス生成時には何もしないことにしました。生成後に、描画開始のメソッドを実行する必要があります。

その他の構成は、既存のメソッドのままで大丈夫そうです。当然、メソッド内部の処理は変更しますが、メソッドの構成は変えなくても問題ないでしょう。まとめると、次のようなメソッド構成になります。

描画ライブラリ(拡張版)のメソッド
・描画の開始(イメージの大きさを指定)
・描画の終了
・色(コンテキスト)登録のクリアー
・色(コンテキスト)の追加
・色(コンテキスト)の設定(線色)
・色(コンテキスト)の設定(塗りぶつし色)
・UIImageの取得
・各種描画(線、長方形、文字列など)

どのメソッドも1つずつ作りますが、描画メソッドだけは描画の種類ごとに作ります。

 

ライブラリの質を高めるためには、エラーチェックが欠かせません。細かな値のエラーチェックよりも、呼び出してはいけない条件で呼び出されたとき、エラーで処理するようなチェック仕様が大事です。

このライブラリには、2つの状態があります。描画可能な状態と、描画不可能な状態です。もちろん最初は描画不可能な状態で、描画開始メソッドにより描画可能状態となり、描画終了メソッドにより描画不可能状態に戻ります。開始と終了以外のメソッドを、2つの状態との関係で1つずつ見ていきましょう。

色登録のクリアーメソッドは、描画中に実行されては困ります。実行可能なのは、描画不可能状態のみとしましょう。これにより、描画が終了した後でクリアーするという、使い方の制限が生まれます。

色の追加メソッドは、描画中(描画可能状態)でも、描画不可能状態でも使えなければなりません。描画状態のチェックはしないこととします。追加する色数の値チェックだけは必要ですね。

2種類の色の設定メソッドはどちらも、描画状態に関係なく使えたほうが便利でしょう。描画状態のチェックはしません。ただし、指定した色番号が登録数の範囲内かだけは、チェックが必要です。

UIImageの取得メソッドは、当然ながら描画状態でないと使えません。チェックが必要です。

各種描画メソッドも、描画状態でないと使えません。これにも、チェックが必要です。また、色番号を指定するメソッドでは、登録数の範囲内かチェックする必要があります。

整理すると、以上のような形となりました。あとは、この仕様に合わせて、実際のSwiftコードを修正するだけです。

 

それぞれのメソッドの細かな説明は、以前に書いた投稿に含まれています。ここでは、修正後のソースコードを載せるだけで十分でしょう。次のようなSwiftコードになりました。

// オフスクリーン描画の機能拡張版
enum DrawType : Int {
    case None = 0
    case Stroke = 1
    case Fill = 2
    case Both = 3
}

class DrawImageOffScreen {
    // 描画可能フラグ
    var enabledDraw : Bool = false
    // 色のデフォルト値
    let COLOR_DFAULT_STROKE : [CGFloat] = [0.0, 0.0, 0.0, 1.0] // 黒で不透明
    let COLOR_DFAULT_FILL : [CGFloat] = [1.0, 1.0, 1.0, 1.0] // 白で不透明
    // CGContextに設定する値の入れ物
    var numContext : Int = 0
    var arySColor : [[CGFloat]] = [] // StrokeColor
    var aryFColor : [[CGFloat]] = [] // FillColor

   // =================================== 描画の開始と終了
     // 描画の開始
    func startDrawF(rWidth:CGFloat, _ rHight:CGFloat, _ rOpaque:Bool) {
        if enabledDraw { println("ERROR:DIOS_SP:Disabled"); return }
        // オフスクリーン描画の開始(rOpaqueがfalseで背景が透明に,scaleが0.0で自動)
        let iCGSize : CGSize = CGSizeMake(rWidth, rHight)
        UIGraphicsBeginImageContextWithOptions(iCGSize, rOpaque, 0.0)
        enabledDraw = true
    }
    // 描画の終了
    func stopDrawF() {
        if !enabledDraw { println("ERROR:DIOS_SP:Disabled"); return }
        // 描画の終了
        enabledDraw = false
        UIGraphicsEndImageContext()
        return
    }
    // =================================== コンテキスト管理
    // コンテキストの初期化
    func clearContextF() {
        if enabledDraw { println("ERROR:DIOS_AC:Disabled"); return }
        numContext = 0
        arySColor = []
        aryFColor = []
    }
    // 指定した個数のコンテキストを追加する
    func addContextF(rNumAdd:Int) {
        if (rNumAdd < 1) { println("ERROR:DIOS_AC:rNumAddが小さすぎ"); return }
        numContext += rNumAdd
        for _ in 1...rNumAdd {
            arySColor.append(COLOR_DFAULT_STROKE)
            aryFColor.append(COLOR_DFAULT_FILL)
        }
    }
    // コンテキストの線色を設定する
    func setContextStrokeColorF(rNum:Int, _ rRed:CGFloat, _ rGreen:CGFloat, _ rBlue:CGFloat, _ rAlpha:CGFloat) {
        if (rNum >= numContext || rNum < 0) { println("ERROR:DIOS_SC:rNumが範囲超え"); return }
        arySColor[rNum][0] = rRed
        arySColor[rNum][1] = rGreen
        arySColor[rNum][2] = rBlue
        arySColor[rNum][3] = rAlpha
    }
    // コンテキストの塗りつぶし色を設定する
    func setContextFillColorF(rNum:Int, _ rRed:CGFloat, _ rGreen:CGFloat, _ rBlue:CGFloat, _ rAlpha:CGFloat) {
        if (rNum >= numContext || rNum < 0) { println("ERROR:DIOS_FC:rNumが範囲超え"); return }
        aryFColor[rNum][0] = rRed
        aryFColor[rNum][1] = rGreen
        aryFColor[rNum][2] = rBlue
        aryFColor[rNum][3] = rAlpha
    }
    // =================================== 描画結果を返す
    // 描画内容をUIImageとして返す
    func getImageF() -> UIImage! {
        if !enabledDraw { println("ERROR:DIOS_GI:Disabled"); return nil }
        // 描き終わった内容をUIImageに保存する
        let iImage:UIImage = UIGraphicsGetImageFromCurrentImageContext()
        return iImage
    }
    // =================================== いろいろな描画(以下省略)
}

図形や文字を描くメソッドは、前と同じままなので掲載を省略しました。前のままコピーして付け加えると、拡張版のクラスとして動作するはずです。

 

当然ですが、使い方も少し変わります。インスタンスを生成した後、何度も描画できます。それぞれの描画処理では、描画を開始し、コンテキストを追加して色を設定し、描画メソッドを繰り返して描き、描き終わったらUIImageを取得して、最後に描画を終了します。以下のSwiftコードは、以前に掲載したものを、拡張版に合わせて直した結果です。

// 架空の整理券を描く(飾りを省いたバージョンを、拡張版に合わせて修正)
var iDraw = DrawImageOffScreen()
// 描画の開始
iDraw.startDrawF(400, 150, false)
// 色ごとに変数を用意する
iDraw.addContextF(10)
let cMain : Int = 0
let cMainL : Int = 1
let cTextDark : Int = 2
let cTextLight : Int = 3
let cTextSp : Int = 4
let cLineStrong : Int = 5
let cLineWeak : Int = 6
let cDecoLine : Int = 7
let cDecoBox : Int = 8
let cDecoCircle : Int = 9
// 色に値を設定する
iDraw.setContextStrokeColorF(cMain, 0.1, 0.1, 1.0, 1.0)
iDraw.setContextFillColorF(cMain, 0.9, 0.9, 1.0, 1.0)
iDraw.setContextStrokeColorF(cTextDark, 0.1, 0.1, 0.1, 1.0)
iDraw.setContextStrokeColorF(cTextLight, 0.5, 0.5, 0.5, 1.0)
iDraw.setContextStrokeColorF(cTextSp, 1.0, 0.0, 0.0, 1.0)
iDraw.setContextStrokeColorF(cLineStrong, 0.1, 0.1, 1.0, 1.0)
iDraw.setContextStrokeColorF(cLineWeak, 0.7, 0.7, 1.0, 1.0)
iDraw.setContextStrokeColorF(cDecoLine, 0.1, 0.1, 1.0, 1.0)
iDraw.setContextFillColorF(cDecoBox, 0.1, 0.1, 1.0, 0.5)
iDraw.setContextFillColorF(cDecoCircle, 0.3, 1.0, 0.3, 0.5)
// ここから描画内容
let iNumTicket : String = "FN-579-413"
let iNumMember : String = "GCG-40167"
// 描画:整理券情報
iDraw.drawRectF(cMain, .Both, 1.0, 10.0, 10.0, 170.0, 60.0)
iDraw.drawTextF(cTextDark, "整理番号:", 16.0, 15.0, 13.0)
iDraw.drawTextF(cTextDark, iNumTicket, 30.0, 15.0, 33.0)
// 描画:タイトルと日付
iDraw.drawTextF(cTextDark, "入場整理券", 16.0, 200.0, 35.0)
iDraw.drawTextF(cTextDark, "  日付:2014年12月20日", 14.0, 200.0, 55.0)
// 中央の区切り線
iDraw.drawLineF(cLineStrong, 3.0, 10.0, 80.0, 380.0, 80.0)
// 描画:会員情報
iDraw.drawTextF(cTextDark, ("会員番号:" + iNumMember), 14.0, 15.0, 90.0)
iDraw.drawTextF(cTextSp, "開催場所:秘密(招待メール参照)", 14.0, 180.0, 90.0)
// 描画:発行人
iDraw.drawTextF(cTextLight, "発行人:ガイガー・カウント・グループ", 12.0, 180.0, 115.0)
iDraw.drawTextF(cTextLight, "watashida@gaigaigai.com", 12.0, 250.0, 130.0)
// UIImageの取得
iImageView20c.image = iDraw.getImageF()
// 描画の終了
iDraw.stopDrawF()

基本的には開始と終了が違っていて、他にも色(コンテキスト)の追加が必要となります。描画メソッドや色設定メソッドなどは、前と同じまま使えます。また、UIImageの取得も、途中で何回でも可能になっています。

何度も描くため、描画の開始メソッドで、イメージの大きさを指定しています。このため、違うサイズのイメージでも、連続して描けるようになりました。

 

使う上で注意しなければならないのは、エラーチェックに引っかからない点でしょう。たとえば、色(コンテキスト)をクリアーするメソッドは、描画中にはエラーになります。描画途中でクリアーされると困るので、そうしたわけです。色をクリアーしたいときは、描画を終了してからクリアーを実行し、新たな色を追加する手順となります。

どのエラーチェックにも意味があり、間違った使い方ができにくいように考えて付けてあります。エラーチェックに引っかからないような使い方が正しい使い方であり、その使い方で困らないはずです。

使い方の例は、これぐらいで大丈夫でしょう。それほど難しい機能は搭載していないので、普通に使えると思います。

 

今回の拡張バージョンですが、私に限っては、前のバージョンの方が使いやすいと思っています。こうした描画ライブラリを使うときは、描画部分を関数として1つにまとめるでしょう。すると、描画する度に、その関数を呼び出す使い方です。連続して何個も描くような使い方は、おそらく生じないと考えられます。関数内に収めるため、拡張版でも構わないといえば構わないのですが、拡張したメリットはほとんどなさそうです。

もちろん、拡張版のような形を望む人もいるでしょう。該当する人は、今回のソースコードを参考にして、自分なりのライブラリを作ってください。

 

(使用開発ツール:Xcode 6.1.1, SDK iOS 8.1)

0 件のコメント:

コメントを投稿