開源項目-拼圖驗證控件TTGPuzzleVerify的實現

yqnet2013 7年前發布 | 17K 次閱讀 開源 SVG iOS開發 移動開發

最近抽空寫了個拼圖驗證控件,用戶可以通過水平、垂直,或者直接拖動拼圖塊,完成拼圖圖案,來完成驗證。拼圖塊的形狀可以自定義,默認提供了經典的拼圖形狀、圓形、正方形,整個拼圖的圖案也是支持從圖片生成的。

CocoaPods: pod "TTGPuzzleVerify"
 

接下來,說說TTGPuzzleVerify的整體設計思路和實現原理。

分析

組成部分

先直觀上分析一下控件的組成部分:

可以直觀看出,控件應該由三個部分組成:

  • 拼圖塊:就是從原圖上 “摳” 出來的拼圖形狀的圖片塊
  • 原始圖片:拼圖整體圖案的圖片
  • 鏤空背景:從原始圖片上摳出拼圖塊以后,給用戶提示用的,透明度更低的拼圖塊鏤空背景

拆解步驟

分析完了控件的組成,現在就來梳理一下控件的整個創建、交互流程。

  • 輸入:圖片、拼圖形狀、拼圖大小、鏤空的位置、拼圖塊的起始位置
  • 生成拼圖路徑:根據拼圖類型,生成對應的貝塞爾曲線
  • 從圖片摳出拼圖塊、鏤空:有了拼圖路徑,就可以摳出拼圖塊和鏤空的部分
  • 設置位置、樣式:根據參數設置拼圖塊的初始位置、樣式,如陰影、鏤空的透明度等
  • 用戶拖動:響應用戶的頭動手勢,移動拼圖塊,根據坐標判斷是否完成拼圖
  • 回調:用戶完成拼圖后,要回調,讓外部響應拼圖完成事件

關鍵技術點

從上面的步驟可以整理出來幾個關鍵的技術點,如下:

拼圖的路徑生成

普通的形狀,如正方形、圓形之類的還好,代碼實現比較方便,但是復雜的拼圖圖案怎么辦?

從圖片上“摳出”拼圖塊、鏤空

有了拼圖的形狀路徑,如何從圖片上摳出拼圖塊和鏤空?

用戶拖動拼圖、完成拼圖驗證

摳出了拼圖塊,如何將用戶的手勢對應到移動拼圖塊,最終完成驗證?

下面針對這三個點著重寫下實現原理。

實現

拼圖的路徑生成

既然手寫代碼太難,那就從現有的圖案生成代碼。

1. 找SVG素材

既然要能隨意改變大小,就要找矢量的圖,Google “free svg”、“free icon”之類的,很容易就能找到免費的矢量圖素材,例如TTGPuzzleVerify里面用的拼圖形狀就是從 iconmonstr.com 找來的。

2. 編輯

直接下載回來的SVG還需要調整一下,如去除背景,我是用Sketch來編輯的。

3. 生成代碼

拼圖的路徑本質上就是拼圖的輪廓,在Sketch里面把拼圖SVG的背景色等顏色刪除,然后就是將其轉換為代碼。

這里可以用 PaintCode plugin for Sketch ,將Sketch的拼圖矢量路徑導出成代碼。

4. 調整代碼

直接生成的代碼有時可能會不適用,可以人工調整下,然后就可以放到自己的類里面了:

+ (UIBezierPath *)classicPuzzlePath {
    static UIBezierPath *puzzleShape = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        puzzleShape = [UIBezierPath bezierPath];
        [puzzleShape moveToPoint:CGPointMake(17.45, 71.16)];
        // 此處省略很多行=。=,PaintCode生成的
        [puzzleShape closePath];
    });
    return puzzleShape;
}

5. UIBezierPath拼圖路徑的大小、位置調整

直接導出的拼圖路徑的大小、位置是固定的,但是控件是要可以支持改變大小、位置的,所以,要對路徑做變換,剛好 UIBezierPath 支持 CGAffineTransform 變換。

大小變換:

// 大小變換,path是生成的原始路徑,_puzzleSize是傳入的拼圖塊大小
[path applyTransform:CGAffineTransformMakeScale(_puzzleSize.width / path.bounds.size.width,
                                                _puzzleSize.height / path.bounds.size.height)];

位置變換:

// 位置變換,path是生成的原始路徑,_puzzleBlankPosition是傳入的拼圖塊、鏤空位置
[path applyTransform:CGAffineTransformMakeTranslation(
            _puzzleBlankPosition.x - path.bounds.origin.x,
            _puzzleBlankPosition.y - path.bounds.origin.y)];

至此,拼圖的路徑就寫好了。

從圖片上“摳出”拼圖塊、鏤空

1. 拼圖塊

有了UIBezierPath路徑,摳出拼圖塊還是比較容易的,直接用 CAShapeLayer 來 “mask” 圖片即可:

// 拼圖路徑
UIBezierPath *puzzlePath = [self getNewScaledPuzzledPath];

// 創建mask layer
CAShapeLayer *puzzleMaskLayer = [CAShapeLayer new];
puzzleMaskLayer.frame = self.bounds;
puzzleMaskLayer.path = puzzlePath.CGPath;

// Mask到圖片imageView上
_puzzleImageView.layer.mask = puzzleMaskLayer;

效果就是:

2. 鏤空圖片

摳出拼圖塊還是很容易的,但是“鏤空”怎么辦?

其實道理是一樣的,都是 mask ,只不過mask的區域反過來而已。

CAShapeLayer 的 fillRule 就可以調整“填充的規則”,此處用 kCAFillRuleEvenOdd 就可以了,文檔的解釋如下:

Specifies the even-odd winding rule. Count the total number of path crossings. If the number of crossings is even, the point is outside the path. If the number of crossings is odd, the point is inside the path and the region containing it should be filled.

理解過來就是:區域內一點,往外劃一條線,經過奇數個數的交點,則點在區域內,偶數個數的交點,則在區域外。

所以,只要在拼圖路徑的基礎上,補充最外部的邊框的路徑即可:

// 創建矩形邊框路徑
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRect:self.bounds];
// 添加拼圖路徑
[maskPath appendPath:[UIBezierPath bezierPathWithCGPath:puzzlePath.CGPath]];
// 設置規則
maskPath.usesEvenOddFillRule = YES;

// 創建mask的layer
CAShapeLayer *frontMaskLayer = [CAShapeLayer new];
frontMaskLayer.frame = self.bounds;
frontMaskLayer.path = maskPath.CGPath;
frontMaskLayer.fillRule = kCAFillRuleEvenOdd; // 填充規則

// 設置mask
_frontImageView.layer.mask = frontMaskLayer;

效果:

3. 鏤空背景

有了拼圖塊、鏤空,還差一塊,就是鏤空的“半透明背景”,其實這個“半透明背景”和拼圖塊是一模一樣的,所以不再重復代碼,唯一增加的就是設置其透明度。

4. 組合

將上面的組合到一起,就能形成最終的效果,從Reveal里面看到分層結構如下:

其實就是三張一樣的圖片,按照不同的mask規則做了處理,疊加起來。

用戶拖動拼圖、完成拼圖驗證

1. 拖動圖片

有了上面的結構,拖動拼圖的處理也就容易多了,只要移動最上層的拼圖塊就行了,這里直接用 UIPanGestureRecognizer 實現,移動的時候基于拼圖的中心移動。

// 創建手勢
UIPanGestureRecognizer *panGestureRecognizer = [UIPanGestureRecognizer new];
[panGestureRecognizer addTarget:self action:@selector(onPanGesture:)];
[self addGestureRecognizer:panGestureRecognizer];

// 手勢處理
- (void)onPanGesture:(UIPanGestureRecognizer *)panGestureRecognizer {
    // 獲取坐標
    CGPoint panLocation = [panGestureRecognizer locationInView:self];
    // 拼圖中心偏移
    CGPoint position = CGPointZero;
    position.x = panLocation.x - _puzzleSize.width / 2;
    position.y = panLocation.y - _puzzleSize.height / 2;
    // 設置坐標
    [self setPuzzlePosition:position];
    // 判斷、回調。。。
}

2. 驗證用戶的拼圖是否完成

用戶移動拼圖時,只要判斷拼圖的坐標和鏤空的位置是否一致就行。但是如果完全精確的判斷,用戶很難準確的把拼圖拼對上,所以還要增加一定的 “tolerance” ,讓拼圖更加容易,同時暴漏出讓外部設置:

@property (nonatomic, assign) CGFloat verificationTolerance; // Verification tolerance, default is 8

判斷的邏輯如下:

// puzzlePosition是當前拼圖塊的位置,_puzzleBlankPosition是拼圖鏤空的位置
- (BOOL)isVerified {
    return fabsf([self puzzlePosition].x - _puzzleBlankPosition.x) <= _verificationTolerance &&
           fabsf([self puzzlePosition].y - _puzzleBlankPosition.y) <= _verificationTolerance;
}

3. 完成拼圖

當用戶完成拼圖后,外部會得到 delegate 、 block 的回調響應,接著就可以“完成拼圖驗證”,對此向外提供一個方法: - (void)completeVerificationWithAnimation:(BOOL)withAnimation ,調用后拼圖塊將會移動回鏤空位置,模擬出拼圖完成的效果。

至此,TTGPuzzleVerify的關鍵實現技術點就是這些了~

 

 

 本文由用戶 yqnet2013 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!