241 lines
7.4 KiB
Objective-C
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
|