可以在React Native中检测出View Component的侧面吗?

use*_*203 6 javascript mobile reactjs react-native

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

谢谢

Adr*_*isa 6

我不会接受“不”的答案,因此我进行了很多挖掘以找到符合我需求的解决方案。

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

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

演示

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

在此输入图像描述

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

首先,我们需要捕获 UI 中的所有点击,无论是 Web 还是本机。看来这件事并不容易做到。嵌套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>
        );
    }
}
Run Code Online (Sandbox Code Playgroud)

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
 * /sf/ask/2840074961/
 *
 * <!> 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;
};
Run Code Online (Sandbox Code Playgroud)

Interfaces/self-close.ts - 一些无聊的打字稿接口,以帮助项目维护。

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;
}
Run Code Online (Sandbox Code Playgroud)

现在是有趣的部分。 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
    }

}
Run Code Online (Sandbox Code Playgroud)

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


小智 5

正如安德鲁所说:您可以使用 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
}
Run Code Online (Sandbox Code Playgroud)

...

    <View
      onStartShouldSetResponder={(evt) => true}
      onMoveShouldSetResponder={(evt) => true}
      onResponderGrant={this.handlePressIn}
      onResponderMove={this.handlePressIn}
      onResponderRelease={this.handlePressOut}
    >
         ...
    </View>
Run Code Online (Sandbox Code Playgroud)

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

  • 当我在滚动视图中执行操作时,它对我不起作用 (2认同)

And*_*nko 3

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

<TouchableWithoutFeedback 
  onPress={ /*handle tap outside of view*/ }
  style={ /* fullscreen styles */}
>
    <View>
     ...
    </View
</TouchableWithoutFeedback>
Run Code Online (Sandbox Code Playgroud)