從本文開始,我將專門開闢一個Github Code系列,開源本身寫的一部分有意思並且實用的demo,共同窗習。之前都發布在git OSChina上,後面有空會陸陸續續整理到Github上。OSChina最大的優勢是能夠免費託管私有項目,服務器在國內速度快,這些是Github所比不了的。不過Github優點在於開源氛圍濃烈,有利於向各位開源大牛學習交流。長話短說,just do it!html
本文只要實現隨滾動實時模糊(或稱爲動態模糊)的效果(請見文章末尾處),這種效果被普遍應用於各大天氣類APP中,如墨跡天氣、黃曆天氣、雅虎天氣等。隨着scrollView向上滾動,背景逐漸模糊,兩者是相互聯動的,實現起來很簡單。在文章的後半部分咱們嘗試用兩種方法實現這種效果。不過我要強調的一點是,個人一些文章的方法思路雖簡單,但我更注重這個過程當中對一些「坑」的處理,這是個融匯貫通的過程。好比預告一下,本文提到了幾個坑:KVO陷阱、autoRelease坑、drawRect坑...git
首先,咱們利用KVO將scrollView的contentOffset與Image的模糊度進行綁定,這樣咱們就能實時檢測到scrollview(本文爲tableview)的滾動偏移量,從而改變image的模糊度。github
在此以前,咱們從UIImageView派生出子類WZLLiveBlurImageView來實現圖像的模糊改變,先看頭文件無論實現:算法
1 #import <UIKit/UIKit.h> 2 //default initial blur level 3 #define kImageBlurLevelDefault 0.9f 4 @interface WZLLiveBlurImageView : UIImageView 5 6 /** 7 * set blur level 8 * 9 * @param level blur level 10 */ 11 - (void)setBlurLevel:(CGFloat)level; 12 13 @end
setBlurLevel:是惟一的方法接口給調用者調用。接着咱們在一個ViewController中把這個WZLLiveBlurImageView顯示出來。在ViewController中還應該有一個tableView用於模擬天氣類應用的使用場景。那WZLLiveBlurImageView要放在哪裏比較合適呢?tableView有一個subView叫作backgroundView,就選它了。如下是初始化代碼,放在ViewController的viewDidLoad方法中:編程
1 //generate item content for tableView 2 _items = [self items]; 3 self.tableView.dataSource = self; 4 self.tableView.delegate = self; 5 self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; 6 self.tableView.separatorColor = [UIColor clearColor]; 7 _blurImgView = [[WZLLiveBlurImageView alloc] initWithImage:[UIImage imageNamed:@"bg.jpg"]]; 8 _blurImgView.frame = self.tableView.frame; 9 self.tableView.backgroundView = _blurImgView; 10 self.tableView.backgroundColor = [UIColor clearColor]; 11 self.tableView.contentInset = UIEdgeInsetsMake(CGRectGetHeight(self.tableView.bounds) - 100, 0, 0, 0);
那咱們怎麼知道tableview到底滾動了多少呢?答案是KVO。KVO的使用當中是有坑的,寫出一個健壯穩定的KVO須要注意不少細節,好比要當心崩潰問題,要防止KVO鏈斷開等...對KVO不熟的同窗請移步個人另外一篇博文:《KVO使用中的陷阱》。 緊接着上面的代碼,咱們繼續配置KVO,"contentOffset"字符串必須與tableview的屬性值徹底一致纔有效:api
1 //setup kvo on tableview`s contentoffset 2 [self.tableView addObserver:self forKeyPath:@"contentOffset" 3 options:NSKeyValueObservingOptionNew 4 context:(__bridge void *)(kWZLLiveBlurImageViewContext)];//kWZLLiveBlurImageViewContext是一個全局的字符串
註冊與註銷要成對出現,儘管是ARC,但在dealloc也要清理現場:服務器
1 - (void)dealloc 2 { 3 [self.tableView removeObserver:self forKeyPath:@"contentOffset" 4 context:(__bridge void *)kWZLLiveBlurImageViewContext]; 5 }
添加KVO的默認回調函數,當前類的全部KVO都走的這裏,建議KVO都寫成以下這樣,注意對super以及context的處理:微信
1 #pragma mark - KVO configuration 2 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 3 { 4 if (context == (__bridge void *)(kWZLLiveBlurImageViewContext)) { 5 CGFloat blurLevel = (self.tableView.contentInset.top + self.tableView.contentOffset.y) / CGRectGetHeight(self.tableView.bounds); 6 [_blurImgView setBlurLevel:blurLevel]; 7 } else { 8 [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 9 } 10 }
以上代碼基本就造成了實時模糊的總體框架了,如今還差WZLLiveBlurImageView中對模糊圖像的實現。對UIImage進行模糊處理的代碼網上有不少,基本都是同一個版本,這裏咱們就站在巨人的肩膀上,用一下這個算法,使用 - (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius; 方法對image進行模糊處理:app
1 // 2 // UIImage+Blur.h 3 // WZLLiveBlurImageView 4 // 5 // Created by zilin_weng on 15/3/23. 6 // Copyright (c) 2015年 Weng-Zilin. All rights reserved. 7 // 8 9 #import <UIKit/UIKit.h> 10 11 @interface UIImage (Blur) 12 13 - (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius; 14 15 @end
1 // 2 // UIImage+Blur.m 3 // WZLLiveBlurImageView 4 // 5 // Created by zilin_weng on 15/3/23. 6 // Copyright (c) 2015年 Weng-Zilin. All rights reserved. 7 // 8 9 #import "UIImage+Blur.h" 10 #import <Accelerate/Accelerate.h> 11 12 @implementation UIImage (Blur) 13 14 - (UIImage *)applyBlurWithRadius:(CGFloat)blur 15 { 16 if (blur < 0.f || blur > 1.f) { 17 blur = 0.5f; 18 } 19 int boxSize = (int)(blur * 40); 20 boxSize = boxSize - (boxSize % 2) + 1; 21 22 CGImageRef img = self.CGImage; 23 vImage_Buffer inBuffer, outBuffer; 24 vImage_Error error; 25 void *pixelBuffer; 26 27 //create vImage_Buffer with data from CGImageRef 28 CGDataProviderRef inProvider = CGImageGetDataProvider(img); 29 CFDataRef inBitmapData = CGDataProviderCopyData(inProvider); 30 31 inBuffer.width = CGImageGetWidth(img); 32 inBuffer.height = CGImageGetHeight(img); 33 inBuffer.rowBytes = CGImageGetBytesPerRow(img); 34 35 inBuffer.data = (void*)CFDataGetBytePtr(inBitmapData); 36 37 //create vImage_Buffer for output 38 pixelBuffer = malloc(CGImageGetBytesPerRow(img) * CGImageGetHeight(img)); 39 40 if(pixelBuffer == NULL) 41 NSLog(@"No pixelbuffer"); 42 43 outBuffer.data = pixelBuffer; 44 outBuffer.width = CGImageGetWidth(img); 45 outBuffer.height = CGImageGetHeight(img); 46 outBuffer.rowBytes = CGImageGetBytesPerRow(img); 47 48 // Create a third buffer for intermediate processing 49 /*void *pixelBuffer2 = malloc(CGImageGetBytesPerRow(img) * CGImageGetHeight(img)); 50 vImage_Buffer outBuffer2; 51 outBuffer2.data = pixelBuffer2; 52 outBuffer2.width = CGImageGetWidth(img); 53 outBuffer2.height = CGImageGetHeight(img); 54 outBuffer2.rowBytes = CGImageGetBytesPerRow(img);*/ 55 //perform convolution 56 error = vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, NULL, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend) 57 ?: vImageBoxConvolve_ARGB8888(&outBuffer, &inBuffer, NULL, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend) 58 ?: vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, NULL, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend); 59 60 if (error) { 61 NSLog(@"error from convolution %ld", error); 62 } 63 64 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 65 CGContextRef ctx = CGBitmapContextCreate(outBuffer.data, 66 outBuffer.width, 67 outBuffer.height, 68 8, 69 outBuffer.rowBytes, 70 colorSpace, 71 (CGBitmapInfo)kCGImageAlphaNoneSkipLast); 72 CGImageRef imageRef = CGBitmapContextCreateImage (ctx); 73 UIImage *returnImage = [UIImage imageWithCGImage:imageRef]; 74 //clean up 75 CGContextRelease(ctx); 76 CGColorSpaceRelease(colorSpace); 77 free(pixelBuffer); 78 //free(pixelBuffer2); 79 CFRelease(inBitmapData); 80 CGImageRelease(imageRef); 81 return returnImage; 82 } 83 84 85 86 @end
接下來有兩種思路可供參考:(1)直接法;(2)間接法框架
(1)直接法
從字面上看直接法能夠理解爲直接改變圖片的模糊度,也就是在KVO響應函數中根據tableView偏移量算出一個模糊目標值,每次目標值更新了,就作一次圖像模糊處理算法。這種方法簡單粗暴,思路直接。因爲滾動過程當中對模糊處理調用很是頻繁,這個時候要特別注意性能問題。在這裏影響性能問題主要有兩個因素:內存跟繪圖。若是內存在短期內不能及時釋放,則在小內存設備上將顯得很脆弱!在內存問題上,注意到上述圖像模糊算法返回的是一個autoRelease類型的對象,可是autoRelease對象並不會在做用域以外就自動釋放的(what?若是你以爲驚訝,說明你對autoRelease認識還不夠,建議你看看個人另外一篇文章:《你真的懂autoRelease嗎?》)。解決辦法就是使用autoReleasePool手動干預對象的釋放時機。內存問題解決了,接着是繪圖問題。不少文章都說使用比UIKIt更底層的CGGraphy繪圖能夠達到更高的效率,因而咱們寫出以下代碼:(函數內的單元檢測是必須的)
1 #import "WZLLiveBlurImageView.h" 2 #import "UIImage+Blur.h" 3 4 @interface WZLLiveBlurImageView () 5 { 6 UIImage *_originImage; 7 CGFloat _blurLevel; 8 } 9 @end 11 @implementation WZLLiveBlurImageView 18 - (void)setBlurLevel:(CGFloat)level 19 { 20 if (!self.image) { 21 NSLog(@"image is empty!"); 22 return; 23 } 24 _blurLevel = level; 25 [self setNeedsDisplay]; 26 } 27 28 #pragma mark - private apis 29 - (void)drawRect:(CGRect)rect 30 { 31 @autoreleasepool { 32 if (_originImage) { 33 UIImage *blurImg = [_originImage applyBlurWithRadius:_blurLevel]; 34 [blurImg drawInRect:self.bounds]; 35 } 36 } 37 } 38 @end
是否是以爲很完美,該考慮的都考慮到了。運行一下發現,圖像模糊度沒發生改變。爲啥?由於drawRect函數根本沒調用到!按command+shift+0打開APPLE文檔查看UIImageVIew發現如下的描述:
蘋果說的很清楚了,意思是UIImageView類已經作了優化,若是你子類化UIImageView是不會調用drawRect的!只有從UIView派生的類才容許走drawRect。雖然遇到了坑,不過咱們能夠放心地直接使用 self.image = xxxBlurImage; 這樣的方法來更新模糊圖像了。同時,爲了不 self.image = [_originImage applyBlurWithRadius:level]; 方法太耗時從而阻塞UI,咱們使用GCD去處理模糊操做,處理結束後回到主線程更新image。使用GCD過程當中要防止循環引用(點我點我),下面就是完整的代碼:
1 #import "WZLLiveBlurImageView.h" 2 #import "UIImage+Blur.h" 3 4 @interface WZLLiveBlurImageView () 5 @property (nonatomic, strong) UIImage *originImage; 6 @end 7 8 @implementation WZLLiveBlurImageView 9 /** 10 * set blur level 11 * 12 * @param level blur level 13 */ 14 - (void)setBlurLevel:(CGFloat)level 15 { 16 if (!self.image) { 17 NSLog(@"image is empty!"); 18 return; 19 } 20 level = (level > 1 ? 1 : (level < 0 ? 0 : level)); 21 NSLog(@"level:%@", @(level)); 22 //self.realBlurImageView.alpha = level; 23 @autoreleasepool { 24 if (_originImage) { 25 __weak typeof(WZLLiveBlurImageView*) weakSelf = self; 26 __block UIImage *blurImage = nil; 27 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 28 blurImage = [weakSelf.originImage applyBlurWithRadius:level]; 29 //get back to main thread to update UI 30 dispatch_async(dispatch_get_main_queue(), ^{ 31 weakSelf.image = blurImage; 32 }); 33 }); 34 } 35 } 36 }
缺點是:在低性能設備上雖然能夠實現滾動流暢,但因爲CPU性能捉急,致使tableView已經滾動了,image才慢慢模糊,這種延時給人不好的體驗。若是去掉GCD,則延時消失,但滾動卡頓嚴重。在寫這篇文章時最新設備是iphone6p,iphone4基本淘汰。用iphone4s真機調試時,直接法hold不住。固然,模擬器無壓力妥妥的,不過那又怎樣!附直接法在模擬器的效果,錄製過程失真了致使模糊效果看起來不天然:
========================
(2)間接法
既然直接法暫時作不了,那咱們能夠考慮曲線救國。個人策略是在WZLLiveBlurImageView上添加一個UIImageVIew,稱爲 realBlurImageView ,用 realBlurImageView 來實現真的模糊,可是隻模糊一次。在tableview滾動過程當中只改變 realBlurImageView 的透明度alpha。此法可謂移花接木。
1 #import "WZLLiveBlurImageView.h" 2 #import "UIImage+Blur.h" 3 4 @interface WZLLiveBlurImageView () 5 @property (nonatomic, strong) UIImage *originImage; 6 @property (nonatomic, strong) UIImageView *realBlurImageView; 7 @end 8 9 @implementation WZLLiveBlurImageView 10 11 - (void)setBlurLevel:(CGFloat)level 12 { 13 if (!self.image || !self.realBlurImageView) { 14 NSLog(@"image is empty!"); 15 return; 16 } 17 level = (level > 1 ? 1 : (level < 0 ? 0 : level)); 18 NSLog(@"level:%@", @(level)); 19 self.realBlurImageView.alpha = level; 20 } 21 22 #pragma mark - private apis 23 24 - (void)setImage:(UIImage *)image 25 { 26 [super setImage:image]; 27 if (_originImage == nil && image) { 28 _originImage = image; 29 } 30 if (!self.realBlurImageView) { 31 UIImage *blurImage = [image applyBlurWithRadius:kImageBlurLevelDefault]; 32 self.realBlurImageView = [[UIImageView alloc] initWithImage:blurImage]; 33 self.realBlurImageView.backgroundColor = [UIColor clearColor]; 34 self.realBlurImageView.frame = self.bounds; 35 self.realBlurImageView.alpha = 0; 36 [self addSubview:self.realBlurImageView]; 37 } 38 }
間接法雖然引入了新的對象,但換來了性能上的完美折中,我認爲是值得的。不論是模擬器仍是低性能真機,調試都OK,實時性很好:
咱們拋出的問題都完美解決了,也順帶解決了一些坑。方法雖簡單,但過程仍是比較有意義的。最後附上整個demo的github地址.
=======================================
原創文章,轉載請註明 編程小翁@博客園,郵件zilin_weng@163.com,微信Jilon,歡迎各位與我在C/C++/Objective-C/機器視覺等領域展開交流!
======================================