汪建军的博客

休迅飞凫,飘忽若神,凌波微步,罗袜生尘。

Android developer, sometimes thinking, sometimes try it.


React Native原生页面与React页面之间的跳转

本文内容以及所提到的类均基于上一篇博客,由上一篇可知MainActivity相当于index.js的一个容器,index.js主要就一句AppRegistry.registerComponent('AwesomeProject', () => App);注册了App.js组件,而App.js内容是单个页面,运行Android程序打开MainActivity,看到的内容是App.js的内容。因此,某种角度上可以理解为App.js相当于MainActivity的View,MainActivity就是装载React页面的壳。实际上,react-native绘制view的思路是将js写的控件解析成native的对应控件,这些控件是封装原生控件的,如ReactTextView、ReactImageView,在com.facebook.react.views包下。一个应用不止一个页面,有多个React页面也可能有多个原生页面(即Activity),这些页面直接的相互跳转是本文所关心的问题。

React页面之间的相互跳转

这里简单的介绍一下基于导航器进行React页面跳转,这是官方文档

第一步,创建第二个React页面

在AwesomeProject/目录下新建一个ReactPage2.js文件,内容和App.js类似,随意写点布局内容,我们加一个接收参数的例子吧

...
class ReactPage2 extends React.Component {  
  static navigationOptions = ({ navigation }) => ({
    title: `标题 ${navigation.state.params.title}`,
  });
  render() {
    const { params } = this.props.navigation.state;
    return (
      <View>
        <Text>传过来key=title的参数:{params.title}</Text>
      </View>
    );
  }
}

这里的navigationOptions可以理解为Android里的AndroidManifest.xml里activity标签的android:theme

第二步,创建导航器

在AwesomeProject/目录下新建一个Navigation.js文件,内容如下:

import {  
    StackNavigator,
} from 'react-navigation';

import App from './App';  
import ReactPage2 from './ReactPage2';

const stackNav = StackNavigator({  
    App: {screen: App},
    ReactPage2: {screen: ReactPage2}
},
    {
        navigationOptions: ({navigation}) => ({
            headerStyle: {
                backgroundColor: '#E56646',
            },
            headerTitleStyle: {
                color: 'white',
                marginHorizontal: 0,
                alignSelf: 'center',
            },
            headerBackTitleStyle: {
                color: 'white',
            },
            headerTintColor: '#fff',
        }),
    });

export default stackNav;  

这里navigationOptions是页面的统一样式配置,可以理解为Android里的AndroidManifest.xml里application标签的android:theme

第三步,修改index.js

将原先注册App改为注册Navigation

import { AppRegistry } from 'react-native';  
import Navigation from './Navigation';

AppRegistry.registerComponent('AwesomeProject', () => Navigation);  

如此打开应用还是会看到App.js的内容,因为会默认显示Navigation.js里StackNavigator的第一个

第四步,修改App.js

在App里加个按钮,跳转到ReactPage2

...
render() {  
        const { navigate } = this.props.navigation;
        return (
            <View style={styles.container}>
                ...
                <Button
                    title="跳转到ReactPage2"
                    onPress={() => navigate('ReactPage2', { title: '子页面' })} />
            </View>
        )
    }
...

至此,就实现了从React的一个页面跳转到另一个页面,并传递一个参数,当然还可以通过加一个callback参数实现类似Android原生Activity里的onActivityResult方法。

React页面跳转到原生页面

其实很简单,相当于调用原生方法,我们封装一下,在MyNativeModule.java里添加如下方法

    /**
     * js页面跳转到activity 并传数据
     */
    @ReactMethod
    public void startActivityByString(String activityName, String title) {
        try {
            Activity currentActivity = getCurrentActivity();
            if (null != currentActivity) {

                Class aimActivity = Class.forName(activityName);
                Intent intent = new Intent(currentActivity, aimActivity);
                intent.putExtra("title", title);
                currentActivity.startActivity(intent);

            }
        } catch (Exception e) {
            throw new JSApplicationIllegalArgumentException(
                    "Could not open the activity : " + e.getMessage());
        }
    }

React调用举例:

...
render() {  
        return (
            <View style={styles.container}>
                ...
                <Button
                    title="跳转到ReactPage2"
                    onPress={() => 
                         NativeModules.MyNativeModule.startActivityByString("com.awesomeproject.MainNativeTestActivity", "标题")
                            } />
            </View>
        )
    }
...

原生页面跳转到React页面

我们说React页面都被装载在了MainActivity这个壳里,所以原生页面跳转到React页面,通过

Intent intent = new Intent(activity, MainActivity.class);  
activity.startActivity(intent);  

是可以进入React页面的。

我们可以封装一下,带上参数,使其通过navigation跳转到不同的React页面。

还是在MyNativeModule.java里加一个方法

    /**
     * native页面跳转到rn 并传数据
     */
    @ReactMethod
    public void getDataFromIntent(Callback successBack, Callback errorBack) {
        try {
            Activity currentActivity = getCurrentActivity();
            Toast.makeText(currentActivity, currentActivity.getClass().getName(), Toast.LENGTH_SHORT).show();
            String result = currentActivity.getIntent().getStringExtra("result");
            if (TextUtils.isEmpty(result)) {
                result = "No Data";
            }
            successBack.invoke(result);
        } catch (Exception e) {
            errorBack.invoke(e.getMessage());
        }
    }

然后原生页面如下调用

                Intent intent = new Intent(context, MainActivity.class);
                intent.putExtra("result", "ReactPage2");
                startActivity(intent);

然后在App.js里添加如下监听

...
export default class App extends Component<{}> {

    constructor(props) {
        super(props);
        this.state = {
            TEXT: 'Input Text',
        }
    }
    componentDidMount() { //这是React的生命周期函数,会在界面加载完成后执行一次
        NativeModules.MyNativeModule.getDataFromIntent(
            (successMsg) => {
                this.setState({TEXT: successMsg,}); //状态改变的话重新绘制界面
                if (successMsg === "ReactPage2") {
                    const { navigate } = this.props.navigation;
                    navigate('ReactPage2')
                }
            },
            (errorMsg) => {
                alert(errorMsg)
            }
        );
    }
....

至此就实现回到React模块并跳转到了想要的页面。

这种做法有一个弊端,就是要先回到App,再由App跳转到指定页面,如果navigationOptions设置了页面跳转动画那效果就不太理想了。我们可以通过再注册一个原生界面壳来解决,下面说说步骤。

第一步,修改index.js,注册两个组件

import { AppRegistry } from 'react-native';  
import Navigation from './Navigation';  
import MyReactActivity from './MyReactActivity';

AppRegistry.registerComponent('AwesomeProject', () => Navigation);  
AppRegistry.registerComponent('hello', () => MyReactActivity);  

第二步,新建一个原生容器SecondActivity

/**
 * 第二个AppRegistry.registerComponent('hello', () => MyReactActivity);
 * 要注意一点是如果偷懒将SecondActivity同MainActivity一样简单继承ReactActivity的话,问题就来了,你会发现第一次跳转是没问题的,但再次回到MainActivity时点击界面会不响应。
 * 这是因为ReactInstanceManager没有重新创建,两个activity共用一个。这样当关闭一个时,ReactInstanceManager就会被destroy,另外一个自然就不会有界面响应了。
 * Created by wang on 2018/2/26.
 */
public class SecondActivity extends Activity implements DefaultHardwareBackBtnHandler {  
    private ReactRootView mReactRootView;
    private ReactInstanceManager mReactInstanceManager;

    @Override
    public void invokeDefaultOnBackPressed() {
        super.onBackPressed();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mReactRootView = new ReactRootView(this);
        mReactInstanceManager = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setBundleAssetName("index.android.bundle") // assets目录下的bundle文件名
                .setJSMainModulePath("index") // 入口index.js
                .addPackages(Arrays.asList(
                        new MainReactPackage(),
                        new MyReactPackage()))//自定义的ReactPackage
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();
        // 指定getMainComponentName
        mReactRootView.startReactApplication(mReactInstanceManager, "hello", null);
        setContentView(mReactRootView);
    }

    @Override
    protected void onPause() {
        super.onPause();

        if (mReactInstanceManager != null) {
            mReactInstanceManager.onHostPause();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();

        if (mReactInstanceManager != null) {
            mReactInstanceManager.onHostResume(this, this);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        if (mReactInstanceManager != null) {
            mReactInstanceManager.onHostDestroy();
        }
    }

    @Override
    public void onBackPressed() {
        if (mReactInstanceManager != null) {
            mReactInstanceManager.onBackPressed();
        } else {
            super.onBackPressed();
        }
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) {
            mReactInstanceManager.showDevOptionsDialog();
            return true;
        }
        return super.onKeyUp(keyCode, event);
    }
}

这里使用了ReactRootView这个类,这是将RN作为一个视图/片段,类似于Android里的fragment,可以实现同一个界面部分是原生控件,部分是RN内容,相关官方文档

第三步,修改MyReactActivity并接收传参

import React, {Component} from 'react';  
import {  
    StyleSheet,
    NativeModules,
    TextInput,
    View,
} from 'react-native';

export default class MyReactActivity extends React.Component {  
    constructor(props) {
        super(props);
        this.state = {
            TEXT: 'Input Text',
        };
    }

    render() {
        return (
            <View style={styles.container}>
                <TextInput
                    style={styles.welcome}
                    onChangeText={(text) => this.setState({text})}
                    value={this.state.TEXT}/>
            </View>
        )
    }

    componentDidMount() {
        NativeModules.MyNativeModule.getDataFromIntent(
            (successMsg) => {
                this.setState({TEXT: successMsg,});
            },
            (errorMsg) => {
                alert(errorMsg)
            }
        );
    }
}

const styles = StyleSheet.create({  
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#F5FCFF',
    },
    welcome: {
        fontSize: 20,
        textAlign: 'center',
        margin: 10,
    },
});

这里还是通过MyNativeModule.getDataFromIntent方法接收传参

第四步,原生页面调用

还是像前面说的,原生页面跳转到容器页面即可

Intent intent = new Intent(context, SecondActivity.class);  
intent.putExtra("result", "Hello React Native");  
startActivity(intent);  

getDataFromIntent方法我加了一行Toast.makeText(currentActivity, currentActivity.getClass().getName(), Toast.LENGTH_SHORT).show();可以发现之前的跳转,弹出的是"com.awesomeproject.MainActivity",而这里弹出的是"com.awesomeproject.SecondActivity",而TextInput也顺利的显示为"Hello React Native"。

至此就可以实现原生页面跳转到另外一个独立的React页面。

感谢

RN之Android:原生界面与React界面的相互调用及数据传递

禁止商业用途转载,其他转载请告知,谢谢!

更早的文章

React Native原生模块与React模块之间的数据传递

我眼中的React Native 关于React Native的介绍与评价网上随处可见,比如知乎,初探RN,在我看来,RN是一种新的开发模式,前端语言写界面,写一次多端可用(Android、Ios、微…

前端继续阅读
comments powered by Disqus