iOS CAAnimation(二)Layer層隱式動畫相關

我們知道,修改自定義創建的layer的部分屬性時,會有動畫的效果。但是修改view 的屬性時,卻沒有動畫出現,這是爲什麼呢?

Core Animation implements its implicit animation behaviors for layers using action objects. An action object is an object that conforms to the CAAction protocol and defines some relevant behavior to perform on a layer. All CAAnimation objects implement the protocol, and it is these objects that are usually assigned to be executed whenever a layer property changes.

Animating properties is one type of action but you can define actions with almost any behavior you want. To do that, though, you have to define your action objects and associate them with your app’s layer objects.

CoreAnimation通過使用action對象來實現layer層的隱式動畫。

@protocol CAAction
- (void)runActionForKey:(NSString *)event object:(id)anObject
    arguments:(nullable NSDictionary *)dict;
@end

action是實現了CAAction協議,定義了layer層相關動畫的對象。所有的CAAnimation對象都實現了這個協議,當layer的屬性改變時,action對象會被分配關聯到layer上,其動畫方法被調用。

我們可以自定義action對象並關聯在相應的layer上實現想要的任何效果。

Before an action can be performed, the layer needs to find the corresponding action object to execute. The key for layer-related actions is either the name of the property being modified or a special string that identifies the action. When an appropriate event occurs on the layer, the layer calls its actionForKey: method to search for the action object associated with the key. Your app can interpose itself at several points during this search and provide a relevant action object for that key.

開始展示動畫之前,layer需要找到合適的action對象。

layer通過key去查找合適的action對象,可以用這個key返回一個action對象給layer,key是將要修改的屬性名或指定的action的名字。查找流程式怎樣的呢?

來到CALayer類中查看一些方法:

///* Returns the action object associated with the event named by the
// * string 'event'. The default implementation searches for an action
 //* object in the following places:
// *
// * 1. if defined, call the delegate method -actionForLayer:forKey:
// * 2. look in the layer's `actions' dictionary
// * 3. look in any `actions' dictionaries in the `style' hierarchy
// * 4. call +defaultActionForKey: on the layer's class
// *
// * If any of these steps results in a non-nil action object, the
// * following steps are ignored. If the final result is an instance of
// * NSNull, it is converted to `nil'. */
- (nullable id<CAAction>)actionForKey:(NSString *)event;

///* Returns the default action object associated with the event named by
// * the string 'event'. The default implementation returns a suitable
 //* animation object for events posted by animatable properties, nil
// * otherwise. */
+ (nullable id<CAAction>)defaultActionForKey:(NSString *)event;

///* Attach an animation object to the layer. Typically this is implicitly
// * invoked through an action that is an CAAnimation object.
// *
// * 'key' may be any string such that only one animation per unique key
// * is added per layer. The special key 'transition' is automatically
// * used for transition animations. The nil pointer is also a valid key.
// *
 //* If the `duration' property of the animation is zero or negative it
// * is given the default duration, either the value of the
// * `animationDuration' transaction property or .25 seconds otherwise.
// *
// * The animation is copied before being added to the layer, so any
// * subsequent modifications to `anim' will have no affect unless it is
// * added to another layer. */

- (void)addAnimation:(CAAnimation *)anim forKey:(nullable NSString *)key;

同時查看CALayer的協議中的這個方法:

/* If defined, called by the default implementation of the
 * -actionForKey: method. Should return an object implementing the
 * CAAction protocol. May return 'nil' if the delegate doesn't specify
 * a behavior for the current event. Returning the null object (i.e.
 * '[NSNull null]') explicitly forces no further search. (I.e. the
 * +defaultActionForKey: method will not be called.) */

- (nullable id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event;

我們先來分析一下他們之間的聯繫:

Core Animation looks for action objects in the following order:

  1. If the layer has a delegate and that delegate implements the actionForLayer:forKey: method, the layer calls that method. The delegate must do one of the following:

    • Return the action object for the given key.

    • Return nil if it does not handle the action, in which case the search continues.

    • Return the NSNull object, in which case the search ends immediately.

  2. The layer looks for the given key in the layer’s actions dictionary.

  3. The layer looks in the style dictionary for an actions dictionary that contains the key. (In other word, the style dictionary contains an actions key whose value is also a dictionary. The layer looks for the given key in this second dictionary.)

  4. The layer calls its defaultActionForKey: class method.

  5. The layer performs the implicit action (if any) defined by Core Animation.

If you provide an action object at any of the appropriate search points, the layer stops its search and executes the returned action object. When it finds an action object, the layer calls that object’s runActionForKey:object:arguments: method to perform the action. If the action you define for a given key is already an instance of the CAAnimation class, you can use the default implementation of that method to perform the animation. If you are defining your own custom object that conforms to the CAAction protocol, you must use your object’s implementation of that method to take whatever actions are appropriate.

當layer的屬性改變的時候會按照這樣的順序來執行:

1.首先調用- (nullable id<CAAction>)actionForKey:(NSString *)event;

2.如果view代理方實現了- actionForLayer: forKey: 方法則會調用,

可以返回給定key的action對象,查找停止;可以返回nil讓查找繼續;可以返回NSNull讓查找停止;

3.接着查找action這個字典;

4.然後查找style這個字典;

5.調用+ (nullable id<CAAction>)defaultActionForKey:(NSString *)event;這個方法。

6.layer執行CoreAnimation定義的隱式動畫。

如果2-5條步驟返回的不是nil則會停止查找過程。

這裏是一些部署aciton對象位置的建議:

Where you install your action objects depends on how you intend to modify the layer.

  • For actions that you might apply only in specific circumstances, or for layers that already use a delegate object, provide a delegate and implement its actionForLayer:forKey: method.

  • For layer objects that do not normally use a delegate, add the action to the layer’s actions dictionary.

  • For actions related to custom properties that you define on the layer object, include the action in the layer’s style dictionary.

  • For actions that are fundamental to the behavior of the layer, subclass the layer and override the defaultActionForKey: method.

我們用代碼來驗證一下:

創建一個繼承UIView的TestView類,重寫其+layerClass方法,使其和TestViewLayer(繼承CALayer)綁定。

//TestView.h
#import <UIKit/UIKit.h>
@interface TestView : UIView
@end

//TestView.m
#import "TestView.h"
#import "TestViewLayer.h"
@implementation TestView
+(Class)layerClass{
    return [TestViewLayer class];
}
-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event{
    NSLog(@"%s",__func__);
    return [super actionForLayer:layer forKey:event];
}
@end
//TestViewLayer.h
#import <QuartzCore/QuartzCore.h>
@interface TestViewLayer : CALayer
@end

//TestViewLayer.m
#import "TestViewLayer.h"
@implementation TestViewLayer
-(id<CAAction>)actionForKey:(NSString *)event{
    NSLog(@"%s",__func__);
    return [super actionForKey:event];
}
+(id<CAAction>)defaultActionForKey:(NSString *)event{
    NSLog(@"%s",__func__);
    return [super defaultActionForKey:event];
}
-(void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key{
    NSLog(@"%s",__func__);
    [super addAnimation:anim forKey:key];
}
@end

創建一個繼承CALayer的TestLayer類。

//TestLayer.h
#import <QuartzCore/QuartzCore.h>
@interface TestLayer : CALayer
@end

//TestLayer.m
#import "TestLayer.h"
@implementation TestLayer
-(id<CAAction>)actionForKey:(NSString *)event{
    NSLog(@"%s",__func__);
    return [super actionForKey:event];
}
+(id<CAAction>)defaultActionForKey:(NSString *)event{
    NSLog(@"%s",__func__);
    return [super defaultActionForKey:event];
}
-(void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key{
    NSLog(@"%s",__func__);
    [super addAnimation:anim forKey:key];
}
@end

將TestView的view 和 TestLayer的layer添加在TestVC的view和view.layer上。

//TestVC.m
#import "TestVC.h"
#import "TestLayer.h"
#import "TestView.h"
@interface TestVC (){
    TestLayer *tLayer;
    TestView *tView;
}
@end
@implementation TestVC
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    tLayer = [TestLayer layer];
    tLayer.frame = CGRectMake(100, 150, 200, 200);
    tLayer.backgroundColor = [UIColor redColor].CGColor;
    [self.view.layer addSublayer:tLayer];
    
    tView = [[TestView alloc]initWithFrame:CGRectMake(200, 400, 200, 200)];
    tView.backgroundColor = [UIColor greenColor];
    [self.view addSubview:tView];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    tLayer.position = CGPointMake(200, 200);
   // tView.center = CGPointMake(100, 100);
}
@end

準備工作做好之後,我們來看一下,在TestVC的touchBegan方法中改變tLayer的position。來看一下各個方法的調用順序:

可見當我們修改一個layer的屬性時,會通過-actionForKey:event方法獲得一個動畫對象(這裏得到的是CABasicAnimation),然後通過-addAnimation:forkey:方法添加到layer上,這樣系統就爲我們自動添加了一個隱式動畫。

如果是view的情況呢,我們在touchBegan方法中只改變view的center屬性,來看一下:

view實現了layer協議中的-actionForLayer:forkey:方法,則actionForKey:會首先調用這個代理方法,然後就沒有往下進行了,我們來具體看一下actionForLayer:forKey這個方法的註釋:

///* If defined, called by the default implementation of the
// * -actionForKey: method. Should return an object implementing the
 //* CAAction protocol. May return 'nil' if the delegate doesn't specify
 //* a behavior for the current event. Returning the null object (i.e.
// * '[NSNull null]') explicitly forces no further search. (I.e. the
// * +defaultActionForKey: method will not be called.) */

可以得知如果返回nil則表示沒有特別指定一個動畫,系統會自動生成一個,如果返回null([NSNull null])則不會再往下進行,也就是說沒有動畫出現。

可以發現,默認返回值是null,所以不會有動畫產生,這就是改變view的屬性沒有動畫產生的原因,如果我們要使得改變view的屬性也有動畫產生,根據上面的註釋,我們可以在這個方法內返回nil。

這樣我們在重新點擊屏幕,就會發現view在移動過程中也有了動畫效果。

最後我們再來看一下CALayerDelegate中的其他方法:

///* If defined, called by the default implementation of the -display
// * method, in which case it should implement the entire display
// * process (typically by setting the `contents' property). */
- (void)displayLayer:(CALayer *)layer;

///* If defined, called by the default implementation of -drawInContext: */
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;

///* If defined, called by the default implementation of the -display method.
// * Allows the delegate to configure any layer state affecting contents prior
// * to -drawLayer:InContext: such as `contentsFormat' and `opaque'. It will not
 //* be called if the delegate implements -displayLayer. */
- (void)layerWillDraw:(CALayer *)layer
  API_AVAILABLE(macos(10.12), ios(10.0), watchos(3.0), tvos(10.0));

我們知道當我們給一個UIImageView設置圖片時,我們可以直接設置

imgView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"xx.png"]];
imgView.frame = CGRectMake(0.f, 0.f, 100.f, 100.f);
[self.view addSubview:imgView];

也可以通過layer的contents:

ImgView = [[UIImageView alloc] initWithFrame:CGRectMake(0.f, 0.f, 100.f, 100.f)];
ImgView.layer.contents = (__bridge id)[UIImage imageNamed:@"xx.png"].CGImage;
[self.view addSubview:ImgView];

我們也可以通過代理方法來給view設置圖片:

//繪製會啓動離屏渲染 增加內存消耗
- (void)drawRect:(CGRect)rect {
    UIImage *image = [UIImage imageNamed:@"xx.png"];
    [image drawInRect:rect];
    //[super drawRect:rect];
    NSLog(@"drawRect");
}
//繪製內容  CGContext方法來繪製
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    NSLog(@"%s", __func__);
    [super drawLayer:layer inContext:ctx];//該方法調用了drawRect
}
-(void)layerWillDraw:(CALayer *)layer{
    NSLog(@"%s", __func__);
//系統提供的可以讓我們在drawLayer: inContext:方法之前對layer進行配置
    [super layerWillDraw:layer];
}

- (void)displayLayer:(CALayer *)layer {
//可以在這個方法內給contents賦值
//重寫該方法後,系統不再調用drawlayer:InContext:方法
   NSLog(@"%s", __func__);
   self.layer.contents = (__bridge id)[UIImage imageNamed:@"xx.png"].CGImage;
}

---

這一小節就總結到這裏。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章