Something went wrong. Try again.
Bluesky app fork with some witchin' additions 💫 forked from jollywhoppers.com/witchsky.app
Something went wrong. Try again.
12 kB · 411 lines
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412#import "ScrollForwarderView.h"
#import <React/RCTEnhancedScrollView.h>#import <React/RCTScrollViewComponentView.h>#import <react/renderer/components/ScrollForwarderViewSpec/ComponentDescriptors.h>#import <react/renderer/components/ScrollForwarderViewSpec/EventEmitters.h>#import <react/renderer/components/ScrollForwarderViewSpec/Props.h>#import <react/renderer/components/ScrollForwarderViewSpec/RCTComponentViewHelpers.h>
#import "RCTFabricComponentsPlugins.h"
using namespace facebook::react;
// How far down a pull needs to be to trigger a refreshstatic const CGFloat kPullThreshold = 130.0;static const CGFloat kDampingFactor = 0.55;// The top speed that free scrolling can havestatic const CGFloat kMaxVelocity = 5000.0;// Free scrolling decay. This seems to be close to the default iOS valuestatic const CGFloat kVelocityDecay = 0.9875;// What scroll release velocity will actually trigger free scrollingstatic const CGFloat kMinimumVelocity = 5.0;
@interface ScrollForwarderView () <RCTScrollForwarderViewViewProtocol, UIGestureRecognizerDelegate>
@end
@implementation ScrollForwarderView { NSArray<UIGestureRecognizer *> * _cancelGestureRecognizers; RCTScrollViewComponentView * _svcv; CGPoint _initialOffset; CADisplayLink * _displayLink; CGFloat _currentVelocity; CGFloat _accumulatedTranslation; bool _didImpact;}
+ (ComponentDescriptorProvider)componentDescriptorProvider{ return concreteComponentDescriptorProvider<ScrollForwarderViewComponentDescriptor>();}
- (instancetype)initWithFrame:(CGRect)frame{ if (self = [super initWithFrame:frame]) { static const auto defaultProps = std::make_shared<const ScrollForwarderViewProps>(); _props = defaultProps; UIPanGestureRecognizer *pg = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; pg.delegate = self; pg.cancelsTouchesInView = false; [self addGestureRecognizer:pg]; UITapGestureRecognizer *tg = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; [tg setEnabled:false]; tg.delegate = self; UILongPressGestureRecognizer *lpg = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; [lpg setMinimumPressDuration:0.01]; [lpg setEnabled:false]; lpg.delegate = self; NSArray<UIGestureRecognizer *> *cancelGestureRecognizers = [NSArray arrayWithObjects:lpg, tg, nil]; _cancelGestureRecognizers = cancelGestureRecognizers; }
return self;}
// MARK: - Lifecycle
- (void)dealloc{ [self stopAnimation]; [self removeCancelGestureRecognizers]; _svcv = nil; for (UIGestureRecognizer *gr in _cancelGestureRecognizers) { gr.delegate = nil; }}
- (void)prepareForRecycle{ [super prepareForRecycle]; [self stopAnimation]; [self removeCancelGestureRecognizers]; _svcv = nil;}
// MARK: - Props
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps{ const auto &oldViewProps = *std::static_pointer_cast<ScrollForwarderViewProps const>(_props); const auto &newViewProps = *std::static_pointer_cast<ScrollForwarderViewProps const>(props);
if (oldViewProps.scrollViewTag != newViewProps.scrollViewTag) { [self tryFindScrollView]; } if (oldViewProps.refreshing != newViewProps.refreshing) { if (!newViewProps.refreshing) { [self endRefreshing]; } } [super updateProps:props oldProps:oldProps];}
// MARK: - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{ if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] && [otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) { return NO; } return YES;}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{ if (![gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) { return YES; } UIPanGestureRecognizer *pg = (UIPanGestureRecognizer *)gestureRecognizer; CGPoint velocity = [pg velocityInView:self]; return fabs(velocity.y) > fabs(velocity.x);}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ [self stopAnimation]; [super touchesBegan:touches withEvent:event];}
// MARK: - Scroll Forwarding
- (void)removeCancelGestureRecognizers{ if (!_svcv) return; for (UIGestureRecognizer *gr in _cancelGestureRecognizers) { [_svcv.scrollView removeGestureRecognizer:gr]; }}
- (void)addCancelGestureRecognizers{ if (!_svcv) return; for (UIGestureRecognizer *gr in _cancelGestureRecognizers) { [_svcv.scrollView addGestureRecognizer:gr]; }}
- (void)enableCancelGestureRecognizers{ for (UIGestureRecognizer *gr in _cancelGestureRecognizers) { [gr setEnabled:true]; }}
- (void)disableCancelGestureRecognizers{ for (UIGestureRecognizer *gr in _cancelGestureRecognizers) { [gr setEnabled:false]; }}
- (void)scrollToOffset:(CGPoint)offset animated:(bool)animated{ if (!_svcv) return; [_svcv scrollToOffset:offset animated:animated];}
- (void)stopAnimation{ [self disableCancelGestureRecognizers]; [_displayLink invalidate]; _displayLink = nil;}
- (void)handlePan:(UIPanGestureRecognizer *)gesture { if (!_svcv) return; UIScrollView *sv = _svcv.scrollView; CGPoint translation = [gesture translationInView:self]; if (gesture.state == UIGestureRecognizerStateBegan) { _didImpact = false; if (sv.contentOffset.y < 0) { CGPoint newOffset = CGPointMake(sv.contentOffset.x, 0); sv.contentOffset = newOffset; } _initialOffset = sv.contentOffset; } if (gesture.state == UIGestureRecognizerStateChanged) { CGPoint newOffset = CGPointMake(sv.contentOffset.x, [self dampenOffset:(-translation.y + _initialOffset.y)]); sv.contentOffset = newOffset; if (sv.contentOffset.y <= -kPullThreshold && !_didImpact) { UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight]; [generator impactOccurred]; _didImpact = true; } } if (gesture.state == UIGestureRecognizerStateEnded) { CGPoint velocity = [gesture velocityInView:self];
if (sv.contentOffset.y <= -kPullThreshold) { [self refresh]; return; } if (sv.contentOffset.y < 0) { CGPoint newOffset = CGPointMake(sv.contentOffset.x, 0); [self scrollToOffset:newOffset animated:true]; return; } if (abs(velocity.y) < 250 && sv.contentOffset.y >= 0) { return; } [self startDecayWithInitialTranslation:translation.y velocity:velocity.y]; }}
- (CGFloat)dampenOffset:(CGFloat)offset{ if (offset < 0) { return offset - (offset * kDampingFactor); } return offset;}
- (void)handleTap:(UITapGestureRecognizer *)gesture { [self stopAnimation];}
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture { [self stopAnimation];}
- (void)startDecayWithInitialTranslation:(CGFloat)translation velocity:(CGFloat)startVelocity{ if (!_svcv) return; startVelocity = MAX(-kMaxVelocity, MIN(kMaxVelocity, startVelocity)); _currentVelocity = startVelocity; _accumulatedTranslation = -translation; [self enableCancelGestureRecognizers]; [_displayLink invalidate]; CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDecayStep:)]; link.preferredFramesPerSecond = 60; [link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; _displayLink = link;}
- (void)handleDecayStep:(CADisplayLink *)link{ _currentVelocity *= kVelocityDecay; CGFloat delta = -_currentVelocity / link.preferredFramesPerSecond; _accumulatedTranslation += delta; CGFloat rawY = _accumulatedTranslation + _initialOffset.y; CGFloat nextY = rawY > 0 ? rawY : 0; CGPoint newOffset = CGPointMake( _svcv.scrollView.contentOffset.x, nextY ); _svcv.scrollView.contentOffset = newOffset; if (fabs(_currentVelocity) < kMinimumVelocity || nextY <= 0) { [link invalidate]; _displayLink = nil; [self disableCancelGestureRecognizers]; }}
/* * We use this component on profile pages. The screne consists of a header component, a scrollview with buttons to * switch between profile tabs, and a pager view (RNCPagerViewComponentView). Both the header and the tab bar are * inside the same RCTViewComponentView. The view heirarchy looks something like this: * - RCTViewComponentView * -- RNCPagerViewComponentView * ----- (Many views deep) RCTScrollViewComponentView * ------ RCTEnhancedScrollView * -- RCTViewComponentView * --- RCTViewComponentView * ---- ScrollForwarderView * --- RCTScrollViewComponentView * ---- RCTEnhancedScrollView * * We want to find that RCTScrollViewComponentView inside of the RNCPagerViewComponentView. To achieve this, we can * use self.superview.superview.superview to get to the root RCTViewComponentView, find the RNCPagerViewComponentView, * then iterate through that view's subviews until we find a RCTScrollViewComponentView. * * This isn't great, because if we reorder the React components, we'll need to update this logic. There's probably * an easier way to achieve this, similar to how we used to do it in Paper (ie, get the scroll view's tag and find that), * but this also comes with some benefits, eg being able to reduce a lot of the logic in the JS code and just find the * scrollview when subviews change. */- (void)tryFindScrollView{ [self removeCancelGestureRecognizers]; // The root RCTViewComponentView UIView *rootView = self.superview.superview.superview; UIView *pagerView; NSString *targetClsName = @"RNCPagerViewComponentView"; Class targetCls = NSClassFromString(targetClsName);
for (UIView *subview in rootView.subviews) { if ([subview isKindOfClass:targetCls]) { pagerView = subview; break; } } if (!pagerView) return; RCTScrollViewComponentView *svcv = [self findRTCScrollViewComponentViewInView:pagerView]; if (!svcv) return; _svcv = svcv; [self addCancelGestureRecognizers];}
- (RCTScrollViewComponentView *)findRTCScrollViewComponentViewInView:(UIView *)view{ for (UIView *subview in view.subviews) { if ([subview isKindOfClass:[RCTScrollViewComponentView class]]) { RCTScrollViewComponentView *svcv = (RCTScrollViewComponentView *) subview; return svcv; } RCTScrollViewComponentView *svcv = [self findRTCScrollViewComponentViewInView:subview]; if (svcv) return svcv; } return nil;}
- (UIRefreshControl *)refreshContorl{ if (!_svcv) return nil; return _svcv.scrollView.refreshControl;}
- (void)refresh{ __weak ScrollForwarderView *weakSelf = self; [_svcv.scrollView.refreshControl beginRefreshing]; [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^(void) { if (!weakSelf) return; __strong ScrollForwarderView *self = weakSelf; // Whenever we call this method, the scrollview will always be at a position of // -130 or less. Scrolling back to -80 simulates the default behavior of RCTRefreshControl [self->_svcv.scrollView setContentOffset:CGPointMake(0, -65)]; } completion:^(__unused BOOL finished) { __strong ScrollForwarderView *self = weakSelf;
if (self->_eventEmitter != nullptr) { std::dynamic_pointer_cast<const facebook::react::ScrollForwarderViewEventEmitter>(self->_eventEmitter) ->onRefresh(facebook::react::ScrollForwarderViewEventEmitter::OnRefresh{}); } } ];}
- (void)endRefreshing{ UIRefreshControl *rc = [self refreshContorl]; CGPoint newOffset = CGPointMake(_svcv.scrollView.contentOffset.x, 0.0); [self scrollToOffset:newOffset animated:true]; [rc endRefreshing];}
Class<RCTComponentViewProtocol> ScrollForwarderViewCls(void){ return ScrollForwarderView.class;}
@end