diff --git a/docs/about.md b/docs/about.md index 34dcc77..449fd16 100644 --- a/docs/about.md +++ b/docs/about.md @@ -8,7 +8,7 @@ React Native及相关标识版权由©Facebook Inc.所有。本网站并非隶 ## 需要帮助? ## -### 免费QQ群 ### +### QQ群 ### “React Native中文网”的维护人员和其他参与者都在 > 1群: 439022088(已满) @@ -18,14 +18,19 @@ React Native及相关标识版权由©Facebook Inc.所有。本网站并非隶 5群: 138811944(已满) 6群: 446548027(已满) 7群: 115487854(已满) -8群: 317323710 - +8群: 317323710(已满) +9群: 128731649 + 讨论交流。所有群里都会经常发布许多新的教程和资讯。 ### 付费技术支持 ### 需要量身订做、随叫随到的完善技术支持?请联络[QQ:402740419](tencent://message/?uin=402740419&Site=react-native.cn&Menu=yes) 或[通过猪八戒平台对我们进行雇佣](http://shop.zbj.com/14338306/) +### 高级商务合作 ### + +如果您想要更深入地定制化服务,比如技术内部培训、项目技术咨询管理、项目解决方案等,请联系:__王先生 [18513831297](tel:18513831297) [wuxieken@qq.com](mailto:wuxieken@qq.com)__ + ## 捐助 本网站目前流量压力巨大,服务器成本较高。如果您认为本网站对您有所帮助,希望支持本网站的持续发展和维护,可以使用支付宝`转账`到下面地址对我们进行捐助。谢谢! @@ -87,6 +92,4 @@ React Native中文网是由杭州欧石南网络科技有限公司创办的, 地址:杭州市西湖区古墩路616号同人精华大厦2座1516室 -联系电话:王先生 [18513831297](tel:18513831297) - -QQ:[402740419](tencent://message/?uin=402740419&Site=react-native.cn&Menu=yes) +邮箱:[eliza@hzerica.cn](mailto:eliza@hzerica.cn) diff --git a/docs/ads/ads.js b/docs/ads/ads.js index 064cb93..9eaf764 100644 --- a/docs/ads/ads.js +++ b/docs/ads/ads.js @@ -1,19 +1,18 @@ export default { index: { banner: { - img: require('./img/122live.jpg'), - text: '1月22日天地之灵直播MobX案例精讲', - link: 'http://reactnative.cn/post/3337', - gainfo: '122live/index' + img: require('./img/course01.png'), + text: '听晴明老师从头讲React Native', + link: '/post/3798', + gainfo: 'post3798/index' } }, docs: { banner: { - img: require('./img/122live.jpg'), - text: '1月22日天地之灵直播MobX案例精讲', - link: 'http://reactnative.cn/post/3337', - gainfo: '122live/docs' + img: require('./img/course01.png'), + text: '听晴明老师从头讲React Native', + link: '/post/3798', + gainfo: 'post3798/docs' } } - -} \ No newline at end of file +} diff --git a/docs/ads/img/classmates.png b/docs/ads/img/classmates.png new file mode 100644 index 0000000..551903a Binary files /dev/null and b/docs/ads/img/classmates.png differ diff --git a/docs/ads/img/course01.png b/docs/ads/img/course01.png new file mode 100644 index 0000000..c878edb Binary files /dev/null and b/docs/ads/img/course01.png differ diff --git a/docs/cases/cases.json b/docs/cases/cases.json index ffb32fc..ca82dcf 100644 --- a/docs/cases/cases.json +++ b/docs/cases/cases.json @@ -1,7 +1,62 @@ [ + { + "name": "京东金融", + "desc": "金融生活服务平台", + "ios": "https://itunes.apple.com/cn/app/id895682747?mt=8", + "android": "https://download.jr.jd.com/downapp/jrapp_jr464.apk", + "icon": "http://is5.mzstatic.com/image/thumb/Purple127/v4/5f/bd/de/5fbdde01-36e5-5d52-9b38-e82de0faa8cb/source/175x175bb.jpg" + }, + { + "name": "漂漂共享书", + "desc": "与朋友同事分享纸质图书", + "ios": "https://itunes.apple.com/cn/app/id1201678520?ls=1&mt=8", + "android": "http://sj.qq.com/myapp/detail.htm?apkName=com.jubing.piaopiaoshu", + "icon": "http://pp.myapp.com/ma_icon/0/icon_52416269_1494393330/96" + }, + { + "name": "若爱", + "desc": "免费实名制相亲婚恋平台", + "ios": "", + "android": "http://android.myapp.com/myapp/detail.htm?apkName=com.ruoaiwang", + "icon": "http://pp.myapp.com/ma_icon/0/icon_52449613_1521015507/96" + }, + { + "name": "立刻说", + "desc": "在线英语学习网站", + "ios": "https://itunes.apple.com/us/app/立刻说/id1190202556?ls=1&mt=8", + "android": "http://android.myapp.com/myapp/detail.htm?apkName=com.lks", + "icon": "https://attach.likeshuo.com/content/images/other/logo/logo_app.png" + }, + { + "name": "农眼", + "desc": "农业大数据平台", + "ios": "https://itunes.apple.com/cn/app/nonyang/id1177902411?mt=8", + "android": "http://app.airag.cn/app/update/farmer/visionfarmer.apk", + "icon": "http://app.airag.cn/app/icon/icon_vision_512.png" + }, + { + "name": "一键康", + "desc": "您的公立医院体检平台", + "ios": "https://itunes.apple.com/cn/app/id1100633202?mt=8", + "android": "http://www.touchealth.com.cn/", + "icon": "http://ol3l72k79.bkt.clouddn.com/20170512172843_jXH13w_logo512.jpeg" + }, + { + "name": "找车场", + "desc": "简单方便的帮你找车场", + "ios": "https://itunes.apple.com/us/app/id1223491793?l=zh&ls=1&mt=8", + "android": "http://a.app.qq.com/o/simple.jsp?pkgname=com.zcc", + "icon": "https://images.vmartaw.com/2017/04/07/icon.png" + },{ + "name": "幕布", + "desc": "管理你的大脑", + "ios": "https://itunes.apple.com/app/id1214302139", + "android": "https://mubu.com/app-dl", + "icon": "https://img.mubu.com/static/wechat-share-icon.png" + }, { "name": "大浙惠生活", - "desc": "腾讯大浙网旗下,浙江省本地吃喝玩乐活动、优惠福利信息的推荐平台", + "desc": "大浙网优惠福利信息", "ios": "https://itunes.apple.com/app/apple-store/id1184729110??l=zh&ls=1&mt=8", "android": "", "icon": "http://is4.mzstatic.com/image/thumb/Purple122/v4/8f/87/30/8f873055-d51e-ec43-a57c-2057a8cb95a8/source/175x175bb.jpg" @@ -175,13 +230,6 @@ "android": "", "icon": "http://img2.yhwj168.com/1024.png" }, - { - "name": "16直播", - "desc": "土豪离不开的直播", - "ios": "https://itunes.apple.com/us/app/mei-nu-zha-jin-hua-zhi-bo/id1151272467?l=zh&ls=1&mt=8", - "android": "http://zhushou.360.cn/detail/index/soft_id/3438248?recrefer=SE_D_16%E7%9B%B4%E6%92%AD", - "icon": "http://p17.qhimg.com/t012e5fe09adb1d0ffa.png" - }, { "name": "水稻汽车", "desc": "平价放心的超级4S店", @@ -228,7 +276,49 @@ "name": "红包圈", "desc": "红包交友同城聊天应用", "ios": "https://itunes.apple.com/cn/app/hong-bao-quan-hong-bao-jiao/id1193509121?mt=8", - "android": "http://h5.down.bwgame.com.cn/redpackage.apk", + "android": "http://h5.down.bwgame.com.cn/redenvelope2017041916.apk", "icon": "http://ohg9m5uoq.bkt.clouddn.com/1024.png" - } + }, + { + "name": "万颗商城", + "desc": "针对农村的电商平台", + "ios": "https://itunes.apple.com/cn/app/%E4%B8%87%E9%A2%97%E5%95%86%E5%9F%8E/id1136517970?mt=8", + "android": "http://zhushou.360.cn/detail/index/soft_id/3349739?recrefer=SE_D_%E4%B8%87%E9%A2%97%E5%95%86%E5%9F%8E", + "icon": "http://is1.mzstatic.com/image/thumb/Purple122/v4/75/db/c9/75dbc963-7696-1583-d2f8-055596654daf/source/175x175bb.jpg" + }, + { + "name": "花匠铺", + "desc": "家庭园艺种植交流平台", + "ios": "https://itunes.apple.com/app/id1219173067", + "android": "https://huajiangpu.com/m", + "icon": "http://oiifzbqya.bkt.clouddn.com/hjplogo128.png" + }, + { + "name": "鲜易商城", + "desc": "鲜易商城是国内领先的B2B生鲜电商", + "ios": "https://itunes.apple.com/us/app/鲜易商城/id1039746711?l=zh&ls=1&mt=8", + "android": "http://a.app.qq.com/o/simple.jsp?pkgname=com.xebest.b2c", + "icon": "http://pp.myapp.com/ma_icon/0/icon_12159939_1493113253/96" + }, + { + "name": "澳門手遊", + "desc": "澳門手遊平台", + "ios": "https://www.gaaamee.com/download-app.html", + "android": "https://www.gaaamee.com/download-app.html", + "icon": "https://www.gaaamee.com/download/images/download/code.png" + }, + { + "name": "宾得相机伴侣", + "desc": "更好的宾得相机同步助手", + "ios": "https://itunes.apple.com/us/app/pentax-photo-sync/id1260957200?l=zh&ls=1&mt=8", + "android": "https://play.google.com/apps/publish/?account=7528363839997010893#ManageReleasesPlace:p=com.luckyxmobile.pentaxphotosync", + "icon": "https://raw.githubusercontent.com/SleetShang/SomeMaterial/master/PentaxPhotoSync/AppIcon120x120.png" + }, + { + "name": "我来贷银行版", + "desc": "年轻人的口袋银行", + "ios": "https://itunes.apple.com/us/app/id218618489?ls=1&mt=8", + "android": "http://web.wolaidai.com/webapp/qxsd/download/download.html?channel=bd_qxsd_share", + "icon": "https://web.wolaidai.com/webapp/qxsd/img/login/logo.png" + } ] diff --git a/docs/docs/0.34/permissionsandroid.md b/docs/docs/0.34/permissionsandroid.md index e2d6386..6970110 100644 --- a/docs/docs/0.34/permissionsandroid.md +++ b/docs/docs/0.34/permissionsandroid.md @@ -9,8 +9,8 @@ ```js async function requestCameraPermission() { try { - const granted = await AndroidPermissions.requestPermission( - AndroidPermissions.PERMISSIONS.CAMERA, + const granted = await PermissionsAndroid.requestPermission( + PermissionsAndroid.PERMISSIONS.CAMERA, { 'title': '申请摄像头权限', 'message': '一个很牛逼的应用想借用你的摄像头,' + @@ -53,4 +53,4 @@ async function requestCameraPermission() {

其中rationale参数是可选的,其结构为包含titlemessage)的对象。此方法会和系统协商,是弹出系统内置的权限申请对话框,还是显示rationale中的信息以向用户进行解释。具体原理请参阅android官方文档 (https://developer.android.com/training/permissions/requesting.html#explain)。

- \ No newline at end of file + diff --git a/docs/docs/0.40/appstate.md b/docs/docs/0.40/appstate.md index 47034c0..e678151 100644 --- a/docs/docs/0.40/appstate.md +++ b/docs/docs/0.40/appstate.md @@ -27,8 +27,11 @@ componentDidMount() { componentWillUnmount() { AppState.removeEventListener('change', this._handleAppStateChange); } -_handleAppStateChange(currentAppState) { - this.setState({ currentAppState, }); +_handleAppStateChange = (nextAppState) => { + if (this.state.appState.match(/inactive|background/) && nextAppState === 'active') { + console.log('App has come to the foreground!') + } + this.setState({appState: nextAppState}); } render() { return ( diff --git a/docs/docs/0.40/getting-started.md b/docs/docs/0.40/getting-started.md index 202609b..05237b3 100644 --- a/docs/docs/0.40/getting-started.md +++ b/docs/docs/0.40/getting-started.md @@ -89,7 +89,7 @@ sudo chown -R `whoami` /usr/local brew install node ``` -安装完node后建议设置npm镜像以加速后面的过程(或使用科学上网工具)。 +安装完node后建议设置npm镜像以加速后面的过程(或使用科学上网工具)。注意:不要使用cnpm!cnpm安装的模块路径比较奇怪,packager不能正常识别! ``` npm config set registry https://registry.npm.taobao.org --global @@ -104,6 +104,14 @@ npm config set disturl https://npm.taobao.org/dist --global npm install -g yarn react-native-cli ``` +安装完yarn后同理也要设置镜像源: + +``` +yarn config set registry https://registry.npm.taobao.org --global +yarn config set disturl https://npm.taobao.org/dist --global +``` + + 如果你看到`EACCES: permission denied`这样的权限报错,那么请参照上文的homebrew译注,修复`/usr/local`目录的所有权: ```bash @@ -114,7 +122,7 @@ sudo chown -R `whoami` /usr/local #### Xcode -React Native目前需要[Xcode](https://developer.apple.com/xcode/downloads/) 7.0 或更高版本。你可以通过App Store或是到[Apple开发者官网](https://developer.apple.com/xcode/downloads/)上下载。这一步骤会同时安装Xcode IDE和Xcode的命令行工具。 +React Native目前需要[Xcode](https://developer.apple.com/xcode/downloads/) 8.0 或更高版本。你可以通过App Store或是到[Apple开发者官网](https://developer.apple.com/xcode/downloads/)上下载。这一步骤会同时安装Xcode IDE和Xcode的命令行工具。 > 虽然一般来说命令行工具都是默认安装了,但你最好还是启动Xcode,并在`Xcode | Preferences | Locations`菜单中检查一下是否装有某个版本的`Command Line Tools`。Xcode的命令行工具中也包含一些必须的工具,比如`git`等。 @@ -151,13 +159,13 @@ Android Studio包含了运行和测试React Native应用所需的Android SDK和 ![platforms](img/react-native-android-studio-android-sdk-platforms.png) -- 在`SDK Tools`窗口中,选择`Show Package Details`,然后在`Android SDK Build Tools`中勾选`Android SDK Build-Tools 23.0.1`。(必须是这个版本) +- 在`SDK Tools`窗口中,选择`Show Package Details`,然后在`Android SDK Build Tools`中勾选`Android SDK Build-Tools 23.0.1`(必须是这个版本)。然后还要勾选最底部的`Android Support Repository`. ![build tools](img/react-native-android-studio-android-sdk-build-tools.png) #### ANDROID_HOME环境变量 -确保`ANDROID_HOME`环境变量正确地指向了你安装的Android SDK的路径。具体的做法是把下面的命令加入到`~/.bash_profile`文件中:(__译注__:~表示用户目录,即`/Users/你的用户名/`,而小数点开头的文件在Finder中是隐藏的,并且这个文件有可能并不存在。请在终端下使用`sudo vi ~/.bash_profile`命令创建或编辑。如不熟悉vi操作,请点击[这里](http://www.eepw.com.cn/article/48018.htm)学习) +确保`ANDROID_HOME`环境变量正确地指向了你安装的Android SDK的路径。具体的做法是把下面的命令加入到`~/.bash_profile`文件中:(__译注__:~表示用户目录,即`/Users/你的用户名/`,而小数点开头的文件在Finder中是隐藏的,并且这个文件有可能并不存在。请在终端下使用`vi ~/.bash_profile`命令创建或编辑。如不熟悉vi操作,请点击[这里](http://www.eepw.com.cn/article/48018.htm)学习) ``` # 如果你不是通过Android Studio安装的sdk,则其路径可能不同,请自行确定清楚。 @@ -172,7 +180,8 @@ source ~/.bash_profile 可以使用`echo $ANDROID_HOME`检查此变量是否已正确设置。 -
+
+
### 推荐安装的工具 @@ -203,10 +212,6 @@ brew install flow export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools ``` -#### Gradle Daemon - -开启[Gradle Daemon](https://docs.gradle.org/2.9/userguide/gradle_daemon.html)可以极大地提升java代码的增量编译速度。 - ### 其他可选的安装项 #### Git @@ -234,43 +239,13 @@ brew install git 比起Android Studio自带的原装模拟器,Genymotion是一个性能更好的选择,但它只对个人用户免费。 -1. 下载和安装[Genymotion](https://www.genymotion.com/)(译注:你需要先注册登录,然后才能找到免费下载的链接!另外,genymotion需要依赖VirtualBox虚拟机,下载选项中提供了包含VirtualBox和不包含的选项,请按需选择)。 +1. 下载和安装[Genymotion](https://www.genymotion.com/download)(genymotion需要依赖VirtualBox虚拟机,下载选项中提供了包含VirtualBox和不包含的选项,请按需选择)。 2. 打开Genymotion。如果你还没有安装VirtualBox,则此时会提示你安装。 3. 创建一个新模拟器并启动。 4. 启动React Native应用后,可以按下⌘+M来打开开发者菜单。 -### 常见问题 - -#### 安装Android Studio时无法创建虚拟设备 - -某些版本的Android Studio可能存在一个[已知的bug](https://code.google.com/p/android/issues/detail?id=207563),导致在安装时无法创建虚拟设备。安装过程中可能看到如下报错: - -``` -Creating Android virtual device -Unable to create a virtual device: Unable to create Android virtual device -``` - -如果你碰到了这个问题,可以运行`android avd`来手工创建虚拟设备。 - -![avd](img/react-native-android-studio-avd.png) - -然后在AVD管理器(AVD Manager)窗口中选择新设备并点击`Start...`来启动。 - -#### Shell命令无响应的异常 - -如果你碰到了下面这样的异常): - -``` -Execution failed for task ':app:installDebug'. - com.android.builder.testing.api.DeviceException: com.android.ddmlib.ShellCommandUnresponsiveException -``` - -试着将`项目目录/android/build.gradle`中的Gradle版本改为1.2.3。 - - - - -
+
+
## 安装 @@ -324,7 +299,7 @@ sudo ln -s /usr/bin/nodejs /usr/bin/node choco install nodejs.install ``` -安装完node后建议设置npm镜像以加速后面的过程(或使用科学上网工具)。 +安装完node后建议设置npm镜像以加速后面的过程(或使用科学上网工具)。注意:不要使用cnpm!cnpm安装的模块路径比较奇怪,packager不能正常识别! ``` npm config set registry https://registry.npm.taobao.org --global @@ -341,6 +316,12 @@ npm config set disturl https://npm.taobao.org/dist --global npm install -g yarn react-native-cli ``` +安装完yarn后同理也要设置镜像源: + +``` +yarn config set registry https://registry.npm.taobao.org --global +yarn config set disturl https://npm.taobao.org/dist --global +``` > 如果你遇到`EACCES: permission denied`权限错误,可以尝试运行下面的命令(限linux系统): > `sudo npm install -g yarn react-native-cli`. @@ -412,7 +393,7 @@ Android Studio包含了运行和测试React Native应用所需的Android SDK和
-- 在`SDK Tools`窗口中,选择`Show Package Details`,然后在`Android SDK Build Tools`中勾选`Android SDK Build-Tools 23.0.1`。(必须是这个版本) +- 在`SDK Tools`窗口中,选择`Show Package Details`,然后在`Android SDK Build Tools`中勾选`Android SDK Build-Tools 23.0.1`(必须是这个版本)。然后还要勾选最底部的`Android Support Repository`.
@@ -491,25 +472,29 @@ sudo make install npm install -g flow-bin ``` -
+
+
#### Gradle Daemon 开启[Gradle Daemon](https://docs.gradle.org/2.9/userguide/gradle_daemon.html)可以极大地提升java代码的增量编译速度。 -
+
+
``` touch ~/.gradle/gradle.properties && echo "org.gradle.daemon=true" >> ~/.gradle/gradle.properties ``` -
+
+
``` (if not exist "%USERPROFILE%/.gradle" mkdir "%USERPROFILE%/.gradle") && (echo org.gradle.daemon=true >> "%USERPROFILE%/.gradle/gradle.properties") ``` -
+
+
#### Android模拟器加速器 @@ -583,7 +568,7 @@ choco install git 比起Android Studio自带的原装模拟器,Genymotion是一个性能更好的选择,但它只对个人用户免费。 -1. 下载和安装[Genymotion](https://www.genymotion.com/)(译注:你需要先注册登录,然后才能找到免费下载的链接!另外,genymotion需要依赖VirtualBox虚拟机,下载选项中提供了包含VirtualBox和不包含的选项,请按需选择)。 +1. 下载和安装[Genymotion](https://www.genymotion.com/download)(genymotion需要依赖VirtualBox虚拟机,下载选项中提供了包含VirtualBox和不包含的选项,请按需选择)。 2. 打开Genymotion。如果你还没有安装VirtualBox,则此时会提示你安装。 3. 创建一个新模拟器并启动。 4. 启动React Native应用后,可以按下F1来打开开发者菜单。 @@ -593,7 +578,6 @@ choco install git #### Visual Studio Emulator for Android [Visual Studio Emulator for Android](https://www.visualstudio.com/zh-cn/features/msft-android-emulator-vs.aspx#中国 (简体中文))是利用了Hyper-V技术进行硬件加速的免费android模拟器。也是Android Studio自带的原装模拟器之外的一个很好的选择。而且你并不需要安装Visual Studio。 - 在用于React Native开发前,需要先在注册表中进行一些修改: 1. 打开运行命令(按下Windows+R键) @@ -603,7 +587,8 @@ choco install git 5. 名称设为`Path` 6. 双击`Path`,将其值设为你的Android SDK的路径。(例如`C:\Program Files\Android\sdk`) -
+
+
## 测试安装 @@ -675,11 +660,8 @@ cd AwesomeProject react-native start ``` -
- -如果你碰到了`ERROR Watcher took too long to load`的报错,请尝试将[这个文件](https://github.com/facebook/react-native/blob/5fa33f3d07f8595a188f6fe04d6168a6ede1e721/packager/react-packager/src/DependencyResolver/FileWatcher/index.js#L16)中的MAX_WAIT_TIME值改得更大一些 (文件在`node_modules/react-native/`目录下)。 - -
+
+
### 修改项目 diff --git a/docs/docs/0.40/linking.md b/docs/docs/0.40/linking.md index f7f08ad..b8b25a4 100644 --- a/docs/docs/0.40/linking.md +++ b/docs/docs/0.40/linking.md @@ -28,7 +28,7 @@ componentDidMount() { 对于iOS来说,如果要在App启动后也监听传入的App链接,那么首先需要在项目中链接`RCTLinking`,具体步骤请参考[使用链接库](linking-libraries-ios.html)这篇文档,然后需要在`AppDelegate.m`中增加以下代码: ```objective-c -#import "RCTLinkingManager.h" +#import - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation diff --git a/docs/docs/0.40/mapview.md b/docs/docs/0.40/mapview.md index dedb012..75ad320 100644 --- a/docs/docs/0.40/mapview.md +++ b/docs/docs/0.40/mapview.md @@ -46,8 +46,7 @@

iosoverlays [{coordinates: [{latitude: number, longitude: number}], lineWidth: number, strokeColor: ColorPropType, fillColor: ColorPropType, id: string}] #

-

地图的覆盖层。

< - /div> +

地图的覆盖层。

onAnnotationPress function #

diff --git a/docs/docs/0.40/native-component-ios.md b/docs/docs/0.40/native-component-ios.md index c278fa6..828c6e4 100644 --- a/docs/docs/0.40/native-component-ios.md +++ b/docs/docs/0.40/native-component-ios.md @@ -18,7 +18,7 @@ // RCTMapManager.m #import -#import "RCTViewManager.h" +#import @interface RCTMapManager : RCTViewManager @end @@ -214,7 +214,7 @@ var RCTSwitch = requireNativeComponent('RCTSwitch', Switch, { #import -#import "RCTComponent.h" +#import @interface RCTMap: MKMapView @@ -239,7 +239,7 @@ var RCTSwitch = requireNativeComponent('RCTSwitch', Switch, { #import #import "RCTMap.h" -#import "UIView+React.h" +#import @interface RCTMapManager() @end diff --git a/docs/docs/0.40/navigatorios.md b/docs/docs/0.40/navigatorios.md index 0a649a2..cc888be 100644 --- a/docs/docs/0.40/navigatorios.md +++ b/docs/docs/0.40/navigatorios.md @@ -361,7 +361,9 @@ In the example above the navigation bar color is changed when the new route is p const React = require('react'); const ReactNative = require('react-native'); const ViewExample = require('./ViewExample'); + const createExamplePage = require('./createExamplePage'); +const nativeImageSource = require('nativeImageSource'); const { AlertIOS, NavigatorIOS, @@ -372,8 +374,8 @@ const { View, } = ReactNative; -const EmptyPage = React.createClass({ - render: function() { +class EmptyPage extends React.Component { + render() { return ( @@ -381,11 +383,11 @@ const EmptyPage = React.createClass({ ); - }, -}); + } +} -const NavigatorIOSExamplePage = React.createClass({ - render: function() { +class NavigatorIOSExamplePage extends React.Component { + render() { var recurseTitle = 'Recurse Navigation'; if (!this.props.depth || this.props.depth === 1) { recurseTitle += ' - more examples here'; @@ -408,6 +410,13 @@ const NavigatorIOSExamplePage = React.createClass({ component: createExamplePage(null, ViewExample), }); })} + {this._renderRow('Custom title image Example', () => { + this.props.navigator.push({ + title: 'Custom title image Example', + titleImage: require('./relay.png'), + component: createExamplePage(null, ViewExample), + }); + })} {this._renderRow('Custom Right Button', () => { this.props.navigator.push({ title: NavigatorIOSExample.title, @@ -419,13 +428,52 @@ const NavigatorIOSExamplePage = React.createClass({ } }); })} + {this._renderRow('Custom Right System Button', () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: EmptyPage, + rightButtonSystemIcon: 'bookmarks', + onRightButtonPress: () => this.props.navigator.pop(), + passProps: { + text: 'This page has a right system button in the nav bar', + } + }); + })} {this._renderRow('Custom Left & Right Icons', () => { this.props.navigator.push({ title: NavigatorIOSExample.title, component: EmptyPage, leftButtonTitle: 'Custom Left', onLeftButtonPress: () => this.props.navigator.pop(), - rightButtonIcon: require('image!NavBarButtonPlus'), + rightButtonIcon: nativeImageSource({ + ios: 'NavBarButtonPlus', + width: 17, + height: 17 + }), + onRightButtonPress: () => { + AlertIOS.alert( + 'Bar Button Action', + 'Recognized a tap on the bar button icon', + [ + { + text: 'OK', + onPress: () => console.log('Tapped OK'), + }, + ] + ); + }, + passProps: { + text: 'This page has an icon for the right button in the nav bar', + } + }); + })} + {this._renderRow('Custom Left & Right System Icons', () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: EmptyPage, + leftButtonSystemIcon: 'cancel', + onLeftButtonPress: () => this.props.navigator.pop(), + rightButtonSystemIcon: 'search', onRightButtonPress: () => { AlertIOS.alert( 'Bar Button Action', @@ -457,9 +505,9 @@ const NavigatorIOSExamplePage = React.createClass({ ); - }, + } - _renderReplace: function() { + _renderReplace = () => { if (!this.props.depth) { // this is to avoid replacing the top of the stack return null; @@ -477,9 +525,9 @@ const NavigatorIOSExamplePage = React.createClass({ } }); }); - }, + }; - _renderReplacePrevious: function() { + _renderReplacePrevious = () => { if (!this.props.depth || this.props.depth < 2) { // this is to avoid replacing the top of the stack return null; @@ -494,9 +542,9 @@ const NavigatorIOSExamplePage = React.createClass({ wrapperStyle: styles.customWrapperStyle, }); }); - }, + }; - _renderReplacePreviousAndPop: function() { + _renderReplacePreviousAndPop = () => { if (!this.props.depth || this.props.depth < 2) { // this is to avoid replacing the top of the stack return null; @@ -511,9 +559,9 @@ const NavigatorIOSExamplePage = React.createClass({ wrapperStyle: styles.customWrapperStyle, }); }); - }, + }; - _renderRow: function(title: string, onPress: Function) { + _renderRow = (title: string, onPress: Function) => { return ( @@ -526,17 +574,15 @@ const NavigatorIOSExamplePage = React.createClass({ ); - }, -}); + }; +} -const NavigatorIOSExample = React.createClass({ - statics: { - title: '', - description: 'iOS navigation capabilities', - external: true, - }, +class NavigatorIOSExample extends React.Component { + static title = ''; + static description = 'iOS navigation capabilities'; + static external = true; - render: function() { + render() { const {onExampleExit} = this.props; return ( ); - }, -}); + } +} const styles = StyleSheet.create({ container: { diff --git a/docs/docs/0.40/performance.md b/docs/docs/0.40/performance.md index 061cdc2..cdb88cc 100644 --- a/docs/docs/0.40/performance.md +++ b/docs/docs/0.40/performance.md @@ -28,17 +28,20 @@ 在运行打好了离线包的应用时,控制台打印语句可能会极大地拖累JavaScript线程。注意有些第三方调试库也可能包含控制台打印语句,比如[redux-logger](https://github.com/evgenyrodionov/redux-logger),所以在发布应用前请务必仔细检查,确保全部移除。 -> 有个[babel插件](https://babeljs.io/docs/plugins/transform-remove-console/)可以帮你移除所有的`console.*`调用。首先需要使用`npm install babel-plugin-transform-remove-console --save`来安装,然后在项目根目录下编辑(或者是新建)一个名为`.babelrc`的文件,在其中加入: -```json -{ - "env": { - "production": { - "plugins": ["transform-remove-console"] - } - } +> 这里有个小技巧可以在发布时屏蔽掉所有的`console.*`调用。React Native中有一个全局变量`__DEV__`用于指示当前运行环境是否是开发环境。我们可以据此在正式环境中替换掉系统原先的console实现。 + +```js +if (!__DEV__) { + global.console = { + info: () => {}, + log: () => {}, + warn: () => {}, + error: () => {}, + }; } ``` -这样在打包发布时,所有的控制台语句就会被自动移除,而在调试时它们仍然会被正常调用。 + +这样在打包发布时,所有的控制台语句就会被自动替换为空函数,而在调试时它们仍然会被正常调用。 ### 开发模式 (dev=true) diff --git a/docs/docs/0.40/permissionsandroid.md b/docs/docs/0.40/permissionsandroid.md index b8d314a..b002c58 100644 --- a/docs/docs/0.40/permissionsandroid.md +++ b/docs/docs/0.40/permissionsandroid.md @@ -1,10 +1,6 @@ `PermissionsAndroid`可以访问Android M(也就是6.0)开始提供的权限模型。有一些权限写在`AndroidManifest.xml`就可以在安装时自动获得。但有一些“危险”的权限则需要弹出提示框供用户选择。本API即用于后一种情形。 -<<<<<<< Updated upstream 在低于Android 6.0的设备上,权限只要写在`AndroidManifest.xml`里就会自动获得,此情形下`check`和`request` 方法将始终返回true。 -======= -在低于Android 6.0的设备上,权限只要写在`AndroidManifest.xml`里就会自动获得,此情形下`checkPermission`和`requestPermission` 方法将始终返回true。 ->>>>>>> Stashed changes 如果用户之前拒绝过你的某项权限请求,那么系统会建议你显示一个为什么需要这个权限的“详细解释”(rationale参数)。如果用户之前拒绝过,那么当你再次申请的时候,弹出的就可能不是原先的申请信息,而是`rationale`参数里提供的进一步解释。 @@ -13,11 +9,7 @@ ```js async function requestCameraPermission() { try { -<<<<<<< Updated upstream const granted = await AndroidPermissions.request( -======= - const granted = await AndroidPermissions.requestPermission( ->>>>>>> Stashed changes AndroidPermissions.PERMISSIONS.CAMERA, { 'title': '申请摄像头权限', @@ -25,11 +17,7 @@ async function requestCameraPermission() { '然后你就可以拍出酷炫的皂片啦。' } ) -<<<<<<< Updated upstream if (granted === PermissionsAndroid.RESULTS.GRANTED) { -======= - if (granted) { ->>>>>>> Stashed changes console.log("现在你获得摄像头权限了") } else { console.log("用户并不屌你") @@ -50,41 +38,24 @@ async function requestCameraPermission() {
-<<<<<<< Updated upstream

check(permission) # -======= -

checkPermission(permission) - # ->>>>>>> Stashed changes

返回一个promise,最终值为用户是否授权过的布尔值。

-<<<<<<< Updated upstream

request(permission, rationale?) # -======= -

requestPermission(permission, rationale?) - # ->>>>>>> Stashed changes

弹出提示框向用户请求某项权限。返回一个promise,最终值为用户是否同意了权限申请的布尔值。

其中rationale参数是可选的,其结构为包含titlemessage)的对象。此方法会和系统协商,是弹出系统内置的权限申请对话框,还是显示rationale中的信息以向用户进行解释。具体原理请参阅android官方文档 (https://developer.android.com/training/permissions/requesting.html#explain)。

-<<<<<<< Updated upstream

requestMultiple(permissions) #

-

Prompts the user to enable multiple permissions in the same dialog and -returns an object with the permissions as keys and strings as values -indicating whether the user allowed or denied the request

+

在一个弹出框中向用户请求多个权限。返回值为一个object,key为各权限名称,对应值为用户授权与否。

-======= ->>>>>>> Stashed changes
\ No newline at end of file diff --git a/docs/docs/0.40/running-on-device-android.md b/docs/docs/0.40/running-on-device-android.md index 592ca5e..5bc930b 100644 --- a/docs/docs/0.40/running-on-device-android.md +++ b/docs/docs/0.40/running-on-device-android.md @@ -17,6 +17,10 @@ __译注__:如果你连接了多个设备(包含模拟器在内),后续 __译注__:在真机上运行时可能会遇到白屏的情况,请找到并开启`悬浮窗权限`。比如miui系统的设置[在此处](http://jingyan.baidu.com/article/f25ef25466c0fc482d1b824d.html)。 +> 提示 +> +> 你还可以运行`react-native run-android --variant=release`来安装release版的应用。当然你需要[先配置好签名](signed-apk-android.html),且此时无法再开启开发者菜单。注意在debug和release版本间来回切换安装时可能会报错签名不匹配,此时需要先卸载前一个版本再尝试安装。 + ## 从设备上访问开发服务器。 在启用开发服务器的情况下,你可以快速的迭代修改应用,然后在设备上查看结果。按照下面描述的任意一种方法来使你的运行在电脑上的开发服务器可以从设备上访问到。 diff --git a/docs/docs/0.40/signed-apk-android.md b/docs/docs/0.40/signed-apk-android.md index 895f5c8..32e7036 100644 --- a/docs/docs/0.40/signed-apk-android.md +++ b/docs/docs/0.40/signed-apk-android.md @@ -77,6 +77,7 @@ Gradle的`assembleRelease`参数会把所有用到的JavaScript代码都打包 生成的APK文件位于`android/app/build/outputs/apk/app-release.apk`,它已经可以用来发布了。 + ### 测试应用的发行版本 在把发行版本提交到Play Store之前,你应该做一次最终测试。输入以下命令可以在设备上安装发行版本: @@ -88,6 +89,8 @@ $ cd android && ./gradlew installRelease 注意`installRelease`参数只能在你完成了上面的签名配置之后才可以使用。 你现在可以关掉运行中的packager了,因为你所有的代码和框架依赖已经都被打包到apk包中,可以离线运行了。 +> 在debug和release版本间来回切换安装时可能会报错签名不匹配,此时需要先卸载前一个版本再尝试安装。 + ### 启用Proguard代码混淆来缩小APK文件的大小(可选) Proguard是一个Java字节码混淆压缩工具,它可以移除掉React Native Java(和它的依赖库中)中没有被使用到的部分,最终有效的减少APK的大小。 diff --git a/docs/docs/0.40/text.md b/docs/docs/0.40/text.md index 7ace7df..d21d1be 100644 --- a/docs/docs/0.40/text.md +++ b/docs/docs/0.40/text.md @@ -22,7 +22,7 @@ var styles = StyleSheet.create({ fontSize: 20, fontWeight: 'bold', }, -}; +}); ``` ### 截图 @@ -40,7 +40,7 @@ var styles = StyleSheet.create({

allowFontScaling bool #

-

控制字体是否要根据iOS的“文本大小”辅助选项来进行缩放。

+

控制字体是否要根据系统的“字体大小”辅助选项来进行缩放。

@@ -1076,4 +1076,4 @@ var styles = StyleSheet.create({ }); module.exports = TextExample; -``` \ No newline at end of file +``` diff --git a/docs/docs/0.40/upgrading.md b/docs/docs/0.40/upgrading.md index 5349dea..d21a305 100644 --- a/docs/docs/0.40/upgrading.md +++ b/docs/docs/0.40/upgrading.md @@ -1,4 +1,4 @@ -时刻将React Native更新到最新的版本,可以获得更多API、视图、开发者工具以及其他一些好东西(译注:官方开发任务繁重,人手紧缺,几乎不会对旧版本提供维护支持,所以即便更新可能带来一些兼容上的变更,但建议开发者还是尽一切可能第一时间更新)。由于一个完整的React Native项目是由Android项目、iOS项目和JavaScript项目组成的,且都打包在一个npm包中,所以升级可能会有一些麻烦。我们会尽量简化这一流程。以下是目前所需的升级步骤: +时刻将React Native更新到最新的版本,可以获得更多API、视图、开发者工具以及其他一些好东西(译注:官方开发任务繁重,人手紧缺,几乎不会对旧版本提供维护支持,所以即便更新可能带来一些兼容上的变更,但建议开发者还是尽一切可能第一时间更新)。由于一个完整的React Native项目是由Android项目、iOS项目和JavaScript项目组成的,且都打包在一个npm包中,所以升级可能会有一些麻烦。我们会尽量简化这一流程。你可以在项目目录下使用`react-native -v`命令查看当前的版本。以下是目前所需的升级步骤: __译注__:[更新日志点这里查看](http://bbs.reactnative.cn/category/1) @@ -12,10 +12,6 @@ __译注__:[更新日志点这里查看](http://bbs.reactnative.cn/category/1) ### 2. 安装`react-native-git-upgrade`工具模块 -注意:如果你的React Native版本已经大于等于0.40的话,则`不需要安装此模块`。你可以在项目目录下使用`react-native -v`命令查看当前的版本。 - -如果你的React Native版本低于0.40,则需要全局安装此命令行工具模块: - ```sh $ npm install -g react-native-git-upgrade ``` @@ -27,20 +23,6 @@ $ npm install -g react-native-git-upgrade ### 3. 运行更新命令 -如果你的React Native版本已经大于等于0.40的话,则运行: - -```sh -$ react-native upgrade -# 这样会直接把react native升级到最新版本 - -# 或者是: - -$ react-native upgrade X.Y.Z -# 这样把react native升级到指定的X.Y.Z版本 -``` - -如果你的React Native版本低于0.40,则运行: - ```sh $ react-native-git-upgrade # 这样会直接把react native升级到最新版本 diff --git a/docs/docs/0.41/accessibility.md b/docs/docs/0.41/accessibility.md new file mode 100644 index 0000000..2ed438c --- /dev/null +++ b/docs/docs/0.41/accessibility.md @@ -0,0 +1,161 @@ +## iOS与Android原生App的无障碍功能(accessibility) +__译注__:accessibility一词常见多种译法:可访问性、无障碍性、辅助功能等等,其中文意思都不太能准确表达其功能的本质——即为残障人士提供便利。本文主要采用“无障碍功能”和“辅助技术/服务”的说法。如果你或你的公司暂时没有资源和精力去服务这些用户,那么你可以跳过本文。但是,`译者个人希望借本文档,呼吁有能力有资源的商业公司/组织/个人,重视残障人士使用智能手机的权利`。 + +iOS和Android都提供了便于残障人士无障碍使用App的API。此外,两个平台都提供了整套的辅助技术,比如都有针对视力受损人士的读屏软件(iOS的VoiceOver和Android的TalkBack)。同样地,在React Native中我们也封装了对应的API,使开发者能够在App中集成无障碍功能。注意:iOS与Android在具体方法上会有所区别,因此React Native的实现也会因平台而异。 + +## 使App能够无障碍使用 + +### 无障碍功能属性 + +#### accessible (iOS, Android) + +设置为`true`时表示当前视图是一个“无障碍元素”(accessibility element)。无障碍元素会将其所有子组件视为一整个可以选中的组件。默认情况下,所有可点击的组件(Touchable系列组件)都是无障碍元素。 + +在Android上,React Native视图的‘accessible={true}’属性会被转译为原生视图对应的‘focusable={true}’属性。 + +```javascript + + text one + text two + +``` + +在上面这个例子中,当父视图开启无障碍属性后,我们就无法单独选中'text one'和'text two',而只能选中整个父视图。 + + + +#### 无障碍标签accessibilityLabel (iOS, Android) + +当一个视图启用无障碍属性后,最好再加上一个accessibilityLabel(无障碍标签),这样可以让使用VoiceOver的人们清楚地知道自己选中了什么。VoiceOver会读出选中元素的无障碍标签。 + +设定`accessibilityLabel`属性并赋予一个字符串内容即可在视图中启用无障碍标签: + +```javascript + + + Press me! + + +``` + +在上面这段示例代码中,如果不在TouchableOpacity上设置无障碍标签,那么其默认值就会是"Press me!"(即Text子组件的文本值)。此时无障碍标签是通过自动取所有Text子节点的值,然后用空格连起来生成。 + +#### 无障碍元素特性accessibilityTraits (iOS) + +无障碍元素特性可以使VoiceOver的用户知道自己选中的是什么类型的元素。是文本标签?是按钮?还是头部?`accessibilityTraits`回答了这一问题。 + +设定`accessibilityTraits`属性并赋予以下一个或多个(以数组的形式)特性字符串即可启用无障碍元素特性: + +* **none** 无特性元素。 +* **button** 具有按钮特性。 +* **link** 具有链接特性。 +* **header** 作为内容区域的头部(比如导航栏的标题)。 +* **search** 用作搜索框的文本框。 +* **image** 具有图片特性。可以和按钮或链接等连用。 +* **selected** 元素被选中时使用。比如表格中被选中的一行或是[segmented control](segmentedcontrolios.html)中被选中的一个按钮。 +* **plays** 在元素被点击后播放音效时使用。 +* **key** 元素作为虚拟键盘的一个键使用。 +* **text** 具有不可修改的文本的特性。 +* **summary** 在App冷启动(指完全退出后台后再进入)时提供当前的简要总结信息的元素。比如当天气应用冷启动时,显示当前天气情况的元素就会被标记为**summary**。 +* **disabled** 在元素被禁用,不接受用户输入时使用。 +* **frequentUpdates** 有些元素会频繁更新其标签或值,但我们又不希望太频繁地接受到通知,那么就使用这一特性标记。这一特性标记会使无障碍功能的客户端隔一段时间后再去检查变化(避免频繁打扰用户)。秒表就是个典型的例子。 +* **startsMedia** 在元素启动一个多媒体会话时使用(比如播放电影或是录音),此时不应该被VoiceOver这样的辅助技术打断。 +* **adjustable** 元素具有可调整的特性(比如一个滑块)。 +* **allowsDirectInteraction** 在元素可以接受VoiceOver用户的直接触摸交互时使用(比如展示钢琴键盘的视图)。 +* **pageTurn** 用于通知VoiceOver当前页面已经阅读完毕,可以滚动到下一个页面了。 + +#### 无障碍元素的点击事件onAccessibilityTap (iOS) + +使用这一属性来绑定一个自定义的事件处理函数,这一函数会在当用户双击某个已经选中的无障碍元素时调用。 + +#### MagicTap双指双击事件onMagicTap (iOS) + +使用这一属性来绑定一个自定义的事件处理函数,这一函数会在当用户执行"magic tap"操作(即使用两个指头来双击)时调用。magic tap的事件处理函数应该做与当前组件相关性最高的操作,比如在电话应用中,magic tap的操作就应该接通电话,或是挂断已经接通的电话。如果当前选中的元素并没有`onMagicTap`函数,则系统会自动遍历视图层,直到找到一个可以响应此操作的。 + +#### 无障碍组件类型accessibilityComponentType (Android) + +在某些情况下,我们也希望告知用户他选中的组件的类型(比如是个按钮)。如果我们使用的是原生按钮,这一行为会自动进行。但既然我们主要是使用javascript,则还需要为Android的TalkBack技术提供更多信息。要实现这一点,就必须为所有UI组件指定`accessibilityComponentType`属性。比如可以指定`button`,`radiobutton_checked`以及`radiobutton_unchecked`等值。 + +```javascript + + + Press me! + + +``` + +上面这个例子里,TouchableWithoutFeedback在TalkBack中被声明为一个原生按钮。 + +#### 无障碍的动态区域accessibilityLiveRegion (Android) + +组件发生动态变化时,我们希望TalkBack能够提醒用户。这一行为可以通过设置`accessibilityLiveRegion`属性来实现。具体值可以设置为`none`,`polite`以及`assertive`: + +* **none** 辅助服务不应该提醒用户当前视图的变化。 +* **polite** 辅助服务应该提醒用户当前视图的变化。 +* **assertive** 辅助服务应该立即打断当前的语音会话,提醒用户当前视图的变化。 + +```javascript + + + Click me + + + + Clicked {this.state.count} times + +``` + +上面这个例子中,_addOne方法会改变state.count这个变量。那么只要用户点击了 TouchableWithoutFeedback,TalkBack就会读出Text组件中的值,因为它设置了`accessibilityLiveRegion=”polite”`属性。 + +#### 无障碍功能优先级importantForAccessibility (Android) + +如果有两个UI组件同时层叠覆盖在父视图之上,那么默认的无障碍功能的焦点位置就可能难以预料。`importantForAccessibility`属性解决了这一问题,它可以控制某个视图是否触发无障碍功能事件,以及是否将其报告给辅助服务。具体值可以设置为`auto`,`yes`,`no`和`no-hide-descendants`(最后一个值会强制辅助服务忽略当前组件及其所有子组件)。 + +```javascript + + + First layout + + + Second layout + + +``` + +上面这个例子里,第二个View的组件对于TalkBack和其他一些辅助服务来说是完全不可见的。这样我们就可以轻易地把两个视图覆盖到同一个父容器上,而不用担心影响TalkBack服务。 + + + +### 发送无障碍功能的相关事件 (Android) + +有时候需要在UI组件上主动触发一个无障碍功能的事件(比如当某个自定义的视图出现在屏幕上或是某个自定义的单选框被选中)。为此Native UIManager模块提供了一个`sendAccessibilityEvent`方法。它接受两个参数:view标签和事件类型。 + +```javascript +_onPress: function() { + this.state.radioButton = this.state.radioButton === “radiobutton_checked” ? + “radiobutton_unchecked” : “radiobutton_checked”; + if (this.state.radioButton === “radiobutton_checked”) { + RCTUIManager.sendAccessibilityEvent( + React.findNodeHandle(this), + RCTUIManager.AccessibilityEventTypes.typeViewClicked); + } +} + + +``` + +在上面这个例子里我们创建了一个自定义的单选框(CustomRadioButton),并且使其具有了和原生单选框一样的无障碍功能。具体来说,也就是TalkBack可以正确地通知用户当前选项的变更了。 + + +## 测试VoiceOver (iOS) + +要开启VoiceOver功能,先打开iOS设备的设置选项。点击“通用”,然后是“辅助选项”,你会看到很多为残障人群使用手机减少障碍的工具,比如更大的字体、更高的对比度以及VoiceOver。 + +在“视觉”菜单下点击VoiceOver,将开关置为打开状态即可启用。 + +在辅助选项的最底部,有一个“辅助选项快捷键”,开启之后可以通过点击三次Home按钮来快速关闭或打开VoiceOver工具。 diff --git a/docs/docs/0.41/actionsheetios.md b/docs/docs/0.41/actionsheetios.md new file mode 100644 index 0000000..a9764ea --- /dev/null +++ b/docs/docs/0.41/actionsheetios.md @@ -0,0 +1,249 @@ +### 截图 +![showActionSheetWithOptions](img/api/actionsheetios1.png) + +![showShareActionSheetWithOptions](img/api/actionsheetios2.png) + +### 方法 + +
+

static showActionSheetWithOptions(options: Object, callback: Function) #

+
+

在iOS设备上显示一个ActionSheet弹出框,其中options参数为一个对象,其属性必须包含以下一项或多项:

+
    +
  • options(字符串数组) - 一组按钮的标题(必选)
  • +
  • cancelButtonIndex(整型) - 选项中取消按钮所在的位置(索引)
  • +
  • destructiveButtonIndex(整型) - 选项中删除按钮所在的位置(索引)
  • +
  • title(字符串) - 弹出框顶部的标题
  • +
  • message(字符串) - 弹出框顶部标题下方的信息
  • +
+
+
+

static showShareActionSheetWithOptions(options: Object, failureCallback: Function, successCallback: Function) #

+ +
+

在iOS设备上显示一个分享弹出框,其中options参数为一个对象,其属性包含以下几项(必须至少有message或url):

+
    +
  • message(字符串) - 要分享的信息
  • +
  • url(字符串) - 要分享的URL地址
  • +
  • subject(字符串) - 要分享的信息主题
  • +
  • excludedActivityTypes(数组) - 指定在actionsheet中不显示的活动
  • +
+

注:如果url指向本地文件,或者是一个base64编码的url,则会直接读取并分享相应的文件。你可以用这样的方式来分享图片、视频以及PDF文件等。

+
+
+
+ +### 例子 + +```javascript +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + ActionSheetIOS, + StyleSheet, + Text, + UIManager, + View, +} = ReactNative; + +var BUTTONS = [ + 'Option 0', + 'Option 1', + 'Option 2', + 'Delete', + 'Cancel', +]; +var DESTRUCTIVE_INDEX = 3; +var CANCEL_INDEX = 4; + +var ActionSheetExample = React.createClass({ + getInitialState() { + return { + clicked: 'none', + }; + }, + + render() { + return ( + + + Click to show the ActionSheet + + + Clicked button: {this.state.clicked} + + + ); + }, + + showActionSheet() { + ActionSheetIOS.showActionSheetWithOptions({ + options: BUTTONS, + cancelButtonIndex: CANCEL_INDEX, + destructiveButtonIndex: DESTRUCTIVE_INDEX, + }, + (buttonIndex) => { + this.setState({ clicked: BUTTONS[buttonIndex] }); + }); + } +}); + +var ActionSheetTintExample = React.createClass({ + getInitialState() { + return { + clicked: 'none', + }; + }, + + render() { + return ( + + + Click to show the ActionSheet + + + Clicked button: {this.state.clicked} + + + ); + }, + + showActionSheet() { + ActionSheetIOS.showActionSheetWithOptions({ + options: BUTTONS, + cancelButtonIndex: CANCEL_INDEX, + destructiveButtonIndex: DESTRUCTIVE_INDEX, + tintColor: 'green', + }, + (buttonIndex) => { + this.setState({ clicked: BUTTONS[buttonIndex] }); + }); + } +}); + +var ShareActionSheetExample = React.createClass({ + getInitialState() { + return { + text: '' + }; + }, + + render() { + return ( + + + Click to show the Share ActionSheet + + + {this.state.text} + + + ); + }, + + showShareActionSheet() { + ActionSheetIOS.showShareActionSheetWithOptions({ + url: this.props.url, + message: 'message to go with the shared url', + subject: 'a subject to go in the email heading', + excludedActivityTypes: [ + 'com.apple.UIKit.activity.PostToTwitter' + ] + }, + (error) => alert(error), + (success, method) => { + var text; + if (success) { + text = `Shared via ${method}`; + } else { + text = 'You didn\'t share'; + } + this.setState({text}); + }); + } +}); + +var ShareScreenshotExample = React.createClass({ + getInitialState() { + return { + text: '' + }; + }, + + render() { + return ( + + + Click to show the Share ActionSheet + + + {this.state.text} + + + ); + }, + + showShareActionSheet() { + // Take the snapshot (returns a temp file uri) + UIManager.takeSnapshot('window').then((uri) => { + // Share image data + ActionSheetIOS.showShareActionSheetWithOptions({ + url: uri, + excludedActivityTypes: [ + 'com.apple.UIKit.activity.PostToTwitter' + ] + }, + (error) => alert(error), + (success, method) => { + var text; + if (success) { + text = `Shared via ${method}`; + } else { + text = 'You didn\'t share'; + } + this.setState({text}); + }); + }).catch((error) => alert(error)); + } +}); + +var style = StyleSheet.create({ + button: { + marginBottom: 10, + fontWeight: '500', + } +}); + +exports.title = 'ActionSheetIOS'; +exports.description = 'Interface to show iOS\' action sheets'; +exports.examples = [ + { + title: 'Show Action Sheet', + render(): ReactElement { return ; } + }, + { + title: 'Show Action Sheet with tinted buttons', + render(): ReactElement { return ; } + }, + { + title: 'Show Share Action Sheet', + render(): ReactElement { + return ; + } + }, + { + title: 'Share Local Image', + render(): ReactElement { + return ; + } + }, + { + title: 'Share Screenshot', + render(): ReactElement { + return ; + } + } +]; +``` diff --git a/docs/docs/0.41/activityindicator.md b/docs/docs/0.41/activityindicator.md new file mode 100644 index 0000000..0092a18 --- /dev/null +++ b/docs/docs/0.41/activityindicator.md @@ -0,0 +1,196 @@ +显示一个圆形的loading提示符号。 + +### 属性 + +
+ +
+

animating bool #

+
+

是否要显示指示器,默认为true,表示显示。

+
+
+
+

color string #

+
+

滚轮的前景颜色(默认为灰色)。

+
+
+
+

ioshidesWhenStopped bool #

+
+

在没有动画的时候,是否要隐藏指示器(默认为true)。

+
+
+
+

size enum('small', 'large') #

+
+

指示器的大小。small的高度为20,large为36。

+
+
+
+ +### 例子 + +```javascript +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const { + ActivityIndicator, + StyleSheet, + View, +} = ReactNative; +const TimerMixin = require('react-timer-mixin'); + +const ToggleAnimatingActivityIndicator = React.createClass({ + mixins: [TimerMixin], + + getInitialState() { + return { + animating: true, + }; + }, + + setToggleTimeout() { + this.setTimeout(() => { + this.setState({animating: !this.state.animating}); + this.setToggleTimeout(); + }, 2000); + }, + + componentDidMount() { + this.setToggleTimeout(); + }, + + render() { + return ( + + ); + } +}); + +exports.displayName = (undefined: ?string); +exports.framework = 'React'; +exports.title = ''; +exports.description = 'Animated loading indicators.'; + +exports.examples = [ + { + title: 'Default (small, white)', + render() { + return ( + + ); + } + }, + { + title: 'Gray', + render() { + return ( + + + + + ); + } + }, + { + title: 'Custom colors', + render() { + return ( + + + + + + + ); + } + }, + { + title: 'Large', + render() { + return ( + + ); + } + }, + { + title: 'Large, custom colors', + render() { + return ( + + + + + + + ); + } + }, + { + title: 'Start/stop', + render() { + return ; + } + }, + { + title: 'Custom size', + render() { + return ( + + ); + } + }, +]; + +const styles = StyleSheet.create({ + centering: { + alignItems: 'center', + justifyContent: 'center', + padding: 8, + }, + gray: { + backgroundColor: '#cccccc', + }, + horizontal: { + flexDirection: 'row', + justifyContent: 'space-around', + padding: 8, + }, +}); +``` \ No newline at end of file diff --git a/docs/docs/next/adsupportios.md b/docs/docs/0.41/adsupportios.md similarity index 100% rename from docs/docs/next/adsupportios.md rename to docs/docs/0.41/adsupportios.md diff --git a/docs/docs/0.41/alert.md b/docs/docs/0.41/alert.md new file mode 100644 index 0000000..9925778 --- /dev/null +++ b/docs/docs/0.41/alert.md @@ -0,0 +1,146 @@ +启动一个提示对话框,包含对应的标题和信息。 + +你还可以指定一系列的按钮,点击对应的按钮会调用对应的onPress回调并且关闭提示框。默认情况下,对话框会仅有一个'确定'按钮。 + +本接口可以在iOS和Android上显示一个静态的提示框。如果要在显示提示框的同时接受用户输入一些信息,那你可能需要[`AlertIOS`](alertios.html)。 + +### iOS +在iOS上你可以指定任意数量的按钮。每个按钮还都可以指定自己的样式,此外还可以指定提示的类别。参阅[AlertIOS](alertios.html)来了解更多细节。 + +### Android +在Android上最多能指定三个按钮,这三个按钮分别具有“中间态”、“消极态”和“积极态”的概念: + +如果你只指定一个按钮,则它具有“积极态”的属性(比如“确定”);两个按钮,则分别是“消极态”和“积极态”(比如“取消”和“确定”);三个按钮则意味着“中间态”、“消极态”和“积极态”(比如“稍候再说”,“取消”,“确定”)。 + +### 方法 + +
+

static alert(title: string, message?: string, button?: Buttons, type?: AlertType) #

+
+ +### 例子 + +```javascript +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + Alert, + StyleSheet, + Text, + TouchableHighlight, + View, +} = ReactNative; + +var UIExplorerBlock = require('./UIExplorerBlock'); + +// corporate ipsum > lorem ipsum +var alertMessage = 'Credibly reintermediate next-generation potentialities after goal-oriented ' + + 'catalysts for change. Dynamically revolutionize.'; + +/** + * Simple alert examples. + */ +var SimpleAlertExampleBlock = React.createClass({ + + render: function() { + return ( + + Alert.alert( + 'Alert Title', + alertMessage, + )}> + + Alert with message and default button + + + Alert.alert( + 'Alert Title', + alertMessage, + [ + {text: 'OK', onPress: () => console.log('OK Pressed!')}, + ] + )}> + + Alert with one button + + + Alert.alert( + 'Alert Title', + alertMessage, + [ + {text: 'Cancel', onPress: () => console.log('Cancel Pressed!')}, + {text: 'OK', onPress: () => console.log('OK Pressed!')}, + ] + )}> + + Alert with two buttons + + + Alert.alert( + 'Alert Title', + null, + [ + {text: 'Foo', onPress: () => console.log('Foo Pressed!')}, + {text: 'Bar', onPress: () => console.log('Bar Pressed!')}, + {text: 'Baz', onPress: () => console.log('Baz Pressed!')}, + ] + )}> + + Alert with three buttons + + + Alert.alert( + 'Foo Title', + alertMessage, + '..............'.split('').map((dot, index) => ({ + text: 'Button ' + index, + onPress: () => console.log('Pressed ' + index) + })) + )}> + + Alert with too many buttons + + + + ); + }, +}); + +var AlertExample = React.createClass({ + statics: { + title: 'Alert', + description: 'Alerts display a concise and informative message ' + + 'and prompt the user to make a decision.', + }, + render: function() { + return ( + + + + ); + } +}); + +var styles = StyleSheet.create({ + wrapper: { + borderRadius: 5, + marginBottom: 5, + }, + button: { + backgroundColor: '#eeeeee', + padding: 10, + }, +}); + +module.exports = { + AlertExample, + SimpleAlertExampleBlock, +}; +``` \ No newline at end of file diff --git a/docs/docs/0.41/alertios.md b/docs/docs/0.41/alertios.md new file mode 100644 index 0000000..a078b66 --- /dev/null +++ b/docs/docs/0.41/alertios.md @@ -0,0 +1,204 @@ +启动一个提示对话框,包含对应的标题和信息。 + +你还可以指定一系列的按钮,点击对应的按钮会调用对应的onPress回调并且关闭提示框。默认情况下,对话框会仅有一个'确定'按钮。 + +这个API主要用于需要iOS特有功能的场景,比如提示用户输入一些信息等。其他情况下,尤其是仅仅显示一个静态的提示框时,应该使用跨平台的[`Alert`](alert.html)接口。 + +```javascript +AlertIOS.alert( + 'Foo Title', + 'My Alert Msg', + [ + {text: 'Foo', onPress: () => console.log('Foo Pressed!')}, + {text: 'Bar', onPress: () => console.log('Bar Pressed!')}, + ] +) +``` +### 截图 +![alertios1](img/api/alertios1.png) + +![alertios2](img/api/alertios2.png) + +### 方法 + +
+

static alert(title: string, message?: string, buttons?: Array<{ + text?: string; + onPress?: ?Function; + style?: AlertButtonStyle; + }>, type?: AlertType) #

+

static prompt(title: string, value?: string, buttons?: Array<{ + text?: string; + onPress?: ?Function; + style?: AlertButtonStyle; + }>, callback?: Function) #

提示用户输入一些文字。

+
+ +### 例子 + +```javascript +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + StyleSheet, + View, + Text, + TouchableHighlight, + AlertIOS, +} = ReactNative; + +var { SimpleAlertExampleBlock } = require('./AlertExample'); + +exports.framework = 'React'; +exports.title = 'AlertIOS'; +exports.description = 'iOS alerts and action sheets'; +exports.examples = [{ + title: 'Alerts', + render() { + return ; + } +}, +{ + title: 'Prompt Options', + render(): ReactElement { + return ; + } +}, +{ + title: 'Prompt Types', + render() { + return ( + + AlertIOS.prompt('Plain Text Entry')}> + + + + plain-text + + + + + AlertIOS.prompt('Secure Text', null, null, 'secure-text')}> + + + + secure-text + + + + + AlertIOS.prompt('Login & Password', null, null, 'login-password')}> + + + + login-password + + + + + + ); + } +}]; + +class PromptOptions extends React.Component { + state: any; + customButtons: Array; + + constructor(props) { + super(props); + + // $FlowFixMe this seems to be a Flow bug, `saveResponse` is defined below + this.saveResponse = this.saveResponse.bind(this); + + this.customButtons = [{ + text: 'Custom OK', + onPress: this.saveResponse + }, { + text: 'Custom Cancel', + style: 'cancel', + }]; + + this.state = { + promptValue: undefined, + }; + } + + render() { + return ( + + + Prompt value: {this.state.promptValue} + + + AlertIOS.prompt('Type a value', null, this.saveResponse)}> + + + + prompt with title & callback + + + + + AlertIOS.prompt('Type a value', null, this.customButtons)}> + + + + prompt with title & custom buttons + + + + + AlertIOS.prompt('Type a value', null, this.saveResponse, undefined, 'Default value')}> + + + + prompt with title, callback & default value + + + + + AlertIOS.prompt('Type a value', null, this.customButtons, 'login-password', 'admin@site.com')}> + + + + prompt with title, custom buttons, login/password & default value + + + + + ); + } + + saveResponse(promptValue) { + this.setState({ promptValue: JSON.stringify(promptValue) }); + } +} + +var styles = StyleSheet.create({ + wrapper: { + borderRadius: 5, + marginBottom: 5, + }, + button: { + backgroundColor: '#eeeeee', + padding: 10, + }, +}); +``` \ No newline at end of file diff --git a/docs/docs/0.41/android-building-from-source.md b/docs/docs/0.41/android-building-from-source.md new file mode 100644 index 0000000..6eefebe --- /dev/null +++ b/docs/docs/0.41/android-building-from-source.md @@ -0,0 +1,117 @@ +如果你想使用新的功能,获得官方的修复补丁,尝试还没发布的最新特性,或者维护你自己的不能合并到核心版本的补丁,你可能需要自己从源代码编译React Native。 + +# 预备条件 + +如果你已经安装了安卓SDK,那么运行`android`命令打开安卓SDK管理器。 + +确保你已经安装了以下模块: + +* Android SDK version 23 (编译SDK版本号在[build.gradle](https://github.com/facebook/react-native/blob/master/ReactAndroid/build.gradle)中可以找到) +* SDK build tools version 23.0.1(编译工具版本号在[build.gradle](https://github.com/facebook/react-native/blob/master/ReactAndroid/build.gradle)中可以找到) +* Android Support Repository >= 17 +* Android NDK(下载及解压指南看[这里](http://developer.android.com/ndk/downloads/index.html)) + +将Gradle指向你的安卓SDK: 设置`$ANDROID_SDK`和`$ANDORID_NDK`为对应的目录,或者按照以下内容在react-native根目录下创建local.properties文件(注意:windows下需要使用反双斜杠)。 + +``` +sdk.dir=指向android sdk目录的绝对路径 +ndk.dir=指向android ndk目录的绝对路径 +``` +例如: + +``` +sdk.dir=/Users/your_unix_name/android-sdk-macosx +ndk.dir=/Users/your_unix_name/android-ndk/android-ndk-r10e +``` +# 从下载链接安装Android NDK + +1. Mac OS (64-bit) - http://dl.google.com/android/repository/android-ndk-r10e-darwin-x86_64.zip +2. Linux (64-bit) - http://dl.google.com/android/repository/android-ndk-r10e-linux-x86_64.zip +3. Windows (64-bit) - http://dl.google.com/android/repository/android-ndk-r10e-windows-x86_64.zip +4. Windows (32-bit) - http://dl.google.com/android/repository/android-ndk-r10e-windows-x86.zip + +更多参考您可以访问官网NDK界面 [official page](http://developer.android.com/ndk/downloads/index.html). + +__译注__:建议安装r10e版本,否则在编译过程可能会出错 +# 编译源代码: + +## 1.在你的分支代码中进行安装 + +首先,在你的分支代码中安装react-native。例如从官方地址安装主干版本: + +``` +npm install --save github:facebook/react-native#master +``` + +或者,你也可以把仓库克隆到你的`node_modules`目录,然后运行`npm install`进行安装 + +## 2.添加gradle依赖 + +在`android/build.gradle`中添加`gradle-download-task`依赖 + +``` +... + dependencies { + classpath 'com.android.tools.build:gradle:1.3.1' + classpath 'de.undercouch:gradle-download-task:3.1.2' + + // 注意:不要把你的应用的依赖放在这里; + // 它们应该放在各自模块的build.gradle文件中 + } +... +``` + +## 添加`:ReactAndroid `项目 + +在`android/settings.gradle`中添加`:ReactAndroid`项目 + +``` +... +include ':ReactAndroid' + +project(':ReactAndroid').projectDir = new File(rootProject.projectDir, '../node_modules/react-native/ReactAndroid') +... +``` + +修改你的`android/app/build.gradle`文件,使用`:ReactAndroid`替换预编译库。例如用`compile project(':ReactAndroid'):`替换`compile 'com.facebook.react:react-native:0.16.+'` + +``` +... +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:23.0.1' + + compile project(':ReactAndroid') + + ... +} +... +``` + +## 让第三方模块使用你的分支 +如果你使用第三方的React Native模块,你需要重写它们的依赖以避免它们仍然打包官方的预编译库。否则当你编译时会报错-`Error: more than one library with package name 'com.facebook.react'.`(错误:有几个重名的'com.facebook.react'的包) + +修改你的`android/app/build.gradle`文件,添加如下内容: + +``` +configurations.all { + exclude group: 'com.facebook.react', module: 'react-native' +} +``` + +# 在Android Studio中构建您的项目 + +在Android Studio欢迎页中选择`Import project`,随后选择您应用所在的文件夹。 + +您还需要使用_Run_按钮(__译注__:Android Studio中绿色的运行按钮)来在设备上运行您的app,此外Android Studio不会自动开启服务,你还需要通过`npm start`来启动。 + +# 其他注意事项 +从源码进行编译将会花费很长时间,尤其是第一次编译,需要下载接近200M的文件然后编译原生代码。每次你在自己的仓库更新`react-native`版本时,构建的目录可能会被删除,所有的文件都需要重新下载。为了避免构建目录被删,你需要编辑`~/.gradle/init.gradle`文件来修改构建目录路径。 + +``` +gradle.projectsLoaded { + rootProject.allprojects { + buildDir = "/path/to/build/directory/${rootProject.name}/${project.name}" + } +} +``` diff --git a/docs/docs/0.41/android-setup.md b/docs/docs/0.41/android-setup.md new file mode 100644 index 0000000..549954c --- /dev/null +++ b/docs/docs/0.41/android-setup.md @@ -0,0 +1,86 @@ +本指南主要介绍在Android模拟器上运行React Native Android应用所必须的准备步骤。 + +### 安装Git + + - **Mac**上如果你已经安装了[XCode](https://developer.apple.com/xcode/),那么Git也就随之安装了,否则请使用homebrew进行安装: + + brew install git + + - **Linux**上请使用你系统对应的[包管理器](https://git-scm.com/download/linux)来安装Git。 + + - **Windows**上请下载并安装[Git for Windows](https://git-for-windows.github.io/)。在安装过程中,请务必记得勾选`Run Git from Windows Command Prompt`,这样会把Git的可执行程序加入到`PATH`环境变量中,这样其他程序才能在命令行中正确调用Git。 + + +### 安装Android SDK(已安装的请跳过这一步) + +1. [安装最新版的JDK](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) + +2. 安装Android SDK: + - **Mac**: `brew install android-sdk` + - **Linux或Windows**: [从Android开发者官网下载](https://developer.android.com/sdk/installing/index.html) +__译注__:国内用户推荐从[AndroidDevTools](http://androiddevtools.cn/)下载。 + +### 定义ANDROID_HOME环境变量 + +__重要__: 确保`ANDROID_HOME`环境变量指向你已经安装的Android SDK目录: + + - **Mac**, 往你的`~/.bashrc`, `~/.bash_profile` 或者你终端所用的其它配置文件中增加以下内容: + (__译注__:~表示用户目录,即`/Users/你的用户名/`,而小数点开头的文件在Finder中是隐藏的,并且这两个文件有可能还没有被创建。请在终端下使用`sudo vi ~/.bashrc`命令创建或编辑。如不熟悉vi操作,请点击[这里](http://www.eepw.com.cn/article/48018.htm)学习) + + # 如果你是通过Homebrew安装SDK的,则加入下列路径 + export ANDROID_HOME=/usr/local/opt/android-sdk + # 否则可能是(当然具体视你把SDK放在哪): + export ANDROID_HOME=~/Library/Android/sdk + - **Linux**,往你的`~/.bashrc`, `~/.bash_profile` 或者你终端所用的其它配置文件中增加以下内容: + + export ANDROID_HOME=<你把Android SDK解压后放置的位置> + + - **Windows**,打开控制面板,选择`系统和安全`->`系统`->`高级系统设置`->`高级`->`环境变量`->`新建`,变量名填写ANDROID_HOME,变量值填写你把Android SDK解压后放置的位置。 + +__译注__: 如果你在windows下找不到对应的控制面板项,也可以右键点击`我的电脑`,然后在菜单中选择`属性`,然后选择`高级系统设置`->`高级`->`环境变量`->`新建`。__注意__:必须将现有的CMD窗口全部关闭,重新打开后新的环境变量才能生效。 + + +### 开启gradle daemon + +React Native Android使用的构建系统是[gradle](https://docs.gradle.org)。我们建议你开启gradle daemon功能,它可以带来高达50%的java编译速度提升。点击[这里](https://docs.gradle.org/2.9/userguide/gradle_daemon.html)来了解如何针对你的平台开启这一功能。 + + +### 设置SDK + +1. 打开Android SDK Manager(**Mac**用户在终端下输入`android`)。 +2. 选中以下项目: + * Android SDK Build-tools version 23.0.1(这个必须版本严格匹配23.0.1) + * Android 6.0 (API 23) + * Local Maven repository for Support Libraries(之前叫做Android Support Repository) +3. 点击"Install Packages" + (__译注__:国内用户推荐使用[腾讯Bugly的镜像](http://android-mirror.bugly.qq.com:8080/include/usage.html)来加速下载) +![SDK Manager窗口](img/AndroidSDK1.png) ![SDK Manager 窗口](img/AndroidSDK2.png) + +### 安装Genymotion + +Genymotion是一个第三方模拟器,它比Google官方的模拟器更易设置且性能更好。但是,它只针对个人用户免费。如果你想使用Google模拟器,请往下看。 + +1. 下载并安装[Genymotion](https://www.genymotion.com/)。 +2. 打开Genymotion。如果你尚未安装VirtualBox,它有可能会提示你安装。 +3. 创建一个模拟器并启动。 +4. 按下`⌘+M`可以打开开发者菜单(在安装并启动了React Native应用之后)。 + +### 备选方案:使用Google官方模拟器 + +1. 打开Android SDK Manager(参见"设置SDK"一步) +2. 选中以下项目: + * Intel x86 Atom System Image (for Android 5.1.1 - API 22) + * Intel x86 Emulator Accelerator (HAXM installer) +3. 点击"Install Packages" +4. [配置硬件加速(HAXM)](http://developer.android.com/tools/devices/emulator.html#vm-mac),否则模拟器会运行的相当缓慢。 +5. 创建Android虚拟设备(AVD): + 1. 运行`android avd`并且点击**Create...** + (__译注__:在Windows系统下,android.bat在Android SDK的`tools`文件夹下,请注意设置PATH环境变量以便于使用) + ![创建虚拟设备对话框](img/CreateAVD.png) + 2. 选中新创建的虚拟设备,并点击`Start...` + +__译注__:对于Windows用户而言,Intel x86 Emulator Accelerator和HyperV(系统内置的虚拟机功能)不能同时启用。所以要么选择关闭HyperV(控制面板-程序-启动和关闭Windows功能,取消选择HyperV并点确定),要么选择Genymotion、Bluestacks或Visual Studio Emulator for Android作为模拟器。 + +### 在Android Studio中编辑Java代码 + +对于JavaScript代码,你可以使用任何编辑器来编辑。如果你想在Android Studio中编辑原生Java代码的话,请在Android Studio的欢迎屏幕上选择"Import project",然后选择你的项目目录中的`android`文件夹即可。 diff --git a/docs/docs/0.41/android-ui-performance.md b/docs/docs/0.41/android-ui-performance.md new file mode 100644 index 0000000..842da69 --- /dev/null +++ b/docs/docs/0.41/android-ui-performance.md @@ -0,0 +1,147 @@ +我们尽最大的努力来争取使UI组件的性能如丝般顺滑,但有的时候这根本不可能做到。要知道,Android有超过一万种不同型号的手机,而在框架底层进行软件渲染的时候是统一处理的,这意味着你没办法像iOS那样自由。不过有些时候,你还是可以想办法提升应用的性能(有的时候问题根本不是出在原生代码上!) + +要想解决应用的性能问题,第一步就是搞明白在每个16毫秒的帧中,时间都去哪儿了。为此,我们会使用一个标准的Android性能分析工具`systrace`,不过在此之前…… + +> 请先确定JS的开发者模式已经关闭! +> +> 你应该在应用的日志里看到`__DEV__ === false, development-level warning are OFF, performance optimizations are ON`等字样(你可以通过adb logcat来查看应用日志) + +## 使用Systrace进行性能分析 + +Systrace是一个标准的基于标记的Android性能分析工具(如果你安装了Android platform-tool包,它也会一同安装)。被调试的代码段在开始和结束处加上标记,在执行的过程中标记会被记录,最后会以图表形式展现统计结果。包括Android SDK自己和React Native框架都已经提供了标准的标记供你查看。 + +### 收集一次数据 + +> 注意: +> +> Systrace从React Native `v0.15`版本开始支持。你需要在此版本下构建项目才能收集相应的性能数据。 + +首先,把你想分析的、运行不流畅的设备使用USB线链接到电脑上,然后操作应用来到你想分析的导航/动画之前,接着这样运行systrace: + +``` +$ /platform-tools/systrace/systrace.py --time=10 -o trace.html sched gfx view -a <你的应用包名> +``` + +对于此命令做一个简单的说明: + +- `time`参数控制本次数据收集的持续时间,单位是秒。 +- `schd`, `gfx`, 和`view`是我们所关心的Android SDK内置的tag(标记的集合):`schd`提供了你的设备的每个CPU核心正在做什么的信息,`gfx`提供了你的图形相关信息,譬如每帧的时间范围,而`view`提供了一些关于视图布局和渲染相关性能的信息。 +- `-a <你的应用包名>`启用了针对应用的过滤。在这里填写你用React Native创建的应用包名。`你的应用包名`可以在你应用中的`AndroidManifest.xml`里找到,形如`com.example.app` + +_译注_:实际上,AndroidManifest.xml里的应用包名会被`app/build.gradle`里的`applicationId`取代。如果二者不一致,应当以`app/build.gradle`里的为准。 + +一旦systrace开始收集数据,你可以操作应用执行你所关心的动画和操作。在收集结束后,systrace会给你提供一个链接,你可以在浏览器中打开这个链接来查看数据收集的结果。 + +## 查看性能数据 + +在浏览器中打开数据页面(建议使用Chrome),你应该能看到类似这样的结果: + +![Example](img/SystraceExample.png) + +**提示**: 你可以使用WSAD键来滚动和缩放性能数据图表。 + +### 启用垂直同步高亮 + +接下来你首先应该启用16毫秒帧区间的高亮。在屏幕顶端点击对应的复选框: + +![Enable VSync Highlighting](img/SystraceHighlightVSync.png) + +然后你应该能在屏幕上看到类似上图的斑马状条纹。如果你无法看到这样的条纹,可以尝试换一台设备来进行分析:部分三星手机显示垂直同步高亮存在已知问题,而Nexus系列大部分情况都相当可靠。 + +### 找到你的进程 + +滚动图表直到你找到你的应用包名。在上面的例子里,我正在分析`com.facebook.adsmanager`,由于内核的线程名字长度限制,它会显示成`book.adsmanager`。 + +在左侧,你应该能看到一系列线程对应着右边的时间轴。有3到4个线程是我们必须关注的:UI线程(名字可能是`UI Thread`或者是你的包名), `mqt_js`和`mqt_native_modules`。如果你在Android 5.0以上版本运行,我们还需要关注`Render`(渲染)线程。 + +### UI 线程 + +标准的Android布局和绘制都在UI线程里发生。右侧显示的线程名字会是你的包名(在我的例子里是book.adsmanager)或者UI Thread.你在这个线程里看到的事件可能会是一些`Choreographer`, `traversals`或者`DispatchUI`: + +![UI Thread Example](img/SystraceUIThreadExample.png) + +### JS线程 + +这是用于执行JavaScript代码的线程。根据Android系统版本或者设备的不同,线程名可能是`mqt_js`或者`<...>`。如果看不到对应的名字的话,寻找类似`JSCall`,`Bridge.executeJSCall`这样的事件。 + +![JS Thread Example](img/SystraceJSThreadExample.png) + +### 原生模块线程 + +这里是用于原生模块执行代码(譬如`UIManager`)的线程,线程名可能是`mqt_native_modules`或`<...>`。在后一种情况下,寻找类似`NativeCall`, `CallJavaModuleMethod`, 还有`onBatchComplete`这样的事件名: + +![Native Modules Thread Example](img/SystraceNativeModulesThreadExample.png) + +### 额外的:渲染线程 + +如果你在使用Android L(5.0)或者更高版本,你应该还会在你的应用里看到一个渲染线程。这个线程真正生成OpenGL渲染序列来渲染你的UI。这个线程的名字可能为`RenderThread`或者`<...>`,在后一种情况下,寻找类似`DrawFrame`或`queueBuffer`这样的事件: + +![Render Thread Example](img/SystraceRenderThreadExample.png) + +## 寻找导致卡顿的罪魁祸首 + +一个流畅的动画应该看起来像这样: + +![Smooth Animation](img/SystraceWellBehaved.png) + +每个背景颜色不同的部分我们称作“一帧”——记住要渲染一个流畅的帧,我们所有的界面工作都需要在16毫秒内完成。注意没有任何一个线程在靠近帧的边界处工作。类似这样的一个应用程序就正在60FPS(帧每秒)的情况下流畅表现。 + +如果你发现一些起伏的地方,譬如这样: + +![Choppy Animation from JS](img/SystraceBadJS.png) + +注意在上图中JS线程基本上一直在执行,并且超越了帧的边界。这个应用就没法以60FPS渲染了。在这种情况下,**问题出在JS中**。 + +你还有可能会看到一些类似这样的东西: + +![Choppy Animation from UI](img/SystraceBadUI.png) + +在这种情况下,UI和渲染线程有一些重负荷的工作,以至于超越了帧的边界。这可能是由于我们每帧试图渲染的UI太多了导致的。在这种情况下,**问题出在需要渲染的原生视图上**。 + +并且,你还应该能看到一些可以指导接下来优化工作的有用的信息。 + +## JS的问题 + +如果你发现问题出在JS上,在你正在执行的JS代码中寻找线索。在上面的图中,我们会发现`RCTEventEmitter`每帧被执行了很多次。这是上面的数据统计放大后的内容: + +![Too much JS](img/SystraceBadJS2.png) + +这看起来不是很正常,为什么事件被调用的如此频繁?它们是不同的事件吗?具体的答案取决于你的产品的代码。在许多情况下,你可能需要看看[shouldComponentUpdate](https://facebook.github.io/react/docs/component-specs.html#updating-shouldcomponentupdate)的介绍。 + +> **TODO**: 我们还在准备更多的JS性能分析的工具,会在将来的版本中加入。 + +## 原生UI问题 + +如果你发现问题出在原生UI上,有两种常见的情况: + +1. 你每帧在渲染的UI给GPU带来了太重的负载,或者: +2. 你在动画、交互的过程中不断创建新的UI对象(譬如在scroll的过程中加载新的内容) + +### GPU负担过重 + +在第一种情况下,你应该能看到UI线程的图表类似这样: + +![Overloaded GPU](img/SystraceBadUI.png) + +注意`DrawFrame`花费了很多时间,超越了帧的边界。这些时间用来等待GPU获取它的操作缓存。 + +要缓解这个问题,你应该: + +- 检查`renderToHardwareTextureAndroid`的使用,有这个属性的View的子节点正在进行动画或变形会导致性能大幅下降(譬如`Navigator`提供的滑动、淡入淡出动画)。 +- 确保你**没有**使用`needsOffscreenAlphaCompositing`,这个默认是关闭的,因为它在大部分情况下都会带来GPU消耗的大幅提升。 + +如果这还不能帮你解决问题,你可能需要更深入的探索GPU到底在做什么。参见[Tracer for OpenGL ES](http://developer.android.com/tools/help/gltracer.html)。 + +### 在UI线程创建大量视图 + +如果是第二种情况,你可能会看到类似这样的结果: + +![Creating Views](img/SystraceBadCreateUI.png) + +注意一开始JS线程工作了很久,然后你看到原生模块线程干了些事情,最后带来了UI线程的巨大开销。 + +这个问题并没有什么简单直接的优化办法,除非你能把创建UI的步骤推迟到交互结束以后去进行,或者你能直接简化你所要创建的UI。React Native小组正在架构层设法提供一个方案,使得新的UI视图可以在主线程之外去创建和配置,这样就可以使得交互变得更加流畅。 + +## 还是没搞定? + +如果你还是很迷惑或者不知如何进展,你可以在[Stack Overflow的react-native标签下](http://stackoverflow.com/tags/react-native)提交一个问题。如果你在这里得不到响应,或者找到了一个核心组件的问题,你可以[提交一个Github issue](https://github.com/facebook/react-native/issues)给我们。 diff --git a/docs/docs/0.41/animated.md b/docs/docs/0.41/animated.md new file mode 100644 index 0000000..3fa515c --- /dev/null +++ b/docs/docs/0.41/animated.md @@ -0,0 +1,535 @@ +动画是现代用户体验中非常重要的一个部分,`Animated`库就是用来创造流畅、强大、并且易于构建和维护的动画。 + +最简单的工作流程就是创建一个`Animated.Value`,把它绑定到组件的一个或多个样式属性上。然后可以通过动画驱动它,譬如`Animated.timing`,或者通过`Animated.event`把它关联到一个手势上,譬如拖动或者滑动操作。除了样式,`Animated.value`还可以绑定到props上,并且一样可以被插值。这里有一个简单的例子,一个容器视图会在加载的时候淡入显示: + +```javascript +class FadeInView extends React.Component { + constructor(props) { + super(props); + this.state = { + fadeAnim: new Animated.Value(0), // init opacity 0 + }; + } + componentDidMount() { + Animated.timing( // Uses easing functions + this.state.fadeAnim, // The value to drive + {toValue: 1}, // Configuration + ).start(); // Don't forget start! + } + render() { + return ( + // Binds + {this.props.children} + + ); + } + } + ``` + + + 注意只有声明为可动画化的组件才能被关联动画。`View`、`Text`,还有`Image`都是可动画化的。如果你想让自定义组件可动画化,可以用`createAnimatedComponent`。这些特殊的组件里面用了一些黑魔法,来把动画数值绑定到属性上,然后在每帧去执行原生更新,来避免每次render和同步过程的开销。他们还处理了在节点卸载时的清理工作以确保使用安全。 + + 动画具备很强的可配置性。自定义或者预定义的过渡函数、延迟、时间、衰减比例、刚度等等。取决于动画类型的不同,你还可以配置更多的参数。 + + 一个`Animated.Value`可以驱动任意数量的属性,并且每个属性可以配置一个不同的插值函数。插值函数把一个输入的范围映射到输出的范围,通常我们用线性插值,不过你也可以使用其他的过渡函数。默认情况下,当输入超出范围时,它也会对应的进行转换,不过你也可以把输出约束到范围之内。  + + 举个例子,你可能希望你的`Animated.Value`从0变化到1时,把组件的位置从150px移动到0px,不透明度从0到1。可以通过以下的方法修改`style`属性来实现: + + ```javascript + style={{ + opacity: this.state.fadeAnim, // Binds directly + transform: [{ + translateY: this.state.fadeAnim.interpolate({ + inputRange: [0, 1], + outputRange: [150, 0] // 0 : 150, 0.5 : 75, 1 : 0 + }), + }], + }}> + ``` + + 动画还可以被更复杂地组合,通过一些辅助函数例如`sequence`或者`parallel`(它们分别用于先后执行多个动画和同时执行多个动画),而且还可以通过把toValue设置为另一个Animated.Value来产生一个动画序列。 + + `Animated.ValueXY`则用来处理一些2D动画,譬如滑动。并且还有一些辅助功能譬如`setOffset`和`getLayout`来帮助实现一些常见的交互效果,譬如拖放操作(Drag and drop)。 + + 你可以在`AnimationExample.js`中找到一些更复杂的例子。你还可以看看Gratuitous Animation App,以及[动画指南文档](animations.html)。 + +注意`Animated`模块被设计为可完全序列化的,这样动画可以脱离JavaScript事件循环,以一种高性能的方式运行。这可能会导致API看起来比较难懂,与一个完全同步的动画系统相比稍微有一些奇怪。`Animated.Value.addListener`可以帮助你解决一些相关限制,不过使用它的时候需要小心,因为将来的版本中它可能会牵扯到性能问题。 + +### 方法 + +
+
+

static decay(value: AnimatedValue | AnimatedValueXY, config: DecayAnimationConfig) #

+
+

推动一个值以一个初始的速度和一个衰减系数逐渐变为0。

+
+
+
+

static timing(value: AnimatedValue | AnimatedValueXY, config: TimingAnimationConfig) #

+
+

推动一个值按照一个过渡曲线而随时间变化。Easing模块定义了一大堆曲线,你也可以使用你自己的函数。

+
+
+
+

static spring(value: AnimatedValue | AnimatedValueXY, config: SpringAnimationConfig) #

+
+

产生一个基于Rebound和Origami实现的Spring动画。它会在toValue值更新的同时跟踪当前的速度状态,以确保动画连贯。可以链式调用。

+
+
+

static add(a: Animated, b: Animated) #

+

将两个动画值相加计算,创建一个新的动画值。

+

static multiply(a: Animated, b: Animated) #

+

将两个动画值相乘计算,创建一个新的动画值。

+
+

static delay(time: number) #

+
+

在指定的延迟之后开始动画。

+
+
+
+

static sequence(animations: Array<CompositeAnimation>) #

+
+

按顺序执行一个动画数组里的动画,等待一个完成后再执行下一个。如果当前的动画被中止,后面的动画则不会继续执行。

+
+
+
+

static parallel(animations: Array<CompositeAnimation>, config?: ParallelConfig) #

+
+

同时开始一个动画数组里的全部动画。默认情况下,如果有任何一个动画停止了,其余的也会被停止。你可以通过stopTogether选项来改变这个效果。

+
+
+
+

static stagger(time: number, animations: Array<CompositeAnimation>) #

+
+

一个动画数组,里面的动画有可能会同时执行(重叠),不过会以指定的延迟来开始。用来制作拖尾效果非常合适。

+
+
+
+

static event(argMapping: Array<Mapping>, config?: EventConfig) #

+
+

接受一个映射的数组,对应的解开每个值,然后调用所有对应的输出的setValue方法。例如:

+
 onScroll={this.AnimatedEvent(
+   [{nativeEvent: {contentOffset: {x: this._scrollX}}}]
+   {listener},          // 可选的异步监听函数
+ )
+ ...
+ onPanResponderMove: this.AnimatedEvent([
+   null,                // 忽略原始事件
+   {dx: this._panX},    // 手势状态参数
+ ]),
+
+
+
+
+

static createAnimatedComponent(Component: any) #

+
+

使得任何一个React组件支持动画。用它来创建Animated.View等等。

+
+
+
+ +### 属性 + +
+
+

Value: AnimatedValue #

+
+

表示一个数值的类,用于驱动动画。通常用new Animated.Value(0);来初始化。

+
+
+
+

ValueXY: AnimatedValueXY #

+
+

表示一个2D值的类,用来驱动2D动画,例如拖动操作等。

+
+
+
+ +## class AnimatedValue + +用于驱动动画的标准值。一个`Animated.Value`可以用一种同步的方式驱动多个属性,但同时只能被一个行为所驱动。启用一个新的行为(譬如开始一个新的动画,或者运行`setValue`)会停止任何之前的动作。 + +### 方法 + +
+
+

constructor(value: number) #

+
+
+

setValue(value: number) #

+
+

直接设置它的值。这个会停止任何正在进行的动画,然后更新所有绑定的属性。

+
+
+
+

setOffset(offset: number) #

+
+

设置一个相对值,不论接下来的值是由setValue、一个动画,还是Animated.event产生的,都会加上这个值。常用来在拖动操作一开始的时候用来记录一个修正值(譬如当前手指位置和View位置)。

+
+
+
+

flattenOffset() #

+
+

把当前的相对值合并到值里,并且将相对值设为0。最终输出的值不会变化。常在拖动操作结束后调用。

+
+
+
+

addListener(callback: ValueListenerCallback) #

+
+

添加一个异步监听函数,这样你就可以监听动画值的变更。这有时候很有用,因为你没办法同步的读取动画的当前值,因为有时候动画会在原生层次运行。

+
+
+
+

removeListener(id: string) #

+
+
+

removeAllListeners() #

+
+
+

stopAnimation(callback?: ?(value: number) => void) #

+
+

停止任何正在运行的动画或跟踪值。callback会被调用,参数是动画结束后的最终值,这个值可能会用于同步更新状态与动画位置。

+
+
+
+

interpolate(config: InterpolationConfigType) #

+
+

在更新属性之前对值进行插值。譬如:把0-1映射到0-10。

+
+
+
+

animate(animation: Animation, callback: EndCallback) #

+
+

一般仅供内部使用。不过有可能一个自定义的动画类会用到此方法。

+
+
+
+

stopTracking() #

+
+

仅供内部使用。

+
+
+
+

track(tracking: Animated) #

+
+

仅供内部使用。

+
+
+
+ +## class AnimatedValueXY + +用来驱动2D动画的2D值,譬如滑动操作等。API和普通的`Animated.Value`几乎一样,只不过是个复合结构。它实际上包含两个普通的`Animated.Value`。 + +例子: + +```javascript +class DraggableView extends React.Component { + constructor(props) { + super(props); + this.state = { + pan: new Animated.ValueXY(), // inits to zero + }; + this.state.panResponder = PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onPanResponderMove: Animated.event([null, { + dx: this.state.pan.x, // x,y are Animated.Value + dy: this.state.pan.y, + }]), + onPanResponderRelease: () => { + Animated.spring( + this.state.pan, // Auto-multiplexed + {toValue: {x: 0, y: 0}} // Back to zero + ).start(); + }, + }); + } + render() { + return ( + + {this.props.children} + + ); + } + } +``` + +### 方法 + +
+
+

constructor(valueIn?: ?{x: number | AnimatedValue; y: number | AnimatedValue}) #

+ +
+
+

setValue(value: {x: number; y: number}) #

+ +
+
+

setOffset(offset: {x: number; y: number}) #

+ +
+
+

flattenOffset() #

+ +
+
+

stopAnimation(callback?: ?() => number) #

+ +
+
+

addListener(callback: ValueXYListenerCallback) #

+ +
+
+

removeListener(id: string) #

+ +
+
+

getLayout() #

+
+

将一个{x, y}组合转换为{left, top}以用于样式。例如:

+
 style={this.state.anim.getLayout()}
+
+
+
+
+

getTranslateTransform() #

+
+

将一个{x, y} 组合转换为一个可用的位移变换(translation transform),例如:

+
 style={{
+   transform: this.state.anim.getTranslateTransform()
+ }}
+
+
+
+
+ + ### 例子 + + ```javascript +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + Animated, + Easing, + StyleSheet, + Text, + View, +} = ReactNative; +var UIExplorerButton = require('./UIExplorerButton'); + +exports.framework = 'React'; +exports.title = 'Animated - Examples'; +exports.description = 'Animated provides a powerful ' + + 'and easy-to-use API for building modern, ' + + 'interactive user experiences.'; + +exports.examples = [ + { + title: 'FadeInView', + description: 'Uses a simple timing animation to ' + + 'bring opacity from 0 to 1 when the component ' + + 'mounts.', + render: function() { + class FadeInView extends React.Component { + state: any; + + constructor(props) { + super(props); + this.state = { + fadeAnim: new Animated.Value(0), // opacity 0 + }; + } + componentDidMount() { + Animated.timing( // Uses easing functions + this.state.fadeAnim, // The value to drive + { + toValue: 1, // Target + duration: 2000, // Configuration + }, + ).start(); // Don't forget start! + } + render() { + return ( + + {this.props.children} + + ); + } + } + class FadeInExample extends React.Component { + state: any; + + constructor(props) { + super(props); + this.state = { + show: true, + }; + } + render() { + return ( + + { + this.setState((state) => ( + {show: !state.show} + )); + }}> + Press to {this.state.show ? + 'Hide' : 'Show'} + + {this.state.show && + + FadeInView + + } + + ); + } + } + return ; + }, + }, + { + title: 'Transform Bounce', + description: 'One `Animated.Value` is driven by a ' + + 'spring with custom constants and mapped to an ' + + 'ordered set of transforms. Each transform has ' + + 'an interpolation to convert the value into the ' + + 'right range and units.', + render: function() { + this.anim = this.anim || new Animated.Value(0); + return ( + + { + Animated.spring(this.anim, { + toValue: 0, // Returns to the start + velocity: 3, // Velocity makes it move + tension: -10, // Slow + friction: 1, // Oscillate a lot + }).start(); }}> + Press to Fling it! + + + Transforms! + + + ); + }, + }, + { + title: 'Composite Animations with Easing', + description: 'Sequence, parallel, delay, and ' + + 'stagger with different easing functions.', + render: function() { + this.anims = this.anims || [1,2,3].map( + () => new Animated.Value(0) + ); + return ( + + { + var timing = Animated.timing; + Animated.sequence([ // One after the other + timing(this.anims[0], { + toValue: 200, + easing: Easing.linear, + }), + Animated.delay(400), // Use with sequence + timing(this.anims[0], { + toValue: 0, + easing: Easing.elastic(2), // Springy + }), + Animated.delay(400), + Animated.stagger(200, + this.anims.map((anim) => timing( + anim, {toValue: 200} + )).concat( + this.anims.map((anim) => timing( + anim, {toValue: 0} + ))), + ), + Animated.delay(400), + Animated.parallel([ + Easing.inOut(Easing.quad), // Symmetric + Easing.back(1.5), // Goes backwards first + Easing.ease // Default bezier + ].map((easing, ii) => ( + timing(this.anims[ii], { + toValue: 320, easing, duration: 3000, + }) + ))), + Animated.delay(400), + Animated.stagger(200, + this.anims.map((anim) => timing(anim, { + toValue: 0, + easing: Easing.bounce, // Like a ball + duration: 2000, + })), + ), + ]).start(); }}> + Press to Animate + + {['Composite', 'Easing', 'Animations!'].map( + (text, ii) => ( + + {text} + + ) + )} + + ); + }, + }, + { + title: 'Continuous Interactions', + description: 'Gesture events, chaining, 2D ' + + 'values, interrupting and transitioning ' + + 'animations, etc.', + render: () => ( + Checkout the Gratuitous Animation App! + ), + } +]; + +var styles = StyleSheet.create({ + content: { + backgroundColor: 'deepskyblue', + borderWidth: 1, + borderColor: 'dodgerblue', + padding: 20, + margin: 20, + borderRadius: 10, + alignItems: 'center', + }, +}); + ``` \ No newline at end of file diff --git a/docs/docs/0.41/animations.md b/docs/docs/0.41/animations.md new file mode 100644 index 0000000..75cd3c4 --- /dev/null +++ b/docs/docs/0.41/animations.md @@ -0,0 +1,414 @@ +流畅、有意义的动画对于移动应用用户体验来说是非常必要的。和React Native的其他部分一样,动画API也还在积极开发中,不过我们已经可以联合使用两个互补的系统:用于全局的布局动画`LayoutAnimation`,和用于创建更精细的交互控制的动画`Animated`。 + +### Animated + +`Animated`库使得开发者可以非常容易地实现各种各样的动画和交互方式,并且具备极高的性能。`Animated`仅关注动画的输入与输出声明,在其中建立一个可配置的变化函数,然后使用简单的`start/stop`方法来控制动画按顺序执行。下面是一个在加载时带有简单的弹跳动画的组件示例: + +```javascript +class Playground extends React.Component { + constructor(props: any) { + super(props); + this.state = { + bounceValue: new Animated.Value(0), + }; + } + render(): ReactElement { + return ( + + ); + } + componentDidMount() { + this.state.bounceValue.setValue(1.5); // 设置一个较大的初始值 + Animated.spring( // 可选的基本动画类型: spring, decay, timing + this.state.bounceValue, // 将`bounceValue`值动画化 + { + toValue: 0.8, // 将其值以动画的形式改到一个较小值 + friction: 1, // Bouncier spring + } + ).start(); // 开始执行动画 + } +} +``` + +`bounceValue`在构造函数中初始化为`state`的一部分,然后和图片的缩放比例进行绑定。在动画执行的背后,其数值会被不断的计算并用于设置缩放比例。当组件刚刚挂载的时候,缩放比例被设置到1.5。然后紧跟着在`bounceValue`上执行了一个弹跳动画(spring),会逐帧刷新数值,并同步更新所有依赖本数值的绑定(在这个例子里,就是图片的缩放比例)。比起调用`setState`然后重新渲染,这一运行过程要快得多。因为整个配置都是声明式的,我们可以实现更进一步的优化,只要序列化好配置,然后我们可以在一个高优先级的线程执行动画。 + +#### 核心API + +大部分你需要的东西都来自`Animated`模块。它包括两个值类型,`Value`用于单个的值,而`ValueXY`用于向量值;还包括三种动画类型,`spring`,`decay`,还有`timing`,以及三种组件类型,`View`,`Text`和`Image`。你可以使用`Animated.createAnimatedComponent`方法来对其它类型的组件创建动画。 + +这三种动画类型可以用来创建几乎任何你需要的动画曲线,因为它们每一个都可以被自定义: + +* `spring`: 基础的单次弹跳物理模型,符合[Origami设计标准](https://facebook.github.io/origami/) + * `friction`: 摩擦力,默认为7. + * `tension`: 张力,默认40。 +* `decay`: 以一个初始速度开始并且逐渐减慢停止。 + * `velocity`: 起始速度,必填参数。 + * `deceleration`: 速度衰减比例,默认为0.997。 +* `timing`: 从时间范围映射到渐变的值。 + * `duration`: 动画持续的时间(单位是毫秒),默认为500。 + * `easing`:一个用于定义曲线的渐变函数。阅读`Easing`模块可以找到许多预定义的函数。iOS默认为`Easing.inOut(Easing.ease)`。 + * `delay`: 在一段时间之后开始动画(单位是毫秒),默认为0。 + +动画可以通过调用`start`方法来开始。`start`接受一个回调函数,当动画结束的时候会调用此回调函数。如果动画是因为正常播放完成而结束的,回调函数被调用时的参数为`{finished: true}`,但若动画是在结束之前被调用了`stop`而结束(可能是被一个手势或者其它的动画打断),它会收到参数`{finished: false}`。 + +#### 组合动画 + +多个动画可以通过`parallel`(同时执行)、`sequence`(顺序执行)、`stagger`和`delay`来组合使用。它们中的每一个都接受一个要执行的动画数组,并且自动在适当的时候调用start/stop。举个例子: + +```javascript +Animated.sequence([ // 首先执行decay动画,结束后同时执行spring和twirl动画 + Animated.decay(position, { // 滑行一段距离后停止 + velocity: {x: gestureState.vx, y: gestureState.vy}, // 根据用户的手势设置速度 + deceleration: 0.997, + }), + Animated.parallel([ // 在decay之后并行执行: + Animated.spring(position, { + toValue: {x: 0, y: 0} // 返回到起始点开始 + }), + Animated.timing(twirl, { // 同时开始旋转 + toValue: 360, + }), + ]), +]).start(); // 执行这一整套动画序列 +``` + +默认情况下,如果任何一个动画被停止或中断了,组内所有其它的动画也会被停止。Parallel有一个`stopTogether`属性,如果设置为`false`,可以禁用自动停止。 + +#### 插值(interpolate) + +`Animated` API还有一个很强大的部分就是`interpolate`插值函数。它可以接受一个输入区间,然后将其映射到另一个的输出区间。下面是一个一个简单的从0-1区间到0-100区间的映射示例: + +```javascript +value.interpolate({ + inputRange: [0, 1], + outputRange: [0, 100], +}); +``` + +`interpolate`还支持定义多个区间段落,常用来定义静止区间等。举个例子,要让输入在接近-300时取相反值,然后在输入接近-100时到达0,然后在输入接近0时又回到1,接着一直到输入到100的过程中逐步回到0,最后形成一个始终为0的静止区间,对于任何大于100的输入都返回0。具体写法如下: + +```javascript +value.interpolate({ + inputRange: [-300, -100, 0, 100, 101], + outputRange: [300, 0, 1, 0, 0], +}); +``` + +它的最终映射结果如下: + +输入 | 输出 +------|------- + -400| 450 + -300| 300 + -200| 150 + -100| 0 + -50| 0.5 + 0| 1 + 50| 0.5 + 100| 0 + 101| 0 + 200| 0 + +`interpolate`还支持到字符串的映射,从而可以实现颜色以及带有单位的值的动画变换。例如你可以像下面这样实现一个旋转动画: + + ```javascript + value.interpolate({ + inputRange: [0, 360], + outputRange: ['0deg', '360deg'] + }) + ``` + +`interpolation`还支持任意的渐变函数,其中有很多已经在`Easing`类中定义了,包括二次、指数、贝塞尔等曲线以及step、bounce等方法。`interpolation`还支持限制输出区间`outputRange`。你可以通过设置`extrapolate`、`extrapolateLeft`或`extrapolateRight`属性来限制输出区间。默认值是`extend`(允许超出),不过你可以使用`clamp`选项来阻止输出值超过`outputRange`。 + +#### 跟踪动态值 + +动画中所设的值还可以通过跟踪别的值得到。你只要把toValue设置成另一个动态值而不是一个普通数字就行了。比如我们可以用弹跳动画来实现聊天头像的闪动,又比如通过`timing`设置`duration:0`来实现快速的跟随。他们还可以使用插值来进行组合: + +```javascript +Animated.spring(follower, {toValue: leader}).start(); +Animated.timing(opacity, { + toValue: pan.x.interpolate({ + inputRange: [0, 300], + outputRange: [1, 0], + }), +}).start(); +``` + +`ValueXY`是一个方便的处理2D交互的办法,譬如旋转或拖拽。它是一个简单的包含了两个`Animated.Value`实例的包装,然后提供了一系列辅助函数,使得`ValueXY`在许多时候可以替代`Value`来使用。比如在上面的代码片段中,`leader`和`follower`可以同时为`valueXY`类型,这样x和y的值都会被跟踪。 + +#### 输入事件 + +`Animated.event`是Animated API中与输入有关的部分,允许手势或其它事件直接绑定到动态值上。它通过一个结构化的映射语法来完成,使得复杂事件对象中的值可以被正确的解开。第一层是一个数组,允许同时映射多个值,然后数组的每一个元素是一个嵌套的对象。在下面的例子里,你可以发现`scrollX`被映射到了`event.nativeEvent.contentOffset.x`(`event`通常是回调函数的第一个参数),并且`pan.x`和`pan.y`分别映射到`gestureState.dx`和`gestureState.dy`(`gestureState`是传递给`PanResponder`回调函数的第二个参数)。 + +```javascript +onScroll={Animated.event( + [{nativeEvent: {contentOffset: {x: scrollX}}}] // scrollX = e.nativeEvent.contentOffset.x +)} +onPanResponderMove={Animated.event([ + null, // 忽略原生事件 + {dx: pan.x, dy: pan.y} // 从gestureState中解析出dx和dy的值 +]); +``` + +#### 响应当前的动画值 + +你可能会注意到这里没有一个明显的方法来在动画的过程中读取当前的值——这是出于优化的角度考虑,有些值只有在原生代码运行阶段中才知道。如果你需要在JavaScript中响应当前的值,有两种可能的办法: + +* `spring.stopAnimation(callback)`会停止动画并且把最终的值作为参数传递给回调函数`callback`——这在处理手势动画的时候非常有用。 +* `spring.addListener(callback)` 会在动画的执行过程中持续异步调用`callback`回调函数,提供一个最近的值作为参数。这在用于触发状态切换的时候非常有用,譬如当用户拖拽一个东西靠近的时候弹出一个新的气泡选项。不过这个状态切换可能并不会十分灵敏,因为它不像许多连续手势操作(如旋转)那样在60fps下运行。 + +#### 后续工作 + +如前面所述,我们计划继续优化Animated,以进一步提升性能。我们还想尝试一些声明式的手势响应和触发动画,譬如垂直或者水平的倾斜操作。 + +上面的API提供了一个强大的工具来简明、健壮、高效地组织各种各种不同的动画。你可以在[UIExplorer/AnimationExample](https://github.com/facebook/react-native/tree/master/Examples/UIExplorer/AnimatedGratuitousApp)中看到更多的样例代码。不过还有些时候`Animated`并不能支持你想要的效果,下面的章节包含了一些其它的动画系统。 + +### LayoutAnimation + +`LayoutAnimation`允许你在全局范围内`创建`和`更新`动画,这些动画会在下一次渲染或布局周期运行。它常用来更新flexbox布局,因为它可以无需测量或者计算特定属性就能直接产生动画。尤其是当布局变化可能影响到父节点(譬如“查看更多”展开动画既增加父节点的尺寸又会将位于本行之下的所有行向下推动)时,如果不使用`LayoutAnimation`,可能就需要显式声明组件的坐标,才能使得所有受影响的组件能够同步运行动画。 + +注意尽管`LayoutAnimation`非常强大且有用,但它对动画本身的控制没有`Animated`或者其它动画库那样方便,所以如果你使用`LayoutAnimation`无法实现一个效果,那可能还是要考虑其他的方案。 + +另外,如果要在**Android**上使用LayoutAnimation,那么目前还需要在`UIManager`中启用: +```javascript +UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true); +``` + +![](img/LayoutAnimationExample.gif) + +```javascript +var App = React.createClass({ + componentWillMount() { + // 创建动画 + LayoutAnimation.spring(); + }, + + getInitialState() { + return { w: 100, h: 100 } + }, + + _onPress() { + // 让视图的尺寸变化以动画形式展现 + LayoutAnimation.spring(); + this.setState({w: this.state.w + 15, h: this.state.h + 15}) + }, + + render: function() { + return ( + + + + + Press me! + + + + ); + } +}); +``` +[运行这个例子](https://rnplay.org/apps/uaQrGQ) + +上面这个例子使用了一个预设值,不过你也可以自己配置你需要的动画。参见[LayoutAnimation.js](https://github.com/facebook/react-native/blob/master/Libraries/LayoutAnimation/LayoutAnimation.js)。 + +### requestAnimationFrame + +`requestAnimationFrame`是一个对浏览器标准API的兼容实现,你可能已经熟悉它了。它接受一个函数作为唯一的参数,并且在下一次重绘之前调用此函数。一些基于JavaScript的动画库高度依赖于这一API。通常你不必直接调用它——那些动画库会替你管理好帧的更新。 + +### react-tween-state(不推荐,用[Animated](#animated)来替代) + +[react-tween-state](https://github.com/chenglou/react-tween-state)是一个极小的库,正如它名字(tween:补间)表示的含义:它生成一个节点的状态的中间值,从一个**开始**值,结束于一个**到达**值。这意味着它会生成这两者之间的值,然后在每次`requestAnimationFrame`的时候修改状态。 + +> 在[Wikipedia](https://en.wikipedia.org/wiki/Inbetweening)上对于补间动画(tweening)的定义: +> +> “补间是在两个图像之间生成中间帧的过程,以使得第一个图像能够平滑的变化为第二个图像”。补间帧是指在关键帧之间用于创建过渡假象的图画。” + +一个最基础的从一个值运动到另一个值的办法就是线性过渡:只需要将结束值减去开始值,然后除以动画总共需要经历的帧数,再在每一帧加到当前值上,一直到结束值位置。线性过渡有时候看起来怪异且不自然,所以react-tween-state提供了一系列常用的[过渡函数](http://easings.net/),可以用于使你的动画更加自然。 + +这个库并未随React Native一起发布——要在你的工程中使用它,则需要先在你的工程目录下执行`npm i react-tween-state --save`来安装。 + +```javascript +import tweenState from 'react-tween-state'; +import reactMixin from 'react-mixin'; // https://github.com/brigand/react-mixin + +class App extends React.Component { + constructor(props) { + super(props); + this.state = { opacity: 1 }; + this._animateOpacity = this._animateOpacity.bind(this); + } + + _animateOpacity() { + this.tweenState('opacity', { + easing: tweenState.easingTypes.easeOutQuint, + duration: 1000, + endValue: this.state.opacity === 0.2 ? 1 : 0.2, + }); + } + + render() { + return ( + + + this._box = component} + style={{width: 200, height: 200, backgroundColor: 'red', + opacity: this.getTweeningValue('opacity')}} /> + + + ) + } +} + +reactMixin.onClass(App, tweenState.Mixin); +``` +[运行这个例子](https://rnplay.org/apps/4FUQ-A) + +![](img/TweenState.gif) + +在上面的例子里我们变化的是透明度,但你可能也猜到了,我们能变化任何数值的值。可以参考它的[说明文档](https://github.com/chenglou/react-tween-state)来了解更多信息。 + +#### Rebound (不推荐 - 使用[Animated](#animated)来替代) + +[Rebound.js](https://github.com/facebook/rebound-js)是一个[安卓版Rebound](https://github.com/facebook/rebound)的JavaScript移植版。它在概念上类似react-tween-state:你有一个起始值,然后定义一个结束值,然后Rebound会生成所有中间的值并用于你的动画。Rebound基于弹性物理模型,你不需要提供一个动画的持续时间,它会自动根据弹性系数、助力、当前值和结束值来计算。我们[在React Native内部应用](https://github.com/facebook/react-native/search?utf8=%E2%9C%93&q=rebound)了Rebound,比如`Navigator`和`WarningBox`。 + +![](img/ReboundImage.gif) + +需要注意的是Rebound动画可以被中断——如果你在按下动画的过程中释放手指,它会从当前状态弹回初始值。 + +```javascript +var rebound = require('rebound'); + +var App = React.createClass({ + // 首先我们初始化一个spring动画,并添加监听函数, + // 这个函数会在spring更新时调用setState + componentWillMount() { + // 初始化spring + this.springSystem = new rebound.SpringSystem(); + this._scrollSpring = this.springSystem.createSpring(); + var springConfig = this._scrollSpring.getSpringConfig(); + springConfig.tension = 230; + springConfig.friction = 10; + + this._scrollSpring.addListener({ + onSpringUpdate: () => { + this.setState({scale: this._scrollSpring.getCurrentValue()}); + }, + }); + + // 将spring的初始值设为1 + this._scrollSpring.setCurrentValue(1); + }, + + _onPressIn() { + this._scrollSpring.setEndValue(0.5); + }, + + _onPressOut() { + this._scrollSpring.setEndValue(1); + }, + + render: function() { + var imageStyle = { + width: 250, + height: 200, + transform: [{scaleX: this.state.scale}, {scaleY: this.state.scale}], + }; + + var imageUri = "https://facebook.github.io/react-native/img/ReboundExample.png"; + + return ( + + + + + + ); + } +}); +``` + +你还可以为弹跳值启用边界,这样它们不会超出,而是会缓缓接近最终值。在上面的例子里,我们可以添加`this._scrollSpring.setOvershootClampingEnabled(true)`来启用边界。参见下面的gif动画来看一个启用了边界的效果: + +![](img/Rebound.gif) 截图来自 +[react-native-scrollable-tab-view](https://github.com/brentvatne/react-native-scrollable-tab-view)。 + +你可以在[这里](https://rnplay.org/apps/qHU_5w)看到一个类似的例子。 + +#### 关于setNativeProps + +正如[直接操作](direct-manipulation.html)文档所说,`setNativeProps`方法可以使我们直接修改基于原生视图的组件的属性,而不需要使用`setState`来重新渲染整个组件树。 + +我们可以把这个用在Rebound样例中来更新缩放比例——如果我们要更新的组件有一个非常深的内嵌结构,并且没有使用`shouldComponentUpdate`来优化,那么使用`setNativeProps`就将大有裨益。 + +```javascript +// 回到上面示例的那个组件中,找到componentWillMount方法, +// 然后将scrollSpring的监听函数替换为如下代码: +this._scrollSpring.addListener({ + onSpringUpdate: () => { + if (!this._photo) { return } + var v = this._scrollSpring.getCurrentValue(); + var newProps = {style: {transform: [{scaleX: v}, {scaleY: v}]}}; + this._photo.setNativeProps(newProps); + }, +}); + +// 最后,我们修改render方法,不再通过style来传入transform(避免 +// 重新渲染时产生冲突);然后给图片加上ref引用。 +render: function() { + return ( + + + this._photo = component} + source={{uri: "https://facebook.github.io/react-native/img/ReboundExample.png"}} + style={{width: 250, height: 200}} /> + + + ); +} +``` +[运行这个例子](https://rnplay.org/apps/fUqjAg) + +不过你没办法把`setNativeProps`和react-tween-state结合使用,因为更新的补间值会自动被库设置到state上——Rebound则不同,它通过`onSprintUpdate`函数在每一帧中给我们提供一个更新后的值。 + +如果你发现你的动画丢帧(低于60帧每秒),可以尝试使用`setNativeProps`或者`shouldComponentUpdate`来优化它们。你还可能需要将部分计算工作放在动画完成之后进行,这时可以使用[InteractionManager](/react-native/docs/interactionmanager.html)。你还可以使用应用内的开发者菜单中的“FPS Monitor”工具来监控应用的帧率。 + +### 导航器场景切换 + +正如文档[导航器对比](navigator-comparison.html#content)所说,`Navigator`使用JavaScript实现,而`NavigatoIOS`则是一个对于`UINavigationController`提供的原生功能的包装。所以这些场景切换动画仅仅对`Navigator`有效。为了在Navigator中重新创建`UINavigationController`所提供的动画并且使之可以被自定义,React Native导出了一个[NavigatorSceneConfigs](https://github.com/facebook/react-native/blob/master/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js)API。 + +```javascript +import { Dimensions } from 'react-native'; +var SCREEN_WIDTH = Dimensions.get('window').width; +var BaseConfig = Navigator.SceneConfigs.FloatFromRight; + +var CustomLeftToRightGesture = Object.assign({}, BaseConfig.gestures.pop, { + // 用户中断返回手势时,迅速弹回 + snapVelocity: 8, + + // 如下设置可以使我们在屏幕的任何地方拖动它 + edgeHitWidth: SCREEN_WIDTH, +}); + +var CustomSceneConfig = Object.assign({}, BaseConfig, { + // 如下设置使过场动画看起来很快 + springTension: 100, + springFriction: 1, + + // 使用上面我们自定义的手势 + gestures: { + pop: CustomLeftToRightGesture, + } +}); +``` +[运行这个例子](https://rnplay.org/apps/HPy6UA) + +要了解更多有关自定义场景切换的信息,你可以[阅读相应的源码](https://github.com/facebook/react-native/blob/master/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js)。 diff --git a/docs/docs/0.41/appregistry.md b/docs/docs/0.41/appregistry.md new file mode 100644 index 0000000..f4d5dde --- /dev/null +++ b/docs/docs/0.41/appregistry.md @@ -0,0 +1,16 @@ +`AppRegistry`是JS运行所有React Native应用的入口。应用的根组件应当通过`AppRegistry.registerComponent`方法注册自己,然后原生系统才可以加载应用的代码包并且在启动完成之后通过调用`AppRegistry.runApplication`来真正运行应用。 + +要“结束”一个应用并销毁视图的话,请调用`AppRegistry.unmountApplicationComponentAtRootTag`方法,参数为在`runApplication`中使用的标签名。它们必须严格匹配。 + +`AppRegistry`应当在`require`序列中尽可能早的被require到,以确保JS运行环境在其它模块之前被准备好。 + +### 方法 + +
+

static registerConfig(config: Array<AppConfig>) #

+

static registerComponent(appKey: string, getComponentFunc: ComponentProvider) #

+

static registerRunnable(appKey: string, func: Function) #

+

static getAppKeys() #

+

static runApplication(appKey: string, appParameters: any) #

+

static unmountApplicationComponentAtRootTag(rootTag: number) #

+
diff --git a/docs/docs/0.41/appstate.md b/docs/docs/0.41/appstate.md new file mode 100644 index 0000000..a824b94 --- /dev/null +++ b/docs/docs/0.41/appstate.md @@ -0,0 +1,157 @@ +`AppState`能告诉你应用当前是在前台还是在后台,并且能在状态变化的时候通知你。 + +AppState通常在处理推送通知的时候用来决定内容和对应的行为。 + +### App States + +* `active` - 应用正在前台运行 +* `background` - 应用正在后台运行。用户既可能在别的应用中,也可能在桌面。 +* `inactive` - 这是一个过渡状态,不会在正常的React Native应用中出现。 + +要了解更多信息,可以阅读[Apple的文档](https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/TheAppLifeCycle/TheAppLifeCycle.html)。 + +### 基本用法 + +要获取当前的状态,你可以使用`AppState.currentState`,这个变量会一直保持更新。不过在启动的过程中,`currentState`可能为null,直到`AppState`从原生代码得到通知为止。 + +```javascript +constructor(props) { + super(props); + this.state = { + currentAppState: AppState.currentState, + }; +} +componentDidMount() { + AppState.addEventListener('change', this._handleAppStateChange); +} +componentWillUnmount() { + AppState.removeEventListener('change', this._handleAppStateChange); +} +_handleAppStateChange = (nextAppState) => { + if (this.state.appState.match(/inactive|background/) && nextAppState === 'active') { + console.log('App has come to the foreground!') + } + this.setState({appState: nextAppState}); +} +render() { + return ( + Current state is: {this.state.currentAppState} + ); +} +``` + +上面的这个例子只会显示"Current state is: active",这是因为应用只有在`active`状态下才能被用户看到。并且null状态只会在一开始的一瞬间出现。 + +### 方法 + +
+
+

static addEventListener(type: string, handler: Function) #

+
+

添加一个监听函数,用于监听应用状态的变化。type参数应填`change`

+
+
+
+

static removeEventListener(type: string, handler: Function) #

+
+

移除一个监听函数。type参数应填change

+
+
+
+ +### 属性 + +
+
+

currentState: TypeCastExpression #

+
+
+ +### 例子 + +```javascript +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const { + AppState, + Text, + View +} = ReactNative; + +var AppStateSubscription = React.createClass({ + getInitialState() { + return { + appState: AppState.currentState, + previousAppStates: [], + memoryWarnings: 0, + }; + }, + componentDidMount: function() { + AppState.addEventListener('change', this._handleAppStateChange); + AppState.addEventListener('memoryWarning', this._handleMemoryWarning); + }, + componentWillUnmount: function() { + AppState.removeEventListener('change', this._handleAppStateChange); + AppState.removeEventListener('memoryWarning', this._handleMemoryWarning); + }, + _handleMemoryWarning: function() { + this.setState({memoryWarnings: this.state.memoryWarnings + 1}); + }, + _handleAppStateChange: function(appState) { + var previousAppStates = this.state.previousAppStates.slice(); + previousAppStates.push(this.state.appState); + this.setState({ + appState, + previousAppStates, + }); + }, + render() { + if (this.props.showMemoryWarnings) { + return ( + + {this.state.memoryWarnings} + + ); + } + if (this.props.showCurrentOnly) { + return ( + + {this.state.appState} + + ); + } + return ( + + {JSON.stringify(this.state.previousAppStates)} + + ); + } +}); + +exports.title = 'AppState'; +exports.description = 'app background status'; +exports.examples = [ + { + title: 'AppState.currentState', + description: 'Can be null on app initialization', + render() { return {AppState.currentState}; } + }, + { + title: 'Subscribed AppState:', + description: 'This changes according to the current state, so you can only ever see it rendered as "active"', + render(): ReactElement { return ; } + }, + { + title: 'Previous states:', + render(): ReactElement { return ; } + }, + { + platform: 'ios', + title: 'Memory Warnings', + description: 'In the IOS simulator, hit Shift+Command+M to simulate a memory warning.', + render(): ReactElement { return ; } + }, +]; +``` diff --git a/docs/docs/0.41/asyncstorage.md b/docs/docs/0.41/asyncstorage.md new file mode 100644 index 0000000..d1cc11d --- /dev/null +++ b/docs/docs/0.41/asyncstorage.md @@ -0,0 +1,76 @@ +AsyncStorage是一个简单的、异步的、持久化的Key-Value存储系统,它对于App来说是全局性的。它用来代替LocalStorage。 + +我们推荐您在AsyncStorage的基础上做一层抽象封装,而不是直接使用AsyncStorage。 + +__译注__:推荐由`React Native中文网`封装维护的[`react-native-storage`](https://github.com/sunnylqm/react-native-storage/blob/master/README-CHN.md)模块,提供了较多便利功能。 + +本模块的JS代码提供了对原生实现的一个封装,以提供一个更清晰的JS API、返回真正的错误对象,以及简单的单项对象操作函数。每个方法都会返回一个`Promise`对象。 + +### 方法 + +
+
+

static getItem(key: string, callback?: ?(error: ?Error, result: ?string) => void) #

+
+

读取key字段并将结果作为第二个参数传递给callback。如果有任何错误发生,则会传递一个Error对象作为第一个参数。返回一个Promise对象。

+
+
+
+

static setItem(key: string, value: string, callback?: ?(error: ?Error) => void) #

+
+

key字段的值设置成value,并在完成后调用callback函数。如果有任何错误发生,则会传递一个Error对象作为第一个参数。返回一个Promise对象。

+
+
+
+

static removeItem(key: string, callback?: ?(error: ?Error) => void) #

+
+

删除一个字段。返回一个Promise对象。

+
+
+
+

static mergeItem(key: string, value: string, callback?: ?(error: ?Error) => void) #

+
+

假设已有的值和新的值都是字符串化的JSON,则将两个值合并。返回一个Promise对象。还没有被所有原生实现都支持。

+
+
+
+

static clear(callback?: ?(error: ?Error) => void) #

+
+

删除全部的AsyncStorage数据,不论来自什么库或调用者。通常不应该调用这个函数——使用removeItem或者multiRemove来清除你自己的key。返回一个Promise对象。

+
+
+
+

static getAllKeys(callback?: ?(error: ?Error, keys: ?Array<string>) => void) #

+
+

获取所有本应用可以访问到的数据,不论来自什么库或调用者。返回一个Promise对象。

+
+
+

static flushGetRequests() #

清除所有进行中的查询操作。

+
+

static multiGet(keys: Array<string>, callback?: ?(errors: ?Array<Error>, result: ?Array<Array<string>>) => void) #

+
+

获取keys所包含的所有字段的值,调用callback回调函数时返回一个key-value数组形式的数组。返回一个Promise对象。

+

multiGet(['k1', 'k2'], cb) -> cb([['k1', 'val1'], ['k2', 'val2']])

+
+
+
+

static multiSet(keyValuePairs: Array<Array<string>>, callback?: ?(errors: ?Array<Error>) => void) #

+
+

multiSet和multiMerge都接受一个与multiGet输出值一致的key-value数组的数组。返回一个Promise对象。

+

multiSet([['k1', 'val1'], ['k2', 'val2']], cb);

+
+
+
+

static multiRemove(keys: Array<string>, callback?: ?(errors: ?Array<Error>) => void) #

+
+

删除所有键在keys数组中的数据。返回一个Promise对象。

+
+
+
+

static multiMerge(keyValuePairs: Array<Array<string>>, callback?: ?(errors: ?Array<Error>) => void) #

+
+

将多个输入的值和已有的值合并,要求都是字符串化的JSON。返回一个Promise对象。

+

还没有被所有原生实现都支持。

+
+
+
diff --git a/docs/docs/0.41/backandroid.md b/docs/docs/0.41/backandroid.md new file mode 100644 index 0000000..b7c014a --- /dev/null +++ b/docs/docs/0.41/backandroid.md @@ -0,0 +1,22 @@ +监听硬件的`back`键操作。如果没有任何监听函数,或者监听函数的返回值不是true,则会调用默认的back键功能来退出应用。 + +例子: + +```javascript +BackAndroid.addEventListener('hardwareBackPress', function() { + if (!this.onMainScreen()) { + this.goBack(); + return true; + } + return false; +}); +``` +__译注__:以上的`this.onMainScreen()`和`this.goBack()`两个方法都只是伪方法,需要你自己去实现!具体可以参考这篇[博文](http://bbs.reactnative.cn/topic/480)。 + +### 方法 + +
+

static exitApp() #

+

static addEventListener(eventName: BackPressEventName, handler: Function) #

+

static removeEventListener(eventName: BackPressEventName, handler: Function) #

+
diff --git a/docs/docs/0.41/button.md b/docs/docs/0.41/button.md new file mode 100644 index 0000000..c8e75cc --- /dev/null +++ b/docs/docs/0.41/button.md @@ -0,0 +1,134 @@ +一个简单的跨平台的按钮组件。可以进行一些简单的定制。 + +![](img/components/buttonExample.png) + +如果这个组件外观并不怎么搭配你的设计,那你可以使用`TouchableOpacity`或是`TouchableNativeFeedback`组件来制作自己所需要的按钮,视频教程[如何制作一个按钮](http://v.youku.com/v_show/id_XMTQ5OTE3MjkzNg==.html?f=26822355&from=y1.7-1.3)讲述了完整的过程。或者你也可以在github.com网站上搜索'react native button'来看看社区其他人的作品。 + + +用法示例: + +```js + +
+ + + + Animation Type + + + + + + + Transparent + + + + + + ); + }, +}); + +exports.examples = [ + { + title: 'Modal Presentation', + description: 'Modals can be presented with or without animation', + render: () => , + }, +]; + +var styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + padding: 20, + }, + innerContainer: { + borderRadius: 10, + alignItems: 'center', + }, + row: { + alignItems: 'center', + flex: 1, + flexDirection: 'row', + marginBottom: 20, + }, + rowTitle: { + flex: 1, + fontWeight: 'bold', + }, + button: { + borderRadius: 5, + flex: 1, + height: 44, + alignSelf: 'stretch', + justifyContent: 'center', + overflow: 'hidden', + }, + buttonText: { + fontSize: 18, + margin: 5, + textAlign: 'center', + }, + modalButton: { + marginTop: 10, + }, +}); +``` diff --git a/docs/docs/0.41/more-resources.md b/docs/docs/0.41/more-resources.md new file mode 100644 index 0000000..6735969 --- /dev/null +++ b/docs/docs/0.41/more-resources.md @@ -0,0 +1,43 @@ +如果你耐心的读完并理解了本网站上的所有文档,那么你应该已经可以编写一个像样的React Native应用了。但是React Native并不全是某一家公司的作品——它汇聚了成千上万开源社区开发者的智慧结晶。如果你想深入研究React Native,那么建议不要错过下面这些参考资源。 + +## 常用的第三方库 + +如果你正在使用React Native,那你应该已经对[React](https://facebook.github.io/react/)有一定的了解了。React是基础中的基础所以我其实不太好意思提这个——但是,如果不幸你属于“但是”,那么请一定先了解下React,它也非常适合编写现代化的网站。 + +开发实践中的一个常见问题就是如何管理应用的“状态(state)”。这方面目前最流行的库非[Redux](http://redux.js.org/)莫属了。不要被Redux中经常出现的类似"reducer"这样的概念术语给吓住了——它其实是个很简单的库,网上也有很多优秀的[视频教程(英文)](https://egghead.io/courses/getting-started-with-redux) 。。 + +如果你在寻找具有某个特定功能的第三方库,那么可以看看别人[精心整理的资源列表](https://github.com/jondot/awesome-react-native)。这里还有个类似的[中文资源列表](https://github.com/reactnativecn/react-native-guide)。 + +## 示例应用 + +在[React Native Playground](https://rnplay.org/apps/picks)网站上有很多示例的代码。这个网站有个很酷的特性:它直接对接了真实设备,可以实时在网页上显示运行效果。当然,对于国内用户来说,可能访问很困难。 + +另外就是Facebook的F8开发大会有一个对应的app,这个app现在已经[开源](https://github.com/fbsamples/f8app),其开发者还详细地撰写了[相关教程](http://f8-app.liaohuqiu.net/#content)。如果你想学习一个更实际更有深度的例子,那你应该看看这个。 + +## 开发工具 + +[Nuclide](https://nuclide.io/)是Facebook内部所使用的React Native开发工具。它最大的特点是自带调试功能,并且非常好地支持flow语法规则。(译注:然而我们还是推荐webstorm或是sublime text)。 + +[Ignite](https://github.com/infinitered/ignite)是一套整合了Redux以及一些常见UI组件的脚手架。它带有一个命令行可以生成app、组件或是容器。如果你喜欢它的选择搭配,那么不妨一试。 + +[CodePush](https://microsoft.github.io/code-push/)是由微软提供的热更新服务。热更新可以使你绕过AppStore的审核机制,直接修改已经上架的应用。对于国内用户,我们也推荐由本网站提供的[Pushy](http://update.reactnative.cn)热更新服务,相比CodePush来说,提供了全中文的文档和技术支持,服务器部署在国内速度更快,还提供了全自动的差量更新方式,大幅节约更新流量,欢迎朋友们试用和反馈意见! + +[Exponent](http://docs.getexponent.com/versions/v6.0.0/index.html)是一套开发环境,还带有一个已上架的空应用容器。这样你可以在没有原生开发平台(Xcode或是Android Studio)的情况下直接编写React Native应用(当然这样你只能写js部分代码而没法写原生代码)。 + +[Deco](https://www.decosoftware.com/)是一个专为React Native设计的集成开发环境。它可以自动创建新项目、搜索开源组件并插入到项目中。你还可以实时地可视化地调整应用的界面。不过目前还只支持mac。 + +## React Native的交流社区 + +以下这些都是英文的交流区,我也就不翻译了…… + +The [React Native Community](https://www.facebook.com/groups/react.native.community) Facebook group has thousands of developers, and it's pretty active. Come there to show off your project, or ask how other people solved similar problems. + +[Reactiflux](https://discord.gg/0ZcbPKXt5bZjGY5n) is a Discord chat where a lot of React-related discussion happens, including React Native. Discord is just like Slack except it works better for open source projects with a zillion contributors. Check out the #react-native channel. + +The [React Twitter account](https://twitter.com/reactjs) covers both React and React Native. Following that account is a pretty good way to find out what's happening in the world of React. + +There are a lot of [React Native Meetups](http://www.meetup.com/topics/react-native/) that happen around the world. Often there is React Native content in React meetups as well. + +Sometimes we have React conferences. We posted the [videos from React.js Conf 2016](https://www.youtube.com/playlist?list=PLb0IAmt7-GS0M8Q95RIc2lOM6nc77q1IY), and we'll probably have more conferences in the future, too. Stay tuned. + +欢迎朋友们在下方评论区分享中文教程和资源。 \ No newline at end of file diff --git a/docs/docs/0.41/native-component-android.md b/docs/docs/0.41/native-component-android.md new file mode 100644 index 0000000..714423f --- /dev/null +++ b/docs/docs/0.41/native-component-android.md @@ -0,0 +1,172 @@ +在如今的App中,已经有成千上万的原生UI部件了——其中的一些是平台的一部分,另一些可能来自于一些第三方库,而且可能你自己还收藏了很多。React Native已经封装了大部分最常见的组件,譬如`ScrollView`和`TextInput`,但不可能封装全部组件。而且,说不定你曾经为自己以前的App还封装过一些组件,React Native肯定没法包含它们。幸运的是,在React Naitve应用程序中封装和植入已有的组件非常简单。 + +和原生模块向导一样,本向导也是一个相对高级的向导,我们假设你已经对Android编程颇有经验。本向导会引导你如何构建一个原生UI组件,带领你了解React Native核心库中`ImageView`组件的具体实现。 + +## ImageView样例 + +在这个例子里,我们来看看为了让JavaScript中可以使用ImageView,需要做哪些准备工作。 + +原生视图需要被一个`ViewManager`的派生类(或者更常见的,`SimpleViewManage`的派生类)创建和管理。一个`SimpleViewManager`可以用于这个场景,是因为它能够包含更多公共的属性,譬如背景颜色、透明度、Flexbox布局等等。 + +这些子类本质上都是单例——React Native只会为每个管理器创建一个实例。它们创建原生的视图并提供给`NativeViewHierarchyManager`,NativeViewHierarchyManager则会反过来委托它们在需要的时候去设置和更新视图的属性。`ViewManager`还会代理视图的所有委托,并给JavaScript发回对应的事件。 + +提供原生视图很简单: + +1. 创建一个ViewManager的子类。 +2. 实现`createViewInstance`方法。 +3. 导出视图的属性设置器:使用`@ReactProp`(或`@ReactPropGroup`)注解。 +4. 把这个视图管理类注册到应用程序包的`createViewManagers`里。 +5. 实现JavaScript模块。 + +## 1. 创建`ViewManager`的子类 + +在这个例子里我们创建一个视图管理类`ReactImageManager`,它继承自`SimpleViewManager`。`ReactImageView`是这个视图管理类所管理的对象类型,这应当是一个自定义的原生视图。`getName`方法返回的名字会用于在JavaScript端引用这个原生视图类型。 + +```java +... + +public class ReactImageManager extends SimpleViewManager { + + public static final String REACT_CLASS = "RCTImageView"; + + @Override + public String getName() { + return REACT_CLASS; + } +``` + +## 2. 实现方法`createViewInstance` + +视图在`createViewInstance`中创建,且应当把自己初始化为默认的状态。所有属性的设置都通过后续的`updateView`来进行。 + +```java + @Override + public ReactImageView createViewInstance(ThemedReactContext context) { + return new ReactImageView(context, Fresco.newDraweeControllerBuilder(), mCallerContext); + } +``` + +## 3. 通过`@ReactProp`(或`@ReactPropGroup`)注解来导出属性的设置方法。 + +要导出给JavaScript使用的属性,需要申明带有`@ReactProp`(或`@ReactPropGroup`)注解的设置方法。方法的第一个参数是要修改属性的视图实例,第二个参数是要设置的属性值。方法的返回值类型必须为`void`,而且访问控制必须被声明为`public`。JavaScript所得知的属性类型会由该方法第二个参数的类型来自动决定。支持的类型有:`boolean`, `int`, `float`, `double`, `String`, `Boolean`, `Integer`, `ReadableArray`, `ReadableMap`。 + +`@ReactProp`注解必须包含一个字符串类型的参数`name`。这个参数指定了对应属性在JavaScript端的名字。 + +除了`name`,`@ReactProp`注解还接受这些可选的参数:`defaultBoolean`, `defaultInt`, `defaultFloat`。这些参数必须是对应的基础类型的值(也就是`boolean`, `int`, `float`),这些值会被传递给setter方法,以免JavaScript端某些情况下在组件中移除了对应的属性。注意这个"默认"值只对基本类型生效,对于其他的类型而言,当对应的属性删除时,`null`会作为默认值提供给方法。 + +使用`@ReactPropGroup`来注解的设置方法和`@ReactProp`不同。请参见`@ReactPropGroup`注解类源代码中的文档来获取更多详情。 + +**重要!** 在ReactJS里,修改一个属性会引发一次对设置方法的调用。有一种修改情况是,移除掉之前设置的属性。在这种情况下设置方法也一样会被调用,并且“默认”值会被作为参数提供(对于基础类型来说可以通过`defaultBoolean`、`defaultFloat`等`@ReactProp`的属性提供,而对于复杂类型来说参数则会设置为`null`) + +```java + @ReactProp(name = "src") + public void setSrc(ReactImageView view, @Nullable String src) { + view.setSource(src); + } + + @ReactProp(name = "borderRadius", defaultFloat = 0f) + public void setBorderRadius(ReactImageView view, float borderRadius) { + view.setBorderRadius(borderRadius); + } + + @ReactProp(name = ViewProps.RESIZE_MODE) + public void setResizeMode(ReactImageView view, @Nullable String resizeMode) { + view.setScaleType(ImageResizeMode.toScaleType(resizeMode)); + } +``` + +## 4. 注册`ViewManager` + +在Java中的最后一步就是把视图控制器注册到应用中。这和[原生模块](NativeModulesAndroid.md)的注册方法类似,唯一的区别是我们把它放到`createViewManagers`方法的返回值里。 + +```java + @Override + public List createViewManagers( + ReactApplicationContext reactContext) { + return Arrays.asList( + new ReactImageManager() + ); + } +``` + +## 5. 实现对应的JavaScript模块 + +整个过程的最后一步就是创建JavaScript模块并且定义Java和JavaScript之间的接口层。大部分过程都由React底层的Java和JavaScript代码来完成,你所需要做的就是通过`propTypes`来描述属性的类型。 + +```js +// ImageView.js + +import { PropTypes } from 'react'; +import { requireNativeComponent, View } from 'react-native'; + +var iface = { + name: 'ImageView', + propTypes: { + src: PropTypes.string, + borderRadius: PropTypes.number, + resizeMode: PropTypes.oneOf(['cover', 'contain', 'stretch']), + ...View.propTypes // 包含默认的View的属性 + }, +}; + +module.exports = requireNativeComponent('RCTImageView', iface); +``` + +`requireNativeComponent`通常接受两个参数,第一个参数是原生视图的名字,而第二个参数是一个描述组件接口的对象。组件接口应当声明一个友好的`name`,用来在调试信息中显示;组件接口还必须声明`propTypes`字段,用来对应到原生视图上。这个`propTypes`还可以用来检查用户使用View的方式是否正确。 + +注意,如果你还需要一个JavaScript组件来做一些除了指定`name`和`propTypes`以外的事情,譬如事件处理,你可以把原生组件用一个普通React组件封装。在这种情况下,`requireNativeComponent`的第二个参数变为用于封装的组件。这个在后文的`MyCustomView`例子里面用到。 + +_译注_:和原生模块不同,原生视图的前缀RCT不会被自动去掉。 + +# 事件 + +现在我们已经知道了怎么导出一个原生视图组件,并且我们可以在JS里很方便的控制它了。不过我们怎么才能处理来自用户的事件,譬如缩放操作或者拖动?当一个原生事件发生的时候,它应该也能触发JavaScript端视图上的事件,这两个视图会依据getId()而关联在一起。 + +```java +class MyCustomView extends View { + ... + public void onReceiveNativeEvent() { + WritableMap event = Arguments.createMap(); + event.putString("message", "MyMessage"); + ReactContext reactContext = (ReactContext)getContext(); + reactContext.getJSModule(RCTEventEmitter.class).receiveEvent( + getId(), + "topChange", + event); + } +} +``` + +这个事件名`topChange`在JavaScript端映射到`onChange`回调属性上(这个映射关系在`UIManagerModuleConstants.java`文件里)。这个回调会被原生事件执行,然后我们通常会在封装组件里构造一个类似的API: + +```js +// MyCustomView.js + +class MyCustomView extends React.Component { + constructor() { + this._onChange = this._onChange.bind(this); + } + _onChange(event: Event) { + if (!this.props.onChangeMessage) { + return; + } + this.props.onChangeMessage(event.nativeEvent.message); + } + render() { + return ; + } +} +MyCustomView.propTypes = { + /** + * Callback that is called continuously when the user is dragging the map. + */ + onChangeMessage: React.PropTypes.func, + ... +}; + +var RCTMyCustomView = requireNativeComponent(`RCTMyCustomView`, MyCustomView, { + nativeOnly: {onChange: true} +}); +``` + +注意上面用到了`nativeOnly`。有时候有一些特殊的属性,想从原生组件中导出,但是又不希望它们成为对应React封装组件的属性。举个例子,`Switch`组件可能在原生组件上有一个`onChange`事件,然后在封装类中导出`onValueChange`回调属性。这个属性在调用的时候会带上Switch的状态作为参数之一。这样的话你可能不希望原生专用的属性出现在API之中,也就不希望把它放到`propTypes`里。可是如果你不放的话,又会出现一个报错。解决方案就是带上`nativeOnly`选项。 diff --git a/docs/docs/0.41/native-component-ios.md b/docs/docs/0.41/native-component-ios.md new file mode 100644 index 0000000..811b8e2 --- /dev/null +++ b/docs/docs/0.41/native-component-ios.md @@ -0,0 +1,362 @@ +在如今的App中,已经有成千上万的原生UI部件了——其中的一些是平台的一部分,另一些可能来自于一些第三方库,而且可能你自己还收藏了很多。React Native已经封装了大部分最常见的组件,譬如`ScrollView`和`TextInput`,但不可能封装全部组件。而且,说不定你曾经为自己以前的App还封装过一些组件,React Native肯定没法包含它们。幸运的是,在React Naitve应用程序中封装和植入已有的组件非常简单。 + +和原生模块向导一样,本向导也是一个相对高级的向导,我们假设你已经对iOS编程颇有经验。本向导会引导你如何构建一个原生UI组件,带领你了解React Native核心库中`MapView`组件的具体实现。 + +## iOS MapView样例 + +假设我们要把地图组件植入到我们的App中——我们用到的是[`MKMapView`](https://developer.apple.com/library/prerelease/mac/documentation/MapKit/Reference/MKMapView_Class/index.html),而现在只需要让它可以被Javascript重用。 + +原生视图都需要被一个`RCTViewManager`的子类来创建和管理。这些管理器在功能上有些类似“视图控制器”,但它们本质上都是单例 - React Native只会为每个管理器创建一个实例。它们创建原生的视图并提供给`RCTUIManager`,`RCTUIManager`则会反过来委托它们在需要的时候去设置和更新视图的属性。`RCTViewManager`还会代理视图的所有委托,并给JavaScript发回对应的事件。 + +提供原生视图很简单: + +- 首先创建一个子类 +- 添加`RCT_EXPORT_MODULE()`标记宏 +- 实现`-(UIView *)view`方法 + +```objective-c +// RNTMapManager.m +#import + +#import + +@interface RNTMapManager : RCTViewManager +@end + +@implementation RNTMapManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [[MKMapView alloc] init]; +} + +@end +``` + +接下来你需要一些Javascript代码来让这个视图变成一个可用的React组件: + +```javascript +// MapView.js + +var { requireNativeComponent } = require('react-native'); + +// requireNativeComponent 自动把这个组件提供给 "RNTMapManager" +module.exports = requireNativeComponent('RNTMap', null); +``` + +现在我们就已经实现了一个完整功能的地图组件了,诸如捏放和其它的手势都已经完整支持。但是现在我们还不能真正的从Javascript端控制它。(╯﹏╰) + +## 属性 + +我们能让这个组件变得更强大的第一件事情就是要能够封装一些原生属性供Javascript使用。举例来说,我们希望能够禁用手指捏放操作,然后指定一个初始的地图可见区域。禁用捏放操作只需要一个布尔值类型的属性就行了,所以我们添加这么一行: + +```objective-c +// RNTMapManager.m +RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL) +``` + +注意我们现在把类型声明为`BOOL`类型——React Native用`RCTConvert`来在JavaScript和原生代码之间完成类型转换。如果转换无法完成,会产生一个“红屏”的报错提示,这样你就能立即知道代码中出现了问题。如果一切进展顺利,上面这个宏就已经包含了导出属性的全部实现。 + +现在要想禁用捏放操作,我们只需要在JS里设置对应的属性: + +```javascript +// MyApp.js + +``` + +但这样并不能很好的说明这个组件的用法——用户要想知道我们的组件有哪些属性可以用,以及可以取什么样的值,他不得不一路翻到Objective-C的代码。要解决这个问题,我们可以创建一个封装组件,并且通过`PropTypes`来说明这个组件的接口。 + +```javascript +// MapView.js +import React, { Component, PropTypes } from 'react'; +import { requireNativeComponent } from 'react-native'; + +var RNTMap = requireNativeComponent('RNTMap', MapView); + +export default class MapView extends Component { + static propTypes = { + /** + * 当这个属性被设置为true,并且地图上绑定了一个有效的可视区域的情况下, + * 可以通过捏放操作来改变摄像头的偏转角度。 + * 当这个属性被设置成false时,摄像头的角度会被忽略,地图会一直显示为俯视状态。 + */ + pitchEnabled: PropTypes.bool, + }; + render() { + return ; + } +} +``` + +_译注_:使用了封装组件之后,你还需要注意到module.exports导出的不再是requireNativeComponent的返回值,而是所创建的包装组件。 + +现在我们有了一个封装好的组件,还有了一些注释文档,用户使用起来也更方便了。注意我们现在把`requireNativeComponent`的第二个参数从null变成了用于封装的组件`MapView`。这使得React Native的底层框架可以检查原生属性和包装类的属性是否一致,来减少出现问题的可能。 + +现在,让我们添加一个更复杂些的`region`属性。我们首先添加原生代码: + +```objective-c +// RNTMapManager.m +RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RNTMap) +{ + [view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES]; +} +``` + +这段代码比刚才的一个简单的`BOOL`要复杂的多了。现在我们多了一个需要做类型转换的`MKCoordinateRegion`类型,还添加了一部分自定义的代码,这样当我们在JS里改变地图的可视区域的时候,视角会平滑地移动过去。在我们提供的函数体内,`json`代表了JS中传递的尚未解析的原始值。函数里还有一个`view`变量,使得我们可以访问到对应的视图实例。最后,还有一个`defaultView`对象,这样当JS给我们发送null的时候,可以把视图的这个属性重置回默认值。 + +你可以为视图编写任何你所需要的转换函数——下面就是`MKCoordinateRegion`的转换实现,它通过两个RCTConvert的扩展来完成: + +```objective-c +@implementation RCTConvert(CoreLocation) + +RCT_CONVERTER(CLLocationDegrees, CLLocationDegrees, doubleValue); +RCT_CONVERTER(CLLocationDistance, CLLocationDistance, doubleValue); + ++ (CLLocationCoordinate2D)CLLocationCoordinate2D:(id)json +{ + json = [self NSDictionary:json]; + return (CLLocationCoordinate2D){ + [self CLLocationDegrees:json[@"latitude"]], + [self CLLocationDegrees:json[@"longitude"]] + }; +} + +@end + +@implementation RCTConvert(MapKit) + ++ (MKCoordinateSpan)MKCoordinateSpan:(id)json +{ + json = [self NSDictionary:json]; + return (MKCoordinateSpan){ + [self CLLocationDegrees:json[@"latitudeDelta"]], + [self CLLocationDegrees:json[@"longitudeDelta"]] + }; +} + ++ (MKCoordinateRegion)MKCoordinateRegion:(id)json +{ + return (MKCoordinateRegion){ + [self CLLocationCoordinate2D:json], + [self MKCoordinateSpan:json] + }; +} +``` + +这些转换函数被设计为可以安全的处理任何JS扔过来的JSON:当有任何缺少的键或者其它问题发生的时候,显示一个“红屏”的错误提示。 + +为了完成`region`属性的支持,我们还需要在`propTypes`里添加相应的说明(否则我们会立刻收到一个错误提示),然后就可以像使用其他属性一样使用了: + +```javascript +// MapView.js + +MapView.propTypes = { + /** + * 当这个属性被设置为true,并且地图上绑定了一个有效的可视区域的情况下, + * 可以通过捏放操作来改变摄像头的偏转角度。 + * 当这个属性被设置成false时,摄像头的角度会被忽略,地图会一直显示为俯视状态。 + */ + pitchEnabled: React.PropTypes.bool, + + /** + * 地图要显示的区域。 + * + * 区域由中心点坐标和区域范围坐标来定义。 + * + */ + region: React.PropTypes.shape({ + /** + * 地图中心点的坐标。 + */ + latitude: React.PropTypes.number.isRequired, + longitude: React.PropTypes.number.isRequired, + + /** + * 最小/最大经、纬度间的距离。 + */ + latitudeDelta: React.PropTypes.number.isRequired, + longitudeDelta: React.PropTypes.number.isRequired, + }), +}; + +// MyApp.js + + render() { + var region = { + latitude: 37.48, + longitude: -122.16, + latitudeDelta: 0.1, + longitudeDelta: 0.1, + }; + return ; + } + +``` + +现在你可以看到region属性的整个结构已经加上了文档说明——将来可能我们会自动生成一些类似的代码,但目前还没有这样的手段。 + +有时候你的原生组件有一些特殊的属性希望导出,但并不希望它成为公开的接口。举个例子,`Switch`组件可能会有一个`onChange`属性用来传递原始的原生事件,然后导出一个`onValueChange`属性,这个属性在调用的时候会带上`Switch`的状态作为参数之一。这样的话你可能不希望原生专用的属性出现在API之中,也就不希望把它放到`propTypes`里。可是如果你不放的话,又会出现一个报错。解决方案就是带上额外的`nativeOnly`参数,像这样: + +```javascript +var RCTSwitch = requireNativeComponent('RCTSwitch', Switch, { + nativeOnly: { onChange: true } +}); +``` + +## 事件 + +现在我们已经有了一个原生地图组件,并且从JS可以很容易的控制它了。不过我们怎么才能处理来自用户的事件,譬如缩放操作或者拖动来改变可视区域?关键的步骤是在`RNTMapManager`中声明一个事件处理函数的属性(onChange),来委托我们提供的所有视图,然后把事件传递给JavaScript。最终的代码看起来类似这样(比起完整的实现有所简化): + +```objective-c +// RNTMap.h + +#import + +#import + +@interface RNTMap: MKMapView + +@property (nonatomic, copy) RCTBubblingEventBlock onChange; + +@end +``` + +```objective-c +// RNTMap.m + +#import "RNTMap.h" + +@implementation RNTMap + +@end +``` + +```objective-c +#import "RNTMapManager.h" + +#import + +#import "RNTMap.h" +#import + +@interface RNTMapManager() +@end + +@implementation RNTMapManager + +RCT_EXPORT_MODULE() + +RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) + +- (UIView *)view +{ + RNTMap *map = [RNTMap new]; + map.delegate = self; + return map; +} + +#pragma mark MKMapViewDelegate + +- (void)mapView:(RNTMap *)mapView regionDidChangeAnimated:(BOOL)animated +{ + if (!mapView.onChange) { + return; + } + + MKCoordinateRegion region = mapView.region; + mapView.onChange(@{ + @"region": @{ + @"latitude": @(region.center.latitude), + @"longitude": @(region.center.longitude), + @"latitudeDelta": @(region.span.latitudeDelta), + @"longitudeDelta": @(region.span.longitudeDelta), + } + }); +} +``` + +如你所见,我们刚才通过继承`MKMapView`添加了事件处理函数,然后我们将`onChange`暴露出来,委托`RNTMapManager`代理其创建的所有视图。最后在委托方法`-mapView:regionDidChangeAnimated:`中,根据对应的视图调用事件处理函数并传递区域数据。调用`onChange`事件会触发JavaScript端的同名回调函数。这个回调会被原生事件执行,然后我们通常都会在封装组件里做一些处理,来使得API更简明: + + +```javascript +// MapView.js + +class MapView extends React.Component { + constructor() { + this._onChange = this._onChange.bind(this); + } + _onChange(event: Event) { + if (!this.props.onRegionChange) { + return; + } + this.props.onRegionChange(event.nativeEvent.region); + } + render() { + return ; + } +} +MapView.propTypes = { + /** + * Callback that is called continuously when the user is dragging the map. + */ + onRegionChange: React.PropTypes.func, + ... +}; +``` + +## 样式 + +因为我们所有的视图都是`UIView`的子类,大部分的样式属性应该直接就可以生效。但有一部分组件会希望使用自己定义的默认样式,例如`UIDatePicker`希望自己的大小是固定的。这个默认属性对于布局算法的正常工作来说很重要,但我们也希望在使用这个组件的时候可以覆盖这些默认的样式。`DatePickerIOS`实现这个功能的办法是通过封装一个拥有弹性样式的额外视图,然后在内层的视图上应用一个固定样式(通过原生传递来的常数生成): + +```javascript +// DatePickerIOS.ios.js + +var RCTDatePickerIOSConsts = require('react-native').UIManager.RCTDatePicker.Constants; +... + render: function() { + return ( + + + + ); + } +}); + +var styles = StyleSheet.create({ + rkDatePickerIOS: { + height: RCTDatePickerIOSConsts.ComponentHeight, + width: RCTDatePickerIOSConsts.ComponentWidth, + }, +}); +``` + +常量`RCTDatePickerIOSConsts`在原生代码中导出,从一个组件的实际布局上获取到: + +```objective-c +// RCTDatePickerManager.m + +- (NSDictionary *)constantsToExport +{ + UIDatePicker *dp = [[UIDatePicker alloc] init]; + [dp layoutIfNeeded]; + + return @{ + @"ComponentHeight": @(CGRectGetHeight(dp.frame)), + @"ComponentWidth": @(CGRectGetWidth(dp.frame)), + @"DatePickerModes": @{ + @"time": @(UIDatePickerModeTime), + @"date": @(UIDatePickerModeDate), + @"datetime": @(UIDatePickerModeDateAndTime), + } + }; +} +``` + +本向导覆盖了包装原生组件所需了解的许多方面,不过你可能还有很多知识需要了解,譬如特殊的方式来插入和布局子视图。如果你想更深入了解,可以阅读`RNTMapManager`和其它的组件的[源代码](https://github.com/facebook/react-native/blob/master/React/Views)。 + diff --git a/docs/docs/0.41/native-modules-android.md b/docs/docs/0.41/native-modules-android.md new file mode 100644 index 0000000..7f1c9b0 --- /dev/null +++ b/docs/docs/0.41/native-modules-android.md @@ -0,0 +1,431 @@ +有时候App需要访问平台API,但React Native可能还没有相应的模块包装;或者你需要复用一些Java代码,而不是用Javascript重新实现一遍;又或者你需要实现某些高性能的、多线程的代码,譬如图片处理、数据库、或者各种高级扩展等等。 + +我们把React Native设计为可以在其基础上编写真正的原生代码,并且可以访问平台所有的能力。这是一个相对高级的特性,我们并不认为它应当在日常开发的过程中经常出现,但具备这样的能力是很重要的。如果React Native还不支持某个你需要的原生特性,你应当可以自己实现该特性的封装。 + +## Toast模块 + +本向导会用[Toast](http://developer.android.com/reference/android/widget/Toast.html)作为例子。假设我们希望可以从Javascript发起一个Toast消息(Android中的一种会在屏幕下方弹出、保持一段时间的消息通知) + +我们首先来创建一个原生模块。一个原生模块是一个继承了`ReactContextBaseJavaModule`的Java类,它可以实现一些JavaScript所需的功能。我们这里的目标是可以在JavaScript里写`ToastAndroid.show('Awesome', ToastAndroid.SHORT);`,来调起一个Toast通知。 + +```java +package com.facebook.react.modules.toast; + +import android.widget.Toast; + +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; + +import java.util.Map; + +public class ToastModule extends ReactContextBaseJavaModule { + + private static final String DURATION_SHORT_KEY = "SHORT"; + private static final String DURATION_LONG_KEY = "LONG"; + + public ToastModule(ReactApplicationContext reactContext) { + super(reactContext); + } +} +``` + +`ReactContextBaseJavaModule`要求派生类实现`getName`方法。这个函数用于返回一个字符串名字,这个名字在JavaScript端标记这个模块。这里我们把这个模块叫做`ToastAndroid`,这样就可以在JavaScript中通过`React.NativeModules.ToastAndroid`访问到这个模块。**译注:RN已经内置了一个名为ToastAndroid的模块,所以如果你在练习时完全照抄,那么运行时会报错名字冲突!所以请在这里选择另外一个名字!** + +```java + @Override + public String getName() { + return "ToastAndroid"; + } +``` + +_译注_:模块名前的RCT前缀会被自动移除。所以如果返回的字符串为"RCTToastAndroid",在JavaScript端依然通过`React.NativeModules.ToastAndroid`访问到这个模块。 + +一个可选的方法`getContants`返回了需要导出给JavaScript使用的常量。它并不一定需要实现,但在定义一些可以被JavaScript同步访问到的预定义的值时非常有用。 + +```java + @Override + public Map getConstants() { + final Map constants = new HashMap<>(); + constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT); + constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG); + return constants; + } +``` + +要导出一个方法给JavaScript使用,Java方法需要使用注解`@ReactMethod`。方法的返回类型必须为`void`。React Native的跨语言访问是异步进行的,所以想要给JavaScript返回一个值的唯一办法是使用回调函数或者发送事件(参见下文的描述)。 + +```java + @ReactMethod + public void show(String message, int duration) { + Toast.makeText(getReactApplicationContext(), message, duration).show(); + } +``` + +### 参数类型 + +下面的参数类型在`@ReactMethod`注明的方法中,会被直接映射到它们对应的JavaScript类型。 + +``` +Boolean -> Bool +Integer -> Number +Double -> Number +Float -> Number +String -> String +Callback -> function +ReadableMap -> Object +ReadableArray -> Array +``` +参阅[ReadableMap](https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java)和[ReadableArray](https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableArray.java)。 + +### 注册模块 + +在Java这边要做的最后一件事就是注册这个模块。我们需要在应用的Package类的`createNativeModules`方法中添加这个模块。如果模块没有被注册,它也无法在JavaScript中被访问到。 + +```java +package com.facebook.react.modules.toast; + +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 AnExampleReactPackage implements ReactPackage { + + @Override + public List> createJSModules() { + return Collections.emptyList(); + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules( + ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + + modules.add(new ToastModule(reactContext)); + + return modules; + } +``` + +这个package需要在`MainApplication.java`文件的`getPackages`方法中提供。这个文件位于你的react-native应用文件夹的android目录中。具体路径是: `android/app/src/main/java/com/your-app-name/MainApplication.java`. + +```java +protected List getPackages() { + return Arrays.asList( + new MainReactPackage(), + new AnExampleReactPackage()); // <-- 添加这一行,类名替换成你的Package类的名字. +} +``` + +为了让你的功能从JavaScript端访问起来更为方便,通常我们都会把原生模块封装成一个JavaScript模块。这不是必须的,但省下了每次都从`NativeModules`中获取对应模块的步骤。这个JS文件也可以用于添加一些其他JavaScript端实现的功能。 + +```javascript +'use strict'; + +/** + * This exposes the native ToastAndroid module as a JS module. This has a function 'show' + * which takes the following parameters: + * + * 1. String message: A string with the text to toast + * 2. int duration: The duration of the toast. May be ToastAndroid.SHORT or ToastAndroid.LONG + */ +import { NativeModules } from 'react-native'; + +// 下一句中的ToastAndroid即对应上文 +// public String getName()中返回的字符串 +// 练习时请务必选择另外的名字! + +export default NativeModules.ToastAndroid; +``` + +现在,在别处的JavaScript代码中可以这样调用你的方法: + +```javascript +import ToastAndroid from './ToastAndroid'; +ToastAndroid.show('Awesome', ToastAndroid.SHORT); +``` + +## 更多特性 + +### 回调函数 + +原生模块还支持一种特殊的参数——回调函数。它提供了一个函数来把返回值传回给JavaScript。 + +```java +public class UIManagerModule extends ReactContextBaseJavaModule { + +... + + @ReactMethod + public void measureLayout( + int tag, + int ancestorTag, + Callback errorCallback, + Callback successCallback) { + try { + measureLayout(tag, ancestorTag, mMeasureBuffer); + float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]); + float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]); + float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]); + float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]); + successCallback.invoke(relativeX, relativeY, width, height); + } catch (IllegalViewOperationException e) { + errorCallback.invoke(e.getMessage()); + } + } + +... +``` + +这个函数可以在JavaScript里这样使用: + +```js +UIManager.measureLayout( + 100, + 100, + (msg) => { + console.log(msg); + }, + (x, y, width, height) => { + console.log(x + ':' + y + ':' + width + ':' + height); + } +); +``` + +原生模块通常只应调用回调函数一次。但是,它可以保存callback并在将来调用。 + +请务必注意callback并非在对应的原生函数返回后立即被执行——注意跨语言通讯是异步的,这个执行过程会通过消息循环来进行。 + +## Promises + +__译注__:这一部分涉及到较新的js语法和特性,不熟悉的读者建议先阅读ES6的相关书籍和文档。 + +原生模块还可以使用promise来简化代码,搭配ES2016(ES7)标准的`async/await`语法则效果更佳。如果桥接原生方法的最后一个参数是一个`Promise`,则对应的JS方法就会返回一个Promise对象。 + +我们把上面的代码用promise来代替回调进行重构: + +```java +import com.facebook.react.bridge.Promise; + +public class UIManagerModule extends ReactContextBaseJavaModule { + +... + + @ReactMethod + public void measureLayout( + int tag, + int ancestorTag, + Promise promise) { + try { + measureLayout(tag, ancestorTag, mMeasureBuffer); + + WritableMap map = Arguments.createMap(); + + map.putDouble("relativeX", PixelUtil.toDIPFromPixel(mMeasureBuffer[0])); + map.putDouble("relativeY", PixelUtil.toDIPFromPixel(mMeasureBuffer[1])); + map.putDouble("width", PixelUtil.toDIPFromPixel(mMeasureBuffer[2])); + map.putDouble("height", PixelUtil.toDIPFromPixel(mMeasureBuffer[3])); + + promise.resolve(map); + } catch (IllegalViewOperationException e) { + promise.reject(e.getMessage()); + } + } + +... +``` + +现在JavaScript端的方法会返回一个Promise。这样你就可以在一个声明了`async`的异步函数内使用`await`关键字来调用,并等待其结果返回。(虽然这样写着看起来像同步操作,但实际仍然是异步的,并不会阻塞执行来等待)。 + +```js +async function measureLayout() { + try { + var { + relativeX, + relativeY, + width, + height, + } = await UIManager.measureLayout(100, 100); + + console.log(relativeX + ':' + relativeY + ':' + width + ':' + height); + } catch (e) { + console.error(e); + } +} + +measureLayout(); +``` + +### 多线程 + +原生模块不应对自己被调用时所处的线程做任何假设,当前的状况有可能会在将来的版本中改变。如果一个过程要阻塞执行一段时间,这个工作应当分配到一个内部管理的工作线程,然后从那边可以调用任意的回调函数。_译注_:我们通常用AsyncTask来完成这项工作。 + +### 发送事件到JavaScript + +原生模块可以在没有被调用的情况下往JavaScript发送事件通知。最简单的办法就是通过`RCTDeviceEventEmitter`,这可以通过`ReactContext`来获得对应的引用,像这样: + +```java +... +private void sendEvent(ReactContext reactContext, + String eventName, + @Nullable WritableMap params) { + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(eventName, params); +} +... +WritableMap params = Arguments.createMap(); +... +sendEvent(reactContext, "keyboardWillShow", params); +``` + +JavaScript模块可以通过使用`DeviceEventEmitter`模块来监听事件: + +```js +import { DeviceEventEmitter } from 'react-native'; +... +componentWillMount: function() { + DeviceEventEmitter.addListener('keyboardWillShow', function(e: Event) { + // handle event. + }); +} +... +``` + +### 从`startActivityForResult`中获取结果 + +如果你使用`startActivityForResult`调起了一个activity并想从其中获取返回结果,那么你需要监听`onActivityResult`事件。具体的做法是继承`BaseActivityEventListener`或是实现`ActivityEventListener`。我们推荐前一种做法,因为它相对来说不太会受到API变更的影响。然后你需要在模块的构造函数中注册这一监听事件。 + +```java +reactContext.addActivityEventListener(mActivityResultListener); +``` + +现在你可以通过重写下面的方法来实现对`onActivityResult`的监听: + +```java +@Override +public void onActivityResult( + final Activity activity, + final int requestCode, + final int resultCode, + final Intent intent) { + // 在这里实现你自己的逻辑 +} +``` + +下面我们写一个简单的图片选择器来实践一下。这个图片选择器会把`pickImage`方法暴露给JavaScript,而这个方法在调用时就会把图片的路径返回到JS端。 + +```java +public class ImagePickerModule extends ReactContextBaseJavaModule { + + private static final int IMAGE_PICKER_REQUEST = 467081; + private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST"; + private static final String E_PICKER_CANCELLED = "E_PICKER_CANCELLED"; + private static final String E_FAILED_TO_SHOW_PICKER = "E_FAILED_TO_SHOW_PICKER"; + private static final String E_NO_IMAGE_DATA_FOUND = "E_NO_IMAGE_DATA_FOUND"; + + private Promise mPickerPromise; + + private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() { + + @Override + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) { + if (requestCode == IMAGE_PICKER_REQUEST) { + if (mPickerPromise != null) { + if (resultCode == Activity.RESULT_CANCELED) { + mPickerPromise.reject(E_PICKER_CANCELLED, "Image picker was cancelled"); + } else if (resultCode == Activity.RESULT_OK) { + Uri uri = intent.getData(); + + if (uri == null) { + mPickerPromise.reject(E_NO_IMAGE_DATA_FOUND, "No image data found"); + } else { + mPickerPromise.resolve(uri.toString()); + } + } + + mPickerPromise = null; + } + } + } + }; + + public ImagePickerModule(ReactApplicationContext reactContext) { + super(reactContext); + + // Add the listener for `onActivityResult` + reactContext.addActivityEventListener(mActivityEventListener); + } + + @Override + public String getName() { + return "ImagePickerModule"; + } + + @ReactMethod + public void pickImage(final Promise promise) { + Activity currentActivity = getCurrentActivity(); + + if (currentActivity == null) { + promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist"); + return; + } + + // Store the promise to resolve/reject when picker returns data + mPickerPromise = promise; + + try { + final Intent galleryIntent = new Intent(Intent.ACTION_PICK); + + galleryIntent.setType("image/*"); + + final Intent chooserIntent = Intent.createChooser(galleryIntent, "Pick an image"); + + currentActivity.startActivityForResult(chooserIntent, IMAGE_PICKER_REQUEST); + } catch (Exception e) { + mPickerPromise.reject(E_FAILED_TO_SHOW_PICKER, e); + mPickerPromise = null; + } + } +} +``` + +### 监听生命周期事件 + +监听activity的生命周期事件(比如`onResume`, `onPause`等等)和我们在前面实现 `ActivityEventListener`的做法类似。模块必须实现`LifecycleEventListener`,然后需要在构造函数中注册一个监听函数: + +```java +reactContext.addLifecycleEventListener(this); +``` + +现在你可以通过实现下列方法来监听activity的生命周期事件了: + +```java +@Override +public void onHostResume() { + // Activity `onResume` +} + +@Override +public void onHostPause() { + // Activity `onPause` +} + +@Override +public void onHostDestroy() { + // Activity `onDestroy` +} +``` diff --git a/docs/docs/0.41/native-modules-ios.md b/docs/docs/0.41/native-modules-ios.md new file mode 100644 index 0000000..0ffa289 --- /dev/null +++ b/docs/docs/0.41/native-modules-ios.md @@ -0,0 +1,453 @@ +有时候App需要访问平台API,但React Native可能还没有相应的模块封装;或者你需要复用Objective-C、Swift或C++代码,而不是用JavaScript重新实现一遍;又或者你需要实现某些高性能、多线程的代码,譬如图片处理、数据库、或者各种高级扩展等等。 + +我们把React Native设计为可以在其基础上编写真正的原生代码,并且可以访问平台所有的能力。这是一个相对高级的特性,我们并不认为它应当在日常开发的过程中经常出现,但具备这样的能力是很重要的。如果React Native还不支持某个你需要的原生特性,你应当可以自己实现该特性的封装。 + +本文是关于如何封装原生模块的高级向导,我们假设您已经具备Objective-C或者Swift,以及iOS核心库(Foundation、UIKit)的相关知识。 + +## iOS 日历模块演示 + +本向导将会用[iOS日历API](https://developer.apple.com/library/mac/documentation/DataManagement/Conceptual/EventKitProgGuide/Introduction/Introduction.html)作为示例。我们的目标就是在Javascript中可以访问到iOS的日历功能。 + +在React Native中,一个“原生模块”就是一个实现了“RCTBridgeModule”协议的Objective-C类,其中RCT是ReaCT的缩写。 + +```objective-c +// CalendarManager.h +#import +#import + +@interface CalendarManager : NSObject +@end +``` + +为了实现`RCTBridgeModule`协议,你的类需要包含`RCT_EXPORT_MODULE()`宏。这个宏也可以添加一个参数用来指定在Javascript中访问这个模块的名字。如果你不指定,默认就会使用这个Objective-C类的名字。 + +```objective-c +// CalendarManager.m +@implementation CalendarManager + +RCT_EXPORT_MODULE(); + +@end +``` + +你必须明确的声明要给Javascript导出的方法,否则React Native不会导出任何方法。声明通过`RCT_EXPORT_METHOD()`宏来实现: + +```objective-c +RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location) +{ + RCTLogInfo(@"Pretending to create an event %@ at %@", name, location); +} +``` + +现在从Javascript里可以这样调用这个方法: + +```javascript +import { NativeModules } from 'react-native'; +var CalendarManager = NativeModules.CalendarManager; +CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey'); +``` + +> **注意**: Javascript方法名 +> +> 导出到Javascript的方法名是Objective-C的方法名的第一个部分。React Native还定义了一个`RCT_REMAP_METHOD()`宏,它可以指定Javascript方法名。当许多方法的第一部分相同的时候用它来避免在Javascript端的名字冲突。 + +桥接到Javascript的方法返回值类型必须是`void`。React Native的桥接操作是异步的,所以要返回结果给Javascript,你必须通过回调或者触发事件来进行。(参见本文档后面的部分) + +## 参数类型 + +`RCT_EXPORT_METHOD` 支持所有标准JSON类型,包括: + +- string (`NSString`) +- number (`NSInteger`, `float`, `double`, `CGFloat`, `NSNumber`) +- boolean (`BOOL`, `NSNumber`) +- array (`NSArray`) 包含本列表中任意类型 +- object (`NSDictionary`) 包含string类型的键和本列表中任意类型的值 +- function (`RCTResponseSenderBlock`) + +除此以外,任何`RCTConvert`类支持的的类型也都可以使用(参见[`RCTConvert`](https://github.com/facebook/react-native/blob/master/React/Base/RCTConvert.h)了解更多信息)。`RCTConvert`还提供了一系列辅助函数,用来接收一个JSON值并转换到原生Objective-C类型或类。 + +在我们的`CalendarManager`例子里,我们需要把事件的时间交给原生方法。我们不能在桥接通道里传递Date对象,所以需要把日期转化成字符串或数字来传递。我们可以这么实现原生函数: + +```objective-c +RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)secondsSinceUnixEpoch) +{ + NSDate *date = [RCTConvert NSDate:secondsSinceUnixEpoch]; +} +``` + +或者这样: + +```objective-c +RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSString *)ISO8601DateString) +{ + NSDate *date = [RCTConvert NSDate:ISO8601DateString]; +} +``` + +不过我们可以依靠自动类型转换的特性,跳过手动的类型转换,而直接这么写: + +```objective-c +RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(NSDate *)date) +{ + // Date is ready to use! +} +``` + +在Javascript既可以这样: + +```javascript +CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey', date.getTime()); // 把日期以unix时间戳形式传递 +``` + +也可以这样: + +```javascript +CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey', date.toISOString()); // 把日期以ISO-8601的字符串形式传递 +``` + +两个值都会被转换为正确的`NSDate`类型。但如果提供一个不合法的值,譬如一个`Array`,则会产生一个“红屏”报错信息。 + +随着`CalendarManager.addEvent`方法变得越来越复杂,参数的个数越来越多,其中有一些可能是可选的参数。在这种情况下我们应该考虑修改我们的API,用一个dictionary来存放所有的事件参数,像这样: + +```objective-c +#import + +RCT_EXPORT_METHOD(addEvent:(NSString *)name details:(NSDictionary *)details) +{ + NSString *location = [RCTConvert NSString:details[@"location"]]; + NSDate *time = [RCTConvert NSDate:details[@"time"]]; + ... +} +``` + +然后在JS里这样调用: + +```javascript +CalendarManager.addEvent('Birthday Party', { + location: '4 Privet Drive, Surrey', + time: date.toTime(), + description: '...' +}) +``` + +> **注意**: 关于数组和映射 +> +> Objective-C并没有提供确保这些结构体内部值的类型的方式。你的原生模块可能希望收到一个字符串数组,但如果JavaScript在调用的时候提供了一个混合number和string的数组,你会收到一个`NSArray`,里面既有`NSNumber`也有`NSString`。对于数组来说,`RCTConvert`提供了一些类型化的集合,譬如`NSStringArray`或者`UIColorArray`,你可以用在你的函数声明中。对于映射而言,开发者有责任自己调用`RCTConvert`的辅助方法来检测和转换值的类型。 + +## 回调函数 + +> **警告** +> +> 本章节内容目前还处在实验阶段,因为我们还并没有太多的实践经验来处理回调函数。 + +原生模块还支持一种特殊的参数——回调函数。它提供了一个函数来把返回值传回给JavaScript。 + +```objective-c +RCT_EXPORT_METHOD(findEvents:(RCTResponseSenderBlock)callback) +{ + NSArray *events = ... + callback(@[[NSNull null], events]); +} +``` + +`RCTResponseSenderBlock`只接受一个参数——传递给JavaScript回调函数的参数数组。在上面这个例子里我们用Node.js的常用习惯:第一个参数是一个错误对象(没有发生错误的时候为null),而剩下的部分是函数的返回值。 + +```javascript +CalendarManager.findEvents((error, events) => { + if (error) { + console.error(error); + } else { + this.setState({events: events}); + } +}) +``` + +原生模块通常只应调用回调函数一次。但是,它可以保存callback并在将来调用。这在封装那些通过“委托函数”来获得返回值的iOS API时最为常见。[`RCTAlertManager`](https://github.com/facebook/react-native/blob/master/React/Modules/RCTAlertManager.m)中就属于这种情况。 + +如果你想传递一个更接近`Error`类型的对象给Javascript,可以用[`RCTUtils.h`](https://github.com/facebook/react-native/blob/master/React/Base/RCTUtils.h)提供的`RCTMakeError`函数。现在它仅仅是发送了一个和Error结构一样的dictionary给Javascript,但我们考虑在将来版本里让它产生一个真正的`Error`对象。 + +> **注意** +> +> 如果你传递了回调函数,那么在原生端就必须执行它(如果传递了两个,比如onSuccess和onFail,那么执行其中一个即可),否则会导致内存泄漏。 + +## Promises + +__译注__:这一部分涉及到较新的js语法和特性,不熟悉的读者建议先阅读ES6的相关书籍和文档。 + +原生模块还可以使用promise来简化代码,搭配ES2016(ES7)标准的`async/await`语法则效果更佳。如果桥接原生方法的最后两个参数是`RCTPromiseResolveBlock`和`RCTPromiseRejectBlock`,则对应的JS方法就会返回一个Promise对象。 + +我们把上面的代码用promise来代替回调进行重构: + +```objective-c +RCT_REMAP_METHOD(findEvents, + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSArray *events = ... + if (events) { + resolve(events); + } else { + reject(error); + } +} +``` + +现在JavaScript端的方法会返回一个Promise。这样你就可以在一个声明了`async`的异步函数内使用`await`关键字来调用,并等待其结果返回。(虽然这样写着看起来像同步操作,但实际仍然是异步的,并不会阻塞执行来等待)。 + +```js +async function updateEvents() { + try { + var events = await CalendarManager.findEvents(); + + this.setState({ events }); + } catch (e) { + console.error(e); + } +} + +updateEvents(); +``` + +## 多线程 + +原生模块不应对自己被调用时所处的线程做任何假设。React Native在一个独立的串行GCD队列中调用原生模块的方法,但这属于实现的细节,并且可能会在将来的版本中改变。通过实现方法`- (dispatch_queue_t)methodQueue`,原生模块可以指定自己想在哪个队列中被执行。具体来说,如果模块需要调用一些必须在主线程才能使用的API,那应当这样指定: + +```objective-c +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} +``` + +类似的,如果一个操作需要花费很长时间,原生模块不应该阻塞住,而是应当声明一个用于执行操作的独立队列。举个例子,`RCTAsyncLocalStorage`模块创建了自己的一个queue,这样它在做一些较慢的磁盘操作的时候就不会阻塞住React本身的消息队列: + +```objective-c +- (dispatch_queue_t)methodQueue +{ + return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL); +} +``` + +指定的`methodQueue`会被你模块里的所有方法共享。如果你的方法中“只有一个”是耗时较长的(或者是由于某种原因必须在不同的队列中运行的),你可以在函数体内用`dispatch_async`方法来在另一个队列执行,而不影响其他方法: + +```objective-c +RCT_EXPORT_METHOD(doSomethingExpensive:(NSString *)param callback:(RCTResponseSenderBlock)callback) +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // 在这里执行长时间的操作 + ... + // 你可以在任何线程/队列中执行回调函数 + callback(@[...]); + }); +} +``` + +> **注意**: 在模块之间共享分发队列 +> +> `methodQueue`方法会在模块被初始化的时候被执行一次,然后会被React Native的桥接机制保存下来,所以你不需要自己保存队列的引用,除非你希望在模块的其它地方使用它。但是,如果你希望在若干个模块中共享同一个队列,则需要自己保存并返回相同的队列实例;仅仅是返回相同名字的队列是不行的。 + +## 依赖注入 +bridge会自动注册实现了`RCTBridgeModule`协议的模块,但是你可能也希望能够初始化自定义的模块实例(这样可以注入依赖)。 + +要实现这个功能,你需要实现`RTCBridgeDelegate`协议,初始化`RTCBridge`,并且在初始化方法里指定代理。然后用初始化好的`RTCBridge`实例初始化一个`RTCRootView`。 + +```objective-c +id moduleInitialiser = [[classThatImplementsRTCBridgeDelegate alloc] init]; + +RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:moduleInitialiser launchOptions:nil]; + +RCTRootView *rootView = [[RCTRootView alloc] + initWithBridge:bridge + moduleName:kModuleName + initialProperties:nil]; +``` + +## 导出常量 + +原生模块可以导出一些常量,这些常量在JavaScript端随时都可以访问。用这种方法来传递一些静态数据,可以避免通过bridge进行一次来回交互。 + +```objective-c +- (NSDictionary *)constantsToExport +{ + return @{ @"firstDayOfTheWeek": @"Monday" }; +} +``` + +Javascript端可以随时同步地访问这个数据: + +```javascript +console.log(CalendarManager.firstDayOfTheWeek); +``` + +但是注意这个常量仅仅在初始化的时候导出了一次,所以即使你在运行期间改变`constantToExport`返回的值,也不会影响到JavaScript环境下所得到的结果。 + +### 枚举常量 + +用`NS_ENUM`定义的枚举类型必须要先扩展对应的RCTConvert方法才可以作为函数参数传递。 + +假设我们要导出如下的`NS_ENUM`定义: + +```objc +typedef NS_ENUM(NSInteger, UIStatusBarAnimation) { + UIStatusBarAnimationNone, + UIStatusBarAnimationFade, + UIStatusBarAnimationSlide, +}; +``` + +你需要这样来扩展RCTConvert类: + +```objc +@implementation RCTConvert (StatusBarAnimation) + RCT_ENUM_CONVERTER(UIStatusBarAnimation, (@{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone), + @"statusBarAnimationFade" : @(UIStatusBarAnimationFade), + @"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide)}), + UIStatusBarAnimationNone, integerValue) +@end +``` + +接着你可以这样定义方法并且导出enum值作为常量: + +```objc +- (NSDictionary *)constantsToExport +{ + return @{ @"statusBarAnimationNone" : @(UIStatusBarAnimationNone), + @"statusBarAnimationFade" : @(UIStatusBarAnimationFade), + @"statusBarAnimationSlide" : @(UIStatusBarAnimationSlide) } +}; + +RCT_EXPORT_METHOD(updateStatusBarAnimation:(UIStatusBarAnimation)animation + completion:(RCTResponseSenderBlock)callback) +``` + +你的枚举现在会用上面提供的选择器进行转换(上面的例子中是`integerValue`),然后再传递给你导出的函数。 + +## 给Javascript发送事件 + +即使没有被JavaScript调用,原生模块也可以给JavaScript发送事件通知。最好的方法是继承`RCTEventEmitter`,实现`suppportEvents`方法并调用`self sendEventWithName:`。 + +```objective-c +// CalendarManager.h +#import +#import + +@interface CalendarManager : RCTEventEmitter + +@end +``` +```objective-c +// CalendarManager.m +#import "CalendarManager.h" + +@implementation CalendarManager + +RCT_EXPORT_MODULE(); + +- (NSArray *)supportedEvents +{ + return @[@"EventReminder"]; +} + +- (void)calendarEventReminderReceived:(NSNotification *)notification +{ + NSString *eventName = notification.userInfo[@"name"]; + [self sendEventWithName:@"EventReminder" body:@{@"name": eventName}]; +} + +@end +``` +JavaScript代码可以创建一个包含你的模块的`NativeEventEmitter`实例来订阅这些事件。 + +```javascript +import { NativeEventEmitter, NativeModules } from 'react-native'; +const { CalendarManager } = NativeModules; + +const calendarManagerEmitter = new NativeEventEmitter(CalendarManager); + +const subscription = calendarManagerEmitter.addListener( + 'EventReminder', +  (reminder) => console.log(reminder.name) +); +... +// 别忘了取消订阅,通常在componentWillUnmount生命周期方法中实现。 +subscription.remove(); +``` +更多给JavaScript发送事件的例子请看[`RCTLocationObserver`](https://github.com/facebook/react-native/blob/master/Libraries/Geolocation/RCTLocationObserver.m)。 + + +## 优化无监听处理的事件 + +如果你发送了一个事件却没有任何监听处理,则会因此收到一个资源警告。要优化因此带来的额外开销,你可以在你的`RCTEventEmitter`子类中覆盖`startObserving`和`stopObserving`方法。 + +```objective-c +@implementation CalendarManager +{ + bool hasListeners; +} + +// 在添加第一个监听函数时触发 +-(void)startObserving { + hasListeners = YES; + // Set up any upstream listeners or background tasks as necessary +} + +// Will be called when this module's last listener is removed, or on dealloc. +-(void)stopObserving { + hasListeners = NO; + // Remove upstream listeners, stop unnecessary background tasks +} + +- (void)calendarEventReminderReceived:(NSNotification *)notification +{ + NSString *eventName = notification.userInfo[@"name"]; + if (hasListeners) { // Only send events if anyone is listening + [self sendEventWithName:@"EventReminder" body:@{@"name": eventName}]; + } +} +``` + +## 从Swift导出 + +Swift不支持宏,所以从Swift向React Native导出类和函数需要多做一些设置,但是大致与Objective-C是相同的。 + +假设我们已经有了一个一样的`CalendarManager`,不过是用Swift实现的类: + +```swift +// CalendarManager.swift + +@objc(CalendarManager) +class CalendarManager: NSObject { + + @objc func addEvent(name: String, location: String, date: NSNumber) -> Void { + // Date is ready to use! + } + +} +``` + +> **注意**: 你必须使用@objc标记来确保类和函数对Objective-C公开。 + +接着,创建一个私有的实现文件,并将必要的信息注册到React Native中。 + +```objc +// CalendarManagerBridge.m +#import + +@interface RCT_EXTERN_MODULE(CalendarManager, NSObject) + +RCT_EXTERN_METHOD(addEvent:(NSString *)name location:(NSString *)location date:(nonnull NSNumber *)date) + +@end +``` + +请注意,一旦你[在IOS中混用2种语言](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html), 那就还需要一个额外的桥接头文件,称作“bridging header”,用来导出Objective-C文件给Swift。如果你是通过Xcode菜单中的`File>New File`来创建的Swift文件,Xcode会自动为你创建这个头文件。在这个头文件中,你需要引入`RCTBridgeModule.h`。 + +```objc +// CalendarManager-Bridging-Header.h +#import +``` + +同样的,你也可以使用`RCT_EXTERN_REMAP_MODULE`和`RCT_EXTERN_REMAP_METHOD`来改变导出模块和方法的JavaScript调用名称。 +了解更多信息,请参阅[`RCTBridgeModule`](https://github.com/facebook/react-native/blob/master/React/Base/RCTBridgeModule.h). + + diff --git a/docs/docs/0.41/nativemethodsmixin.md b/docs/docs/0.41/nativemethodsmixin.md new file mode 100644 index 0000000..f9fab4f --- /dev/null +++ b/docs/docs/0.41/nativemethodsmixin.md @@ -0,0 +1,48 @@ +`NativeMethodsMixin`提供了一些用于直接访问底层原生组件的方法。这在你需要聚焦(focus)一个视图或者计算它在屏幕上显示的尺寸之类的情况下可能会需要。 + +这些方法在大部分React Native提供的默认的组件中都可以使用。注意,它们不能在一些复合组件(并非直接由原生视图构成)中使用,这可能包括你自己在应用中定义的绝大部分组件。想了解更多信息,可以参阅[直接操作](direct-manipulation.html)。 + +### 方法 + +
+
+

static measure(callback: MeasureOnSuccessCallback) #

+
+

计算指定视图在屏幕上显示的位置和尺寸,通过一个异步回调返回计算的结果。如果成功,回调函数会被调用,并带有以下参数::

+
    +
  • x
  • +
  • y
  • +
  • width
  • +
  • height
  • +
  • pageX
  • +
  • pageY
  • +
+

注意这些信息直到原生渲染完成之前都不能使用。如果你希望尽快获取视图的位置和尺寸信息,考虑使用onLayout属性来替代。

+
+
+
+

static measureLayout(relativeToNativeNode: number, onSuccess: MeasureLayoutOnSuccessCallback, onFail: () => void) #

+
+

measure()函数类似,不过计算的是相对指定祖先节点relativeToNativeNode的位置和尺寸。这意味着返回的x, y是相对于指定祖先视图的。

+

要找到一个组件的原生节点的ID,一般做法是调用React.findNodeHandle(component)

+
+
+
+

static setNativeProps(nativeProps: Object) #

+
+

这个函数直接发送属性到原生代码。这些属性不会参与后续的对比过程——这意味着如果你不在下一次render中包含这些属性,这些属性还会保持有效(参见直接操作)。

+
+
+
+

static focus() #

+
+

请求聚焦指定的输入框或者视图。具体的效果要取决于平台和视图的类型。

+
+
+
+

static blur() #

+
+

移除指定的输入框或者视图的焦点。这是focus()的相反操作。。

+
+
+
\ No newline at end of file diff --git a/docs/docs/0.41/navigation.md b/docs/docs/0.41/navigation.md new file mode 100644 index 0000000..1cba7e8 --- /dev/null +++ b/docs/docs/0.41/navigation.md @@ -0,0 +1,333 @@ +本文档总结对比了React Native中现有的几个导航组件。如果你刚开始接触,那么直接选择`Navigator`就好。如果你只针对iOS平台开发,并且想和系统原生外观一致,那么可以选择`NavigatorIOS`。如果你想更好地管理导航栈,那么应该尝试一下`NavigationExperimental`。 + +## Navigator + +`Navigator`使用纯JavaScript实现了一个导航栈,因此可以跨平台工作,同时也便于定制。这也是我们在[使用导航器跳转页面](using-navigators.html)的教程中示例用的组件。 + +![](img/NavigationStack-Navigator.gif) + +`Navigator`可以在`renderScene`方法中根据当前路由渲染不同的组件。默认情况下新的场景会从屏幕右侧滑进来,但你也可以通过`configureScene`方法来管理这一行为。你还可以通过`navigationBar`属性来配置一个跨场景的导航栏。(译注:但我们不推荐使用跨场景的navigationBar,它的代码逻辑维护起来很困难!建议自己在场景中用`View`实现自定义的导航栏。) + +点击这里阅读[Navigator的API文档](navigator.html)。 + +## NavigatorIOS + +如果你只针对iOS平台开发,那么可以考虑使用[NavigatorIOS](navigatorios.html)。它是基于 [`UINavigationController`](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UINavigationController_Class/)封装的,所以看起来很像。 + +![](img/NavigationStack-NavigatorIOS.gif) + +```javascript + +``` + +用法类似`Navigator`,`NavigatorIOS`也使用路由对象来描述场景,但有一些重要区别。其中要渲染的组件在路由对象的`component`字段中指定,要给目标组件传递的参数则写在`passProps`中。被渲染的component都会自动接受到一个名为`navigator`的属性,你可以直接调用此对象(this.props.navigator)的`push`和`pop`方法。 + +由于`NavigatorIOS`使用的是原生的UIKit导航,所以它会自动渲染一个带有返回按钮和标题的导航栏。 + +```javascript +import React, { Component, PropTypes } from 'react'; +import { NavigatorIOS, Text, TouchableHighlight, View } from 'react-native'; + +export default class NavigatorIOSApp extends Component { + render() { + return ( + + ) + } +} + +class MyScene extends Component { + static propTypes = { + title: PropTypes.string.isRequired, + navigator: PropTypes.object.isRequired, + } + + constructor(props, context) { + super(props, context); + this._onForward = this._onForward.bind(this); + } + + _onForward() { + this.props.navigator.push({ + title: 'Scene ' + nextIndex, + }); + } + + render() { + return ( + + Current Scene: { this.props.title } + + Tap me to load the next scene + + + ) + } +} +``` + +点击这里阅读[Navigator的API文档](navigatorios.html)。 + +> 你还可以看看[react-navigation](https://reactnavigation.org/),这是一个尚处在实验阶段的官方组件,旨在于提供原生的跨平台的导航组件。 + +## NavigationExperimental + +`Navigator`和`NavigatorIOS`都是有状态的组件。如果你在app中多处使用这些组件,那么维护工作就会变得非常麻烦。`NavigationExperimental`以不同的方式实现了导航,它可以使用任何视图来作为导航视图,同时还用到了规约函数(reducer)自顶向下地管理状态。正如名字中的`Experimental`所示,这一组件的整体实现具有一定的实验性,但我们仍然建议你尝试一下用它去更好地管理应用的导航。 + +```javascript + +``` + +引入`NavigationExperimental`的步骤和React Native中的其他组件一样。在引入此组件之后,还可以进一步解构其中一些有用的子组件,比如这里我们会从中解构`NavigationCardStack`和 `NavigationStateUtils`这两个子组件。 + +```javascript +import React, { Component } from 'react'; +import { NavigationExperimental } from 'react-native'; + +const { + CardStack: NavigationCardStack, + StateUtils: NavigationStateUtils, +} = NavigationExperimental; +``` + +正如上文所说,`NavigationExperimental`的实现机制与`Navigator`和`NavigatorIOS`有所不同。用它来构筑导航栈还需要一些额外的步骤,但这些步骤并不是无用功。 + +### 第一步:定义初始状态和根容器 + +首先创建一个新组件,我们会把它作为根容器,并在这里定义初始状态。导航栈会定义在`navigationState`字段中,其中也包含了初始的路由定义: + +```javascript +class BleedingEdgeApplication extends Component { + constructor(props, context) { + super(props, context); + + this.state = { + // 定义初始的导航状态 + navigationState: { + index: 0, // 现在是第一页(索引从0开始) + routes: [{key: 'My Initial Scene'}], // 初始仅设定一个路由 + }, + }; + + // 我们稍后再补充此函数的实现细节 + this._onNavigationChange = this._onNavigationChange.bind(this); + } + + _onNavigationChange(type) { + // 我们稍后再补充此函数的实现细节 + } + + render() { + return ( + 这是一段占位的文字。稍后我们会在这里渲染导航。 + ); + } +} +``` + +现在我们定义了一个有状态的组件,然而暂时并无太多卵用。我们的初始状态包含了一个路由对象,以及当前页面的索引值。但是这看起来跟Navigator的初始路由定义好像没什么区别嘛!回忆一下navigator对象提供了哪些操作?——对的,push和pop,看起来也非常直观。但是前面我们说过了,现在我们会在根容器上使用规约函数来管理状态。下面注意仔细看好了。 + +### 第二步:规约导航状态 + +NavigationExperimental内置了一些有用的规约函数(reducer),都放在NavigationStateUtils中。我们现在要用的两个就是push和pop了。它们接受一个navigationState对象参数,然后返回新的navigationState对象。 + +据此我们可以这样来编写`_onNavigationChange`函数,在其中判断"push"和"pop"的行为,并分别规约对应的状态。 + +```javascript +_onNavigationChange(type) { + // 从state中解构出navigationState + let {navigationState} = this.state; + + switch (type) { + case 'push': + // push一个新路由,在这里就是一个带有key属性的对象。 + // 我个人喜欢随机数的key(但是说正经的,key必须要确保唯一性) + const route = {key: 'Route-' + Date.now()}; + + // 调用NavigationStateUtils提供的push规约函数 + navigationState = NavigationStateUtils.push(navigationState, route); + break; + + case 'pop': + // 使用pop函数来弹出当前路由 + navigationState = NavigationStateUtils.pop(navigationState); + break; + } + + // 如果没有实际变化,则NavigationStateUtils会返回同样的`navigationState` + // 我们只会更新确实发生变化的状态 + if (this.state.navigationState !== navigationState) { + // 请记住更新状态必须通过setState()方法! + this.setState({navigationState}); + // 如果你还不了解ES6中的新语法,那么简单讲解一下上面那一句 + // 如果key和value的字面一样,那么可以简写成一个,等同于下面的写法: + // this.setState({navigationState: navigationState}); + } +} +``` + +Cool.我们已经触碰到了NavigationExperimental的精髓之所在。这里我们只处理了两种行为,实际开发中行为可能更复杂,比如可能会考虑后退(back)行为,又或者是tab间的切换过渡行为等等。 + +我们现在还没写初始场景和实际的导航器,不过别急,我们一步一步来。 + +### 第三步:定义场景 + +为方便起见我们先定义一个Row(行)组件。其中显示了一些文字,并带有点击事件。 + +```javascript +class TappableRow extends Component { + render() { + return ( + + + {this.props.text} + + + ); + } +} +``` + +现在来定义实际的场景。其中用到了一个ScrollView来显示一个垂直列表,第一行显示当前路由对象的key字段值,后两行用来点击后调用导航器的push和pop方法。 + +```javascript +class MyVeryComplexScene extends Component { + render() { + return ( + + + Route: {this.props.route.key} + + + + + ); + } +} +``` + +### 第四步:创建导航栈 + +我们之前已经定义了状态和管理状态的规约函数,现在可以创建导航器组件了。在写导航器的同时,我们可以使用当前路由的属性来配置场景并渲染它了。 + +```javascript +class MyVerySimpleNavigator extends Component { + + // 在这里绑定一些导航用的方法 + constructor(props, context) { + super(props, context); + + this._onPushRoute = this.props.onNavigationChange.bind(null, 'push'); + this._onPopRoute = this.props.onNavigationChange.bind(null, 'pop'); + + this._renderScene = this._renderScene.bind(this); + } + + // Now we finally get to use the `NavigationCardStack` to render the scenes. + render() { + return ( + + ); + } + + // 根据路由来渲染场景 + // `sceneProps`的具体结构定义在`NavigationTypeDefinition`的`NavigationSceneRendererProps`中 + // 这里你可以根据路由的不同来返回不同的场景组件,我们这里为了简要说明,始终只返回这一个场景组件 + _renderScene(sceneProps) { + return ( + + ); + } +} +``` + +差不多了!我已经可以闻到终点线的味道啦。现在把我们新做的导航器放到根容器中: + +```javascript +class BleedingEdgeApplication extends Component { + + // 为了简化说明,这里省略了constructor和其他的方法 + + render() { + return ( + + ); + } +} +``` + +完工了!赞美NavigationExperimental吧! + +#### 等一下——好像少了什么? + +(啊没错,我们忘了引入组件和样式。) + +```javascript +import { NavigationExperimental, PixelRatio, ScrollView, StyleSheet, Text, TouchableHighlight } from 'react-native'; + +const styles = StyleSheet.create({ + navigator: { + flex: 1, + }, + scrollView: { + marginTop: 64 + }, + row: { + padding: 15, + backgroundColor: 'white', + borderBottomWidth: 1 / PixelRatio.get(), + borderBottomColor: '#CDCDCD', + }, + rowText: { + fontSize: 17, + }, + buttonText: { + fontSize: 17, + fontWeight: '500', + }, +}); +``` + +### 小作业 + +你现在是导航器的专家了!参考下我们写的[NavigationExperimental的例子](https://github.com/facebook/react-native/tree/master/Examples/UIExplorer/js/NavigationExperimental),学习如何实现其他类型的导航结构,比如多个tab对应多个导航栈的情况。 + diff --git a/docs/docs/next/navigator-comparison.md b/docs/docs/0.41/navigator-comparison.md similarity index 100% rename from docs/docs/next/navigator-comparison.md rename to docs/docs/0.41/navigator-comparison.md diff --git a/docs/docs/0.41/navigator.md b/docs/docs/0.41/navigator.md new file mode 100644 index 0000000..672829c --- /dev/null +++ b/docs/docs/0.41/navigator.md @@ -0,0 +1,127 @@ +使用导航器可以让你在应用的不同场景(页面)间进行切换。导航器通过路由对象来分辨不同的场景。利用`renderScene`方法,导航栏可以根据指定的路由来渲染场景。 + +可以通过`configureScene`属性获取指定路由对象的配置信息,从而改变场景的动画或者手势。查看`Navigator.SceneConfigs`来获取默认的动画和更多的场景配置选项。 + +__译注__:本文档的说明较为简略,使用上有一定的难度。论坛中有一篇更为详细的[教程](http://bbs.reactnative.cn/topic/20),推荐阅读。 + +### 截图 +![](img/components/navigator1.png) + +![](img/components/navigator2.png) + +### 基本用法 +```javascript + + { + var nextIndex = route.index + 1; + navigator.push({ + name: 'Scene ' + nextIndex, + index: nextIndex, + }); + }} + onBack={() => { + if (route.index > 0) { + navigator.pop(); + } + }} + /> + } + /> +``` + +### 导航方法 +如果你得到了一个navigator对象的引用(__译注__:再次推荐仔细阅读此[教程](http://bbs.reactnative.cn/topic/20),理解如何在renderScene方法中传递navigator对象,否则直接调用会报undefined错误),则可以调用许多方法来进行导航: + +* getCurrentRoutes() - 获取当前栈里的路由,也就是push进来,没有pop掉的那些。 +* jumpBack() - 跳回之前的路由,当然前提是保留现在的,还可以再跳回来,会给你保留原样。 +* jumpForward() - 上一个方法不是调到之前的路由了么,用这个跳回来就好了。 +* jumpTo(route) - 跳转到已有的场景并且不卸载。 +* push(route) - 跳转到新的场景,并且将场景入栈,你可以稍后跳转过去 +* pop() - 跳转回去并且卸载掉当前场景 +* replace(route) - 用一个新的路由替换掉当前场景 +* replaceAtIndex(route, index) - 替换掉指定序列的路由场景 +* replacePrevious(route) - 替换掉之前的场景 +* resetTo(route) - 跳转到新的场景,并且重置整个路由栈 +* immediatelyResetRouteStack(routeStack) - 用新的路由数组来重置路由栈 +* popToRoute(route) - pop到路由指定的场景,在整个路由栈中,处于指定场景之后的场景将会被卸载。 +* popToTop() - pop到栈中的第一个场景,卸载掉所有的其他场景。 + +### 属性 + +
+
+

configureScene function #

+
+

可选的函数,用来配置场景动画和手势。会带有两个参数调用,一个是当前的路由,一个是当前的路由栈。然后它应当返回一个场景配置对象

+
(route, routeStack) => Navigator.SceneConfigs.FloatFromRight
+ +
    +
  • Navigator.SceneConfigs.PushFromRight (默认)
  • +
  • Navigator.SceneConfigs.FloatFromRight
  • +
  • Navigator.SceneConfigs.FloatFromLeft
  • +
  • Navigator.SceneConfigs.FloatFromBottom
  • +
  • Navigator.SceneConfigs.FloatFromBottomAndroid
  • +
  • Navigator.SceneConfigs.FadeAndroid
  • +
  • Navigator.SceneConfigs.HorizontalSwipeJump
  • +
  • Navigator.SceneConfigs.HorizontalSwipeJumpFromRight
  • +
  • Navigator.SceneConfigs.VerticalUpSwipeJump
  • +
  • Navigator.SceneConfigs.VerticalDownSwipeJump
  • +
+
+
+
+

initialRoute object #

+
+

定义启动时加载的路由。路由是导航栏用来识别渲染场景的一个对象。initialRoute必须是initialRouteStack中的一个路由。initialRoute默认为initialRouteStack中最后一项。

+
+
+
+

initialRouteStack [object] #

+
+

提供一个路由集合用来初始化。如果没有设置初始路由的话则必须设置该属性。如果没有提供该属性,它将被默认设置成一个只含有initialRoute的数组。

+
+
+
+

navigationBar node #

+
+

可选参数,提供一个在场景切换的时候保持的导航栏。

+
+
+
+

navigator object #

+
+

可选参数,提供从父导航器获得的导航器对象。

+
+
+
+

onDidFocus function #

+
+

每当导航切换完成或初始化之后,调用此回调,参数为新场景的路由。

+
+
+
+

onWillFocus function #

+
+

会在导航切换之前调用,参数为目标路由。

+
+
+
+

renderScene function #

+
+

必要参数。用来渲染指定路由的场景。调用的参数是路由和导航器。

+
(route, navigator) =>
+  <MySceneComponent title={route.title} navigator={navigator} />
+
+
+
+
+

sceneStyle View#style #

+
+

将会应用在每个场景的容器上的样式。

+
+
+
diff --git a/docs/docs/0.41/navigatorios.md b/docs/docs/0.41/navigatorios.md new file mode 100644 index 0000000..cc888be --- /dev/null +++ b/docs/docs/0.41/navigatorios.md @@ -0,0 +1,650 @@ +`NavigatorIOS` is a wrapper around `UINavigationController`, enabling you to implement a navigation stack. It works exactly the same as it would on a native app using `UINavigationController`, providing the same animations and behavior from UIKIt. + +As the name implies, it is only available on iOS. Take a look at Navigator for a similar solution for your cross-platform needs, or check out [react-native-navigation](https://github.com/wix/react-native-navigation), a component that aims to provide native navigation on both iOS and Android. + +To set up the navigator, provide the `initialRoute` prop with a route object. A route object is used to describe each scene that your app navigates to. `initialRoute` represents the first route in your navigator. + +```js +import React, { Component, PropTypes } from 'react'; +import { NavigatorIOS, Text } from 'react-native'; + +export default class NavigatorIOSApp extends Component { + render() { + return ( + + ); + } +} + +class MyScene extends Component { + static propTypes = { + title: PropTypes.string.isRequired, + navigator: PropTypes.object.isRequired, + } + + constructor(props, context) { + super(props, context); + this._onForward = this._onForward.bind(this); + this._onBack = this._onBack.bind(this); + } + + _onForward() { + this.props.navigator.push({ + title: 'Scene ' + nextIndex, + }); + } + + render() { + return ( + + Current Scene: { this.props.title } + + Tap me to load the next scene + + + ) + } +} +``` + +In this code, the navigator renders the component specified in `initialRoute`, which in this case is `MyScene`. This component will receive a `route` prop and a `navigator` prop representing the navigator. The navigator's navigation bar will render the title for the current scene, "My Initial Scene". + +You can optionally pass in a `passProps` property to your `initialRoute`. `NavigatorIOS` passes this in as props to the rendered component: + +```js +initialRoute={{ + component: MyScene, + title: 'My Initial Scene', + passProps: { myProp: 'foo' } +}} +``` + +You can then access the props passed in via `{this.props.myProp}`. + +### 处理导航 + +To trigger navigation functionality such as pushing or popping a view, you have access to a navigator object. The object is passed in as a prop to any component that is rendered by NavigatorIOS. You can then call the relevant methods to perform the navigation action you need: + +```js +class MyView extends Component { + _handleBackPress() { + this.props.navigator.pop(); + } + + _handleNextPress(nextRoute) { + this.props.navigator.push(nextRoute); + } + + render() { + const nextRoute = { + component: MyView, + title: 'Bar That', + passProps: { myProp: 'bar' } + }; + return( + this._handleNextPress(nextRoute)}> + + See you on the other nav {this.props.myProp}! + + + ); + } +} +You can also trigger navigator functionality from the NavigatorIOS component: + +class NavvyIOS extends Component { + _handleNavigationRequest() { + this.refs.nav.push({ + component: MyView, + title: 'Genius', + passProps: { myProp: 'genius' }, + }); + } + + render() { + return ( + this._handleNavigationRequest(), + }} + style={{flex: 1}} + /> + ); + } +} +``` + +The code above adds a `_handleNavigationRequest` private method that is invoked from the `NavigatorIOS` component when the right navigation bar item is pressed. To get access to the navigator functionality, a reference to it is saved in the `ref` prop and later referenced to push a new scene into the navigation stack. + +### 导航栏的配置 + +Props passed to `NavigatorIOS` will set the default configuration for the navigation bar. Props passed as properties to a route object will set the configuration for that route's navigation bar, overriding any props passed to the `NavigatorIOS `component. + +```js +_handleNavigationRequest() { + this.refs.nav.push({ + //... + passProps: { myProp: 'genius' }, + barTintColor: '#996699', + }); +} + +render() { + return ( + + ); +} +``` + +In the example above the navigation bar color is changed when the new route is pushed. + +### 截图 +![](img/components/navigatorios1.png) + +![](img/components/navigatorios2.png) + +### 属性 + +
+
+

barTintColor string #

+
+

导航条的背景颜色。

+
+
+
+

initialRoute {component: function, title: string, passProps: object, backButtonIcon: Image.propTypes.source, backButtonTitle: string, leftButtonIcon: Image.propTypes.source, leftButtonTitle: string, onLeftButtonPress: function, rightButtonIcon: Image.propTypes.source, rightButtonTitle: string, onRightButtonPress: function, wrapperStyle: [object Object]} #

+
+

NavigatorIOS使用"路由"对象来包含要渲染的子视图、它们的属性、以及导航条配置。"push"和任何其它的导航函数的参数都是这样的路由对象。

+
+
+
+

itemWrapperStyle View#style #

+
+

导航器中的组件的默认属性。一个常见的用途是设置所有页面的背景颜色。

+
+
+
+

navigationBarHidden bool #

+
+

一个布尔值,决定导航栏是否隐藏。

+
+
+
+

shadowHidden bool #

+
+

一个布尔值,决定是否要隐藏1像素的阴影

+
+
+
+

tintColor string #

+
+

导航栏上按钮的颜色。

+
+
+
+

titleTextColor string #

+
+

导航器标题的文字颜色。

+
+
+
+

translucent bool #

+
+

一个布尔值,决定是否导航条是半透明的。

+
+
+
+

interactivePopGestureEnabled bool #

+
+

决定是否启用滑动返回手势。不指定此属性时,手势会根据navigationBar的显隐情况决定是否启用(显示时启用手势,隐藏时禁用手势)。指定此属性后,手势与navigationBar的显隐情况无关。

+
+
+
+ +### 方法 + +
+

push(route: { + component: Function; + title: string; + passProps?: Object; + backButtonTitle?: string; + backButtonIcon?: Object; + leftButtonTitle?: string; + leftButtonIcon?: Object; + onLeftButtonPress?: Function; + rightButtonTitle?: string; + rightButtonIcon?: Object; + onRightButtonPress?: Function; + wrapperStyle?: any; +}) #

+

Navigate forward to a new route

+
+

popN(n: number) #

+

Go back N pages at once. When N=1, behavior matches pop()

+
+

pop() #

+

Go back one page

+
+

replaceAtIndex(route: { + component: Function; + title: string; + passProps?: Object; + backButtonTitle?: string; + backButtonIcon?: Object; + leftButtonTitle?: string; + leftButtonIcon?: Object; + onLeftButtonPress?: Function; + rightButtonTitle?: string; + rightButtonIcon?: Object; + onRightButtonPress?: Function; + wrapperStyle?: any; +}, index: number) #

+

Replace a route in the navigation stack.

+

index specifies the route in the stack that should be replaced. + If it's negative, it counts from the back.

+
+

replace(route: { + component: Function; + title: string; + passProps?: Object; + backButtonTitle?: string; + backButtonIcon?: Object; + leftButtonTitle?: string; + leftButtonIcon?: Object; + onLeftButtonPress?: Function; + rightButtonTitle?: string; + rightButtonIcon?: Object; + onRightButtonPress?: Function; + wrapperStyle?: any; +}) #

+

Replace the route for the current page and immediately + load the view for the new route.

+
+

replacePrevious(route: { + component: Function; + title: string; + passProps?: Object; + backButtonTitle?: string; + backButtonIcon?: Object; + leftButtonTitle?: string; + leftButtonIcon?: Object; + onLeftButtonPress?: Function; + rightButtonTitle?: string; + rightButtonIcon?: Object; + onRightButtonPress?: Function; + wrapperStyle?: any; +}) #

+

Replace the route/view for the previous page.

+
+

popToTop() #

+

Go back to the top item

+
+

popToRoute(route: { + component: Function; + title: string; + passProps?: Object; + backButtonTitle?: string; + backButtonIcon?: Object; + leftButtonTitle?: string; + leftButtonIcon?: Object; + onLeftButtonPress?: Function; + rightButtonTitle?: string; + rightButtonIcon?: Object; + onRightButtonPress?: Function; + wrapperStyle?: any; +}) #

+

Go back to the item for a particular route object

+
+

replacePreviousAndPop(route: { + component: Function; + title: string; + passProps?: Object; + backButtonTitle?: string; + backButtonIcon?: Object; + leftButtonTitle?: string; + leftButtonIcon?: Object; + onLeftButtonPress?: Function; + rightButtonTitle?: string; + rightButtonIcon?: Object; + onRightButtonPress?: Function; + wrapperStyle?: any; +}) #

+

Replaces the previous route/view and transitions back to it.

+
+

resetTo(route: { + component: Function; + title: string; + passProps?: Object; + backButtonTitle?: string; + backButtonIcon?: Object; + leftButtonTitle?: string; + leftButtonIcon?: Object; + onLeftButtonPress?: Function; + rightButtonTitle?: string; + rightButtonIcon?: Object; + onRightButtonPress?: Function; + wrapperStyle?: any; +}) #

+

Replaces the top item and popToTop

+
+
+ +### 例子 + +```javascript +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const ViewExample = require('./ViewExample'); + +const createExamplePage = require('./createExamplePage'); +const nativeImageSource = require('nativeImageSource'); +const { + AlertIOS, + NavigatorIOS, + ScrollView, + StyleSheet, + Text, + TouchableHighlight, + View, +} = ReactNative; + +class EmptyPage extends React.Component { + render() { + return ( + + + {this.props.text} + + + ); + } +} + +class NavigatorIOSExamplePage extends React.Component { + render() { + var recurseTitle = 'Recurse Navigation'; + if (!this.props.depth || this.props.depth === 1) { + recurseTitle += ' - more examples here'; + } + return ( + + + + {this._renderRow(recurseTitle, () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: NavigatorIOSExamplePage, + backButtonTitle: 'Custom Back', + passProps: {depth: this.props.depth ? this.props.depth + 1 : 1}, + }); + })} + {this._renderRow('Push View Example', () => { + this.props.navigator.push({ + title: 'Very Long Custom View Example Title', + component: createExamplePage(null, ViewExample), + }); + })} + {this._renderRow('Custom title image Example', () => { + this.props.navigator.push({ + title: 'Custom title image Example', + titleImage: require('./relay.png'), + component: createExamplePage(null, ViewExample), + }); + })} + {this._renderRow('Custom Right Button', () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: EmptyPage, + rightButtonTitle: 'Cancel', + onRightButtonPress: () => this.props.navigator.pop(), + passProps: { + text: 'This page has a right button in the nav bar', + } + }); + })} + {this._renderRow('Custom Right System Button', () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: EmptyPage, + rightButtonSystemIcon: 'bookmarks', + onRightButtonPress: () => this.props.navigator.pop(), + passProps: { + text: 'This page has a right system button in the nav bar', + } + }); + })} + {this._renderRow('Custom Left & Right Icons', () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: EmptyPage, + leftButtonTitle: 'Custom Left', + onLeftButtonPress: () => this.props.navigator.pop(), + rightButtonIcon: nativeImageSource({ + ios: 'NavBarButtonPlus', + width: 17, + height: 17 + }), + onRightButtonPress: () => { + AlertIOS.alert( + 'Bar Button Action', + 'Recognized a tap on the bar button icon', + [ + { + text: 'OK', + onPress: () => console.log('Tapped OK'), + }, + ] + ); + }, + passProps: { + text: 'This page has an icon for the right button in the nav bar', + } + }); + })} + {this._renderRow('Custom Left & Right System Icons', () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: EmptyPage, + leftButtonSystemIcon: 'cancel', + onLeftButtonPress: () => this.props.navigator.pop(), + rightButtonSystemIcon: 'search', + onRightButtonPress: () => { + AlertIOS.alert( + 'Bar Button Action', + 'Recognized a tap on the bar button icon', + [ + { + text: 'OK', + onPress: () => console.log('Tapped OK'), + }, + ] + ); + }, + passProps: { + text: 'This page has an icon for the right button in the nav bar', + } + }); + })} + {this._renderRow('Pop', () => { + this.props.navigator.pop(); + })} + {this._renderRow('Pop to top', () => { + this.props.navigator.popToTop(); + })} + {this._renderReplace()} + {this._renderReplacePrevious()} + {this._renderReplacePreviousAndPop()} + {this._renderRow('Exit NavigatorIOS Example', this.props.onExampleExit)} + + + + ); + } + + _renderReplace = () => { + if (!this.props.depth) { + // this is to avoid replacing the top of the stack + return null; + } + return this._renderRow('Replace here', () => { + var prevRoute = this.props.route; + this.props.navigator.replace({ + title: 'New Navigation', + component: EmptyPage, + rightButtonTitle: 'Undo', + onRightButtonPress: () => this.props.navigator.replace(prevRoute), + passProps: { + text: 'The component is replaced, but there is currently no ' + + 'way to change the right button or title of the current route', + } + }); + }); + }; + + _renderReplacePrevious = () => { + if (!this.props.depth || this.props.depth < 2) { + // this is to avoid replacing the top of the stack + return null; + } + return this._renderRow('Replace previous', () => { + this.props.navigator.replacePrevious({ + title: 'Replaced', + component: EmptyPage, + passProps: { + text: 'This is a replaced "previous" page', + }, + wrapperStyle: styles.customWrapperStyle, + }); + }); + }; + + _renderReplacePreviousAndPop = () => { + if (!this.props.depth || this.props.depth < 2) { + // this is to avoid replacing the top of the stack + return null; + } + return this._renderRow('Replace previous and pop', () => { + this.props.navigator.replacePreviousAndPop({ + title: 'Replaced and Popped', + component: EmptyPage, + passProps: { + text: 'This is a replaced "previous" page', + }, + wrapperStyle: styles.customWrapperStyle, + }); + }); + }; + + _renderRow = (title: string, onPress: Function) => { + return ( + + + + + {title} + + + + + + ); + }; +} + +class NavigatorIOSExample extends React.Component { + static title = ''; + static description = 'iOS navigation capabilities'; + static external = true; + + render() { + const {onExampleExit} = this.props; + return ( + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + customWrapperStyle: { + backgroundColor: '#bbdddd', + }, + emptyPage: { + flex: 1, + paddingTop: 64, + }, + emptyPageText: { + margin: 10, + }, + list: { + backgroundColor: '#eeeeee', + marginTop: 10, + }, + group: { + backgroundColor: 'white', + }, + groupSpace: { + height: 15, + }, + line: { + backgroundColor: '#bbbbbb', + height: StyleSheet.hairlineWidth, + }, + row: { + backgroundColor: 'white', + justifyContent: 'center', + paddingHorizontal: 15, + paddingVertical: 15, + }, + separator: { + height: StyleSheet.hairlineWidth, + backgroundColor: '#bbbbbb', + marginLeft: 15, + }, + rowNote: { + fontSize: 17, + }, + rowText: { + fontSize: 17, + fontWeight: '500', + }, +}); + +module.exports = NavigatorIOSExample; +``` diff --git a/docs/docs/0.41/navigators.md b/docs/docs/0.41/navigators.md new file mode 100644 index 0000000..eb389f8 --- /dev/null +++ b/docs/docs/0.41/navigators.md @@ -0,0 +1,167 @@ +--- +id: navigators +title: Using Navigators +layout: docs +category: The Basics +permalink: docs/using-navigators.html +next: more-resources +--- + +Mobile apps rarely consist of just one screen. As soon as you add a second screen to your app, you will have to take into consideration how the user will navigate from one screen to the other. + +You can use navigators to transition between multiple screens. These transitions can be typical side-to-side animations down a master/detail stack, or vertical modal popups. + +## Navigator + +React Native has several built-in navigation components, but for your first app you will probably want to use `Navigator`. It provides a JavaScript implementation of a navigation stack, so it works on both iOS and Android and is easy to customize. + +![](img/NavigationStack-Navigator.gif) + +### Working with Scenes + +At this point you should feel comfortable rendering all sorts of components in your app, be it a simple `View` with `Text` inside, or a `ScrollView` with a list of `Image`s. Together, these components make up a scene (another word for screen) in your app. + +A scene is nothing other than a React component that is typically rendered full screen. This is in contrast to a `Text`, an `Image`, or even a custom `SpinningBeachball` component that is meant to be rendered as part of a screen. You may have already used one without realizing it - the ["HelloWorldApp"](docs/tutorial.html), the ["FlexDirectionBasics"](docs/flexbox.html), and the ["ListViewBasics"](docs/using-a-listview.html) components covered earlier in the tutorial are all examples of scenes. + +For simplicity's sake, let's define a simple scene that displays a bit of text. We will come back to this scene later as we add navigation to our app. Create a new file called "MyScene.js" with the following contents: + +```javascript +import React, { Component } from 'react'; +import { View, Text } from 'react-native'; + +export default class MyScene extends Component { + getDefaultProps() { + return { + title: 'MyScene' + }; + } + + render() { + return ( + + Hi! My name is {this.props.title}. + + ) + } +} +``` + +Notice the `export default` in front of the component declaration. This will _export_ the component, and in turn allow other components to _import_ it later on, like so: + +```javascript +import React, { Component } from 'react'; +import { View, Text } from 'react-native'; + +import MyScene from './MyScene'; + +class YoDawgApp extends Component { + render() { + return ( + + ) + } +} + +AppRegistry.registerComponent('YoDawgApp', () => YoDawgApp); +``` + +We now have a simple app that renders your scene and nothing else. In this case, `MyScene` is a simple example of a [reusable React component](https://facebook.github.io/react/docs/reusable-components.html). + +### Using Navigator + +Enough about scenes, let's start navigating. We will start by rendering a `Navigator`, and then let the `Navigator` render the scene for you by passing in your own render function to its `renderScene` prop. + +```javascript +render() { + return ( + { + + }} + /> + ); +} +``` + +Something you will encounter a lot when dealing with navigation is the concept of routes. A route is an object that contains information about a scene. It is used to provide all the context that the navigator's `renderScene` function needs to render a scene. It can have any number of keys to help distinguish your scene, and I happened to pick a single `title` key for the above example. + +#### Pushing scenes onto the stack + +In order to transition to a new scene, you will need to learn about `push` and `pop`. These two methods are provided by the `navigator` object that is passed to your `renderScene` function above. They can be used, as you may have realized, to push and pop routes into your navigation stack. + +```javascript +navigator.push({ + title: 'Next Scene', + index: 1, +}); + +navigator.pop(); +``` + +A more complete example that demonstrates the pushing and popping of routes could therefore look something like this: + +```javascript +import React, { Component, PropTypes } from 'react'; +import { Navigator, Text, TouchableHighlight, View } from 'react-native'; + +export default class SimpleNavigationApp extends Component { + render() { + return ( + + { + const nextIndex = route.index + 1; + navigator.push({ + title: 'Scene ' + nextIndex, + index: nextIndex, + }); + }} + + // Function to call to go back to the previous scene + onBack={() => { + if (route.index > 0) { + navigator.pop(); + } + }} + /> + } + /> + ) + } +} + +class MyScene extends Component { + static propTypes = { + title: PropTypes.string.isRequired, + onForward: PropTypes.func.isRequired, + onBack: PropTypes.func.isRequired, + } + render() { + return ( + + Current Scene: { this.props.title } + + Tap me to load the next scene + + + Tap me to go back + + + ) + } +} +``` + +In this example, the `MyScene` component is passed the title of the current route via the `title` prop. It displays two tappable components that call the `onForward` and `onBack` functions passed through its props, which in turn will call `navigator.push()` and `navigator.pop()` as needed. + +Check out the [Navigator API reference](docs/navigator.html) for more `Navigator` code samples, or read through the [Navigation guide](docs/navigation.html) for other examples of what you can do with navigators. + +## High Five! + +If you've gotten here by reading linearly through the tutorial, then you are a pretty impressive human being. Congratulations. Next, you might want to check out [all the cool stuff the community does with React Native](/react-native/docs/more-resources.html). diff --git a/docs/docs/0.41/netinfo.md b/docs/docs/0.41/netinfo.md new file mode 100644 index 0000000..e286111 --- /dev/null +++ b/docs/docs/0.41/netinfo.md @@ -0,0 +1,286 @@ +NetInfo模块可以获知设备联网或离线的状态信息。 + +```javascript +NetInfo.fetch().done((reach) => { + console.log('Initial: ' + reach); +}); +function handleFirstConnectivityChange(reach) { + console.log('First change: ' + reach); + NetInfo.removeEventListener( + 'change', + handleFirstConnectivityChange + ); +} +NetInfo.addEventListener( + 'change', + handleFirstConnectivityChange +); +``` + +### IOS +以异步的方式判断设备是否联网,以及是否使用了移动数据网络。 + +- `none` - 设备处于离线状态。 +- `wifi` - 设备处于联网状态且通过wifi链接,或者是一个iOS的模拟器。 +- `cell` - 设备是通过Edge、3G、WiMax或是LTE网络联网的。 +- `unknown` - 发生错误,网络状况不可知 + +### Android +请求网络信息需要先在应用的`AndroidManifest.xml`文件中添加如下权限字段: +``` + +``` +Android的联网类型: +- `NONE` - 设备处于离线状态 +- `BLUETOOTH` - 蓝牙数据连接 +- `DUMMY` - 模拟数据连接 +- `ETHERNET` - 以太网数据连接 +- `MOBILE` - 移动网络数据连接 +- `MOBILE_DUN` - 拨号移动网络数据连接 +- `MOBILE_HIPRI` - 高优先级移动网络数据连接 +- `MOBILE_MMS` - 彩信移动网络数据连接 +- `MOBILE_SUPL` - 安全用户面定位(SUPL)数据连接 +- `VPN` - 虚拟网络连接。需要Android5.0以上 +- `WIFI` - WIFI数据连接 +- `WIMAX` - WiMAX数据连接 +- `UNKNOWN` - 未知数据连接 + +其他的连接状态已被Android API隐藏,但可以在需要时使用。 + +### isConnectionExpensive +此方法仅Android可用。用于判断当前活动的连接是否计费。如果当前连接是通过移动数据网络,或者通过基于移动数据网络所创建的wifi热点,都有可能被判定为计费的数据连接。 + +```javascript +NetInfo.isConnectionExpensive((isConnectionExpensive) => { + console.log('Connection is ' + (isConnectionExpensive ? 'Expensive' : 'Not Expensive')); +}); +``` + +### isConnected +此方法所有平台皆可使用。以异步方式获取一个布尔值,用于判断当前设备是否联网。 + +```javascript +NetInfo.isConnected.fetch().done((isConnected) => { + console.log('First, is ' + (isConnected ? 'online' : 'offline')); +}); +function handleFirstConnectivityChange(isConnected) { + console.log('Then, is ' + (isConnected ? 'online' : 'offline')); + NetInfo.isConnected.removeEventListener( + 'change', + handleFirstConnectivityChange + ); +} +NetInfo.isConnected.addEventListener( + 'change', + handleFirstConnectivityChange +); +``` + + +### 方法 + +
+
+

static addEventListener(eventName, handler) #

+

在网络状况/类型发生变化时调用此监听函数。回调的参数为上面列出的网络状况/类型。

+
+
+
+

static removeEventListener(eventName, handler) #

+

移除网络状况/类型变化的监听函数。

+
+

static fetch() #

+

返回一个promise,用于获取当前的网络状况/类型。

+
+
+

static isConnectionExpensive() #

+
+
+ +### 属性 + +
+

isConnected: ObjectExpression + #

+

此属性为一个对象,也可调用上面列出的方法。但其监听函数接受的参数为一个布尔值,仅仅能表明当前网络是否联通。如果你只关心设备是否连上网了(不关心网络类型),那么使用此属性即可。

+
+
+ +### 例子 + +```javascript +'use strict'; + +const React = require('react'); +const ReactNative = require('react-native'); +const { + NetInfo, + Text, + View, + TouchableWithoutFeedback, +} = ReactNative; + +const ConnectionInfoSubscription = React.createClass({ + getInitialState() { + return { + connectionInfoHistory: [], + }; + }, + componentDidMount: function() { + NetInfo.addEventListener( + 'change', + this._handleConnectionInfoChange + ); + }, + componentWillUnmount: function() { + NetInfo.removeEventListener( + 'change', + this._handleConnectionInfoChange + ); + }, + _handleConnectionInfoChange: function(connectionInfo) { + const connectionInfoHistory = this.state.connectionInfoHistory.slice(); + connectionInfoHistory.push(connectionInfo); + this.setState({ + connectionInfoHistory, + }); + }, + render() { + return ( + + {JSON.stringify(this.state.connectionInfoHistory)} + + ); + } +}); + +const ConnectionInfoCurrent = React.createClass({ + getInitialState() { + return { + connectionInfo: null, + }; + }, + componentDidMount: function() { + NetInfo.addEventListener( + 'change', + this._handleConnectionInfoChange + ); + NetInfo.fetch().done( + (connectionInfo) => { this.setState({connectionInfo}); } + ); + }, + componentWillUnmount: function() { + NetInfo.removeEventListener( + 'change', + this._handleConnectionInfoChange + ); + }, + _handleConnectionInfoChange: function(connectionInfo) { + this.setState({ + connectionInfo, + }); + }, + render() { + return ( + + {this.state.connectionInfo} + + ); + } +}); + +const IsConnected = React.createClass({ + getInitialState() { + return { + isConnected: null, + }; + }, + componentDidMount: function() { + NetInfo.isConnected.addEventListener( + 'change', + this._handleConnectivityChange + ); + NetInfo.isConnected.fetch().done( + (isConnected) => { this.setState({isConnected}); } + ); + }, + componentWillUnmount: function() { + NetInfo.isConnected.removeEventListener( + 'change', + this._handleConnectivityChange + ); + }, + _handleConnectivityChange: function(isConnected) { + this.setState({ + isConnected, + }); + }, + render() { + return ( + + {this.state.isConnected ? 'Online' : 'Offline'} + + ); + } +}); + +const IsConnectionExpensive = React.createClass({ + getInitialState() { + return { + isConnectionExpensive: (null : ?boolean), + }; + }, + _checkIfExpensive() { + NetInfo.isConnectionExpensive().then( + isConnectionExpensive => { this.setState({isConnectionExpensive}); } + ); + }, + render() { + return ( + + + + Click to see if connection is expensive: + {this.state.isConnectionExpensive === true ? 'Expensive' : + this.state.isConnectionExpensive === false ? 'Not expensive' + : 'Unknown'} + + + + + ); + } +}); + +exports.title = 'NetInfo'; +exports.description = 'Monitor network status'; +exports.examples = [ + { + title: 'NetInfo.isConnected', + description: 'Asynchronously load and observe connectivity', + render(): ReactElement { return ; } + }, + { + title: 'NetInfo.update', + description: 'Asynchronously load and observe connectionInfo', + render(): ReactElement { return ; } + }, + { + title: 'NetInfo.updateHistory', + description: 'Observed updates to connectionInfo', + render(): ReactElement { return ; } + }, + { + platform: 'android', + title: 'NetInfo.isConnectionExpensive (Android)', + description: 'Asynchronously check isConnectionExpensive', + render(): ReactElement { return ; } + }, +]; +``` diff --git a/docs/docs/0.41/network.md b/docs/docs/0.41/network.md new file mode 100644 index 0000000..5b7e159 --- /dev/null +++ b/docs/docs/0.41/network.md @@ -0,0 +1,138 @@ +很多移动应用都需要从远程地址中获取数据或资源。你可能需要给某个REST API发起POST请求以提交用户数据,又或者可能仅仅需要从某个服务器上获取一些静态内容——以下就是你会用到的东西。新手可以对照这个[简短的视频教程](http://v.youku.com/v_show/id_XMTUyNTEwMTA5Ng==.html)加深理解。 + +## 使用Fetch + +React Native提供了和web标准一致的[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API),用于满足开发者访问网络的需求。如果你之前使用过`XMLHttpRequest`(即俗称的ajax)或是其他的网络API,那么Fetch用起来将会相当容易上手。这篇文档只会列出Fetch的基本用法,并不会讲述太多细节,你可以使用你喜欢的搜索引擎去搜索`fetch api`关键字以了解更多信息。 + +#### 发起网络请求 + +要从任意地址获取内容的话,只需简单地将网址作为参数传递给fetch方法即可(fetch这个词本身也就是`获取`的意思): + +```js +fetch('https://mywebsite.com/mydata.json') +``` + +Fetch还有可选的第二个参数,可以用来定制HTTP请求一些参数。你可以指定header参数,或是指定使用POST方法,又或是提交数据等等: + +```js +fetch('https://mywebsite.com/endpoint/', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + firstParam: 'yourValue', + secondParam: 'yourOtherValue', + }) +}) +``` + +译注:如果你的服务器无法识别上面POST的数据格式,那么可以尝试传统的form格式,示例如下: + +```js +fetch('https://mywebsite.com/endpoint/', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: 'key1=value1&key2=value2' +}) +``` + +可以参考[Fetch请求文档](https://developer.mozilla.org/en-US/docs/Web/API/Request)来查看所有可用的参数。 + +#### 处理服务器的响应数据 + +上面的例子演示了如何发起请求。很多情况下,你还需要处理服务器回复的数据。 + +网络请求天然是一种异步操作(译注:同样的还有[asyncstorage](asyncstorage.html),请不要再问怎样把异步变成同步!无论在语法层面怎么折腾,它们的异步本质是无法变更的。异步的意思是你应该趁这个时间去做点别的事情,比如显示loading,而不是让界面卡住傻等)。Fetch 方法会返回一个[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise),这种模式可以简化异步风格的代码(译注:同样的,如果你不了解promise,建议使用搜索引擎补课): + + ```js + getMoviesFromApiAsync() { + return fetch('http://facebook.github.io/react-native/movies.json') + .then((response) => response.json()) + .then((responseJson) => { + return responseJson.movies; + }) + .catch((error) => { + console.error(error); + }); + } + ``` + +你也可以在React Native应用中使用ES7标准中的`async`/`await` 语法: + + ```js + // 注意这个方法前面有async关键字 + async getMoviesFromApi() { + try { + // 注意这里的await语句,其所在的函数必须有async关键字声明 + let response = await fetch('http://facebook.github.io/react-native/movies.json'); + let responseJson = await response.json(); + return responseJson.movies; + } catch(error) { + console.error(error); + } + } + ``` + +别忘了catch住`fetch`可能抛出的异常,否则出错时你可能看不到任何提示。 + +> 默认情况下,iOS会阻止所有非HTTPS的请求。如果你请求的接口是http协议,那么首先需要添加一个App Transport Securty的例外,详细可参考[这篇帖子](https://segmentfault.com/a/1190000002933776)。 + + +### 使用其他的网络库 + +React Native中已经内置了[XMLHttpRequest API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)(也就是俗称的ajax)。一些基于XMLHttpRequest封装的第三方库也可以使用,例如[frisbee](https://github.com/niftylettuce/frisbee)或是[axios](https://github.com/mzabriskie/axios)等。但注意不能使用jQuery,因为jQuery中还使用了很多浏览器中才有而RN中没有的东西(所以也不是所有web中的ajax库都可以直接使用)。 + +```js +var request = new XMLHttpRequest(); +request.onreadystatechange = (e) => { + if (request.readyState !== 4) { + return; + } + + if (request.status === 200) { + console.log('success', request.responseText); + } else { + console.warn('error'); + } +}; + +request.open('GET', 'https://mywebsite.com/endpoint/'); +request.send(); +``` + +> 需要注意的是,安全机制与网页环境有所不同:在应用中你可以访问任何网站,没有[跨域](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing)的限制。 + +## WebSocket支持 + +React Native还支持[WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket),这种协议可以在单个TCP连接上提供全双工的通信信道。 + +```js +var ws = new WebSocket('ws://host.com/path'); + +ws.onopen = () => { + // 打开一个连接 + + ws.send('something'); // 发送一个消息 +}; + +ws.onmessage = (e) => { + // 接收到了一个消息 + console.log(e.data); +}; + +ws.onerror = (e) => { + // 发生了一个错误 + console.log(e.message); +}; + +ws.onclose = (e) => { + // 连接被关闭了 + console.log(e.code, e.reason); +}; +``` + +现在你的应用已经可以从各种渠道获取数据了,那么接下来面临的问题多半就是如何在不同的页面间组织和串联内容了。要管理页面的跳转,你需要学习使用[导航器](using-navigators.html)。 diff --git a/docs/docs/0.41/panresponder.md b/docs/docs/0.41/panresponder.md new file mode 100644 index 0000000..f4afb4d --- /dev/null +++ b/docs/docs/0.41/panresponder.md @@ -0,0 +1,236 @@ +`PanResponder`类可以将多点触摸操作协调成一个手势。它使得一个单点触摸可以接受更多的触摸操作,也可以用于识别简单的多点触摸手势。 + +它提供了一个对[触摸响应系统](gesture-responder-system.html)响应器的可预测的包装。对于每一个处理函数,它在原生事件之外提供了一个新的`gestureState`对象。 + +```javascript +onPanResponderMove: (event, gestureState) => {} +``` + +原生事件是指由以下字段组成的合成触摸事件: + +- `nativeEvent` + + `changedTouches` - 在上一次事件之后,所有发生变化的触摸事件的数组集合(即上一次事件后,所有移动过的触摸点) + + `identifier` - 触摸点的ID + + `locationX` - 触摸点相对于父元素的横坐标 + + `locationY` - 触摸点相对于父元素的纵坐标 + + `pageX` - 触摸点相对于根元素的横坐标 + + `pageY` - 触摸点相对于根元素的纵坐标 + + `target` - 触摸点所在的元素ID + + `timestamp` - 触摸事件的时间戳,可用于移动速度的计算 + + `touches` - 当前屏幕上的所有触摸点的集合 + +一个`gestureState`对象有如下的字段: + +* `stateID` - 触摸状态的ID。在屏幕上有至少一个触摸点的情况下,这个ID会一直有效。 +* `moveX` - 最近一次移动时的屏幕横坐标 +* `moveY` - 最近一次移动时的屏幕纵坐标 +* `x0` - 当响应器产生时的屏幕坐标 +* `y0` - 当响应器产生时的屏幕坐标 +* `dx` - 从触摸操作开始时的累计横向路程 +* `dy` - 从触摸操作开始时的累计纵向路程 +* `vx` - 当前的横向移动速度 +* `vy` - 当前的纵向移动速度 +* `numberActiveTouches` - 当前在屏幕上的有效触摸点的数量 + +### 基本用法 + +```javascript + componentWillMount: function() { + this._panResponder = PanResponder.create({ + // 要求成为响应者: + onStartShouldSetPanResponder: (evt, gestureState) => true, + onStartShouldSetPanResponderCapture: (evt, gestureState) => true, + onMoveShouldSetPanResponder: (evt, gestureState) => true, + onMoveShouldSetPanResponderCapture: (evt, gestureState) => true, + + onPanResponderGrant: (evt, gestureState) => { + // 开始手势操作。给用户一些视觉反馈,让他们知道发生了什么事情! + + // gestureState.{x,y}0 现在会被设置为0 + }, + onPanResponderMove: (evt, gestureState) => { + // 最近一次的移动距离为gestureState.move{X,Y} + + // 从成为响应者开始时的累计手势移动距离为gestureState.d{x,y} + }, + onPanResponderTerminationRequest: (evt, gestureState) => true, + onPanResponderRelease: (evt, gestureState) => { + // 用户放开了所有的触摸点,且此时视图已经成为了响应者。 + // 一般来说这意味着一个手势操作已经成功完成。 + }, + onPanResponderTerminate: (evt, gestureState) => { + // 另一个组件已经成为了新的响应者,所以当前手势将被取消。 + }, + onShouldBlockNativeResponder: (evt, gestureState) => { + // 返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者 + // 默认返回true。目前暂时只支持android。 + return true; + }, + }); + }, + + render: function() { + return ( + + ); + }, +``` + +### 可运行的例子 + +要想看看可以直接使用的例子,请参阅[UIExplorer中的PanResponder](https://github.com/facebook/react-native/blob/master/Examples/UIExplorer/js/PanResponderExample.js) + +### 方法 + +
+
+

static create(config: object) #

+
+

@param {object} 配置所有响应器回调的加强版本,不仅仅包括原本的ResponderSyntheticEvent,还包括PanResponder手势状态的回调。你只要简单的把onResponder*回调中的Responder替换为PanResponder。举例来说,这个config对象可能看起来像这样:

+
    +
  • onMoveShouldSetPanResponder: (e, gestureState) => {...}
  • +
  • onMoveShouldSetPanResponderCapture: (e, gestureState) => {...}
  • +
  • onStartShouldSetPanResponder: (e, gestureState) => {...}
  • +
  • onStartShouldSetPanResponderCapture: (e, gestureState) => {...}
  • +
  • onPanResponderReject: (e, gestureState) => {...}
  • +
  • onPanResponderGrant: (e, gestureState) => {...}
  • +
  • onPanResponderStart: (e, gestureState) => {...}
  • +
  • onPanResponderEnd: (e, gestureState) => {...}
  • +
  • onPanResponderRelease: (e, gestureState) => {...}
  • +
  • onPanResponderMove: (e, gestureState) => {...}
  • +
  • onPanResponderTerminate: (e, gestureState) => {...}
  • +
  • onPanResponderTerminationRequest: (e, gestureState) => {...}
  • +
  • +

    onShouldBlockNativeResponder: (e, gestureState) => {...}

    +

    通常来说,对那些有对应捕获事件的事件来说,我们在捕获阶段更新gestureState一次,然后在冒泡阶段直接使用即可。

    +

    注意onStartShould* 回调。他们只会在此节点冒泡/捕获的开始/结束事件中提供已经更新过的gestureState。一旦这个节点成为了事件的响应者,则所有的开始/结束事件都会被手势正确处理,并且gestureState也会被正确更新。(numberActiveTouches)有可能没有包含所有的触摸点,除非你就是触摸事件的响应者。

    +
  • +
+
+
+
+ +### 例子 + +```javascript +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + PanResponder, + StyleSheet, + View, + processColor, +} = ReactNative; + +var CIRCLE_SIZE = 80; + +var PanResponderExample = React.createClass({ + + statics: { + title: 'PanResponder Sample', + description: 'Shows the use of PanResponder to provide basic gesture handling.', + }, + + _panResponder: {}, + _previousLeft: 0, + _previousTop: 0, + _circleStyles: {}, + circle: (null : ?{ setNativeProps(props: Object): void }), + + componentWillMount: function() { + this._panResponder = PanResponder.create({ + onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder, + onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder, + onPanResponderGrant: this._handlePanResponderGrant, + onPanResponderMove: this._handlePanResponderMove, + onPanResponderRelease: this._handlePanResponderEnd, + onPanResponderTerminate: this._handlePanResponderEnd, + }); + this._previousLeft = 20; + this._previousTop = 84; + this._circleStyles = { + style: { + left: this._previousLeft, + top: this._previousTop, + backgroundColor: 'green', + } + }; + }, + + componentDidMount: function() { + this._updateNativeStyles(); + }, + + render: function() { + return ( + + { + this.circle = circle; + }} + style={styles.circle} + {...this._panResponder.panHandlers} + /> + + ); + }, + + _highlight: function() { + this._circleStyles.style.backgroundColor = 'blue'; + this._updateNativeStyles(); + }, + + _unHighlight: function() { + this._circleStyles.style.backgroundColor = 'green'; + this._updateNativeStyles(); + }, + + _updateNativeStyles: function() { + this.circle && this.circle.setNativeProps(this._circleStyles); + }, + + _handleStartShouldSetPanResponder: function(e: Object, gestureState: Object): boolean { + // Should we become active when the user presses down on the circle? + return true; + }, + + _handleMoveShouldSetPanResponder: function(e: Object, gestureState: Object): boolean { + // Should we become active when the user moves a touch over the circle? + return true; + }, + + _handlePanResponderGrant: function(e: Object, gestureState: Object) { + this._highlight(); + }, + _handlePanResponderMove: function(e: Object, gestureState: Object) { + this._circleStyles.style.left = this._previousLeft + gestureState.dx; + this._circleStyles.style.top = this._previousTop + gestureState.dy; + this._updateNativeStyles(); + }, + _handlePanResponderEnd: function(e: Object, gestureState: Object) { + this._unHighlight(); + this._previousLeft += gestureState.dx; + this._previousTop += gestureState.dy; + }, +}); + +var styles = StyleSheet.create({ + circle: { + width: CIRCLE_SIZE, + height: CIRCLE_SIZE, + borderRadius: CIRCLE_SIZE / 2, + position: 'absolute', + left: 0, + top: 0, + }, + container: { + flex: 1, + paddingTop: 64, + }, +}); + +module.exports = PanResponderExample; +``` \ No newline at end of file diff --git a/docs/docs/0.41/performance.md b/docs/docs/0.41/performance.md new file mode 100644 index 0000000..cdb88cc --- /dev/null +++ b/docs/docs/0.41/performance.md @@ -0,0 +1,173 @@ +使用React Native替代基于WebView的框架来开发App的一个强有力的理由,就是为了使App可以达到每秒60帧(足够流畅),并且能有类似原生App的外观和手感。因此我们也尽可能地优化React Native去实现这一目标,使开发者能集中精力处理App的业务逻辑,而不用费心考虑性能。但是,总还是有一些地方有所欠缺,以及在某些场合React Native还不能够替你决定如何进行优化(用原生代码写也无法避免),因此人工的干预依然是必要的。 +本文的目的是教给你一些基本的知识,来帮你排查性能方面的问题,以及探讨这些问题产生的原因和推荐的解决方法。 + +## 关于“帧”你所需要知道的 + +老一辈人常常把电影称为“移动的画”,是因为视频中逼真的动态效果其实是一种幻觉,这种幻觉是由一组静态的图片以一个稳定的速度快速变化所产生的。我们把这组图片中的每一张图片叫做一帧,而每秒钟显示的帧数直接的影响了视频(或者说用户界面)的流畅度和真实感。iOS设备提供了每秒60的帧率,这就留给了开发者和UI系统大约16.67ms来完成生成一张静态图片(帧)所需要的所有工作。如果在这分派的16.67ms之内没有能够完成这些工作,就会引发‘丢帧’的后果,使界面表现的不够流畅。 + +下面要讲的事情可能更为复杂:请先调出你应用的开发菜单,打开`Show FPS Monitor`. 你会注意到有两个不同的帧率. + +### JavaScript 帧率 + +对大多数React Native应用来说,业务逻辑是运行在JavaScript线程上的。这是React应用所在的线程,也是发生API调用,以及处理触摸事件等操作的线程。更新数据到原生支持的视图是批量进行的,并且在事件循环每进行一次的时候被发送到原生端,这一步通常会在一帧时间结束之前处理完(如果一切顺利的话)。如果JavaScript线程有一帧没有及时响应,就被认为发生了一次丢帧。 例如,你在一个复杂应用的根组件上调用了`this.setState`,从而导致一次开销很大的子组件树的重绘,可想而知,这可能会花费200ms也就是整整12帧的丢失。此时,任何由JavaScript控制的动画都会卡住。只要卡顿超过100ms,用户就会明显的感觉到。 + +这种情况经常发生在Navigator的切换过程中:当你push一个新的路由时,JavaScript需要绘制新场景所需的所有组件,以发送正确的命令给原生端去创建视图。由于切换是由JavaScript线程所控制,因此经常会占用若干帧的时间,引起一些卡顿。有的时候,组件会在`componentDidMount`函数中做一些额外的事情,这甚至可能会导致页面切换过程中多达一秒的卡顿。 + +另一个例子是触摸事件的响应:如果你正在JavaScript线程处理一个跨越多个帧的工作,你可能会注意到TouchableOpacity的响应被延迟了。这是因为JavaScript线程太忙了,不能够处理主线程发送过来的原始触摸事件。结果TouchableOpacity就不能及时响应这些事件并命令主线程的页面去调整透明度了。 + +### 主线程 (也即UI线程) 帧率 + +很多人会注意到,`NavigatorIOS`的性能要比Navigator好的多。原因就是它的切换动画是完全在主线程上执行的,因此不会被JavaScript线程上的掉帧所影响。([阅读关于为何你仍然需要使用Navigator](using-navigators.html)) + +同样,当JavaScript线程卡住的时候,你仍然可以欢快的上下滚动ScrollView,因为ScrollView运行在主线程之上(尽管滚动事件会被分发到JS线程,但是接收这些事件对于滚动这个动作来说并不必要)。 + +## 性能问题的常见原因 + +### console.log语句 + +在运行打好了离线包的应用时,控制台打印语句可能会极大地拖累JavaScript线程。注意有些第三方调试库也可能包含控制台打印语句,比如[redux-logger](https://github.com/evgenyrodionov/redux-logger),所以在发布应用前请务必仔细检查,确保全部移除。 + + +> 这里有个小技巧可以在发布时屏蔽掉所有的`console.*`调用。React Native中有一个全局变量`__DEV__`用于指示当前运行环境是否是开发环境。我们可以据此在正式环境中替换掉系统原先的console实现。 + +```js +if (!__DEV__) { + global.console = { + info: () => {}, + log: () => {}, + warn: () => {}, + error: () => {}, + }; +} +``` + +这样在打包发布时,所有的控制台语句就会被自动替换为空函数,而在调试时它们仍然会被正常调用。 + +### 开发模式 (dev=true) + +JavaScript线程的性能在开发模式下是很糟糕的。这是不可避免的,因为有许多工作需要在运行的时候去做,譬如使你获得良好的警告和错误信息,又比如验证属性类型(propTypes)以及产生各种其他的警告。 + +### 缓慢的导航器(Navigator)切换 + +如之前说,`Navigator`的动画是由JavaScript线程所控制的。想象一下“从右边推入”这个场景的切换:每一帧中,新的场景从右向左移动,从屏幕右边缘开始(不妨认为是320单位宽的的x轴偏移),最终移动到x轴偏移为0的屏幕位置。切换过程中的每一帧,JavaScript线程都需要发送一个新的x轴偏移量给主线程。如果JavaScript线程卡住了,它就无法处理这项事情,因而这一帧就无法更新,动画就被卡住了。 + +长远的解决方法,其中一部分是要允许基于JavaScript的动画从主线程分离。同样是上面的例子,我们可以在切换动画开始的时候计算出一个列表,其中包含所有的新的场景需要的x轴偏移量,然后一次发送到主线程以某种优化的方式执行。由于JavaScript线程已经从更新x轴偏移量给主线程这个职责中解脱了出来,因此JavaScript线程中的掉帧就不是什么大问题了 —— 用户将基本上不会意识到这个问题,因为用户的注意力会被流畅的切换动作所吸引。 + +不幸的是,这个方案还没有被实现。所以当前的解决方案是,在动画的进行过程中,利用InteractionManager来选择性的渲染新场景所需的最小限度的内容。 + +`InteractionManager.runAfterInteractions`的参数中包含一个回调,这个回调会在navigator切换动画结束的时候被触发(每个来自于`Animated`接口的动画都会通知InteractionManager,不过这个就超出了本文的讨论)。 + +你的场景组件看上去应该是这样的: + +```javascript +class ExpensiveScene extends React.Component { + constructor(props, context) { + super(props, context); + this.state = {renderPlaceholderOnly: true}; + } + + componentDidMount() { + InteractionManager.runAfterInteractions(() => { + this.setState({renderPlaceholderOnly: false}); + }); + } + + render() { + if (this.state.renderPlaceholderOnly) { + return this._renderPlaceholderView(); + } + + return ( + + Your full view goes here + + ); + } + + + _renderPlaceholderView() { + return ( + + Loading... + + ); + } +}; +``` + +你不必被限制在仅仅是做一些loading指示的渲染,你也可以绘制部分的页面内容 —— 例如,当你加载Facebook应用的时候,你会看见一个灰色方形的消息流的占位符,是将来用来显示文字的地方。如果你正在场景中绘制地图,那么最好在场景切换完成之前,显示一个灰色的占位页面或者是一个转动的动画,因为切换过程的确会导致主线程的掉帧。 + +### ListView初始化渲染太慢以及列表过长时滚动性能太差 +这是一个频繁出现的问题。因为iOS配备了UITableView,通过重用底层的UIViews实现了非常高性能的体验(相比之下ListView的性能没有那么好)。用React Native实现相同效果的工作仍正在进行中,但是在此之前,我们有一些可用的方法来稍加改进性能以满足我们的需求。 + +#### initialListSize + +这个属性定义了在首次渲染中绘制的行数。如果我们关注于快速的显示出页面,可以设置`initialListSize`为1,然后我们会发现其他行在接下来的帧中被快速绘制到屏幕上。而每帧所显示的行数由`pageSize`所决定。 + +#### pageSize + +在初始渲染也就是`initialListSize`被使用之后,ListView将利用`pageSize`来决定每一帧所渲染的行数。默认值为1 —— 但是如果你的页面很小,而且渲染的开销不大的话,你会希望这个值更大一些。稍加调整,你会发现它所起到的作用。 + +#### scrollRenderAheadDistance + +“在将要进入屏幕区域之前的某个位置,开始绘制一行,距离按像素计算。” + +如果我们有一个2000个元素的列表,并且立刻全部渲染出来的话,无论是内存还是计算资源都会显得很匮乏。还很可能导致非常可怕的阻塞。因此`scrollRenderAheadDistance`允许我们来指定一个超过视野范围之外所需要渲染的行数。 + +#### removeClippedSubviews + +“当这一选项设置为true的时候,超出屏幕的子视图(同时`overflow`值为`hidden`)会从它们原生的父视图中移除。这个属性可以在列表很长的时候提高滚动的性能。默认为false。(0.14版本后默认为true)” + +这是一个应用在长列表上极其重要的优化。Android上,`overflow`值总是`hidden`的,所以你不必担心没有设置它。而在iOS上,你需要确保在行容器上设置了`overflow: hidden`。 + +### 我的组件渲染太慢,我不需要立即显示全部 + +这在初次浏览ListView时很常见,适当的使用它是获得稳定性能的关键。就像之前所提到的,它可以提供一些手段在不同帧中来分开渲染页面,稍加改进就可以满足你的需求。此外要记住的是,ListView也可以横向滚动。 + +### 在重绘一个几乎没有什么变化的页面时,JS帧率严重降低 + +如果你正在使用一个ListView,你必须提供一个`rowHasChanged`函数,它通过快速的算出某一行是否需要重绘,来减少很多不必要的工作。如果你使用了不可变的数据结构,这项工作就只需检查其引用是否相等。 + +同样的,你可以实现`shouldComponentUpdate`函数来指明在什么样的确切条件下,你希望这个组件得到重绘。如果你编写的是纯粹的组件(返回值完全由props和state所决定),你可以利用`PureRenderMixin`来为你做这个工作。再强调一次,不可变的数据结构在提速方面非常有用 —— 当你不得不对一个长列表对象做一个深度的比较,它会使重绘你的整个组件更加快速,而且代码量更少。 + +### 由于在JavaScript线程中同时做很多事情,导致JS线程掉帧 + +“导航切换极慢”是该问题的常见表现。在其他情形下,这种问题也可能会出现。使用`InteractionManager`是一个好的方法,但是如果在动画中,为了用户体验的开销而延迟其他工作并不太能接受,那么你可以考虑一下使用`LayoutAnimation`。 + +`Animated`的接口一般会在JavaScript线程中计算出所需要的每一个关键帧,而`LayoutAnimation`则利用了`Core Animation`,使动画不会被JS线程和主线程的掉帧所影响。 + +举一个需要使用这项功能的例子:比如需要给一个模态框做动画(从下往上划动,并在半透明遮罩中淡入),而这个模态框正在初始化,并且可能响应着几个网络请求,渲染着页面的内容,并且还在更新着打开这个模态框的父页面。了解更多有关如何使用LayoutAnimation的信息,请查看[动画指南](/docs/animations.html)。 + +注意: + + - `LayoutAnimation`只工作在“一次性”的动画上("静态"动画) -- 如果动画可能会被中途取消,你还是需要使用`Animated`。 + +### 在屏幕上移动视图(滚动,切换,旋转)时,UI线程掉帧 + +当具有透明背景的文本位于一张图片上时,或者在每帧重绘视图时需要用到透明合成的任何其他情况下,这种现象尤为明显。设置`shouldRasterizeIOS`或者`renderToHardwareTextureAndroid`属性可以显著改善这一现象。 +注意不要过度使用该特性,否则你的内存使用量将会飞涨。在使用时,要评估你的性能和内存使用情况。如果你没有需要移动这个视图的需求,请关闭这一属性。 + +### 使用动画改变图片的尺寸时,UI线程掉帧 + +在iOS上,每次调整Image组件的宽度或者高度,都需要重新裁剪和缩放原始图片。这个操作开销会非常大,尤其是大的图片。比起直接修改尺寸,更好的方案是使用`transform: [{scale}]`的样式属性来改变尺寸。比如当你点击一个图片,要将它放大到全屏的时候,就可以使用这个属性。 + +### Touchable系列组件不能很好的响应 + +有些时候,如果我们有一项操作与点击事件所带来的透明度改变或者高亮效果发生在同一帧中,那么有可能在`onPress`函数结束之前我们都看不到这些效果。比如在`onPress`执行了一个`setState`的操作,这个操作需要大量计算工作并且导致了掉帧。对此的一个解决方案是将`onPress`处理函数中的操作封装到`requestAnimationFrame`中: + + +```javascript +handleOnPress() { + // 谨记在使用requestAnimationFrame、setTimeout以及setInterval时 + // 要使用TimerMixin(其作用是在组件unmount时,清除所有定时器) + this.requestAnimationFrame(() => { + this.doExpensiveAction(); + }); +} +``` + +## 分析 + +你可以利用内置的分析器来同时获取JavaScript线程和主线程中代码执行情况的详细信息。 + +对于iOS来说,Instruments是一个宝贵的工具库,Android的话,你可以使用systrace,参见[调试Android UI性能](/docs/android-ui-performance.html#content)。 diff --git a/docs/docs/0.41/permissionsandroid.md b/docs/docs/0.41/permissionsandroid.md new file mode 100644 index 0000000..b002c58 --- /dev/null +++ b/docs/docs/0.41/permissionsandroid.md @@ -0,0 +1,61 @@ +`PermissionsAndroid`可以访问Android M(也就是6.0)开始提供的权限模型。有一些权限写在`AndroidManifest.xml`就可以在安装时自动获得。但有一些“危险”的权限则需要弹出提示框供用户选择。本API即用于后一种情形。 + +在低于Android 6.0的设备上,权限只要写在`AndroidManifest.xml`里就会自动获得,此情形下`check`和`request` 方法将始终返回true。 + +如果用户之前拒绝过你的某项权限请求,那么系统会建议你显示一个为什么需要这个权限的“详细解释”(rationale参数)。如果用户之前拒绝过,那么当你再次申请的时候,弹出的就可能不是原先的申请信息,而是`rationale`参数里提供的进一步解释。 + +### 例子 + +```js +async function requestCameraPermission() { + try { + const granted = await AndroidPermissions.request( + AndroidPermissions.PERMISSIONS.CAMERA, + { + 'title': '申请摄像头权限', + 'message': '一个很牛逼的应用想借用你的摄像头,' + + '然后你就可以拍出酷炫的皂片啦。' + } + ) + if (granted === PermissionsAndroid.RESULTS.GRANTED) { + console.log("现在你获得摄像头权限了") + } else { + console.log("用户并不屌你") + } + } catch (err) { + console.warn(err) + } +} +``` +### 方法 + +
+
+

+ constructor + () + # +

+
+
+

check(permission) + # +

+

返回一个promise,最终值为用户是否授权过的布尔值。

+
+
+

request(permission, rationale?) + # +

+

弹出提示框向用户请求某项权限。返回一个promise,最终值为用户是否同意了权限申请的布尔值。

+

其中rationale参数是可选的,其结构为包含titlemessage)的对象。此方法会和系统协商,是弹出系统内置的权限申请对话框,还是显示rationale中的信息以向用户进行解释。具体原理请参阅android官方文档 + (https://developer.android.com/training/permissions/requesting.html#explain)。

+
+
+

requestMultiple(permissions) + #

+

在一个弹出框中向用户请求多个权限。返回值为一个object,key为各权限名称,对应值为用户授权与否。

+
+
\ No newline at end of file diff --git a/docs/docs/0.41/picker.md b/docs/docs/0.41/picker.md new file mode 100644 index 0000000..d8d6f07 --- /dev/null +++ b/docs/docs/0.41/picker.md @@ -0,0 +1,56 @@ +本组件可以在iOS和Android上渲染原生的选择器(Picker)。用例: +```js + this.setState({language: lang})}> + + + +``` + +### 属性 + +
+ +

onValueChange function #

+

某一项被选中时执行此回调。调用时带有如下参数: +

    +
  • itemValue: 被选中项的value属性
  • +
  • itemPosition: 被选中项在picker中的索引位置
  • +
+

+
+

selectedValue any #

+

默认选中的值。可以是字符串或整数。

+
+

style pickerStyleType + #

+

testID string #

+

用于在端对端测试中定位此视图。

+
+

androidenabled + bool #

+

如果设为false,则会禁用此选择器。

+
+

androidmode + enum('dialog', 'dropdown') #

+

在Android上,可以指定在用户点击选择器时,以怎样的形式呈现选项:

+
    +
  • dialog(对话框形式): 显示一个模态对话框。默认选项。
  • +
  • dropdown(下拉框形式): 以选择器所在位置为锚点展开一个下拉框。
  • +
+
+
+

androidprompt + string #

+

设置选择器的提示字符串。在Android的对话框模式中用作对话框的标题。

+
+

iositemStyle + itemStylePropType #

+

指定应用在每项标签上的样式。

+
+
diff --git a/docs/docs/0.41/pickerios.md b/docs/docs/0.41/pickerios.md new file mode 100644 index 0000000..8af8904 --- /dev/null +++ b/docs/docs/0.41/pickerios.md @@ -0,0 +1,154 @@ +### 截图 +![](img/components/pickerios.png) + +### 属性 + +
+

itemStyle itemStylePropType #

+

onValueChange function #

selectedValue any #

+ +### 例子 + +```javascript +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + PickerIOS, + Text, + View, +} = ReactNative; + +var PickerItemIOS = PickerIOS.Item; + +var CAR_MAKES_AND_MODELS = { + amc: { + name: 'AMC', + models: ['AMX', 'Concord', 'Eagle', 'Gremlin', 'Matador', 'Pacer'], + }, + alfa: { + name: 'Alfa-Romeo', + models: ['159', '4C', 'Alfasud', 'Brera', 'GTV6', 'Giulia', 'MiTo', 'Spider'], + }, + aston: { + name: 'Aston Martin', + models: ['DB5', 'DB9', 'DBS', 'Rapide', 'Vanquish', 'Vantage'], + }, + audi: { + name: 'Audi', + models: ['90', '4000', '5000', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'Q5', 'Q7'], + }, + austin: { + name: 'Austin', + models: ['America', 'Maestro', 'Maxi', 'Mini', 'Montego', 'Princess'], + }, + borgward: { + name: 'Borgward', + models: ['Hansa', 'Isabella', 'P100'], + }, + buick: { + name: 'Buick', + models: ['Electra', 'LaCrosse', 'LeSabre', 'Park Avenue', 'Regal', + 'Roadmaster', 'Skylark'], + }, + cadillac: { + name: 'Cadillac', + models: ['Catera', 'Cimarron', 'Eldorado', 'Fleetwood', 'Sedan de Ville'], + }, + chevrolet: { + name: 'Chevrolet', + models: ['Astro', 'Aveo', 'Bel Air', 'Captiva', 'Cavalier', 'Chevelle', + 'Corvair', 'Corvette', 'Cruze', 'Nova', 'SS', 'Vega', 'Volt'], + }, +}; + +var PickerExample = React.createClass({ + getInitialState: function() { + return { + carMake: 'cadillac', + modelIndex: 3, + }; + }, + + render: function() { + var make = CAR_MAKES_AND_MODELS[this.state.carMake]; + var selectionString = make.name + ' ' + make.models[this.state.modelIndex]; + return ( + + Please choose a make for your car: + this.setState({carMake, modelIndex: 0})}> + {Object.keys(CAR_MAKES_AND_MODELS).map((carMake) => ( + + ))} + + Please choose a model of {make.name}: + this.setState({modelIndex})}> + {CAR_MAKES_AND_MODELS[this.state.carMake].models.map((modelName, modelIndex) => ( + + ))} + + You selected: {selectionString} + + ); + }, +}); + +var PickerStyleExample = React.createClass({ + getInitialState: function() { + return { + carMake: 'cadillac', + modelIndex: 0, + }; + }, + + render: function() { + var make = CAR_MAKES_AND_MODELS[this.state.carMake]; + var selectionString = make.name + ' ' + make.models[this.state.modelIndex]; + return ( + this.setState({carMake, modelIndex: 0})}> + {Object.keys(CAR_MAKES_AND_MODELS).map((carMake) => ( + + ))} + + ); + }, +}); + +exports.displayName = (undefined: ?string); +exports.title = ''; +exports.description = 'Render lists of selectable options with UIPickerView.'; +exports.examples = [ +{ + title: '', + render: function(): ReactElement { + return ; + }, +}, +{ + title: ' with custom styling', + render: function(): ReactElement { + return ; + }, +}]; +``` \ No newline at end of file diff --git a/docs/docs/0.41/pixelratio.md b/docs/docs/0.41/pixelratio.md new file mode 100644 index 0000000..485755c --- /dev/null +++ b/docs/docs/0.41/pixelratio.md @@ -0,0 +1,67 @@ +PixelRatio类提供了访问设备的像素密度的方法。 + +使用PixelRatio有如下几种常用情形: + +### 根据像素密度获取指定大小的图片 + +如果应用运行在一个高像素密度的设备上,显示的图片也应当分辨率更高。一个取得缩略图的好规则就是将显示尺寸乘以像素密度比: + +```javascript +var image = getImage({ + width: PixelRatio.getPixelSizeForLayoutSize(200), + height: PixelRatio.getPixelSizeForLayoutSize(100), +}); + +``` +__译注__: 这段代码的意思是,如果你要在屏幕上摆放一个宽200高100的图片,那么首先要准备多个分辨率尺寸的图。`PixelRatio.getPixelSizeForLayoutSize(200)`方法会根据当前设备的pixelratio返回对应值,比如当前设备的pixelratio为2,则返回 200 * 2 = 400,最后生成的参数为{ width: 400, height: 200 },然后开发者自己实现getImage方法,根据这一参数,返回最符合此尺寸的图片地址。 + +### 方法 + +
+
+

static get() #

+
+

返回设备的像素密度,例如:

+
    +
  • PixelRatio.get() === 1
    • mdpi Android 设备 (160 dpi)
  • +
  • PixelRatio.get() === 1.5
    • hdpi Android 设备 (240 dpi)
  • +
  • PixelRatio.get() === 2
    • iPhone 4, 4S
    • iPhone 5, 5c, 5s
    • iPhone 6
    • xhdpi Android 设备 (320 dpi)
  • +
  • PixelRatio.get() === 3
    • iPhone 6 plus
    • xxhdpi Android 设备 (480 dpi)
  • +
  • PixelRatio.get() === 3.5
    • Nexus 6
  • +
+
+
+
+

static getFontScale() #

+
+

返回字体大小缩放比例。这个比例可以用于计算绝对的字体大小,所以很多深度依赖字体大小的组件需要用此函数的结果进行计算。

+

如果没有设置字体大小,它会直接返回设备的像素密度。

+

目前这个函数仅仅在Android设备上实现了,它会体现用户选项里的“设置 > 显示 > 字体大小”。在iOS设备上它会直接返回默认的像素密度。

+
+
+
+

static getPixelSizeForLayoutSize(layoutSize: number) #

+
+

将一个布局尺寸(dp)转换为像素尺寸(px)。

+

一定会返回一个整数数值。

+
+
+
+

static startDetecting() #

+
+

// 本函数在移动设备上没有作用。

+
+
+
+ +### 描述 + +## 像素网格对齐 + +在iOS设备上,你可以给元素指定任意精度的坐标和尺寸,例如29.674825。不过最终的物理屏幕上只会显示固定的坐标数。譬如iPhone4的分辨率是640x960,而iPhone6是750*1334。iOS会试图尽可能忠实地显示你指定的坐标,所以它采用了一种把一个像素分散到多个像素里的做法来欺骗眼睛。但这个作用的负面影响是显示出来的元素看起来会有一些模糊。 + +在实践中,我们发现开发者们并不想要这个特性,反而需要去做一些额外的工作来确保坐标与像素坐标对齐,来避免元素显得模糊。在React Native中,我们会自动对齐坐标到像素坐标。 + +我们做这个对齐的时候必须十分小心。如果你同时使用已经对齐的值和没有对齐的值,就会很容易产生一些因为近似导致的累积错误。即使这样的累积错误只发生一次,后果也可能会很严重,因为很可能会导致一个像素宽的边框最终突然消失或者显示为两倍的宽度。 + +在React Native中,所有JS中的东西,包括布局引擎,都使用任意精度的数值。我们只在主线程最后设置原生组件的位置和坐标的时候才去做对齐工作。而且,对齐是相对于屏幕进行的,而非相对于父元素进行,进一步避免近似误差的累积。 diff --git a/docs/docs/0.41/platform-specific-code.md b/docs/docs/0.41/platform-specific-code.md new file mode 100644 index 0000000..7414dad --- /dev/null +++ b/docs/docs/0.41/platform-specific-code.md @@ -0,0 +1,93 @@ +在制作跨平台的App时,多半会碰到针对不同平台编写不同代码的需求。最直接的方案就是把组件放置到不同的文件夹下: + +```sh +/common/components/ +/android/components/ +/ios/components/ +``` + +另一个选择是根据平台不同在组件的文件命名上加以区分,如下: + +```sh +BigButtonIOS.js +BigButtonAndroid.js +``` + +但除此以外React Native还提供了另外两种简单区分平台的方案: + +## 特定平台扩展名 +React Native会检测某个文件是否具有`.ios.`或是`.android.`的扩展名,然后根据当前运行的平台加载正确对应的文件。 + +假设你的项目中有如下两个文件: + +```sh +BigButton.ios.js +BigButton.android.js +``` + +这样命名组件后你就可以在其他组件中直接引用,而无需关心当前运行的平台是哪个。 + +```javascript +import BigButton from './components/BigButton'; +``` + +React Native会根据运行平台的不同引入正确对应的组件。 + +还有个实用的方法是Platform.select(),它可以以Platform.OS为key,从传入的对象中返回对应平台的值,见下面的示例: + +```javascript +import { Platform, StyleSheet } from 'react-native'; + +var styles = StyleSheet.create({ + container: { + flex: 1, + ...Platform.select({ + ios: { + backgroundColor: 'red', + }, + android: { + backgroundColor: 'blue', + }, + }), + }, +}); +``` + +上面的代码会根据平台的不同返回不同的container样式——iOS上背景色为红色,而android为蓝色。 + +这一方法可以接受任何合法类型的参数,因此你也可以直接用它针对不同平台返回不同的组件,像下面这样: + + +```javascript +var Component = Platform.select({ + ios: () => require('ComponentIOS'), + android: () => require('ComponentAndroid'), +})(); + +; +``` + + +## 平台模块 +React Native提供了一个检测当前运行平台的模块。如果组件只有一小部分代码需要依据平台定制,那么这个模块就可以派上用场。 + +```javascript +import { Platform, StyleSheet } from 'react-native'; + +var styles = StyleSheet.create({ + height: (Platform.OS === 'ios') ? 200 : 100, +}); +``` + +`Platform.OS`在iOS上会返回`ios`,而在Android设备或模拟器上则会返回`android`。 + +### 检测Android版本 +在Android上,平台模块还可以用来检测当前所运行的Android平台的版本: + +```javascript +import { Platform } from 'react-native'; + +if(Platform.Version === 21){ + console.log('Running on Lollipop!'); +} +``` diff --git a/docs/docs/0.41/progressbarandroid.md b/docs/docs/0.41/progressbarandroid.md new file mode 100644 index 0000000..e434d7c --- /dev/null +++ b/docs/docs/0.41/progressbarandroid.md @@ -0,0 +1,165 @@ +封装了Android平台上的`ProgressBar`的React组件。这个组件可以用来表示应用正在加载或者有些事情正在进行中。 + +例子: + +```javascript +render: function() { + var progressBar = + + + ; + + return ( + + ); +}, +``` +### 截图 +![](img/components/progressbarandroid.png) + +### 属性 + +
+ +
+

color string #

+
+

进度条的颜色。

+
+
+
+

indeterminate indeterminateType #

+
+

决定进度条是否要显示一个不确定的进度。注意这个在styleAttr是Horizontal的时候必须是false。

+
+
+
+

progress number #

+
+

当前的进度值(在0到1之间)。

+
+
+
+

styleAttr STYLE_ATTRIBUTES #

+
+

进度条的样式。可取值有:

+
    +
  • Horizontal
  • +
  • Small
  • +
  • Large
  • +
  • Inverse
  • +
  • SmallInverse
  • +
  • LargeInverse
  • +
+
+
+
+

testID string #

+
+

用来在端到端测试中定位这个视图。

+
+
+
+ +### 样例 + +```javascript +'use strict'; + +var ProgressBar = require('ProgressBarAndroid'); +var React = require('React'); +var UIExplorerBlock = require('UIExplorerBlock'); +var UIExplorerPage = require('UIExplorerPage'); + +var TimerMixin = require('react-timer-mixin'); + +var MovingBar = React.createClass({ + mixins: [TimerMixin], + + getInitialState: function() { + return { + progress: 0 + }; + }, + + componentDidMount: function() { + this.setInterval( + () => { + var progress = (this.state.progress + 0.02) % 1; + this.setState({progress: progress}); + }, 50 + ); + }, + + render: function() { + return ; + }, +}); + +var ProgressBarAndroidExample = React.createClass({ + + statics: { + title: '', + description: 'Visual indicator of progress of some operation. ' + + 'Shows either a cyclic animation or a horizontal bar.', + }, + + render: function() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + }, +}); + +module.exports = ProgressBarAndroidExample; +``` \ No newline at end of file diff --git a/docs/docs/0.41/progressviewios.md b/docs/docs/0.41/progressviewios.md new file mode 100644 index 0000000..4c571ba --- /dev/null +++ b/docs/docs/0.41/progressviewios.md @@ -0,0 +1,123 @@ +使用`ProgressViewIOS`来在iOS上渲染一个UIProgressView。 + +### 截图 +![](img/components/progressviewios.png) + +### 属性 + +
+ +
+

progress number #

+
+

当前的进度值(0到1之间)。

+
+
+
+

progressImage Image.propTypes.source #

+
+

一个可以拉伸的图片,用于显示进度条。

+
+
+
+

progressTintColor string #

+
+

进度条本身染上的颜色。

+
+
+
+

progressViewStyle enum('default', 'bar') #

+
+

进度条的样式。

+
+
+
+

trackImage Image.propTypes.source #

+
+

一个可拉伸的图片,用于显示进度条后面的轨道。

+
+
+
+

trackTintColor string #

+
+

进度条轨道染上的颜色。

+
+
+
+ +### 例子 + +```javascript +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + ProgressViewIOS, + StyleSheet, + View, +} = ReactNative; +var TimerMixin = require('react-timer-mixin'); + +var ProgressViewExample = React.createClass({ + mixins: [TimerMixin], + + getInitialState() { + return { + progress: 0, + }; + }, + + componentDidMount() { + this.updateProgress(); + }, + + updateProgress() { + var progress = this.state.progress + 0.01; + this.setState({ progress }); + this.requestAnimationFrame(() => this.updateProgress()); + }, + + getProgress(offset) { + var progress = this.state.progress + offset; + return Math.sin(progress % Math.PI) % 1; + }, + + render() { + return ( + + + + + + + + ); + }, +}); + +exports.displayName = (undefined: ?string); +exports.framework = 'React'; +exports.title = 'ProgressViewIOS'; +exports.description = 'ProgressViewIOS'; +exports.examples = [{ + title: 'ProgressViewIOS', + render() { + return ( + + ); + } +}]; + +var styles = StyleSheet.create({ + container: { + marginTop: -20, + backgroundColor: 'transparent', + }, + progressView: { + marginTop: 20, + } +}); +``` \ No newline at end of file diff --git a/docs/docs/0.41/props.md b/docs/docs/0.41/props.md new file mode 100644 index 0000000..0f9b238 --- /dev/null +++ b/docs/docs/0.41/props.md @@ -0,0 +1,60 @@ +大多数组件在创建时就可以使用各种参数来进行定制。用于定制的这些参数就称为`props`(属性)。 + +以常见的基础组件`Image`为例,在创建一个图片时,可以传入一个名为`source`的prop来指定要显示的图片的地址,以及使用名为`style`的prop来控制其尺寸。 + +```ReactNativeWebPlayer +import React, { Component } from 'react'; +import { AppRegistry, Image } from 'react-native'; + +class Bananas extends Component { + render() { + let pic = { + uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg' + }; + return ( + + ); + } +} + +AppRegistry.registerComponent('Bananas', () => Bananas); +``` + +译注:在iOS上使用http链接的图片地址可能不会显示,参见[这篇说明修改](https://segmentfault.com/a/1190000002933776)。 + +请注意`{pic}`外围有一层括号,我们需要用括号来把`pic`这个变量嵌入到JSX语句中。括号的意思是括号内部为一个js变量或表达式,需要执行后取值。因此我们可以把任意合法的JavaScript表达式通过括号嵌入到JSX语句中。 + +自定义的组件也可以使用`props`。通过在不同的场景使用不同的属性定制,可以尽量提高自定义组件的复用范畴。只需在`render`函数中引用`this.props`,然后按需处理即可。下面是一个例子: + +```ReactNativeWebPlayer +import React, { Component } from 'react'; +import { AppRegistry, Text, View } from 'react-native'; + +class Greeting extends Component { + render() { + return ( + Hello {this.props.name}! + ); + } +} + +class LotsOfGreetings extends Component { + render() { + return ( + + + + + + ); + } +} + +AppRegistry.registerComponent('LotsOfGreetings', () => LotsOfGreetings); +``` + +我们在`Greeting`组件中将`name`作为一个属性来定制,这样可以复用这一组件来制作各种不同的“问候语”。上面的例子把`Greeting`组件写在JSX语句中,用法和内置组件并无二致——这正是React体系的魅力所在——如果你想搭建一套自己的基础UI框架,那就放手做吧! + +上面的例子出现了一样新的名为[`View`](view.html)的组件。[`View`](view.html) 常用作其他组件的容器,来帮助控制布局和样式。 + +仅仅使用`props`和基础的[`Text`](text.html)、[`Image`](image.html)以及[`View`](view.html)组件,你就已经足以编写各式各样的UI组件了。要学习如何动态修改你的界面,那就需要进一步[学习State(状态)的概念](state.html)。 diff --git a/docs/docs/0.41/pushnotificationios.md b/docs/docs/0.41/pushnotificationios.md new file mode 100644 index 0000000..a8adf39 --- /dev/null +++ b/docs/docs/0.41/pushnotificationios.md @@ -0,0 +1,372 @@ +本模块帮助你处理应用的推送通知,包括权限控制以及应用图标上的角标数(未读消息数)。 + +要使用推送通知功能,首先[在苹果后台配置推送通知服务](https://developer.apple.com/library/ios/documentation/IDEs/Conceptual/AppDistributionGuide/AddingCapabilities/AddingCapabilities.html#//apple_ref/doc/uid/TP40012582-CH26-SW6)并且准备好服务端的系统。设置的过程可以参考[Parse的教程](https://parse.com/tutorials/ios-push-notifications) + +首先请[手动链接](linking-libraries-ios.html)PushNotificationIOS的库(以下操作如果不熟悉,请自行补习Xcode的使用教程): +- 将`node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj`文件拖到Xcode界面中 +- 在Xcode的`Link Binary With Libraries`中添加`libRCTPushNotification.a` +- 在`Header Search Paths`中添加: `$(SRCROOT)/../node_modules/react-native/Libraries/PushNotificationIOS` +- 将搜索选项设为`recursive` + +然后你需要在AppDelegate中启用推送通知的支持以及注册相应的事件。 + +在`AppDelegate.m`开头: + +```objective-c +#import "RCTPushNotificationManager.h" +``` + +然后在AppDelegate实现中添加如下的代码: + +```objective-c + // Required to register for notifications + - (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings + { + [RCTPushNotificationManager didRegisterUserNotificationSettings:notificationSettings]; + } + // Required for the register event. + - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken + { + [RCTPushNotificationManager didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; + } + // Required for the notification event. + - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification + { + [RCTPushNotificationManager didReceiveRemoteNotification:notification]; + } + // Required for the localNotification event. + - (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification + { + [RCTPushNotificationManager didReceiveLocalNotification:notification]; + } +``` + +### 方法 + +
+
+

static presentLocalNotification(details: Object) #

+
+

立即产生一个本地通知

+

details参数是一个对象,包含:

+
    +
  • alertBody : 要在通知提示中显示的消息。
  • +
  • alertAction : 在交互式通知提示下显示的"action"。默认为"view"。
  • +
  • soundName : 通知触发时播放的声音名字(可选)。
  • +
  • category : 可选的通知类型,但对于交互式通知为必填。
  • +
  • userInfo : 提供一个可选的object,可以在其中提供额外的数据。
  • +
  • applicationIconBadgeNumber : 指定显示在应用右上角的数字角标(可选)。默认值为0,即不显示角标。
  • +
+
+
+
+

static scheduleLocalNotification(details: Object) #

+
+

计划一个本地通知,在将来进行提示。

+

details参数是一个对象,包含:

+
    +
  • fireDate : 系统发送这个提示的日期和时间。
  • +
  • alertBody : 要在通知提示中显示的消息。
  • +
  • alertAction : 在交互式通知提示下显示的"action"。默认为"view"。
  • +
  • soundName : 通知触发时播放的声音名字(可选)。
  • +
  • category : 可选的通知类型,但对于交互式通知为必填。
  • +
  • userInfo : 提供一个可选的object,可以在其中提供额外的数据。
  • +
  • applicationIconBadgeNumber : 指定显示在应用右上角的数字角标(可选)。默认值为0,即不显示角标。
  • +
+
+
+
+

static cancelAllLocalNotifications() #

+
+

取消所有已计划的本地通知

+
+
+
+

static setApplicationIconBadgeNumber(number: number) #

+
+

设置要在手机主屏幕应用图标上显示的角标数(未读消息数)。

+
+
+
+

static getApplicationIconBadgeNumber(callback: Function) #

+
+

获取目前在手机主屏幕应用图标上显示的角标数(未读消息数)。

+
+
+
+

static addEventListener(type: string, handler: Function) #

+
+

添加一个监听器,监听远程或本地推送的通知事件,不论应用在前台还是在后台运行

+

事件类型有:

+
    +
  • notification : 当收到来自远程的推送通知时调用handler函数,第一个参数是一个PushNotificationIOS实例。
  • +
  • localNotification : 当收到来自本地的推送通知时调用handler函数,第一个参数是一个PushNotificationIOS实例。
  • +
  • register: 当用户注册远程通知的时候调用handler函数。参数是一个十六进制的字符串,表示了设备标识(deviceToken)。
  • +
+
+
+
+

static requestPermissions(permissions?: { + alert?: boolean, + badge?: boolean, + sound?: boolean + }) #

+
+

向iOS系统请求通知权限,给用户展示一个对话框。默认情况下,它会请求所有的权限。不过你可以通过传递一个映射(map)到permissions参数来请求指定的权限子集。可以请求的权限类型有:

+
    +
  • alert
  • +
  • badge
  • +
  • sound
  • +
+

如果提供了一个映射(map)作为参数,只有值为真值的权限才会被请求。

+
+
+
+

static abandonPermissions() #

+
+

注销所有从苹果推送通知服务收到的远程消息。

+

你应该只会在极少的情况下需要调用此函数,譬如一个新版本的App要取消所有远程推送通知的支持。如果是用户希望关闭推送通知,他可以打开系统设置的推送通知一栏来暂时屏蔽。应用通过此方法注销后,可以随时重新注册。

+
+
+
+

static checkPermissions(callback: Function) #

+
+

检查哪些推送通知权限被开启。 callback函数会被调用,参数为permissions 对象:

+
    +
  • alert :boolean
  • +
  • badge :boolean
  • +
  • sound :boolean
  • +
+
+
+
+

static removeEventListener(type: string, handler: Function) #

+

移除注册事件监听器。在componentWillUnmount中调用此函数以避免内存泄露。

+
+
+
+

static popInitialNotification() #

+
+

如果用户通过点击推送通知来冷启动应用(即:之前应用不在运行状态),此函数会返回一个初始的通知。

+

第一次调用popInitialNotification会返回初始的通知对象,或者返回null。后续的调用全部会返回null.

+
+
+
+

constructor(nativeNotif: Object) #

+

你应该永远不需要自己实例化PushNotificationIOS对象。监听notification事件和调用popInitialNotification应当足够了。

+
+
+
+

getMessage() #

+
+

getAlert方法的别名。获取推送通知的主消息内容。

+
+
+
+

getSound() #

+
+

aps对象中获取声音字符串

+
+
+
+

getAlert() #

+
+

aps对象中获取推送通知的主消息内容。

+
+
+
+

getBadgeCount() #

+
+

aps对象中获取推送通知的角标数(未读消息数)。

+
+
+
+

getData() #

+
+

获取推送的数据对象。

+
+
+
+ +### 例子 + +```javascript +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + AlertIOS, + PushNotificationIOS, + StyleSheet, + Text, + TouchableHighlight, + View, +} = ReactNative; + +var Button = React.createClass({ + render: function() { + return ( + + + {this.props.label} + + + ); + } +}); + +class NotificationExample extends React.Component { + componentWillMount() { + // Add listener for push notifications + PushNotificationIOS.addEventListener('notification', this._onNotification); + // Add listener for local notifications + PushNotificationIOS.addEventListener('localNotification', this._onLocalNotification); + } + + componentWillUnmount() { + // Remove listener for push notifications + PushNotificationIOS.removeEventListener('notification', this._onNotification); + // Remove listener for local notifications + PushNotificationIOS.removeEventListener('localNotification', this._onLocalNotification); + } + + render() { + return ( + +