今天在簡書上看到了一個刮刮樂的demo,作者的思路很有意思,推薦大家去閱讀下。
最近的項目要做im,有下面的場景:

這個氣泡的實(shí)現(xiàn)用到了maskLayer,正好可以實(shí)現(xiàn)一個刮獎的demo。于是乎...搞起!
maskLayer介紹
CALayer有一個mask屬性,這便是我們今天的主角??聪滤歉墒裁吹?
/* A layer whose alpha channel is used as a mask to select between the
* layer's background and the result of compositing the layer's
* contents with its filtered background. Defaults to nil. When used as
* a mask the layer's `compositingFilter' and `backgroundFilters'
* properties are ignored. When setting the mask to a new layer, the
* new layer must have a nil superlayer, otherwise the behavior is
* undefined. Nested masks (mask layers with their own masks) are
* unsupported. */
@property(nullable, strong) CALayer *mask;
簡單理解就是,如果mask不為nil,那么mask以內(nèi)的區(qū)域會顯示layer本身的內(nèi)容,mask以外的區(qū)域會顯示layer后面的內(nèi)容(相當(dāng)于透明)。這里需要兩點(diǎn)注意:
-
mask必須是一個獨(dú)立的layer,不能擁有super layer - 不支持嵌套的
mask
上圖

View Hierarchy
沒用Reveal,大伙湊活看吧。


主要三個View:
- 背景UIImageView--scratch_bg.png(藍(lán)色背景)
- ScratchView--設(shè)置
mask的自定義view - UILabel--顯示刮獎結(jié)果,可以根據(jù)具體需求改為其他view
工作原理
如上所示,mask的設(shè)置在ScratchView中,捕獲手指的移動創(chuàng)建mask的layer并設(shè)置給ScratchView。
這樣一來,mask區(qū)域內(nèi)顯示ScratchView本身的內(nèi)容(ScratchView的子view),mask區(qū)域外繼續(xù)顯示ScratchView后面的內(nèi)容(背景圖)。
如何繪制maskLayer?
首先要明白,mask是一個CALayer,創(chuàng)建一個不規(guī)則的CALayer首選CAShapeLayer ;
其次,CAShapeLayer通過path來定義形狀,我們的目標(biāo)就是把用戶的每一次移動軌跡通過path來表示;
再其次,用戶移動軌跡必然不能通過一個path來表示(做path的union操作......想都不敢想),所以我們把每個用戶軌跡用一個CAShapeLayer表示,然后通過addSublayer方法添加到mask中。
最后,明白了我們的繪制方法,剩下最后的問題就是如何繪制path。為了體現(xiàn)出用戶移動軌跡的圓滑邊界和手指寬度,我們需要在每次移動之后繪制一個從上一次起點(diǎn)到此次終點(diǎn)的圓柱型path,如下圖:

Code
ScratchView.h定義如下:
#import <UIKit/UIKit.h>
IB_DESIGNABLE
@interface ScratchView : UIView
@property (nonatomic) IBInspectable CGFloat scratchLineWidth;
@end
scratchLineWidth用來表示圓柱形軌跡的寬度。
ScratchView.m:
#import "ScratchView.h"
@interface ScratchView ()
{
CGPoint startPoint;
}
@property (nonatomic, strong) CALayer * maskLayer;
@end
@implementation ScratchView
- (void) awakeFromNib
{
[super awakeFromNib];
self.layer.mask = [CALayer new];
}
- (void) touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouches] anyObject];
CGPoint touchLocation = [touch locationInView:self];
startPoint = touchLocation;
}
- (void) touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouches] anyObject];
CGPoint touchLocation = [touch locationInView:self];
CAShapeLayer * layer = [CAShapeLayer new];
layer.path = [self getPathFromPointA:startPoint toPointB:touchLocation].CGPath;
if(!_maskLayer){
_maskLayer = [CALayer new];
}
[_maskLayer addSublayer:layer];
self.layer.mask = _maskLayer;
startPoint = touchLocation;
}
- (void) touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouches] anyObject];
CGPoint touchLocation = [touch locationInView:self];
CAShapeLayer * layer = [CAShapeLayer new];
layer.path = [self getPathFromPointA:startPoint toPointB:touchLocation].CGPath;
if(!_maskLayer){
_maskLayer = [CALayer new];
}
[_maskLayer addSublayer:layer];
self.layer.mask = _maskLayer;
}
- (UIBezierPath *) getPathFromPointA:(CGPoint)a toPointB : (CGPoint) b
{
UIBezierPath * path = [UIBezierPath new];
UIBezierPath * curv1 = [UIBezierPath bezierPathWithArcCenter:a radius:self.scratchLineWidth startAngle:angleBetweenPoints(a, b)+M_PI_2 endAngle:angleBetweenPoints(a, b)+M_PI+M_PI_2 clockwise:b.x >= a.x];
[path appendPath:curv1];
UIBezierPath * curv2 = [UIBezierPath bezierPathWithArcCenter:b radius:self.scratchLineWidth startAngle:angleBetweenPoints(a, b)-M_PI_2 endAngle:angleBetweenPoints(a, b)+M_PI_2 clockwise:b.x >= a.x];
[path addLineToPoint:CGPointMake(b.x * 2 - curv2.currentPoint.x, b.y * 2 - curv2.currentPoint.y)];
[path appendPath:curv2];
[path addLineToPoint:CGPointMake(a.x * 2 - curv1.currentPoint.x, a.y * 2 - curv1.currentPoint.y)];
[path closePath];
return path;
}
CGFloat angleBetweenPoints(CGPoint first, CGPoint second) {
CGFloat height = second.y - first.y;
CGFloat width = first.x - second.x;
CGFloat rads = atan(height/width);
return -rads;
}
@end
在- (void) awakeFromNib中執(zhí)行self.layer.mask = [CALayer new];可以把當(dāng)前view設(shè)置為全透。
- (UIBezierPath *) getPathFromPointA:(CGPoint)a toPointB : (CGPoint) b方法負(fù)責(zé)生成兩點(diǎn)之間的圓柱型path。
每當(dāng)用戶移動一小段距離之后,我們便創(chuàng)建一個新的CAShapeLayer,添加到mask中。
Next
- 因為在
touchesMoved和touchesEnded會創(chuàng)建新對象并且add到mask中,無疑會持續(xù)消耗內(nèi)存,還是要考慮添加一些path union之類的策略。準(zhǔn)備從CALayer的- (BOOL)containsPoint:(CGPoint)p;方法入手。