251 lines
8.6 KiB
Objective-C
251 lines
8.6 KiB
Objective-C
//
|
|
// QXFloatAnimationHelper.m
|
|
// QXLive
|
|
//
|
|
// Created by 启星 on 2025/11/22.
|
|
//
|
|
|
|
#import "QXFloatAnimationHelper.h"
|
|
#import <objc/runtime.h>
|
|
|
|
// 关联对象键
|
|
static void * const kFloatAnimationKey = "kFloatAnimationKey";
|
|
|
|
@interface QXFloatAnimationHelper ()
|
|
@property (nonatomic, strong) NSMutableSet<UIView *> *animatedViews;
|
|
@end
|
|
|
|
@implementation QXFloatAnimationHelper
|
|
|
|
+ (instancetype)sharedController {
|
|
static QXFloatAnimationHelper *instance = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
instance = [[QXFloatAnimationHelper alloc] init];
|
|
});
|
|
return instance;
|
|
}
|
|
|
|
- (instancetype)init {
|
|
self = [super init];
|
|
if (self) {
|
|
_animatedViews = [NSMutableSet set];
|
|
_style = FloatAnimationStyleSmooth;
|
|
_amplitude = 1.0;
|
|
_duration = 3.0;
|
|
_autoStart = YES;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)addFloatAnimationToView:(UIView *)view {
|
|
[self addFloatAnimationToView:view withStyle:self.style];
|
|
}
|
|
|
|
- (void)addFloatAnimationToView:(UIView *)view withStyle:(FloatAnimationStyle)style {
|
|
if (!view || [self.animatedViews containsObject:view]) {
|
|
return;
|
|
}
|
|
|
|
[self.animatedViews addObject:view];
|
|
|
|
// 保存原始位置
|
|
CGPoint originalCenter = view.center;
|
|
objc_setAssociatedObject(view, kFloatAnimationKey, [NSValue valueWithCGPoint:originalCenter], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
|
|
// 根据样式创建不同的动画
|
|
switch (style) {
|
|
case FloatAnimationStyleSmooth:
|
|
[self addSmoothFloatAnimationToView:view];
|
|
break;
|
|
case FloatAnimationStyleBounce:
|
|
[self addBounceFloatAnimationToView:view];
|
|
break;
|
|
case FloatAnimationStyleWave:
|
|
[self addWaveFloatAnimationToView:view];
|
|
break;
|
|
case FloatAnimationStyleBreath:
|
|
[self addBreathFloatAnimationToView:view];
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (void)addSmoothFloatAnimationToView:(UIView *)view {
|
|
CGPoint originalCenter = [objc_getAssociatedObject(view, kFloatAnimationKey) CGPointValue];
|
|
|
|
// 创建关键帧动画
|
|
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
|
|
|
|
NSMutableArray *pathValues = [NSMutableArray array];
|
|
NSInteger frames = 60;
|
|
|
|
for (int i = 0; i <= frames; i++) {
|
|
CGFloat progress = (CGFloat)i / frames;
|
|
CGFloat radians = progress * M_PI * 2;
|
|
CGFloat offsetY = sin(radians) * self.amplitude;
|
|
|
|
CGPoint point = CGPointMake(originalCenter.x, originalCenter.y + offsetY);
|
|
[pathValues addObject:[NSValue valueWithCGPoint:point]];
|
|
}
|
|
|
|
animation.values = pathValues;
|
|
animation.duration = self.duration;
|
|
animation.repeatCount = HUGE_VALF;
|
|
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
|
|
|
|
// 随机延迟,避免所有视图同步
|
|
animation.beginTime = CACurrentMediaTime() + (arc4random_uniform(1000) / 1000.0) * 1.0;
|
|
|
|
[view.layer addAnimation:animation forKey:@"smoothFloatAnimation"];
|
|
}
|
|
|
|
- (void)addBounceFloatAnimationToView:(UIView *)view {
|
|
CGPoint originalCenter = [objc_getAssociatedObject(view, kFloatAnimationKey) CGPointValue];
|
|
|
|
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position.y"];
|
|
|
|
// 弹跳效果的值
|
|
NSArray *values = @[
|
|
@(originalCenter.y),
|
|
@(originalCenter.y - self.amplitude * 0.3),
|
|
@(originalCenter.y - self.amplitude),
|
|
@(originalCenter.y - self.amplitude * 0.7),
|
|
@(originalCenter.y - self.amplitude * 0.3),
|
|
@(originalCenter.y),
|
|
@(originalCenter.y + self.amplitude * 0.2),
|
|
@(originalCenter.y + self.amplitude * 0.5),
|
|
@(originalCenter.y + self.amplitude * 0.2),
|
|
@(originalCenter.y)
|
|
];
|
|
|
|
animation.values = values;
|
|
animation.duration = self.duration;
|
|
animation.repeatCount = HUGE_VALF;
|
|
|
|
// 弹跳的时间函数
|
|
animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.5 :1.8 :0.8 :0.8];
|
|
animation.beginTime = CACurrentMediaTime() + (arc4random_uniform(1000) / 1000.0) * 1.0;
|
|
|
|
[view.layer addAnimation:animation forKey:@"bounceFloatAnimation"];
|
|
}
|
|
|
|
- (void)addWaveFloatAnimationToView:(UIView *)view {
|
|
CGPoint originalCenter = [objc_getAssociatedObject(view, kFloatAnimationKey) CGPointValue];
|
|
|
|
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position.y"];
|
|
|
|
NSMutableArray *values = [NSMutableArray array];
|
|
NSInteger frames = 60;
|
|
|
|
// 波浪效果 - 多个正弦波叠加
|
|
for (int i = 0; i <= frames; i++) {
|
|
CGFloat progress = (CGFloat)i / frames;
|
|
|
|
// 主波
|
|
CGFloat wave1 = sin(progress * M_PI * 2) * self.amplitude;
|
|
// 次波
|
|
CGFloat wave2 = sin(progress * M_PI * 4) * self.amplitude * 0.3;
|
|
|
|
CGFloat offsetY = wave1 + wave2;
|
|
[values addObject:@(originalCenter.y + offsetY)];
|
|
}
|
|
|
|
animation.values = values;
|
|
animation.duration = self.duration;
|
|
animation.repeatCount = HUGE_VALF;
|
|
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
|
|
animation.beginTime = CACurrentMediaTime() + (arc4random_uniform(1000) / 1000.0) * 1.0;
|
|
|
|
[view.layer addAnimation:animation forKey:@"waveFloatAnimation"];
|
|
}
|
|
|
|
- (void)addBreathFloatAnimationToView:(UIView *)view {
|
|
CGPoint originalCenter = [objc_getAssociatedObject(view, kFloatAnimationKey) CGPointValue];
|
|
|
|
// 组合动画:位置 + 透明度
|
|
CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position.y"];
|
|
CAKeyframeAnimation *alphaAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
|
|
|
|
NSMutableArray *positionValues = [NSMutableArray array];
|
|
NSMutableArray *alphaValues = [NSMutableArray array];
|
|
NSInteger frames = 60;
|
|
|
|
for (int i = 0; i <= frames; i++) {
|
|
CGFloat progress = (CGFloat)i / frames;
|
|
CGFloat radians = progress * M_PI * 2;
|
|
|
|
// 位置变化
|
|
CGFloat offsetY = sin(radians) * self.amplitude;
|
|
[positionValues addObject:@(originalCenter.y + offsetY)];
|
|
|
|
// 透明度变化 (0.8 - 1.0)
|
|
CGFloat alpha = 0.9 + 0.1 * sin(radians);
|
|
[alphaValues addObject:@(alpha)];
|
|
}
|
|
|
|
positionAnimation.values = positionValues;
|
|
positionAnimation.duration = self.duration;
|
|
positionAnimation.repeatCount = HUGE_VALF;
|
|
|
|
alphaAnimation.values = alphaValues;
|
|
alphaAnimation.duration = self.duration;
|
|
alphaAnimation.repeatCount = HUGE_VALF;
|
|
|
|
CAAnimationGroup *group = [CAAnimationGroup animation];
|
|
group.animations = @[positionAnimation, alphaAnimation];
|
|
group.duration = self.duration;
|
|
group.repeatCount = HUGE_VALF;
|
|
group.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
|
|
group.beginTime = CACurrentMediaTime() + (arc4random_uniform(1000) / 1000.0) * 1.0;
|
|
|
|
[view.layer addAnimation:group forKey:@"breathFloatAnimation"];
|
|
}
|
|
|
|
- (void)addFloatAnimationToViews:(NSArray<UIView *> *)views {
|
|
for (UIView *view in views) {
|
|
[self addFloatAnimationToView:view];
|
|
}
|
|
}
|
|
|
|
- (void)removeFloatAnimationFromView:(UIView *)view {
|
|
if (!view) return;
|
|
|
|
[self.animatedViews removeObject:view];
|
|
|
|
// 移除所有漂浮动画
|
|
[view.layer removeAnimationForKey:@"smoothFloatAnimation"];
|
|
[view.layer removeAnimationForKey:@"bounceFloatAnimation"];
|
|
[view.layer removeAnimationForKey:@"waveFloatAnimation"];
|
|
[view.layer removeAnimationForKey:@"breathFloatAnimation"];
|
|
|
|
// 恢复原始位置
|
|
NSValue *originalCenterValue = objc_getAssociatedObject(view, kFloatAnimationKey);
|
|
if (originalCenterValue) {
|
|
view.center = [originalCenterValue CGPointValue];
|
|
}
|
|
}
|
|
|
|
- (void)removeAllFloatAnimations {
|
|
for (UIView *view in self.animatedViews) {
|
|
[self removeFloatAnimationFromView:view];
|
|
}
|
|
[self.animatedViews removeAllObjects];
|
|
}
|
|
|
|
- (void)pauseFloatAnimationForView:(UIView *)view {
|
|
CFTimeInterval pausedTime = [view.layer convertTime:CACurrentMediaTime() fromLayer:nil];
|
|
view.layer.speed = 0.0;
|
|
view.layer.timeOffset = pausedTime;
|
|
}
|
|
|
|
- (void)resumeFloatAnimationForView:(UIView *)view {
|
|
CFTimeInterval pausedTime = view.layer.timeOffset;
|
|
view.layer.speed = 1.0;
|
|
view.layer.timeOffset = 0.0;
|
|
view.layer.beginTime = 0.0;
|
|
CFTimeInterval timeSincePause = [view.layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
|
|
view.layer.beginTime = timeSincePause;
|
|
}
|
|
|
|
@end
|