《iOS Drawing Practical UIKit Solutions》讀書筆記(四) —— 遮罩,模糊和動畫
遮罩,模糊和動畫會為我們的APP增色不少,現在,就讓我們瞭解一下吧。
用Blocks繪製Images
利用下面工具函式,可以簡化建立image的過程。
typedef void(^DrawingStateBlock)();
UIImage * DrawIntoImage(CGSize size, DrawingStateBlock block) {
UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);
if (block) {
block();
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
簡單的遮罩
我們可以使用Core Animation中的CAShapeLayer結合maskLayer對view進行裁剪。而這裡,則介紹了Core Graphics中如何裁剪圖片
上面的圖片,僅在黑色圓環內的內容才能夠被顯示。
我們可以通過Quartz或UIKit來實現,比如通過CGContextClip()或呼叫UIBezierPath物件的addClip方法。
下面,我們通過addClip方法來實現這種遮罩效果:
- (UIImage *)buildSampleMaskImage {
CGSize targetSize = CGSizeMake(200 , 200);
CGRect targetRect = CGRectMake(0, 0, 200, 200);
UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0.0);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:targetRect];
UIBezierPath *innerPath = [UIBezierPath bezierPathWithOvalInRect:RectInsetByPercent(targetRect, 0.4)];
[path appendPath:innerPath];
path.usesEvenOddFillRule = YES;
[path addClip];
UIImage *image = [UIImage imageNamed:@"Sample"];
[image drawInRect:targetRect];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
注意,當我們使用了addClip方法後,之後在Context中繪製的任何內容,都會被Clip path所限制。如果我們的Clip只是針對部分內容,可以在addClip前先saveContextState(CGContextSaveGState),在之後restoreContextState(CGContextRestoreGState)即可。
用Mask Image裁剪圖片
當我們要裁剪的圖片形狀較為複雜時,我們可以使用mask image對Context進行修改稿,進而達到繪製在Context中的圖片被裁剪的效果。注意,這裡要使用的mask image必須是灰度圖才有效。iOS會自動根據圖片的灰度值對原圖進行過濾(越黑,約不顯示,越白,則越現實)。
運用mask image需要呼叫Quartz方法
void CGContextClipToMask(CGContextRef c, CGRect rect, CGImageRef mask)
//第一個引數表示context 指標
//第二個引數表示clip到context的區域,也是mask 圖片對映到context的區域
//第三個引數表示mask的圖片,對於裁剪區域Rect中的點是否變化取決於mask圖中的alpha值,若alpha為0,則對應clip rect中的點為透明,如果alpha為1,則對應clip Rect中的點無變化。
- (UIImage *)buildSampleMaskImage {
CGSize targetSize = CGSizeMake(200, 200);
CGRect targetRect = CGRectMake(0, 0, 200, 200);
UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0.0);
// UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:targetRect];
// UIBezierPath *innerPath = [UIBezierPath bezierPathWithOvalInRect:RectInsetByPercent(targetRect, 0.4)];
// [path appendPath:innerPath];
// path.usesEvenOddFillRule = YES;
// [path addClip];
UIImage *maskImage = [UIImage imageNamed:@"MaskImg"];
CGContextClipToMask(UIGraphicsGetCurrentContext(), targetRect, maskImage.CGImage);
UIImage *image = [UIImage imageNamed:@"Sample"];
[image drawInRect:targetRect];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
mask image:
效果:
注意,我們這裡使用了暱圖網提供的灰度圖,可以發現,暱圖網的logo本來是在圖片下方的,而現在整個的翻轉到圖片上方。這是因為,我們使用的是Quartz方法,而Quartz的座標系和UIKit的座標系是相反的,因此,在使用
CGContextClipToMask
方法時,我們需要現將UIKit的Context進行翻轉,然後在轉回來:
void FlipContextVertically(CGSize size)
{
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL)
{
NSLog(@"Error: No context to flip");
return;
}
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformScale(transform, 1.0f, -1.0f);
transform = CGAffineTransformTranslate(transform, 0.0f, -size.height);
CGContextConcatCTM(context, transform);
}
- (UIImage *)buildSampleMaskImage {
CGSize targetSize = CGSizeMake(200, 200);
CGRect targetRect = CGRectMake(0, 0, 200, 200);
UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0.0);
UIImage *maskImage = [UIImage imageNamed:@"MaskImg"];
FlipContextVertically(targetSize); // 先翻轉
CGContextClipToMask(UIGraphicsGetCurrentContext(), targetRect, maskImage.CGImage);
FlipContextVertically(targetSize); // clip操作之後,再轉回來
UIImage *image = [UIImage imageNamed:@"Sample"];
[image drawInRect:targetRect];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
經過調整後,所得到的結果就是正確的了:
模糊
模糊效果能夠給我們的APP帶來一種朦朧美,在iOS中,為我們提供瞭如下方法來實現朦朧效果:
- Core Image API
- vImage
- iOS 7之後提供的UIKit方法
Core Image API
Core Image API 的介面相對明確,比較好理解,主要使用到了模糊blur濾鏡。
iOS5.0之後就出現了Core Image的API,Core Image的API被放在CoreImage.framework庫中, 在iOS和OS X平臺上,Core Image都提供了大量的濾鏡(Filter),在OS X上有120多種Filter,而在iOS上也有90多。
原圖
- (UIImage *)coreBlurImage:(UIImage *)sourceImage withBlurNumber:(NSNumber *)blur {
CIContext *context = [CIContext context];
CIImage *inputImage = [CIImage imageWithCGImage:sourceImage.CGImage];
// 設定濾鏡
CIFilter *blurFilter = [CIFilter filterWithName:@"CIGaussianBlur"];
[blurFilter setValue:inputImage forKey:kCIInputImageKey];
[blurFilter setValue:blur forKey:@"inputRadius"];
CIImage *result = [blurFilter valueForKey:kCIOutputImageKey];
CGImageRef outPutImage = [context createCGImage:result fromRect:[result extent]];
UIImage *retImage = [UIImage imageWithCGImage:outPutImage];
CGImageRelease(outPutImage);
return retImage;
}
blur值為7時的模糊效果:
vImage
vImage屬於Accelerate.Framework,需要匯入 Accelerate下的 Accelerate標頭檔案, Accelerate主要是用來做數字訊號處理、影象處理相關的向量、矩陣運算的庫。影象可以認為是由向量或者矩陣資料構成的,Accelerate裡既然提供了高效的數學運算API,自然就能方便我們對影象做各種各樣的處理 ,模糊演算法使用的是vImageBoxConvolve_ARGB8888這個函式。
作者:零距離仰望星空
連結:https://www.jianshu.com/p/6dd0eab888a6
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
vImage用起來比較繁瑣,網路上摘抄一段,大概瞭解一下就好:
+(UIImage *)boxblurImage:(UIImage *)image withBlurNumber:(CGFloat)blur
{
if (blur < 0.f || blur > 1.f) {
blur = 0.5f;
}
int boxSize = (int)(blur * 40);
boxSize = boxSize - (boxSize % 2) + 1;
CGImageRef img = image.CGImage;
vImage_Buffer inBuffer, outBuffer;
vImage_Error error;
void *pixelBuffer;
//從CGImage中獲取資料
CGDataProviderRef inProvider = CGImageGetDataProvider(img);
CFDataRef inBitmapData = CGDataProviderCopyData(inProvider);
//設定從CGImage獲取物件的屬性
inBuffer.width = CGImageGetWidth(img);
inBuffer.height = CGImageGetHeight(img);
inBuffer.rowBytes = CGImageGetBytesPerRow(img);
inBuffer.data = (void*)CFDataGetBytePtr(inBitmapData);
pixelBuffer = malloc(CGImageGetBytesPerRow(img) * CGImageGetHeight(img));
if(pixelBuffer == NULL)
NSLog(@"No pixelbuffer");
outBuffer.data = pixelBuffer;
outBuffer.width = CGImageGetWidth(img);
outBuffer.height = CGImageGetHeight(img);
outBuffer.rowBytes = CGImageGetBytesPerRow(img);
error = vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, NULL, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
if (error) {
NSLog(@"error from convolution %ld", error);
}
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef ctx = CGBitmapContextCreate( outBuffer.data, outBuffer.width, outBuffer.height, 8, outBuffer.rowBytes, colorSpace, kCGImageAlphaNoneSkipLast);
CGImageRef imageRef = CGBitmapContextCreateImage (ctx);
UIImage *returnImage = [UIImage imageWithCGImage:imageRef];
//clean up CGContextRelease(ctx);
CGColorSpaceRelease(colorSpace);
free(pixelBuffer);
CFRelease(inBitmapData);
CGColorSpaceRelease(colorSpace);
CGImageRelease(imageRef);
return returnImage;
}
UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(0, 300, SCREENWIDTH, 100)];
imageView.contentMode=UIViewContentModeScaleAspectFill;
imageView.image=[UIImage boxblurImage:image withBlurNumber:0.5];
imageView.clipsToBounds=YES;
[self.view addSubview:imageView];
作者:零距離仰望星空
連結:https://www.jianshu.com/p/6dd0eab888a6
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
UIKit
在iOS 7之後,系統預設的UI介面大量使用了模糊效果,如下拉提醒框後面的背景。
因此,Apple同樣在UIKit中,加入了支援模糊效果的方法。
UIToolBar
UIToolbar的列舉樣式:
typedef NS_ENUM(NSInteger, UIBarStyle) {
UIBarStyleDefault = 0,
UIBarStyleBlack = 1,
UIBarStyleBlackOpaque = 1, // Deprecated. Use UIBarStyleBlack
UIBarStyleBlackTranslucent = 2, // Deprecated. Use UIBarStyleBlack and set the translucent property to YES
}
UIVisualEffectView
在iOS8之後,Apple新增的新類UIVisualEffectView,用於支援快速的建立毛玻璃效果。
CGRect screenRect = [[UIScreen mainScreen] bounds];
//新增待模糊的圖片檢視
UIImageView * imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"image"]];
[imageView setFrame:screenRect];
[self.view addSubview:imageView];
// 生成特定樣式的模糊效果
UIBlurEffect * blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark];
// 根據模糊效果生成模糊檢視
UIVisualEffectView * effectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
// 設定模糊區域大小
[effectView setFrame:screenRect];
[self.imageView addSubview:effectView];
這裡主要是設定UIBlurEffect,共有六種:
typedef NS_ENUM(NSInteger, UIBlurEffectStyle) {
UIBlurEffectStyleExtraLight,
UIBlurEffectStyleLight,
UIBlurEffectStyleDark,
UIBlurEffectStyleExtraDark __TVOS_AVAILABLE(10_0) __IOS_PROHIBITED __WATCHOS_PROHIBITED,
UIBlurEffectStyleRegular NS_ENUM_AVAILABLE_IOS(10_0), // Adapts to user interface style
UIBlurEffectStyleProminent NS_ENUM_AVAILABLE_IOS(10_0), // Adapts to user interface style
} NS_ENUM_AVAILABLE_IOS(8_0);
Animation
Creating Display Links for Drawing
繪製動畫的原理在於週期性的改變畫面的內容。如果這個週期太慢,則會讓人產生卡頓的感覺。
那麼,我們採用什麼來計算這個週期呢?推薦使用CADisplayLink,而不是NSTimer。
CADisplayLink屬於QuartzCore framework,我們需要將DisplayLink新增到runloop上。
為什麼不用NSTimer?
相比NSTimer,CADisplayLink有如下優點:
- CADisplayLink是和螢幕重新整理率相關的,能夠提供理想的動畫間隔。
- 相比NSTimer,CADisplayLink更為精確,因為根據Apple文件所述,NSTimer在當前執行緒忙碌的情況下,可能會跳過這次scheduled fire,而大大滯後於既定的間隔。
CADisplayLink
結合Core Graphics,我們可以不斷的更新所繪製的內容來達到動畫效果:
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self.view selector:@selector(setNeedsDisplay)];
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
當我們link每次fire時,會呼叫UIView的setNeedsDisplay方法,而該方法會讓iOS跟新當前的View內容,即呼叫drawRect方法。
預設的,link和螢幕的重新整理頻率一致(在表現良好的UI重新整理率下,是60fps)。我們可以設定CADisplayLink的屬性
link.preferredFramesPerSecond = 2
PreferredFramesPerSecond:設定每秒多少幀,CADisplayLink預設每秒執行60次,通過它的PreferredFramesPerSecond屬性改變每秒執行幀數,如設定為2,意味CADisplayLink每隔一幀執行一次,有效的邏輯每秒執行30次。
當我們不需要再使用CADisplayLink時,需要將其與runloop解綁銷燬,可呼叫如下函式:
/* Removes the receiver from the given mode of the runloop. This will
* implicitly release it when removed from the last mode it has been
* registered for. */
- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
/* Removes the object from all runloop modes (releasing the receiver if
* it has been implicitly retained) and releases the 'target' object. */
- (void)invalidate;