iOS開發筆記--使用blend改變圖片顏色

最近對Core AnimationCore Graphics的內容東西比較感興趣,自己之前也在這塊相對薄弱,趁此機會也想補習一下這塊的內容,所以之後幾篇可能都會是對CA和CG學習的記錄的文章。

在應用裏一個很常見的需求是主題變換:同樣的圖標,同樣的素材,但是需要按照用戶喜愛變爲不同的顏色。在iOS5和6的SDK裏部分標準控件引入了tintColor,來滿足個性化界面的需求,但是Apple在這方面還遠遠做的不夠。一是現在用默認控件根本難以做出界面優秀的應用,二是tintColor所覆蓋的並不夠全面,在很多情況下開發者都無法使用其來完成個性化定義。解決辦法是什麼?最簡單當然是拜託設計師大大出圖,想要藍色主題?那好,開PS蓋個藍色圖層出一套藍色的UI;又想加粉色UI,那好,再出一套粉色的圖然後導入Xcode。代碼上的話根據顏色需求使用image-blue或者image-pink這樣的名字來加載圖片。

如果有一丁點重構的意識,就會知道這不是一個很好的解決方案。工程中存在大量的冗餘和重複(就算你要狡辯這些圖片顏色不同不算重複,你也會在內心裏知道這種狡辯是多麼無力),這是非常致命的。想象一下如果你有10套主題界面,先不論應用的體積會膨脹到多少,光是想做一點修改就會痛苦萬分,比如希望改一下某個按鈕的形狀,很好,設計師大大請重複地修改10遍,並出10套UI,然後一系列的重命名,文件移動和導入…一場災難。

當然有其他辦法,因爲說白了就是tint不同的顏色到圖片上而已,如果我們能實現改變UIImage的顏色,那我們就只需要一套UI,然後用代碼來改變UI的顏色就可以了,生活有木有一下光明起來呀。嗯,讓我們先從一張圖片開始吧~下面是一張帶有alpha通道的圖片,原始顏色是純的灰色(當然什麼顏色都可以,只不過我這個人現在暫時比較喜歡灰色而已)。



我們將用blending給這張圖片加上另一個純色作爲tint,並保持原來的alpha通道。用Core Graphics來做的話,大概的想法很直接:

  1. 創建一個上下文用以畫新的圖片
  2. 將新的tintColor設置爲填充顏色
  3. 將原圖片畫在創建的上下文中,並用新的填充色着色(注意保持alpha通道)
  4. 從當前上下文中取得圖片並返回

最麻煩的部分可能就是保持alpha通道了。UIImage的文檔中提供了使用blend繪圖的方法drawInRect:blendMode:alpha:rectalpha都沒什麼問題,但是blendMode是個啥玩意兒啊…繼續看文檔,關於CGBlendMode的文檔,裏面有一大堆看不懂的枚舉值,比如這樣:

[objc] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. kCGBlendModeDestinationOver  
  2. R = S*(1 - Da) + D  
  3. Available in iOS 2.0 and later.  
  4. Declared in CGContext.h.  

完全不懂..直接看之後的Discussion部分:

The blend mode constants introduced in OS X v10.5 represent the Porter-Duff blend modes. The symbols in the equations for these blend modes are:
R is the premultiplied result
S is the source color, and includes alpha
D is the destination color, and includes alpha
Ra, Sa, and Da are the alpha components of R, S, and D

原來如此,R表示結果,S表示包含alpha的原色,D表示包含alpha的目標色,Ra,Sa和Da分別是三個的alpha。明白了這些以後,就可以開始尋找我們所需要的blend模式了。相信你可以和我一樣,很快找到這個模式:

[objc] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. kCGBlendModeDestinationIn  
  2. R = D*Sa  
  3. Available in iOS 2.0 and later.  
  4. Declared in CGContext.h.  

結果 = 目標色和原色透明度的加成,看起來正式所需要的。啦啦啦,還等什麼呢,開始動手實現看看對不對吧~

爲了以後使用方便,當然是祭出Category,先創建一個UIImage的類別:

[objc] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. //  UIImage+Tint.h  
  2.   
  3. #import <UIKit/UIKit.h>  
  4.   
  5. @interface UIImage (Tint)  
  6.   
  7. - (UIImage *) imageWithTintColor:(UIColor *)tintColor;  
  8.   
  9. @end  

暫時先這樣,當然我們也可以創建一個類方法直接完成從bundle讀取圖片然後加tintColor,但是很多時候並不如上面一個實例方法方便(比如想要從非bundle的地方獲取圖片),這個問題之後再說。那麼就按照之前設想的步驟來實現吧:

[objc] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. //  UIImage+Tint.m  
  2.   
  3. #import "UIImage+Tint.h"  
  4.   
  5. @implementation UIImage (Tint)  
  6. - (UIImage *) imageWithTintColor:(UIColor *)tintColor  
  7. {  
  8.     //We want to keep alpha, set opaque to NO; Use 0.0f for scale to use the scale factor of the device’s main screen.  
  9.     UIGraphicsBeginImageContextWithOptions(self.sizeNO0.0f);  
  10.     [tintColor setFill];  
  11.     CGRect bounds = CGRectMake(00self.size.widthself.size.height);  
  12.     UIRectFill(bounds);  
  13.   
  14.     //Draw the tinted image in context  
  15.     [self drawInRect:bounds blendMode:kCGBlendModeDestinationIn alpha:1.0f];  
  16.   
  17.     UIImage *tintedImage = UIGraphicsGetImageFromCurrentImageContext();  
  18.     UIGraphicsEndImageContext();  
  19.   
  20.     return tintedImage;  
  21. }  
  22. @end  

簡單明瞭,沒有任何難點。測試之:[[UIImage imageNamed:@"image"] imageWithTintColor:[UIColor orangeColor]];,得到的結果爲:


嗯…怎麼說呢,雖然tintColor的顏色是變了,但是總覺得怪怪的。仔細對比一下就會發現,原來灰色圖裏星星和周圍的灰度漸變到了橙色的圖裏好像都消失了:星星整個變成了橙色,周圍的一圈漂亮的光暈也沒有了,這是神馬情況啊…這種圖能交差的話那算見鬼了,會被設計和產品打死的吧。對於無漸變的純色圖的圖來說直接用上面的方法是沒問題的,但是現在除了Metro的大色塊以外哪裏無灰度漸變的設計啊…檢查一下使用的blend,R = D * Sa,恍然大悟,我們雖然保留了原色的透明度,但是卻把它的所有的灰度信息弄丟了。怎麼辦?繼續刨CGBlendMode的文檔吧,那麼多blend模式應該總有我們需要的。功夫不負有心人,kCGBlendModeOverlay一副嗷嗷待選的樣子:

[objc] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. kCGBlendModeOverlay  
  2. Either multiplies or screens the source image samples with the background image samples, depending on the background color. The result is to overlay the existing image samples while preserving the highlights and shadows of the background. The background color mixes with the source image to reflect the lightness or darkness of the background.  
  3. Available in iOS 2.0 and later.  
  4. Declared in CGContext.h.  

kCGBlendModeOverlay可以保持背景色的明暗,也就是灰度信息,聽起來正是我們需要的。加入到聲明中,並且添加相應的實現( 順便重構一下原來的代碼 :) ):

[objc] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. //  UIImage+Tint.h  
  2.   
  3. #import <UIKit/UIKit.h>  
  4.   
  5. @interface UIImage (Tint)  
  6.   
  7. - (UIImage *) imageWithTintColor:(UIColor *)tintColor;  
  8. - (UIImage *) imageWithGradientTintColor:(UIColor *)tintColor;  
  9.   
  10. @end  

[objc] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. //  UIImage+Tint.m  
  2.   
  3. #import "UIImage+Tint.h"  
  4.   
  5. @implementation UIImage (Tint)  
  6. - (UIImage *) imageWithTintColor:(UIColor *)tintColor  
  7. {  
  8.     return [self imageWithTintColor:tintColor blendMode:kCGBlendModeDestinationIn];  
  9. }  
  10.   
  11. - (UIImage *) imageWithGradientTintColor:(UIColor *)tintColor  
  12. {  
  13.     return [self imageWithTintColor:tintColor blendMode:kCGBlendModeOverlay];  
  14. }  
  15.   
  16. - (UIImage *) imageWithTintColor:(UIColor *)tintColor blendMode:(CGBlendMode)blendMode  
  17. {  
  18.     //We want to keep alpha, set opaque to NO; Use 0.0f for scale to use the scale factor of the device’s main screen.  
  19.     UIGraphicsBeginImageContextWithOptions(self.sizeNO0.0f);  
  20.     [tintColor setFill];  
  21.     CGRect bounds = CGRectMake(00self.size.widthself.size.height);  
  22.     UIRectFill(bounds);  
  23.   
  24.     //Draw the tinted image in context  
  25.     [self drawInRect:bounds blendMode:blendMode alpha:1.0f];  
  26.   
  27.     UIImage *tintedImage = UIGraphicsGetImageFromCurrentImageContext();  
  28.     UIGraphicsEndImageContext();  
  29.   
  30.     return tintedImage;  
  31. }  
  32.   
  33. @end  

完成,測試之…好吧,好尷尬,雖然顏色和周圍的光這次對了,但是透明度又沒了啊魂淡..一點不奇怪啊,因爲kCGBlendModeOverlay本來就沒承諾給你保留原圖的透明度的說。


那麼..既然我們用kCGBlendModeOverlay能保留灰度信息,用kCGBlendModeDestinationIn能保留透明度信息,那就兩個blendMode都用不就完事兒了麼~嘗試之,如果在blend繪圖時不是kCGBlendModeDestinationIn模式的話,則再用kCGBlendModeDestinationIn畫一次:

[objc] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. //  UIImage+Tint.m  
  2.   
  3. #import "UIImage+Tint.h"  
  4.   
  5. @implementation UIImage (Tint)  
  6. - (UIImage *) imageWithTintColor:(UIColor *)tintColor  
  7. {  
  8.     return [self imageWithTintColor:tintColor blendMode:kCGBlendModeDestinationIn];  
  9. }  
  10.   
  11. - (UIImage *) imageWithGradientTintColor:(UIColor *)tintColor  
  12. {  
  13.     return [self imageWithTintColor:tintColor blendMode:kCGBlendModeOverlay];  
  14. }  
  15.   
  16. - (UIImage *) imageWithTintColor:(UIColor *)tintColor blendMode:(CGBlendMode)blendMode  
  17. {  
  18.     //We want to keep alpha, set opaque to NO; Use 0.0f for scale to use the scale factor of the device’s main screen.  
  19.     UIGraphicsBeginImageContextWithOptions(self.sizeNO0.0f);  
  20.     [tintColor setFill];  
  21.     CGRect bounds = CGRectMake(00self.size.widthself.size.height);  
  22.     UIRectFill(bounds);  
  23.   
  24.     //Draw the tinted image in context  
  25.     [self drawInRect:bounds blendMode:blendMode alpha:1.0f];  
  26.   
  27.     if (blendMode != kCGBlendModeDestinationIn) {  
  28.         [self drawInRect:bounds blendMode:kCGBlendModeDestinationIn alpha:1.0f];  
  29.     }  
  30.   
  31.     UIImage *tintedImage = UIGraphicsGetImageFromCurrentImageContext();  
  32.     UIGraphicsEndImageContext();  
  33.   
  34.     return tintedImage;  
  35. }  
  36.   
  37. @end  

結果如下:


已經很完美了,這樣的話只要在代碼裏設定一下顏色,我們就能夠很輕易地使用同樣一套UI,將其blend爲需要的顏色,來實現素材的重用了。唯一需要注意的是,因爲每次使用UIImage+Tint的方法繪圖時,都使用了CG的繪製方法,這就意味着每次調用都會是用到CPU的Offscreen drawing,大量使用的話可能導致性能的問題(主要對於iPhone 3GS或之前的設備,可能同時處理大量這樣的繪製調用的能力會有不足)。關於CA和CG的性能的問題,打算在之後用一篇文章來介紹一下。對於這裏的UIImage+Tint的實現,可以寫一套緩存的機制,來確保大量重複的元素只在load的時候blend一次,之後將其緩存在內存中以快速讀取。當然這是一個權衡的問題,在時間和空間中做出正確的平衡和選擇,也正是程序設計的樂趣所在。

這篇文章中作爲示例的工程和UIImage+Tint可以在Github上找到,您可以隨意玩弄..我相信也會是個來研究每種blend的特性的好機會~


轉載自:http://blog.csdn.net/hopedark/article/details/17761177


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章