可以在本机反应中检测到视图组件吗?

Posted

技术标签:

【中文标题】可以在本机反应中检测到视图组件吗?【英文标题】:Can touch out side a View Component be detected in react native? 【发布时间】:2017-02-26 03:54:01 【问题描述】:

我的 React 本机应用程序屏幕具有带有少量文本输入的 View 组件。如何在该视图之外的屏幕上检测到触摸?请帮忙。

谢谢

【问题讨论】:

视图的“外部”是什么意思?您的 View 是根组件还是嵌套在另一个组件中?更重要的是,你想达到什么目标? 视图是嵌套视图。我想要一个具有很少文本输入的视图的显示隐藏功能。就像..当它被加载时,它看起来像一个输入文本,但是当你点击它时,它会展开成一个具有多个文本输入的表单,当你点击视图之外的任何部分时,表单会折叠。我希望我现在能更好地阐述它 【参考方案1】:

将您的View 放在TouchableWithoutFeedback 中,展开TouchableWithoutFeedback 全屏并向其中添加onPress 处理程序。

<TouchableWithoutFeedback 
  onPress= /*handle tap outside of view*/ 
  style= /* fullscreen styles */
>
    <View>
     ...
    </View
</TouchableWithoutFeedback>

【讨论】:

【参考方案2】:

您可以尝试使用Modal 来创建此行为。

当您单击输入字段时,您会显示包含多个文本输入的模态。如果您在 Modal 之外单击它会隐藏。

【讨论】:

【参考方案3】:

作为 Andrew said:您可以使用 TouchableWithoutFeedback 包装您的视图并添加一个 onPress,您可以检测何时点击视图。

实现此目的的另一种方法是对来自view 的触摸事件做出响应。

 /* Methods that handled the events */
handlePressIn(event) 
  // Do stuff when the view is touched


handlePressOut(event) 
    // Do stuff when the the touch event is finished

...

    <View
      onStartShouldSetResponder=(evt) => true
      onMoveShouldSetResponder=(evt) => true
      onResponderGrant=this.handlePressIn
      onResponderMove=this.handlePressIn
      onResponderRelease=this.handlePressOut
    >
         ...
    </View>

Grant 和 move 的区别在于 Grant 是当用户按下时,而 Move 是当用户按下并移动按下的位置时

【讨论】:

当我在滚动视图中这样做时,它对我不起作用【参考方案4】:

我不接受否定的答案,所以我挖了很多东西来找到符合我需求的解决方案。

在我的情况下,当我打开另一个组件时,我有多个组件需要折叠。 此行为必须是自动的,并且任何贡献者都易于编写代码。 在我的情况下,将父级引用传递给子级或调用特殊的全局方法不是可接受的解决方案。 使用透明背景来捕捉所有点击不会消除它。

This Question 完美地说明了需求。

演示

这是最终结果。单击组件本身以外的任何位置都会将其折叠。

警告 该解决方案包括使用私有 React 组件属性。我知道使用这种方法的固有风险,只要我的应用程序符合我的预期并且满足所有其他限制条件,我很乐意使用它们。简短的免责声明,可能存在更智能、更清洁的解决方案。这是我自己对 React 的有限知识所能做的最好的事情。

首先,我们需要捕获 UI 中的所有点击,包括 Web 和 Native。这似乎并不容易做到。嵌套TouchableOpacity似乎一次只允许一个响应者。所以我不得不在这里即兴发挥。

app.tsx(精简为基本内容)

import * as div from './app.style';
import  screenClicked, screenTouched  from './shared/services/self-close-signal.service';
// ... other imports

class App extends React.Component<Props, State> 

    public render() 

        return (
            <div.AppSafeArea 
                onTouchStart=e => screenTouched(e)
                onClick=e => screenClicked(e)>

                /* App Routes */
                <>appRoutes(loginResponse)</>

            </div.AppSafeArea>
        );
    

self-close-signal.service.ts 该服务旨在检测应用程序屏幕上的所有点击。我在整个应用程序中使用反应式编程,所以这里使用了 rxjs。如果您愿意,请随意使用更简单的方法。这里的关键部分是检测点击的元素是否是扩展组件层次结构的一部分。当我写出这样的烂摊子时,我通常会完整记录为什么要这样构建,以防止“急切”的开发人员进行清理。

import  AncestorNodeTrace, DebugOwner, SelfCloseEvent  from '../interfaces/self-close';
import  GestureResponderEvent  from 'react-native';
import  Subject  from 'rxjs';

/**
 * <!> Problem:
 * Consider the following scenario:
 * We have a dropdown opened and we want to open the second one. What should happen?
 * The first dropdown should close when detecting click outside.
 * Detecting clicks outside is not a trivial task in React Native.
 * The react events system does not allow adding event listeners.
 * Even worse adding event listener is not available in react native.
 * Further more, TouchableOpacity swallows events.
 * This means that a child TouchableOpacity inside a parent TouchableOpacity will consume the event.
 * Event bubbling will be stopped at the responder.
 * This means simply adding a backdrop as TouchableOpacity for the entire app won't work.
 * Any other TouchableOpacity nested inside will swallow the event.
 *
 * <!> Further reading:
 * https://levelup.gitconnected.com/how-exactly-does-react-handles-events-71e8b5e359f2
 * https://***.com/questions/40572499/touchableopacity-swallow-touch-event-and-never-pass
 *
 * <!> Solution:
 * Touch events can be captured in the main view on mobile.
 * Clicks can be captured in the main view on web.
 * We combine these two data streams in one single pipeline.
 * All self closeable components subscribe to this data stream.
 * When a click is detected each component checks if it was triggered by it's own children.
 * If not, it self closes.
 *
 * A simpler solution (with significant drawbacks) would be:
 * https://www.jaygould.co.uk/2019-05-09-detecting-tap-outside-element-react-native/
 */

/** Combines both screen touches on mobile and clicks on web. */
export const selfCloseEvents$ = new Subject<SelfCloseEvent>();

export const screenTouched = (e: GestureResponderEvent) => 
    selfCloseEvents$.next(e);
;

export const screenClicked = (e: React.MouseEvent) => 
    selfCloseEvents$.next(e);
;

/**
 * If the current host component ancestors set contains the clicked element,
 * the click is inside of the currently verified component.
 */
export const detectClickIsOutside = (event: SelfCloseEvent, host: React.Component): boolean => 
    let hostTrace = getNodeSummary((host as any)._reactInternalFiber);
    let ancestorsTrace = traceNodeAncestors(event);
    let ancestorsTraceIds = ancestorsTrace.map(trace => trace.id);

    let clickIsOutside: boolean = !ancestorsTraceIds.includes(hostTrace.id);
    return clickIsOutside;
;

// ====== PRIVATE ======

/**
 * Tracing the ancestors of a component is VITAL to understand
 * if the click originates from within the component.
 */
const traceNodeAncestors = (event: SelfCloseEvent): AncestorNodeTrace[] => 
    let ancestorNodes: AncestorNodeTrace[] = [];
    let targetNode: DebugOwner = (event as any)._targetInst; // <!WARNING> Private props

    // Failsafe
    if (!targetNode)  return; 

    traceAncestor(targetNode);

    function traceAncestor(node: DebugOwner) 
        node && ancestorNodes.push(getNodeSummary(node));
        let parent = node._debugOwner;
        parent && traceAncestor(parent);
    

    return ancestorNodes;
;

const getNodeSummary = (node: DebugOwner): AncestorNodeTrace => 
    let trace: AncestorNodeTrace = 
        id: node._debugID,
        type: node.type && node.type.name,
        file: node._debugSource && node._debugSource.fileName,
    ;

    return trace;
;

interfaces/self-close.ts - 一些无聊的 typescript 接口来帮助项目维护。

import  NativeSyntheticEvent  from 'react-native';

/** Self Close events are all the taps or clicks anywhere in the UI. */
export type SelfCloseEvent = React.SyntheticEvent | NativeSyntheticEvent<any>;

/**
 * Interface representing some of the internal information used by React.
 * All these fields are private, and they should never be touched or read.
 * Unfortunately, there is no public way to trace parents of a component.
 * Most developers will advise against this pattern and for good reason.
 * Our current exception is an extremely rare exception.
 *
 * <!> WARNING
 * This is internal information used by React.
 * It might be possible that React changes implementation without warning.
 */
export interface DebugOwner 
    /** Debug ids are used to uniquely identify React components in the components tree */
    _debugID: number;
    type: 
        /** Component class name */
        name: string;
    ;
    _debugSource: 
        /** Source code file from where the class originates */
        fileName: string;
    ;
    _debugOwner: DebugOwner;


/**
 * Debug information used to trace the ancestors of a component.
 * This information is VITAL to detect click outside of component.
 * Without this script it would be impossible to self close menus.
 * Alternative "clean" solutions require polluting ALL components with additional custom triggers.
 * Luckily the same information is available in both React Web and React Native.
 */
export interface AncestorNodeTrace 
    id: number;
    type: string;
    file: string;

现在是有趣的部分。 dots-menu.tsx - 精简到示例的基本要素

import * as div from './dots-menu.style';
import  detectClickIsOutside, selfCloseEvents$  from '../../services/self-close-signal.service';
import  Subject  from 'rxjs';
// ... other imports

export class DotsMenu extends React.Component<Props, State> 

    private destroyed$ = new Subject<void>();

    constructor(props: Props) 
        // ...
    

    public render() 
        const  isExpanded  = this.state;

        return (
            <div.DotsMenu ...['more props here'] >

                /* Trigger */
                <DotsMenuItem expandMenu=() => this.toggleMenu() ...['more props here'] />

                /* Items */
                
                    isExpanded &&
                    // ... expanded option here
                

            </div.DotsMenu>
        );
    

    public componentDidMount() 
        this.subscribeToSelfClose();
    

    public componentWillUnmount() 
        this.destroyed$.next();
    

    private subscribeToSelfClose() 
        selfCloseEvents$.pipe(
            takeUntil(this.destroyed$),
            filter(() => this.state.isExpanded)
        )
            .subscribe(event => 
                let clickOutside = detectClickIsOutside(event, this);

                if (clickOutside) 
                    this.toggleMenu();
                
            );
    

    private toggleMenu() 
        // Toggle visibility and animation logic goes here
    


希望它也适用于您。 附言我是所有者,请随意使用这些代码示例。希望你会喜欢这个答案并查看Visual School 以获取未来的 React Native 教程。

【讨论】:

嗨阿德里安。请告诉我你在这个演示中使用了什么下拉菜单? 嗨,阿德里安,这看起来不错!这就是说,它是否适用于嵌套的TouchableOpacity?看来他们会吞下这个事件? @Velidan 下拉菜单是我项目的自定义构建。 @walidvb 我不记得了。我已经有一段时间没有研究这个问题了。 回想起来,我会说这是一个人为的解决方法,考虑到项目中的新开发人员调试的复杂性,我不会再次使用它。虽然我不确定解决这个确切问题的更好解决方案是什么。【参考方案5】:

你可以使用

   <View>
       <TouchableWithoutFeedback
           onPress=()=>
                 //do something
             
        style=position:'absolute',top:0 , right:0 , bottom:0 ,left:0/>
       <YourComp></YourComp>
    </View>

【讨论】:

【参考方案6】:

如here 所述,一个更简单的解决方案是检测菜单外触摸动作的开始,并在这种情况下关闭菜单。

请记住,要使其正常工作,第一个捕捉到触摸的View 应该占据全屏高度,并且应用内容和菜单应该在里面。这允许触摸事件正确级联。

例如:

    const [isOverflowMenuDisplayed, setOverflowMenuDisplayed] = useState(false)
    const [childrenIds, setChildrenIds] = useState([])

    const handleTouchShouldSetResponder = (event) => 
        // To be able to close the overflow menu, the content of the screen need to be inside this top view, and detect if the pressed view if the menu item or the app content
        if (childrenIds.length) 
            if (childrenIds.includes(event.target)) 
                return true
            
            setOverflowMenuDisplayed(false)
            return false
        
        return false
    
    

     
     return  <View
                onStartShouldSetResponder=handleTouchShouldSetResponder
                onMoveShouldSetResponder=handleTouchShouldSetResponder>
                <AppBar title=title onLeftIconPress=onLeftIconPress isCloseLeftIcon=isCloseLeftIcon>
                    actions
                    overflowAction && <AppBarActionOverflow onOpen=() => setOverflowMenuDisplayed(true) />
                </AppBar>

                <AppBarOverflowMenu
                    overflowAction=overflowAction
                    isOpen=isOverflowMenuDisplayed
                    childrenIds=childrenIds
                    setChildrenIds=setChildrenIds
                    onPress=() => setOverflowMenuDisplayed(false)
                />

                children
            </View>

还有溢出菜单:

export const AppBarOverflowMenu = ( isOpen, setChildrenIds, childrenIds, onPress, overflowAction ) => 
    if (!isOpen) 
        return null
    

    return (
        <View
            style=thisStyles.menuContainer
            ref=(component) => 
                if (component) 
                    const ids = component._children[0]._children.map((el) => el._nativeTag)
                    if (ids.length > 0 && (childrenIds.length !== ids.length || !childrenIds.includes(ids[0]))) 
                        setChildrenIds(ids)
                    
                
            >
            <View style=thisStyles.menu>
                React.cloneElement(overflowAction, 
                    onPress: () => 
                        onPress(false)
                        overflowAction.props.onPress()
                    ,
                )
            </View>
        </View>
    )


【讨论】:

以上是关于可以在本机反应中检测到视图组件吗?的主要内容,如果未能解决你的问题,请参考以下文章

检测何时触摸另一个视图 - 在本机反应中使用 PanResponder 拖动

Web视图点击:在android中未检测到url,iOS工作:本机反应

反应本机列表视图添加项目不起作用

如何在本机反应中向视图添加动态宽度和颜色?

如何在示例应用程序中使用 N 个视图进行本机反应

反应路由器本机渲染链接组件超过前一个