前段時間看了一本書《 A GUIDE TO IOS ANIMATION:Kitten 的 iOS 動畫學習手冊 》 在里面看到一個很酷的側邊欄彈出效果.
最近打算重寫一個我自己個私人項目,就想到了用這個效果來作為界面跳轉的方式,下面就來介紹一下
我們首先來看一下完成的效果
整個菜單欄的彈出效果可以分為兩個部分,下面會一一分析.其實我之前很少接觸動畫效果,但是(對,這里有但是),遇到動畫不能害怕去寫,只要學會分析動畫的效果,其實并不難.
第一部分動畫: 先來看看效果
這部分可以看做是菜單欄喚醒之前和菜單欄沒有喚醒的動畫效果.
這一部分我參考了iOS - 用 UIBezierPath 實現果凍效果這篇文章實現的方式
好了,回到正題,首先,用KVO的方式監聽一個 view的坐標點(上圖中紅色的小點點),通過這個坐標點來更新CAShapeLayer的形狀,進而達到動畫的效果
下面是觀察的方法與設置的方法(具體可以看代碼或者參照上面那個鏈接)
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:kX] || [keyPath isEqualToString:kY]) {
[self updateShapeLayerPathWithEnd];
}
}
- (void)updateShapeLayerPath
{
// 更新_shapeLayer形狀
UIBezierPath *tPath = [UIBezierPath bezierPath];
[tPath moveToPoint:CGPointMake(0, 0)]; // 1點
[tPath addLineToPoint:CGPointMake(0, SYS_DEVICE_HEIGHT)]; //2點
[tPath addQuadCurveToPoint:CGPointMake(0, 0)
controlPoint:CGPointMake(_curveX, _curveY)]; // 確定一個弧線
[tPath closePath];
_shapeLayer.path = tPath.CGPath;
}
下面重點來了.怎么實現彈簧的效果呢
先來介紹一個東東 : CADisplayLink
CADisplayLink默認每秒運行60次calculatePath是算出在運行期間_curveView的坐標,從而確定_shapeLayer的形狀
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(calculatePath)];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
_displayLink.paused = YES;//這里先暫停
//CADisplayLink 綁定的方法
- (void)calculatePath
{
// 由于手勢結束時,view會執行一個彈簧動畫,把這個過程的坐標記錄下來,并相應的畫出_shapeLayer形狀
CALayer *layer = _curveView.layer.presentationLayer;
self.curveX = layer.position.x;
self.curveY = layer.position.y;
}
iOS7之后蘋果更加注重用戶交互,所以添加了一部分動畫效果的接口.其中有一個就是實現彈簧效果的函數
[UIView animateWithDuration:(NSTimeInterval)//動畫持續時間
delay:(NSTimeInterval)//動畫延遲時間
usingSpringWithDamping:(CGFloat)//類似彈簧振動效果0~1
initialSpringVelocity:(CGFloat)//初始速度
options:(UIViewAnimationOptions)//動畫過渡效果
animations:^{
// code
} completion:^(BOOL finished) {
// code
}]
usingSpringWithDamping:它的范圍為0.0f到1.0f,數值越小「彈簧」的振動效果越明顯。
initialSpringVelocity:初始的速度,數值越大一開始移動越快。值得注意的是,初始速度取值較高而時間較短時,也會出現反彈情況。
Spring Animation是線性動畫或ease-out動畫的理想替代品。由于iOS本身大量使用的就是Spring Animation,用戶已經習慣了這種動畫效果,因此使用它能使App讓人感覺更加自然,用Apple的話說就是「instantly familiar」。此外,Spring Animation不只能對位置使用,它適用于所有可被添加動畫效果的屬性。
上面說到用的是一個view的坐標點來更新CAShapeLayer層來實現效果的,所以我們只要在手勢的綁定的方法中實現以下代碼即可
if(pan.state == UIGestureRecognizerStateChanged)
{
// 手勢移動時,_shapeLayer跟著手勢擴大區域
CGPoint point = [pan translationInView:self];
// 這部分代碼使view與手指的坐標綁定并更新view的坐標,通過kvo修改坐標的值
_mHeight = point.y*0.7 + MIN_HEIGHT;
self.curveX = point.x;
self.curveY =SYS_DEVICE_HEIGHT/2;
_curveView.frame = CGRectMake(_curveX,
_curveY,
_curveView.frame.size.width,
_curveView.frame.size.height);
}else if (pan.state == UIGestureRecognizerStateCancelled ||
pan.state == UIGestureRecognizerStateEnded ||
pan.state == UIGestureRecognizerStateFailed)
{
// 手勢結束時,_shapeLayer返回原狀并產生彈簧動效
_displayLink.paused = NO; //開啟displaylink,會執行方法calculatePath.
_curveX = _curveView.frame.origin.x;
// 彈簧動效
[UIView animateWithDuration:1.0
delay:0.0
usingSpringWithDamping:0.3
initialSpringVelocity:0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
// 曲線點(r5點)是一個view.所以在block中有彈簧效果.然后根據他的動效路徑,在calculatePath中計算彈性圖形的形狀
_curveView.frame = CGRectMake(0, self.window.frame.size.height/2, 3, 3);
} completion:^(BOOL finished) {
if(finished)
{
_displayLink.paused = YES;
_isAnimating = NO;
}
}];
}
至此,第一部分的動畫效果就已經實現.
第二部分效果就是彈出菜單的動畫效果了
第二部分我是參考 《 A GUIDE TO IOS ANIMATION:Kitten 的 iOS 動畫學習手冊 》這本書中提到的動畫實現效果,書中大神說到他做動畫的心得: “「善于拆解」。即把一個復雜的動畫分解為幾個分動畫,然后再把這些分動畫逐一解決。”
廢話不多說,看看大神是怎么實現的吧
我們先把整個第二部分拆解成一個 View 從屏幕左側移入
self.frame = CGRectMake(-keyWindow.frame.size.width/2-EXTRAAREA, 0, keyWindow.frame.size.width/2+EXTRAAREA, keyWindow.frame.size.height);
self.backgroundColor = [UIColor clearColor];
[self.view insertSubview:self belowSubview:helperSideView];
//“右側還留出了 30px(即代碼中的 EXTRAAREA ) 的透明區域。理由很簡單,因為如果不這么做,發生彈性時向右突出的邊界就看不到了。”
這個View創建之后我們可以加上動畫效果,使它從屏幕外移到屏幕上顯示,或者菜單欄消失的時候使它移出屏幕
接下來就是實現動畫了.思路跟上面那個差不多一樣,也是用貝塞爾曲線畫出范圍,然后用CADisplayLink來實現.
但是(對,還是一個但是),到這的時候我發現一個問題,實現彈簧震動效果的地方不止一處,可我只有一個CADisplayLink的對象,該怎么實現呢.
有一個辦法,就是每次需要實現動畫效果的時候,實現CADisplayLink綁定一個方法,動畫完成之后,再把CADisplayLink對象綁定的方法給解掉.這樣一個界面就能反復使用同一個CADisplayLink對象實現不同的動畫效果.
這里跑偏一下說一下CADisplayLink這個東東的其他的一些屬性
@property(readonly, nonatomic) CFTimeInterval duration;//每幀之間的時間
@property(nonatomic) NSInteger frameInterval;//間隔多少幀調用一次 selector 方法,默認值是 1 ,即每幀都調用一次。如果每幀都調用一次的話,對于 iOS 設備來說那刷新頻率就是 60HZ 也就是每秒 60 次,如果將 frameInterval 設為 2 那么就會兩幀調用一次,也就是變成了每秒刷新 30 次。
@property(getter=isPaused, nonatomic) BOOL paused;//是否暫停當前的定時器,控制 CADisplayLink 的運行。
好,回到正題,現在我們要實現的動畫效果式菜單欄彈出的過程中,實現側邊凸起,和菜單欄返回的時候,側邊凹進去的效果,即:
“ 如何產生一組變化的數值,x 增加到某個正數,再從這個正數(也就是最大值 P 點)遞減到一個負數,最后從這個負數(也就是最小值 Q 點)遞增到 x ?” (x是view 中心點的x坐標)
我在書中看到作者提供了一個技巧,就是創建兩個輔助視圖(代碼中的helperSideView與helperCenterView),然后設置兩個輔助視圖動畫移動的起點與終點一樣,只是把兩個動畫效果的初始速度設置一個時間差,這樣就會起到一個獲取到一個變化的差值,這個差值就可以用來刷新動畫,形成回彈效果了
額....代碼Show起來:
[self beforeAnimation];
[UIView animateWithDuration:0.7
delay:0.0f
usingSpringWithDamping:0.3f
initialSpringVelocity:0.9f //輔助視圖1的動畫起始時間設為0.9
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
helperSideView.center = CGPointMake(keyWindow.center.x, helperSideView.frame.size.height/2);
} completion:^(BOOL finished) {
[self finishAnimation];
}];
[UIView animateWithDuration:0.3 animations:^{
blurView.alpha = 1.0f;//這個視圖為最下層的淡灰色半透明毛玻璃視圖
}];
[self beforeAnimation];
[UIView animateWithDuration:0.7
delay:0.0f
usingSpringWithDamping:0.8f
initialSpringVelocity:2.0f //輔助視圖2的動畫起始時間設為2.0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
helperCenterView.center = keyWindow.center;
} completion:^(BOOL finished) {
if (finished) {
//動畫2 完成之后要給灰色區域添加點擊手勢,用來取消菜單欄.
UITapGestureRecognizer *tapGes = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapToUntrigger:)];
[blurView addGestureRecognizer:tapGes];
[self finishAnimation];
}
}];
其中的方法:[self beforeAnimation] 和 [self finishAnimation] 用于控制 CADisplayLink 什么時候應該移除。用到了動畫的累加計數:每開始一個動畫時計數器加 1,每停止一個動畫時計數器減 1,當兩個動畫都完成時,計數器為 0,此時移除 CADisplayLink,就是上文提到的用一個CADisplayLink對象完成多個動畫,并在不用的時候移除CADisplayLink對象的方法
//動畫開始之前調用
-(void)beforeAnimation{
if (self.displayLink == nil) {
self.displayLink = [CADisplayLink displayLinkWithTarget:self
selector:@selector(displayLinkAction:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
self.animationCount ++;
}
//動畫完成之后調用
-(void)finishAnimation{
self.animationCount --;
if (self.animationCount == 0) {
[self.displayLink invalidate];
self.displayLink = nil;
}
}
上面實現了菜單欄的動畫的計算.接下來就是怎么讓這個動畫顯示出來,在第一階段的時候,我們是用的KVO的方式,只要是標記view的坐標有改變,就會刷新layer層的動畫效果.現在這個動畫是有兩個標記view,怎么搞?
還是一樣的,在beforeAnimation的方法中,displayLink綁定了一個方法displayLinkAction:, 在這個方法里,我們除了要獲取到前面提到的那個差值,還需要另外一個方法,區別于第一種KVO更新動畫效果的方式,調用[self setNeedsDisplay]方法,調用這個方法的時候就會觸發UIView的- (void)drawRect:(CGRect)rect;方法或 CALayer 的 drawRectInContext方法
-(void)displayLinkAction:(CADisplayLink *)dis{
CALayer *sideHelperPresentationLayer = (CALayer *)[helperSideView.layer presentationLayer];
CALayer *centerHelperPresentationLayer = (CALayer *)[helperCenterView.layer presentationLayer];
CGRect centerRect = [[centerHelperPresentationLayer valueForKeyPath:@"frame"]CGRectValue];
CGRect sideRect = [[sideHelperPresentationLayer valueForKeyPath:@"frame"]CGRectValue];
diff = sideRect.origin.x - centerRect.origin.x; //注意,這個diff值就是計算出來的那個差值
[self setNeedsDisplay];
}
因為[self setNeedsDisplay] 處于CADisplayLink 的方法里面,所以會連續調用UIView的- (void)drawRect:(CGRect)rect;方法,下面是drawRect: 的實現
- (void)drawRect:(CGRect)rect {
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, 0)];
[path addLineToPoint:CGPointMake(self.frame.size.width-EXTRAAREA, 0)];
[path addQuadCurveToPoint:CGPointMake(self.frame.size.width-EXTRAAREA, self.frame.size.height)
controlPoint:CGPointMake(keyWindow.frame.size.width/2+diff, keyWindow.frame.size.height/2)];
//注意,這里要加上之前計算出來的diff值
[path addLineToPoint:CGPointMake(0, self.frame.size.height)];
[path closePath];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddPath(context, path.CGPath);
[_menuColor set];
CGContextFillPath(context);
}
好了,到現在為止基本就可以實現動畫效果了,最后就只剩下按鈕的動畫效果了
其實按鈕的動畫效果也是回彈效果,只不過開始的起始時間依次增加,所以,我們只要在菜單視圖完全彈出之后再調用按鈕的動畫效果就可以實現.
-(void)animateButtons{
for (NSInteger i = 0; i < self.subviews.count; i++) {
UIView *menuButton = self.subviews[i];
menuButton.transform = CGAffineTransformMakeTranslation(-90, 0);
[UIView animateWithDuration:0.7
delay:i*(0.3/self.subviews.count) //動畫開始的時間依次延遲0.3秒.
usingSpringWithDamping:0.4f
initialSpringVelocity:0.0f
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
menuButton.transform = CGAffineTransformIdentity;
} completion:NULL];
}
}
然后,我們來看看在displayLinkAction 的方法中 CALayer *sideHelperPresentationLayer = (CALayer *)[helperSideView.layer presentationLayer]; CALayer的presentationLayer的作用:
當你給一個 CALayer 添加動畫的時候,動畫其實并沒有改變這個 layer 的實際屬性。取而代之的,系統會創建一個原始 layer 的拷貝。在文檔中,蘋果稱這個原始 layer 為 Model Layer ,而這個復制的 layer 則被稱為 Presentation Layer 。 Presentation Layer 的屬性會隨著動畫的進度實時改變,而 Model Layer 中對應的屬性則并不會改變。在這里我們只是用到了layer的x坐標用來計算.其他的應用,我也在學習中.
上文中的工程地址: 一個簡單的抽屜側邊欄
寫在最后: 這是我第一次寫這類的文章,思路不是很清晰,敬請諒解.這個效果庫我會趁著有空慢慢優化,現在還只是一個開始,有什么好的建議可以給我說下.另外歡迎大神幫忙優化代碼.敬表感謝. 對了,還有,《 A GUIDE TO IOS ANIMATION:Kitten 的 iOS 動畫學習手冊 》這本書真的很好,有心學習動畫效果的朋友一定要看看.