手把手教你使用Quartz2D製作彩色塗鴉板和手勢解鎖

NO IMAGE

轉載自:http://www.jianshu.com/p/593c11453e07

我們已經學習完了
Quartz2D的一些基本的用法
,在實際開發過程中,經常使用Quartz2D,可以幫助我們少使用蘋果自帶的控制元件,直接畫圖到上下文,對系統的效能是一個非常好的優化方式。Quartz2D的功能強大,絕逼不是畫線,繪製圖片那麼easy,今天講一下他在實際專案中的應用,順便將思路理清楚,方便大家看

塗鴉板demo
,還有
手勢解鎖

文章中的幾個demo

  • 1.使用圖形上下文製作 塗鴉板
  • 2.使用貝塞爾路徑製作 塗鴉板
  • 3.手勢解鎖

下面詳細的介紹一下專案的思路

一.使用圖形上下文製作 塗鴉板

效果圖

點選儲存,在相簿中的圖片

分析

1.塗鴉板實際上就是繪製很多的線條

2.儲存線條,使用可變陣列

3.使用上下文繪製圖片,使用drawRect方法

4.和螢幕互動,應該使用touchesBegin方法

程式碼分析

1.自定義一個DBPainterView

2.在view中生成一個可變陣列作為變數,懶載入處理,可以供程式使用

//MARK: - 懶載入屬性
//用於盛放所有單個路徑的陣列
private lazy var pointArr:NSMutableArray = {
return NSMutableArray()
}()

3.實現touchesBegin,touchesMoved,touchesEnd方法

//MARK: - 重寫touch三個方法
//touchBegin
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let startPoint = touch?.locationInView(touch?.view)
let linePathArr = NSMutableArray()
linePathArr.addObject(NSValue.init(CGPoint: startPoint!))
pointArr.addObject(linePathArr)
setNeedsDisplay()
}
//touchMoved
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let startPoint = touch?.locationInView(touch?.view)
let lastLineArr = pointArr.lastObject
lastLineArr!.addObject(NSValue.init(CGPoint: startPoint!))
setNeedsDisplay()
}
//touchEnd
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let startPoint = touch?.locationInView(touch?.view)
let lastLineArr = pointArr.lastObject
lastLineArr!.addObject(NSValue.init(CGPoint: startPoint!))
setNeedsDisplay()
}

程式碼分析,

3.1. touchesBegin 就是開始繪製,現在沒有拿到路徑的具體的點,所以我們應該給每一個路勁用一個小陣列儲存所有點的陣列
linePathArr (儲存每一根line的陣列),每一次呼叫都應該是建立一個新的路徑(新的linePathArr),然後加到儲存所有路徑的陣列中(
pointArr儲存了所有line的陣列 ),然後呼叫 setNeedsDisplay 方法,繪製路徑

3.2. touchesMoved 方法是手指在螢幕移動的時候呼叫的,頻率最高,就是一直在新增point,說白了,就是給最新新增的那個路徑新增點,所以應當找到陣列中最後一個路徑,然後給這個路徑新增point,
let lastLineArr = pointArr.lastObject , lastLineArr!.addObject(NSValue.init(CGPoint: startPoint!))

3.3. touchEnd 方法和 2 的事情是一樣的,所以可以提煉一下程式碼,我就不寫了

4.繪製圖片 drawRect

override func drawRect(rect: CGRect) {
let ctx = UIGraphicsGetCurrentContext()
for index in 0 ..< pointArr.count
{
//獲取單根線
let linePathArr = pointArr.objectAtIndex(index)
for j in 0 ..< linePathArr.count
{
let point = linePathArr.objectAtIndex(j).CGPointValue()
if j == 0 {
CGContextMoveToPoint(ctx, point.x, point.y)
}else
{
CGContextAddLineToPoint(ctx, point.x, point.y)
}
}
}
//設定上下文的屬性
CGContextSetLineWidth(ctx, 3)
UIColor.redColor().set()
CGContextSetLineCap(ctx, CGLineCap.Round)
//渲染
CGContextStrokePath(ctx)
}
}

4.1 首先遍歷大陣列A,獲取每一條線(所有點)的陣列B,遍歷B中所有的點,但是B中的第一個b[0]應該是呼叫 CGContextMoveToPoint ,b[其他]應當呼叫
CGContextAddLineToPoint 方法,

4.2.可以設定一下圖形上下文的屬性,最後渲染就好了.

4.3 可以設定好多種顏色,使用 圖形上下文棧 就可以實現

5.DBPainterView 對外實現的“上一步”,”清空”,“儲存”功能

//刪除
func clear(){
pointArr.removeAllObjects()
setNeedsDisplay()
}
//上一步
func preview()
{
pointArr.removeLastObject()
setNeedsDisplay()
}
//儲存到本地
func saveToAbum() {
//儲存圖片的事件
UIGraphicsBeginImageContextWithOptions(self.frame.size, false, 0.0)
let ctx = UIGraphicsGetCurrentContext()
self.layer.renderInContext(ctx!)
//獲取圖片
let image = UIGraphicsGetImageFromCurrentImageContext()
//結束點陣圖上下文
UIGraphicsEndImageContext()
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}

程式碼太簡單,就不解釋了哈

二.使用貝塞爾路徑製作 塗鴉板

彩色畫板

剛才使用了圖形上下文繪製路徑,感覺還行,但是可以簡化,剛才說的將一個路徑的所有點放到路徑的陣列中,然後根據點來繪製,可以理解,但是會很麻煩,因為底層就是通過
CGContextPathRef 繪製路徑的,因為 CGContextPathRef 是C語言,大陣列不能新增它,所以我們放棄,然後選擇
貝塞爾路徑 ,他是oc中物件,非常適合製作塗鴉板

//繪製一條路徑的寫法,非常的簡單
let path = UIBezierPath()
path.moveToPoint(CGPoint.init(x: 9, y: 9))
path.addLineToPoint(CGPoint.init(x: 40, y: 50))
path.stroke()

使用貝塞爾路徑製作塗鴉板的步驟(和圖形上下文基本一致)

  • 1.懶載入一個用來橙裝所有路徑的陣列 pathArr
  • 2. touchesBegin 的時候,生成一個路徑,呼叫moveToPoint方法,新增起點,將path儲存到陣列中
  • 3.更改線寬和更改線的顏色,要個自定義的view設定lineWidth,和lineColor這個屬性,最後要去給path設定這兩個屬性
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let startPoint = touch?.locationInView(touch?.view)
//1.建立路徑
let path = UIBezierPath()
//2.設定起點
path.moveToPoint(startPoint!)
//3.將path,新增到pathArr上
pathArr.addObject(path)
//4.繪圖
setNeedsDisplay()
}
  • 3. touchesMovedtouchesEnd 方法功能一致,就合二為一了,就是獲取大陣列中最後一個路徑,然後呼叫
    addLineToPoint 方法
//touchMoved
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
addPointToPath(touches)
}
//touchEnd
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
addPointToPath(touches)
}
//touchMoved和touchEnd統一的程式碼
private func addPointToPath(touches: Set<UITouch>){
let touch = touches.first
let movePoint = touch?.locationInView(touch?.view)
//獲取最後一個path
let path = pathArr.lastObject as! UIBezierPath;
path.addLineToPoint(movePoint!)
setNeedsDisplay()
}

4.繪製路徑

override func drawRect(rect: CGRect) {
//繪製線條
for index in 0 ..< pathArr.count
{
let path = pathArr[index] as! UIBezierPath
path.stroke()
}
}

5.新增線寬和線顏色的屬性

//設定一個變數,用來儲存線寬
var lineWidth:CGFloat = 2;
//設定一個變臉,用來儲存線顏色
var lineColor:UIColor = UIColor.blackColor();

5.1 我們要將顏色和寬的的屬性使用到以後的線上,不能影響到過去的,所以,應該在生成一個path的時候,直接設定他的這兩個屬性,因為path中沒有lineColor這個屬性,所以自定義一個
DBBezierPath

class DBBezierPath: UIBezierPath {
var lineColor:UIColor?
}

5.2 重新修改一下 touchesBegin 方法

//touchBegin
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let startPoint = touch?.locationInView(touch?.view)
//1.建立路徑
let path = DBBezierPath()
path.lineWidth = lineWidth
//2.設定線條的顏色
path.lineColor = lineColor
//2.設定起點
path.moveToPoint(startPoint!)
//3.將path,新增到pathArr上
pathArr.addObject(path)
//4.繪圖
setNeedsDisplay()
}

5.3 在渲染的時候,我們要將自定義的 lineColor 取出來,渲染

override func drawRect(rect: CGRect) {
//繪製線條
for index in 0 ..< pathArr.count
{
let path = pathArr[index] as! DBBezierPath
path.lineColor!.set()
path.stroke()
}
}

5.4 這樣就可以製作出彩色的畫板了,而且其他儲存,上一步等功能都可以正常使用

使用了貝塞爾路徑,遠離了兩個陣列,執行和理解起來超級簡單?

三.手勢解鎖

要做這樣的手勢解鎖控制元件

思路和注意點

  • 1.建立基本的九宮格樣式UI
  • 2.抽取方法類和自定義一個button(注意 btn.userInteractionEnabled = false
  • 3.提出工具方法
    3.1 獲取當前的觸控點
    3.2 判斷當前點是不是在btn中
  • 4.儲存選中的所有按鈕
  • 5.通過選中的按鈕連線
    5.1 防止陣列中多次新增同一個button
  • 6.使用touchesEnd方法清空陣列
    6.1 給drawRect方法新增判空的條件
    6.2 重新繪製狀態
    6.3 使用makeObjectsSelect方法讓所有的button的選中狀態為NO
  • 7.繪製最後一個按鈕和手指移動的地點的連線
    7.1 在touchesMoved方法中儲存手指的所在的point
    7.2 在drawRect方法中連結最後一個按鈕和point
    7.3 設定bezierPath的基本屬性
    7.4 解決剛剛點選第一個按鈕,但是連線到CGPointZore的bug(在TouchBegin中清空,在drawRect判斷)
  • 8.減小觸控button的響應範圍
  • 9.拼接使用者的觸控路徑
  • 10.新增代理方法,讓外界知道使用者的觸控路徑(給代理新增IBOut)
  • 11.修改連線的具體顏色

程式碼講解分析

1.建立基本的九宮格樣式UI

基本目標

  • 1.1 自定義一個GULockView,然後使用經典九宮格演算法實現
//設定初始化函式,建立9個按鈕
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUpBsicUI()
}
//初始化函式
private func setUpBsicUI()
{
//建立九個按鈕
for index in 0 ..< 9
{
let btn = GUButton.init(type: UIButtonType.Custom);
addSubview(btn)
btn.tag = index
btn.addTarget(self, action: "btnClick:", forControlEvents: UIControlEvents.TouchUpInside)
}
}
  • 1.2 佈局UI
//設定九個按鈕的位置
override func layoutSubviews() {
super.layoutSubviews()
//經典的九宮格演算法
let totalColume = 3
let bWidth:CGFloat = 74
let bHeight:CGFloat = bWidth
let margin = (self.frame.width - bWidth * CGFloat(totalColume))/CGFloat(totalColume 1)
//自己的高度是bHeight*3
for index in 0 ..< self.subviews.count
{
let currentRow = index/totalColume
let currentColumn = index%totalColume
let bX = margin   (CGFloat(currentColumn) * (bWidth margin))
let bY = CGFloat(currentRow) * (bHeight margin)
let btn = self.subviews[index]
btn.frame = CGRectMake(bX, bY, bWidth, bHeight)
}
}

2.抽取方法類和自定義一個button

  • 2.1 自定義一個 GUButton ,設定內部的圖片等樣式,
setImage(UIImage.init(named: "gesture_node_normal"), forState: UIControlState.Normal)
setImage(UIImage.init(named: "gesture_node_highlighted"), forState: UIControlState.Selected)
setImage(UIImage.init(named: "gesture_node_disable"), forState: UIControlState.Disabled)
contentMode = UIViewContentMode.Center
userInteractionEnabled = false
  • 2.2 btn.userInteractionEnabled = false 一定要寫

userInteractionEnabled = false

userInteractionEnabled = ture

這個涉及到了時間傳遞,我們馬上要去實現觸控的三個方法,如果手勢路過btn,恰巧 userInteractionEnabled = ture ,那麼手勢直接讓btn截獲,那麼
GULockView 獲取不到手勢,造成了問題,所以一定要設定為no,(如果就是不想設定為no,其實也可以在自定義的CGButton中實現touches三個方法,呼叫super.touches三個方法往上傳遞,不推薦)

3.提出工具方法,設定btn被選中的條件

3.1 獲取當前的觸控點

/**
獲取是否當前觸控點的座標
:returns: 所在的點的座標
*/
private func pointWithTouches(touches: Set<UITouch>) -> CGPoint?
{
let touch = touches.first
let locPoint = touch?.locationInView(touch?.view)
return locPoint
}

3.2 判斷當前點是不是在btn中

/**
判斷是否選中的點的依據
:param: point 當前點
:returns: 所在的按鈕,可能沒有
*/
private func buttonWithPoint(point:CGPoint) -> UIButton?
{
//遍歷陣列,看看是不是在9個按鈕的裡面
for index in 0 ..< self.subviews.count
{
let b = self.subviews[index] as! UIButton
let isIn = CGRectContainsPoint(b.frame, point)
if isIn {
return b
}
}
return nil
}

3.2 設定btn被選中的狀態

//touchesMoved 和 touchesBegin此刻的內容都是這個
override func touchesMoved(touches: Set<UITouch>,
withEvent event: UIEvent?) {
let point = pointWithTouches(touches)
let locBtn = buttonWithPoint(point!)
if (locBtn != nil)  {
locBtn?.selected = true
}
setNeedsDisplay()
}

4.儲存選中的所有按鈕

可以通過一個陣列來儲存所有的按鈕

//MARK: - 懶載入陣列
private lazy var btns:NSMutableArray = NSMutableArray()

touchesBegantouchesMoved 方法中新增所選擇的按鈕

if (locBtn != nil) {
locBtn?.selected = true
//新增到陣列中
btns.addObject(locBtn!)
}

5.通過選中的按鈕連線

5.1 防止陣列中多次新增同一個button

出現了問題,陣列中新增了重複的btn

解決方法

//在判斷的時候,新增一個條件,是否selected == false 
if (locBtn != nil && locBtn?.selected == false) {
locBtn?.selected = true
//新增到陣列中
btns.addObject(locBtn!)
}

6.使用touchesEnd方法清空陣列

//手勢釋放時,要做的事情
//1.應當清空陣列,
//2.應當將所選中的按鈕全部設定為selected == false 
//3.重新繪製
//遍歷所有的陣列,是他的select == false
for index in 0 ..< btns.count
{
let btn = btns[index] as! UIButton
btn.selected = false
}
btns.removeAllObjects()
setNeedsDisplay()

6.1 給drawRect方法新增判空的條件

//算是優化吧,已經入drawRect方法,首先去判斷一下是不是空的
if btns.count == 0 {
return
}

6.2 重新繪製狀態

override func drawRect(rect: CGRect) {
if btns.count == 0 {
return
}
//使用UIBezierPath繪製路徑
let bezierPath = UIBezierPath()
for index in 0 ..< btns.count
{
//獲取每一個點
let btn = btns[index]
if index == 0 {
bezierPath.moveToPoint(btn.center)
}else{
bezierPath.addLineToPoint(btn.center)
}
}
bezierPath.lineWidth = 10
bezierPath.lineJoinStyle = CGLineJoin.Round
UIColor.blueColor().set()
bezierPath.stroke()
}

7.繪製最後一個按鈕和手指移動的地點的連線

7.1 在touchesMoved方法中儲存手指的所在的point

/// 當前觸控點,預設是零
private var currentPoint:CGPoint = CGPointZero

7.2 在drawRect方法中連結最後一個按鈕和point

//drawRect方法中,可以這樣實現
bezierPath.addLineToPoint(currentPoint)

7.3 設定bezierPath的基本屬性

bezierPath.lineWidth = 10
bezierPath.lineJoinStyle = CGLineJoin.Round
UIColor.blueColor().set()

currentPoint一直沒有清空,所以touchesBegin的時候,連結點都是過去的那個,只要清空就好

7.4 解決剛剛點選第一個按鈕,但是連線到CGPointZore的bug(在TouchBegin中清空,在drawRect判斷)

//新增最後一個線的和最後一個
if (CGPointEqualToPoint(CGPointZero,currentPoint) == false) {
bezierPath.addLineToPoint(currentPoint)
}

8.減小觸控button的響應範圍

現在的專案,你的滑鼠剛剛到一個btn的邊緣,就已經連結了線,這樣的體驗不好,我們想去設定當手勢到了btn的圓心才連線(減小連結線的響應範圍)

/**
判斷是否選中的點的依據
:param: point 當前點
:returns: 所在的按鈕,可能沒有
*/
private func buttonWithPoint(point:CGPoint) -> UIButton?
{
//遍歷陣列,看看是不是在9個按鈕的裡面
for index in 0 ..< self.subviews.count
{
let b = self.subviews[index] as! UIButton
let wh:CGFloat = 24
let frameX = b.center.x - wh * 0.5
let frameY = b.center.y - wh * 0.5
let isIn = CGRectContainsPoint(CGRectMake(frameX, frameY, wh, wh), point)
//            let isIn = CGRectContainsPoint(b.frame, point)
if isIn {
return b
}
}
return nil
}

9.拼接使用者的觸控路徑

//拼接字串,儲存使用者的觸控路徑
private func appendCode()
{
let code = NSMutableString()
for var btn in btns {
code.appendString("\(btn.tag)")
}
print(code)
}

10.新增代理方法,讓外界知道使用者的觸控路徑(給代理新增IBOut)

protocol GULockViewDelegate: NSObjectProtocol{
//獲取使用者的手勢密碼
func lockViewWithUserCode(lockView:GULockView,code:String)
}

新增屬性

//代理
weak var  delegate:GULockViewDelegate?

11.修改連線的具體顏色

你隨意吧~