研究了一下抖音的动态贴图,抓包抖音的资源发现,其动态贴图资源包(一个zip压缩包)包括一张包含所有帧的大图,一个标识所有帧位置的json和一个lua脚本,还有其他一些配置文件,
这些最终合成了一个帧动画,lua脚本中有个什么EffectSDK,设置帧率,旋转,放大等效果,帧率一般为16。
其中把所有帧放到同一张图片然后和配置文件(可能是json或其他格式的,标明每一帧在图片中的位置的)一起使用上是游戏开发中常用的手段,一般称为spriteSheet,这种动画可以称为 spriteSheet Animation 或者spriteAnimation,可以参考 https://www.codeandweb.com/texturepacker 抖音也是用这个工具的。
有了这些资源,就可以实现动画了
其实如何实现动画texturePackers的官网上有相关教程,参考https://www.codeandweb.com/texturepacker/tutorials/uikit-animations-with-texturepacker
核心是利用了CALayer的contentsRect属性;
/* A rectangle in normalized image coordinates defining the
* subrectangle of the `contents' property that will be drawn into the
* layer. If pixels outside the unit rectangles are requested, the edge
* pixels of the contents image will be extended outwards. If an empty
* rectangle is provided, the results are undefined. Defaults to the
* unit rectangle [0 0 1 1]. Animatable. */
@property CGRect contentsRect;
contentsRect配合bounds,这两个属性就可以实现显示图片的某一部分,然后依次显示图片的指定部分,就是动画了;
他的代码自定义了一个frameIndex属性,然后通过给这个属性添加动画来实现最终的效果;(如何给CALayer添加自定义属性动画可以参考:Layer 中自定义属性的动画,这里不再复述;)
我感觉自定义frameIndex然后对其做动画没不是必须的,可以直接给layer的contentsRect和bounds做keyframeAnimation,可以达到完全一样的效果。
这里还更深入的理解了一下CALayer,主要是presentationLayer和modelLayer,参考:https://blog.csdn.net/u013282174/article/details/50388546
顺便记录一下开发过程中遇到的小问题:
1,CALayer的隐式动画
当直接修改CALayer的标为animatable的属性时,系统不是瞬间就将其变过来的,而是会经历一个动画,就是系统的隐式动画。
如果想要关闭这个隐式动画,主要有两种方式:
1)使用CATransaction并将DisableActions设置为YES;注意不是CATransition!!!别看错了,我因为没看清这个被自己坑了半天。。。
需要注意的是,这个方法仅针对这次修改有效,如果之后再普通的修改属性,还是会触发隐式动画
[CATransaction begin];
[CATransaction setValue:@(true) forKey:kCATransactionDisableActions];
//在这里修改属性 不会触发隐式动画
[CATransaction commit];
2)将对应属性的action设置为nil;
注意:这种方法是针对整个实例生效的,设置之后,之后随便修改属性,都不会触发隐式动画
//1 如果要自定义类,且这个类的所有实例都要忽略某个属性的隐式动画,可以用这几句代码
+ (id<CAAction>)defaultActionForKey:(NSString *)event {
if ([event isEqualToString:@"contentsRect"]) {
return (id<CAAction>)[NSNull null];
}
return [super defaultActionForKey:event];
}
//2 如果仅仅是某个实例要忽略,可以用这一句代码
self.actions = @{@"contentsRect" : [NSNull null]};
2, removedOnCompletion属性,
anim1.removedOnCompletion = false;//没有这一句的话动画根本就不会开始,奇葩了
anim1.fillMode = kCAFillModeForwards;
之前没记得removedOnCompletion这么重要啊,这次在写动画的时候,只要不将其设置为false,动画就不会开始。。。
连最简单的position动画,从左移动到右的这种,没有设置removedOnCompletion都不行。。。难道是iOS12以后系统改了?
3,如果使用了自定义属性的动画,并且实现了dealloc的方法,会发现在动画过程中dealloc会不停的调用,
因为不知道CALayer的designedInit方法是什么,所以不知道是不是也在不停的创建,
难道是系统是用runtime创建了CALayer的子类,然后用子类怎么实现了自定义属性动画???
有知道原因的,望不吝赐教
4, @property(copy) CAAnimationCalculationMode calculationMode;
这次第一次注意到calculationMode这个属性,关键是用了kCAAnimationDiscrete
这个值。
这个值的意思离散插值,注意了,不是不插值,而是让值保持不变,具体还是官方文档说的最清楚:https://developer.apple.com/documentation/quartzcore/kcaanimationdiscrete
而且还有一点需要注意的是:如果用了discrete,那keyframeAnimation的keyTimes要比values多一个,且第一个必须为0,最后一个必须为1,官方文档说的:
* If the calculationMode is set to kCAAnimationLinear or kCAAnimationCubic, the first value in the array must be 0.0 and the last value must be 1.0\. All intermediate values represent time points between the start and end times.
* If the calculationMode is set to kCAAnimationDiscrete, the first value in the array must be 0.0 and the last value must be 1.0\. The array should have one more entry than appears in the values array. For example, if there are two values, there should be three key times.
* If the calculationMode is set to kCAAnimationPaced or kCAAnimationCubicPaced, the values in this property are ignored.
5,CAKeyframeAnimation的keyTimes属性是可选的,如果设置了values又没有设置keyTimes的话,就会平均计算插值,
所以一般情况下来说,如果不需要精确控制帧的时间,还是不设置的为好。
6,CALayer的position一定要注意
如果单纯设置CALayer的bounds,那其position还是(0,0),如果设置CALayer的frame,那其position会被同时设置到frame的center。
7,CALayer的contents也是可以用来做动画的,网上很多现实gif的功能就是通过给contents做CAKeyframeAnimation来实现的。
这里有个坑是:这个方法会很占内存,图片在所占的内存大概是(CGImage.with * CGImage.height * 4 / 1024 / 1024 )M, 如果是UIImage的话就要乘以scale;
如果帧动画要用50张图片,每张1M的好,就要占50M,如果很不巧有一张大图是2000*2000的,那内存直接就爆了,APP就直接被kill掉了。。。。
8,在实现sprite动画的时候,想到了可以用把每一帧截出来保存成CGImage数组的方式:
NSMutableArray *imageArray = [NSMutableArray array];
CGFloat scale = 1;
for (int i = 0; i < self.frameArray.count; i++) {
OJATextureFrameModel *targetFrame = [self.frameArray objectAtIndex:i];
CGRect bounds = CGRectMake(targetFrame.frame.x * scale,
targetFrame.frame.y * scale,
targetFrame.frame.w * scale,
targetFrame.frame.h * scale);
CGImageRef subImage = CGImageCreateWithImageInRect(image.CGImage, bounds);
[imageArray addObject:(__bridge id _Nonnull)(subImage)];
}
按CGImageCreateWithImageInRect的文档
/* Create an image using the data contained within the subrectangle `rect'
of `image'.
The new image is created by
1) adjusting `rect' to integral bounds by calling "CGRectIntegral";
2) intersecting the result with a rectangle with origin (0, 0) and size
equal to the size of `image';
3) referencing the pixels within the resulting rectangle, treating the
first pixel of the image data as the origin of the image.
If the resulting rectangle is the null rectangle, this function returns
NULL.
If W and H are the width and height of image, respectively, then the
point (0,0) corresponds to the first pixel of the image data; the point
(W-1, 0) is the last pixel of the first row of the image data; (0, H-1)
is the first pixel of the last row of the image data; and (W-1, H-1) is
the last pixel of the last row of the image data.
The resulting image retains a reference to the original image, so you may
release the original image after calling this function. */
创建出来的新的CGImage会对原来图片的强引用,那应该不会重新分配内存才对,
按我的理解,比如把一张1000 * 1000 的图分解成100张100*100的图,所占的内存应该是一样的。
在iOS12上也确实是这样,直接对contents添加keyframeAnimation,所占用的内存就是一张大图的内存(貌似解析图片的时候会卡一下。。。)
但在iOS11上,内存直接就爆了,在模拟器上内存比较大,才看出来,内存的大小是 小图的个数 * 大图的内存。。。。。。感觉是系统bug呀,解决不了。
9,如果上了上面所说的方法,创建了一个CGImage的array,那一定要在dealloc的时候将里面的CGImage挨个CGImageRelease,否则就会内存泄露,而且没有任何提示,
也不会影响layer的是否,如果不是刚好看了内存变化,根本发现不了。。。。
10,beginTime属性,或者timeOffset属性,
如果想动画迟计秒再开始,可以利用beginTime属性来实现,
如果想要实现动画的暂停与继续,就稍微麻烦一点了,可以参考的代码
- (void)pause
{
if(state == TPSPRITE_RUNNING)
{
state = TPSPRITE_PAUSED;
CFTimeInterval pausedTime = [self convertTime:CACurrentMediaTime() fromLayer:nil];
self.speed = 0.0;
self.timeOffset = pausedTime;
}
}
- (void)resume
{
if(state == TPSPRITE_PAUSED)
{
state = TPSPRITE_RUNNING;
CFTimeInterval pausedTime = [self timeOffset];
self.speed = 1.0;
self.timeOffset = 0.0;
self.beginTime = 0.0;
CFTimeInterval timeSincePause = [self convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
self.beginTime = timeSincePause;
}
}