Skip to content

Commit

Permalink
fix(ios): support intercept touche event
Browse files Browse the repository at this point in the history
  • Loading branch information
ozonelmy authored and zealotchen0 committed Aug 1, 2023
1 parent c459934 commit a85040d
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
*/

#import <UIKit/UIKit.h>

#import "NativeRenderTouchesProtocol.h"
#import "HPConvert+NativeRender.h"

NS_ASSUME_NONNULL_BEGIN

Expand All @@ -32,6 +34,11 @@ NS_ASSUME_NONNULL_BEGIN
*/
@interface NativeRenderTouchesView : UIView<NativeRenderTouchesProtocol>

/**
* Used to control how touch events are processed.
*/
@property (nonatomic, assign) NativeRenderPointerEvents pointerEvents;

@end

NS_ASSUME_NONNULL_END
162 changes: 128 additions & 34 deletions renderer/native/ios/renderer/component/view/NativeRenderTouchesView.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
*/

#import "NativeRenderTouchesView.h"
#import "objc/runtime.h"
#import "UIView+NativeRender.h"
#import "UIEvent+TouchResponder.h"
#import "UIView+DomEvent.h"
#import "UIView+MountEvent.h"
#import "UIView+NativeRender.h"
#import "objc/runtime.h"

@interface NativeRenderTouchesView () {
NSMutableDictionary<NSNumber *, OnTouchEventHandler> *_touchesEvents;
Expand Down Expand Up @@ -143,6 +145,59 @@ - (BOOL)canBePreventInBubbling:(const char *)name {
return NO;
}

- (void)setPointerEvents:(NativeRenderPointerEvents)pointerEvents {
_pointerEvents = pointerEvents;
self.userInteractionEnabled = (pointerEvents != NativeRenderPointerEventsNone);
if (pointerEvents == NativeRenderPointerEventsBoxNone) {
self.accessibilityViewIsModal = NO;
}
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
[event removeAllResponders];
BOOL canReceiveTouchEvents = ([self isUserInteractionEnabled] && ![self isHidden]);
if (!canReceiveTouchEvents) {
return nil;
}

// `hitSubview` is the topmost subview which was hit. The hit point can
// be outside the bounds of `view` (e.g., if -clipsToBounds is NO).
UIView *hitSubview = nil;
BOOL isPointInside = [self pointInside:point withEvent:event];
BOOL needsHitSubview = !(_pointerEvents == NativeRenderPointerEventsNone || _pointerEvents == NativeRenderPointerEventsBoxOnly);
if (needsHitSubview && (![self clipsToBounds] || isPointInside)) {
// The default behaviour of UIKit is that if a view does not contain a point,
// then no subviews will be returned from hit testing, even if they contain
// the hit point. By doing hit testing directly on the subviews, we bypass
// the strict containment policy (i.e., UIKit guarantees that every ancestor
// of the hit view will return YES from -pointInside:withEvent:). See:
// - https://developer.apple.com/library/ios/qa/qa2013/qa1812.html
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
hitSubview = [subview hitTest:convertedPoint withEvent:event];
if (hitSubview != nil) {
break;
}
}
}

UIView *hitView = (isPointInside ? self : nil);

switch (_pointerEvents) {
case NativeRenderPointerEventsNone:
return nil;
case NativeRenderPointerEventsUnspecified:
return hitSubview ?: hitView;
case NativeRenderPointerEventsBoxOnly:
return hitView;
case NativeRenderPointerEventsBoxNone:
return hitSubview;
default:
HPLogError(@"Invalid pointer-events specified %ld on %@", (long)_pointerEvents, self);
return hitSubview ?: hitView;
}
}

#pragma mark Touch Event Listener Add Methods
- (void)setTouchEventListener:(OnTouchEventHandler)eventListener forEvent:(NativeRenderViewEventType)event {
if (eventListener) {
Expand Down Expand Up @@ -188,15 +243,16 @@ - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (_pressInEventEnabled) {
_pressInTimer = [NSTimer scheduledTimerWithTimeInterval:.1f target:self selector:@selector(handlePressInEvent) userInfo:nil repeats:NO];
}
OnTouchEventHandler listener = [self eventListenerForEventType:NativeRenderViewEventTypeTouchStart];
if (listener) {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:[self NativeRenderRootView]];
listener(point);
}
else {
[super touchesBegan:touches withEvent:event];
if ([self tryToHandleEvent:event forEventType:NativeRenderViewEventTypeTouchStart]) {
OnTouchEventHandler listener = [self eventListenerForEventType:NativeRenderViewEventTypeTouchStart];
if (listener) {
UITouch *touch = [touches anyObject];
UIView *rootView = [self NativeRenderRootView];
CGPoint point = [touch locationInView:rootView];
listener(point);
}
}
[super touchesBegan:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
Expand All @@ -205,28 +261,30 @@ - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
_pressInTimer = nil;
}
[self handlePressOutEvent];
OnTouchEventHandler listener = [self eventListenerForEventType:NativeRenderViewEventTypeTouchEnd];
if (listener) {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:[self NativeRenderRootView]];
listener(point);
}
else {
[super touchesEnded:touches withEvent:event];
if ([self tryToHandleEvent:event forEventType:NativeRenderViewEventTypeTouchEnd]) {
OnTouchEventHandler listener = [self eventListenerForEventType:NativeRenderViewEventTypeTouchEnd];
if (listener) {
UITouch *touch = [touches anyObject];
UIView *rootView = [self NativeRenderRootView];
CGPoint point = [touch locationInView:rootView];
listener(point);
}
}
[super touchesEnded:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
OnTouchEventHandler listener = [self eventListenerForEventType:NativeRenderViewEventTypeTouchMove];
if (listener) {
[self handlePressOutEvent];
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:[self NativeRenderRootView]];
listener(point);
}
else {
[super touchesMoved:touches withEvent:event];
if ([self tryToHandleEvent:event forEventType:NativeRenderViewEventTypeTouchMove]) {
OnTouchEventHandler listener = [self eventListenerForEventType:NativeRenderViewEventTypeTouchMove];
if (listener) {
[self handlePressOutEvent];
UITouch *touch = [touches anyObject];
UIView *rootView = [self NativeRenderRootView];
CGPoint point = [touch locationInView:rootView];
listener(point);
}
}
[super touchesMoved:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
Expand All @@ -235,15 +293,51 @@ - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
_pressInTimer = nil;
}
[self handlePressOutEvent];
OnTouchEventHandler listener = [self eventListenerForEventType:NativeRenderViewEventTypeTouchCancel];
if (listener) {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:[self NativeRenderRootView]];
listener(point);
if ([self tryToHandleEvent:event forEventType:NativeRenderViewEventTypeTouchCancel]) {
OnTouchEventHandler listener = [self eventListenerForEventType:NativeRenderViewEventTypeTouchCancel];
if (listener) {
UITouch *touch = [touches anyObject];
UIView *rootView = [self NativeRenderRootView];
CGPoint point = [touch locationInView:rootView];
listener(point);
}
}
else {
[super touchesCancelled:touches withEvent:event];
[super touchesCancelled:touches withEvent:event];
}

- (BOOL)tryToHandleEvent:(UIEvent *)event forEventType:(NativeRenderViewEventType)eventType {
id responder = [event responderForType:eventType];
if (self == responder) {
return YES;
}
if (nil == responder) {
// assume first responder is self
UIView *responder = nil;
// find out is there any parent view who can handle `eventType` and `onInterceptTouchEvent` is YES
UIView *testingView = self;
while (testingView) {
OnTouchEventHandler handler = [testingView eventListenerForEventType:eventType];
if (!responder && handler) {
responder = testingView;
}
BOOL onInterceptTouchEvent = testingView.onInterceptTouchEvent;
if (handler && onInterceptTouchEvent) {
responder = testingView;
}
testingView = [testingView parentComponent];
}
// set first responder for `eventType`
if (responder) {
[event setResponder:responder forType:eventType];
}
else {
[event setResponder:[NSNull null] forType:eventType];
}
if (responder == self) {
return YES;
}
}
return NO;
}

- (void)handleClickEvent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,13 @@

#import <UIKit/UIKit.h>

#import "HPConvert+NativeRender.h"
#import "NativeRenderComponentProtocol.h"
#import "NativeRenderTouchesView.h"

@class NativeRenderGradientObject;

@interface NativeRenderView : NativeRenderTouchesView

/**
* Used to control how touch events are processed.
*/
@property (nonatomic, assign) NativeRenderPointerEvents pointerEvents;

/**
* z-index, used to override sibling order in didUpdateHippySubviews. This is
* inherited from UIView+NativeRender, but we override it here to reduce the boxing
Expand Down
73 changes: 5 additions & 68 deletions renderer/native/ios/renderer/component/view/NativeRenderView.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
* limitations under the License.
*/

#import "NativeRenderView.h"
#import "NativeRenderBorderDrawing.h"
#import "HPConvert.h"
#import "HPToolUtils.h"
#import "UIView+NativeRender.h"
#import "NativeRenderBorderDrawing.h"
#import "NativeRenderGradientObject.h"
#import "NativeRenderView.h"
#import "UIEvent+TouchResponder.h"
#import "UIView+DomEvent.h"
#import "UIView+NativeRender.h"

static CGSize makeSizeConstrainWithType(CGSize originSize, CGSize constrainSize, NSString *resizeMode) {
// width / height
Expand Down Expand Up @@ -98,71 +100,6 @@ - (NSString *)accessibilityLabel {
return NativeRenderRecursiveAccessibilityLabel(self);
}

- (void)setPointerEvents:(NativeRenderPointerEvents)pointerEvents {
_pointerEvents = pointerEvents;
self.userInteractionEnabled = (pointerEvents != NativeRenderPointerEventsNone);
if (pointerEvents == NativeRenderPointerEventsBoxNone) {
self.accessibilityViewIsModal = NO;
}
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
BOOL canReceiveTouchEvents = ([self isUserInteractionEnabled] && ![self isHidden]);
if (!canReceiveTouchEvents) {
return nil;
}

// `hitSubview` is the topmost subview which was hit. The hit point can
// be outside the bounds of `view` (e.g., if -clipsToBounds is NO).
UIView *hitSubview = nil;
BOOL isPointInside = [self pointInside:point withEvent:event];
BOOL needsHitSubview = !(_pointerEvents == NativeRenderPointerEventsNone || _pointerEvents == NativeRenderPointerEventsBoxOnly);
if (needsHitSubview && (![self clipsToBounds] || isPointInside)) {
// The default behaviour of UIKit is that if a view does not contain a point,
// then no subviews will be returned from hit testing, even if they contain
// the hit point. By doing hit testing directly on the subviews, we bypass
// the strict containment policy (i.e., UIKit guarantees that every ancestor
// of the hit view will return YES from -pointInside:withEvent:). See:
// - https://developer.apple.com/library/ios/qa/qa2013/qa1812.html
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
hitSubview = [subview hitTest:convertedPoint withEvent:event];
if (hitSubview != nil) {
break;
}
}
}

UIView *hitView = (isPointInside ? self : nil);

switch (_pointerEvents) {
case NativeRenderPointerEventsNone:
return nil;
case NativeRenderPointerEventsUnspecified:
return hitSubview ?: hitView;
case NativeRenderPointerEventsBoxOnly:
return hitView;
case NativeRenderPointerEventsBoxNone:
return hitSubview;
default:
HPLogError(@"Invalid pointer-events specified %ld on %@", (long)_pointerEvents, self);
return hitSubview ?: hitView;
}
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
//require clickable when animating.
//we check presentationLayer frame.
//point inside presentationLayer means point inside view
if ([[self.layer animationKeys] count] > 0) {
CGRect presentationLayerFrame = self.layer.presentationLayer.frame;
CGRect convertPresentationLayerFrame = [self.superview convertRect:presentationLayerFrame toView:self];
return CGRectContainsPoint(convertPresentationLayerFrame, point);
}
BOOL pointInside = [super pointInside:point withEvent:event];
return pointInside;
}

- (NSString *)description {
NSString *superDescription = super.description;
NSRange semicolonRange = [superDescription rangeOfString:@";"];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ - (void)measureInAppWindow:(NSNumber *)componentTag

NATIVE_RENDER_EXPORT_VIEW_PROPERTY(backgroundPositionX, CGFloat)
NATIVE_RENDER_EXPORT_VIEW_PROPERTY(backgroundPositionY, CGFloat)

NATIVE_RENDER_EXPORT_VIEW_PROPERTY(onInterceptTouchEvent, BOOL)
NATIVE_RENDER_CUSTOM_VIEW_PROPERTY(visibility, NSString, NativeRenderView) {
if (json) {
NSString *status = [HPConvert NSString:json];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*!
* iOS SDK
*
* Tencent is pleased to support the open source community by making
* NativeRender available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#import <UIKit/UIKit.h>
#import "NativeRenderViewEventType.h"

NS_ASSUME_NONNULL_BEGIN

@interface UIEvent (TouchResponder)

- (void)setResponder:(__weak id)responder forType:(NativeRenderViewEventType)type;

- (id)responderForType:(NativeRenderViewEventType)type;

- (void)removeAllResponders;

@end

NS_ASSUME_NONNULL_END

Loading

0 comments on commit a85040d

Please sign in to comment.