美文网首页iOS
Core Animation 高级技巧(四)视觉效果

Core Animation 高级技巧(四)视觉效果

作者: 豆瓣菜 | 来源:发表于2017-04-22 22:53 被阅读21次

    我们在第三章图层几何学中讨论了图层的frame,第二章寄宿图则讨论了图层的寄宿图。但是图层不仅仅可以是图片或是颜色的容器;还有一系列内建的特性使得创造美丽优雅的令人深刻的界面元素成为可能。在这一章,我们将会探索一些能够通过使用CALayer属性实现的视觉效果。

    圆角

    圆角矩形是iOS的一个标志性审美特性。这在iOS的每一个地方都得到了体现,不论是主屏幕图标,还是警告弹框,甚至是文本框。按照这流行程度,你可能会认为一定有不借助Photoshop就能轻易创建圆角矩形的方法。恭喜你,猜对了。

    CALayer有一个叫做conrnerRadius的属性控制着图层角的曲率。它是一个浮点数,默认为0(为0的时候就是直角),但是你可以把它设置成任意值。默认情况下,这个曲率值只影响背景颜色而不影响背景图片或是子图层。不过,如果把masksToBounds设置成YES的话,图层里面的所有东西都会被截取。

    我们可以通过一个简单的项目来演示这个效果。在Interface Builder中,我们放置一些视图,他们有一些子视图。而且这些子视图有一些超出了边界(如图4.1)。你可能无法看到他们超出了边界,因为在编辑界面的时候,超出的部分总是被Interface Builder裁切掉了。不过,你相信我就好了

    4.1.png

    图4.1 两个白色的大视图,他们都包含了小一些的红色视图。

    然后在代码中,我们设置角的半径为20个点,并裁剪掉第一个视图的超出部分(见清单4.1)。技术上来说,这些属性都可以在Interface Builder的探测板中分别通过『用户定义运行时属性』和勾选『裁剪子视图』(Clip Subviews)选择框来直接设置属性的值。不过,在这个示例中,代码能够表示得更清楚。图4.2是运行代码的结果

    清单4.1 设置cornerRadiusmasksToBounds

    @interface ViewController ()
    
    @property (nonatomic, weak) IBOutlet UIView *layerView1;
    @property (nonatomic, weak) IBOutlet UIView *layerView2;
    
    @end
    
    @implementation ViewController
    - (void)viewDidLoad
    {
      [super viewDidLoad];
    
      //set the corner radius on our layers
      self.layerView1.layer.cornerRadius = 20.0f;
      self.layerView2.layer.cornerRadius = 20.0f;
    
      //enable clipping on the second layer
      self.layerView2.layer.masksToBounds = YES;
    }
    @end
    
    4.2.png

    右图中,红色的子视图沿角半径被裁剪了

    如你所见,右边的子视图沿边界被裁剪了。

    单独控制每个层的圆角曲率也不是不可能的。如果想创建有些圆角有些直角的图层或视图时,你可能需要一些不同的方法。比如使用一个图层蒙板(本章稍后会讲到)或者是CAShapeLayer(见第六章『专用图层』)。

    图层边框

    CALayer另外两个非常有用属性就是borderWidthborderColor。二者共同定义了图层边的绘制样式。这条线(也被称作stroke)沿着图层的bounds绘制,同时也包含图层的角。

    borderWidth是以点为单位的定义边框粗细的浮点数,默认为0.borderColor定义了边框的颜色,默认为黑色。

    borderColorCGColorRef类型,而不是UIColor,所以它不是Cocoa的内置对象。不过呢,你肯定也清楚图层引用了borderColor,虽然属性声明并不能证明这一点。CGColorRef在引用/释放时候的行为表现得与NSObject极其相似。但是Objective-C语法并不支持这一做法,所以CGColorRef属性即便是强引用也只能通过assign关键字来声明。

    边框是绘制在图层边界里面的,而且在所有子内容之前,也在子图层之前。如果我们在之前的示例中(清单4.2)加入图层的边框,你就能看到到底是怎么一回事了(如图4.3).

    清单4.2 加上边框

    @implementation ViewController
    
    - (void)viewDidLoad
    {
      [super viewDidLoad];
    
      //set the corner radius on our layers
      self.layerView1.layer.cornerRadius = 20.0f;
      self.layerView2.layer.cornerRadius = 20.0f;
    
      //add a border to our layers
      self.layerView1.layer.borderWidth = 5.0f;
      self.layerView2.layer.borderWidth = 5.0f;
    
      //enable clipping on the second layer
      self.layerView2.layer.masksToBounds = YES;
    }
    
    @end
    
    4.3.png

    图4.3 给图层增加一个边框

    仔细观察会发现边框并不会把寄宿图或子图层的形状计算进来,如果图层的子图层超过了边界,或者是寄宿图在透明区域有一个透明蒙板,边框仍然会沿着图层的边界绘制出来(如图4.4).

    4.4.png

    图4.4 边框是跟随图层的边界变化的,而不是图层里面的内容

    阴影

    iOS的另一个常见特性呢,就是阴影。阴影往往可以达到图层深度暗示的效果。也能够用来强调正在显示的图层和优先级(比如说一个在其他视图之前的弹出框),不过有时候他们只是单纯的装饰目的。

    shadowOpacity属性一个大于默认值(也就是0)的值,阴影就可以显示在任意图层之下。shadowOpacity是一个必须在0.0(不可见)和1.0(完全不透明)之间的浮点数。如果设置为1.0,将会显示一个有轻微模糊的黑色阴影稍微在图层之上。若要改动阴影的表现,你可以使用CALayer的另外三个属性:shadowColorshadowOffsetshadowRadius

    显而易见,shadowColor属性控制着阴影的颜色,和borderColorbackgroundColor一样,它的类型也是CGColorRef。阴影默认是黑色,大多数时候你需要的阴影也是黑色的(其他颜色的阴影看起来是不是有一点点奇怪。。)。

    shadowOffset属性控制着阴影的方向和距离。它是一个CGSize的值,宽度控制着阴影横向的位移,高度控制着纵向的位移。shadowOffset的默认值是 {0, -3},意即阴影相对于Y轴有3个点的向上位移。

    为什么要默认向上的阴影呢?尽管Core Animation是从图层套装演变而来(可以认为是为iOS创建的私有动画框架),但是呢,它却是在Mac OS上面世的,前面有提到,二者的Y轴是颠倒的。这就导致了默认的3个点位移的阴影是向上的。在Mac上,shadowOffset的默认值是阴影向下的,这样你就能理解为什么iOS上的阴影方向是向上的了(如图4.5).

    4.5.png

    图4.5 在iOS(左)和Mac OS(右)上shadowOffset的表现。

    苹果更倾向于用户界面的阴影应该是垂直向下的,所以在iOS把阴影宽度设为0,然后高度设为一个正值不失为一个做法。

    shadowRadius属性控制着阴影的模糊度,当它的值是0的时候,阴影就和视图一样有一个非常确定的边界线。当值越来越大的时候,边界线看上去就会越来越模糊和自然。苹果自家的应用设计更偏向于自然的阴影,所以一个非零值再合适不过了。

    通常来讲,如果你想让视图或控件非常醒目独立于背景之外(比如弹出框遮罩层),你就应该给shadowRadius设置一个稍大的值。阴影越模糊,图层的深度看上去就会更明显(如图4.6).

    4.6.png

    图4.6 大一些的阴影位移和角半径会增加图层的深度即视感

    阴影裁剪

    和图层边框不同,图层的阴影继承自内容的外形,而不是根据边界和角半径来确定。为了计算出阴影的形状,Core Animation会将寄宿图(包括子视图,如果有的话)考虑在内,然后通过这些来完美搭配图层形状从而创建一个阴影(见图4.7)。

    4.7.png

    图4.7 阴影是根据寄宿图的轮廓来确定的

    当阴影和裁剪扯上关系的时候就有一个头疼的限制:阴影通常就是在Layer的边界之外,如果你开启了masksToBounds属性,所有从图层中突出来的内容都会被才剪掉。如果我们在我们之前的边框示例项目中增加图层的阴影属性时,你就会发现问题所在(见图4.8).

    4.8.png

    图4.8 maskToBounds属性裁剪掉了阴影和内容

    从技术角度来说,这个结果是可以理解的,但确实又不是我们想要的效果。如果你想沿着内容裁切,你需要用到两个图层:一个只画阴影的空的外图层,和一个用masksToBounds裁剪内容的内图层。

    如果我们把之前项目的右边用单独的视图把裁剪的视图包起来,我们就可以解决这个问题(如图4.9).

    4.9.png

    图4.9 右边,用额外的阴影转换视图包裹被裁剪的视图

    我们只把阴影用在最外层的视图上,内层视图进行裁剪。清单4.3是代码实现,图4.10是运行结果。

    清单4.3 用一个额外的视图来解决阴影裁切的问题

    @interface ViewController ()
    
    @property (nonatomic, weak) IBOutlet UIView *layerView1;
    @property (nonatomic, weak) IBOutlet UIView *layerView2;
    @property (nonatomic, weak) IBOutlet UIView *shadowView;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad
    {
      [super viewDidLoad];
    
      //set the corner radius on our layers
      self.layerView1.layer.cornerRadius = 20.0f;
      self.layerView2.layer.cornerRadius = 20.0f;
    
      //add a border to our layers
      self.layerView1.layer.borderWidth = 5.0f;
      self.layerView2.layer.borderWidth = 5.0f;
    
      //add a shadow to layerView1
      self.layerView1.layer.shadowOpacity = 0.5f;
      self.layerView1.layer.shadowOffset = CGSizeMake(0.0f, 5.0f);
      self.layerView1.layer.shadowRadius = 5.0f;
    
      //add same shadow to shadowView (not layerView2)
      self.shadowView.layer.shadowOpacity = 0.5f;
      self.shadowView.layer.shadowOffset = CGSizeMake(0.0f, 5.0f);
      self.shadowView.layer.shadowRadius = 5.0f;
    
      //enable clipping on the second layer
      self.layerView2.layer.masksToBounds = YES;
    }
    
    @end
    
    4.10.png

    图4.10 右边视图,不受裁切阴影的阴影视图。

    shadowPath属性

    我们已经知道图层阴影并不总是方的,而是从图层内容的形状继承而来。这看上去不错,但是实时计算阴影也是一个非常消耗资源的,尤其是图层有多个子图层,每个图层还有一个有透明效果的寄宿图的时候。

    如果你事先知道你的阴影形状会是什么样子的,你可以通过指定一个shadowPath来提高性能。shadowPath是一个CGPathRef类型(一个指向CGPath的指针)。CGPath是一个Core Graphics对象,用来指定任意的一个矢量图形。我们可以通过这个属性单独于图层形状之外指定阴影的形状。

    图4.11 展示了同一寄宿图的不同阴影设定。如你所见,我们使用的图形很简单,但是它的阴影可以是你想要的任何形状。清单4.4是代码实现。

    4.11.png

    图4.11 用shadowPath指定任意阴影形状

    清单4.4 创建简单的阴影形状

    @interface ViewController ()
    
    @property (nonatomic, weak) IBOutlet UIView *layerView1;
    @property (nonatomic, weak) IBOutlet UIView *layerView2;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad
    {
      [super viewDidLoad];
    
      //enable layer shadows
      self.layerView1.layer.shadowOpacity = 0.5f;
      self.layerView2.layer.shadowOpacity = 0.5f;
    
      //create a square shadow
      CGMutablePathRef squarePath = CGPathCreateMutable();
      CGPathAddRect(squarePath, NULL, self.layerView1.bounds);
      self.layerView1.layer.shadowPath = squarePath; 
      CGPathRelease(squarePath);
    
      //create a circular shadow
      CGMutablePathRef circlePath = CGPathCreateMutable();
      CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds);
      self.layerView2.layer.shadowPath = circlePath; 
      CGPathRelease(circlePath);
    }
    @end
    

    如果是一个矩形或者是圆,用CGPath会相当简单明了。但是如果是更加复杂一点的图形,UIBezierPath类会更合适,它是一个由UIKit提供的在CGPath基础上的Objective-C包装类。

    图层蒙板

    通过masksToBounds属性,我们可以沿边界裁剪图形;通过cornerRadius属性,我们还可以设定一个圆角。但是有时候你希望展现的内容不是在一个矩形或圆角矩形。比如,你想展示一个有星形框架的图片,又或者想让一些古卷文字慢慢渐变成背景色,而不是一个突兀的边界。

    使用一个32位有alpha通道的png图片通常是创建一个无矩形视图最方便的方法,你可以给它指定一个透明蒙板来实现。但是这个方法不能让你以编码的方式动态地生成蒙板,也不能让子图层或子视图裁剪成同样的形状。

    CALayer有一个属性叫做mask可以解决这个问题。这个属性本身就是个CALayer类型,有和其他图层一样的绘制和布局属性。它类似于一个子图层,相对于父图层(即拥有该属性的图层)布局,但是它却不是一个普通的子图层。不同于那些绘制在父图层中的子图层,mask图层定义了父图层的部分可见区域。

    mask图层的Color属性是无关紧要的,真正重要的是图层的轮廓。mask属性就像是一个饼干切割机,mask图层实心的部分会被保留下来,其他的则会被抛弃。(如图4.12)

    如果mask图层比父图层要小,只有在mask图层里面的内容才是它关心的,除此以外的一切都会被隐藏起来。

    4.12.png

    图4.12 把图片和蒙板图层作用在一起的效果

    我们将代码演示一下这个过程,创建一个简单的项目,通过图层的mask属性来作用于图片之上。为了简便一些,我们用Interface Builder来创建一个包含UIImageView的图片图层。这样我们就只要代码实现蒙板图层了。清单4.5是最终的代码,图4.13是运行后的结果。

    清单4.5 应用蒙板图层

    @interface ViewController ()
    
    @property (nonatomic, weak) IBOutlet UIImageView *imageView;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad
    {
      [super viewDidLoad];
    
      //create mask layer
      CALayer *maskLayer = [CALayer layer];
      maskLayer.frame = self.imageView.bounds;
      UIImage *maskImage = [UIImage imageNamed:@"Cone.png"];
      maskLayer.contents = (__bridge id)maskImage.CGImage;
    
      //apply mask to image layer
      self.imageView.layer.mask = maskLayer;
    }
    @end
    
    4.13.png

    图4.13 使用了mask之后的UIImageView

    CALayer蒙板图层真正厉害的地方在于蒙板图不局限于静态图。任何有图层构成的都可以作为mask属性,这意味着你的蒙板可以通过代码甚至是动画实时生成。

    拉伸过滤

    最后我们再来谈谈minificationFiltermagnificationFilter属性。总得来讲,当我们视图显示一个图片的时候,都应该正确地显示这个图片(意即:以正确的比例和正确的1:1像素显示在屏幕上)。原因如下:

    • 能够显示最好的画质,像素既没有被压缩也没有被拉伸。
    • 能更好的使用内存,因为这就是所有你要存储的东西。
    • 最好的性能表现,CPU不需要为此额外的计算。

    不过有时候,显示一个非真实大小的图片确实是我们需要的效果。比如说一个头像或是图片的缩略图,再比如说一个可以被拖拽和伸缩的大图。这些情况下,为同一图片的不同大小存储不同的图片显得又不切实际。

    当图片需要显示不同的大小的时候,有一种叫做拉伸过滤的算法就起到作用了。它作用于原图的像素上并根据需要生成新的像素显示在屏幕上。

    事实上,重绘图片大小也没有一个统一的通用算法。这取决于需要拉伸的内容,放大或是缩小的需求等这些因素。CALayer为此提供了三种拉伸过滤方法,他们是:

    • kCAFilterLinear
    • kCAFilterNearest
    • kCAFilterTrilinear

    minification(缩小图片)和magnification(放大图片)默认的过滤器都是kCAFilterLinear,这个过滤器采用双线性滤波算法,它在大多数情况下都表现良好。双线性滤波算法通过对多个像素取样最终生成新的值,得到一个平滑的表现不错的拉伸。但是当放大倍数比较大的时候图片就模糊不清了。

    kCAFilterTrilinearkCAFilterLinear非常相似,大部分情况下二者都看不出来有什么差别。但是,较双线性滤波算法而言,三线性滤波算法存储了多个大小情况下的图片(也叫多重贴图),并三维取样,同时结合大图和小图的存储进而得到最后的结果。

    这个方法的好处在于算法能够从一系列已经接近于最终大小的图片中得到想要的结果,也就是说不要对很多像素同步取样。这不仅提高了性能,也避免了小概率因舍入错误引起的取样失灵的问题

    4.21.png

    图4.21 修正后的图

    总结

    这一章介绍了一些可以通过代码应用到图层上的视觉效果,比如圆角,阴影和蒙板。我们也了解了拉伸过滤器和组透明。

    在第五章,『变换』中,我们将会研究图层变化和3D转换。

    相关文章

      网友评论

        本文标题:Core Animation 高级技巧(四)视觉效果

        本文链接:https://www.haomeiwen.com/subject/yglszttx.html