CALayer的needsDisplayForKey方法使用说明

本人录制技术视频地址:https://edu.csdn.net/lecturer/1899 欢迎观看。

今天为大家介绍的是如何在执行动画的时候,完成Core Graphics 图形的绘制工作。 主要把重心放在needsDisplayForKey 方法上面。先看看实现绘制的动画效果图。


一、理论基础

首先了解下layer自己的属性如何实现动画。

1. layer首次加载时会调用 +(BOOL)needsDisplayForKey:(NSString *)key方法来判断当前指定的属性key改变是否需要重新绘制。

2. Core Animartion中的key或者keypath等于+(BOOL)needsDisplayForKey:(NSString *)key 方法中指定的key便会自动调用setNeedsDisplay方法,这样就会触发重绘,达到我们想要的效果。


layer方法响应链有两种:

1. [layer setNeedDisplay] -> [layer displayIfNeed] -> [layer display] -> [layerDelegate displayLayer:]

2. [layer setNeedDisplay] -> [layer displayIfNeed] -> [layer display] -> [layer drawInContext:] -> [layerDelegate drawLayer: inContext:]

说明一下,如果layerDelegate实现了displayLayer:协议,之后layer就不会再调用自身的重绘代码。

这里使用第二种方式来实现圆形进度条,将代码集成到layer中,降低耦合。


二、代码实现

1. 自定义一个类CircleLayer,其继承自CALayer。

.h 文件代码如下:

@interface CircleLayer : CALayer
@end
在 .m文件中定义一个progress属性,可以通过对这个属性的监听来完成绘制操作。
@interface CircleLayer()
@property (nonatomic, assign) CGFloat progress;
@end

下面所述的代码均是.m文件中的代码 。


2. 重写父类CALayer的  needsDisplayForKey方法,也就是 "理论基础" 的第一点。
+ (BOOL)needsDisplayForKey:(NSString *)key {
    BOOL result;
    if ([key isEqualToString:@"progress"]) {
        result = YES;
    } else {
        result = [super needsDisplayForKey:key];
    }
    return result;
}
需要说明的有以下几点:

2.1 此方法只会在图层初始化的时候被调用一次。

2.2 代码中通过判断图层的属性名称来决定是否需要对对应的Core Animation动画执行UI重绘工作(本例中就是对自定义的progress属性进行处理)。

2.3 [super needsDisplayForKey:key]; 这个父类方法默认的返回值是NO。


3. 自定义动画,完成绘制工作。

首先在.h文件中定义执行动画的方法

- (void)animateCircle;
在.m文件中实现这个方法
- (void)animateCircle {
    CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"progress"];
    anim.values = [self valuesListWithAnimationDuration: 3];
    anim.duration = 3.0;
    anim.fillMode = kCAFillModeForwards;
    anim.removedOnCompletion = NO;
    anim.delegate = self;
    [self addAnimation:anim forKey:@"circle"];
}

- (NSMutableArray *)valuesListWithAnimationDuration:(CGFloat)duration {
    NSInteger numberOfFrames = duration * 60;
    NSMutableArray *values = [NSMutableArray array];
    // 注意这里的 fromValue和toValue是针对的progress的值的大小。
    CGFloat fromValue = 0.0;
    CGFloat toValue = 1.0;
    CGFloat diff = toValue - fromValue;
    for (NSInteger frame = 1; frame <= numberOfFrames; frame++) {
        CGFloat piece = (CGFloat)frame / (CGFloat)numberOfFrames;
        CGFloat currentValue = fromValue + diff * piece;
        [values addObject:@(currentValue)];
    }
    return values;
}
3.1 animationWithKeyPath 中指定的属性是progress,这里就是 "理论基础" 中说明的第二点。

因为在needsDisplayForKey方法中指定了key的值是progress,所以这里的animationWithKeyPath动画操作会在动画执行期间,不停的促发Core Graphics的重绘工作,即不停的调用 - (void)drawInContext:(CGContextRef)ctx方法进行绘制。

3.2 - (NSMutableArray *)valuesListWithAnimationDuration:(CGFloat)duration 方法完成绘制点的 "收集" 工作。由于默认情况下,Core Graphics 绘制的帧率为一秒钟60次,所以可以根据绘制时间计算出绘制帧数: numberOfFrame = duration * 60。然后在fromValue 和 toValue之间根据帧率取点,依次放入到一个数组中。

3.3 fillMode 和 removeOnCompletion 两个属性指定动画在绘制完成后,对应的动画对象不会从内存中移除掉。如果对这两个属性有什么不了解的地方,请参照:Core Animation 基本动画效果汇总


4. 绘制图形。

- (void)drawInContext:(CGContextRef)ctx {
    NSLog(@"progress: %f", self.progress);
    CGContextSetLineWidth(ctx, 5.0f);
    CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);
    CGContextAddArc(ctx, CGRectGetWidth(self.bounds) * 0.5, CGRectGetHeight(self.bounds) * 0.5, CGRectGetWidth(self.bounds) * 0.5 - 6, 0, 2 * M_PI * self.progress, 0);
    CGContextStrokePath(ctx);
}
这里就是简单的绘制一个圆形,注意到绘制的终止点是 2 * M_PI * self.progress, 因为在第三点中的动画处理中已经指定了绘制的KeyPath为progress,并且指定了动画的values数组中的每一个值是0~1之间的浮点值,所以self.progress在绘制过程中会对应的从0递增到1。这样就实现了在动画执行过程中,完成图形的绘制。


5. 在第四点中,我特意加了一句self.progress 值的打印,可以发现,在绘制过程中,self.progress的值的确是递增打印的。动画执行完毕后,self.progress的值是1.000, 但是动画执行完毕后,会一直打印,而且不会停止下来!!! 那是因为在绘制过程中,设置了fillMode为kCAFillModeForwards 和 removeOnCompletion为NO,即动画执行完毕后,动画对象依旧驻留在内存中,所以会一直打印,而且会一直调用的 -(void)drawInContext:(CGContextRef)ctx这个方法不停的进行绘制,虽然最后会不停的绘制self.progress为1的时候的那个点,但是会非常的消耗性能,导致内存直线上升!!! 所以在动画执行完毕后,必须移除掉动画对象。因此需要实现动画执行完毕后的代理方法:

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    [self removeAnimationForKey:@"circle"];
    self.progress = 1.0;
    [self setNeedsDisplay];
}
5.1    动画执行完毕后,通过removeAnimationForKey 移除掉动画对象。

5.2 设置self.progress为1.0, 准备绘制。

5.3 通过调用[self setNeedsDisplay]立刻绘制圆形。

需要注意的是:
第三点中的绘制,是通过CAKeyframeAnimation动画逐点依次绘制出来的; 

而这里的[self setNeedsDisplay]是一次性绘制出来的。

即实现思想是,先通过CAKeyframeAnimation动画从开始"慢慢"的绘制到最后。当动画执行完毕,也就是一个圆正好绘制完毕的时候,立刻移除掉动画对象,然后通过[self setNeedsDisplay]一次性绘制出来。移除动画对象的瞬间,其实绘制出来的圆会消失的,但随即调用[self setNeedsDisplay]绘制圆。由于这两步之间的时间极短,所以感觉还是完整的绘制了一个圆。


6. 主控制中调用代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 实现方式一: 直接添加操作图层处理
    self.layer = [[CircleLayer alloc] init];
    self.layer.frame = CGRectMake(50, 100, 100, 100);
    self.layer.backgroundColor = [UIColor redColor].CGColor;
    [self.view.layer addSublayer:self.layer];
    
    UITapGestureRecognizer* tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(move:)];
    [self.view addGestureRecognizer:tap];
}

- (void)move:(UIGestureRecognizer*)tap{
    [self.layer animateCircle];
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
要在iOS上使用Objective-C实现动态壁纸功能,您可以按照以下步骤进行: 1.创建一个新的Xcode项目,并选择Single View Application模板。 2.在Assets.xcassets中创建包含动态壁纸图像的图像集。 3.在AppDelegate.m文件中添加以下代码,以在应用程序启动时注册动态壁纸: ``` - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Register for dynamic wallpapers NSDictionary *wallpaperOptions = @{UIApplicationRegisteredDefaultDynamicWallpaperOptions : @{}}; [[UIApplication sharedApplication] registerForRemoteNotificationsMatchingTypes:UIRemoteNotificationTypeNone categories:nil]; [[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes: UIUserNotificationTypeNone categories:nil]]; [[UIApplication sharedApplication] registerForRemoteNotifications]; [[UIApplication sharedApplication] registerForRemoteNotificationsWithOptions:wallpaperOptions error:nil]; return YES; } ``` 4.在Info.plist文件中添加以下键/值对,以允许应用程序在后台运行: ``` <key>UIBackgroundModes</key> <array> <string>remote-notification</string> </array> ``` 5.在AppDelegate.m文件中添加以下代码,以处理接收到的动态壁纸: ``` - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { NSString *dynamicWallpaperURLString = userInfo[UIApplicationDynamicWallpaperContentIdentifierKey]; NSURL *dynamicWallpaperURL = [NSURL URLWithString:dynamicWallpaperURLString]; // Download and set the dynamic wallpaper } ``` 6.下载动态壁纸图像并将其设置为应用程序的背景。您可以使用以下代码之一: a.使用UIImageView: ``` UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.bounds]; imageView.contentMode = UIViewContentModeScaleAspectFill; imageView.clipsToBounds = YES; [imageView loadImageWithURL:dynamicWallpaperURL]; [self.view addSubview:imageView]; ``` b.使用CALayer: ``` CALayer *layer = [[CALayer alloc] init]; layer.contentsGravity = kCAGravityResizeAspectFill; layer.masksToBounds = YES; layer.contents = [NSData dataWithContentsOfURL:dynamicWallpaperURL]; [self.view.layer insertSublayer:layer atIndex:0]; ``` 7.运行应用程序并测试您的动态壁纸功能。 请注意,为了使动态壁纸功能正常工作,您需要在应用程序启动时注册动态壁纸,并在接收到远程通知时处理动态壁纸。您还需要确保应用程序在后台运行以接收通知。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋恨雪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值