我的 React 本机应用程序屏幕具有带有少量文本输入的 View 组件。如何在该视图之外的屏幕上检测到触摸?请帮忙。
谢谢
我的 React 本机应用程序屏幕具有带有少量文本输入的 View 组件。如何在该视图之外的屏幕上检测到触摸?请帮忙。
谢谢
正如安德鲁所说:您可以使用 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 是当用户按下并移动按下的位置时
把你的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 教程。
一个简单的解决方案,如规定在这里,是检测菜单的触摸动作之外的启动和关闭菜单在这种情况下。
请记住,要使其正常工作,第一个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>