React Native - 设备后退按钮处理

React Native - Device back button handling

我想检查当按下设备后退按钮时堆栈上是否有多个屏幕。如果是,我想显示上一个屏幕,如果不是,我想退出应用程序。

我检查了一些例子,但那些使用 BackAndroidNavigator。但是他们两个都被弃用了。 BackHandlerBackAndroid 的替代品。我可以使用 props.navigation.goBack(null).

显示上一个屏幕

但我找不到用于在堆栈中查找屏幕数的代码。我不想使用已弃用的 Navigator!

此示例将向您展示大多数流程中通常预期的后退导航。您必须根据预期行为将以下代码添加到每个屏幕。有2种情况: 1. 如果堆栈中有超过 1 个屏幕,设备后退按钮将显示上一个屏幕。 2.如果堆栈上只有1个屏幕,设备返回按钮将退出应用程序。

案例 1:显示上一个屏幕

import { BackHandler } from 'react-native';

constructor(props) {
    super(props)
    this.handleBackButtonClick = this.handleBackButtonClick.bind(this);
}

componentWillMount() {
    BackHandler.addEventListener('hardwareBackPress', this.handleBackButtonClick);
}

componentWillUnmount() {
    BackHandler.removeEventListener('hardwareBackPress', this.handleBackButtonClick);
}

handleBackButtonClick() {
    this.props.navigation.goBack(null);
    return true;
}

重要提示:不要忘记在构造函数中绑定方法并在 componentWillUnmount 中删除侦听器。

情况二:退出应用

在这种情况下,无需在要退出应用程序的屏幕上处理任何内容。

重要提示:这应该只是堆栈上的屏幕。

constructor(props){
    super(props)
    this.onBackPress = this.onBackPress.bind(this);
}

componentWillMount() {
        BackHandler.addEventListener('hardwareBackPress', this.onBackPress);

}

componentWillUnmount(){
    BackHandler.removeEventListener('hardwareBackPress', this.onBackPress);
}

onBackPress(){
    const {dispatch, nav} = this.props;
    if (nav.index < 0) {
        return false;
    }
    dispatch(NavigationActions.back());
    return true;
}

render(){
    const {dispatch, nav} = this.props;
    return(
        <DrawerRouter
            navigation= {
                addNavigationHelpers({
                    dispatch,
                    state: nav,
                    addListener,
                })
            }
        />
    );
}

如果堆栈中堆叠了多个屏幕,react-native 中的默认后退按钮行为是导航回堆栈中的前一个屏幕。当只有一个屏幕退出应用程序时处理设备后退按钮按下需要自定义设置。然而,这可以通过修改特定 StackNavigator 路由器的 getStateForAction 方法来实现,而无需向每个屏幕添加返回处理代码。

假设您在应用程序中使用了以下 StackNavigator

const ScreenStack = StackNavigator(
  {
    'Screen1': {
      screen: Screen1
    },
    'Screen2': {
      screen: Screen2
    },
  },
  {
    initialRouteName: 'Screen1'
  }
);

stack navigator 路由器的 getStateForAction 方法可以修改如下,以实现预期的返回行为。

const defaultStackGetStateForAction =
  ScreenStack.router.getStateForAction;

ScreenStack.router.getStateForAction = (action, state) => {
  if(state.index === 0 && action.type === NavigationActions.BACK){
    BackHandler.exitApp();
    return null;
  }

  return defaultStackGetStateForAction(action, state);
};

仅当堆栈中有一个屏幕时,state.index 变为 0

我在使用 react-native 的 v0.46.0 并且有同样的问题。我在 react-native 代码库

中追踪到这个文件的问题

https://github.com/facebook/react-native/blob/master/Libraries/Utilities/BackHandler.android.js#L25

当 运行 chrome 调试器关闭线路时

var subscriptions = Array.from(_backPressSubscriptions.values()).reverse()

总是returns一个用于订阅的空数组,这反过来会导致 invokeDefault 变量保持为真并调用 .exitApp() 函数。

经过更多调查,我认为这个问题在下面 PR #15182 中被发现和讨论。

即使在 copy/pasting 旧版本 RN 中的 PR 更改后,它也无法正常工作,这很可能是由 PR 中描述的问题引起的。

经过一些非常轻微的修改后,我通过更改为

使其正常工作
RCTDeviceEventEmitter.addListener(DEVICE_BACK_EVENT, function() {
  var invokeDefault = true;
  var subscriptions = []
  _backPressSubscriptions.forEach(sub => subscriptions.push(sub))

  for (var i = 0; i < subscriptions.reverse().length; ++i) {
    if (subscriptions[i]()) {
      invokeDefault = false;
      break;
    }
  }

  if (invokeDefault) {
    BackHandler.exitApp();
  }
});

简单地使用 .forEach,它是 PR 上的原始实现,然后修改后的 Array.from 语法在整个过程中起作用。

所以你可以 fork react-native 并使用一个修改后的版本,提交一个 PR,虽然我想这需要一点时间才能被批准并合并到上游,或者你可以做一些类似于我所做的事情覆盖 hardwareBackPress 事件的 RCTDeviceEventEmitter.addListener(...)。

// other imports
import { BackHandler, DeviceEventEmitter } from 'react-native'

class MyApp extends Component {
  constructor(props) {
    super(props)
    this.backPressSubscriptions = new Set()
  }

  componentDidMount = () => {
    DeviceEventEmitter.removeAllListeners('hardwareBackPress')
    DeviceEventEmitter.addListener('hardwareBackPress', () => {
      let invokeDefault = true
      const subscriptions = []

      this.backPressSubscriptions.forEach(sub => subscriptions.push(sub))

      for (let i = 0; i < subscriptions.reverse().length; i += 1) {
        if (subscriptions[i]()) {
          invokeDefault = false
          break
        }
      }

      if (invokeDefault) {
        BackHandler.exitApp()
      }
    })

    this.backPressSubscriptions.add(this.handleHardwareBack)
  }

  componentWillUnmount = () => {
    DeviceEventEmitter.removeAllListeners('hardwareBackPress')
    this.backPressSubscriptions.clear()
  }

  handleHardwareBack = () => { /* do your thing */ }

  render() { return <YourApp /> }
}

我用助焊剂导航。

    const RouterComp = () => {

    let backLoginScene=false;

    return (

        <Router
        backAndroidHandler={() => {
            const back_button_prohibited = ['login','userInfo','dashboard'];
            if (back_button_prohibited.includes(Actions.currentScene) ) {
                if (backLoginScene == false) {
                    ToastAndroid.show("Click back again to exit.", ToastAndroid.SHORT);
                    backLoginScene = !backLoginScene;
                    setTimeout(() => {
                        backLoginScene = false;
                    }, 2000);
                    return true;
                } else {
                    backLoginScene = false;
                    BackHandler.exitApp();
                }
                return false;
            }}}>
            <Scene key='root' hideNavBar>
                <Scene key='guest' hideNavBar >
                    <Scene key='login' component={Login} ></Scene>
                    <Scene key='userInfo' component={UserInfo}></Scene>
                </Scene>

                <Scene key='user' hideNavBar>
                    <Scene key='dashboard' component={Dashboard} title='Dashboard' initial />
                    <Scene key='newAd' component={NewAd} title='New Ad' />

                </Scene>



            </Scene>
        </Router>
    )
}

export default RouterComp;

试试这个 反应导航

componentDidMount() {
        BackHandler.addEventListener('hardwareBackPress', this.handleBackButton);
    }


    handleBackButton = () => {

        const pushAction = StackActions.push({
            routeName: 'DefaultSelections',
        });

        this.props.navigation.dispatch(pushAction);
    }

当前屏幕是 "DefaultSelections" ,在按下后退按钮时,将转移到相同的屏幕上,因此后退按钮禁用解决方法,因为通过

禁用后退按钮
return true

用于后退按钮(如官方文档所建议)禁用所有屏幕上的后退按钮;不需要

以下是我如何使用特定条件成功实施的:

componentWillMount() {
    BackHandler.addEventListener(
      'hardwareBackPress',
      this.handleBackButtonClick,
    );
  }

  componentWillUnmount() {
    BackHandler.removeEventListener(
      'hardwareBackPress',
      this.handleBackButtonClick,
    );
  }

  handleBackButtonClick = () => {
    //some condition
    if (this.state.isSearchBarActive) {
      this.setState({
        isSearchBarActive: false,
      });
      this.props.navigation.goBack(null);
      return true;
    }
    return false;
  };
 import { BackHandler } from 'react-native';
  
 constructor() {
        super();           
        this.handleBackButtonClick = this.handleBackButtonClick.bind(this);
 }

   componentWillMount() {
       BackHandler.addEventListener('hardwareBackPress', this.handleBackButtonClick);
   }

   componentWillUnmount() {
       BackHandler.removeEventListener('hardwareBackPress', this.handleBackButtonClick);
   }

   handleBackButtonClick() {
       //this.props.navigation.goBack(null);
       BackHandler.exitApp();
       return true;
   }

   handleBackButtonClick() {
       return true;   // when back button don't need to go back 
   }

在功能组件中

import { BackHandler } from 'react-native';

function handleBackButtonClick() {
    navigation.goBack();
    return true;
  }

  useEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', handleBackButtonClick);
    return () => {
      BackHandler.removeEventListener('hardwareBackPress', handleBackButtonClick);
    };
  }, []);

在功能组件中:

import { BackHandler } from 'react-native';

function handleBackButtonClick() {
    navigation.goBack();
    return true;
  }

  useEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', handleBackButtonClick);
    return () => {
      BackHandler.removeEventListener('hardwareBackPress', handleBackButtonClick);
    };
  }, []);

效用函数可能非常有用:

backPressHandler.js

import React from 'react';
import {BackHandler} from 'react-native';
const onBackPress = (callback) => {
  BackHandler.addEventListener('hardwareBackPress', callback);
  return () => {
    BackHandler.removeEventListener('hardwareBackPress', callback);
  };
};

export {onBackPress};

现在在我的屏幕上:

myScreen.js

import {onBackPress} from '../utils/backPressHandler';

  function handleBackPress() {
    navigation.goBack();
    return true;
  }
  useEffect(() => {
    onBackPress(handleBackPress);
  }, []);

React Native Hooks 有一个很好的 useBackHandler 挂钩,它简化了为 Android 后退按钮设置事件侦听器的过程。

import { useBackHandler } from '@react-native-community/hooks'

useBackHandler(() => {
  if (shouldBeHandledHere) {
    // handle it
    return true
  }
  // let the default thing happen
  return false
})

在功能组件中使用 like

import { BackHandler } from 'react-native'; **---imports like here**

  **--click function call like below--**
 handleBackButtonClick = () => {
    navigation.goBack();
    return true;
  }

  useEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', handleBackButtonClick);
    return () => {
      BackHandler.removeEventListener('hardwareBackPress', handleBackButtonClick);
    };
  }, []);

就是这样。享受你的编码...