BMMusicPlayer简介 基于FreeStreamer播放器二次封装。仿网易云封面图旋转,转圈音乐动效。全局基于ASDK。
里面有几个算是技术点吧,分别为:
仿’爱奇艺’Tabbar点击动画切换效果
list页面cell采用ASDK实现layer化
播放器单利实现,除”关闭”,”进度条滑块”采用ASDK实现layer化。
播放器背景使用封面图背景高斯模糊效果,获取图片主色调作为shandow。音效动画同样获取图片主色调。
现在来分别的来详细说说
仿”爱奇艺”Tabbar的点击切换效果。 之前一直觉得爱奇艺的动画做的好屌~很多动画单纯用代码实现,仅一个路径就要画死人。并且很多app的小控件做的都很厉害。你可以看看很多主流的播放器的播放暂停按钮,那个动画简直让人不知道如何用纯代码实现。在羡慕的同时就发现一个很有意思的库:lottie
。这个是airbnb 开源的一个动画库,当然现在已经很普及了。它是通过设计师设计或者网上下载一些素材通过Adobe After Effects 配合对应的外挂扩展插件到处一个json。然后再项目带入对应的库,调用json来实现动画效果。
整体体验下来效果还是不错的。而且过程也很简单。但是有一个痛点是灵活性不足—不方便于修改。一般来说都是设计师做好了整个成品(注意:里面可能还设计尺寸匹配),然后生成json,各端导入。而json文件为了大小,可读性基本为0。那么就带来了另外一个问题。几乎不可以二次修改。
有一个官方的素材库:https://www.lottiefiles.com/ 里面的动画真的厉害的不行。但是基于第二点问题。里面真正开源,也就是带有AEP
资源的其实很少。而一般来说这种动画都需要做一点程度的定制化,也就导致真正可用的其实不多。
不过~如果您公司有一个牛逼的设计师。用这个库还是超级爽歪歪的。
ASDK ASDK也就是FaceBook
开源的非常出名的AsyncDisplayKit
,但已经改名为Texture
。
最早知道这个库是在研究YYKit
时,作者提到很多核心渲染逻辑是借鉴的它。具体有多调,网上随便搜索一下就知道了。之前一直没有机会接触到,刚好有一个新项目,就用80%的ASDK
写了。
基本上重新封装了整个UIKit
,大到控制器,小到Label
。主要说一说使用感受。
异步控制:之前一直觉得既然是异步渲染,那么在写业务代码的时候是有多繁琐啊。使用下来发现其实还好。正常的View
里面的线程处理问题不会特别复杂,我也没遇到什么大的问题。主要遇到的问题都在动画上。
View
与Layer
选择:这个库很吊的是,很多空间都可以直接使用Layer
来绘制。比如播放器的List页面,里面的封面图,new整个视图,锁整个视图,标题,时间都是在一个层级。但是带来一个弊端是:调式有点傻~ 需要一个一个把isLayerBacked
关掉。
布局:首先ASDK
是不支持AutoLayout
的,引入了新的布局方式Flex
。要么你不想有成本就傻傻的用frame布局。但是这明显不是一个正确的选择。说说Flex
的学习成本吧,大概就是一天。然后你会发现这种布局方式太爽,太灵活,太兼容性强了。而且代码量也不是特别多。只是刚开始的时候有点不习惯。它的思路永远是从最小控件然后一点一点成Spec到最大。
缺点:学习成本,而且目前的2.7版本也不是一个很稳定版本(截稿位置,最近的commit是1天以前)。第三方的问题。正常的开发其实对第三方的依赖性很强的,而它不是特别的友好。动画问题,不知道这个是不是我个人的原因,总觉得怪怪的。而且因为异步的原因还有闪烁的问题,一直也没有很好的解决办法。
最后,给一点我的个人建议。排除这个库的体积来说,我觉得这个库还是值得引入的。但是已最小模块来使用。也就是说简单的没有交互的简单页面或者控件,小到VIew来用。你可以这样理解 就把他当做是一个可变图片处理器。可以动态绘制出你需要的图。放在对应的View上。或者。。。YYkit
播放器动画 播放器动画其实拆2个,一个是最简单的基础动画,旋转。一个是多条圈的波纹动画。波纹动画上还有一个小圆点,从几个地方旋转出来。
核心思想其实就是把小圆点放在 圈让,然后让圈也旋转。然后通过动画beginTime
参数来控制波纹的效果,简单理解就是每个延时一点产生的视觉效果。
眨眼一看波纹动画的代码量好多。我也很觉得在demo使用UIKit
的时候其实代码了很少的,只是在ASDK
也不知道为什么我用动画组的时候只能执行一个动画。也是排查了很久才找到这个问题。于是需要给每一个动画设置重复的属性。
还有一个点是在圆角上的Shadow
,也不算什么难点吧,但是可以提一下,就是分为2块,一块是是作为Shadow
,一块作为圆角的裁剪
iOSPalette
一个开源的提取一张图片的主色调。使得页面更加的生活灵活,不同的封面图下有不同的光晕和波纹动画,效果十分的赞。算法是Google
拔下来。具体看看作者简书就好了,我这里也不过多介绍了。
截图演示(声音播放部分无法演示)
主代码说明 1.Tabbar部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 BMTabBarController.m - (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item{ NSArray * sss = self.tabBar.subviews; NSMutableArray *tabbatButtonArray = [@[] mutableCopy]; for (UIView *view in sss) { if ([view isKindOfClass:NSClassFromString(@"UITabBarButton")]) { [tabbatButtonArray addObject:view]; } } for (UIView *view in [tabbatButtonArray[item.tag] subviews]) { if ([view isKindOfClass:NSClassFromString(@"UITabBarSwappableImageView")]) { [self.animation removeFromSuperview]; NSString * name = item.title; LOTAnimationView *animation = [LOTAnimationView animationNamed:name]; [view addSubview:animation]; animation.bounds = CGRectMake(0, 0,view.bounds.size.width,view.bounds.size.width); animation.center = CGPointMake(view.bounds.size.width/2, view.bounds.size.height/2); [animation playWithCompletion:^(BOOL animationFinished) { // Do Something }]; self.animation = animation; } } }
使用同一个animation
来播放动画,通过移除添加控制生命周期。
2.播放器页面高斯模糊,shandow,音效动画
因为使用asdk异步线程,告诉模糊放在了图片的归解档中。正常在主线程拿到下载后的图片中操作就好了。
1 2 3 4 5 6 7 8 9 10 11 _imageBackGroudNode.imageModificationBlock = ^UIImage * _Nullable(UIImage * _Nonnull image) { // GPUimage GPUImageGaussianBlurFilter *filter = [[GPUImageGaussianBlurFilter alloc] init]; filter.blurRadiusInPixels = 40.0; [filter forceProcessingAtSize:image.size]; GPUImagePicture *pic = [[GPUImagePicture alloc] initWithImage:image]; [pic addTarget:filter]; [pic processImage]; [filter useNextFrameForImageCapture]; return [filter imageFromCurrentFramebuffer]; };
获取主色作为shandow
1。因为是圆角添加shandow,涉及masksToBounds
所以是在coverPictureNode
背后有一个同样大小的coverPictureShadowNode
上添加的shandow。
2。使用#import "iOSPalette.h"
在- (**void**)getPaletteImageColor:(GetColorBlock)block;
中获取主颜色,并设置给coverPictureShadowNode
。同时这个时候把颜色给音效动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 _coverPictureNode.imageModificationBlock = ^UIImage * _Nullable(UIImage * _Nonnull image) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ [image getPaletteImageColor:^(PaletteColorModel *recommendColor, NSDictionary *allModeColorDic, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ weakSelf.coverPictureShadowNode.layer.shadowColor = [UIColor bm_colorWithHexString:recommendColor.imageColorString].CGColor; weakSelf.coverPictureShadowNode.layer.shadowOffset = CGSizeMake(0,10); weakSelf.coverPictureShadowNode.shadowRadius = 29; weakSelf.coverPictureShadowNode.shadowOpacity = 0.5; for (CALayer *layer in weakSelf.rippleArray) { layer.borderColor = [UIColor bm_colorWithHexString:recommendColor.imageColorString].CGColor; } for (CALayer *layer in weakSelf.rippleCircleArray) { layer.backgroundColor = [UIColor bm_colorWithHexString:recommendColor.imageColorString].CGColor; } }); }]; }); CGRect circleRect = (CGRect) {CGPointZero, CGSizeMake(image.size.width, image.size.height)}; UIGraphicsBeginImageContextWithOptions(circleRect.size, NO, 0); UIBezierPath *circle = [UIBezierPath bezierPathWithRoundedRect:circleRect cornerRadius:circleRect.size.width/2]; [circle addClip]; [[UIColor whiteColor] set]; [circle fill]; [image drawInRect:circleRect]; UIImage *roundedImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return roundedImage; };
音效动画分为2个小模块:1.旋转。2.波纹
旋转部分:
1 2 3 4 5 6 7 8 9 - (void)addAnimation { CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; animation.duration = 15; animation.repeatCount = MAXFLOAT; animation.removedOnCompletion = NO; animation.toValue = @(M_PI*2); [self.coverPictureNode.layer addAnimation:animation forKey:@"rotationAnimation"]; }
波纹部分:
在波纹地方有其实就是有kCoverPictureRippleCount
条圈通过不同的beginTime
实现。在asdk中有一个小问题是当使用动画组的时候,仅显示第一个动画,还没找到原因。当使用正常View时,通过设置动画组,并删除动画相同属性就好了,这样代码也精简很多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 - (void)addRippleAnimation { self.rippleArray = [@[] mutableCopy]; self.rippleCircleArray = [@[] mutableCopy]; CALayer * animationLayer = [CALayer layer]; CGFloat maxRadius = kBMSCREEN_WIDTH /2; for (int i = 0; i<kCoverPictureRippleCount; i++) { CALayer * pulsingLayer = [CALayer layer]; pulsingLayer.frame = CGRectMake(0, 0, maxRadius*2, maxRadius*2); pulsingLayer.position = CGPointMake(BM_FitW(kCoverPictureHW)/2, BM_FitW(kCoverPictureHW)/2); pulsingLayer.backgroundColor = [UIColor clearColor].CGColor; pulsingLayer.cornerRadius = maxRadius; pulsingLayer.borderWidth = kCoverPictureRippleMaxBorderWidth; CALayer *lay = [CALayer layer]; lay.frame = CGRectMake(0, 0, kCoverPictureRippleCircleSize, kCoverPictureRippleCircleSize); lay.cornerRadius = kCoverPictureRippleCircleSize/2; lay.masksToBounds = YES; lay.position = CGPointMake(maxRadius*2 * sin(45), maxRadius*2 * sin(45)); [pulsingLayer addSublayer:lay]; CAMediaTimingFunction * defaultCurve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]; CAAnimationGroup * animationGroup = [CAAnimationGroup animation]; animationGroup.fillMode = kCAFillModeBackwards; animationGroup.beginTime = CACurrentMediaTime() + i * kCoverPictureRippleDuration / kCoverPictureRippleCount; animationGroup.duration = kCoverPictureRippleDuration; animationGroup.repeatCount = HUGE; animationGroup.timingFunction = defaultCurve; CABasicAnimation * scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; scaleAnimation.fromValue = @(BM_FitW(kCoverPictureHW)/2 / maxRadius); scaleAnimation.toValue = @1.0; scaleAnimation.beginTime = CACurrentMediaTime() + i * kCoverPictureRippleDuration / kCoverPictureRippleCount; scaleAnimation.fillMode = kCAFillModeBackwards; scaleAnimation.timingFunction = defaultCurve; scaleAnimation.duration = kCoverPictureRippleDuration; scaleAnimation.repeatCount = HUGE; scaleAnimation.removedOnCompletion = NO; scaleAnimation.fillMode = kCAFillModeForwards; CABasicAnimation *animation = [CABasicAnimation new]; animation.keyPath = @"transform.rotation.z"; animation.beginTime = CACurrentMediaTime() + i * kCoverPictureRippleDuration / kCoverPictureRippleCount; animation.fromValue = [NSNumber numberWithFloat:i *(M_PI/2)]; // 起始角度 animation.toValue = [NSNumber numberWithFloat:i *(M_PI/2) + 2*M_PI]; // 终止角度 animation.duration = 20; animation.repeatCount = HUGE; animation.timingFunction = defaultCurve; animation.removedOnCompletion = NO; animation.fillMode = kCAFillModeForwards; CAKeyframeAnimation * opacityAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"]; opacityAnimation.beginTime = CACurrentMediaTime() + i * kCoverPictureRippleDuration / kCoverPictureRippleCount; opacityAnimation.values = @[@0.3, @0.5, @0]; opacityAnimation.keyTimes = @[@0, @0.3, @1]; opacityAnimation.duration = kCoverPictureRippleDuration; opacityAnimation.repeatCount = HUGE; opacityAnimation.timingFunction = defaultCurve; opacityAnimation.removedOnCompletion = NO; opacityAnimation.fillMode = kCAFillModeForwards; // 有一个位置问题,ASDK使用animationGroup 仅显示一个。 // animationGroup.animations = @[scaleAnimation, opacityAnimation,animation]; [pulsingLayer addAnimation:scaleAnimation forKey:@"plulsing"]; [pulsingLayer addAnimation:animation forKey:@"dsdasdasd"]; [pulsingLayer addAnimation:opacityAnimation forKey:@"plulsidsadang"]; [animationLayer addSublayer:pulsingLayer]; [self.rippleArray addObject:pulsingLayer]; [self.rippleCircleArray addObject:lay]; } _animationLayer = animationLayer; [self.coverPictureShadowNode.layer addSublayer:animationLayer]; }
停止动画:
单纯移除动画会导致旋转归零,产生视觉不适。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 - (void)updateCoverPictureRotating { if (!_palybtnNode.selected) { // 停止动画 CFTimeInterval pausedTime = [self.coverPictureNode.layer convertTime:CACurrentMediaTime() fromLayer:nil]; self.coverPictureNode.layer.speed = 0.0; self.coverPictureNode.layer.timeOffset = pausedTime; _animationLayer.hidden = YES; }else { CFTimeInterval pausedTime = [self.coverPictureNode.layer timeOffset]; self.coverPictureNode.layer.speed = 1.0; self.coverPictureNode.layer.timeOffset = 0.0; self.coverPictureNode.layer.beginTime = 0.0; CFTimeInterval timeSincePause = [self.coverPictureNode.layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime; self.coverPictureNode.layer.beginTime = timeSincePause; _animationLayer.hidden = NO; } }