From 3da1b67ab58c8df75f5e8641c07a54ae58c30c78 Mon Sep 17 00:00:00 2001 From: v_zxingli Date: Wed, 6 Sep 2023 15:48:23 +0800 Subject: [PATCH] feat(iOS): add iOS custom scroll time --- .../DemoCustomScrollViewController.h | 10 + .../DemoCustomScrollViewController.m | 57 +++++ .../HippyDemo/HippyCustomScrollView.h | 59 +++++ .../HippyDemo/HippyCustomScrollView.m | 214 ++++++++++++++++++ 4 files changed, 340 insertions(+) create mode 100644 framework/examples/ios-demo/HippyDemo/DemoCustomScrollViewController.h create mode 100644 framework/examples/ios-demo/HippyDemo/DemoCustomScrollViewController.m create mode 100644 framework/examples/ios-demo/HippyDemo/HippyCustomScrollView.h create mode 100644 framework/examples/ios-demo/HippyDemo/HippyCustomScrollView.m diff --git a/framework/examples/ios-demo/HippyDemo/DemoCustomScrollViewController.h b/framework/examples/ios-demo/HippyDemo/DemoCustomScrollViewController.h new file mode 100644 index 00000000000..5d4fe98e12e --- /dev/null +++ b/framework/examples/ios-demo/HippyDemo/DemoCustomScrollViewController.h @@ -0,0 +1,10 @@ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface DemoCustomScrollViewController : UIViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/framework/examples/ios-demo/HippyDemo/DemoCustomScrollViewController.m b/framework/examples/ios-demo/HippyDemo/DemoCustomScrollViewController.m new file mode 100644 index 00000000000..cee137add0d --- /dev/null +++ b/framework/examples/ios-demo/HippyDemo/DemoCustomScrollViewController.m @@ -0,0 +1,57 @@ + +#import "DemoCustomScrollViewController.h" +#import "HippyCustomScrollView.h" + +@interface DemoCustomScrollViewController () + +@property (nonatomic ,strong) UITableView *tableView; + +@end + +@implementation DemoCustomScrollViewController + +- (UITableView *)tableView { + if (!_tableView) { + _tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain]; + _tableView.delegate = self; + _tableView.dataSource = self; + } + return _tableView; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view. +} + +- (void)click { + [self.tableView setContentOffset:CGPointMake(0, 500) duration:2.25 completion:^{ + + }]; +} + +/* +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. +} +*/ + +- (nonnull UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath { + static NSString *identifier = @"identifier"; + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; + if (!cell) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; + } + cell.textLabel.text = [NSString stringWithFormat:@"%ld",(long)indexPath.row]; + return cell; +} + +- (NSInteger)tableView:(nonnull UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return 100; +} + +@end diff --git a/framework/examples/ios-demo/HippyDemo/HippyCustomScrollView.h b/framework/examples/ios-demo/HippyDemo/HippyCustomScrollView.h new file mode 100644 index 00000000000..2bd6ee94bc3 --- /dev/null +++ b/framework/examples/ios-demo/HippyDemo/HippyCustomScrollView.h @@ -0,0 +1,59 @@ + +#import + +typedef enum +{ + linear = 0, + quadIn, + quadOut, + quadInOut, + cubicIn, + cubicOut, + cubicInOut, + quartIn, + quartOut, + quartInOut, + quintIn, + quintOut, + quintInOut, + sineIn, + sineOut, + sineInOut, + expoIn, + expoOut, + expoInOut, + circleIn, + circleOut, + circleInOut +} HippyScrollTimingEnum; + +NS_ASSUME_NONNULL_BEGIN + +@class HippyScrollTimingFunction; +@interface UIScrollView (HippyCustomOffsetAnimation) + +- (void)setContentOffset:(CGPoint)contentOffset + duration:(NSTimeInterval)duration + completion:(void(^)(void))block; + +@end + +@interface HippyScrollTimingFunction : NSObject + +@property (nonatomic,assign) HippyScrollTimingEnum type; + +@end + +@interface HippyScrollViewAnimator : NSObject + +@property (nonatomic, weak) UIScrollView *scrollView; +@property (nonatomic, copy) void(^block)(void); + +- (void)setContentOffset:(CGPoint)contentOffset duration:(NSTimeInterval)duration; +- (instancetype)initWithScrollView:(UIScrollView *)scrollView + timingFunction:(HippyScrollTimingFunction *)timingFunction + type:(HippyScrollTimingEnum)type; + +@end + +NS_ASSUME_NONNULL_END diff --git a/framework/examples/ios-demo/HippyDemo/HippyCustomScrollView.m b/framework/examples/ios-demo/HippyDemo/HippyCustomScrollView.m new file mode 100644 index 00000000000..1236004bdf0 --- /dev/null +++ b/framework/examples/ios-demo/HippyDemo/HippyCustomScrollView.m @@ -0,0 +1,214 @@ + +#import "HippyCustomScrollView.h" +#import + +@class HippyScrollViewAnimator; +@implementation UIScrollView (HippyCustomOffsetAnimation) + +static NSString *HippyCustomAnimatorKey = @"QModelOverlayParamsKey"; //定义一个key值 + +- (void)setAnimator:(HippyScrollViewAnimator *)animator +{ + objc_setAssociatedObject(self, &HippyCustomAnimatorKey, animator, OBJC_ASSOCIATION_RETAIN); +} + +- (HippyScrollViewAnimator *)animator +{ + return objc_getAssociatedObject(self, &HippyCustomAnimatorKey); +} + +- (void)setContentOffset:(CGPoint)contentOffset + duration:(NSTimeInterval)duration + completion:(void(^)(void))block { + if (!self.animator) { + self.animator = [[HippyScrollViewAnimator alloc] initWithScrollView:self timingFunction:[HippyScrollTimingFunction new] type:sineInOut]; + } + __weak __typeof(self) weakSelf = self; + self.animator.block = ^{ + __strong __typeof(weakSelf) strongSelf = weakSelf; + dispatch_async(dispatch_get_main_queue(), ^{ + if (strongSelf) { + strongSelf.animator = nil; + } + }); + block(); + }; + + [self.animator setContentOffset:contentOffset duration:duration]; + block(); +} + +@end + +@implementation HippyScrollTimingFunction +/// +/// - Parameters: +/// - t: time +/// - b: begin +/// - c: change +/// - d: duration +- (CGFloat)compute:(CGFloat)t b:(CGFloat)b c:(CGFloat)c d:(CGFloat)d { + switch (self.type) { + case linear: + return c * t / d + b; + case quadIn: + t /= d; + return c * t * t + b; + case quadOut: + t /= d; + return -c * t * (t - 2) + b; + case quadInOut: + t /= d / 2; + if (t < 1) { + return c / 2 * t * t + b; + } + t -= 1; + return -c / 2 * (t * (t - 2) - 1) + b; + case cubicIn: + t /= d; + return c * t * t * t + b; + case cubicOut: + t = t / d - 1; + return c * (t * t * t + 1) + b; + case cubicInOut: + t /= d / 2; + if (t < 1) { + return c / 2 * t * t * t + b; + } + t -= 2; + return c / 2 * (t * t * t + 2) + b; + case quartIn: + t /= d; + return c * t * t * t * t + b; + case quartOut: + t = t / d - 1; + return -c * (t * t * t * t - 1) + b; + case quartInOut: + t /= d / 2; + if (t < 1) { + return c / 2 * t * t * t * t + b; + } + t -= 2; + return -c / 2 * (t * t * t * t - 2) + b; + case quintIn: + t /= d; + return c * t * t * t * t * t + b; + case quintOut: + t = t / d - 1; + return c * ( t * t * t * t * t + 1) + b; + case quintInOut: + t /= d / 2; + if (t < 1) { + return c / 2 * t * t * t * t * t + b; + } + t -= 2; + return c / 2 * (t * t * t * t * t + 2) + b; + case sineIn: + return -c * cos(t / d * (M_PI / 2)) + c + b; + case sineOut: + return c * sin(t / d * (M_PI / 2)) + b; + case sineInOut: + return -c / 2 * (cos(M_PI * t / d) - 1) + b; + case expoIn: + return (t == 0) ? b : c * pow(2, 10 * (t / d - 1)) + b; + case expoOut: + return (t == d) ? b + c : c * (-pow(2, -10 * t / d) + 1) + b; + case expoInOut: + if (t == 0) { + return b; + } + if (t == d) { + return b + c; + } + t /= d / 2; + if (t < 1) { + return c / 2 * pow(2, 10 * (t - 1)) + b; + } + t -= 1; + return c / 2 * (-pow(2, -10 * t) + 2) + b; + case circleIn: + t /= d; + return -c * (sqrt(1 - t * t) - 1) + b; + case circleOut: + t = t / d - 1; + return c * sqrt(1 - t * t) + b; + case circleInOut: + t /= d / 2; + if (t < 1) { + return -c / 2 * (sqrt(1 - t * t) - 1) + b; + } + t -= 2; + return c / 2 * (sqrt(1 - t * t) + 1) + b; + } +} + +@end + +@interface HippyScrollViewAnimator () + +@property (nonatomic, assign) HippyScrollTimingEnum type; +@property (nonatomic, strong) HippyScrollTimingFunction *timingFunction; +@property (nonatomic, assign) NSTimeInterval startTime; +@property (nonatomic, assign) CGPoint startOffset; +@property (nonatomic, assign) CGPoint destinationOffset; +@property (nonatomic, assign) NSTimeInterval duration; +@property (nonatomic, assign) NSTimeInterval runTime; +@property (nonatomic, strong) CADisplayLink *timer; + +@end + +@implementation HippyScrollViewAnimator + +- (instancetype)initWithScrollView:(UIScrollView *)scrollView + timingFunction:(HippyScrollTimingFunction *)timingFunction + type:(HippyScrollTimingEnum)type { + if (self = [super init]) { + self.scrollView = scrollView; + self.timingFunction = timingFunction; + self.type = type; + } + return self; +} + +- (void)setContentOffset:(CGPoint)contentOffset duration:(NSTimeInterval)duration { + + if (!self.scrollView) return; + + self.startTime = [[NSDate date] timeIntervalSince1970]; + self.startOffset = self.scrollView.contentOffset; + self.destinationOffset = contentOffset; + self.duration = duration; + self.runTime = 0; + + if (self.duration <= 0) { + [self.scrollView setContentOffset:contentOffset animated:NO]; + return; + } + if (!self.timer) { + self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(animtedScroll)]; + [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } +} + +- (void)animtedScroll { + + if (!self.timer) return; + if (!self.scrollView) return; + + self.runTime += self.timer.duration; + + if (self.runTime >= self.duration) { + [self.scrollView setContentOffset:self.destinationOffset animated:NO]; + [self.timer invalidate]; + self.timer = nil; + self.block(); + return; + } + + CGPoint offset = self.scrollView.contentOffset; + offset.x = [self.timingFunction compute:self.runTime b:self.startOffset.x c:self.destinationOffset.x - self.startOffset.x d:self.duration]; + offset.y = [self.timingFunction compute:self.runTime b:self.startOffset.y c:self.destinationOffset.y - self.startOffset.y d:self.duration]; + [self.scrollView setContentOffset:offset animated:NO]; +} + +@end