1. 程式人生 > >《iOS Drawing Practical UIKit Solutions》讀書筆記(四) —— 遮罩,模糊和動畫

《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中,為我們提供瞭如下方法來實現朦朧效果:

  1. Core Image API
  2. vImage
  3. 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

繪製動畫的原理在於週期性的改變畫面的內容。如果這個週期太慢,則會讓人產生卡頓的感覺。

那麼,我們採用什麼來計算這個週期呢?推薦使用CADisplayLink,而不是NSTimer。
CADisplayLink屬於QuartzCore framework,我們需要將DisplayLink新增到runloop上。

為什麼不用NSTimer?

相比NSTimer,CADisplayLink有如下優點:

  1. CADisplayLink是和螢幕重新整理率相關的,能夠提供理想的動畫間隔。
  2. 相比NSTimer,CADisplayLink更為精確,因為根據Apple文件所述,NSTimer在當前執行緒忙碌的情況下,可能會跳過這次scheduled fire,而大大滯後於既定的間隔。

結合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;