因为近期项目需要实现类似iOS 10相册中的回忆功能:利用照片或图片合成制作视频的功能。
这篇文章的目的是做一个简单的分享,如何利用CALayer、CAAnimation和视频合成的相关知识点来完成这一功能的开发。
设计思路:
流程上首先我们需要获取系统相册里的照片和视频,选择好后经过一些列处理将他们合成为一个视频,我们还需要对原视频进行逐帧分解,为什么这样做我会再后面进行解答。
省略掉从系统相册获取视频和图片的部分,我们分别从处理图片和视频开始。
我需要做的是将图片合成视频并能够和系统相册中的视频再合并成一个视频,并能够在预览的时候有一个很好的交互。我在这里考虑使用CALayer和CAAnimation。通过给一个view的layer添加Animation实现内容切换大小改变甚至是淡入淡出的渐变效果。
CALayer
CALayer是一个管理基于图像的内容的对象,它可以使我们在该内容上执行动画的对象。图层的主要工作是管理我们的视觉内容,但图层本身具有可设置的视觉属性,例如背景颜色,边框和阴影。除了管理可视内容之外,该层还可以用于维护在屏幕上呈现的几何信息(例如其位置,大小和变换)。一个图层对象包含了持续时间和它的走向,它基于支持CAMediaTiming协议的动画,定义了这个图层的时间信息。
CAAnimation
CAAnimation 是核心动画的抽象超类。它为CAMediaTiming和CAAction协议提供基本的支持。
我们要做的事情就是利用CAAnimation在CALayer上执行各个动画。
我们有一个UIImageView,我们将会把我们的图片按照顺序依次显示在这个UIImageView上。
图片处理
我们首先是定义一个动画组Group来放入我们所有的动画。
CAAnimationGroup *group = [CAAnimationGroup animation];
设置某一帧的关键动画显示这张图片
CAKeyframeAnimation * contentsAnimation;
contentsAnimation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
contentsAnimation.duration = 0.5f;
contentsAnimation.removedOnCompletion = NO;
contentsAnimation.fillMode = kCAFillModeForwards;
UIImage *image = /**你的图片**/;
contentsAnimation.values = @[(__bridge UIImage*)image.CGImage];
contentsAnimation.beginTime = totalDuration;
[animations addObject:contentsAnimation];
这是某一张图片的淡入效果:
CAKeyframeAnimation * showAnimation;
showAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
showAnimation.duration = 0.5;
showAnimation.removedOnCompletion = NO;
showAnimation.fillMode = kCAFillModeForwards;
showAnimation.values = @[[NSNumber numberWithFloat:0.0],[NSNumber numberWithFloat:1.0]];
showAnimation.beginTime = AVCoreAnimationBeginTimeAtZero;
[animations addObject:showAnimation];
为了防止图片变形和拉伸,记得设置此时显示图片的Layer的大小
CAKeyframeAnimation * boundsAnimation;
boundsAnimation = [CAKeyframeAnimation animationWithKeyPath:@"bounds"];
boundsAnimation.duration = 0.5f;
boundsAnimation.removedOnCompletion = NO;
boundsAnimation.fillMode = kCAFillModeForwards;
boundsAnimation.values = @[[NSValue valueWithCGRect:CGRectMake(xPoint,yPoint,imageWidth,imageHeight)]];
boundsAnimation.beginTime = AVCoreAnimationBeginTimeAtZero;
[animations addObject:boundsAnimation];
这之后0.5s后给它加个效果吧,比如放大
CAKeyframeAnimation *scaleAnimation;
scaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
scaleAnimation.duration = 1.0f;
scaleAnimation.removedOnCompletion = NO;
scaleAnimation.fillMode = kCAFillModeForwards;
scaleAnimation.values = @[[NSNumber numberWithFloat:1],[NSNumber numberWithFloat:2.0]];
scaleAnimation.beginTime = 0.5f;
[animations addObject:scaleAnimation];
在展示了这一张图片后,我们该让它淡出舞台了
CAKeyframeAnimation * dissAnimation;
dissAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
dissAnimation.duration = 0.5;
dissAnimation.removedOnCompletion = NO;
dissAnimation.fillMode = kCAFillModeForwards;
dissAnimation.values = @[[NSNumber numberWithFloat:1.0],[NSNumber numberWithFloat:0.8],[NSNumber numberWithFloat:0.4],[NSNumber numberWithFloat:0.0]];
dissAnimation.beginTime = totalDuration;
[animations addObject:dissAnimation];
如此循环,我们就可以展示一系列的图片了。
PS:补充一下 animationWithKeyPath可以使用的值:
transform.scale
transform.scale.x
transform.scale.y
transform.rotation.z
opacity
margin
zPosition
backgroundColor
cornerRadius
borderWidth
bounds
contents
contentsRect
cornerRadius
frame
hidden
mask
masksToBounds
opacity
position
shadowColor
shadowOffset
shadowOpacity
shadowRadius
视频处理
下面我将开始处理视频。
因为之前在利用CALayer和CAAnimation处理一些动画效果时,用上了gif图片来展示一系列的动画效果。这里我们也运用类似的方式把视频放入我们的动画Group中。
处理视频首先我们是需要逐帧分解这个视频:
CGSize targetSize = CGSizeMake(600,600);
int fps = 20;
AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
imageGenerator.requestedTimeToleranceBefore = kCMTimeZero;
imageGenerator.requestedTimeToleranceAfter = kCMTimeZero;
imageGenerator.apertureMode = AVAssetImageGeneratorApertureModeProductionAperture;
imageGenerator.appliesPreferredTrackTransform = YES;
imageGenerator.maximumSize = CGSizeMake(targetSize.width/2.0, targetSize.height/2.0);
CMTime cmtime = asset.duration; //视频时间信息结构体
Float64 durationSeconds = CMTimeGetSeconds(cmtime)>3?3:CMTimeGetSeconds(cmtime); //视频总秒数,这里我们只取3秒以内的部分。
NSMutableArray *times = [NSMutableArray array];
Float64 totalFrames = durationSeconds * fps; //获得视频总帧数
CMTime timeFrame;
for (int i = 1; i <= totalFrames; i++) {
timeFrame = CMTimeMake(i, fps); //第i帧 帧率
NSValue *timeValue = [NSValue valueWithCMTime:timeFrame];
[times addObject:timeValue];
}
NSMutableArray *videoThumbArray = [NSMutableArray array];
[imageGenerator generateCGImagesAsynchronouslyForTimes:times
completionHandler:^(CMTime requestedTime,
CGImageRef _Nullable image,
CMTime actualTime,
AVAssetImageGeneratorResult result,
NSError * _Nullable error)
{
if (result == AVAssetImageGeneratorSucceeded)
{
UIImage *tempImage = [UIImage imageWithCGImage:image];
[videoThumbArray addObject:tempImage];
if (requestedTime.value == times.count)
{
NSLog(@"搞定");
}
}
else
{
NSLog(@"获取视频截图出错");
}
}];
然后我们便拿到了分解出来的视频逐帧图片数组。类似图片处理的部分,我们将它放入我们的Group并设定好beginTime。
CAKeyframeAnimation * contentsAnimation;
contentsAnimation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
contentsAnimation.duration = /**视频的时长**/;
contentsAnimation.removedOnCompletion = NO;
contentsAnimation.fillMode = kCAFillModeForwards;
NSMutableArray *values = [NSMutableArray array];
for (int v = 0; v<videoThumbArray.count; v++)
{
UIImage *tempImage = videoThumbArray[v];
[values addObject:(__bridge UIImage*)tempImage.CGImage];
}
contentsAnimation.values = values;
contentsAnimation.beginTime = /**开始时间**/;
[animations addObject:contentsAnimation];
我们也可以像图片那样给这段视频的前后加入淡入淡出的转换动画。完成后我们便可以把这个Group加入我们想要用于显示的Layer中了。
播放
接下来就是要实现如何播放这一些列的动画了。但这之前还是要对几个属性和概念进行了解。
CAMediaTiming协议
该协议通过涂层和动画实现,它构建了一个分层的计时系统,当中的所有对象都描述了其从父对象中的时间值到本地时间的映射。
进行这样一个映射或转换,需要两个步骤:
(1)转换为活动的本地时间。该活动时间包括了该对象出现在父级时间轴上的点,以及与父级的相对速度。
(2)从活动时间转换为本地基本时间。时间模型允许对象多次重复其基本持续时间,并且可选地在重复之前向后播放。
当把这个让人摸不着头脑的协议拿出来谈着后,我们实际需要的是它的属性,speed和timeOffset。
speed
图层的速度,用于将父类时间缩放至本地时间。例如,若当前对象或图层速度为2,那么它相对于父类其速度是父类的2倍。
timeOffset
活动时间的偏移值。父类时间转换至活动的本地时间存在着这么一个公式:t = (tp - begin) * speed + offset.
提到这两个属性是因为我们需要它们来暂停和播放我们的预览视频。我们将所有的动画放入Group中,把它添加在一个speed为0的图层上,然后运用类似Timer的方式,以60帧速率播放每一帧的动画,即设置这个图层的timeOffset,从而实现我们对这些动画的播放、暂停和定位。
CADisplayLink
在这里,我们使用CADisplayLink来作为Timer来刷新我们的动画。CADisplayLink是一个与屏幕刷新率相同的Timer类。之所以使用它是因为相较于NSTimer,CADisplayLink的触发时间更加精准,更适合用于一帧一帧地播放我们Group中的动画。在播放时我们只需要如上文所述调整图层的timeOffset即可。
target.layer.timeOffset += 1.0/60.0;
CADisplayLink也可以很方便地进行暂停等操作。
在完成了图片合成视频的预览之后,我们可以通过AVComposition和AVAssetExportSession真正地合成完整的视频文件,还可以加入音乐等处理。
这个是本文章DEMO的地址:Github