This app provides monitoring and information features for the common freifunk user and the technical stuff of a freifunk community.
Code base is taken from a TUM Practical Course project and added here to see if Freifunk Altdorf can use it.
https://www.freifunk-altdorf.de
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
380 lines
12 KiB
380 lines
12 KiB
/** |
|
* Copyright (c) 2015-present, Facebook, Inc. |
|
* |
|
* This source code is licensed under the MIT license found in the |
|
* LICENSE file in the root directory of this source tree. |
|
*/ |
|
|
|
#import "RCTTouchHandler.h" |
|
|
|
#import <UIKit/UIGestureRecognizerSubclass.h> |
|
|
|
#import "RCTAssert.h" |
|
#import "RCTBridge.h" |
|
#import "RCTEventDispatcher.h" |
|
#import "RCTLog.h" |
|
#import "RCTTouchEvent.h" |
|
#import "RCTUIManager.h" |
|
#import "RCTUtils.h" |
|
#import "UIView+React.h" |
|
|
|
@interface RCTTouchHandler () <UIGestureRecognizerDelegate> |
|
@end |
|
|
|
// TODO: this class behaves a lot like a module, and could be implemented as a |
|
// module if we were to assume that modules and RootViews had a 1:1 relationship |
|
@implementation RCTTouchHandler |
|
{ |
|
__weak RCTEventDispatcher *_eventDispatcher; |
|
|
|
/** |
|
* Arrays managed in parallel tracking native touch object along with the |
|
* native view that was touched, and the React touch data dictionary. |
|
* These must be kept track of because `UIKit` destroys the touch targets |
|
* if touches are canceled, and we have no other way to recover this info. |
|
*/ |
|
NSMutableOrderedSet<UITouch *> *_nativeTouches; |
|
NSMutableArray<NSMutableDictionary *> *_reactTouches; |
|
NSMutableArray<UIView *> *_touchViews; |
|
|
|
__weak UIView *_cachedRootView; |
|
|
|
uint16_t _coalescingKey; |
|
} |
|
|
|
- (instancetype)initWithBridge:(RCTBridge *)bridge |
|
{ |
|
RCTAssertParam(bridge); |
|
|
|
if ((self = [super initWithTarget:nil action:NULL])) { |
|
_eventDispatcher = [bridge moduleForClass:[RCTEventDispatcher class]]; |
|
|
|
_nativeTouches = [NSMutableOrderedSet new]; |
|
_reactTouches = [NSMutableArray new]; |
|
_touchViews = [NSMutableArray new]; |
|
|
|
// `cancelsTouchesInView` and `delaysTouches*` are needed in order to be used as a top level |
|
// event delegated recognizer. Otherwise, lower-level components not built |
|
// using RCT, will fail to recognize gestures. |
|
self.cancelsTouchesInView = NO; |
|
self.delaysTouchesBegan = NO; // This is default value. |
|
self.delaysTouchesEnded = NO; |
|
|
|
self.delegate = self; |
|
} |
|
|
|
return self; |
|
} |
|
|
|
RCT_NOT_IMPLEMENTED(- (instancetype)initWithTarget:(id)target action:(SEL)action) |
|
|
|
- (void)attachToView:(UIView *)view |
|
{ |
|
RCTAssert(self.view == nil, @"RCTTouchHandler already has attached view."); |
|
|
|
[view addGestureRecognizer:self]; |
|
} |
|
|
|
- (void)detachFromView:(UIView *)view |
|
{ |
|
RCTAssertParam(view); |
|
RCTAssert(self.view == view, @"RCTTouchHandler attached to another view."); |
|
|
|
[view removeGestureRecognizer:self]; |
|
} |
|
|
|
#pragma mark - Bookkeeping for touch indices |
|
|
|
- (void)_recordNewTouches:(NSSet<UITouch *> *)touches |
|
{ |
|
for (UITouch *touch in touches) { |
|
|
|
RCTAssert(![_nativeTouches containsObject:touch], |
|
@"Touch is already recorded. This is a critical bug."); |
|
|
|
// Find closest React-managed touchable view |
|
UIView *targetView = touch.view; |
|
while (targetView) { |
|
if (targetView.reactTag && targetView.userInteractionEnabled) { |
|
break; |
|
} |
|
targetView = targetView.superview; |
|
} |
|
|
|
NSNumber *reactTag = [targetView reactTagAtPoint:[touch locationInView:targetView]]; |
|
if (!reactTag || !targetView.userInteractionEnabled) { |
|
continue; |
|
} |
|
|
|
// Get new, unique touch identifier for the react touch |
|
const NSUInteger RCTMaxTouches = 11; // This is the maximum supported by iDevices |
|
NSInteger touchID = ([_reactTouches.lastObject[@"identifier"] integerValue] + 1) % RCTMaxTouches; |
|
for (NSDictionary *reactTouch in _reactTouches) { |
|
NSInteger usedID = [reactTouch[@"identifier"] integerValue]; |
|
if (usedID == touchID) { |
|
// ID has already been used, try next value |
|
touchID ++; |
|
} else if (usedID > touchID) { |
|
// If usedID > touchID, touchID must be unique, so we can stop looking |
|
break; |
|
} |
|
} |
|
|
|
// Create touch |
|
NSMutableDictionary *reactTouch = [[NSMutableDictionary alloc] initWithCapacity:RCTMaxTouches]; |
|
reactTouch[@"target"] = reactTag; |
|
reactTouch[@"identifier"] = @(touchID); |
|
|
|
// Add to arrays |
|
[_touchViews addObject:targetView]; |
|
[_nativeTouches addObject:touch]; |
|
[_reactTouches addObject:reactTouch]; |
|
} |
|
} |
|
|
|
- (void)_recordRemovedTouches:(NSSet<UITouch *> *)touches |
|
{ |
|
for (UITouch *touch in touches) { |
|
NSUInteger index = [_nativeTouches indexOfObject:touch]; |
|
if (index == NSNotFound) { |
|
continue; |
|
} |
|
|
|
[_touchViews removeObjectAtIndex:index]; |
|
[_nativeTouches removeObjectAtIndex:index]; |
|
[_reactTouches removeObjectAtIndex:index]; |
|
} |
|
} |
|
|
|
- (void)_updateReactTouchAtIndex:(NSInteger)touchIndex |
|
{ |
|
UITouch *nativeTouch = _nativeTouches[touchIndex]; |
|
CGPoint windowLocation = [nativeTouch locationInView:nativeTouch.window]; |
|
RCTAssert(_cachedRootView, @"We were unable to find a root view for the touch"); |
|
CGPoint rootViewLocation = [nativeTouch.window convertPoint:windowLocation toView:_cachedRootView]; |
|
|
|
UIView *touchView = _touchViews[touchIndex]; |
|
CGPoint touchViewLocation = [nativeTouch.window convertPoint:windowLocation toView:touchView]; |
|
|
|
NSMutableDictionary *reactTouch = _reactTouches[touchIndex]; |
|
reactTouch[@"pageX"] = @(RCTSanitizeNaNValue(rootViewLocation.x, @"touchEvent.pageX")); |
|
reactTouch[@"pageY"] = @(RCTSanitizeNaNValue(rootViewLocation.y, @"touchEvent.pageY")); |
|
reactTouch[@"locationX"] = @(RCTSanitizeNaNValue(touchViewLocation.x, @"touchEvent.locationX")); |
|
reactTouch[@"locationY"] = @(RCTSanitizeNaNValue(touchViewLocation.y, @"touchEvent.locationY")); |
|
reactTouch[@"timestamp"] = @(nativeTouch.timestamp * 1000); // in ms, for JS |
|
|
|
// TODO: force for a 'normal' touch is usually 1.0; |
|
// should we expose a `normalTouchForce` constant somewhere (which would |
|
// have a value of `1.0 / nativeTouch.maximumPossibleForce`)? |
|
if (RCTForceTouchAvailable()) { |
|
reactTouch[@"force"] = @(RCTZeroIfNaN(nativeTouch.force / nativeTouch.maximumPossibleForce)); |
|
} |
|
} |
|
|
|
/** |
|
* Constructs information about touch events to send across the serialized |
|
* boundary. This data should be compliant with W3C `Touch` objects. This data |
|
* alone isn't sufficient to construct W3C `Event` objects. To construct that, |
|
* there must be a simple receiver on the other side of the bridge that |
|
* organizes the touch objects into `Event`s. |
|
* |
|
* We send the data as an array of `Touch`es, the type of action |
|
* (start/end/move/cancel) and the indices that represent "changed" `Touch`es |
|
* from that array. |
|
*/ |
|
- (void)_updateAndDispatchTouches:(NSSet<UITouch *> *)touches |
|
eventName:(NSString *)eventName |
|
{ |
|
// Update touches |
|
NSMutableArray<NSNumber *> *changedIndexes = [NSMutableArray new]; |
|
for (UITouch *touch in touches) { |
|
NSInteger index = [_nativeTouches indexOfObject:touch]; |
|
if (index == NSNotFound) { |
|
continue; |
|
} |
|
|
|
[self _updateReactTouchAtIndex:index]; |
|
[changedIndexes addObject:@(index)]; |
|
} |
|
|
|
if (changedIndexes.count == 0) { |
|
return; |
|
} |
|
|
|
// Deep copy the touches because they will be accessed from another thread |
|
// TODO: would it be safer to do this in the bridge or executor, rather than trusting caller? |
|
NSMutableArray<NSDictionary *> *reactTouches = |
|
[[NSMutableArray alloc] initWithCapacity:_reactTouches.count]; |
|
for (NSDictionary *touch in _reactTouches) { |
|
[reactTouches addObject:[touch copy]]; |
|
} |
|
|
|
BOOL canBeCoalesced = [eventName isEqualToString:@"touchMove"]; |
|
|
|
// We increment `_coalescingKey` twice here just for sure that |
|
// this `_coalescingKey` will not be reused by ahother (preceding or following) event |
|
// (yes, even if coalescing only happens (and makes sense) on events of the same type). |
|
|
|
if (!canBeCoalesced) { |
|
_coalescingKey++; |
|
} |
|
|
|
RCTTouchEvent *event = [[RCTTouchEvent alloc] initWithEventName:eventName |
|
reactTag:self.view.reactTag |
|
reactTouches:reactTouches |
|
changedIndexes:changedIndexes |
|
coalescingKey:_coalescingKey]; |
|
|
|
if (!canBeCoalesced) { |
|
_coalescingKey++; |
|
} |
|
|
|
[_eventDispatcher sendEvent:event]; |
|
} |
|
|
|
/*** |
|
* To ensure compatibilty when using UIManager.measure and RCTTouchHandler, we have to adopt |
|
* UIManager.measure's behavior in finding a "root view". |
|
* Usually RCTTouchHandler is already attached to a root view but in some cases (e.g. Modal), |
|
* we are instead attached to some RCTView subtree. This is also the case when embedding some RN |
|
* views inside a seperate ViewController not controlled by RN. |
|
* This logic will either find the nearest rootView, or go all the way to the UIWindow. |
|
* While this is not optimal, it is exactly what UIManager.measure does, and what Touchable.js |
|
* relies on. |
|
* We cache it here so that we don't have to repeat it for every touch in the gesture. |
|
*/ |
|
- (void)_cacheRootView |
|
{ |
|
UIView *rootView = self.view; |
|
while (rootView.superview && ![rootView isReactRootView]) { |
|
rootView = rootView.superview; |
|
} |
|
_cachedRootView = rootView; |
|
} |
|
|
|
#pragma mark - Gesture Recognizer Delegate Callbacks |
|
|
|
static BOOL RCTAllTouchesAreCancelledOrEnded(NSSet<UITouch *> *touches) |
|
{ |
|
for (UITouch *touch in touches) { |
|
if (touch.phase == UITouchPhaseBegan || |
|
touch.phase == UITouchPhaseMoved || |
|
touch.phase == UITouchPhaseStationary) { |
|
return NO; |
|
} |
|
} |
|
return YES; |
|
} |
|
|
|
static BOOL RCTAnyTouchesChanged(NSSet<UITouch *> *touches) |
|
{ |
|
for (UITouch *touch in touches) { |
|
if (touch.phase == UITouchPhaseBegan || |
|
touch.phase == UITouchPhaseMoved) { |
|
return YES; |
|
} |
|
} |
|
return NO; |
|
} |
|
|
|
#pragma mark - `UIResponder`-ish touch-delivery methods |
|
|
|
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event |
|
{ |
|
[super touchesBegan:touches withEvent:event]; |
|
|
|
[self _cacheRootView]; |
|
|
|
// "start" has to record new touches *before* extracting the event. |
|
// "end"/"cancel" needs to remove the touch *after* extracting the event. |
|
[self _recordNewTouches:touches]; |
|
|
|
[self _updateAndDispatchTouches:touches eventName:@"touchStart"]; |
|
|
|
if (self.state == UIGestureRecognizerStatePossible) { |
|
self.state = UIGestureRecognizerStateBegan; |
|
} else if (self.state == UIGestureRecognizerStateBegan) { |
|
self.state = UIGestureRecognizerStateChanged; |
|
} |
|
} |
|
|
|
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event |
|
{ |
|
[super touchesMoved:touches withEvent:event]; |
|
|
|
[self _updateAndDispatchTouches:touches eventName:@"touchMove"]; |
|
self.state = UIGestureRecognizerStateChanged; |
|
} |
|
|
|
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event |
|
{ |
|
[super touchesEnded:touches withEvent:event]; |
|
|
|
[self _updateAndDispatchTouches:touches eventName:@"touchEnd"]; |
|
|
|
if (RCTAllTouchesAreCancelledOrEnded(event.allTouches)) { |
|
self.state = UIGestureRecognizerStateEnded; |
|
} else if (RCTAnyTouchesChanged(event.allTouches)) { |
|
self.state = UIGestureRecognizerStateChanged; |
|
} |
|
|
|
[self _recordRemovedTouches:touches]; |
|
} |
|
|
|
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event |
|
{ |
|
[super touchesCancelled:touches withEvent:event]; |
|
|
|
[self _updateAndDispatchTouches:touches eventName:@"touchCancel"]; |
|
|
|
if (RCTAllTouchesAreCancelledOrEnded(event.allTouches)) { |
|
self.state = UIGestureRecognizerStateCancelled; |
|
} else if (RCTAnyTouchesChanged(event.allTouches)) { |
|
self.state = UIGestureRecognizerStateChanged; |
|
} |
|
|
|
[self _recordRemovedTouches:touches]; |
|
} |
|
|
|
- (BOOL)canPreventGestureRecognizer:(__unused UIGestureRecognizer *)preventedGestureRecognizer |
|
{ |
|
return NO; |
|
} |
|
|
|
- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer |
|
{ |
|
// We fail in favour of other external gesture recognizers. |
|
// iOS will ask `delegate`'s opinion about this gesture recognizer little bit later. |
|
return ![preventingGestureRecognizer.view isDescendantOfView:self.view]; |
|
} |
|
|
|
- (void)reset |
|
{ |
|
if (_nativeTouches.count != 0) { |
|
[self _updateAndDispatchTouches:_nativeTouches.set eventName:@"touchCancel"]; |
|
|
|
[_nativeTouches removeAllObjects]; |
|
[_reactTouches removeAllObjects]; |
|
[_touchViews removeAllObjects]; |
|
|
|
_cachedRootView = nil; |
|
} |
|
} |
|
|
|
#pragma mark - Other |
|
|
|
- (void)cancel |
|
{ |
|
self.enabled = NO; |
|
self.enabled = YES; |
|
} |
|
|
|
#pragma mark - UIGestureRecognizerDelegate |
|
|
|
- (BOOL)gestureRecognizer:(__unused UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer |
|
{ |
|
// Same condition for `failure of` as for `be prevented by`. |
|
return [self canBePreventedByGestureRecognizer:otherGestureRecognizer]; |
|
} |
|
|
|
@end
|
|
|