CoreGraphic框架解析 (五)—— 基于CoreGraphic的一個(gè)簡單繪制示例 (一)

版本記錄

版本號 時(shí)間
V1.0 2018.10.21 星期日

前言

quartz是一個(gè)通用的術(shù)語,用于描述在iOSMAC OS X 中整個(gè)媒體層用到的多種技術(shù) 包括圖形、動(dòng)畫且轨、音頻、適配虚婿。Quart 2D 是一組二維繪圖和渲染API旋奢,Core Graphic會(huì)使用到這組APIQuartz Core專指Core Animation用到的動(dòng)畫相關(guān)的庫然痊、API和類至朗。CoreGraphicsUIKit下的主要繪圖系統(tǒng),頻繁的用于繪制自定義視圖剧浸。Core Graphics是高度集成于UIView和其他UIKit部分的锹引。Core Graphics數(shù)據(jù)結(jié)構(gòu)和函數(shù)可以通過前綴CG來識別。在app中很多時(shí)候繪圖等操作我們要利用CoreGraphic框架唆香,它能繪制字符串嫌变、圖形、漸變色等等躬它,是一個(gè)很強(qiáng)大的工具腾啥。感興趣的可以看我另外幾篇。
1. CoreGraphic框架解析(一)—— 基本概覽
2. CoreGraphic框架解析(二)—— 基本使用
3. CoreGraphic框架解析(三)—— 類波浪線的實(shí)現(xiàn)
4. CoreGraphic框架解析(四)—— 基本架構(gòu)補(bǔ)充

開始

首先看一下寫作環(huán)境

Swift 4, iOS 11, Xcode 9

想象一下,你已經(jīng)完成了你的應(yīng)用程序倘待,它工作正常疮跑,但界面缺乏風(fēng)格。 您可以在Photoshop中繪制所有自定義控件圖像的幾種尺寸延柠,并希望Apple不會(huì)出現(xiàn)@ 4x視網(wǎng)膜屏幕...或者祸挪,您可以提前思考并使用Core Graphics在代碼中創(chuàng)建一個(gè)圖像,可以清晰地縮放任何圖像設(shè)備尺寸贞间。

Core Graphics是Apple的矢量繪圖框架 - 它是一個(gè)強(qiáng)大而強(qiáng)大的API,需要學(xué)習(xí)很多東西雹仿。 但是從不擔(dān)心 - 這幾篇教程將通過簡單的開始讓您輕松進(jìn)入它增热,最后您將能夠創(chuàng)建可在您的應(yīng)用中使用的令人驚嘆的圖形。

這是一個(gè)全新的系列胧辽,采用現(xiàn)代方法教授Core Graphics峻仇。 該系列還包括@IBDesignable@IBInspectable等酷炫功能,使學(xué)習(xí)Core Graphics變得輕松有趣邑商。

是時(shí)候開始吧摄咆!


Introducing Flo – One glass at a time

您將創(chuàng)建一個(gè)完整的應(yīng)用程序來跟蹤您的飲水習(xí)慣。

具體而言人断,它可以輕松跟蹤您喝多少水吭从。 “他們”告訴我們,每天喝八杯水是健康的恶迈,但幾杯后很容易失去跟蹤涩金。 這就是Flo的用武之地;每當(dāng)你喝掉一杯清爽的水暇仲,點(diǎn)擊柜臺步做。 您還會(huì)看到之前七天消費(fèi)的圖表。

在本系列的第一部分中奈附,您將使用UIKit的繪圖方法創(chuàng)建三個(gè)控件全度。

然后在第二部分中,您將深入了解Core Graphics上下文并繪制圖形斥滤。

在第三部分中将鸵,您將創(chuàng)建一個(gè)帶圖案的背景,并為自己頒發(fā)一張自制的Core Graphics獎(jiǎng)牌中跌。

您的首要任務(wù)是創(chuàng)建自己的Flo應(yīng)用程序咨堤。 沒有下載可以讓你前進(jìn),因?yàn)槿绻銖念^開始構(gòu)建它漩符,你會(huì)學(xué)到更多一喘。

創(chuàng)建一個(gè)新項(xiàng)目(File \ New \ Project ...),選擇模板iOS \ Application \ Single View App并單擊Next。

填寫項(xiàng)目選項(xiàng)凸克。 將Product Name設(shè)置為Flo议蟆,將語言設(shè)置為Swift,然后單擊Next萎战。

在最后一個(gè)屏幕上咐容,取消選中Create Git repository并單擊Create

您現(xiàn)在有一個(gè)帶有故事板和視圖控制器的初學(xué)者項(xiàng)目蚂维。


Custom Drawing on Views - 視圖上的自定義繪圖

自定義繪圖有三個(gè)步驟:

  • 1) 創(chuàng)建一個(gè)UIView子類戳粒。
  • 2) 覆蓋draw(_ :)并添加一些Core Graphics繪圖代碼。
  • 3) 沒有第3步 - 就是這樣虫啥!

你可以通過制作一個(gè)自定義繪制的加號按鈕來嘗試這個(gè)蔚约,如下所示:

創(chuàng)建一個(gè)新文件(File \ New \ File ...),選擇iOS \ Source \ Cocoa Touch Class涂籽,單擊Next苹祟。 在此屏幕中,將新類命名為PushButton评雌,使其成為UIButton的子類树枫,并確保語言為Swift。 單擊Next景东,然后單擊Create砂轻。

UIButton是UIView的子類,因此UIView中的所有方法(例如draw(_ :))也可以在UIButton中使用耐薯。

Main.storyboard中舔清,將UIButton拖到視圖控制器的視圖中,然后選擇Document Outline中的按鈕曲初。

Identity Inspector中体谒,更改類以使用您自己的PushButton

1. Auto Layout Constraints - 自動(dòng)布局約束

現(xiàn)在臼婆,您將設(shè)置自動(dòng)布局約束(文本說明如下):

  • 1) 選中該按鈕后抒痒,按住Control鍵從按鈕中心稍微向左拖動(dòng)(仍然在按鈕內(nèi)),然后從彈出菜單中選擇Width颁褂。
  • 2) 同樣故响,選擇按鈕后,按住Control鍵從按鈕中心稍微向上控制 - 拖動(dòng)(仍然在按鈕內(nèi))颁独,然后從彈出菜單中選擇Height彩届。
  • 3) 按住Control鍵從按鈕內(nèi)部向左拖動(dòng)到按鈕外部,然后選擇Center Vertically in Safe Area誓酒。
  • 4) 最后按住Control鍵從按鈕內(nèi)部向上拖動(dòng)到按鈕外部樟蠕,然后選擇Center Horizontally in Safe Area贮聂。

這將創(chuàng)建四個(gè)必需的自動(dòng)布局約束;您現(xiàn)在可以在Size Inspector中看到它們:

單擊Align center Y約束上的Edit寨辩,并將其常量設(shè)置為100吓懈。這會(huì)將按鈕的垂直位置從中心移動(dòng)到中心下方的100個(gè)點(diǎn)。 將WidthHeight約束常量更改為等于100靡狞。 最終約束應(yīng)如下所示:

Attributes Inspector中耻警,刪除默認(rèn)標(biāo)題Button

如果您愿意甸怕,可以在此時(shí)構(gòu)建和運(yùn)行甘穿,但是現(xiàn)在您只能看到一個(gè)空白屏幕。 是時(shí)候解決這個(gè)問題了梢杭!


Drawing the Button - 繪制按鈕

回想一下你想要制作的按鈕是圓形的:

要在Core Graphics中繪制形狀扒磁,您可以定義一條路徑,告訴Core Graphics要跟蹤的線(如加號的兩條直線)或要填充的線(如此處應(yīng)填充的圓)式曲。 如果您熟悉Photoshop中的Illustrator或矢量形狀,那么您將很容易理解路徑缸榛。

關(guān)于路徑有三個(gè)基本要點(diǎn):

  • 可以描邊和填充路徑吝羞。
  • stroke概述了當(dāng)前描邊顏色的路徑。
  • 填充將填充具有當(dāng)前填充顏色的閉合路徑内颗。

創(chuàng)建Core Graphics路徑的一種簡單方法是通過一個(gè)名為UIBezierPath的便捷類钧排。 這使您可以使用用戶友好的API輕松創(chuàng)建路徑,無論您是要基于線均澳,曲線恨溜,矩形還是一系列連接點(diǎn)創(chuàng)建路徑。

嘗試使用UIBezierPath創(chuàng)建路徑找前,然后用綠色填充它糟袁。 為此,請打開PushButton.swift并添加此方法:

override func draw(_ rect: CGRect) {
  let path = UIBezierPath(ovalIn: rect)
  UIColor.green.setFill()
  path.fill()
}

首先躺盛,創(chuàng)建一個(gè)橢圓形的UIBezierPath项戴,它是傳遞給它的矩形的大小。 在這種情況下槽惫,它將是您在故事板中定義的100×100按鈕的大小周叮,因此“橢圓”實(shí)際上將是一個(gè)圓圈。

路徑本身不會(huì)繪制任何東西界斜。 您可以定義沒有可用繪圖上下文的路徑仿耽。 要繪制路徑,請?jiān)诋?dāng)前上下文中設(shè)置填充顏色(下面有更多內(nèi)容)各薇,然后填充路徑项贺。

構(gòu)建并運(yùn)行應(yīng)用程序,您將看到綠色圓圈。

到目前為止敬扛,您已經(jīng)發(fā)現(xiàn)制作自定義形狀的視圖是多么容易晰洒。您已經(jīng)通過創(chuàng)建UIButton子類,覆蓋draw(_ :)并將UIButton添加到故事板來完成此操作啥箭。


Behind the Scenes in Core Graphics - 在Core Graphics的幕后

每個(gè)UIView都有一個(gè)圖形上下文(context)谍珊,視圖的所有繪圖在傳輸?shù)皆O(shè)備的硬件之前呈現(xiàn)在此上下文中。

每當(dāng)視圖需要更新時(shí)急侥,iOS都會(huì)通過調(diào)用draw(_ :)來更新上下文砌滞。這種情況發(fā)生在:

  • 該視圖是屏幕上的新視圖。
  • 它頂部的視圖被移動(dòng)了坏怪。
  • 視圖的hidden屬性已更改贝润。
  • 您的應(yīng)用程序顯式調(diào)用視圖上的setNeedsDisplay()setNeedsDisplayInRect()方法。

注意:在draw(_ :)中完成的任何繪圖都會(huì)進(jìn)入視圖的圖形上下文铝宵。請注意打掘,如果您在draw(_ :)之外開始繪制繪圖,正如您將在本教程的最后部分所做的那樣鹏秋,您將必須創(chuàng)建自己的圖形上下文尊蚁。

您還沒有在本教程中使用過Core Graphics,因?yàn)閁IKit包含許多Core Graphics函數(shù)的包裝器侣夷。例如横朋,UIBezierPathCGMutablePath的包裝器,CGMutablePath是較低級別的Core Graphics API百拓。

注意:永遠(yuǎn)不要直接調(diào)用draw(_ :)琴锭。如果您的視圖未更新,請?jiān)谝晥D上調(diào)用setNeedsDisplay()衙传。

setNeedsDisplay()本身不調(diào)用draw(_ :)决帖,但它將視圖標(biāo)記為'dirty',在下一個(gè)屏幕更新周期使用draw(_ :)觸發(fā)重繪粪牲。即使你在同一個(gè)方法中調(diào)用setNeedsDisplay()五次古瓤,你也只能實(shí)際調(diào)用draw(_ :)一次。


@IBDesignable – Interactive Drawing - 交互式繪圖

創(chuàng)建代碼來繪制路徑腺阳,然后運(yùn)行應(yīng)用程序以查看它看起來像油漆干燥一樣令人興奮落君,但你有選擇。 實(shí)時(shí)渲染(Live Rendering)允許視圖通過運(yùn)行draw(_ :)方法在故事板中更準(zhǔn)確地繪制自己亭引。 更重要的是绎速,故事板將立即更新為draw(_ :)中的更改。 您只需要一個(gè)屬性焙蚓!

仍然在PushButton.swift中纹冤,就在類聲明之前洒宝,添加:

@IBDesignable

這就是啟用實(shí)時(shí)渲染所需的全部內(nèi)容。 回到Main.storyboard并注意到萌京,現(xiàn)在雁歌,您的按鈕顯示為綠色圓圈葬毫,就像您構(gòu)建和運(yùn)行時(shí)一樣掖肋。

現(xiàn)在設(shè)置你的屏幕,以便你有故事板和代碼并排唉工。

通過選擇PushButton.swift顯示代碼來執(zhí)行此操作求妹,然后在右上角單擊Assistant Editor - 看起來像兩個(gè)交織在一起的環(huán)的圖標(biāo)乏盐。 然后故事板應(yīng)顯示在右側(cè)窗格中。 如果沒有制恍,則必須在窗格頂部的痕跡路徑中選擇故事板:

關(guān)閉故事板左側(cè)的文檔大綱以釋放一些空間父能。 通過拖動(dòng)文檔大綱窗格的邊緣或單擊故事板底部的按鈕來執(zhí)行此操作:

完成所有操作后,您的屏幕應(yīng)如下所示:

PushButtondraw(_:)中净神,改變

UIColor.green.setFill()

UIColor.blue.setFill()

你會(huì)(幾乎)立即看到故事板中的變化何吝。 太酷了!

現(xiàn)在鹃唯,您將為加號創(chuàng)建行岔霸。


Drawing Into the Context - 繪制到上下文

Core Graphics使用“繪制模型(painter’s model)”。 當(dāng)你畫一個(gè)上下文時(shí)俯渤,它幾乎就像畫一幅畫。 你鋪設(shè)一條路并填充它型宝,然后在頂部另一條路徑上鋪設(shè)路徑并填滿它八匠。 您無法更改已放置的像素,但可以在上面覆蓋“繪制”它們趴酣。

Apple的文檔中的這張圖片描述了它的工作原理梨树。 正如您在畫布上繪畫時(shí)一樣,繪制的順序至關(guān)重要岖寞。

你的加號是在藍(lán)色圓圈的頂部抡四,所以首先你編碼藍(lán)色圓圈然后加號。

您可以為加號繪制兩個(gè)矩形仗谆,但是更容易繪制路徑然后用所需的厚度描邊它指巡。

PushButton中添加此結(jié)構(gòu)和這些常量:

private struct Constants {
  static let plusLineWidth: CGFloat = 3.0
  static let plusButtonScale: CGFloat = 0.6
  static let halfPointShift: CGFloat = 0.5
}
  
private var halfWidth: CGFloat {
  return bounds.width / 2
}
  
private var halfHeight: CGFloat {
  return bounds.height / 2
}

現(xiàn)在在draw(_ :)方法的末尾添加此代碼以繪制加號的水平短劃線:

//set up the width and height variables
//for the horizontal stroke
let plusWidth: CGFloat = min(bounds.width, bounds.height) * Constants.plusButtonScale
let halfPlusWidth = plusWidth / 2

//create the path
let plusPath = UIBezierPath()

//set the path's line width to the height of the stroke
plusPath.lineWidth = Constants.plusLineWidth

//move the initial point of the path
//to the start of the horizontal stroke
plusPath.move(to: CGPoint(
  x: halfWidth - halfPlusWidth,
  y: halfHeight))

//add a point to the path at the end of the stroke
plusPath.addLine(to: CGPoint(
  x: halfWidth + halfPlusWidth,
  y: halfHeight))

//set the stroke color
UIColor.white.setStroke()

//draw the stroke
plusPath.stroke()

在此塊中,您設(shè)置UIBezierPath隶垮,為其指定一個(gè)起始位置(圓圈的左側(cè))并繪制到結(jié)束位置(圓圈的右側(cè))藻雪。 然后用白色描繪路徑輪廓。 此時(shí)狸吞,您應(yīng)該在Storyboard中看到這一點(diǎn):

在你的故事板中勉耀,你現(xiàn)在將有一個(gè)藍(lán)色圓圈指煎,中間有一個(gè)破折號:

注意:請記住,路徑只包含點(diǎn)便斥。 這是一個(gè)簡單的方法來掌握這個(gè)概念:創(chuàng)建路徑時(shí)想象你手中拿著筆至壤。 在頁面上放兩個(gè)點(diǎn),然后將筆放在起點(diǎn)枢纠,然后通過畫線畫一條線到下一個(gè)點(diǎn)像街。

這基本上就是你使用move(to :)addLine(to :)來處理上面的代碼。

現(xiàn)在在iPad 2或iPhone 6 Plus模擬器上運(yùn)行應(yīng)用程序京郑,你會(huì)發(fā)現(xiàn)破折號并不像應(yīng)該的那樣清晰宅广。 它有一條淡藍(lán)色的線環(huán)繞著它。


Points and Pixels - 點(diǎn)和像素

回到第一批iPhone的時(shí)代些举,點(diǎn)和像素占據(jù)了相同的空間并且大小相同跟狱,這使得它們基本上是相同的。 當(dāng)視網(wǎng)膜iPhone出現(xiàn)時(shí)户魏,屏幕上突然出現(xiàn)了相同數(shù)量點(diǎn)數(shù)的四倍像素驶臊。

同樣,iPhone 6 Plus再次增加了相同點(diǎn)的像素?cái)?shù)量叼丑。

注意:以下是概念性的 - 實(shí)際硬件像素可能不同关翎。 例如,在渲染3x后鸠信,iPhone 6 Plus會(huì)進(jìn)行縮減采樣以在屏幕上顯示完整圖像纵寝。 要了解有關(guān)iPhone 6 Plus下采樣的更多信息,請查看這篇精彩文章星立。

這是一個(gè)12×12像素的網(wǎng)格爽茴,其中的點(diǎn)以灰色和白色顯示。 第一個(gè)(iPad 2)是點(diǎn)到像素的直接映射绰垂。 第二個(gè)(iPhone 6)是2x視網(wǎng)膜屏幕室奏,其中一個(gè)點(diǎn)有4個(gè)像素,第三個(gè)(iPhone 6 Plus)是一個(gè)3x視網(wǎng)膜屏幕劲装,其中有一個(gè)點(diǎn)有9個(gè)像素胧沫。

你剛剛畫出的線高3點(diǎn)。 線條從路徑的中心開始劃線占业,因此在路徑中心線的兩側(cè)繪制1.5個(gè)點(diǎn)绒怨。

此圖顯示了在每個(gè)設(shè)備上繪制3點(diǎn)線。 您可以看到iPad 2和iPhone 6 Plus導(dǎo)致線條被劃分為半個(gè)像素 - 這當(dāng)然是無法完成的谦疾。 因此窖逗,iOS使用兩種顏色之間的顏色對半填充像素進(jìn)行反鋸齒處理,并且該線看起來模糊餐蔬。

實(shí)際上碎紊,iPhone 6 Plus擁有如此多的像素佑附,您可能不會(huì)注意到它的模糊性,盡管您應(yīng)該在設(shè)備上查看自己的應(yīng)用程序仗考。 但是音同,如果你正在開發(fā)像iPad 2或iPad mini這樣的非視網(wǎng)膜屏幕,你應(yīng)該盡一切可能避免抗鋸齒秃嗜。

如果你有奇怪的直線权均,你需要將它們放在正負(fù)0.5點(diǎn)以防止消除鋸齒。 如果你看一下上面的圖表锅锨,你會(huì)發(fā)現(xiàn)iPad 2上的一半點(diǎn)會(huì)將線條向上移動(dòng)半個(gè)像素叽赊,在iPhone 6上,向上移動(dòng)整個(gè)像素必搞,在iPhone 6 Plus上必指,向上移動(dòng)一個(gè)半像素。

draw(_ :)中恕洲,將move(to :)addLine(to :)代碼行替換為:

//move the initial point of the path
//to the start of the horizontal stroke
plusPath.move(to: CGPoint(
  x: halfWidth - halfPlusWidth + Constants.halfPointShift,
  y: halfHeight + Constants.halfPointShift))
    
//add a point to the path at the end of the stroke
plusPath.addLine(to: CGPoint(
  x: halfWidth + halfPlusWidth + Constants.halfPointShift,
  y: halfHeight + Constants.halfPointShift))

iOS現(xiàn)在將在所有三個(gè)設(shè)備上邊緣清晰的渲染線條塔橡,因?yàn)槟F(xiàn)在將路徑移動(dòng)了半個(gè)點(diǎn)。

注意:對于像素完美線條霜第,您可以繪制和填充UIBezierPath(rect :)而不是線葛家,并使用視圖的contentScaleFactor計(jì)算矩形的寬度和高度。 與從路徑中心向外繪制的筆劃不同泌类,填充僅在路徑內(nèi)繪制癞谒。

在前兩行代碼之后,在draw(_:)中設(shè)置筆觸顏色之前刃榨,添加加號的垂直筆劃扯俱。 我敢打賭,你可以自己弄清楚如何做到這一點(diǎn)喇澡,因?yàn)槟阋呀?jīng)繪制了一個(gè)水平筆劃:

//Vertical Line
 
plusPath.move(to: CGPoint(
  x: halfWidth + Constants.halfPointShift,
  y: halfHeight - halfPlusWidth + Constants.halfPointShift))
      
plusPath.addLine(to: CGPoint(
  x: halfWidth + Constants.halfPointShift,
  y: halfHeight + halfPlusWidth + Constants.halfPointShift))

這與您用于在按鈕上繪制水平線的代碼基本相同。

您現(xiàn)在應(yīng)該在故事板中看到加號按鈕的實(shí)時(shí)渲染殊校。 這樣就完成了加號按鈕的繪制晴玖。


@IBInspectable – Custom Storyboard Properties - 自定義sb屬性

你需要為用戶提供一個(gè)減號按鈕。

減號按鈕與加號按鈕相同为流,只是它沒有垂直條并且顏色不同呕屎。 您將對減號按鈕使用相同的PushButton類,并在將其添加到故事板時(shí)聲明它是什么類型的按鈕及其顏色敬察。

@IBInspectable是一個(gè)可以添加到屬性的屬性秀睛,使Interface Builder可以讀取它。 這意味著您將能夠在故事板中而不是在代碼中配置按鈕的顏色莲祸。

PushButton類的頂部蹂安,添加以下兩個(gè)屬性:

@IBInspectable var fillColor: UIColor = UIColor.green
@IBInspectable var isAddButton: Bool = true

更改draw(_:)頂部的填充顏色代碼

UIColor.blue.setFill()

fillColor.setFill() 

該故事板視圖中的按鈕將變?yōu)榫G色椭迎。

使用if語句將draw(_ :)中的垂直行代碼包圍:

//Vertical Line

if isAddButton {
  //vertical line code move(to:) and addLine(to:)
}
//existing code
//set the stroke color
UIColor.white.setStroke()
plusPath.stroke()

這使得只有在設(shè)置了isAddButton時(shí)才繪制垂直線 - 這樣按鈕可以是加號或減號按鈕。

完成的PushButton看起來像這樣:

import UIKit

@IBDesignable
class PushButton: UIButton {
  
  private struct Constants {
    static let plusLineWidth: CGFloat = 3.0
    static let plusButtonScale: CGFloat = 0.6
    static let halfPointShift: CGFloat = 0.5
  }
  
  private var halfWidth: CGFloat {
    return bounds.width / 2
  }
  
  private var halfHeight: CGFloat {
    return bounds.height / 2
  }
  
  @IBInspectable var fillColor: UIColor = UIColor.green
  @IBInspectable var isAddButton: Bool = true
  
  override func draw(_ rect: CGRect) {
    let path = UIBezierPath(ovalIn: rect)
    fillColor.setFill()
    path.fill()
    
    //set up the width and height variables
    //for the horizontal stroke
    let plusWidth: CGFloat = min(bounds.width, bounds.height) * Constants.plusButtonScale
    let halfPlusWidth = plusWidth / 2
    
    //create the path
    let plusPath = UIBezierPath()
    
    //set the path's line width to the height of the stroke
    plusPath.lineWidth = Constants.plusLineWidth
    
    //move the initial point of the path
    //to the start of the horizontal stroke
    plusPath.move(to: CGPoint(
            x: halfWidth - halfPlusWidth + Constants.halfPointShift,
            y: halfHeight + Constants.halfPointShift))
        
    //add a point to the path at the end of the stroke
    plusPath.addLine(to: CGPoint(
            x: halfWidth + halfPlusWidth + Constants.halfPointShift,
            y: halfHeight + Constants.halfPointShift))

    if isAddButton {
      //move the initial point of the path
      //to the start of the horizontal stroke
      plusPath.move(to: CGPoint(
        x: halfWidth - halfPlusWidth + Constants.halfPointShift,
        y: halfHeight + Constants.halfPointShift))
      
      //add a point to the path at the end of the stroke
      plusPath.addLine(to: CGPoint(
        x: halfWidth + halfPlusWidth + Constants.halfPointShift,
        y: halfHeight + Constants.halfPointShift))
    }
    
    //set the stroke color
    UIColor.white.setStroke()
    plusPath.stroke()
  }
}

在故事板中田盈,選擇按鈕視圖畜号。 使用@IBInspectable聲明的兩個(gè)屬性顯示在Attributes Inspector的頂部:

Fill Color更改為RGB(87, 218, 213),并將Is Add Button更改為off允瞧。 通過轉(zhuǎn)到Fill Color\Other…\Color Sliders并在顏色旁邊的每個(gè)輸入框中輸入值來更改顏色简软,所以它看起來像這樣:

更改將立即在故事板中進(jìn)行:

很酷,嗯述暂? 現(xiàn)在將Is Add Button更改為on痹升,將按鈕返回到加號按鈕。


A Second Button - 第二個(gè)按鈕

將新的UIButton添加到故事板并選擇它畦韭。 將其類更改為PushButton疼蛾,就像使用上一個(gè)類一樣:

綠色加號按鈕將在您的舊加號按鈕下繪制。

在Attributes Inspector中廊驼,將Fill Color更改為RGB(238,77,77)并將Is Add Button更改為off据过。

刪除默認(rèn)標(biāo)題Button。

與以前的方式類似妒挎,為新視圖添加自動(dòng)布局約束:

  • 選擇按鈕后绳锅,按住Control鍵從按鈕中心向左輕微拖動(dòng)(仍在按鈕內(nèi)),然后從彈出菜單中選擇Width酝掩。
  • 同樣鳞芙,在選中按鈕的情況下,按住Control鍵從按鈕中心稍微向上拖動(dòng)(仍在按鈕內(nèi))期虾,然后從彈出菜單中選擇Height原朝。
  • 控按住Control鍵從按鈕內(nèi)部向左拖動(dòng)到按鈕外部,然后選擇Center Horizontally in Safe Area镶苞。
  • 按住Control鍵從底部按鈕向上拖動(dòng)到頂部按鈕喳坠,然后選擇Vertical Spacing

添加約束后茂蚓,在Size Inspector中編輯它們的常量值以匹配以下值:

構(gòu)建并運(yùn)行應(yīng)用程序壕鹉。 您現(xiàn)在擁有可重復(fù)使用的可自定義視圖,您可以將其添加到任何應(yīng)用程序中聋涨。 它在任何尺寸的設(shè)備上都清脆銳利晾浴。 這是在iPhone 4S上。


Arcs with UIBezierPath - 使用UIBezierPath的弧

您將創(chuàng)建的下一個(gè)自定義視圖是:

這看起來像一個(gè)填充的形狀牍白,但弧實(shí)際上只是一個(gè)胖的描邊路徑脊凰。 輪廓是由兩個(gè)弧組成的另一條描邊路徑。

創(chuàng)建一個(gè)新文件File \ New \ File ...茂腥,選擇Cocoa Touch Class狸涌,并將新類命名為CounterView切省。 使它成為UIView的子類,并確保語言為Swift杈抢。 單擊Next数尿,然后單擊Create

將代碼替換為:

import UIKit

@IBDesignable class CounterView: UIView {
  
  private struct Constants {
    static let numberOfGlasses = 8
    static let lineWidth: CGFloat = 5.0
    static let arcWidth: CGFloat = 76
    
    static var halfOfLineWidth: CGFloat {
      return lineWidth / 2
    }
  }
  
  @IBInspectable var counter: Int = 5
  @IBInspectable var outlineColor: UIColor = UIColor.blue
  @IBInspectable var counterColor: UIColor = UIColor.orange
  
  override func draw(_ rect: CGRect) {
    
  }
}

在這里惶楼,您還可以創(chuàng)建一個(gè)包含常量的結(jié)構(gòu)體右蹦。這些常數(shù)將在繪圖時(shí)使用,奇數(shù)一個(gè) - numberOfGlasses - 是每天飲用的目標(biāo)水杯數(shù)歼捐。達(dá)到此數(shù)字時(shí)何陆,計(jì)數(shù)器將達(dá)到最大值。

您還可以創(chuàng)建三個(gè)可以在故事板中更新的@IBInspectable屬性豹储。變量counter跟蹤消耗的杯水?dāng)?shù)量贷盲,它是一個(gè)@IBDesignable屬性,因?yàn)樗軌蛟诠适掳逯懈乃郏@對于測試計(jì)數(shù)器視圖非常有用巩剖。

轉(zhuǎn)到Main.storyboard并在加上PushButton上方添加一個(gè)UIView。與以前的方式類似钠怯,為新視圖添加自動(dòng)布局約束:

  • 1) 選擇視圖后佳魔,按住Control鍵從按鈕中心稍微向左拖動(dòng)(仍然在視圖中),然后從彈出菜單中選擇Width晦炊。
  • 2) 同樣鞠鲜,在選擇視圖的情況下,按住Control鍵從按鈕中心稍微向上(仍在視圖中)進(jìn)行控制 - 拖動(dòng)断国,然后從彈出菜單中選擇Height贤姆。
  • 3) 按住Control鍵從視圖內(nèi)部向左拖動(dòng)到視圖外部,然后選擇Center Horizontally in Safe Area稳衬。
  • 4) 按住Control鍵從視圖向下拖動(dòng)到頂部按鈕霞捡,然后選擇Vertical Spacing

Size Inspector中編輯約束常量薄疚,如下所示:

Identity Inspector中碧信,將UIView的類更改為CounterView。 您在draw(_ :)中編碼的任何繪圖現(xiàn)在都會(huì)顯示在視圖中(但您還沒有添加任何圖形J涮椤)。


Impromptu Math Lesson - 即興數(shù)學(xué)課

我們暫時(shí)打斷了這個(gè)教程的簡短介紹慨畸,希望在高中水平數(shù)學(xué)方面不可怕莱坎。

上下文中的繪圖基于此單位圓。 單位圓是半徑為1.0的圓寸士。

紅色箭頭顯示弧的開始和結(jié)束位置檐什,以順時(shí)針方向繪制碴卧。 你將從3π/ 4弧度的位置繪制一個(gè)弧 - 相當(dāng)于135o,順時(shí)針到π/ 4弧度 - 即45o乃正。

弧度通常用于編程而不是度數(shù)住册,并且能夠以弧度進(jìn)行思考是有用的,這樣您每次想要使用圓時(shí)都不必轉(zhuǎn)換為度數(shù)瓮具。 稍后你需要弄清楚弧長荧飞,這是弧度發(fā)揮作用的時(shí)候。

單位圓中的圓弧長度(半徑為1.0)與弧度中的角度測量值相同名党。 例如叹阔,查看上圖,弧度從0o到90o的長度為π/ 2传睹。 要計(jì)算實(shí)際情況下弧的長度耳幢,請取單位圓弧長度并將其乘以實(shí)際半徑。

要計(jì)算上面紅色箭頭的長度欧啤,您只需要計(jì)算它跨越的弧度數(shù):

 2π – end of arrow (3π/4) + point of arrow (π/4) = 3π/2

轉(zhuǎn)換為度數(shù)就是

 360o – 135o + 45o = 270o

Back to Drawing Arcs - 回到繪制弧

CounterView.swift中睛藻,添加此代碼到draw(_:)以繪制弧:

// 1
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)

// 2
let radius: CGFloat = max(bounds.width, bounds.height)

// 3
let startAngle: CGFloat = 3 * .pi / 4
let endAngle: CGFloat = .pi / 4

// 4
let path = UIBezierPath(arcCenter: center,
                           radius: radius/2 - Constants.arcWidth/2,
                       startAngle: startAngle,
                         endAngle: endAngle,
                        clockwise: true)

// 5
path.lineWidth = Constants.arcWidth
counterColor.setStroke()
path.stroke()

下面進(jìn)行細(xì)分:

  • 1) 定義視圖的中心點(diǎn)邢隧,您可以在其中旋轉(zhuǎn)圓弧店印。
  • 2) 根據(jù)視圖的最大尺寸計(jì)算半徑。
  • 3) 定義弧的起始角和終止角府框。
  • 4) 根據(jù)剛剛定義的中心點(diǎn)吱窝,半徑和角度創(chuàng)建路徑。
  • 5) 在最終描邊路徑之前設(shè)置線寬和顏色迫靖。

想象一下用指南針繪制它 - 你將指南針的點(diǎn)放在中心院峡,將手臂打開到你需要的半徑,用粗筆裝上它并旋轉(zhuǎn)它以畫出你的弧線系宜。

在此代碼中照激,center是指南針的點(diǎn),radius是指南針打開的寬度(減去筆寬度的一半)盹牧,弧寬是指筆的寬度俩垃。

在故事板中以及運(yùn)行應(yīng)用程序時(shí),您將看到以下內(nèi)容:


Outlining the Arc

當(dāng)用戶表示他們已經(jīng)享用了一杯水時(shí)汰寓,counter上的輪廓顯示了朝向八杯水的目標(biāo)的進(jìn)展口柳。

該輪廓將包括兩個(gè)弧,一個(gè)外部和一個(gè)內(nèi)部有滑,以及兩條連接它們的線跃闹。

CounterView.swift中,將此代碼添加到draw(_:)結(jié)束:

//Draw the outline

//1 - first calculate the difference between the two angles
//ensuring it is positive
let angleDifference: CGFloat = 2 * .pi - startAngle + endAngle
//then calculate the arc for each single glass
let arcLengthPerGlass = angleDifference / CGFloat(Constants.numberOfGlasses)
//then multiply out by the actual glasses drunk
let outlineEndAngle = arcLengthPerGlass * CGFloat(counter) + startAngle

//2 - draw the outer arc
let outlinePath = UIBezierPath(arcCenter: center,
                                  radius: bounds.width/2 - Constants.halfOfLineWidth,
                              startAngle: startAngle,
                                endAngle: outlineEndAngle,
                               clockwise: true)

//3 - draw the inner arc
outlinePath.addArc(withCenter: center,
                       radius: bounds.width/2 - Constants.arcWidth + Constants.halfOfLineWidth,
                   startAngle: outlineEndAngle,
                     endAngle: startAngle,
                    clockwise: false)
    
//4 - close the path
outlinePath.close()
    
outlineColor.setStroke()
outlinePath.lineWidth = Constants.lineWidth
outlinePath.stroke()

這里要介紹幾件事:

  • 1) outlineEndAngle是弧應(yīng)該結(jié)束的角度,使用當(dāng)前counter值計(jì)算望艺。
  • 2) outlinePath是外弧苛秕。 半徑被賦予UIBezierPath()以計(jì)算弧的實(shí)際長度,因?yàn)樵摶〔皇菃挝粓A找默。
  • 3) 向第一個(gè)弧添加內(nèi)弧艇劫。 它具有相同的角度但反向繪制(順時(shí)針設(shè)置為false)。 此外惩激,這會(huì)自動(dòng)在內(nèi)弧和外弧之間畫一條線店煞。
  • 4) 關(guān)閉路徑會(huì)自動(dòng)在弧的另一端繪制一條線。

CounterView.swift中的counter屬性設(shè)置為5咧欣,您的CounterView現(xiàn)在應(yīng)該在故事板中如下所示:

打開Main.storyboard浅缸,選擇CounterView,在Attributes Inspector中魄咕,更改Counter屬性以檢查繪圖代碼衩椒。 你會(huì)發(fā)現(xiàn)它是完全互動(dòng)的。 嘗試將計(jì)數(shù)器調(diào)整為大于8且小于零哮兰。 你稍后會(huì)解決這個(gè)問題毛萌。

Counter Color更改為RGB(87,218,213),并將Outline Color更改為RGB(34,110,100)喝滞。


Making it All Work

恭喜阁将! 你有控制,您所要做的就是將它們連接起來右遭,以便加號按鈕遞增計(jì)數(shù)器做盅,減號按鈕遞減計(jì)數(shù)器。

Main.storyboard中窘哈,將UILabel拖動(dòng)到Counter View的中心吹榴,并確保它是Counter View的子視圖。 它在文檔大綱中看起來像這樣:

添加約束以垂直和水平居中標(biāo)簽滚婉。 最后图筹,標(biāo)簽應(yīng)該具有如下所示的約束:

Attributes Inspector中,將Alignment更改為center让腹,將font size更改為36远剩,將默認(rèn)標(biāo)簽標(biāo)題更改為8。

轉(zhuǎn)到ViewController.swift并將這些屬性添加到類的頂部:

//Counter outlets
@IBOutlet weak var counterView: CounterView!
@IBOutlet weak var counterLabel: UILabel!

仍然在ViewController.swift中骇窍,將此方法添加到類的末尾:

@IBAction func pushButtonPressed(_ button: PushButton) {
  if button.isAddButton {
    counterView.counter += 1
  } else {
    if counterView.counter > 0 {
      counterView.counter -= 1
    }
  }
  counterLabel.text = String(counterView.counter)
}

在這里瓜晤,您可以根據(jù)按鈕的isAddButton屬性遞增或遞減計(jì)數(shù)器,確保計(jì)數(shù)器不會(huì)降至零以下 - 沒有人可以喝負(fù)水腹纳。 您還可以更新標(biāo)簽中的計(jì)數(shù)器值痢掠。

還要將此代碼添加到viewDidLoad()的末尾哈恰,以確保counterLabel的初始值將更新:

counterLabel.text = String(counterView.counter)  

Main.storyboard中,連接CounterView outlet和UILabel outlet志群。 將方法連接到兩個(gè)PushButtonTouch Up Inside事件。

運(yùn)行該應(yīng)用程序蛔钙,看看您的按鈕是否更新了計(jì)數(shù)器標(biāo)簽锌云。 他們應(yīng)該。

但是等等吁脱,為什么計(jì)數(shù)器視圖不更新桑涎?

想想回到本教程的開頭,以及如何在移動(dòng)其上的其他視圖兼贡,或者更改其hidden屬性攻冷,或者視圖是屏幕新視圖或應(yīng)用程序調(diào)用時(shí),視圖上的setNeedsDisplay()setNeedsDisplayInRect()方法就會(huì)調(diào)用draw(_ :)方法遍希。

但是等曼,只要計(jì)數(shù)器屬性更新,計(jì)數(shù)器視圖就需要更新凿蒜,否則用戶會(huì)認(rèn)為您的應(yīng)用程序已被破壞禁谦。

轉(zhuǎn)到CounterView.swift并將counter屬性聲明更改為:

@IBInspectable var counter: Int = 5 {
  didSet {
    if counter <=  Constants.numberOfGlasses {
      //the view needs to be refreshed
      setNeedsDisplay()
    }
  }
}

此代碼使得僅當(dāng)計(jì)數(shù)器小于或等于用戶的目標(biāo)杯水?dāng)?shù)時(shí)視圖才會(huì)刷新,因?yàn)檩喞獌H上升到8废封。

再次運(yùn)行您的應(yīng)用州泊。 現(xiàn)在一切都應(yīng)該正常運(yùn)作。

后記

本篇主要講述了基于CoreGraphic的一個(gè)簡單繪制示例漂洋,感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末遥皂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子刽漂,更是在濱河造成了極大的恐慌演训,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件爽冕,死亡現(xiàn)場離奇詭異仇祭,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)颈畸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門乌奇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人眯娱,你說我怎么就攤上這事礁苗。” “怎么了徙缴?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵试伙,是天一觀的道長嘁信。 經(jīng)常有香客問我,道長疏叨,這世上最難降的妖魔是什么潘靖? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮蚤蔓,結(jié)果婚禮上卦溢,老公的妹妹穿的比我還像新娘。我一直安慰自己秀又,他們只是感情好单寂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吐辙,像睡著了一般宣决。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上昏苏,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天尊沸,我揣著相機(jī)與錄音,去河邊找鬼贤惯。 笑死椒丧,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的救巷。 我是一名探鬼主播壶熏,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼浦译!你這毒婦竟也來了棒假?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤精盅,失蹤者是張志新(化名)和其女友劉穎帽哑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體叹俏,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡妻枕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了粘驰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屡谐。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蝌数,靈堂內(nèi)的尸體忽然破棺而出愕掏,到底是詐尸還是另有隱情,我是刑警寧澤顶伞,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布饵撑,位于F島的核電站剑梳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏滑潘。R本人自食惡果不足惜垢乙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望语卤。 院中可真熱鬧侨赡,春花似錦、人聲如沸粱侣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽齐婴。三九已至,卻和暖如春稠茂,著一層夾襖步出監(jiān)牢的瞬間柠偶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工睬关, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留诱担,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓电爹,卻偏偏與公主長得像蔫仙,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子丐箩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容