2011年4月21日

iOS SDK 認識 Quartz 2D (1)

Hi all, 接續上一篇iOS SDK 認識 Quartz 2D(0)的內容,我們希望將我們所畫出來的矩形包裝成一個Class,已達到可以在畫面上呈現多個矩形,並且不會造成程式碼過長的情況;這一篇的內容將可以持續使用上篇的已經編輯好的專案繼續使用,內容上,除了將矩形包裝成一個Class外,還會介紹將上一篇提到的轉至矩陣帶入Class中。

我會將閱讀文章的人視作對 c / obj-c 以及 iOS有一定的認識,因此在obj-c/c的一些使用內容將不會有太多的解釋。另外會以AS3為對照的語言,作一些相關的引用跟對照。

首先先新增一個Class,命名為Sprite 繼承自 NSObject。
Sprite.h:
@interface Sprite: NSObject {
    CGFloat x;
    CGFloat y;
    CGFloat width;
    CGFloat height;
    CGFloat red;
    CGFloat blue;
    CGFloat green;
    CGFloat alpha;
}

@property (assign) CGFloat x, y;
@property (assign) CGFloat width, height;
@property (assign) CGFloat red, green, blue, alpha;

-(void) draw:(CGContextRef) context;

@end
在Sprite.h內,我們知道這個Class會有(x,y)座標、長、寬以及RGBA等參數,並宣告了一個實體method draw;接下來我們開始實做這Class。
Sprite.m:
@implementation Sprite
@synthesize x, y;
@synthesize width, height;
@synthesize red, green, blue, alpha;

-(id)init
{
    self = [super init];
    if(self)
    {
        x = y = 0.0;
        width = height = 0.0;
        red = green = blue = 0.0;
        alpha = 0.0;
    }
}
-(void) draw:(CGContextRef) context
{
    CGContextSaveGState(context);

    CGContextBeginPath(context);
    CGContextSetRGBFillColor(context, red, green, blue, alpha);
    CGContextAddRect(context, CGRectMake(x, y, width, height) );
    CGContextClosePath(context);
    CGContextDrawPath(context, kCGPathFill);

    CGContextRestoreGState(context);
}
在上面,你會注意到兩行(19跟27)分別被我highlight起來;在這邊會需要將他的狀況保存起來是因為我們之後將會對這個Class去進行擴充,所以要將這個Class(或者說被建立出來的實體)本身的CTM去獨立的保存與釋放;如果不這麼作,可能會遇到圖形變形或者是座標...等等出現問題;大家可以在後面進行縮放跟旋轉擴充後,在去測試將這個動作移除後的差異。

然後回到Quartz2DView的部分,分別對 .h 以及 .m 作下面的修改:
//-----Quartz2DView.h
#import "Sprite.h"
@interface Quartz2DView : UIView {
    Sprite  *mySprite0;
    NSTimer *timer;
}
-(void) dt:(NSTimer*) theTimer
@end
//Quartz2DView.m 
-(id) initWithFrame:(CGRect) frame
{
    self = [super initWithFrame:frame];
    if(self)
    {
        mySprite0 = [[Sprite alloc] init];
        mySprite0.width = 100;
        mySprite0.height = 100;
        timer = [NSTimer scheduledTimerWithTimeInterval:1/30.0
                                                 target:self
                                               selector:@selector(dt:)
                                               userInfo:nil
                                                repeats:YES];
    }
}
-(void) dt:(NSTimer*) theTimer
{
    mySprite0.y+=1;
    [self setNeedsDisplay];
}
-(void) drawRect:(CGRect) rect{
    CGContextRef context = UIGraphicsGetCurrentContext(); 
    CGContextSaveGState(context); 
     
    CGAffineTransform t0 = CGContextGetCTM( context );
    t0 = CGAffineTransformInvert(t0);
    CGContextConcatCTM(context, t0);
     
    [mySprite0 draw:context];
     
    CGContextRestoreGState(context);
}
到這邊就修改完畢了,執行的結果,應該是上一篇最後執行的結果,沿著Y軸移動。

接下來我們將要進行Sprite的擴充,希望讓Sprite有旋轉和縮放的能力,這就要使用到上一篇提到的轉置矩陣,轉置矩陣可以參考一下AS3的Matrix;那麼現在,我們就開始修改Sprite的內容。
Sprite.m
@interface Sprite: NSObject {
    CGFloat x;
    CGFloat y;
    CGFloat width;
    CGFloat height;
    CGFloat red;
    CGFloat blue;
    CGFloat green;
    CGFloat alpha;
    CGFloat rotation;
    CGFloat scaleX;
    CGFloat scaleY;
    CGRect  box;
}

@property (assign) CGFloat x, y;
@property (assign) CGFloat width, height;
@property (assign) CGFloat red, green, blue, alpha;
@property (assign) CGFloat rotation;
@property (assign) CGFloat scaleX,scaleY;
@property (assign) CGRect  box;

-(void) drawOutlinePath:(CGContextRef) context;
-(void) drawBody:(CGContextRef) context;
-(void) draw:(CGContextRef) context;
-(void) updateBox;
@end
上面我們對Sprite Class增加了旋轉(rotation)及縮放(scaleX,scaleY)的參數,以及drawBody、drawOutlinePath兩個instance method,box則是用來記錄Sprite在畫面上的起點跟大小,updateBox則是用來更新box的內容後面會有更詳細的解釋;這邊我們會注意當我們在旋轉的時候,會以Sprite的原點作旋轉,而現在的原點,在左下角如下圖所示。
如果依照上圖,以左下角原點做旋轉的話,在大多的時候不適用在我們的物件上,所以我們希望將員點移到物件的中心點,如下圖一所示。這樣才能達到理想中的旋轉情況,如圖二所示。
圖一

圖二
因此我們在Sprite.m 的 drawOutlinePath 中將Sprite的原點做改變
Sprite.m
-(void) drawOutlinePath:(CGContextRef) context
{
    CGFloat w2 = box.size.width  * 0.5;// 半寬
    CGFloat h2 = box.size.height * 0.5;// 半高

    CGContextBeginPath( context );
    CGContextMoveToPoint( context, -w2, h2);
    CGContextAddLineToPoint( context, w2, h2);
    CGContextAddLineToPoint( context, w2, -h2);
    CGContextAddLineToPoint( context, -w2, -h2);
    CGContextAddLineToPoint( context, -w2, h2);
    CGContextClosePath( context );
}
上面的程式碼中,如果你熟悉AS3應該會覺得CGContextMoveToPoint、CGContextAddLineToPoint很像是AS3中的graphic.MoveTo()graphic.LineTo(),其實非常的類似;也就是先將起始點移到(-w2,h2),然後再依照寬高將外框的線畫出來並畫回起始點;這樣,物件的原點與中心點就重合到在一起了(上圖一)。
(其實這邊也可以用簡單的CGContextAddRect去繪製矩形,不過在這邊介紹另一個用法給大家參考)

接下來我們繼續將 drawBody 、 draw 兩個method完成:
//Sprite.m
-(void) drawBody:(CGContextRef) context
{
    CGContextSetRGBFillColor( context , red, green, alpha);
    [self drawOutlinePath:context];
    CGContextDrawPath( context );
}
-(void) draw:(CGContextRef) context
{
    CGContextSaveGState(context);

    CGAffineTransform t0 = CGAffineTransformIsIdentity;
    t0 = CGAffineTransformTranslate( context, x, y);
    t0 = CGAffineTransformRotate( context, rotation);
    t0 = CGAffineTransformScale ( context, scaleX, scaleY);
    CGContextConcatCTM( context, t0);

    [self drawBody:context];

    CGContextRestoreGState(context);
}
上面被Highlight的部分,就是對Sprite Class本身去進行旋轉、縮放以及位移的動作,CGAffineTransformTranslate、CGAffineTransformRotate跟CGAffineTransformScale 使用順序可能要特別注意,不然出來的結果可能會跟你想的很不一樣。

接著再來講解box以及updateBox內容,你也許在上面drawOutlinePath已經有注意到box有被使用到,但是我們並沒有去設定box相關的內容,那他的寬、高是如何得到的?我們配合下面的程式碼以及圖三來進行解釋:
//Sprite.m
-(void)updateBox
{
    // 取得現在的寬高
    CGFloat w0 = width * scaleX;
    CGFloat h0 = height * scaleY;
    // 求出半高及半寬
    CGFloat w2 = w0 * 0.5;
    CGFloat h2 = h0 * 0.5;

    CGSize size = box.size;
    CGPoint origin = box.origin;
    //更新圖形的寬高以及起始點
    size.width = w0;
    size.height = h0;
    origin.x = x - w2;
    origin.y = y - h2;

    box.size = size;
    box.origin = origin;
}
圖三
上面的程式碼,主要事要隨著改變Sprite的寬、高以及對x、y方向的縮放,去調整實際上Sprite物件的大小,並將線在範圍重新定位。

最後因為Quartz 2D的旋轉是以弧度來計算所以在 rotation 的 setter & getter 要再作一些修改:
-(void)setRotation:(CGFloat) degree
{
    rotation = degree * 3.141592 / 180;
}
-(CGFloat) rotation
{
    return rotation * 180 / 3.141592;
}
最後,我們將Quartz2DView的mySprite作一些修改
-(id) initWithFrame:(CGRect) frame
{
    self = [super initWithFrame:frame];
    if(self)
    {
        mySprite0 = [[Sprite alloc] init];
        mySprite0.width = 100;
        mySprite0.height = 100;
        mySprite0.red = 1;
        mySprite0.rotation = 30;
        mySprite0.height = 100;        
        mySprite0.scaleX = 1.5;
        //timer = [NSTimer scheduledTimerWithTimeInterval:1/30.0
        //                                         target:self
        //                                       selector:@selector(dt:)
        //                                       userInfo:nil
        //                                        repeats:YES];
    }
}
執行後的結果

到這篇,這一篇就告一段落了,內容比我想像中的多了很多 -.-",看來應該是我沒有掌控好,不過內容也大多是被程式碼給佔據就是;最後希望這篇對大家有幫助,也跟大家一起共勉求進步。對於內容有任何錯誤或者建議,也請大家不要吝嗇,謝謝。

參考資料:
iOS Developer Library CGContext Reference
iOS Developer Library CGAffineTransform Reference
ActionScript3.0 Matrix
ActionScript3.0 Graphics

範例下載:Sample2.zip

上一篇相關文章:iOS SDK 認識 Quartz 2D(0)

沒有留言:

張貼留言