《iOS Drawing Practical UIKit Solutions》读书笔记(四) —— 遮罩,模糊和动画

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013378438/article/details/79676082

遮罩,模糊和动画会为我们的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;

猜你喜欢

转载自blog.csdn.net/u013378438/article/details/79676082