自學iOS以來一直沒有做筆記的習慣,但是看了很多大牛關於技術總結、探索以及學習的技術部落格,感覺還是很有必要的保證學習筆記的積累和更新的。
最近買了一本關於iOS動畫的書,書名《A Guide To iOS Animation》,作者是KittenYang,很年輕的一位大神,動畫功底非常深。通過對他的書的練習以及自己程式碼的實踐,感覺自己的動畫認知和製作水平有了比較大的提高。
說到動畫,就離不開貝塞爾曲線。iOS中的基本上只要是複雜動畫就一定會用到貝塞爾曲線。
比如一個圓形檢視,可以通過給UIView的layer設定conerRadius然後mask或clip來呈現出圓形。也可以直接在layer上呼叫drawInContext方法通過
UIBezierPath* ovalPath = [UIBezierPath bezierPath];
[ovalPath moveToPoint: pointA];
[ovalPath addCurveToPoint:pointB controlPoint1:c1 controlPoint2:c2];
[ovalPath addCurveToPoint:pointC controlPoint1:c3 controlPoint2:c4];
[ovalPath addCurveToPoint:pointD controlPoint1:c5 controlPoint2:c6];
[ovalPath addCurveToPoint:pointA controlPoint1:c7 controlPoint2:c8];
[ovalPath closePath];
CGContextAddPath(ctx, ovalPath.CGPath);
CGContextDrawPath(ctx, kCGPathFillStroke);
複製程式碼
來畫圓,其中ABCD是圓的4個端點,兩個端點之間通過兩個中間點來用貝塞爾曲線連線起來。中間點的位置選擇取決於矩形的邊長。這樣子組成的圓形可擴充套件性更高,並能實現各種偏移動畫。比如利用手指拖拽圓的偏移量使圓壓扁,變長,以及阻尼效果。
iOS動畫中還有一個重要的物件CADisplayLink,設定好CADisplayLink的target和selector後將其放入runloop,就可以配合貝塞爾曲線精準的顯示動畫了。由於CADisplayLink繫結的方法每次螢幕重新整理都會被呼叫,而iOS預設螢幕重新整理次數是1s/60次(可以設定CADisplayLink的frameInterval屬性來控制重新整理次數),所以非常適合UI重繪。
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkAction:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];```
就我目前比較淺薄的水平而已,我感覺CoreAnimation(CAPropertyAnimation、CABasicAnimation、CAKeyframeAnimation)可以滿足日常工作中的90%的動畫要求。
而我看到的那些很牛逼很複雜的動畫,只不過是一個個簡單的動畫組合而成,只不過由於動畫時間比較短,所以看著很複雜。通過各種組合動畫(duration,delay,timeFunction,damping,velocity...)以及它的基本單位(transition、rotation、scale...)以及合理的引數調節,就能做出優秀的動畫了。
俗話說:好動畫,是慢慢調出來的~
一個優秀動畫的特點除了在短時間內輸出足夠多的資訊量;另一個特點,就是細節決定質量,使用者通過自己的觀察從而發現這種細節會是一件讓使用者很有成就感的事情。
就我用動畫而言,大多數都要用到這個函式
複製程式碼
[UIView animateWithDuration: delay: usingSpringWithDamping:initialSpringVelocity: options: animations: completion:];
通過設定damping和初速度來設定彈簧效果,通過options來設定動畫顯示方式,其他引數就不用一一贅述了。
我做了一個簡單的動畫,沒有用到任何複雜的數學知識,就是單純的用UIKIT自帶的函式實現簡單的動畫。
首先建立一個bottomView,為了保障程式的健壯性,最好remove掉所有的子檢視。
複製程式碼
bottomView = [[UIView alloc] initWithFrame:CGRectMake(0, SCREENHEIGHT, SCREENWIDTH, HEIGHT)];
bottomView.backgroundColor = [UIColor blueColor];
for (id subview in bottomView.subviews) {
[subview removeFromSuperview]; }
[self.view addSubview:bottomView];
然後給bottomView新增幾個按鈕,個數隨意
複製程式碼
for (int i = 0; i < BUTTONCOUNT; i++) {
UIButton *button = [[UIButton alloc] init];
CGFloat buttonWidth = SCREENWIDTH / 4;
CGFloat buttonHeight = HEIGHT / 2;
int row = i / 4;
int line = i % 4;
button.frame = CGRectMake(line * buttonWidth, row * buttonHeight, buttonWidth, buttonHeight);
[button setTitle:[NSString stringWithFormat:@"第%d個",i] forState:UIControlStateNormal];
button.backgroundColor = [UIColor redColor];
[bottomView addSubview:button];
}
然後新增一個成員變數triggered,來判斷bottomView是否彈出。
複製程式碼
@implementation ViewController{
BOOL triggered;
UIView *bottomView;
}``` 然後給檢視控制器的view(self.view)新增一個triggerButton,通過點選來呼叫彈出退下的動畫。
這裡也可以通過UIPanGestureRecognizer給self.view新增手勢操作,target設定為self,action設定為@selector:(pan:)然後override掉pan:,通過pan:傳入的gestureRecognizer引數可以得到手指滑動的偏移量和滑動狀態。然後根據滑動狀態覺得bottomView的偏移量程式碼如下:
-(void)pan:(UIPanGestureRecognizer *)gestureRecognizer{
CGPoint translationPoint = [gestureRecognizer translationInView:self.view];
CGFloat translationY = translationPoint.y;
//手指在螢幕上移動
if (gestureRecognizer.state == UIGestureRecognizerStateChanged) {
if (translationY < -20 &&translationY > - 220) {
CGFloat realOffsetY = translationY + 20;
NSLog(@" 111--- %f ---111",realOffsetY );
CGFloat frameY = SCREENHEIGHT + realOffsetY;
[UIView animateWithDuration:1/ 60 animations:^{
bottomView.frame = CGRectMake(0, frameY, SCREENWIDTH, HEIGHT);
}];//中間空40px用來做防止誤觸的處理
}else if (translationY > 20){
if (triggered) {
CGFloat realOffsetY = translationY - 20;
CGFloat frameY = SCREENHEIGHT-HEIGHT + realOffsetY;
[UIView animateWithDuration:1/ 60 animations:^{
bottomView.frame = CGRectMake(0, frameY, SCREENWIDTH, HEIGHT);
}];
}
}
}//手指離開螢幕
else if (gestureRecognizer.state == UIGestureRecognizerStateCancelled || gestureRecognizer.state == UIGestureRecognizerStateEnded){
if (triggered) {
if (translationY > 50){
[UIView animateWithDuration:0.3 delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:1 options:UIViewAnimationOptionCurveEaseOut animations:^{
bottomView.frame = CGRectMake(0, SCREENHEIGHT, SCREENWIDTH, HEIGHT);
triggered = NO;
}completion:^(BOOL finished) {
}];
}else{
[UIView animateWithDuration:0.3 animations:^{
bottomView.frame = CGRectMake(0, SCREENHEIGHT - HEIGHT, SCREENWIDTH, HEIGHT);
}];
triggered = YES;
}
}else{
if (translationY < - 50) {
[UIView animateWithDuration:0.3 animations:^{
bottomView.frame = CGRectMake(0, SCREENHEIGHT-HEIGHT, SCREENWIDTH, HEIGHT);
triggered = YES;
}];
}else{
[UIView animateWithDuration:0.3 animations:^{
bottomView.frame = CGRectMake(0, SCREENHEIGHT, SCREENWIDTH, HEIGHT);
triggered = NO;
}];
}
}
}
}```
上面的例子最好的做法是把bottomView抽出來,然後預留一個superView的介面,這樣子動畫的實現全部在bottomView這個類中實現,減輕Controller的負擔並降低耦合性。~(>_<)~ 所以將當做一個失敗的MVC案例來看吧,畢竟目前討論的不是設計模式。
接下來是關於CoreAnimatio和轉場動畫的簡單例子。模仿iOS上的GameCenter,首先在xib上建立4個button:
` NSArray *btnAry = [NSArray arrayWithObjects:self.blueBtn,self.orrangeBtn,self.redButton,self.yellowBtn,nil];
`
然後在`viewWillAppear`裡設定
複製程式碼
self.redButton.transform = CGAffineTransformMakeTranslation(-29, 700); self.blueBtn.transform = CGAffineTransformMakeTranslation(20, 700); self.orrangeBtn.transform = CGAffineTransformMakeTranslation(40, 700); self.yellowBtn.transform = CGAffineTransformMakeTranslation(60, 700);
這樣子就會有一個從底部升起的動畫。
為了保證動畫的協調性:
複製程式碼
for (int i = 0; i < btnAry.count; i ++) { [UIView animateWithDuration:1 delay:i * 0.15 usingSpringWithDamping:1 initialSpringVelocity:0.8 options:UIViewAnimationOptionTransitionCurlDown animations:^{ UIButton *btn = btnAry[i]; btn.transform = CGAffineTransformIdentity; }
然後遍歷這4個button,分別設定CAKeyframeAnimation 的keyPath為position(4個button的小範圍飄動),scale(4個button的小範圍呼吸效應)。(正好驗證一句話簡單動畫疊加可以得到複雜動畫)
這裡移動的路徑用到` CGPathAddEllipseInRect(ellipsePath, nil, circleContainer);
`作用是在circleContainer裡擷取一個內接橢圓的路徑。
部分程式碼如下:
複製程式碼
if ( i == btnAry.count - 1) {
for (UIButton *btn in btnAry) {
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"]; // pathAnimation.calculationMode = kCAAnimationPaced; pathAnimation.fillMode = kCAFillModeForwards; pathAnimation.removedOnCompletion = false; pathAnimation.repeatCount = MAXFLOAT; pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; if (btn == self.yellowBtn) { pathAnimation.duration = 5.0; }else if (btn == self.orrangeBtn){ pathAnimation.duration = 6.0; }else if (btn == self.redButton){ pathAnimation.duration = 7.0; }else if (btn == self.blueBtn){ pathAnimation.duration = 8.0; } //設定button移動路徑 CGMutablePathRef ellipsePath = CGPathCreateMutable();
CGRect circleContainer = CGRectInset(btn.frame, btn.frame.size.width/2 - 3 , btn.frame.size.width / 2 - 3);
CGPathAddEllipseInRect(ellipsePath, nil, circleContainer); pathAnimation.path = ellipsePath; [btn.layer addAnimation:pathAnimation forKey:@"myCircleAnimation"];
CAKeyframeAnimation *scaleX = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale.x"]; scaleX.values = @[@1.0,@1.1,@1.0]; scaleX.keyTimes = @[@0.0,@0.5,@1.0]; scaleX.repeatCount = MAXFLOAT; scaleX.autoreverses = YES; scaleX.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; if (btn == self.yellowBtn) { scaleX.duration = 3; }else if (btn == self.orrangeBtn){ scaleX.duration = 4; }else if (btn == self.redButton){ scaleX.duration = 6; }else if (btn == self.blueBtn){ scaleX.duration = 5; } [btn.layer addAnimation:scaleX forKey:@"scaleXAnimation"];
CAKeyframeAnimation *scaleY = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale.y"]; scaleY.values = @[@1.0,@1.1,@1.0]; scaleY.keyTimes = @[@0.0,@0.5,@1.0]; scaleY.repeatCount = MAXFLOAT; scaleY.autoreverses = YES; scaleY.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; if (btn == self.yellowBtn) { scaleY.duration = 4; }else if (btn == self.orrangeBtn){ scaleY.duration = 5; }else if (btn == self.redButton){ scaleY.duration = 2; }else if (btn == self.blueBtn){ scaleY.duration = 3; } [btn.layer addAnimation:scaleY forKey:@"scaleYAnimation"];
}
} }]; }
接下來是轉場動畫。iOS7開始蘋果推出了自定義轉場動畫,只要是用到CoreAnimation的地方就可以出現在兩個Controller切換之間。這裡我們就要用到UINavigationControllerDelegate裡一個方法進行override:
`- (nullable id <UIViewControllerAnimatedTransitioning>) navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC;`
然後在`- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;`這個方法中寫所有的轉場動畫的程式碼。例如:
複製程式碼
- (void)animateTransition:(id )transitionContext{ //containerView是包含兩個切換的控制器的檢視的容器,一切動畫效果都在這上面進行 UIView *containerView = [transitionContext containerView]; //radius是不能為0,否則在containerView上沒有承載動畫效果的檢視 CGFloat radius;
radius = sqrtf((self.startPoint.x * self.startPoint.x) + (containerView.bounds.size.height-self.startPoint.y)*(containerView.bounds.size.height-self.startPoint.y));
CGSize size = CGSizeMake(radius2, radius2); self.bubble = [[UIView alloc]initWithFrame:CGRectMake(0, 0, size.width, size.height)]; self.bubble.center = self.startPoint; self.bubble.layer.cornerRadius = size.width / 2; self.bubble.backgroundColor = self.bubbleColor;
if (self.transitionMode == Present) {
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
CGPoint originalCenter = toView.center;
self.bubble.transform = CGAffineTransformMakeScale(0.001, 0.001); //剛開始我也比較疑惑為什麼要把toView放在containerView上,後來經過測試,假如不放在上面,動畫效果有,但是動畫結束後會黑屏。上面註釋有寫containerView是包含 toView.center = self.startPoint; toView.transform = CGAffineTransformMakeScale(0.001, 0.001); toView.alpha = 0.0; _bubble.alpha = 1.0;
[containerView addSubview:_bubble]; [containerView addSubview:toView];
[UIView animateWithDuration:self.duration animations:^{ _bubble.transform = CGAffineTransformIdentity; toView.transform = CGAffineTransformIdentity; toView.alpha = 1.0f; toView.center = originalCenter; } completion:^(BOOL finished) { [UIView animateWithDuration:0.3 animations:^{ _bubble.alpha = 0.0; }completion:^(BOOL finished) { [_bubble removeFromSuperview]; [transitionContext completeTransition:YES]; }]; }];
}else{ UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
[containerView addSubview:toView]; [containerView addSubview:self.bubble];
[UIView animateWithDuration:self.duration animations:^{ self.bubble.transform = CGAffineTransformMakeScale(0.001, 0.001);
} completion:^(BOOL finished) { [self.bubble removeFromSuperview]; [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; }];
}
}
總之記住一句話,不要害怕做複雜動畫,因為複雜動畫就是一個個簡單動畫堆積而成,僅此而已。
最後貼出demo連結https://github.com/Yang9322/AnimationDemo.git
部分程式碼引用來自: “A GUIDE TO IOS ANIMATION”. iBooks.
複製程式碼