Files
featherVoice/QXLive/Tools/Category/UIView+FloatingAnimation.m
2026-01-08 18:31:27 +08:00

241 lines
7.4 KiB
Objective-C

//
// UIView+FloatingAnimation.m
// QXLive
//
// Created by 启星 on 2026/1/5.
//
#import "UIView+FloatingAnimation.h"
#import <objc/runtime.h>
// 关联对象键
static char kFloatingAnimationKey;
static char kFloatingDistanceKey;
static char kFloatingDurationKey;
static char kFloatingDirectionKey;
static char kOriginalCenterKey;
static char kFloatingStateKey;
@interface UIView ()
// 内部属性
@property (nonatomic, assign) BOOL isFloating;
@property (nonatomic, assign) CGPoint originalCenter;
@property (nonatomic, assign) CGFloat floatingDistance;
@property (nonatomic, assign) NSTimeInterval floatingDuration;
@property (nonatomic, assign) FloatingDirection floatingDirection;
@end
@implementation UIView (FloatingAnimation)
#pragma mark - 公共方法
- (void)startInfiniteFloatingAnimationWithDistance:(CGFloat)distance
duration:(NSTimeInterval)duration
direction:(FloatingDirection)direction
autoStart:(BOOL)autoStart {
// 保存原始位置
self.originalCenter = self.center;
// 保存参数
self.floatingDistance = distance;
self.floatingDuration = duration;
self.floatingDirection = direction;
if (autoStart) {
[self startFloating];
}
}
- (void)startUpDownFloatingWithDistance:(CGFloat)distance duration:(NSTimeInterval)duration {
[self startInfiniteFloatingAnimationWithDistance:distance
duration:duration
direction:FloatingDirectionUpDown
autoStart:YES];
}
- (void)startFloating {
if (self.isFloating) {
return;
}
self.isFloating = YES;
// 根据方向选择动画方法
switch (self.floatingDirection) {
case FloatingDirectionUpDown:
[self startUpDownAnimation];
break;
case FloatingDirectionLeftRight:
[self startLeftRightAnimation];
break;
}
}
- (void)stopFloatingAnimation {
if (!self.isFloating) {
return;
}
self.isFloating = NO;
// 移除所有动画
[self.layer removeAllAnimations];
// 平滑回到原始位置
[UIView animateWithDuration:0.3
delay:0
usingSpringWithDamping:0.7
initialSpringVelocity:0.5
options:UIViewAnimationOptionCurveEaseOut
animations:^{
self.center = self.originalCenter;
} completion:nil];
}
- (void)pauseFloatingAnimation {
if (!self.isFloating) {
return;
}
// 暂停图层动画
CFTimeInterval pausedTime = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil];
self.layer.speed = 0.0;
self.layer.timeOffset = pausedTime;
}
- (void)resumeFloatingAnimation {
if (!self.isFloating) {
return;
}
// 恢复图层动画
CFTimeInterval pausedTime = [self.layer timeOffset];
self.layer.speed = 1.0;
self.layer.timeOffset = 0.0;
self.layer.beginTime = 0.0;
CFTimeInterval timeSincePause = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
self.layer.beginTime = timeSincePause;
}
- (BOOL)isFloating {
return [objc_getAssociatedObject(self, &kFloatingStateKey) boolValue];
}
#pragma mark - 私有动画方法
- (void)startUpDownAnimation {
// 计算目标位置
CGPoint upPoint = CGPointMake(self.center.x, self.center.y - self.floatingDistance);
CGPoint downPoint = CGPointMake(self.center.x, self.center.y + self.floatingDistance);
// 创建关键帧动画
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position.y"];
// 设置动画路径
NSArray *values = @[
@(self.center.y),
@(upPoint.y),
@(self.center.y),
@(downPoint.y),
@(self.center.y)
];
animation.values = values;
// 设置时间点
animation.keyTimes = @[@0, @0.25, @0.5, @0.75, @1];
// 配置动画属性
animation.duration = self.floatingDuration * 4; // 完整上下循环
animation.repeatCount = HUGE_VALF; // 无限循环
animation.autoreverses = NO;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
// 保存动画引用
objc_setAssociatedObject(self, &kFloatingAnimationKey, animation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 添加动画
[self.layer addAnimation:animation forKey:@"floatingAnimation"];
}
- (void)startLeftRightAnimation {
// 计算目标位置
CGPoint leftPoint = CGPointMake(self.center.x - self.floatingDistance, self.center.y);
CGPoint rightPoint = CGPointMake(self.center.x + self.floatingDistance, self.center.y);
// 创建关键帧动画
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position.x"];
// 设置动画路径
NSArray *values = @[
@(self.center.x),
@(leftPoint.x),
@(self.center.x),
@(rightPoint.x),
@(self.center.x)
];
animation.values = values;
// 设置时间点
animation.keyTimes = @[@0, @0.25, @0.5, @0.75, @1];
// 配置动画属性
animation.duration = self.floatingDuration * 2;
animation.repeatCount = HUGE_VALF;
animation.autoreverses = NO;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
// 保存动画引用
objc_setAssociatedObject(self, &kFloatingAnimationKey, animation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 添加动画
[self.layer addAnimation:animation forKey:@"floatingAnimation"];
}
#pragma mark - 关联属性
- (void)setIsFloating:(BOOL)isFloating {
objc_setAssociatedObject(self, &kFloatingStateKey, @(isFloating), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isFloatingAnimating {
return [objc_getAssociatedObject(self, &kFloatingStateKey) boolValue];
}
- (void)setOriginalCenter:(CGPoint)originalCenter {
objc_setAssociatedObject(self, &kOriginalCenterKey, [NSValue valueWithCGPoint:originalCenter], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (CGPoint)originalCenter {
NSValue *value = objc_getAssociatedObject(self, &kOriginalCenterKey);
return value ? [value CGPointValue] : self.center;
}
- (void)setFloatingDistance:(CGFloat)floatingDistance {
objc_setAssociatedObject(self, &kFloatingDistanceKey, @(floatingDistance), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (CGFloat)floatingDistance {
NSNumber *number = objc_getAssociatedObject(self, &kFloatingDistanceKey);
return number ? [number floatValue] : 10.0;
}
- (void)setFloatingDuration:(NSTimeInterval)floatingDuration {
objc_setAssociatedObject(self, &kFloatingDurationKey, @(floatingDuration), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSTimeInterval)floatingDuration {
NSNumber *number = objc_getAssociatedObject(self, &kFloatingDurationKey);
return number ? [number doubleValue] : 1.0;
}
- (void)setFloatingDirection:(FloatingDirection)floatingDirection {
objc_setAssociatedObject(self, &kFloatingDirectionKey, @(floatingDirection), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (FloatingDirection)floatingDirection {
NSNumber *number = objc_getAssociatedObject(self, &kFloatingDirectionKey);
return number ? [number integerValue] : FloatingDirectionUpDown;
}
@end