2012年2月5日 星期日

Filter4Cam 學習之 Core Graphics isn’t scary, honest!

since: 2012/01/30
update: 2012/02/05

reference: Core Graphics isn't scary, honest!

A. 前言
   1. 任何在 iOS 上曾經只用 UIViews 來開發應用程式的人, 可能會對這篇文章的標題
       感到有點奇特. 他們可能會說 "什麼?", "你是精神錯亂嗎? Core Graphics 不僅僅
       只是 CAPI, 並且有讓人感到困惑的函式名稱, 而且要比我用 UIView 能夠作到
       相同的事情需要更多的程式碼". 是的, 他們可能是正確的, 但是有一個
       Core Graphics 存在的理由: 它是速度快的!

       但是使用 Core Graphics 並不意謂著你的程式碼就會是讓人感到困惑的, 或者你
       必須適應去跟效能妥協. 魚與熊掌可兼得(You can  have your cake and eat it too),
       或者說你可以擁有易於閱讀且高執行力的程式碼. 請繼續閱讀來瞭解我的意思.

   2. 本篇文章以原始作者的內容為主, 來建立整個專案.

------------------------------------------------------------------------------------------

B. 新增專案
   1. Xcode > File > New > New Project...
       iOS > Application > Single View Application > Next
       Product Name: MyCoreGraphics
       Device Family: iPhone
       Use Storyboard: checked
       Use Automatic Reference Couting: checked
       > Next

------------------------------------------------------------------------------------------

C. 新增 UIView 子類別
    1. Xcode > File > New > New File...
        iOS > Cocoa Touch > Objective-C class > Next
        Class: CGView
        Subclass of: UIView
        > Next

    2. 調整 MainStoryboard.storyboard 內容如下:
        先選取 View, 接著將其 Class 由 UIView 改選為 CGView.

------------------------------------------------------------------------------------------

D. 使用 drawRect: 的嬰兒步驟
    1. 要在 Core Graphics 中畫圖, 你的進入點是 drawRect: 方法, 並且(大多數情況下)
        是繪製作業能夠被執行的唯一地方. 當開始在 iOS 中用低階的方式來繪圖, 這是
        新手所會有的最大問題, 因為他們習慣於在任何偶然的方法中能夠任意地增加
        subViews 或 subLayers.

        drawRect: 是在其主要框架(main frame)的描繪迴圈(rendering loop)中, 被底層的
        繪圖子系統所呼叫. 繪製下一個 frame 的時機為何, iOS 會去探究螢幕上所有的
        view 並且檢查是否它的內容需要被更新. 如果是的話, drawRect: 方法就會在需要
        被更新的適當矩形範圍被呼叫. 就這麼簡單. 只有當你需要的時候, 它就會被要求
        去繪製內容.

    2. 找到你的環境(context)
        所有在 Core Graphics 中的繪圖操作利用了一個 context 指標, 來對即將要作繪圖
        操作的地區持續地追蹤. 這是一個你會在 C 的 API 中發現的標準作法, 因為沒有了
        物件導向容易可用的 "self" 屬性, 你就必須採用某些手段: 提供 context.

        相關程式碼調整:
        開啓 CGView.m 檔案, 將 drawRect: 方法的註解取消, 並修改如下:
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
    // Drawing code
    //@add: Do something with your context
    CGContextRef ctx = UIGraphicsGetCurrentContext();
}

       說明: 從這個觀點看來, 你可以在之後所有的 Core Graphics 呼叫中使用 ctx 變數.

    3. 儲存狀態(state)
        a. 如果你想要在螢幕上畫任何東西, 甚至是像一條線一樣簡單的事情, 你將會需要
            使用數個函式呼叫來作此事. 這是因為要用各別的呼叫來設定屬性, 例如: 線條
            的顏色, 線條的寬度, 任何你在線條角落(轉彎處)想要的端蓋(end-cap)設定, 然後
            你的線條就會沿著實際的路徑行進. Core Graphics 藉由更新內部的狀態來對那些
            所有不同的屬性保持追蹤.

        b. 把繪畫的狀態想成是一支筆: 你可以改變筆尖, 顏色, 然後可以用同一支筆畫出
            可供區別的線條. 但是如果你忘了將筆改變回去, 使用後沒有自行清理, 你可能
            會在未來的繪畫中招來混亂的情況.

        c. 要避免在工作上遇到類似這樣的問題, 最好使用以下的二個函式:
             CGContextSaveGStateCGContextRestoreGState. 把它想成在你目前的
             繪畫狀態中儲存一個書籤或是檢查點. 然後你就可以改變任何你喜歡的繪畫
             屬性: 畫圖, 填滿, 畫任何的線條; 當都完成後, 便可以將繪畫狀態重設回之前
             的狀態.

        d. 然而要抓住一個重點: 你所呼叫的儲存跟回復的次數必須完美地相配合, 因為
            不然的話, 當無論是誰呼叫你的類別時, 你可能會把繪畫狀態弄混亂了. 這是
            重要的, 因為不然的話你將會在螢幕上看到一些很不可思議的垃圾剛被畫出來.
            要避免這種情況, 我使用了一個非常簡單的技巧, 將在以下的範例程式中展示.

            相關程式碼調整:
            開啓 CGView.m 檔案, 修改 drawRect: 方法如下:
- (void)drawRect:(CGRect)rect
{
    // Drawing code
    //@add: Do something with your context
    CGContextRef ctx = UIGraphicsGetCurrentContext();
   
    CGContextSaveGState(ctx);
    {
        // Draw whatever you like
    }
    CGContextRestoreGState(ctx);
}

           說明: 藉由在儲存回復函式呼叫之間, 包含新的一對括弧, 你就有了一個程式的
                     縮排區塊.(可以在其中寫入任何你想要畫的圖之程式碼)

------------------------------------------------------------------------------------------

E. 使用 UIView 類別來簡單繪畫
   1. 藉由在一些 UIView 子類別所提供的幾個簡單的助手方法, 有一些相當簡單的操作
       讓人可以容易地完成想要做的事情. 例如, 假定你正在建構下一個最偉大的 Twitter
       應用程式, 並且你想要盡可能地在主控項目(feed items)之外榨出更多的效能.
       明顯地, 如果你想要去處理將數以千計的發文以每秒顯示60幀數(60fps)的方式呈現,
       那麼, 描繪文本, 影像, 坡度, 邊界與陰影會是相當昂貴的操作.

      幸運地, 你不需要將 UIView 類別用數百行的 Core Graphics 呼叫所取代. 許多大部份
      常見的類別, 像是: UIImage, UIBezierPath 等等, 都藉由直接使用 Core Graphics 的
      drawRect:方法, 來提供方便繪製內容的方法.

      更新: 原始投遞(post)的文章有一些 bugs, 大部份由於這篇文章是在聖誕假期時,
                只憑記憶寫出來的. 然而有一些對此篇文章非常有用的評註, 讓我必須做些
                改變. 我對第一個版本的 bugs 道歉.

   2. 畫出 UIImage
       我們可以使用 UIImage 物件直接地畫到 Core Graphics 的 context. 當你必須去
       建立一個 CGImage 物件, 色彩(color-space)空間物件等等, 對於每個冗長的
       程式碼, 都要使用原生的(raw) API 來呼叫. 取而代之, 畫出一個影像, 可以如
       以下般地容易: 

            相關程式碼調整:
            開啓 CGView.m 檔案, 修改 drawRect: 方法如下:
- (void)drawRect:(CGRect)rect
{
    // Drawing code
    //@add: Do something with your context
    CGContextRef ctx = UIGraphicsGetCurrentContext();
   
    CGContextSaveGState(ctx);
    {
        // Draw whatever you like
        UIImage *img = [UIImage imageNamed:@"MyImage.png"];
        [img drawInRect:rect];
    }
    CGContextRestoreGState(ctx);
  
}

           結果:
           說明: 用這個技巧, 你可以有更多的選項, 例如: 混合(blending), 與不透明度
                     (alpha)參數. 還有其它方法可讓你在特定的點畫出圖片, 畫圖案等等.

   3. 畫出貝茲路徑(BezierPath)
       經常有時候, 你想要在一些 view 上顯示圓角, 但是你只想讓某幾個角是圓的. 例如,
       假定你想要顯示一個只有頂部二個角為圓形的 view 容器(container). 大多數最直接
       的方式是將它丟入 Core Graphics 中去作描繪. 當可允許單獨使用低階的 API 來依
       路徑構圖, 就可以相當簡單地作到以下的事.

            相關程式碼調整:
            開啓 CGView.m 檔案, 修改 drawRect: 方法如下:
- (void)drawRect:(CGRect)rect
{
    // Drawing code
    //@add: Do something with your context
    CGContextRef ctx = UIGraphicsGetCurrentContext();
   
    CGContextSaveGState(ctx);
    {
        // Draw whatever you like
        //@UIImage
        //UIImage *img = [UIImage imageNamed:@"MyImage.png"];
        //[img drawInRect:rect];
        
        //@UIBezierPath
        CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);
        CGContextSetLineWidth(ctx, 3);
       
        UIBezierPath *path;
       
        path = [UIBezierPath bezierPathWithRoundedRect:rect
                                     byRoundingCorners:(UIRectCornerTopLeft |
                                                        UIRectCornerTopRight)
                                           cornerRadii:CGSizeMake(10.0, 10.0)];
         [path stroke];
       
    }
    CGContextRestoreGState(ctx);
}

           結果:

------------------------------------------------------------------------------------------

F. 回應使用者的輸入
   1. Core Graphics 會盡可能地去試著提高效率, 所以代表你可以使用一些捷徑. 大多數
       那些正好是你想要與期待的部分, 以及作為當你從其它平台轉換過來時的驚喜.
       但是有些時候, 你可以用自己的方式來達成, 所以進階地去瞭解那些最佳化是什麼,
       以及如何去控制它們, 是有幫助的. 並且, 作戰中一半的工作是去了解情報
       (knowing is half the battle)....

   2. 了解 view 何時需要顯示(needs display)
        a. drawRect: 方法只有在當 UIKit 覺得你的內容已經變得陳舊並需要被重畫時才會
            被喚起. 大部份的情況代表你的 drawRect: 方法, 將於 view 被加到 superview 時
            被呼叫. 當 frame 被畫時, drawRect: 就會被呼叫, 明確地去呼叫 drawRect:
            並不會如你預期般地發生作用; 所以如果某些跟你互動的使用者要求重畫你的
            內容, 這裡有一個簡單的解決方式. 

        b. 所有的 UIView 物件都有一個方法: setNeedsDisplay. 呼叫這個方法將會在你的
         物件
內部繫緊(toggle)一個旗標(flag), 用來指明在下一個 frame 時, 你的 view
            需要被重新畫出. 這樣有幾個好處, 事實上包括讓你的程式碼看起來非常敏捷;
            因為無論你在相同的執行迴圈(runloop)裡呼叫了多少次, 你的 frame 將仍然
            只會在下一個 frame 時被重畫一次. 因此, 你不需擔心過度呼叫這個方法, 保證
            你可以從任何 UI 邏輯中決定需要被改變的內容, 而將其內容保持在更新狀態.

        c. 說明: 原始的 code 無法正常執行, 因此在先此不實作.

   3. 當 frame 大小改變時, 作出回應
       a. 當你的 view 改變了它的大小, 無論是明確地設定或者 superview 自動調整
           了 subviews 的大小, 你的內容在某方面來說已經無效了(invalidated). 當這
          情況發生時, 與其總是呼叫昂貴的 drawRect: 方法, UIView 物件有一個
          contentMode 屬性可以讓你用來給 UIKit 一個暗示(hint), 告知在此情況下你想要
          它作什麼.

       b. 這個屬性是一個名為 UIViewContentMode類舉型別之定義類型(typedef enum),
           並且有一串你可以用來檢查的不同項目. 預設值為
            UIViewContentModeScaleToFill, 它代表著: 假如你的 frame 大小改變了, UIKit
           將會簡單地採用最近一次 drawRect: 方法所建立的描繪結果, 並且將其結果放大
           或縮小來符合可用的空間.

          最簡單的方式來讓你的內容重畫, 是去將 view 的 contentMode 設為
          UIViewContentModeRedraw, 但是在許多實例上, 這可能會造成過度的殺傷力
          (overkill).

       c. 例如, 假定你的 drawRect: 方法畫了一個邊界(border)及沿著 frame 的底部畫
           一些線條. 如果你的 frame 改變了高度, 當大部分的內容都沒有改變時, 去執行
           完整的重畫工作是一件浪費的事情. 所以, 如果你將 contentMode 設為
     
UIViewContentModeBottomLeft 的話, UIKit 將會在 view 的左下角重新排列
          內容, 而保持其餘的內容照舊. 

   4. 避免畫過像素的邊界
       a. 當你將元件置放到螢幕上(不只是用 Core Graphics, 一般更常用 UIKit), 你需要
           去確認你的 view 是存在於整數像素座標上的. iOS 的繪圖座標系統是使用點來
           操作的, 不是絕對的像素, 這意謂著有可能告訴 UIKit 或 Core Graphics 在分數
           像素(fractional-pixel)上去繪製內容. 因為這是不可能的, iOS 裝置會試圖去將
           內容作反鋸齒(anti-alias), 所以它會在幾個像素之間變得模糊. 這幾乎不曾是你
           想要的結果, 所以當在計算位置時(特別是當有除值計算時), 使用 floorf
     
ceilf 函數來將其值往上或往下調整至最接近的整數是一件重要的事.   

       b. 當在螢幕上畫圖, 畫線或畫路徑時, 這種情況更是明顯. 看看以下的例子, 我們
           沿著 view 的底部畫一條線. 然而, 這個例子我們將使用低階的畫圖程序來畫出
           路徑, 取代使用 UIBezierPath 物件的方式.

            相關程式碼調整:
            開啓 CGView.m 檔案, 修改 drawRect: 方法如下:
- (void)drawRect:(CGRect)rect
{
    // Drawing code
    //@add: Do something with your context
    CGContextRef ctx = UIGraphicsGetCurrentContext();
   
    CGContextSaveGState(ctx);
    {
        // Draw whatever you like
        //@add: button line
        CGContextSetStrokeColorWithColor(ctx, [UIColor greenColor].CGColor);
        CGContextSetLineWidth(ctx, 1);
       
        CGContextBeginPath(ctx);
        CGContextMoveToPoint(ctx, 0, CGRectGetMaxY(rect));
        CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMaxY(rect));
        CGContextStrokePath(ctx);
    }
    CGContextRestoreGState(ctx);
}

           結果:
           說明: 這段程式碼似乎相當直截了當; 先將畫圖的筆移到左下角, 接著新增一條線
                     到右下角, 然後利用先前定義好的線寬與線的顏色將結果路徑畫出來. 

                     抓住一點, 當你在 (0, 0) 建立一個點, 它代表螢幕上合理的左上角. 然而,
                     左上角的像素佔據了定義為 frame (0, 0, 1, 1) 的空間(亦即, 從 (0,0)
                     作為起始的一個點之正方形). 當你畫線時, 是以任何你給的座標為參考中心
                     的. 因此, 上面的範例中, 你的程式向 Core Graphics 要求畫一條線, 其中 0.5
                     在一個像素上, 另外的 0.5 在另外的像素上. 

                     要想避開這個問題, 你應該將要畫的 frame 作偏移, 所以你把要畫的點作
                     0.5 的位移. 底下是更新後的程式碼版本, 會畫出實心且不會模糊的線:  

            相關程式碼調整:
            開啓 CGView.m 檔案, 修改 drawRect: 方法如下:- (void)drawRect:(CGRect)rect
{
    // Drawing code
    //@add: Do something with your context
    CGContextRef ctx = UIGraphicsGetCurrentContext();
   
    CGContextSaveGState(ctx);
    {
        // Draw whatever you like
        //@add: button line
        CGContextSetStrokeColorWithColor(ctx, [UIColor greenColor].CGColor);
        CGContextSetLineWidth(ctx, 1);
       
        CGContextBeginPath(ctx);
        //CGContextMoveToPoint(ctx, 0, CGRectGetMaxY(rect));
        CGContextMoveToPoint(ctx, 0, CGRectGetMaxY(rect) - 0.5);

        //CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMaxY(rect));
        CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMaxY(rect) - 0.5);

        CGContextStrokePath(ctx);
    }
    CGContextRestoreGState(ctx);
}

           結果:

   5. 畫彩色的容易方法
       a. Core Graphics 已隨著時間逐漸發展, 有了新的與改進過的 API. 這意謂著, 對許多
          繪畫的操作, 可能會有幾個不同的函式呼叫, 會讓你得到相同的結果. 特別是在使用
          顏色時會是如此.

       b. 舉例來說, 如果你想要在 Core Graphics 中設定線條的顏色, 分別有四個函數呼叫
           可以達成相同的結果.
           你第一個念頭可能會先採用顯示的第一個結果, 但是那代表需要做更多的工作.
           標準的 CGContextSetStrokeColor 函式呼叫, 帶有一個 CGFloats 的陣列, 在其中
           指出你想要畫的不同顏色之值.

       c. 然而, 有一個更容易的方式. CGContextSetStrokeColorWithColor 是一個稍微更
           冗長的函式名稱, 但是允許你可以提供一個 CGColorRef 的值來描述一個顏色.
    
並且方便, UIColor 類別提供一個助手(helper)的屬性叫作 CGColor, 它會回傳
          預先計算好的(pre-calculated)顏色參照(reference), 當 UIColor 物件離開時, 會
     自動釋放記憶體.
這意謂著, 你不需要手動去配置記憶體(malloc)或釋放(release)
          顏色的參照, 因此, 最後結果會使用較少的程式碼.

       d. 每當你看到一個函式有一個 "ColorWithColor" 可以選擇, 它即是使用了這個模式.

------------------------------------------------------------------------------------------

G. 使用適合的工具來工作
     1. 我能給的最佳忠告或許是: 使用適合的工具來工作. 例如, 如果你需要設計一些
         視覺上的介面元素, 你就不該去新增不需要的 subviews, 只要去畫東西像是邊界,
         圓角, 瓷磚圖案等等. 此外, 有些個案使用了大量的 subviews (就像是在
         UITableViewCells 裡) 是沒有效率的, 並且繪製簡單的元素像是圖像, 文字或邊界
         是更適合使用 Core Graphics 的.

     2. iOS 與 UIKit 是跟 HTML 絕然不同的環境, 並且你不能把設備的版面編排與繪圖
         看作相同的方式來處理.

     3. 最後, 確認你有做練習, 並且不要害怕試驗 Core Graphics. 從簡單的事開始, 像是
         邊界, 畫圖, 與文字或者以程式化的方式去繪製出特效(否則使用 Core Animation
         或是基於圖像的紋理(textures)的話, 速度會較慢).

沒有留言:

張貼留言

注意:只有此網誌的成員可以留言。