本文為投稿文章,作者:Zhiyi(Github)
PreLoader是由Volodymyr Kurbatov設計的一個很有意思的HUD,通過運動污點和固定污點之間的粘黏動畫吸引用戶的眼球跟蹤,能有效分散等待注意力。
這篇文章簡單剖析本人使用OC實現PreLoader的原理思路和做法。
噴出來的油污
根據這個Loading動畫的粘黏特征,我把它裡面這些有顏色的物體比作油污,觀察這個動畫發現,可將它分成兩個整體,左右兩邊兩個固定的油污,還有移動中的三個小油污點,左右兩個固定的油污輪流向對方噴射油污,雙方都會因為吸收油污而變大,噴射油污而變小。
首先我們從左右循環移動的污點著手,因為路徑不是平滑一步到位,我這裡選擇使用CAKeyframeAnimation關鍵幀動畫,先做出在左右固定點間來回運動的污點。
//moving Spot for (int i = 0; i < 3; i++) { Spot *movingSpot = [[Spot alloc] initWithFrame:CGRectMake(originX - UNIT_RADIUS, self.bounds.size.height / 2 - UNIT_RADIUS , 2 * UNIT_RADIUS, 2 * UNIT_RADIUS) color:spotColor]; //1 CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"position.x"]; anim.values = @[@(originX), @(originX), @(finalX), @(finalX), @(originX), @(originX)]; anim.keyTimes = @[@(0.0), @(0.25), @(0.35), @(0.75), @(0.85), @(1.0)];//sleep 0.4 ratio anim.duration = PROCESS_DURING; anim.repeatCount = HUGE_VALF; anim.beginTime = CACurrentMediaTime() + i * SPOT_DELAY_RATIO * PROCESS_DURING; [movingSpot.layer addAnimation:anim forKey:@"movingAnim"]; [self addSubview:movingSpot]; [CATransaction begin]; [CATransaction setDisableActions:YES]; [CATransaction commit]; }
我把多余的代碼刪除掉了,主要我們來看//1處,先拿一個污點來做參考,我們通過CAKeyFrameAnimation控制污點layer的position.x變化,形成動畫。現在我們只需要兩個控制點(originX, finalX),路徑可以解析為:“休息,左邊出發點,到右邊結束點,休息,然後回到左邊出發點”,這個動作鏈為一個循環。看看我們的keyTimes和values為什麼是這些數字?這裡我們只需要保證兩點:
污點在左固定點 和 右固定點休息的時間相同
污點從左往右 和 從右往左移動的時間相同
這裡我規定了污點休息時間為0.4個單位,因此移動時間就是(1-2*0.4)/2 = 0.1個單位。這裡有兩個問題:
為什麼從0.25個單位開始移動而不是0.0呢?可能我只是想為後面保留靈活性吧,這個其實沒多大關系。
為什麼values前面有兩個重復的originX,後面又有兩個重復的finalX?這個是必須的,雖然CAKeyframeAnim的value不需要從0.0開始1.0結束也可以實現相同的動畫效果,但是如果沒有了這兩個極點,通過presentationLayer取動畫實時位置時會出現超出邊界不准確的負數,後面會再提到這個問題。
有些朋友可能會沒有弄明白這個Animation Path對應的污點休息時間在哪裡,這裡再強化下,0.35-0.75在右邊休息,0.85-0.25在左邊休息,盯著我代碼看,會看懂的。
ok,我們已經理解了一個污點的動畫路徑,那麼我們用一個for循環,很簡單就可以做出三個,一個跟一個出發的污點動畫路徑,保證了前面的基礎以後,這裡只需要保證三個路徑的動畫一個循環的持續時間duration相同,然後我們使用一個延遲系數SPOT_DELAY_RATIO = 0.08f,控制3個動畫分別的beginTime,實現了一個跟一個的效果。
吸收油污,噴射油污
接下來我們做兩個固定污點的變大變小動畫,當然不可以用碰撞檢測來做,那樣不好控制而且受大小影響,會有很多不必要的代碼。思路是在特定的時間做特定大小變化,既然我們已經定義了污點移動和休息的關鍵幀keyframe,為什麼不繼續用上他們呢?看固定油污的動畫代碼:
//Fixed Spot Spot *leftFixedSpot = [[Spot alloc] initWithFrame:CGRectMake(originX - UNIT_RADIUS, self.bounds.size.height / 2 - UNIT_RADIUS, 2 * UNIT_RADIUS, 2 * UNIT_RADIUS) color:spotColor]; Spot *rightFixedSpot = [[Spot alloc] initWithFrame:CGRectMake(self.bounds.size.width - margin - UNIT_RADIUS, self.bounds.size.height / 2 - UNIT_RADIUS, 2 * UNIT_RADIUS, 2 * UNIT_RADIUS) color:spotColor]; NSValue *firstVal = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0f, 1.0f, 0)]; NSValue *secondVal = [NSValue valueWithCATransform3D:CATransform3DMakeScale(2.0f, 2.0f, 0)]; NSValue *thirdVal = [NSValue valueWithCATransform3D:CATransform3DMakeScale(3.0f, 3.0f, 0)]; NSValue *fourthVal = [NSValue valueWithCATransform3D:CATransform3DMakeScale(4.0f, 4.0f, 0)]; //發射點,先調至最大 leftFixedSpot.layer.transform = CATransform3DMakeScale(4.0f, 4.0f, 0); //left CAKeyframeAnimation *leftFixedSpotAnim = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; leftFixedSpotAnim.values = @[thirdVal, thirdVal, fourthVal, fourthVal, thirdVal, thirdVal, secondVal, secondVal, firstVal, firstVal, secondVal, secondVal, thirdVal, thirdVal]; leftFixedSpotAnim.keyTimes = @[@(0.0), @(0.01), @(0.01), @(0.25), @(0.25), @(0.33), @(0.33), @(0.41), @(0.41),//sleep @(0.85), @(0.85), @(0.93), @(0.93), @(1.00)];//SPOT_DELAY_RATIO = 0.08 leftFixedSpotAnim.duration = PROCESS_DURING; leftFixedSpotAnim.repeatCount = HUGE_VALF; [leftFixedSpot.layer addAnimation:leftFixedSpotAnim forKey:@"fixedSpotScaleAnim"]; //right CAKeyframeAnimation *rightFixedSpotAnim = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; rightFixedSpotAnim.values = @[firstVal, firstVal, secondVal, secondVal, thirdVal, thirdVal, fourthVal, fourthVal, thirdVal, thirdVal, secondVal, secondVal, firstVal, firstVal]; rightFixedSpotAnim.keyTimes = @[@(0.0), @(0.25), @(0.25), @(0.33), @(0.33), @(0.41), @(0.41),//sleep @(0.75), @(0.75), @(0.83), @(0.83), @(0.91), @(0.91), @(1.0)];//SPOT_DELAY_RATIO = 0.08 rightFixedSpotAnim.duration = PROCESS_DURING; rightFixedSpotAnim.repeatCount = HUGE_VALF; //0.1 ratio needed that the spot from left to right rightFixedSpotAnim.beginTime = CACurrentMediaTime() + PROCESS_DURING * 0.1; [rightFixedSpot.layer addAnimation:rightFixedSpotAnim forKey:@"fixedSpotScaleAnim"]; [self addSubview:leftFixedSpot]; [self addSubview:rightFixedSpot];
我們先定義好4種Scale對應的污點value(firstVal, secondVal, thirdVal, fourthVal), 使用keyframeAnimation控制這4種value的變化,實現左右固定污點在指定時間點的變大變小。
這裡需要注意什麼:
跟移動的污點保持一致,也是從0.25開始活動
還記得移動污點一個跟一個出發的延遲系數0.08f嗎?它就是固定污點變大變小的間隔時間單位
因為移動污點從一邊移動到另一邊用的時間單位為0.1,所以右固定點的變大動畫開始時間要比左固定點晚0.1,也就是rightFixedSpotAnim.beginTime = CACurrentMediaTime() + PROCESS_DURING * 0.1;
移動污點休息時間在固定點關鍵幀上用不著,因為固定點在最後一個移動污點離開後變成最小,而在第一個移動污點來臨時就要開始變大
因為右固定點的動畫相對於左固定點延遲了0.1開始,所以仍然是從0.25關鍵幀開始活動(變大)
這裡舉例講解下這一part關鍵幀的計算依據:
看左固定點的代碼//left,從0.25開始活動,每過一個延遲系數0.08,會有一個污點觸發(噴出),做一次變小動畫,當第三次變小動畫做完,我們並不知道中間休息時間有多長,這時就要用到0.85這個關鍵幀時間(從移動污點代碼可看出,第一個污點在0.85時回到左固定點),於是從0.85開始做變大動畫,也是延遲系數0.08,最後會操作1.0,沒關系影響不大,最後一個變大動畫在0.01做就好了!
那右固定點的0.75關鍵幀是怎樣算的呢?0.41是最後一次變大(最後一個污點被吸收), 那麼距離第一個污點被噴出的時間間隔就是0.41+移動污點休息時間0.4-兩個延遲系數0.08*2,因為,最後一個污點被吸收時,第一個來的污點已經休息了兩個延遲系數時間了。
緩動脹大
之前scale的KeyframeAnim變化都是突然的,一個scale跳去另外一個scale,看看局部代碼會議下:
firstVal, secondVal, secondVal, thirdVal @(0.25), @(0.25), @(0.33), @(0.33)
可以看出一個Val變化到另一個Val是發生在同一個關鍵幀,沒有做過度效果。這裡為了修改方便,我們只需要引入一個比較短的動畫時間間隔CGFloat ti = SPOT_MAGNIFY_ANIM_DURATION_RATIO = 0.03f即可:
CGFloat ti = SPOT_MAGNIFY_ANIM_DURATION_RATIO; leftFixedSpotAnim.keyTimes = @[@(0.0), @(0.01), @(0.01+ti), @(0.25), @(0.25+ti), @(0.33), @(0.33+ti), @(0.41), @(0.41+ti),//sleep @(0.85), @(0.85+ti), @(0.93), @(0.93+ti), @(1.00)];//SPOT_DEL
粘黏動畫
這裡我使用presentationLayer獲取動畫layer的實時frame信息,然後准備幾個工具函數:
centerDistanceWithPoint:another:計算圓心距CD
faceDistanceWithCircleLayer:another:計算表面距離FD
circleIncirclingWithBigOne:smallOne:判斷兩圓是否包含關系
我使用了CAShapeLayer和UIBezierPath來做這個粘連效果,通過控制CAShapeLayer的顏色控制粘連的顯示和消失,而顯示/消失的依據就是兩圓的表面距離FD。這裡再次強調一遍KeyframeAnim的keyTimes一定要從0.0開始,1.0結束,否則獲取layer實時frame時會有錯誤數據干擾。
那麼“是否包含”用來干什麼呢?我們有3個污點,一個粘連效果ShapeLayer,當第三個污點到來時根據FD計算出粘連Path,准備愉快地表現自己的時候,這個Path又會被第一個污點的FD干擾,計算出一個不正確的Path覆蓋,所以我們讓移動污點跟固定污點內切以後,就不對粘連Path產生影響。順便一提,這一切的動畫邏輯計算,都在CADisplayLink裡完成。
路徑Path:
我們采用兩條曲線銜接兩個圓的這種污點結合方式作為動畫路徑(見上圖),這種方式能很好地模擬呈現液體的吸收結合效果,曲線經過固定污點的頂點Fu和移動污點的頂點Mu,再由FuMu線段中垂線上的一個controlPoint決定了一條曲線。UIBezierPath的API:addQuadCurveToPoint:controlPoint:。整個路徑由曲線FuMu,曲線FdMd,和圓弧MuMd組成,最後由線段FuFd封閉。吸收效果截圖:
回彈粘黏效果
做回彈效果前,我們先得讓移動污點會跑到固定污點的後面,引入originRearX(出發點的後方)和finalRearX(終點的後方),修改移動污點的keyframeAnim:
anim.values = @[@(originX), @(originX), @(finalX), @(finalRearX), @(finalX), @(finalX), @(originX), @(originRearX), @(originX), @(originX)]; anim.keyTimes = @[@(0.0), @(0.25), @(0.35), @(0.38), @(0.41), @(0.75), @(0.85), @(0.88), @(0.91), @(1.0)];//sleep 0.4 ratio
代碼中可以看出,我定義了回彈時間為0.03 * 2,ok這裡沒有什麼問題。
至於回彈的效果Path,因為兩個污點的圓心距比較小,如果仍然沿用上一種路徑方案效果會不太好,我們使用上圖這種Path而不再使用模擬液體吸收的曲線方式。參考上圖,我們通過兩條線段,和一條與移動污點圓周重合的圓弧來組合粘黏效果Path,再由線段FuFd來閉合它,線段經過左固定污點,與溢出的移動污點相切。
還記得“是否包含”嗎,移動污點從固定污點後面溢出來時也就不符合包含關系了,會影響到正面粘黏效果的作畫,於是這裡的回彈粘黏效果跟普通粘黏效果分開兩個獨立的CAShapeLayer來做的,避免干擾。回彈效果截圖:
總體思路就是這樣了,主要的耗時工作就是計算path和協調幾個CAShapeLayer的顯示消失和互相影響,源代碼或者效果可以在Github:https://github.com/liuzhiyi1992/PreLoader中查看,謝謝。