I.4 视觉特效

作者: liril | 来源:发表于2015-08-15 08:56 被阅读679次

    好吧,圆和椭圆是很好的,但圆角矩形如何?我们现在也可以那样做吧?——Steve Jobs

    我们在第3章“图层几何”讲解了图层帧,在第2章“主图像”讲解了图层的主图像。但图层不仅是包含颜色和图像的矩形容器;它同样有一堆用于程序化创建令人印象深刻的优雅界面元素的内置特性。在这一章中,我们将讲解许多使用CALayer属性所能实现的视觉特效。

    圆角

    使用圆角矩形(有圆角的矩形)是iOS美学中的一大显著特征。它们遍布iOS角角落落,从主屏的图标、模态的提醒到文本输入域。介于它们的流行你们可能猜测到,有不需要Photoshop帮助就能创建它们的简便方法。是的,你猜对了。

    CALayer有一个cornerRadius属性用于控制图层四角的弯曲。它是一个默认为0(直角)的浮点数,但可以被设置为任何你喜欢的值(用点设置)。默认情况下,这个弯曲只能影响图层的背影颜色,但不能影响主图像或子图层。然而,当masksToBounds属性被设置为YES(看第2章),图层中的一切都会被裁剪。

    我们可以用一个简单的项目来演示这种效果。让我们在Interface Builder中排列两个视图,它们拥有超出自身边界的子视图(如图4.1)。你并不能真的看见内部视图超出容器视图的情形,这是因为Interface Builder总是在编辑界面裁剪视图,但你只需要相信它们是这样的。

    图4.1 两个大的白色视图,每个包含一个小的红色视图

    通过代码,我们将给第视图加上20点半径的圆角并允许第二个视图剪裁(如表4.1)。从技术层面上说,这些属性都可以直接在Interface Builder中分别使用用户定义运行时属性以及检查器面板中的裁剪子视图的复选框来实现,但在这个例子中为了清晰起见,我们使用代码完成。图4.2展示了结果。

    表4.1 使用cornerRadius和maskToBounds
    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var layerView1: UIView!
        @IBOutlet weak var layerView2: UIView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // 设置图层的圆角
            self.layerView1.layer.cornerRadius = 20.0
            self.layerView2.layer.cornerRadius = 20.0
    
            // 允许第二个图层裁剪
            self.layerView2.layer.masksToBounds = true
        }
    }
    
    图4.2 右边的红色子视图被裁剪到父视图的cornerRadius

    正如你所见,右边的红色视图被裁剪到父视图的圆角。

    并不能单独操作图层每个角的弯曲度,所以如果你想要创建一个有一些圆角一些直角的图层或视图,只能另辟蹊径,比如使用一个图层遮罩(如本章稍后讲解的一样)或里使用CAShapeLayer(见第6章“特定图层”)。

    图层边框

    CALayer另一对有用的属性是borderWidth以及borderColor,它们共同定义了画在图层边缘的直线。这条线(被称作描边)围绕着图层的bounds,包括角的弯曲。

    borderWidth是用点定义描边粗细的浮点数。它默认为0(无边框)。borderColor定义搭边的颜色,默认为黑色。

    borderColor的类型是CGColorRef,而非UIColor,因此本质上并非是Cocoa对象。然而,你应该记住即使无法没有属性声明,图层也一直持有borderColorCGColorRef更像是NSObject而不是持有/释放,但Objective-C语法并没有提供指示,因即使强持有CGColorRef属性也得用assign声明。

    边框是画在图层边界的,并且在包括子图层的所有图层内容之前。如何我们修改例子来引入图层边框(如表4.2),你可以看到它们的效果(如图4.3)。

    表4.2 加边框
    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var layerView1: UIView!
        @IBOutlet weak var layerView2: UIView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // 设置图层的圆角
            self.layerView1.layer.cornerRadius = 20.0
            self.layerView2.layer.cornerRadius = 20.0
    
            // 增加图层的边框
            self.layerView1.layer.borderWidth = 5.0
            self.layerView2.layer.borderWidth = 5.0
    
            // 允许第二个图层裁剪
            self.layerView2.layer.masksToBounds = true
        }
    }
    
    图4.3 在图层周围增加边框

    注意,图层边框并不会计算图层主图像或子图层的形状。如果子图层超出它的bounds,或者主图像有一个透明度遮罩包含透明区域,边框仍然会绕着图层的(可能是圆角)的矩形(如图4.4)。

    图4.4 边框围绕图层的bounds,而非内容的形状

    阴影

    另一个iOS的普遍特征是阴影。阴影被投射在视图后来显示深度。他们被用来指明图层关系和优先级(例如当一个模态提示出现在另一个视图之前),但它们有时只是用来起装饰作用(用来控制一个更完善的界面)。

    通过设置shadowOpacity属性一个大于0(默认值),可以在任意图层后增加阴影。shadowOpacity是一个浮点数,应该被设置在0.0(不可见)和1.0(完全不透明)之间。设置为1.0会显示一个略高于图层的带有轻微模糊的黑色阴影。为了微调阴影的效果,你可以使用CALayer的三个额外属性:shadowColorshadowOffset以及shadowRadius

    shadowColor属性如同名字所示,是一个控制阴影颜色的CGColorRef,就像borderColorbackgroundColor属性一样。默认的阴影为黑色,这通常是大多情况下你所需要的(彩色的阴影在现实中很少见,而且看起来有点奇怪)。

    shadowOffset属性用于控制阴影延伸的方向的距离。它是一个CGSize值,宽用于控制阴影的水平方向偏移,高用于控制竖直方向上的偏移。默认的shadowOffset{0, 3},这会在使阴影位移图层Y轴向上3点处。

    为什么默认阴影指向上?尽管Core Animation是改自Layer Kit(为iOS创建的私有动画框架),它第一次现身是在Mac OS上,而Mac OS用了和iOS相反的坐标系统(Mac OS的Y轴指向上)。在Mac上,同样默认的shadowOffset值会产生一个向-下-指的阴影,所以默认方向在那种情况下更有意义(如图4.5)。

    图4.5 默认shadowOffset在iOS(左)和Mac OS(右)的显示

    Apple的惯例是用户界面的阴影竖直向下,所以在iOS上大多情况下使用零宽度和正高度的阴影可能是最好的。

    shadowRaidus属性控制阴影的模糊程序。如果设为0会创建一个正好符合视图形状的硬边阴影。一个大一点的数值可以创建一个更为自然的软边阴影。Apple自家的应用设计偏好使用软阴影,因此最好给shadowRadius使用一个非零数值。

    通常,如果你应该给一个如模态提醒这样从背景中凸显出来的视图一个更大的shadowRadius;阴影越模糊,显得深度越深(如图4.6)。

    图4.6 一个拥有更大的位移和半径的阴影会增加视深

    阴影裁剪

    不同于图层边框,图层阴影会完整继承内容的形状,而不仅是boundscornerRadius。为了计算阴影的形状,Core Animation用主图像(如果有子图层也会使用)来创建一个完美匹配图层形状的阴影(如图4.7)。

    图4.7 图层阴影会完全围绕图层主图像

    图层阴影在组合剪裁上有一个扰人的限制:因为阴影通常会画在图层边界外,如果你允许masksToBounds属性,阴影会和其它伸出图层的内容一样被裁剪。如果我们给我们的边框案例项目添加图层阴影,你会看到这种问题(如图4.8)。

    图4.8 masksToBounds属性对阴影和边框都会裁剪

    从技术层面来看这种情况是可以理解的,但它可能并不像是你想要的效果。如果你想裁剪内容同时投射阴影,你需要使用两个图层:一个空的外图层用于绘制阴影,一个允许masksToBounds的内图层用来裁剪内容。

    如果你在我们的项目中对右边视图的裁剪视图上添加一个环绕的视图就可以解决这个问题(如图4.9)。

    图4.9 在右边的裁剪视图外额外包围一个阴影投射视图

    我们仅在最外面的视图上添加阴影,仅在内视图中允许裁剪。表4.3展示了修改后的代码,图4.10展示了结果。

    表4.3 使用一个额外视图来解决阴影裁剪问题
    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var layerView1: UIView!
        @IBOutlet weak var layerView2: UIView!
        @IBOutlet weak var shadowView: UIView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // 设置图层的圆角
            self.layerView1.layer.cornerRadius = 20.0
            self.layerView2.layer.cornerRadius = 20.0
    
            // 增加图层的边框
            self.layerView1.layer.borderWidth = 5.0
            self.layerView2.layer.borderWidth = 5.0
    
            // 给layerView1添加阴影
            self.layerView1.layer.shadowOpacity = 0.5
            self.layerView1.layer.shadowOffset = CGSizeMake(0.5, 5.0)
            self.layerView1.layer.shadowRadius = 5.0
    
            // 给shadowView添加同样的阴影(不是layerView2)
            self.shadowView.layer.shadowOpacity = 0.5
            self.shadowView.layer.shadowOffset = CGSizeMake(0.5, 5.0)
            self.shadowView.layer.shadowRadius = 5.0
    
            // 允许第二个图层裁剪
            self.layerView2.layer.masksToBounds = true
        }
    }
    
    图4.10 现在右边的视图有了一个阴影仅管有裁剪子视图

    shadowPath属性

    我们已经创建的阴影并不总是方形,而是从内容的形状继承。这看起来很棒,但在实际情况它的计算代价十分昂贵,尤其当图层包含多个子图层并且每个都有一个透明度遮罩的主图像时。

    如果你事先知道阴影可能的形状,你可以通过指定shadowPath来显著提升性能。shadowPath是一个CGPathRef(一个指向CGPath的对象)。CGPath是一个用来指定直接向量形状的Core Graphics对象。我们可以用它定义独立于图层形状的阴影形状。

    图4.11展示了两个用于同样图层图像的不同的阴影形状。在这里,我们用的形状很简单,但它们完全可以是任何你想要的形状。看表4.4的代码[1]

    图4.11 使用shadowPath直接投射阴影形状
    表4.4 创建简单的阴影路径
    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var layerView1: UIView!
        @IBOutlet weak var layerView2: UIView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // 允许图层阴影
            self.layerView1.layer.shadowOpacity = 0.5
            self.layerView2.layer.shadowOpacity = 0.5
    
            // 创建方形阴影
            let squarePath = CGPathCreateMutable()
            CGPathAddRect(squarePath, nil, self.layerView1.bounds)
            self.layerView1.layer.shadowPath = squarePath
            // 创建圆形阴影
            let circlePath = CGPathCreateMutable()
            CGPathAddEllipseInRect(circlePath, nil, self.layerView2.bounds)
            self.layerView2.layer.shadowPath = circlePath
        }
    }
    

    对于创建一些类似矩形和圆的形状来说,手动创建一个CGPath是十分简单的。对于更复杂的如圆角矩形的形状,你会发现使用UIKit提供的CGPath的Objective-C的封装类UIBezierPath会更加轻松。

    图层遮罩

    我们知道使用masksToBounds属性可以把图层的内容裁剪到它的bounds,而使用cornerRadius属性我们甚至可以给它圆角。但有时你可以想不用矩形甚至不是圆角矩形来展示内容。例如,你可能想给一个图像创建星形相框,或者你想滚动的文字边缘漂亮地渐渐消失在背景中而不是直接被边缘裁剪。

    使用一个有透明组件的32位PNG图像,你可以指定一个有直接透明遮罩的主图像,这通常是创建非矩形视图最简单的方法。但这个方法不允许你程序化地动态裁剪图像来生成遮罩或让子图层、子视图裁剪成同样的形状。

    CALayer有一个叫做mask的属性可以帮助解决这个问题。mask属性本身是一个CALayer并且有其它图层所应有的全部绘图和布局属性。它与相对父图层放置的子图层的使用方法很像,但它并不是显示成一个正常的子图层。并非画进你图层中,mask图层决定父图层的哪些部分是可见的。

    mask图层的颜色是不重要的;有用的是它的轮廓mask如同一个曲奇模具,mask图层的实体部分将会从父图层中切出并保存;其他将被抛弃(如图4.12)。

    如果mask图层小于父图层,只有父图层(或其子图层)与mask相交的部分可见。如果你使用了mask图层,图层之外的一切将隐藏。

    图4.12 结合分离的图像并且使用mask图层创建遮罩后的图像

    为了展示这一点,让我们创建一个简单的项目,我们用图层的mask属性来将一张图像作为另一张图像的遮罩。为了方便起见,我们在Interface Builder中使用UIImageView来创建我们的图像图层,仅仅程序化地创建并应用mask图层。表4.5展示了这些代码,图4.13展示了结果[2]

    表4.5 使用图层遮罩
    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet var imageView: UIView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // 创建遮罩图层
            let maskLayer = CALayer()
    
            // 原书中设置
            // maskLayer.frame = self.imageView.bounds
    
            // 译者设置
            maskLayer.frame = CGRectMake(200, 100, 200, 200)
    
            let maskImage = UIImage(named: "Cone.png")!
            maskLayer.contents = maskImage.CGImage
    
            // 将遮罩应用于图像图层
            self.imageView.layer.mask = maskLayer
    
        }
    }
    
    图4.13 使用了mask图层后的UIImageView

    CALayer遮罩的一个非常酷炫的特性是你并非只能用静态图像作为遮罩。任何可以组成图层的东西都可以被作为mask属性,这意味你的遮罩可以用代码动态生成,甚至产生动画。

    缩放过滤器

    本章最后一个话题是minificationFiltermagnificationFilter属性的作用。通常在iOS上,当你显示图像时,你应该尝试以正确的尺寸显示它们(就是图像像素与屏幕像素以1:1显示)。其原因如下:

    • 它能提供最好的质量,因为像素既不会拉伸也不会被重新采样。
    • 它能最好地利用RAM,因为你不会存储无用的像素点。
    • 它有最好的性能,因为GPU不需要过多工作。

    然而有时的确需要或大或小地显示一个图像。比如人物或化身的缩略图,一个可供用户拖动、缩放的大图。这时如果为每个可能需要展示的图像分别存储不同尺寸的版本就十分不方便。

    当以不同尺寸显示图像时,一个被称作缩放过滤的算法被应用于源始图片来产生新的要显示在屏幕上的图像。

    无论你是放大还是缩小,都没有通用的理想的绽放图像的算法。其方法的优劣决定于被被缩放图像的属性。CALayer提供了三种缩放图像的绽放过滤器供选择。它们用如下的字符串常量表示:

    • kCAFilterLinear
    • kCAFilterNearest
    • kCAFIlterTrilinear

    kCAFilterLinearminification(缩小图像)和magnification(放大图像)的默认过滤器。这个过滤器使用双线性过滤算法,这在大多情况下会产生好的效果。双线性过滤工作原理是对多个像素点采样来创建最终值。结果不错,而且绽放得很平滑,但如果放大过多倍数会使图像看起来模糊(如图4.14)。

    kCAFilterTrlinear选项与kCAFilterLinear十分类似。大多数情况下二者并无视觉上的区别,但三线性过滤法比双线性过滤拥有更好的性能。它通过存储图像的多种尺寸(被称作MIP贴图)然后在三维坐标中重新采样,然后结合大一点和小一点的图像来产生最终的结果。

    这个方法的优势在于可以很好地工作于一组早已经接近最终值的图像。这意味着它不需要同时重新采样尽量多的像素值,这提升了性能而且可以避免由于极小缩放因子导致的精度问题而引发的采样失效。

    图4.14 对于大图像,三线性或双线性过滤器通常更好

    kCAFilterNearest选项是最粗暴的方法。正如名字所示,这个算法(被称作近邻取样过滤)直接采取最近的一个点,而且根本没有颜色混合。这十分快速而且不会模糊图像,但对于缩小图像的质量明显较差,放大图像会变成块状和像素化。

    在图4.14中,注意看在缩小图像时近邻取样会比双线性过滤更加扭曲,而放大时看起来更加模糊。对比图4.15,其开始是一张非常小的图像。这种情况下,近邻取样会更好地保留源始像素,而无论放大缩小线性过滤都将它们变成模糊的一团。

    图4.15 对于无对角的小图像,近邻取样过滤更好

    通常来说,对于有鲜明对比而较少对角线的极小图或极大图(例如,计算机生成的图像),近邻取样缩放可以保留对比产生可能更好的结果。但对于大多数图像,尤其是照片或者有对角线或弯曲的图像,近邻取样会比线性过滤效果差。用另一种方法来说,线性过滤保留形状,近邻取样过滤保留像素

    让我们试一个真实的例子。我们将修改第3章的时钟项目来显示一个LCD风格的数字时钟来代替模拟时钟。这些数字会用十分简单的像素字体创建(这个字体中字符是用独立像素而非向量图形组成),它们存储在一起并会用第2章讲解的精灵图集技术显示(如图4.16)。

    图4.16 一个用于显示LCD风格时钟的简单“像素字体”精灵图集

    我们将在Interface Builder中排列6个视图,两个表示小时,两个表示分钟,两个表示秒。图4.17展示它们是如何排列的。用单独的出口绑定这些视图显得十分笨拙,所以我们用IBOutletCollection来将它们连向控制器,这允许我们以数组的形式访问视图。表4.6展示了这个时钟的代码。

    图4.17 数字时钟视图排列
    表4.6 显示LCD风格时钟
    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet var digitViews: [UIView]!
        var timer: NSTimer!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // 获得精灵图集
            let digits = UIImage(named: "Digits.png")!
    
            // 设置数字视图
            for view in self.digitViews {
                // 设置内容
                view.layer.contents = digits.CGImage
                view.layer.contentsRect = CGRectMake(0, 0, 0.1, 1.0)
                view.layer.contentsGravity = kCAGravityResizeAspect
            }
    
            // 启动计时器
            self.timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "tick", userInfo: nil, repeats: true)
    
            // 设置开始时间
            self.tick()
        }
    
        func setDigit(digit: NSInteger, forView view: UIView) {
            // 调整contentRect到选择的数字
            view.layer.contentsRect = CGRectMake(CGFloat(digit) * 0.1, 0, 0.1, 1.0)
        }
    
        func tick() {
            // 将时间转换成小时、分钟和秒
            let calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierChinese)!
    
            let units = NSCalendarUnit.CalendarUnitHour | NSCalendarUnit.CalendarUnitMinute | NSCalendarUnit.CalendarUnitSecond
    
            let components = calendar.components(units, fromDate: NSDate())
    
            // 设置小时
            self.setDigit(components.hour / 10, forView: self.digitViews[0])
            self.setDigit(components.hour % 10, forView: self.digitViews[1])
    
            // 设置分钟
            self.setDigit(components.minute / 10, forView: self.digitViews[2])
            self.setDigit(components.minute % 10, forView: self.digitViews[3])
    
            // 设置秒
            self.setDigit(components.second / 10, forView: self.digitViews[4])
            self.setDigit(components.second % 10, forView: self.digitViews[5])
        }
    }
    

    译者实现的效果和原著有所不同,因为图像过于清晰,因此会在之后贴出原著效果,在这显示一下译者实现的效果


    译者实现的数字时钟

    正如图4.18所示,它起效了但数字看起来很模糊,看起来应该是默认的kCAFilterLinear选项导致的。

    图4.18 一个由于默认kCAFilterLinear缩放而导致的模糊显示的时钟

    为了得到图4.19所示的清晰时钟,我们只需要在我们程序的for...in循环中加入下面这行:

    view.layer.magnificationFilter = kCAFilterNearest
    
    图4.19 使用近邻取样缩放后清晰显示的时钟

    组透明

    UIView有一个好用的alpha属性可可用于改变透明度。CALayer有一个相同属性叫opacity。这些属性都层次式生效,所以如果你设置了某一图层的opacity,它会同样自动在所有子图层生效。

    iOS中的一个普遍技巧是设置控件的alpha为0.5(50%)来使其不可见。这对于单独视图效果极好,但如果视图有子视图会显得有点奇怪。图4。20展示了一个自定义的有UILabelUIButton;其左侧为一个不透明的按钮,右侧是一个被设为50%透明度的同样按钮。注意观察我们可以在按钮背景上看到内在标签的轮廓。

    图4.20 右边的渐隐按钮上标签边框清晰可见

    这个效果是由于透明度混合导致的。当你以50%透明度显示图层时,图层的每一像素都显示50%的自身颜色以及底层图层的50%。这导致了半透明的效果。但如果这个图层的子图层也显示为50%半透明,当你看向子图层时它会显示子图层的50%颜色,25%容器的颜色,只有25%的背景色。

    在我们的例子里,按钮和标签都有白色背景。即使它们都只有50%透明度,它们结合的透明度就是75%,因此按钮上的标签看起来比它周围透明度小一点。这对所有的子图层都生效,会让一个控件产生不好的视觉效果。

    理想情况下,当你设置图层的opacity,你希望它的整个子树像是一个整体一样渐隐,而不用考虑其内部结构。你可以通过在你的Info.plist文件中将UIViewGroupOpacity设置为YES来达到这一目的,但这会影响整个应用的混合,导致一个小的应用级问题。如果UIViewGroupOpacity键删除了,在iOS6及之前的系统中它默认为NO(这一默认情况在未来的iOS版中可能改变)。

    另一种方法是,你可以指定CALayer的一个叫shouldResterize的属性来实现某一图层子树的组透明(如表4.7)。当设为YES时,shouldRasterize属性会导致在透明度设置应用前图层和它的子图层折叠成一个单独的平面图像,因此解决了混合失效的问题(如图4.21)。

    除了允许shouldRasterize属性,我们也修改了图层的rasterizationScale属性。默认情况下,所有的图层栅格化的缩放比为1.0,因此如果你使用shouldRasterize属性,你应该总是确保自己设置了rasterizationScale来匹配屏幕以防视图在一个Retina显示设备上看起来像素化。

    如同UIViewGroupOpacity一样,使用shouldRasterize属性会有一些隐式的表现(这将在第12章“微调速度”和第15章“图层表现”中解释),但这个表现影响被本土化了。

    表4.7 使用shouldRasterize来修复组混合问题
    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var containerView: UIView!
    
        var customButton: UIButton {
            get {
                // 创建按钮
                var frame = CGRectMake(0, 0, 150, 50)
                let button = UIButton(frame: frame)
                button.backgroundColor = UIColor.whiteColor()
                button.layer.cornerRadius = 10
    
                // 添加标签
                frame = CGRectMake(20, 10, 110, 30)
                let label = UILabel(frame: frame)
                label.text = "Hello World"
                label.backgroundColor = UIColor.whiteColor()
                label.textAlignment = NSTextAlignment.Center
                button.addSubview(label)
                
                return button
            }
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // 创建不透明按钮
            let button1 = self.customButton
            button1.center = CGPointMake(50, 150)
            self.containerView.addSubview(button1)
    
            // 创建半透明按钮
            let button2 = self.customButton
            button2.center = CGPointMake(250, 150)
            button2.alpha = 0.5
            self.containerView.addSubview(button2)
    
            // 允许半透明按钮的栅格化
            button2.layer.shouldRasterize = true
            button2.layer.rasterizationScale = UIScreen.mainScreen().scale
        }
    
    }
    
    图4.21 渐隐按钮的内部结构不再可见

    然而译者未使用栅格化的效果也是如此,猜测Apple在iOS8中改变了


    译者未使用栅格化的效果

    总结

    这一章介绍了你可以程序化应用到图层上的一些视觉特效,例如圆角、阴影和遮罩。我们也介绍了缩放过滤器以及组透明。

    在第5章“变形”中我们将研究图层变形和将我们的图层转入第三维度。


    1. 译者实际使用时发现得加下如下两句:
      self.layerView1.frame = CGRectMake(0, 0, 200, 200)
      self.layerView2.frame = CGRectMake(0, 0, 200, 200)
      使用图像如下:

      Cone.png
      经查明是因为,使用AutoLayout后旋转屏幕后布局将重新改变,因此在viewDidLoad方法里面的改变会受到影响,因此读者应该注意程序的各个阶段的变化,这一问题已经在后面的译文中做了处理。
    2. 素材如下:


      Igloo.png
      Cone.png

    相关文章

      网友评论

      本文标题:I.4 视觉特效

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