前言
在前端開發中少不了動畫的元素,官方提供了我們很多不錯的屬性動畫,但有時滿足不了策劃的需求,這時就需要我們自定義動畫,自定義屬性動畫。既然要自定義動畫,就少不了要對CAAnimation 子類的了解
CAAnimation is an abstract animation class. It provides the basic support for the CAMediaTiming and CAAction protocols. To animate Core Animation layers or Scene Kit objects, create instances of the concrete subclasses CABasicAnimation, CAKeyframeAnimation, CAAnimationGroup, or CATransition.
以上官方描述 CAAnimation是一個抽象動畫類,遵行協議CAMediaTiming 和CAAction,實現的子類CABasicAnimation, CAKeyframeAnimation, CAAnimationGroup, CATransition.
由于本文只用到關鍵幀動畫,所以適當的對CAKeyframeAnimation進行下說明,如果要詳細同學可以自行學習下
CAKeyframeAnimation 比較關鍵兩個屬性 valuses 和path ,其它常用的屬性可以查看CAMediaTiming 協議
The keyframe values represent the values through which the animation must proceed. The time at which a given keyframe value is applied to the layer depends on the animation timing, which is controlled by the calculationMode, keyTimes, and timingFunctions properties. Values between keyframes are created using interpolation, unless the calculation mode is set to kCAAnimationDiscrete.
Depending on the type of the property, you may need to wrap the values in this array with an NSNumber of NSValue object. For some Core Graphics data types, you may also need to cast them to id before adding them to the array.
The values in this property are used only if the value in the path property is nil.
以上valuse屬性官方描述 關鍵是看最后一句 valuses 僅僅只是用于屬性動畫 也就是通過屬性可以拿到valuses 的值path 則不能
準備工作
自定義Animation最終實現要實現的效果圖
在編寫代碼之前我們還需要準備一張圖片
CustomLayer IMP的實現
<pre>
@interface CustomLayer : CALayer
@property CGFloat lighting;//自定義動畫屬性
@end
//CustomLayer.m
-(instancetype) initWithLayer:(id)layer{
if(self=[super initWithLayer:layer]){
if([self isKindOfClass:[CustomLayer class]]){
self.lighting=((CustomLayer*)layer).lighting;
}
}
return self;
}
// layer首次加載時會調用 needsDisplayForKey:(NSString *)key方法來判斷當前指定的屬性key改變時 是否需要重新繪制。
// 返回YES便會自動調用setNeedsDisplay方法,觸發重繪
+(BOOL)needsDisplayForKey:(NSString *)key{
if([key isEqualToString:@"lighting"]){
return YES;
}
return [super needsDisplayForKey:key];
}
CustomImageView.h
@interface CustomImageView : UIView
@property CGFloat cred,cgreed,cblue;
@property (nonatomic,strong) UIImage *image;
@property CGContextRef currentContext;
@property (nonatomic,strong) UIColor *color;
-(void)setAnimation;
@end
CustomImageView.m
import "CustomImageView.h"
import "CustomLayer.h"
@implementation CustomImageView
//重新下layerClass 將CustomImageView CALayer --> 換成CustomLayer
- (Class)layerClass {
return [CustomLayer class];
}
- (instancetype)init {
if (self = [super init]) {
self.layer.opaque = NO;
[self setColor:[UIColor whiteColor]];
}
return self;
} - (UIImage *)image {
if (!_image) {
_image = [UIImage imageNamed:@"bulb.png"];
}
return _image;
}
//設置燈泡顏色 - (void)setColor:(UIColor *)color {
_color = color;
CGColorRef cgColor = color.CGColor;
const CGFloat *colors = CGColorGetComponents(cgColor);
self.cred = *colors * 255.0;
self.cgreed = *(colors + 1) * 255.0;
self.cblue = *(colors + 2) * 255.0;
//基礎動畫
//[self setAnimationFrom:0.0 To:255];
}
//BasicAnimation 實現 - (void)setAnimationFrom:(CGFloat)begin To:(CGFloat)end {
CABasicAnimation *theAnimation = [CABasicAnimation animation];
theAnimation.duration = 1.0;
theAnimation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
theAnimation.fromValue = @(begin);
theAnimation.toValue = @(end);
theAnimation.removedOnCompletion=YES;
[self.layer addAnimation:theAnimation forKey:@"lighting"];
}
// 關鍵幀動畫
-(void)setAnimation{
CAKeyframeAnimation *thenAnimation=[CAKeyframeAnimation animation];
thenAnimation.duration=1;
thenAnimation.values=@[@(0),@(255),@(0),@(255),@(0),@(255)];
thenAnimation.repeatCount=MAXFLOAT;
[self.layer addAnimation:thenAnimation forKey:@"lighting"];
}
// 細心碼猿也許會發現,這個明顯為空,為毛還要寫在這里。待會,后面進行說明 - (void)drawRect:(CGRect)rect {
}
//圖片繪制
-
(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
CGFloat lighting = ((CustomLayer *)layer).lighting;
CGFloat red = 255 - self.cred;
CGFloat greed = 255 - self.cgreed;
CGFloat blue = 255 - self.cblue;
CGFloat curRed = self.cred + red * (lighting / 255.0f);
CGFloat curGreed = self.cgreed + greed * (lighting / 255.0f);
CGFloat curBlue = self.cblue + blue * (lighting / 255.0f);
//創建一個基于位圖的上下文
UIGraphicsBeginImageContextWithOptions(self.image.size, YES, 0);
//獲取當前位圖上下文
self.currentContext = UIGraphicsGetCurrentContext();
CGRect rect = CGContextGetClipBoundingBox(self.currentContext);
UIColor *color = [UIColor colorWithRed:curRed / 255.0f
green:curGreed / 255.0f
blue:curBlue / 255.0f
alpha:1.0];
[color set];
UIBezierPath *path = [UIBezierPath bezierPathWithRect:rect];
// 顏色填充
[path fill];
CGContextDrawImage(self.currentContext, rect, _image.CGImage);
//從當前位圖上下文,獲取繪制的圖片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
//結束圖片繪制
UIGraphicsEndImageContext();
const CGFloat maskingColors[6] = { 255.0, 255.0, 255.0, 255.0, 255.0, 255.0 };
//創建一個顏色遮的圖片
CGImageRef finalImage = CGImageCreateWithMaskingColors( image.CGImage, maskingColors );CGContextDrawImage(ctx, self.bounds, finalImage);
//回收CGImage
CGImageRelease(finalImage);
}
</pre>
CustomImageView.m IMP文件中,重寫了drawRect 方法,但又什么代碼都沒有,能否去掉??
先看下正常情況下UIView 自繪流程圖
- 通過流程UIView 只要重寫drawRect: 才會自動執行layer 重繪工作,UIView 可以看作操作的畫筆,CALayer是UIView的畫布
- 由于UIView 在創建CALayer 的時候會隱式 將本身賦值給layer.Delegate ,所以在重繪的時候,會優先判斷是否實現了代理方法,實現則利用代理方法繪制,而在代理方法 drawlayer: inCentext: 中調用 [super drawlayer: inCentext:] 后,會再次 掉用 drawRect: 否則, layerDelegate 繪制的結果,便是最終顯示的結果
- setNeedDisplay: 不管調用用多少次,只會在下一幀同步到顯示屏
- 假如不重寫drawRect: 可以手動調用[self.layer setNeedDisplay] layer 邊可以重繪,達到相同的目的
- 當實現drawlayer: 方法將不會調用 drawlayer:inCentext: 在該方法內不能調用父類[super drawlayer:]方法 否則將crash
<code>UIGraphicsBeginImageContextWithOptions(self.image.size, YES, 0);?</code>
創建一個基于位圖的上下文,并將其設置為當前上下文(CGContextRef)
size:參數size為新創建的位圖上下文的大小。它同時是由UIGraphicsGetImageFromCurrentImageContext函數返回的圖形大小。
opaque:不透明,如果圖形完全不用透明,設置為YES以優化位圖的存儲。 這里需要顏色遮罩設置為YES 默認情況 NO;
scale:縮放因子
<pre>const CGFloat maskingColors[6] = { 255.0, 255.0, 255.0, 255.0, 255.0, 255.0 };
//創建一個顏色遮的圖片
CGImageRef finalImage = CGImageCreateWithMaskingColors( image.CGImage, maskingColors );
將白色作為顏色遮罩,最終將圖片繪制layer</pre>
看到 (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx 這個方法是否覺得疑問 ??
這里已經有CGContextRef參數傳入 能否使用 ctx 繪制位圖。 答案是可以
<pre>
CGFloat lighting = ((CustomLayer *)layer).lighting;
CGFloat red = 255 - self.cred;
CGFloat greed = 255 - self.cgreed;
CGFloat blue = 255 - self.cblue;
CGFloat curRed = self.cred + red * (lighting / 255.0f);
CGFloat curGreed = self.cgreed + greed * (lighting / 255.0f);
CGFloat curBlue = self.cblue + blue * (lighting / 255.0f);
UIGraphicsBeginImageContextWithOptions(self.image.size, YES, 1.0f);
UIGraphicsPushContext(ctx);
// self.currentContext = UIGraphicsGetCurrentContext();
CGRect rect = CGContextGetClipBoundingBox(ctx);
UIColor *color = [UIColor colorWithRed:curRed / 255.0f
green:curGreed / 255.0f
blue:curBlue / 255.0f
alpha:1.0];
[color set];
UIBezierPath *path = [UIBezierPath bezierPathWithRect:rect];
[path fill];
CGContextTranslateCTM(ctx, 0, self.bounds.size.height);
CGContextScaleCTM(ctx, 1.0, -1.0);
CGContextDrawImage(ctx, rect, _image.CGImage);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsPopContext();
UIGraphicsEndImageContext();
const CGFloat maskingColors[6] = {255.0, 255.0, 255.0, 255.0, 248.0, 255.0};
CGImageRef finalImage =
CGImageCreateWithMaskingColors(image.CGImage, maskingColors);
CGContextDrawImage(ctx, rect, finalImage);
CGImageRelease(finalImage);
</pre>
利用UIGraphicsPushContext(ctx); 切換ctx 為當前上下文
UIGraphicsPopContext(ctx); 切換為原來的上下文
得到圖片是倒立的 設置下
<pre>
CGContextTranslateCTM(ctx, 0, self.bounds.size.height);
CGContextScaleCTM(ctx, 1.0, -1.0);
</pre>
至于原因可以去理解UIKit坐標系與Quartz(Core Graphics坐標系)
將該方法中內容換成以上代碼,可以得到我們想要的圖片結果,但CGImageCreateWithMaskingColors
顏色遮罩將沒有效果,why??
很簡單,我們之前說了,顏色遮罩圖片需要不透明,而傳入CGCentextRef 繪制圖片默認是透明的 所以CGImageCreateWithMaskingColors便會不起效果
ViewController中使用
<pre>
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor yellowColor];
_imageView = [CustomImageView new];
UIButton *greedBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[greedBtn addTarget:self action:@selector(onClick:)
forControlEvents:UIControlEventTouchUpInside];
greedBtn.backgroundColor = [UIColor greenColor];
greedBtn.tag = BTN_TAG;
UIButton *blueBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[blueBtn addTarget:self
action:@selector(onClick:)
forControlEvents:UIControlEventTouchUpInside];
blueBtn.backgroundColor = [UIColor blueColor];
[self.view addSubview:_imageView];
[self.view addSubview:greedBtn];
[self.view addSubview:blueBtn];
//---------------AutoLayout 布局可以跳過 ---------------
_imageView.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *constraintW =[NSLayoutConstraint constraintWithItem:_imageView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:.25 * self.view.frame.size.width];
NSLayoutConstraint *constraintH =
[NSLayoutConstraint constraintWithItem:_imageView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:.20 * self.view.frame.size.height];
NSLayoutConstraint *constraintX =
[NSLayoutConstraint constraintWithItem:_imageView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0];
NSLayoutConstraint *constraintY =
[NSLayoutConstraint constraintWithItem:_imageView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0];
constraintW.active = YES;//8.0后加入
constraintH.active = YES;
constraintX.active = YES;
constraintY.active = YES;
NSDictionary *views =
NSDictionaryOfVariableBindings(greedBtn, blueBtn, _imageView);
greedBtn.translatesAutoresizingMaskIntoConstraints = NO;
blueBtn.translatesAutoresizingMaskIntoConstraints = NO;
[self.view
addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:
@"V:[_imageView]-1-[greedBtn(30)]"
options:0
metrics:nil
views:views]];
[self.view
addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:@"V:[blueBtn(greedBtn)]"
options:0
metrics:nil
views:views]];
[self.view
addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:
@"H:[greedBtn(30)]-20-[blueBtn(greedBtn)]"
options:NSLayoutFormatAlignAllTop
metrics:nil
views:views]];
constraintX = [NSLayoutConstraint constraintWithItem:greedBtn
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1
constant:-25];
constraintX.active = YES;
}
</pre>
動畫執行方法
<pre>- (void)onClick:(UIButton *)sender {
if (sender.tag == BTN_TAG) {
[_imageView setColor:[UIColor greenColor]];
[_imageView setAnimation];
} else {
[_imageView setColor:[UIColor blueColor]];
[_imageView setAnimation];
}
}</pre>
結尾
以上觀點如有錯誤之處歡迎留言,博文是在工作間寫的,細節方面可能不到位,歡迎指正