开篇
废话不多说,先上效果图。
上面一闪一闪的大概是没录好吧。
需求分析
首先分析这段动画的过程:
- 第一阶段:点击图标后,图标像泡泡一样收缩放大;
- 第二阶段:图标缩小,同时向下方移动;
- 第三阶段:波形图绘制,随着数据的增多,波形图向左移动,同时视图的宽度越来越长
- 第四阶段:视图宽度达到一个阈值后不再增大,波形图继续向左移动,超出左侧范围的部分不再显示。
PS. 波形图是上下对称的哦。
实现思路
首先看动画,前两个阶段的动画有先后顺序,第二阶段的两个动画同时发生,可以用动画组来做。
然后,第二、第三阶段之间的节点,可以看到图标刚好缩小到波形视图的高度后,波形视图出现,如果分为两个视图,总有一个生硬的过渡过程,为了达到一个流畅的视觉效果,可以使用一个视图来做。
最后,仔细看波形图,新数据是从右侧进入的,由于这是个录音的过程,因此数据是在不断增多的,可以用实时绘图来实现。
接下来上自定义 button 的代码。
-
第一阶段 ~ 第二阶段
- (void)startAnimation{ self.enabled = NO; //动画过程中禁用 [self moveAnchorPointToCenter]; //将锚点移到中心 (为了达到围绕中心缩放的效果) //放大 CAKeyframeAnimation *scaleToBigAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"]; scaleToBigAnimation.values = @[@(1.0), @(.7f), @(1.f), @(1.3f), @(1.7f)]; //先从1.0缩小到0.7,再放大到1.7,这样就实现了泡泡效果 scaleToBigAnimation.duration = 0.5; scaleToBigAnimation.beginTime = 0; //缩小 CAKeyframeAnimation *scaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"]; scaleAnimation.values = @[@(1.7f), @(1)]; scaleAnimation.duration = 0.75; scaleAnimation.beginTime = scaleToBigAnimation.beginTime + scaleToBigAnimation.duration; //位置下移,与缩小动画同时进行 CABasicAnimation *positionAnimation = [CABasicAnimation animationWithKeyPath:@"position.y"]; positionAnimation.toValue = @(self.layer.position.y - NSWidth(self.frame)); positionAnimation.duration = scaleAnimation.duration; positionAnimation.beginTime = scaleAnimation.beginTime; //添加动画组 CAAnimationGroup *animationGroup = [CAAnimationGroup animation]; animationGroup.delegate = self; animationGroup.duration = scaleToBigAnimation.duration + positionAnimation.duration; [animationGroup setValue:@"animationGroup" forKey:@"AnimationKey"]; animationGroup.animations = @[scaleToBigAnimation, scaleAnimation, positionAnimation]; [self.layer addAnimation:animationGroup forKey:@"animationGroup"]; }
- (void)moveAnchorPointToCenter{ //由于图层锚点默认是在原点(0,0),需要让图层围绕中心点缩放 self.layer.anchorPoint = CGPointMake(0.5, 0.5); //锚点改变后,为了让图层随着视图移动,将图层的位置也改到锚点的位置 NSRect rect = self.frame; CGFloat centerX = rect.origin.x + rect.size.width / 2.f; CGFloat centerY = rect.origin.y + rect.size.height / 2.f; self.layer.position = CGPointMake(centerX, centerY); } - (void)resumeAnchorPoint{ self.layer.anchorPoint = CGPointZero; self.layer.position = self.frame.origin; }
动画结束后,在代理里触发下一步操作。
#pragma mark - CAAnimation delegate - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{ if (flag) { if ([self.delegate respondsToSelector:@selector(voiceRecordingWillBegin)]) { //执行代理方法,准备数据 [self.delegate voiceRecordingWillBegin]; } [self resumeAnchorPoint]; [self startRecording]; } }
-
第三阶段 ~ 第四阶段
本文 demo 里所有的音频数据都是随机生成的模拟数据,也没有实现真正的录音哈。
控制器在 voiceRecordingWillBegin 这个方法里,准备需要绘制的数据。
#pragma mark - voice recording button delegate //录音即将开始 - (void)voiceRecordingWillBegin{ //之前的动画只是图层动画,图层已经到了目标位置,但视图的 frame 还在原来的位置,因此要修改视图的位置和尺寸 NSRect frame = self.voiceRecordBtn.frame; CGFloat centerX = frame.origin.x + frame.size.width / 2.f; CGFloat centerY = frame.origin.y + frame.size.height / 2.f - frame.size.height; frame.size.width = 400; //这是蓝色波形图的最大宽度 frame.origin.x = centerX - frame.size.width / 2.f; frame.origin.y = centerY - frame.size.height / 2.f; self.voiceRecordBtn.frame = frame; //添加计时器,构造模拟数据 [self addTimer]; } - (void)addTimer{ //添加定时器 _timer = [NSTimer scheduledTimerWithTimeInterval:.1f target:self selector:@selector(addPoint) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]; } - (void)addPoint { //随机点 NSPoint point = NSMakePoint(self.voiceRecordBtn.bounds.size.height / 2.f, arc4random_uniform(NSHeight(self.voiceRecordBtn.frame) / 4.f) + 0); //插入到数组(动画视图最右边),array添加CGPoint需要转换一下 // [self.pointArray insertObject:[NSValue valueWithPoint:point] atIndex:0]; [self.pointArray addObject:[NSValue valueWithPoint:point]]; //传值,重绘视图 self.voiceRecordBtn.pointArray = self.pointArray; }
回到自定义按钮的.m 文件
点数组的 setter 方法
- (void)setPointArray:(NSArray *)pointArray{ _pointArray = pointArray; [self setNeedsDisplay:YES]; }
开始录音
- (void)startRecording{ self.talking = YES; [self setNeedsDisplay:YES]; //延迟_recordingDuration执行,若没有手动停止,则自动停止录音 [self performSelector:@selector(stopRecording) withObject:nil afterDelay:_recordingDuration]; }
停止录音
- (void)stopRecording{ //取消延迟执行 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(stopRecording) object:nil]; self.enabled = YES; self.frame = self.initialFrame; //录音结束后,按钮回到点击前的初始状态 self.talking = NO; [self setNeedsDisplay:YES]; if ([self.delegate respondsToSelector:@selector(voiceRecordingDidFinish)]) { [self.delegate voiceRecordingDidFinish]; } }
以下是重头戏了,每次调用
[self setNeedsDisplay:YES]
方法时,系统会自动调用drawRect:(NSRect)dirtyRect
方法,我们在这个方法里绘制数据。- (void)drawRect:(NSRect)dirtyRect{ [super drawRect:dirtyRect]; if (!self.talking) { //现在没有在录音,即初始状态 NSBezierPath *rectPath = [NSBezierPath bezierPathWithOvalInRect:dirtyRect]; [[NSColor blueColor] setFill]; [rectPath fill]; NSImage *image = [NSImage imageNamed:@"SideAudio"]; [image drawInRect:dirtyRect]; return; } CGFloat midY = NSHeight(dirtyRect) / 2.f; CGFloat midX = NSWidth(dirtyRect) / 2.f; CGFloat leftX = midX - _pointArray.count / 2.f - _initialWidth / 2.f; CGFloat rightX = midX + _pointArray.count / 2.f + _initialWidth / 2.f; // Drawing code here. CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; //绘制初始线型,模拟一般录音场景,刚开始可能没有说话,一条横线 CGMutablePathRef linePath = CGPathCreateMutable(); CGPathMoveToPoint(linePath, nil, leftX, midY); CGPathAddLineToPoint(linePath, nil, leftX + _initialWidth, midY); //_initialWidth 横线的宽度,这里给了个固定值 CGContextAddPath(ctx, linePath); //绘制上半部分波形 CGMutablePathRef halfPath = CGPathCreateMutable(); //绘制路径 CGPathMoveToPoint(halfPath, nil, NSWidth(dirtyRect), midY); for (NSInteger i = 0; i < _pointArray.count; i++) { NSValue *pointValue = _pointArray[i]; NSPoint point = pointValue.pointValue; NSInteger j = _pointArray.count - i - 1; if (point.y == 0) { CGPathMoveToPoint(halfPath, nil, rightX - j + 1, midY); CGPathAddLineToPoint(halfPath, NULL, rightX - j, midY); }else{ CGPathMoveToPoint(halfPath, nil, rightX - j, midY); CGPathAddLineToPoint(halfPath, NULL, rightX - j, midY + point.y); } } //实现波形图反转 CGMutablePathRef fullPath = CGPathCreateMutable();//创建新路径 CGPathAddPath(fullPath, NULL, halfPath); //合并路径 CGAffineTransform transform = CGAffineTransformIdentity; //反转 //反转配置 transform = CGAffineTransformTranslate(transform, 0, NSHeight(dirtyRect)); transform = CGAffineTransformScale(transform, 1.0, -1.0); CGPathAddPath(fullPath, &transform, halfPath); //将路径添加到上下文中 CGContextAddPath(ctx, fullPath); //绘制矩形区域,即不断变长的蓝色背景 CGMutablePathRef rectPath = CGPathCreateMutable(); CGPathMoveToPoint(rectPath, nil, leftX, 0); CGPathAddRoundedRect(rectPath, nil, CGRectMake(leftX, 0, _pointArray.count + _initialWidth, NSHeight(dirtyRect)), NSHeight(dirtyRect) / 2.f, NSHeight(dirtyRect) / 2.f); CGContextAddPath(ctx, rectPath); CGContextSetLineWidth(ctx, 1); CGContextSetStrokeColorWithColor(ctx, [NSColor whiteColor].CGColor); CGContextSetFillColorWithColor(ctx, [NSColor blueColor].CGColor); CGContextDrawPath(ctx, kCGPathFillStroke); //移除 CGPathRelease(halfPath); CGPathRelease(fullPath); }
结束,明天放播放录音的动画实现。
Demo 地址:https://github.com/YunFei2015/AudioWaveAnimation.git