本文涉及到的几个问题来源于一次面试经历 ,当时回答的一塌糊涂,所以回来就看看官方文档和一些博客文章,并问下同学看看他们如何回答这几个问题.
希望各位 iOS 开发同学,在学习使用 Cocoa的一些 Framework 时多看看一些官方文档说明,有时候官方文档都有明确说明,我就是没有看, so 我面试时回答不出来.
声明 : 本文代码部分来源于 objc 中国 View-Layer 协作 https://objccn.io/issue-12-4/
GitHub : https://github.com/ZhaoBingDong/CAAnimaiton
一 : 为什么跟 UI 相关的 API 要在主线程 (个人理解)
Note For the most part, use UIKit classes only from your app’s main thread. This is particularly true for classes derived from UIResponder or that involve manipulating your app’s user interface in any way. 在大多数的时候 , 在你的 app 里使用 UIKit 的类只能在主线程. 尤其适用于类UIResponder源于或涉及操纵你的应用程序的用户界面。 苹果 Foundation 和 UIKit 大部分 API 不是线程安全的 因为程序涉及到多线程地方毕竟是少数应用场景 苹果给每个方法属性加这种线程锁 线程同步锁 自旋锁 什么的 太麻烦 和消耗性能 需要开发者在使用到地方时自己注意下线程安全,避免多个线程访问同一块资源,包括多个线程对同一个 UI 对象的操作 像 NSArray NSDictionary 不可变的都是线程安全的 而 NSMutableArray NSMutableDictionary 线程是不安全的.
二 : 改变 UIView 的那些属性能够产生动画效果
The following properties of the UIView class are animatable: frame 包括了 Warning If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored. frame :The frame rectangle, which describes the view’s location and size in its superview’s coordinate system. // frame 是一个 view 在父 view 坐标系中的位置 和 尺寸 相对于父控件而言 Warning If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored. // 意思大概是 如果设置了 transform 属性后 不是 CGAffineTransformIdentity类型的 之前设置的 frame 将不起作用 ,当重新设置成 view.transform = CGAffineTransformIdentity 后 view 会使用原来的 frame , 使用 transfrom 去改变 view 的形变 位移 旋转效果后 ,动画结束后需要 设置 CGAffineTransformIdentity 进行复位. bounds : The bounds rectangle, which describes the view’s location and size in its own coordinate system. // bounds 一个 view 在自身坐标系的 位置和尺寸 相当于自设而言 center transform alpha backgroundColor 改变以上属性的值后 会产生一些动画效果
三 : 为什么设置 view.frame bounds center transform 等会有效果
当设置 view.frame 时 view 内部会调用 view的 actionForLayer:forKey: 生成一个 遵守 CAAction 的对象 ,生成一个动画的对象,并添加到 UIView 的 rootLayer 中去 ,然后调用 layer addAnimation 方法 将这个动画添加到 layer 上
1 通过重写 MyView 的 + (Class)layerClass { return [MyLayer class]; } // 生成动画的对象 基于 CAAction 协议的 动画对象 可以看做是 CAAnimation 或者遵守 CAAction 协议的私有类 - (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event { return [super actionForLayer:layer forKey:event]; } 将 UIView 的 layer 由系统的 CALayer 变成子类化的 MyLayer 这样便于重写 MyLayer 内部的 查看 那些动画对象添加到 MyLayer 上 一个改变 view.frame 的操作 会生成多个 CAAnimtaion 对像 二 将动画添加到 rootLayer 上 (一个 view 只有一个 rootLayer,它是view.layer 属性 ,一个 layer可以有多个 subLayers ,一个 subLayer 有且只有一个父 layer) - (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key { NSLog(@"/nadding animation: %@/n", [anim debugDescription]); [super addAnimation:anim forKey:key]; }
四 : 如何实现自定义的 UIView animations
iOS4以后 UIView 提供了 animateWithDuration:animations: 动画块 方法 支持一些基础动画 ,在此支持 需要使用 beginAnimations:context: 和 commitAnimations 来管理动画
将需要改变 view 的一些动画属性 封装到块代码里边. 那么我们是否可以将一些常用动画封装成这样的块代码呢 ,减少一些重复代码的调用?
如果你看了 问题2后 一定会有所启发 ,其实这是可以实现的,需要用到 runtime 去 拦截UIView系统的的 actionForLayer : forkey 为自己写的方法
+ (void)load { SEL originalSelector = @selector(actionForLayer:forKey:); SEL extendedSelector = @selector(DR_actionForLayer:forKey:); Method originalMethod = class_getInstanceMethod(self, originalSelector); Method extendedMethod = class_getInstanceMethod(self, extendedSelector); NSAssert(originalMethod, @"original method should exist"); NSAssert(extendedMethod, @"exchanged method should exist"); if(class_addMethod(self, originalSelector, method_getImplementation(extendedMethod), method_getTypeEncoding(extendedMethod))) { class_replaceMethod(self, extendedSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, extendedMethod); } } // 判断不同的上下文 区别系统的 UIView animaitons 动画 和自己写的 //[UIView DR_popAnimationWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations] - (id)DR_actionForLayer:(CALayer *)layer forKey:(NSString *)event { if (DR_currentAnimationContext == DR_popAnimationContext) { // 这里写我们自定义的代码... // 保存生成的每个动画对象所在 layer 即要实现动画的 view.layer 在 DR_popAnimationWithDuration 方法里 取出来保存的 layer 并 执行一些自定义的 CAAnimaiton 动画 DRSavedPopAnimationState *state = [DRSavedPopAnimationState savedStateWithLayer:layer keyPath:event]; [[UIView DR_savedPopAnimationStates] addObject:state]; } // 调用原始方法 return [self DR_actionForLayer:layer forKey:event]; // 没错,你没看错。因为它们已经被交换了 }
2 : 实现自定义 UIView animations 代码部分
+ (void)DR_popAnimationWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations { DR_currentAnimationContext = DR_popAnimationContext; // 执行动画 (它将触发交换后的 delegate 方法) 当你在 block 块里改变一些 view.frame 属性时 就会拦截到 UIView 的 actionforLayer 方法 拿到需要执行动画的 layer animations(); // 取出保存的 CALayer 对象 然后生成自定义的 CAAnimaiton 对象 添加到 layer 生成动画 NSMutableArray *states = [UIView DR_savedPopAnimationStates]; for (DRSavedPopAnimationState *state in states) { DRSavedPopAnimationState *savedState = (DRSavedPopAnimationState *)state; CALayer *layer = savedState.layer; NSString *keyPath = savedState.keyPath; id oldValue = savedState.oldValue; id newValue = [layer valueForKeyPath:keyPath]; CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:keyPath]; CGFloat easing = 0.2; CAMediaTimingFunction *easeIn = [CAMediaTimingFunction functionWithControlPoints:1.0 :0.0 :(1.0-easing) :1.0]; CAMediaTimingFunction *easeOut = [CAMediaTimingFunction functionWithControlPoints:easing :0.0 :0.0 :1.0]; anim.duration = duration; anim.keyTimes = @[@0, @(0.35), @1]; anim.values = @[oldValue, newValue, oldValue]; anim.timingFunctions = @[easeIn, easeOut]; // 不带动画地返回原来的值 [CATransaction begin]; [CATransaction setDisableActions:YES]; [layer setValue:oldValue forKeyPath:keyPath]; [CATransaction commit]; // 添加 "pop" 动画 [layer addAnimation:anim forKey:keyPath]; } // 动画执行完后 移除保存的 layer 等一些动画属性信息 [states removeAllObjects]; /* 一会儿再添加 */ 清空上下文信息 下次还根据这个上下文 区分 animateWithDuration 和 自定义的 DR_popAnimationWithDuration 方法 DR_currentAnimationContext = NULL; }
// 通过以上的代码部分 大致了解到了 UIView aniaitons 动画块内部都做了哪些操作 对核心动画有了进一步的了解
最终效果如下 :
Untitled.gif
作者:大兵布莱恩特
链接:https://www.jianshu.com/p/30f76623c171