可以触摸侧视图组件被检测到反应原生?

时间:2016-10-17 08:30:36

标签: javascript reactjs mobile react-native

My React原生应用程序屏幕具有几个文本输入的View组件。如何在View外的屏幕上检测到触摸?请帮忙。

由于

6 个答案:

答案 0 :(得分:2)

作为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>

格兰特和移动之间的区别在于格兰特就在用户按下时,而移动是在用户按下并移动按下位置时

答案 1 :(得分:2)

我不会拒绝,所以我花了很多时间来找到符合我需要的解决方案。

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

This Question完美地说明了这一需求。

演示

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

enter image description here

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

首先,我们需要捕获用户界面中针对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>
        );
    }
}

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://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 -一些无聊的打字稿界面,有助于项目维护。

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教程。

答案 2 :(得分:1)

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

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

答案 3 :(得分:0)

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>
    )
}

答案 4 :(得分:-1)

您可以使用

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

答案 5 :(得分:-2)

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

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