深入研究绘图——图像和图像视图

基本的通用UIKit图像类是UIImage.。UIImage可以读取存储的文件,因此,如果不需要动态创建图像,但在应用程序运行之前已经创建了图像,则绘图可能与提供图像文件作为app bundle中的资源一样简单。系统知道如何处理许多标准图像文件类型,如HEIC、TIFF、JPEG、GIF和PNG;当图像文件包含在app bundle中时,iOS对PNG文件具有特殊的亲和力,您应该尽可能地选择它们。您还可以通过其他方式获取图像数据,例如下载图像,并将其转换为UIImage。

图像文件

App bundle中预先存在的图像文件通常是通过UIImage初始化器init(named:)获取的,它接受一个字符串并返回一个封装在Optional中的UIImage,以防图像不存在。此方法在两个位置查找图像:

  • Asset目录

    我们在asset目录中查找具有所提供名称的图像集。名称区分大小写。

  • 顶级app bundle

    我们查看app bundle的顶层,以获取具有所提供名称的图像文件。名称区分大小写,应包括文件扩展名;如果不包括文件扩展名,则假定为.png。
    调用init(named:)时,将在搜索app bundle的顶层之前先搜索asset目录。如果存在多个asset目录,则都会进行搜索,但搜索顺序不确定,因此请避免使用相同名称的多个图像集。

使用init(named:),图像数据可以缓存在内存中,如果稍后再次调用init(named:)请求相同的图像,则可以立即提供缓存的数据。缓存通常很好,因为将磁盘上的图像解码为可用的位图数据很消耗资源的。
然而,有时缓存可能不是你想要的;如果你知道你只需要取一次图像并立即将其放入界面,那么缓存可能会对你的应用内存造成不必要的压力。如果是这种情况,还有另一种方法:使用init(contentsOfFile:)直接从app bundle(虽然不是从asset目录)读取图像文件,而不进行缓存,这需要一个路径名字符串。要获取该路径名字符串,可以使用Bundle.main获取对app bundle的引用,然后Bundle提供实例方法来获取bundle中的文件路径名,例如path(forResource:ofType:)。

另一个考虑因素可能是速度。使用init(named:)时,可以快速找到asset目录中的图像,但init(contentsOfFile:)可以更快地在应用程序包中找到图像文件;如果您不必调用path(forResource:ofType:),因为您已经知道如何构造路径名,则速度会更快。

用于在app bundle中指定图像文件的方法响应文件名中的特殊后缀:

  • 高分辨率变体

    在具有双分辨率屏幕的设备上,当从app bundle中按名称获取图像时,将自动使用扩展名为@2x的文件(如果有),并通过为其指定比例属性值为2.0来将生成的UIImage标记为双分辨率。同样,如果有一个扩展名为@3x的文件,它将在具有三分辨率屏幕的设备上使用,比例属性值为3.0。

    这样,您的app可以包含不同分辨率的图像文件的不同版本。由于比例属性,高分辨率图像的尺寸与单分辨率图像的尺寸相同。因此,在高分辨率屏幕上,您的代码和界面可以继续工作而不发生变化,但您的图像看起来更清晰。

    这适用于image(named:)和image(contentsOfFile:)。例如,如果存在一个名为pic.png的文件和一个名为pic@2x.png的文件,那么在具有双分辨率屏幕的设备上,这些方法将以2.0的比例访问pic@2x.png作为UIImage:

    let im = UIImage(named:"pic") // uses pic@2x.png
    if let path = Bundle.main.path(forResource: "pic", ofType: "png") {
    	let im2 = UIImage(contentsOfFile:path) // uses pic@2x.png
    }
    
  • 设备类型变体

    如果app在iPad上运行,则会自动使用由~ipad扩展的同名文件。您可以在universal app中使用此功能,根据app是在iPhone(或iPod touch)上运行,还是在iPad上运行,自动提供不同的图像。(这不仅适用于图像,也适用于通过名称从bundle.中获取的任何资源。参见文档档案中的Apple’s Resource Programming Guide。)

    这适用于image(named:)和路径path(forResource:ofType:)。例如,如果有一个名为pic.png的文件和一个名为pic~ ipad.png的文件,那么在ipad上,这些方法将访问pic~ipad.png:

    let im = UIImage(named:"pic") // uses pic~ipad.png
    let path = Bundle.main.path(
    forResource: "pic", ofType: "png") // uses pic~ipad.png
    

不过,asset目录的一大好处是,您可以忘记所有关于名称后缀的约定!asset目录知道何时在图像集中使用备用图像,不是从其名称,而是从其在目录中的位置。将单分辨率、双分辨率和三分辨率的备用图片分别放在标有“1X”、“2X”和“3X”的位置中;要获得不同的iPad版本图像,请查看iPhone和iPad在Attributes inspector中的图像集,这些设备类型的独立的位置将显示在asset目录中。

在asset目录中保存图像的另一个优点是它可以是基于矢量的PDF。将Scales弹出菜单切换为Single Scale,并将图像放入单一位置。它将根据所有分辨率自动调整大小,因为它是一个矢量图像,所以调整大小会很快。如果您还选中Preserve Vector Data,则它将根据任何大小急剧调整大小-例如,当通过UIImageView或其他界面项自动调整大小时,或者当您的代码以不同的大小重新绘制图像时。

asset目录中的图像集可以根据设备的处理器类型、宽颜色功能等做出许多进一步的区分。此外,这些区别不仅在app运行时被运行时使用,而且在为特定目标设备瘦身app时被App Store使用。

图像文件和size classes

asset目录可以区分用于不同size class情况的图像版本。(请参见第1章中有关size classes和trait collections的讨论。)在图像集的Attributes inspector中,使用Width Class和Height Class弹出菜单指定想要位置是哪一个size class。因此,例如,如果我们在iPhone上的应用程序旋转到横向,并且图像集中既有Any Height,又有Compact Height,则使用Compact Height版本。这些功能在应用程序运行时是实时的;如果应用程序从横向旋转到纵向,并且图像集中既有Any height也有Compact height选项,那么Compact Height版本会自动被替换为界面中的Any Height版本。

asset目录如何执行这种魔力? 当通过UIImage的init(named:)从asset目录中获取图像时, 其imageAsset属性是一个UIImageAsset,它有效地指向它来自的图像集中的asset目录。图像集中的每个图像都有一个与其关联的trait collection(traitCollection)。通过调用UIImageAsset方法image(with:),,可以从适用于给定trait collection的同一图像集中向图像请求一个图像的imageAsset。

显示图像的内置界面对象是自动trait collection感知的;它接收traitCollectionDidChange(_:)消息并相应响应。为了演示如何在引擎盖下工作,我们可以构建一个自定义的UIView,其图像属性的行为方式相同:

class MyView: UIView {
	var image : UIImage!
	override func traitCollectionDidChange(_: UITraitCollection?) {
		self.setNeedsDisplay() // causes draw(_:) to be called
	}
	override func draw(_ rect: CGRect) {
		if var im = self.image {
			if let asset = self.image.imageAsset {
				im = asset.image(with:self.traitCollection)
			}
			im.draw(at:.zero)
		}
	}
}

在不使用asset目录的情况下,也可以将图像作为基于trait的备选方案相互关联。例如,您可以这样做,因为您已经用代码构建了图像本身,或者在应用程序运行时通过网络获取了它们。该技术是实例化一个UIImageAsset,然后通过将其注册到同一UIImageAsset,将每个图像与不同的trait collection相关联。例如:

let tcreg = UITraitCollection(verticalSizeClass: .regular)
let tccom = UITraitCollection(verticalSizeClass: .compact)
let moods = UIImageAsset()
let frowney = UIImage(named:"frowney")!
let smiley = UIImage(named:"smiley")!
moods.register(frowney, with: tcreg)
moods.register(smiley, with: tccom)

令人惊讶的是,如果我们现在在UIImageView中显示frowney或smiley,我们会自动看到与环境当前垂直size class相关联的图像,当应用程序改变iPhone上的方向时,它会自动切换到其他图像。此外,即使我没有一直提到frowney, smiley或UIImageAsset!,这仍然有效!(原因是系统缓存了这些图像,并且它们维护对注册它们的UIImageAsset的强引用。)

命名空间图像文件

当图像文件数量众多或需要分组时,就产生了如何将它们划分为命名空间的问题。以下是一些可能性:

  • 文件夹引用

    你可以把图片放在app bundle的文件夹中,而不是放在app bundle的顶层。如果将文件夹引用放入项目中,这是最容易维护的; 然后在构建时将文件夹本身及其所有内容复制到app bundle中。在这样的文件夹中检索图像有多种方法;例如:

    • 使用文件夹名称和名称字符串中图像名称前面的正斜杠调用UIImage
      init(named:)。例如,如果文件夹是称为pix,图像文件称为pic.png,则图像的“名称”为“pix/pic.png”。

    • 调用Bundle path(forResource:ofType:inDirectory:)获取图像文件的路径,然后调用UIImage init(contentsOfFile:)。

    • 获取bundle path (Bundle.main.bundlePath),并使用NSString pathname和FileManager方法钻取到所需文件。

  • Asset目录文件夹

    Asset目录可以提供用作命名空间的虚拟文件夹。例如,图像集myImage可能位于名为pix的asset目录文件夹中。如果选中该文件夹的Attributes inspector中提供了Namespace,则可以通过UIImage init(name:)通过名称“pix/myImage".”访问该图像。

  • Bundle

    init(named:)的完整形式是init(named:in:),其中第二个参数是bundle。这意味着您可以将图像保存在第二个bundle中,如framework中,并将该bundle指定为一种命名空间图像的方式,而不管它是来自asset目录还是位于bundle的顶层。

图像视图

许多内置的Cocoa界面对象将接受UIImage作为它们绘制自己的一部分;例如,UIButton可以显示图像,UINavigationBar或UITabBar可以有背景图像。但是,如果您只想在界面中显示一个图像,那么您可能会将它交给一个图像视图(UIImageView),它在显示图像方面具有最大的学问和灵活性,并用于此目的。

NIB编辑器在这方面提供了一些快捷方式。界面对象的属性检查器可以有图像,包括UIImageView,,它将有一个弹出菜单,您可以从中选择项目中的图像。您的项目的图像也列在媒体库(Command-Shift-M)中;从这里,您可以将图像拖动到画布中的界面对象(如按钮)上,或者,如果直接将其拖动到普通视图中,您将获得显示该图像的UIImageView。

— 个UIImageView实际上可以有两个图像, 一个分配给它的image属性, 另一个分配给它的highlightedImage属性;UIImageView的isHighlighted属性的值指示在任何给定时刻显示这两个图像中的哪一个。UIImageView不会自动highlight显示自身, 仅仅是因为用户按按钮的方式点击它。但是,在某些情况下,UIImageView将响应其周围环境的highlighting显示;例如,在表视图中单元格,当单元格highlighted显示时,UIImageView将显示其highlighted显示的图像)。

UIImageView是一个UIView,因此除了它的图像之外,它还可以有一个背景色,它可以有一个alpha(透明度)值,等等。图像可能具有透明的区域,UIImageView将尊重这一点;因此可以显示任何形状的图像。没有背景色的UIImageView除了其图像之外是不可见的,因此图像只显示在界面中,而用户不知道它位于矩形主体中。没有图像和背景色的UIImageView是不可见的,因此您可以从一个空的UIImageView开始,在以后需要图像的地方,然后用代码分配图像。您可以指定一个新图像来替换另一个图像,或者将图像视图的image属性设置为nil以删除它。

UIImageView如何绘制图像取决于其contentMode属性(UIView.ContentMode)的设置。例如,.scaleToFill表示将图像的宽度和高度设置为视图的宽度和高度,从而完全填充视图,即使这会更改图像的纵横比;.center表示图像在视图中居中绘制,而不改变它的大小。了解各种contentMode设置含义的最佳方法是在NIB编辑器中试用图像视图:在图像视图的属性检查器中,更改Content Mode弹出菜单以查看图像的绘制位置和方式。

您还应该注意UIImageView的clipsToBounds属性;如果该属性为false,则其图像(即使其大于图像视图,并且即使其未按contentMode缩小)也可以整体显示,扩展到图像视图本身之外。

默认情况下,从库中拖动到NIB编辑器中的UIImageView的clipsToBounds为false。这不太可能是你想要的!

在代码中创建UIImageView时,可以利用方便的初始值设定项init(image:)。默认的contentMode为.scaleToFill,但图像最初没有缩放;相反,图像视图本身的大小与图像匹配。您可能仍然需要在其父视图中正确定位UIImageView。在本例中,我将在应用程序界面的中心放置一张火星行星的图片(图2-1):

let iv = UIImageView(image:UIImage(named:"Mars"))
self.view.addSubview(iv)
iv.center = iv.superview!.bounds.center
iv.frame = iv.frame.integral 

在这里插入图片描述
图2-1 火星出现在我的界面上

将新图像分配给现有UIImageView时,其大小会发生什么变化,这取决于图像视图是否使用自动布局。在自动布局下,图像的大小将成为图像视图的intrinsicContentSize内部内容大小,因此图像视图采用图像的大小,除非其他约束阻止。

如果图像视图的adjustsImageSizeForAccessibilityContentSizeCategory为true,则如果用户切换到可访问性文本大小,图像视图将从图像的内部内容大小向上扩展。您可以在NIB编辑器中设置此属性(在属性检查器中调整图像大小)。

图像视图自动从其图像的alignmentRectInsets获取其alignmentRectInsets。因此,如果要使用自动布局将图像视图与其他对象对齐,可以将适当的alignmentRectInsets附加到图像视图将要显示的图像,并且图像视图将执行正确的操作。要在代码中执行此操作,请通过调用原始图像的withAlignmentRectInsets(_:)方法来派生新图像;或者,可以在asset目录中设置图像的alignmentRectInsets(使用四个对齐字段)。

可调整大小的图像

界面中的某些地方需要一个图像,该图像可以按照任何所需的比例一致地调整大小。例如,作为滑块或进度视图(第12章) 轨迹的自定义图像必须能够填充任意长度的空间。这样的图像称为可调整大小的图像。

要在代码中生成可调整大小的图像,请从普通图像开始,并调用其resizableImage(withCapInsets:resizingMode:)方法。capInsets:参数是UIEdgeInsets,其组件表示从图像边缘向内的距离。在大于图像的上下文中,可调整大小的图像可以采用两种方式之一,具体取决于resizingMode:值(UIImage.ResizingMode):

  • .tile

    inset区域的内部矩形在内部平铺(重复);每个边的形成是通过将inset区域外相应的边矩形平铺。inset区域外的四个角矩形绘制不变。
    在这里插入图片描述
    图2-2 平铺火星整个图像
    在这里插入图片描述
    图2-3 平铺火星内部

  • .stretch

    将inset区域的内部矩形拉伸一次以填充内部;每个边形成是通过将inset区域外相应的边矩形拉伸一次。inset区域外的四个角矩形绘制不变。

在这些示例中,假设self.iv是一个具有绝对高度和宽度的UIImageView(这样它就不会采用其图像的大小),contentMode为.scaleToFill ( 这样图像就会显示调整大小的行为) 。首先,我将说明如何平铺整个图像( 图2-2 ) ; 请注意, capInsets:是.zero,表示完全没有insets:

let mars = UIImage(named:"Mars")!
let marsTiled = 
	mars.resizableImage(withCapInsets:.zero, resizingMode: .tile)
self.iv.image = marsTiled

现在,我们将平铺图像的内部,更改capInsets:前一个代码的参数(图2-3):

let marsTiled = mars.resizableImage(withCapInsets:
	UIEdgeInsets(
			top: mars.size.height / 4.0,
			left: mars.size.width / 4.0,
			bottom: mars.size.height / 4.0,
			right: mars.size.width / 4.0
		), resizingMode: .tile)

接下来,我将演示拉伸。我们将从改变resizingMode:开始:从前面的代码(图2-4)开始:
在这里插入图片描述
图2-4 拉伸火星内部
在这里插入图片描述
图2-5. 拉伸火星内部几个像素
在这里插入图片描述
图2-6. 火星,拉伸和修剪

let marsTiled = mars.resizableImage(withCapInsets:
	UIEdgeInsets(
		top: mars.size.height / 4.0,
		left: mars.size.width / 4.0,
		bottom: mars.size.height / 4.0,
		right: mars.size.width / 4.0
	), resizingMode: .stretch)

一种常见的拉伸策略是将几乎一半的原始图像用作帽cap的inset,只在中间留下一个小矩形,必须拉伸以填充生成图像的整个内部(图2-5):

let marsTiled = mars.resizableImage(withCapInsets:
	UIEdgeInsets(
		top: mars.size.height / 2.0 - 1,
		left: mars.size.width / 2.0 - 1,
		bottom: mars.size.height / 2.0 - 1,
		right: mars.size.width / 2.0 - 1
	), resizingMode: .stretch)

在前面的例子中,如果图像视图的contentMode为.scaleAspectFill,并且图像视图的clipsToBounds为true,我们会得到一种渐变效果,因为拉伸图像的顶部和底部在图像视图之外并且没有绘制(图2-6)。
在这里插入图片描述
图2-7 火星,在asset目录中切片
在这里插入图片描述
图2-8 火星,切片和拉伸

或者,可以在asset目录中配置可调整大小的图像。通常情况下,app中使用的特定图像主要是可调整大小的图像,并且始终具有相同的capInsets:和resizingMode:,因此配置此图像一次而不是重复相同的代码是有意义的。

要将asset目录中的图像配置为可调整大小的图像,请选择该图像,然后在Attributes inspector(属性检查器)的Slicing(切片)部分中,将Slicing 弹出菜单更改为水平、垂直或水平和垂直。执行此操作时,将显示其他界面。可以使用中心弹出菜单指定resizingMode。您可以使用数字,也可以单击画布右下角的Show Slicing“显示切片”以图形方式工作。

此功能实际上比resizableImage(withCapInsets:resizingMode:)更强大。它允许您分别指定从平铺或拉伸区域分离出来的端帽caps,图像的其余部分将被切掉。例如,在图2-7中,左上、右上、左下和右下的深色区域将按原样绘制。窄带将被拉伸,顶部中心的小矩形将被拉伸以填充大部分内部。但其余的图像,大中心区域覆盖着一种纱布窗帘,将被完全忽略。结果如图2-8所示。

在这里插入图片描述
图2-9 两种渲染模式下的一幅图像

透明遮罩

iOS应用程序界面中有几个地方希望将图像视为透明遮罩transparency mask,也称为模板template。这意味着图像颜色值被忽略,并且只有每个像素的透明度(alpha)值才重要。屏幕上显示的图像是通过将图像的透明度值与单一的淡色组合而形成的。例如,这是tab bar的item图像的默认行为。
处理图像的方式是图像的一个属性,即渲染模式renderingMode.。此属性是只读的;若要在代码中更改它,请从图像开始,并通过调用其withRenderingMode(_:)方法以不同的渲染模式生成新图像。渲染模式值(UIImage.RenderingMode)为:

  • .automatic
  • .alwaysOriginal
  • .alwaysTemplate

默认值是.automatic,这意味着图像通常被绘制到任何地方,除了在某些有限的上下文中,在那里它被用作透明遮罩。使用其他两个渲染模式值,可以强制正常绘制图像,即使在通常将其视为透明遮罩的上下文中,或者也可以强制将图像视为透明遮罩,即使也将视为正常图像的上下文中。

为配合此功能,iOS为每个UIView提供了一种tintColor,,它将用于对其包含的任何template图像进行tint色。此外,默认情况下,这个tintColor在视图层次结构下继承,实际上在整个app中,从window开始。因此,为app的主窗口指定tint色可能是对窗口所做的少数更改之一;否则,应用程序将采用系统的蓝色tint color。(或者,如果使用的是主情节提要,请在File inspector “文件检查器”中设置全局tint color。)可以为各个视图指定自己的tint color,这些tint色由它们的子视图继承。图2-9显示了在window色调为红色的app中显示相同背景图像的两个按钮,一个在normal正常渲染模式,另一个在template渲染模式。(以后详细介绍template图像和tintColor。)

或者,可以在asset目录中为图像指定渲染模式。选择asset目录中的图像集,并在Attributes inspector使用Render As弹出菜单将渲染模式设置为默认值(.automatic)、原始图像(.alwaysOriginal)或模板图像(.alwaysTemplate)。这是一种很好的方法,当您有一个主要在特定渲染模式下使用的图像时,因为这样可以避免每次获取图像时都必须记住在代码中设置该渲染模式。相反,任何时候调用init(named:)时,此图像就为已经设置好的渲染模式。

可反转图像

如果系统语言是right-to-left的,则当应用程序在应用程序本地化的系统上运行时,整个界面将自动反转。一般来说,这可能不会影响您的图像。运行时假定您不希望在反转界面时反转图像,因此它的默认行为是将图像单独保留。

不过,当界面反转时,您可能需要反转图像。例如,假设您绘制了一个箭头,指向用户点击按钮时新界面将从哪个方向到达。如果按钮将视图控制器推到导航界面上,则该方向在left-to-right系统中是来自right,而在right-to-left系统中是来自left。这个图像在app自己的界面中具有方向性意义;当界面反转时,它需要水平翻转。

要在代码中实现这一点,请调用图像的imageFlippedForRightToLeftLayoutDirection方法,并在界面中使用生成的图像。在left-to-right系统中,将使用普通图像;在right-to-left系统中,将自动创建和使用图像的反转版本。您可以为特定显示图像的UIView(如UIImageView)重写此行为,即使图像是可反转的,通过设置该视图的semanticContentAttribute以防止镜像。

可以使用Direction弹出菜单(选择一个Mirrors选项)对asset目录中的图像做相同的决定。此外,布局方向是一个trait,因此您可以在left-to-right或right-to-left的布局下使用成对的图像。配置这些对的简单方法是在asset目录的Direction弹出菜单中同时选择两个;现在有left-to-right和right-to-left的图像slots位置,您可以在其中放置图像。或者,您可以在代码中用UIImageAsset注册成对的图像,如本章前面所示。

您还可以通过调用图像的withHorizontallyFlippedOrientation方法强制图像水平翻转,而不考虑布局方向或语义内容属性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值