可以在 React Native 中检测到视图组件的触摸吗?

IT技术 javascript reactjs mobile react-native
2021-03-24 14:49:53

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

谢谢

6个回答

正如安德鲁所说:您可以使用 TouchableWithoutFeedback 包装您的视图,并添加一个 onPress 您可以检测到何时点击视图。

实现这一目标的另一种方法是从视图中响应触摸事件

 /* 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 是当用户按下并移动按下的位置时

当我在滚动视图中时,它对我不起作用
2021-05-29 14:49:53

把你的View里面TouchableWithoutFeedback,展开TouchableWithoutFeedback全屏并添加onPress处理程序。

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

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

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

这个问题完美地说明了需要。

演示

这是最终结果。单击除组件本身之外的任何地方都会折叠它。

在此处输入图片说明

警告 该解决方案包括使用私有 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 此服务旨在检测应用程序屏幕上的所有点击。我在整个应用程序中使用react式编程,所以这里使用了 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://stackoverflow.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
    }

}

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

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

一个简单的解决方案,如规定在这里,是检测菜单的触摸动作之外的启动和关闭菜单在这种情况下。

请记住,要使其正常工作,第一个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>
    )
}

您可以使用

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