開(kāi)啟新頁(yè)面-Andorid篇
最近在研究ReactNative,想用于新的項(xiàng)目開(kāi)發(fā),發(fā)現(xiàn)我們傳統(tǒng)的Android中開(kāi)Activity的方式?jīng)]有了,只能通過(guò)導(dǎo)航控制器來(lái)實(shí)現(xiàn),但是導(dǎo)航控制器本身又很難實(shí)現(xiàn)設(shè)計(jì)MM出的效果,故考慮自己寫(xiě)一個(gè)源生模塊來(lái)實(shí)現(xiàn)交互,然后導(dǎo)航控制器自定義就好了
前面
何謂開(kāi)啟新頁(yè)面呢?andorid中有兩種描述頁(yè)面的方式,一個(gè)是Activity,一種是Fragment
我這里的開(kāi)啟新頁(yè)面就是使用Intent的方式開(kāi)啟Activity
這里就是使用ReactNative(后稱(chēng)RN)開(kāi)啟新頁(yè)面
項(xiàng)目
項(xiàng)目地址
目前項(xiàng)目托管在oschina上,后續(xù)遷移到github
開(kāi)發(fā)環(huán)境
macos,用windows/linux的請(qǐng)自行探索相關(guān)的開(kāi)發(fā)步驟或者環(huán)境
node版本
npm版本
RN的版本
package.json 詳見(jiàn)截圖
使用WebStrom
開(kāi)發(fā)js
部分
AndroidStudio
開(kāi)發(fā)Android
的原生部分
思路分析
分析官網(wǎng)模塊的注入
首先肯定要先可以完成交互,再考慮如何去實(shí)現(xiàn)
官方文檔
android原生模塊
這里詳細(xì)解說(shuō)了如何使用js調(diào)用原生模塊
我們都知道ReactNative本身還是渲染js腳本來(lái)形成源生控件,而android必須要有一個(gè)Activity來(lái)作為載體
- 創(chuàng)建原生模塊
- 將原生模塊注入application
- 調(diào)用源生代碼
動(dòng)手寫(xiě)代碼
首先創(chuàng)建一個(gè)模塊
package com.sxwphone;
import android.app.Activity;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
/**
* Created by cai on 2017/7/13.
*/
public class StartNewHelper extends ReactContextBaseJavaModule {
public StartNewHelper(ReactApplicationContext reactContext) {
super(reactContext);
}
@ReactMethod
public void startNewActivity(String name) {
Activity activity = getCurrentActivity();
if (activity instanceof StartNewActivity) {
((StartNewActivity) activity).startNewActivity(name);
}
}
@Override
public String getName() {
return "startNew";
}
}
首先是一個(gè)模塊,這個(gè)模塊的名字是startNew
也就是getName()
中的返回值后面我們會(huì)用到它
這里吐槽自己一下,這類(lèi)名起的真爛,讓后續(xù)維護(hù)的人沒(méi)法用啊(實(shí)際項(xiàng)目中不要這樣隨意,否則會(huì)被罵死的)
這里的@ReactMethod
標(biāo)識(shí)的方法startNewActivity(String name)
就是后續(xù)js要用到的方法,這里記錄一下
關(guān)聯(lián)模塊
我們有了自己的模塊,得將它與項(xiàng)目關(guān)聯(lián)起來(lái)
首先需要?jiǎng)?chuàng)建一個(gè)ReactPackage,將模塊注入其中
package com.sxwphone;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ExampleReactPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new StartNewHelper(reactContext));
return modules;
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
這里在List<NativeModule> createNativeModules(ReactApplicationContext reactContext)
中將module加到集合里
這里還需要將package
加入到ReactNativeHost
中
package com.sxwphone;
import android.app.Application;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import java.util.Arrays;
import java.util.List;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.asList(new MainReactPackage(), new ExampleReactPackage());
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
這個(gè)是application的代碼,其中有一個(gè)ReactNativieHost
,將我們的ExampleReactPackage
加入到List<ReactPackage> getPackages()
創(chuàng)建的集合中,這樣我們就完成了Native模塊的注入
js調(diào)用
到了這里我們就可以通過(guò)js調(diào)用到方法了
/**
* Sample React Native App
* https://github.com/facebook/react-native
* @flow
*/
import React, { Component } from 'react';
import {
AppRegistry, Button,
StyleSheet,
Text,
View
} from 'react-native';
import {NativeModules} from 'react-native';
export default class sxwphone extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
<Text style={styles.instructions}>
To get started, edit index.android.js
</Text>
<Text style={styles.instructions}>
Double tap R on your keyboard to reload,{'\n'}
Shake or press menu button for dev menu
</Text>
<Button title={'點(diǎn)擊'} onPress={() => this.newPage()}/>
</View>
);
}
newPage() {
var startHelper = NativeModules.startNew;
startHelper.startNewActivity("abc")
}
// newPage() {
//
// }
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
AppRegistry.registerComponent('sxwphone', () => sxwphone);
AppRegistry.registerComponent('abc', () => sxwphone);
這里我是比較懶,沒(méi)有寫(xiě)多個(gè)模塊,等于是同一個(gè)頁(yè)面,只是開(kāi)在不同的兩個(gè)Activity上了
這個(gè)時(shí)候運(yùn)行應(yīng)用
Activity
package com.sxwphone;
/**
* Created by cai on 2017/7/13.
*/
public interface StartNewActivity {
void startNewActivity(String name);
String getMainComponentName();
}
MainActivity:
@Override
public void startNewActivity(String name) {
Intent intent = new Intent(this, NewActivity.class);
intent.putExtra(NewActivity.NAME, name);
startActivity(intent);
}
NewActivity:
@Nullable
@Override
public String getMainComponentName() {
if (getIntent() == null) {
return null;
}
String name = getIntent().getStringExtra(NAME);
if (name == null || name.isEmpty()) {
finish();
return "";
}
return name;
}
這里我重寫(xiě)了getMainComponentName()
方法,不直接返回字符串了,返回一個(gè)從上個(gè)頁(yè)面?zhèn)鱽?lái)的值也就是abc
這樣應(yīng)該可以調(diào)用到對(duì)應(yīng)的模塊了吧
接下來(lái)運(yùn)行吧
運(yùn)行
運(yùn)行andorid,發(fā)現(xiàn)崩潰了,崩潰了....
查下原因:
NoFountActivity
哦哦 沒(méi)注冊(cè)Activity啊 打開(kāi)AndoridManifest.xml注冊(cè)下
這個(gè)時(shí)候以為結(jié)束了?太天真了!!!
發(fā)現(xiàn)這時(shí)候新頁(yè)面打開(kāi)了,咋是空白一片呢?
這個(gè)時(shí)候就要考慮問(wèn)題出在哪里了呢
解決方案
我們MainActivity直接返回模塊名就成功了,為啥這里不成功呢,Intent的傳遞一定沒(méi)錯(cuò),那么錯(cuò)在哪里呢
這個(gè)時(shí)候應(yīng)該想如何去解決這樣的問(wèn)題了,為啥會(huì)空白一片呢
我們考慮是不是調(diào)用時(shí)機(jī)出現(xiàn)了問(wèn)題呢?
查看Android代碼
打開(kāi)MainActivity
,發(fā)現(xiàn)MainActivity
是繼承自ReactActivity
的
這時(shí)候打開(kāi)ReactActivity
發(fā)現(xiàn)是直接繼承自Activity的,那么具體實(shí)現(xiàn)就在這里了
onCreate
方法中有一個(gè)delegate,我們發(fā)現(xiàn)所有的Activity生命周期方法都和delegate關(guān)聯(lián)了
具體實(shí)現(xiàn)查看下delegate 是ReactActivityDelegate
ReactActivityDelegate
private final @Nullable String mMainComponentName;
public ReactActivityDelegate(Activity activity, @Nullable String mainComponentName) {
mActivity = activity;
mMainComponentName = mainComponentName;
mFragmentActivity = null;
}
public ReactActivityDelegate(
FragmentActivity fragmentActivity,
@Nullable String mainComponentName) {
mFragmentActivity = fragmentActivity;
mMainComponentName = mainComponentName;
mActivity = null;
}
protected void onCreate(Bundle savedInstanceState) {
boolean needsOverlayPermission = false;
if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Get permission to show redbox in dev builds.
if (!Settings.canDrawOverlays(getContext())) {
needsOverlayPermission = true;
Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getContext().getPackageName()));
FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
Toast.makeText(getContext(), REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
((Activity) getContext()).startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE);
}
}
if (mMainComponentName != null && !needsOverlayPermission) {
loadApp(mMainComponentName);
}
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
}
這里會(huì)發(fā)現(xiàn)我們?cè)贛ainActivity中實(shí)現(xiàn)的mMainComponentName就是被用到這里了
if (mMainComponentName != null && !needsOverlayPermission) {
loadApp(mMainComponentName);
}
而且這個(gè)是一個(gè)final字段,我們不能改寫(xiě),而這個(gè)name又是在Activity的構(gòu)造方法中傳入的
作為多年的Android程序員,我們知道Activity這個(gè)東西是由ActivityThread創(chuàng)建的,這個(gè)時(shí)候Intent還沒(méi)生效呢呢,而構(gòu)造方法又必然首先運(yùn)行,所以運(yùn)行順序是
Activity 構(gòu)造方法->Activity.getMainComponentName()->null
這里如果就這樣運(yùn)行的話(huà),無(wú)論如何也只能獲得空,我們必須讓loadApp可以運(yùn)行,且componentName不是空
這里牽扯到兩種寫(xiě)法,我在onCreate前調(diào)用Intent,獲取到Component的名字,通過(guò)暴力反射的方式,修改名稱(chēng),這里可以這樣寫(xiě),但是我想了一下,放棄了,反射影響效率,而且代碼不優(yōu)雅,所以考慮使用別的方案解決
這里查看下loadApp的調(diào)用,發(fā)現(xiàn)onActivityResult()中也有執(zhí)行
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager()
.onActivityResult(getPlainActivity(), requestCode, resultCode, data);
} else {
// Did we request overlay permissions?
if (requestCode == REQUEST_OVERLAY_PERMISSION_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Settings.canDrawOverlays(getContext())) {
if (mMainComponentName != null) {
loadApp(mMainComponentName);
}
Toast.makeText(getContext(), REDBOX_PERMISSION_GRANTED_MESSAGE, Toast.LENGTH_LONG).show();
}
}
}
}
這里是為什么呢?
這就牽扯到Android6.0的運(yùn)行時(shí)權(quán)限了
這里如果檢查到運(yùn)行時(shí)權(quán)限沒(méi)通過(guò),就需要到activityResult中再執(zhí)行加載界面的代碼
MyReactActivityDelegate
既然我們無(wú)法復(fù)寫(xiě)final的方法,那就需要我們創(chuàng)建自己的Delegate
繼承ReactActivityDelegate
public class MyReactActivityDelegate extends ReactActivityDelegate {
private final Activity activity;
private final String firstMainComponentName;
private static final String REDBOX_PERMISSION_GRANTED_MESSAGE =
"Overlay permissions have been granted.";
public MyReactActivityDelegate(Activity activity, @Nullable String mainComponentName) {
super(activity, mainComponentName);
this.activity = activity;
firstMainComponentName = mainComponentName;
}
public MyReactActivityDelegate(FragmentActivity fragmentActivity, @Nullable String mainComponentName) {
super(fragmentActivity, mainComponentName);
this.activity = fragmentActivity;
firstMainComponentName = mainComponentName;
}
private boolean isLoadApp = false;
public boolean isLoadApp() {
return isLoadApp;
}
@Override
protected void loadApp(String appKey) {
if (activity instanceof StartNewActivity) {
if (isLoadApp()) {
return;
}
String mainComponentName = ((StartNewActivity) activity).getMainComponentName();
super.loadApp(mainComponentName);
isLoadApp = true;
} else {
super.loadApp(appKey);
}
}
private static final String REDBOX_PERMISSION_MESSAGE =
"Overlay permissions needs to be granted in order for react native apps to run in dev mode";
private static final int REQUEST_OVERLAY_PERMISSION_CODE = 1111;
@Override
protected void onCreate(Bundle savedInstanceState) {
String mMainComponentName = null;
if (activity instanceof StartNewActivity) {
mMainComponentName = ((StartNewActivity) activity).getMainComponentName();
}
boolean needsOverlayPermission = false;
if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Get permission to show redbox in dev builds.
if (!Settings.canDrawOverlays(activity)) {
needsOverlayPermission = true;
}
}
if (mMainComponentName != null && !needsOverlayPermission) {
loadApp(mMainComponentName);
return;
}
super.onCreate(savedInstanceState);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
String mMainComponentName = null;
if (activity instanceof StartNewActivity) {
mMainComponentName = ((StartNewActivity) activity).getMainComponentName();
}
if (getReactNativeHost().hasInstance()) {
getReactNativeHost().getReactInstanceManager()
.onActivityResult((Activity) getContext(), requestCode, resultCode, data);
} else {
// Did we request overlay permissions?
if (requestCode == REQUEST_OVERLAY_PERMISSION_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Settings.canDrawOverlays(getContext())) {
if (firstMainComponentName != null) {
loadApp(firstMainComponentName);
} else if (mMainComponentName != null) {
loadApp(mMainComponentName);
}
Toast.makeText(getContext(), REDBOX_PERMISSION_GRANTED_MESSAGE, Toast.LENGTH_LONG).show();
}
}
}
}
protected Context getContext() {
return activity;
}
}
這里將activity和原始的componentName都作為成員變量寫(xiě)了下來(lái),方便后面的調(diào)用
在onCreate的時(shí)候檢查權(quán)限和當(dāng)時(shí)的方法中獲取的名字,如果不是空,則loadApp
activityResult中同理,如果最初的name不為空,則加載最初的名字,如果為空,則繼續(xù)判斷方法中獲取的名字
接著修改NewActivity
package com.sxwphone;
import android.content.Intent;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import javax.annotation.Nullable;
/**
* Created by cai on 2017/7/13.
*/
public class NewActivity extends ReactActivity implements StartNewActivity {
public static final String NAME = "_name";
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new MyReactActivityDelegate(this, getMainComponentName());
}
@Nullable
@Override
public String getMainComponentName() {
if (getIntent() == null) {
return null;
}
String name = getIntent().getStringExtra(NAME);
if (name == null || name.isEmpty()) {
finish();
return "";
}
return name;
}
@Override
public void startNewActivity(String name) {
Intent intent = new Intent(this, NewActivity.class);
intent.putExtra(NewActivity.NAME, name);
startActivity(intent);
}
}
運(yùn)行
發(fā)現(xiàn)點(diǎn)擊按鈕就可以調(diào)用到新模塊了
修改完的最終代碼查看 項(xiàng)目地址
總結(jié)
這個(gè)項(xiàng)目沒(méi)用多長(zhǎng)時(shí)間就完成了,但是可以說(shuō)初窺了RN和android的交互,RN的模塊如何注入,其中牽扯到了一些android的知識(shí),RN的一部分知識(shí),可能對(duì)于RN+android老手來(lái)說(shuō)沒(méi)什么,但是我想對(duì)于RN新手或者前端轉(zhuǎn)android的人來(lái)說(shuō)還算可以看看的文章