以前住在上海,冬天冷的時候常常到 0~2度之間,還記得小時候會在窗戶上吐氣弄出一團霧,然後在上面畫個笑臉,透過笑臉可以看到外面的景色。
這個效果和刮刮卡很像,在可見物上面在塗一層東西,當去掉這層東西後,就可以看到底下的內容了,這次就在 iOS 做一個看看吧。
Scratch Card
就如同上面的動畫,要做到:
- 通過手指在畫面上移動的時候,可以擦拭藍色漸層的畫面,而顯示出背後的人像圖片。
- 而當我們點最上方的橡皮擦時,可以還原。
Mask
要實現上面的方法可能很多,但這次要利用 mask 來實現這個功能。
因為有了 mask 的,要顯示某一張圖的部分內容就顯得特別的方便,尤其是不規則的形狀。
比如下面的圖,我們的 mask 是原型又或者是星星,這張圖片圖片就能呈現出不同的形狀。
有了 mask 我們也能實現我們要的刮刮卡功能了
- cover 就是我們的遮擋層
- content 是去掉遮擋層後能看到的內容
- mask 是 content 的 mask 也就是只有 mask 和 content 重疊的部分我們才能夠看到內容。
View Hierarchy
我們在畫面上依次放入 cover / content / mask
這時候會發現畫面上只會看到一篇橘色。
而當 content 設定 mask 以後,就變成只能看見那張圖片,因為 mask 和圖片完整的重疊到了。
但如果我們將 mask 的背景色改為 clear 以後呢?就會變成只看得見藍色漸層的畫面。
重點來了,因為 mask 上有多少內容,就能看到多少圖片,而圖片是在 cover 之上的,所以圖片能顯示多少,就會改掉多少藍色漸層的畫面,
從而達到了視覺上的「刮掉藍色漸層」的效果。
SKScratchCardView
建立一個繼承於 UIView 的 SKScratchCardView 對外提供設定 contentView 和 coverView 的方法。
拿到 contentView & coverView 以後,設定 frame 並且設定 UIPanGesture.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
func setupWith(coverView:UIView, contentView:UIView) { // cover view self.coverView = coverView // content view self.contentView = contentView // maskView self.contentMaskView.frame = coverView.frame self.contentMaskView.backgroundColor = UIColor.clear self.contentMaskView.strokeWidth = 40.0 // addSubviews addSubviewFullscreen(self.coverView) addSubviewFullscreen(self.contentView) addSubviewFullscreen(self.contentMaskView) // set mask self.contentView.mask = self.contentMaskView // add gesture setupPanGesture() } |
當使用者在 SKScratchCardView 滑動的時候,我們將滑動的事件交給 Mask (SKScratchCardMaskView)
SKScratchCardMaskView
當拿到 panGestureRecognizer 的時候,我們開始在 mask 上面畫線。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@objc func panGestureRecognizer(_ recognizer:UIPanGestureRecognizer) { let location = recognizer.location(in: self) switch recognizer.state { case .began: beginPath(at: location) case .changed: addLine(to: location) default: closePath() } } |
通過 CGMutablePath 來一次完整的畫畫過程(手指按下到拿起)
並將每一次畫好的內容都存在 paths 中
1 |
fileprivate var paths: [CGMutablePath] = [] |
紀錄畫面上滑過的路線
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public func beginPath(at point: CGPoint) { currentPath = CGMutablePath() currentPath?.move(to: point) setNeedsDisplay() } public func addLine(to point: CGPoint) { currentPath?.addLine(to: point) setNeedsDisplay() } public func closePath() { if let currentPath = currentPath { paths.append(currentPath) } currentPath = nil setNeedsDisplay() } |
畫的過程或結束時,通過 setNeedsDisplay 方法來觸發 Draw
draw 方法會將已經畫過 ( paths ) 和最後畫的 (contentPath) 的內容畫在畫面上。
1 2 3 4 5 6 7 8 9 10 11 |
override func draw(_ rect: CGRect) { let context = UIGraphicsGetCurrentContext() context?.setStrokeColor(strokeColor.cgColor) context?.setLineWidth(strokeWidth) context?.setLineCap(.round) for path in paths + [currentPath].flatMap({$0}) { context?.addPath(path) context?.strokePath() } } |
如果要清除畫好的內容,也只需要清空 paths 然後在通過 setNeedsDisplay 觸發 draw 方法達到畫面上沒內容,從而看不到人像圖片。
1 2 3 4 |
public func clearCanvas() { paths.removeAll() setNeedsDisplay() } |
參考
- 官方文件 – CGMutablePath
- 可以到 Github 上看對應的 Source Code 會更好理解。
董哥,請教一下…這段是意思是…每段線都重畫一次嗎?
[currentPath].flatMap({$0}) 的功能是…?
for path in paths + [currentPath].flatMap({$0}) { … }
董哥很忙 讓粉絲來服務您,
[currentPath].flatMap({$0}) 應該只是要過濾nil的狀況 不然無法進行+的行為
swift4.1以後改為compactMap
哈哈~最近真的忙的忙,而且學 Backend 一段時間了
感謝你啦 XD