转盘(旋转)
- 自定义转盘上的button - WheelButton
- 事件处理,重写hitTest方法来寻找最合适的view
// 寻找最合适的view
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 获取真实button的宽度和高度
CGFloat btnW = self.bounds.size.width;
CGFloat btnH = self.bounds.size.height;
// 设置要处理事件的区域
CGFloat x = 0;
CGFloat y = btnH / 2;
CGFloat w = btnW;
CGFloat h = y;
CGRect rect = CGRectMake(x, y, w, h);
if (CGRectContainsPoint(rect, point)) {
return nil;
}else{
return [super hitTest:point withEvent:event];
}
}
- 自定义按钮可以控制按钮上图片显示的位置和尺寸
// 设置UIImageView的尺寸
// contentRect:按钮的尺寸
- (CGRect)imageRectForContentRect:(CGRect)contentRect
{
// 计算UIImageView控件尺寸
CGFloat imageW = 40;
CGFloat imageH = 46;
CGFloat imageX = (contentRect.size.width - imageW) * 0.5;
CGFloat imageY = 20;
return CGRectMake(imageX, imageY, imageW, imageH);
}
- 自定义按钮取消点击时的高亮状态,需重写setHighlighted方法
// 取消高亮状态
- (void)setHighlighted:(BOOL)highlighted
{
}
- 自定义转盘的view - WheelView
- 在.h文件中声明初始化方法和start以及pause方法
+ (instancetype)wheelView;
- (void)start
- (void)pause
- 在wheelView初始化方法中加载xib文件
+ (instancetype)wheelView
{
return [[NSBundle mainBundle] loadNibNamed:@"WheelView" owner:nil options:nil][0];
}
- initWithCoder方法只是在加载xib的时候会调用,但是并不会将xib中的控件和代码声明进行连线
- 可以在awakeFromNib方法中进行添加按钮的操作
- (void)awakeFromNib
{
// 由于imageView的特殊性,默认是不能与用户进行交互的
_centerView.userInteractionEnabled = YES;
CGFloat btnW = 68;
CGFloat btnH = 143;
CGFloat wh = self.bounds.size.width;
// 加载大图片(默认)
UIImage *bigImage = [UIImage imageNamed:@"LuckyAstrology"];
// 加载大图片(选中)
UIImage *selBigImage = [UIImage imageNamed:@"LuckyAstrologyPressed"];
// 获取当前使用的图片像素和点的比例
CGFloat scale = [UIScreen mainScreen].scale;
CGFloat imageW = bigImage.size.width / 12 * scale;
CGFloat imageH = bigImage.size.height * scale;
// CGImageRef image:需要裁减的图片
// rect:裁减区域
// 裁减区域是以像素为基准
// CGImageCreateWithImageInRect(CGImageRef image, CGRect rect)
// 添加按钮
for (int i = 0; i < 12; i++) {
WheelButton *btn = [WheelButton buttonWithType:UIButtonTypeCustom];
// 设置按钮的位置
btn.layer.anchorPoint = CGPointMake(0.5, 1);
btn.bounds = CGRectMake(0, 0, btnW, btnH);
btn.layer.position = CGPointMake(wh * 0.5, wh * 0.5);
// 按钮的旋转角度
CGFloat radion = (30 * i) / 180.0 * M_PI;
btn.transform = CGAffineTransformMakeRotation(radion);
[_centerView addSubview:btn];
// 加载按钮的图片
// 计算裁减区域
CGRect clipR = CGRectMake(i * imageW, 0, imageW, imageH);
// 裁减图片
CGImageRef imgR = CGImageCreateWithImageInRect(bigImage.CGImage, clipR);
UIImage *image = [UIImage imageWithCGImage:imgR];
// 设置按钮的图片
[btn setImage:image forState:UIControlStateNormal];
// 设置选中状态下图片
imgR = CGImageCreateWithImageInRect(selBigImage.CGImage, clipR);
image = [UIImage imageWithCGImage:imgR];
// 设置按钮的图片
[btn setImage:image forState:UIControlStateSelected];
// 设置选中背景图片
[btn setBackgroundImage:[UIImage imageNamed:@"LuckyRototeSelected"] forState:UIControlStateSelected];
// 监听按钮的点击
[btn addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside];
// 默认选中第一个
if (i == 0) {
[self btnClick:btn];
}
// btn.backgroundColor = [UIColor redColor];
}
}
- 默认记录上一次被点击的按钮,当点击下一个按钮时清空上一个按钮,并将当前按钮记录
- (void)btnClick:(UIButton *)btn
{
_selBtn.selected = NO;
btn.selected = YES;
_selBtn = btn;
}
- 当点击开始选号按钮时,通过transform获取选中按钮的旋转角度,并进行逆向旋转,并在动画结束以后重新开启定时器动画
#pragma mark - 点击开始选号的时候
- (IBAction)startPicker:(id)sender {
// 不需要定时器旋转
self.link.paused = YES;
// 中间的转盘快速的旋转,并且不需要与用户交互
CABasicAnimation *anim = [CABasicAnimation animation];
anim.keyPath = @"transform.rotation";
anim.toValue = @(M_PI * 2 * 3);
anim.duration = 0.5;
anim.delegate = self;
[_centerView.layer addAnimation:anim forKey:nil];
// 点击哪个星座,就把当前星座指向中心点上面
// M_PI 3.14
// 根据选中的按钮获取旋转的度数,
// 通过transform获取角度
CGFloat angle = atan2(_selBtn.transform.b, _selBtn.transform.a);
// 旋转转盘
_centerView.transform = CGAffineTransformMakeRotation(-angle);
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.link.paused = NO;
});
}
折叠图片的动画
原理分析:通过两个UIImageView分别显示图片的上半部分(topView)和下半部分(bottonView),然后将其放入同一个UIView(dragView),旋转的时候只旋转上部分的控件,为了让一张完整的图片通过两个控件显示,可以通过layer图层控制图片显示的内容
-
如何快速的把两个控件拼接成一个完整的图片
- 可以通过contentsRect设置图片显示的尺寸,取值0~1
_topView.layer.contentsRect = CGRectMake(0, 0, 1, 0.5);
_topView.layer.anchorPoint = CGPointMake(0.5, 1);
_bottomView.layer.contentsRect = CGRectMake(0, 0.5, 1, 0.5);
_bottomView.layer.anchorPoint = CGPointMake(0.5, 0);
- 分别给dragView添加拖拽手势(UIPanGestureRecognizer)和bottonView添加渐变图层(CAGradientLayer)
// 添加手势
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
[_dragView addGestureRecognizer:pan];
// 渐变图层
CAGradientLayer *gradientL = [CAGradientLayer layer];
// 注意图层需要设置尺寸
gradientL.frame = _bottomView.bounds;
gradientL.opacity = 0;
gradientL.colors = @[(id)[UIColor clearColor].CGColor,(id)[UIColor blackColor].CGColor];
_gradientL = gradientL;
// 设置渐变颜色
// gradientL.colors = @[(id)[UIColor redColor].CGColor,(id)[UIColor greenColor].CGColor,(id)[UIColor yellowColor].CGColor];
// 设置渐变定位点
// gradientL.locations = @[@0.1,@0.4,@0.5];
// 设置渐变开始点,取值0~1
// gradientL.startPoint = CGPointMake(0, 1);
[_bottomView.layer addSublayer:gradientL];
- 在拖拽手势的pan方法中给topView的layer图层添加旋转动画以及设置渐变图层的阴影效果,并在手指抬起的时候添加弹簧效果的动画
// 拖动的时候旋转上部分内容,200 M_PI
- (void)pan:(UIPanGestureRecognizer *)pan
{
// 获取偏移量
CGPoint transP = [pan translationInView:_dragView];
// 旋转角度,往下逆时针旋转
CGFloat angle = -transP.y / 200.0 * M_PI;
CATransform3D transfrom = CATransform3DIdentity;
// 增加旋转的立体感,近大远小,d:距离图层的距离
transfrom.m34 = -1 / 500.0;
transfrom = CATransform3DRotate(transfrom, angle, 1, 0, 0);
_topView.layer.transform = transfrom;
// 设置阴影效果
_gradientL.opacity = transP.y * 1 / 200.0;
if (pan.state == UIGestureRecognizerStateEnded) { // 反弹
// 弹簧效果的动画
// SpringWithDamping:弹性系数,越小,弹簧效果越明显
[UIView animateWithDuration:0.6 delay:0 usingSpringWithDamping:0.2 initialSpringVelocity:10 options:UIViewAnimationOptionCurveEaseInOut animations:^{
_topView.layer.transform = CATransform3DIdentity;
} completion:^(BOOL finished) {
}];
}
}
音量震动条的动画
核心:复制图层CAReplicatorLayer的使用
复制图层:是指可以把图层里面的所有子层复制
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// CAReplicatorLayer复制图层,可以把图层里面所有子层复制
// 创建复制图层
CAReplicatorLayer *repL = [CAReplicatorLayer layer];
repL.frame = _lightView.bounds;
[_lightView.layer addSublayer:repL];
CALayer *layer = [CALayer layer];
layer.anchorPoint = CGPointMake(0.5, 1);
layer.position = CGPointMake(15, _lightView.bounds.size.height);
layer.bounds = CGRectMake(0, 0, 30, 150);
layer.backgroundColor = [UIColor whiteColor].CGColor;
[repL addSublayer:layer];
CABasicAnimation *anim = [CABasicAnimation animation];
anim.keyPath = @"transform.scale.y";
anim.toValue = @0.1;
anim.duration = 0.5;
anim.repeatCount = MAXFLOAT;
// 设置动画反转
anim.autoreverses = YES;
[layer addAnimation:anim forKey:nil];
// 复制层中子层总数
// instanceCount:表示复制层里面有多少个子层,包括原始层
repL.instanceCount = 3;
// 设置复制子层偏移量,不包括原始层,相对于原始层x偏移
repL.instanceTransform = CATransform3DMakeTranslation(45, 0, 0);
// 设置复制层动画延迟时间
repL.instanceDelay = 0.1;
// 如果设置了原始层背景色,就不需要设置这个属性
repL.instanceColor = [UIColor greenColor].CGColor;
repL.instanceGreenOffset = -0.3;
}
活动指示器动画
- 同样是利用复制层制作类似进度条的动画
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
CAReplicatorLayer *repL = [CAReplicatorLayer layer];
repL.frame = _redView.bounds;
[_redView.layer addSublayer:repL];
CALayer *layer = [CALayer layer];
layer.transform = CATransform3DMakeScale(0, 0, 0);
layer.position = CGPointMake(_redView.bounds.size.width / 2, 20);
layer.bounds = CGRectMake(0, 0, 10, 10);
layer.backgroundColor = [UIColor greenColor].CGColor;
[repL addSublayer:layer];
// 设置缩放动画
CABasicAnimation *anim = [CABasicAnimation animation];
anim.keyPath = @"transform.scale";
anim.fromValue = @1;
anim.toValue = @0;
anim.repeatCount = MAXFLOAT;
CGFloat duration = 1;
anim.duration = duration;
[layer addAnimation:anim forKey:nil];
int count = 20;
CGFloat angle = M_PI * 2 / count;
// 设置子层总数
repL.instanceCount = count;
repL.instanceTransform = CATransform3DMakeRotation(angle, 0, 0, 1);
repL.instanceDelay = duration / count;
}
单条路径粒子效果动画
原理分析:在touchesBegan方法中创建UIBezierPath并设置起点,在touchesMoved方法添加线到某点,在awakeFromNib方法中初始化复制层和单个粒子的图层,当用户点击开始动画按钮,给单个粒子设置帧动画,并设置粒子的数量以及延迟动画的时间
当手指触摸开始的时候
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// 重绘
[self reDraw];
// 获取touch对象
UITouch *touch = [touches anyObject];
// 获取当前触摸点
CGPoint curP = [touch locationInView:self];
// 创建一个路径
UIBezierPath *path = [UIBezierPath bezierPath];
// 设置起点
[path moveToPoint:curP];
_path = path;
}
- 在手指移动过程中时刻添加连线并进行重绘
static int _instansCount = 0;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
// 获取touch对象
UITouch *touch = [touches anyObject];
// 获取当前触摸点
CGPoint curP = [touch locationInView:self];
// 添加线到某个点
[_path addLineToPoint:curP];
// 重绘
[self setNeedsDisplay];
_instansCount ++;
}
- (void)drawRect:(CGRect)rect {
[_path stroke];
}
- 在awakeFromNib方法中初始化图层
- (void)awakeFromNib
{
// 创建复制层
CAReplicatorLayer *repL = [CAReplicatorLayer layer];
repL.frame = self.bounds;
[self.layer addSublayer:repL];
// 创建图层
CALayer *layer = [CALayer layer];
CGFloat wh = 10;
layer.frame = CGRectMake(0, -1000, wh, wh);
layer.cornerRadius = wh / 2;
layer.backgroundColor = [UIColor blueColor].CGColor;
[repL addSublayer:layer];
_dotLayer = layer;
_repL = repL;
}
#pragma mark - 开始动画
- (void)startAnim
{
_dotLayer.hidden = NO;
// 创建帧动画
CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
anim.keyPath = @"position";
anim.path = _path.CGPath;
anim.duration = 4;
anim.repeatCount = MAXFLOAT;
[_dotLayer addAnimation:anim forKey:nil];
// 复制子层
_repL.instanceCount = _instansCount;
_repL.instanceDelay = 0.1;
}
- (void)reDraw
{
_path = nil;
[self setNeedsDisplay];
_dotLayer.hidden = YES;
}
多条路径粒子效果动画
原理分析:通过懒加载dotLayer和path,可以保证在程序运行过程中只有一个dotLayer和path对象
懒加载dotLayer和path,需重写其get方法
#pragma mark - 懒加载点层
- (CALayer *)dotLayer
{
if (_dotLayer == nil) {
// 创建图层
CALayer *layer = [CALayer layer];
CGFloat wh = 10;
layer.frame = CGRectMake(0, -1000, wh, wh);
layer.cornerRadius = wh / 2;
layer.backgroundColor = [UIColor blueColor].CGColor;
[_repL addSublayer:layer];
_dotLayer = layer;
}
return _dotLayer;
}
- (UIBezierPath *)path
{
if (_path == nil) {
_path = [UIBezierPath bezierPath];
}
return _path;
}
注意:如果复制的子层有动画,需要先添加动画再复制,否则子层动画可能添加不成功
复制子层:self.repL.instanceCount = self.instanceCount;
延迟图层动画:self.repL.instanceDelay = 0.2;
倒影效果
- 利用复制层,将图片绕X轴旋转后修改图层颜色通道的值
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
CAReplicatorLayer *layer = (CAReplicatorLayer *)_repView.layer;
layer.instanceCount = 2;
CATransform3D transform = CATransform3DMakeTranslation(0, _repView.bounds.size.height, 0);
// 绕着X轴旋转
transform = CATransform3DRotate(transform, M_PI, 1, 0, 0);
// 往下面平移控件的高度
layer.instanceTransform = transform;
layer.instanceAlphaOffset = -0.1;
layer.instanceBlueOffset = -0.1;
layer.instanceGreenOffset = -0.1;
layer.instanceRedOffset = -0.1;
}
QQ粘性效果
注意:touchesBegan方法会和按钮的监听事件冲突,所以在有按钮的监听事件以后,只能使用手势事件代替touchesBegan方法
使用self.transform修改按钮的形变,并不会修改按钮的中心点,所以需要直接修改self.center
每一次相对于上一次的形变,都需要进行复位操作
绘制不规则的矩形,不能通过绘图,因为绘图只能在当前控件上画,超出部分将不会显示,而且只有当两个圆产生距离的时候才需要进行绘制
描述两圆之间的矩形路径需要特定的算法
-
手指抬起的时候,将大圆进行还原,根据圆心的距离判断是否需要移除不规则矩形
- 当大小圆圆心的距离大于设定的最大圆心距离时,需要展示一张爆炸的gif图片,并将大圆从父控件中移除
- 当圆心距离不大于规定的距离时,需要移除不规则矩形,并将大圆还原到默认的位置