CADisplayLink结合UIBezierPath的神奇妙用

做过iOS动画的朋友都知道,动画中一大头疼之处就是弹性、形变之类扭曲的效果。iOS7开始,我们开始可以直接使用UiView的渲染动画API实现简单的弹性效果。


1
+ ( void )animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:( void  (^)( void ))animations completion:( void  (^)( BOOL  finished))completion NS_AVAILABLE_IOS(7_0);


dampingRatio是阻尼系数,取值范围0~1,决定弹性效果的明显程度;


velocity是初速度。


除此之外,iOS7又出现了一个重量级的家伙:UIKit Dynamics ,可以用很简单的代码实现非常逼真的物理效果。


当然,更强大的是Facebook开源的Pop这个介于CAAnimation 和 UIDynamics之间的动画引擎,使用习惯和CAAnimation基本别无二致,很方便上手,而且动画效果非常出色,帧频非常高,所以看上去的动画会很连贯顺滑。


http://img.mukewang.com/54f3d3d600018e2d02100084.jpg



但是以上要实现那种很Q弹、形变的效果还是有点困难。知道我同时遇到了CADisplayLink和贝塞尔曲线UIBezierPath。下面就是一些结合CADisplayLink和UIBezierPath 的案例,并附上了源代码地址。


http://img.mukewang.com/54f3d3f60001d75d03320588.jpg


Github地址


http://img.mukewang.com/54f3d4120001f18200010001.jpg


Github地址



http://img.mukewang.com/54f3d4250001aa9901900194.jpg


Github地址


1、什么是CADisplayLink


简单地说,它就是一个定时器,每隔几毫秒刷新一次屏幕。


CADisplayLink是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。我们在应用中创建一个新的 CADisplayLink 对象,把它添加到一个runloop中,并给它提供一个 target 和 selector 在屏幕刷新的时候调用。


一 但 CADisplayLink 以特定的模式注册到runloop之后,每当屏幕需要刷新的时候,runloop就会调用CADisplayLink绑定的target上的 selector,这时target可以读到 CADisplayLink 的每次调用的时间戳,用来准备下一帧显示需要的数据。例如一个视频应用使用时间戳来计算下一帧要显示的视频数据。在UI做动画的过程中,需要通过时间戳来 计算UI对象在动画的下一帧要更新的大小等等。


在添加进runloop的时候我们应该选用高一些的优先级,来保证动画的平滑。可以设想一 下,我们在动画的过程中,runloop被添加进来了一个高优先级的任务,那么,下一次的调用就会被暂停转而先去执行高优先级的任务,然后在接着执行 CADisplayLink的调用,从而造成动画过程的卡顿,使动画不流畅。


duration属性:提供了每帧之间的时间,也就是屏幕每次 刷新之间的的时间。我们可以使用这个时间来计算出下一帧要显示的UI的数值。但是 duration只是个大概的时间,如果CPU忙于其它计算,就没法保证以相同的频率执行屏幕的绘制操作,这样会跳过几次调用回调方法的机会。


frameInterval 属性:是可读可写的NSInteger型值,标识间隔多少帧调用一次selector 方法,默认值是1,即每帧都调用一次。如果每帧都调用一次的话,对于iOS设备来说那刷新频率就是60HZ也就是每秒60次,如果将 frameInterval 设为2 那么就会两帧调用一次,也就是变成了每秒刷新30次。


pause属性:控制CADisplayLink的运行。当我们想结束一个CADisplayLink的时候,应该调用-(void)invalidate从runloop中删除并删除之前绑定的 target 跟 selector


另外 CADisplayLink 不能被继承。


CADisplayLink 与 NSTimer 有什么不同?


iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。


NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且 NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围。


CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。


NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。


CADisplayLink使用的例子


1
2
3
4
5
6
7
8
9
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateTextColor)];  
self.displayLink.paused = YES;  
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
-( void )updateTextColor{}
- ( void )startAnimation{   self.beginTime = CACurrentMediaTime();   self.displayLink.paused = NO;
}
- ( void )stopAnimation{  self.displayLink.paused = YES;
   [self.displayLink invalidate];  self.displayLink = nil;
}


给非UI对象添加动画效果


我 们知道动画效果就是一个属性的线性变化,比如 UIView 动画的 EasyIn EasyOut 。通过数值按照不同速率的变化我们能生成更接近真实世界的动画效果。我们也可以利用这个特性来使一些其他属性按照我们期望的曲线变化。比如当播放视频时关 掉视频的声音我可以通过 CADisplayLink 来实现一个 EasyOut 的渐出效果:先快速的降低音量,在慢慢的渐变到静音。


注意


通 常来讲:iOS设备的刷新频率事60HZ也就是每秒60次。那么每一次刷新的时间就是1/60秒 大概16.7毫秒。当我们的frameInterval 值为1的时候我们需要保证的是 CADisplayLink调用的target的函数计算时间不应该大于 16.7否则就会出现严重的丢帧现象。 在mac应用中我们使用的不是CADisplayLink而是 CVDisplayLink它是基于C接口的用起来配置有些麻烦但是用起来还是很简单的。


2、Demo


实现这个形变效果的基本思路就是三句话:用CADisplayLink以其自身毫秒级刷新屏幕的特点去不断调用一个方法,这个方法里面画一条贝塞尔曲线,并且贝塞尔曲线的控制点是个动点。


第一个gif的实现思路:


首先我们需要两个辅助视图,并使用UIView的弹性动画usingSpringWithDamping 实现类似下面的效果:



http://img.mukewang.com/54f3d4580001a5ee02710452.jpg


新建 @interface JellyView : UIView


JellyView.m:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- ( void )drawRect:(CGRect)rect {  
   
     CGFloat yOffset = 30.0;    
     CGFloat width   = CGRectGetWidth(rect);    
     CGFloat height  = CGRectGetHeight(rect);
     
     UIBezierPath *path = [UIBezierPath bezierPath];
     [path moveToPoint:CGPointMake(0.0, yOffset)];  //去设置初始线段的起点
     CGPoint controlPoint = CGPointMake(width / 2, yOffset + self.sideToCenterDelta);
     [path addQuadCurveToPoint:CGPointMake(width, yOffset) controlPoint:controlPoint];
     [path addLineToPoint:CGPointMake(width, height)];
     [path addLineToPoint:CGPointMake(0.0, height)];
     [path closePath];
     
     CGContextRef context = UIGraphicsGetCurrentContext();
     CGContextAddPath(context, path.CGPath);
     [fillColor set];
     CGContextFillPath(context);
     
}


上 面代码绘制了一个封闭的贝塞尔曲线,初始时刻封闭曲线是一个四方的矩形,因为 [path addQuadCurveToPoint:CGPointMake(width, yOffset) controlPoint:controlPoint]; 中的 controlPoint 的纵坐标和左右两个定点纵坐标相等。但这其实是个动点。注意到 CGPoint controlPoint = CGPointMake(width / 2, yOffset + self.sideToCenterDelta); ,我们可以看到动点的纵坐标是由yOffset + self.sideToCenterDelta决定的。yOffset是固定的值。self.sideToCenterDelta等于两个辅助视图的高度 差。最后通过 CADisplayLink 的实时绘制,我们可以就可以看到屏幕上出现的形变效果了。


ViewController.m:


先创建一个实例 displayLink.


1
2
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkAction:)];  
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];


实现刷新器绑定的方法:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-( void )displayLinkAction:(CADisplayLink *)dis{
     CALayer *sideHelperPresentationLayer   =  (CALayer *)[self.sideHelperView.layer presentationLayer];
     CALayer *centerHelperPresentationLayer =  (CALayer *)[self.centerHelperView.layer presentationLayer];    
     
     CGPoint position = [[centerHelperPresentationLayer valueForKeyPath:@ "position" ]CGPointValue];    
     
     CGRect centerRect = [[centerHelperPresentationLayer valueForKeyPath:@ "frame" ]CGRectValue];    
     CGRect sideRect = [[sideHelperPresentationLayer valueForKeyPath:@ "frame" ]CGRectValue];    
     
     NSLog(@ "Center:%@" ,NSStringFromCGRect(centerRect));    
     NSLog(@ "Side:%@" ,NSStringFromCGRect(sideRect));    
     
     CGFloat newJellyViewTopConstraint      =  position.y - CGRectGetMaxY(self.view.frame);    
     
     self.jellyViewTopConstraint.constant = newJellyViewTopConstraint;
     [self.jellyView layoutIfNeeded];    
     
     self.jellyView.sideToCenterDelta = centerRect.origin.y - sideRect.origin.y;
}


这 里有个地方花了我好长时间,就是我们不能直接通过 self.sideHelperView.layer 和 self.centerHelperView.layer 获取两个辅助视图动画过程中的变化的坐标,得到的是一个恒定的终点状态的坐标。要想获得动画过程中的每个状态的坐标,我们需要使用layer的 presentationLayer ,并且通过 valueForKeyPath:@"position"的方式实时获取动态坐标。


!!最后千万别忘了调用 [self.jellyView setNeedsDisplay]; ,否则- (void)drawRect:(CGRect)rect不会called.


第二个gif的实现思路:


接下来的思路完全大同小异,只不过实时刷新的定时器从CADisplayLink换成了同样具有实时调用功能的手势:UIGestureRecognizerStateChanged。


新建 @interface BounceView : UIView


BounceView.m: 


先准备好一个CAShapeLayer,并且填充颜色用来显示形变的图形。


1
2
3
4
5
6
7
8
- ( void ) createLine {    
     self.verticalLineLayer = [CAShapeLayer layer];    
     self.verticalLineLayer.strokeColor = [[UIColor whiteColor] CGColor];    
     self.verticalLineLayer.lineWidth = 1.0;    
     self.verticalLineLayer.fillColor = [[UIColor whiteColor] CGColor];    
     
     [self.layer addSublayer:self.verticalLineLayer];
}


当手势开始变化的时候,我们让 self.verticalLineLayer.path 等于变化中的贝塞尔曲线的CGPath,并且把手指的偏移程度的变量CGFloat amountX = [gr translationInView:self].x传过去;


1
self.verticalLineLayer.path = [self getLeftLinePathWithAmount:amountX];


贝塞尔曲线的变化代码如下:


1
2
3
4
5
6
7
8
9
10
11
12
//左边曲线- (CGPathRef) getLeftLinePathWithAmount:(CGFloat)amount {
     UIBezierPath *verticalLine = [UIBezierPath bezierPath];    
     CGPoint topPoint = CGPointMake(0, 0);    
     CGPoint midControlPoint = CGPointMake(amount, self.bounds.size.height/2);    
     CGPoint bottomPoint = CGPointMake(0, self.bounds.size.height);
     
     [verticalLine moveToPoint:topPoint];
     [verticalLine addQuadCurveToPoint:bottomPoint controlPoint:midControlPoint];
     [verticalLine closePath];    
     
     return  [verticalLine CGPath];
}


代码还是大同小异,无非就是改变控制点midControlPoint,只不过这里是改变它的横坐标而已。


3、总结


归 根结底,要实现这个形变的Q弹效果无非就是一个实时调用一个绘制贝塞尔曲线的方法,并且这个贝塞尔曲线的控制点是一个动点。那个实时调用就有很多实现的办 法了。各种原生的代理方法,当然还包括文中提到了毫秒级刷新器CADisplayLink。期待你能做出更加动感的动画:)


转自: http://www.imooc.com/wenda/detail/248979


资料参考:


CADisplayLink   http://www.jianshu.com/p/c35a81c3b9eb 


来源:Kitten's 时间胶囊

原文地址:http://kittenyang.com/cadisplaylinkanduibezierpath/  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Swift 中,主要有以下三种计时器: 1. `Timer`:这是一个基础的计时器,可以周期性地执行某些任务。 2. `DispatchSourceTimer`:这是一个基于 GCD 的计时器,可以更加精确地执行任务。 3. `CADisplayLink`:这是一个适用于游戏和动画的计时器,可以根据屏幕的刷新率来调整任务的执行时间。 下面分别介绍它们的使用方法: ### Timer 使用 `Timer` 类可以创建一个基础的计时器。以下是一个示例: ```swift class ViewController: UIViewController { var timer: Timer? var counter = 0 override func viewDidLoad() { super.viewDidLoad() // 创建计时器,并设置时间间隔为1秒 timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true) } @objc func updateTimer() { counter += 1 print("计时器已运行\(counter)秒") } // 在需要停止计时器的地方调用 invalidate() 方法 // timer?.invalidate() } ``` 在上面的示例中,我们创建了一个计时器,并设置时间间隔为1秒。每当计时器时间间隔到达时,都会调用 `updateTimer` 方法,其中我们可以执行一些操作,比如更新 UI 或者执行一些逻辑。在需要停止计时器的地方,可以调用 `invalidate()` 方法来停止计时器。 ### DispatchSourceTimer 使用 `DispatchSourceTimer` 类可以创建一个更加精确的计时器。以下是一个示例: ```swift class ViewController: UIViewController { var timer: DispatchSourceTimer? var counter = 0 override func viewDidLoad() { super.viewDidLoad() // 创建计时器,设置时间间隔为1秒 timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main) timer?.schedule(deadline: .now(), repeating: .seconds(1)) timer?.setEventHandler(handler: { [weak self] in self?.counter += 1 print("计时器已运行\(self?.counter ?? 0)秒") }) // 启动计时器 timer?.resume() } // 在需要停止计时器的地方调用 cancel() 方法 // timer?.cancel() } ``` 在上面的示例中,我们创建了一个计时器,并设置时间间隔为1秒。每当计时器时间间隔到达时,都会调用 `setEventHandler` 方法中的闭包,其中我们可以执行一些操作,比如更新 UI 或者执行一些逻辑。在需要停止计时器的地方,可以调用 `cancel()` 方法来停止计时器。 ### CADisplayLink 使用 `CADisplayLink` 类可以创建一个适用于游戏和动画的计时器。以下是一个示例: ```swift class ViewController: UIViewController { var displayLink: CADisplayLink? var startTime: CFTimeInterval = 0 override func viewDidLoad() { super.viewDidLoad() // 创建计时器 displayLink = CADisplayLink(target: self, selector: #selector(updateTimer)) displayLink?.add(to: .main, forMode: .common) // 记录开始时间 startTime = CACurrentMediaTime() } @objc func updateTimer() { let currentTime = CACurrentMediaTime() let elapsedTime = currentTime - startTime print("计时器已运行\(elapsedTime)秒") } // 在需要停止计时器的地方调用 invalidate() 方法 // displayLink?.invalidate() } ``` 在上面的示例中,我们创建了一个计时器,并使用 `add(to:forMode:)` 方法将其添加到主循环中。每当屏幕需要刷新时,都会调用 `updateTimer` 方法,其中我们可以执行一些操作,比如更新游戏或者动画。在需要停止计时器的地方,可以调用 `invalidate()` 方法来停止计时器。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值