《幸运大转盘》有一句代码是这样的:
self.rotateView.transform = CGAffineTransformMakeRotation(-angle);
它出现在延迟派遣消息 dispatch_after 里面,然而你真的看懂它了吗?
本文将揭秘这句代码的真相!红字黄底标出!
#import "ViewController.h"
#import "ZHYView.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 让控制器的view以拉伸的方式设置成图片
self.view.layer.contents = (__bridge id)([UIImage imageNamed:@"LuckyBackground"].CGImage);
// 创建转盘的对象
ZHYView *rotateView= [ZHYView rotateImage];
// 设置转盘在屏幕上居中显示
rotateView.center = self.view.center;
// 把装盘添加到控制器当中
[self.view addSubview:rotateView];
// 程序一运行就让锯齿图片旋转
[rotateView startRotate];
}
// 设置状态栏样式为白色字体,更好看一些
-(UIStatusBarStyle)preferredStatusBarStyle{
return UIStatusBarStyleLightContent;
}
@end
#import
@interface CZView : UIView
+ (instancetype)rotateView;
- (void)startRotate;
@end
#import "ZHYView.h"
#define kButtonCount 12
@interface ZHYView () <UIAlertViewDelegate>
@property (weak, nonatomic) IBOutlet UIImageView *rotateView;
@property (nonatomic,weak) UIButton *lastButton;
@property (nonatomic,strong) CADisplayLink *link;
@end
@implementation ZHYView
// 开始旋转
-(void)startRotate{
// CADisplayLink刷帧,默认每秒刷新60次,该定时器创建之后,默认是不会执行的,需要把它加载到主消息循环中才会被执行
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(rotate)];
// 将CADisplayLink定时器加载到主循环中
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
// 给属性link赋值,便于后面对CADisplayLink定时器对象link进行设置
self.link = link;
}
// 旋转
-(void)rotate{
CGFloat round = 5;
CGFloat angle = 2 * M_PI / 60 / round;
// 设置CADisplayLink定时器对象link,让rotateView每5秒转一圈
self.rotateView.transform = CGAffineTransformRotate(self.rotateView.transform, angle);
}
// 开始选号的点击事件
- (IBAction)pickNumber:(UIButton *)sender {
// 如果没有选中任何一个button的时候不旋转
if (!self.lastButton) {
return;
}
// 让CADisplayLink定时器对象link暂停下来,避免UIAlertView的代理方法(点击弹出提示框的确定按钮后执行的方法)结束后选中的button跳屏
self.link.paused = YES;
// 关掉大转盘的用户交互
self.userInteractionEnabled = NO;
// 创建一个基本动画,让转盘快速旋转以备选号
CABasicAnimation *ani = [[CABasicAnimation alloc] init];
// 设置关键路径
ani.keyPath = @"transform.rotation";
// 记录每个button对应的初始角度
CGFloat angle = self.lastButton.tag * 2 * M_PI / 12;
// 设置属性toValue为了让选中的button快速旋转后指向转盘的正上方
ani.toValue = @(2 * M_PI * 5 - angle);
// 设置快速旋转动画的持续时间
ani.duration = 2;
// 修改动画的默认模式,不让动画在结束后复位
ani.fillMode = kCAFillModeForwards;
// 该BOOL属性不设置的话,动画默认修改不成功;
ani.removedOnCompletion = NO;
// 将动画添加到layer上
[self.rotateView.layer addAnimation:ani forKey:@"key"];
// 延迟 ani.duration 时间后派遣{}中的消息
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(ani.duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
/*
这里需要详细解释一下:
假设:
原始角度:狮子座在最上面: 0
当你点击大转盘的星座按钮时狮子座的角度:A
当你接着点击选号按钮时狮子座的角度:B
动画结束后狮子座的位置被设定了在最上面:0
如果下面这句代码不设置,就会发生跳屏现象,即选中的按钮会跳到角度 B
*/
self.rotateView.transform = CGAffineTransformMakeRotation(-angle);
// 创建提示视图,显示信息
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"幸运大转盘,赚的就是你" message:@"13579" delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
// 显示提示视图
[alert show];
// 开启大转盘的用户交互
self.userInteractionEnabled = YES;
});
}
// UIAlertView的代理方法,在点击cancelButtonTitle:@"确定"的时候会执行
-(void)alertView:(UIAlertView *)alertViewclickedButtonAtIndex:(NSInteger)buttonIndex{
// 移除指定AnimationForKey:@"key"的动画
[self.rotateView.layer removeAnimationForKey:@"key"];
/*
其实在大转盘选号结束后这么设置可以让大转盘恢复初始状态[随便玩玩]
self.lastButton.selected = NO;
self.lastButton = nil;
*/
// 让CADisplayLink定时器对象link重新计时
self.link.paused = NO;
}
// button的点击事件
-(void)clickButton:(UIButton *)button{
// 取消上一个button的选中状态
self.lastButton.selected = NO;
// 设置点击到的button的选中状态
button.selected = YES;
// 点击事件的最后将当前button赋值给lastbutton
self.lastButton = button;
}
// 布局子控件
-(void)layoutSubviews{
// 遍历锯齿图片里面的子控件(button)设置frame
for (int i = 0; i < self.rotateView.subviews.count; i++) {
UIButton *button = self.rotateView.subviews[i];
CGFloat buttonCenterX = self.bounds.size.width * 0.5;
CGFloat buttonCenterY = self.bounds.size.height * 0.5;
button.frame = CGRectMake(0, 0, 68, 143);
button.center = CGPointMake(buttonCenterX,buttonCenterY);
// 设置每个button的初始角度
CGFloat angle = i * 2 * M_PI / 12;
// 将每个button散开
button.transform = CGAffineTransformMakeRotation(angle);
// 设置button的内边距
[button setContentEdgeInsets:UIEdgeInsetsMake(-44, 0, 0, 0)];
}
}
//裁剪图片
-(UIImage *)clipImage:(UIImage *)image withIndex:(int)index{
//设置将要从image获取的裁剪到的图片的frame,注意宽高需要乘以设备的缩放因子
CGFloat w = image.size.width / 12 * [UIScreen mainScreen].scale;
CGFloat h = image.size.height * [UIScreen mainScreen].scale;
CGFloat x = index * w;
CGFloat y = 0;
// 获取裁剪image中rect部分裁剪的图片
CGImageRef imageRef = CGImageCreateWithImageInRect(image.CGImage, CGRectMake(x, y, w, h));
// 通过CGImageRef的image转成UIimage,scale是缩放因子,需要手动调试出一个合适的值,orientation是一个方向枚举,0表示默认方向
return [UIImage imageWithCGImage:imageRef scale:2 orientation:0];
}
//从nib加载
-(void)awakeFromNib{
// 创建12个button
for (int i = 0; i < kButtonCount; i++) {
// 创建button
UIButton * button = [[UIButton alloc] init];
// 修改button的锚点为中间最下
button.layer.anchorPoint = CGPointMake(0.5, 1);
// 绑定button的tag属性,便于确定每个button的初始位置角度
button.tag = i;
// 加载button默认和选中两个状态的原图
UIImage *image = [UIImage imageNamed:@"LuckyAstrology"];
UIImage *imagePress = [UIImage imageNamed:@"LuckyAstrologyPressed"];
// 获取通过自定义的方法将两个原图按顺序裁剪的图片
image = [self clipImage:image withIndex:i];
imagePress = [self clipImage:imagePress withIndex:i];
// 给button的默认和选中状态设置裁剪好的图片
[button setImage:image forState:UIControlStateNormal];
[button setImage:imagePress forState:UIControlStateSelected];
// 设置button的背景图片
[button setBackgroundImage:[UIImage imageNamed:@"LuckyRototeSelected"] forState:UIControlStateSelected];
// 设置button的点击事件clickButton:
[button addTarget:self action:@selector(clickButton:) forControlEvents:UIControlEventTouchUpInside];
// 将button添加到rotateView中
[self.rotateView addSubview:button];
}
}
// 一个返回值为大转盘对象的类方法,便于外部访问
+(instancetype)rotateImage{
return [[NSBundle mainBundle] loadNibNamed:@"ZHYView" owner:nil options:nil][0];
}
@end
运行结果赏析: