├── screenshot ├── ios.gif ├── ios-01.png ├── ios-02.png ├── Android.gif ├── Android-01.png └── Android-02.png ├── .gitignore ├── package.json ├── LICENSE ├── src ├── styles.js └── index.js ├── README.md └── example └── index.js /screenshot/ios.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qfight/react-native-actionsheet-api/HEAD/screenshot/ios.gif -------------------------------------------------------------------------------- /screenshot/ios-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qfight/react-native-actionsheet-api/HEAD/screenshot/ios-01.png -------------------------------------------------------------------------------- /screenshot/ios-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qfight/react-native-actionsheet-api/HEAD/screenshot/ios-02.png -------------------------------------------------------------------------------- /screenshot/Android.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qfight/react-native-actionsheet-api/HEAD/screenshot/Android.gif -------------------------------------------------------------------------------- /screenshot/Android-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qfight/react-native-actionsheet-api/HEAD/screenshot/Android-01.png -------------------------------------------------------------------------------- /screenshot/Android-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qfight/react-native-actionsheet-api/HEAD/screenshot/Android-02.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # System 40 | .DS_Store 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-actionsheet-api", 3 | "version": "1.0.4", 4 | "description": "A React Native ActionSheet polyfill for Android written by Javascript, no native code is required.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/zdying/react-native-actionsheet-api.git" 12 | }, 13 | "keywords": [ 14 | "react-native", 15 | "actionsheet", 16 | "api", 17 | "showActionSheetWithOptions", 18 | "action-sheet" 19 | ], 20 | "author": "zdying ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/zdying/react-native-actionsheet-api/issues" 24 | }, 25 | "homepage": "https://github.com/zdying/react-native-actionsheet-api#readme" 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 qfight 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/styles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @author zdying 4 | */ 5 | 6 | 'use strict'; 7 | 8 | import {StyleSheet} from 'react-native'; 9 | 10 | export default StyleSheet.create({ 11 | main: { 12 | flex: 1, 13 | alignItems: 'center', 14 | justifyContent: 'flex-end', 15 | backgroundColor: 'rgba(0, 0, 0, 0.3)', 16 | }, 17 | 18 | body: { 19 | position: 'absolute', 20 | left: 10, 21 | right: 10, 22 | bottom: 10 23 | }, 24 | 25 | header: { 26 | flex: 1, 27 | borderBottomWidth: 1, 28 | borderBottomColor: '#f0eff5', 29 | height: 40, 30 | alignItems: 'center', 31 | justifyContent: 'center' 32 | }, 33 | 34 | headerText: { 35 | fontSize: 14, 36 | color: '#8F8F8F', 37 | }, 38 | 39 | buttonContainer: { 40 | backgroundColor: 'white', 41 | borderRadius: 10, 42 | overflow: 'hidden' 43 | }, 44 | 45 | button: { 46 | height: 58, 47 | flexDirection: 'row', 48 | borderBottomWidth: 1, 49 | borderBottomColor: '#f0eff5', 50 | alignItems: 'center', 51 | justifyContent: 'center' 52 | }, 53 | 54 | buttonText: { 55 | fontSize: 18 56 | }, 57 | 58 | buttonLast: { 59 | borderBottomWidth: 0 60 | }, 61 | 62 | red: { 63 | color: 'red' 64 | } 65 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-actionsheet-api 2 | 3 | 提供Android和iOS平台通用的的`showActionSheetWithOptions()`API。统一使用`ActionSheet`。调用时,如果是iOS,调用`ActionSheetIOS.showActionSheetWithOptions()`。 4 | 5 | ## Why react-native-actionsheet-api 6 | 7 | IOS有`ActionSheetIOS.showActionSheetWithOptions()`,但是在Android中没有这个方法可以使用, 8 | 虽然在Android中使用ActionSheet有人会感觉很别扭,但是有时候确实需要使用(可以把样式改成Android风格的)。 9 | 10 | 当我们必须**要使用ActionSheet**,并且希望跟IOS一样,**通过API调用来展示,而不是每次通过渲染一个组件**来展示, 11 | 基本上就是提供Native提供组件,比如[react-native-actionsheet-native](https://www.npmjs.com/package/react-native-actionsheet-native),但是需要导入Native代码,而我不希望导入,所以开发出这个组件。 12 | 13 | > 提示:这个组件并不完美,使用之前,需要先在页面中渲染**一次**(创建一个实例) 14 | 15 | ## ScreenShot 16 | 17 | IOS效果: 18 | 19 | ![IOS](screenshot/ios.gif) 20 | 21 | Android效果: 22 | 23 | ![Android](screenshot/Android.gif) 24 | 25 | 26 | ## Useage 27 | 28 | ### Step 0: 安装 29 | 30 | ``` 31 | npm install react-native-actionsheet-api --save 32 | ``` 33 | 34 | ### Step 1: 引入 35 | ```js 36 | import ActionSheet from 'react-native-actionsheet-api'; 37 | ``` 38 | 39 | ### Step 2: 实例化 40 | 41 | 一般选择在使用之前实例化`ActionSheet`,但是**只需要实例化一次**。 42 | 43 | ```js 44 | // 在页面中渲染 45 | class MyPage extends React.component { 46 | // ... 47 | 48 | render(){ 49 | return ( 50 | 51 | {/* 只需要实例化一次 */} 52 | 53 | {/* ... */} 54 | 55 | ) 56 | } 57 | } 58 | ``` 59 | 60 | ### Step 3: 调用 61 | ```js 62 | // 然后在任何地方,都可以使用直接调用这个方法了 63 | // IOS和Android都可以使用下面的代码 64 | ActionSheet.showActionSheetWithOptions({ 65 | title: '请选择您最喜欢的水果', 66 | options: ['苹果🍎', '梨🍐', '香蕉🍌', '橘子🍊', '都不喜欢'], 67 | cancelButtonIndex: 4, 68 | //destructiveButtonIndex: 0, 69 | tintColor: 'green', 70 | }, 71 | (buttonIndex) => { 72 | this.setState({ clicked: BUTTONS[buttonIndex] }); 73 | } 74 | ); 75 | ``` 76 | 77 | ## License 78 | 79 | 这个项目采用MIT协议 - 详细信息请查看[LICENSE](https://github.com/qfight/react-native-actionsheet-api/blob/master/LICENSE)。 -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample React Native App 3 | * https://github.com/facebook/react-native 4 | * @flow 5 | */ 6 | 7 | import React, { Component } from 'react'; 8 | import { 9 | AppRegistry, 10 | StyleSheet, 11 | Text, 12 | View, 13 | Dimensions 14 | } from 'react-native'; 15 | import ActionSheet from 'ActionSheet'; 16 | 17 | export default class SucceLover extends Component { 18 | render() { 19 | return ( 20 | 21 | 选择喜欢的水果 22 | 选择喜欢的饮料 23 | 24 | ); 25 | } 26 | 27 | chooseFruit(){ 28 | ActionSheet.showActionSheetWithOptions({ 29 | title: '请选择您最喜欢的水果', 30 | options: ['苹果🍎', '梨🍐', '香蕉🍌', '橘子🍊', '都不喜欢'], 31 | cancelButtonIndex: 4 32 | }, 33 | (buttonIndex) => { 34 | console.log('您的选择:', buttonIndex); 35 | } 36 | ); 37 | } 38 | 39 | chooseDrink(){ 40 | ActionSheet.showActionSheetWithOptions({ 41 | title: '请选择您最喜欢的饮料', 42 | options: ['雪碧', '可口可乐', '脉动', '芬达', '不喜欢喝饮料'], 43 | cancelButtonIndex: 4, 44 | destructiveButtonIndex: 3, 45 | tintColor: 'green', 46 | }, 47 | (buttonIndex) => { 48 | console.log('您的选择:', buttonIndex); 49 | } 50 | ); 51 | } 52 | } 53 | 54 | const styles = StyleSheet.create({ 55 | container: { 56 | flex: 1, 57 | flexDirection: 'column', 58 | justifyContent: 'flex-start', 59 | alignItems: 'center', 60 | backgroundColor: '#F5FCFF', 61 | paddingTop: 45 62 | }, 63 | button1: { 64 | paddingVertical: 10, 65 | margin: 10, 66 | width: Dimensions.get('window').width - 20, 67 | backgroundColor: '#DB5149', 68 | color: 'white', 69 | textAlign: 'center', 70 | borderRadius: 4, 71 | overflow: 'hidden' 72 | }, 73 | button2: { 74 | paddingVertical: 10, 75 | margin: 10, 76 | width: Dimensions.get('window').width - 20, 77 | backgroundColor: '#29A365', 78 | color: 'white', 79 | textAlign: 'center', 80 | borderRadius: 4, 81 | overflow: 'hidden' 82 | }, 83 | }); 84 | 85 | AppRegistry.registerComponent('ActionSheetExample', () => SucceLover); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ActionSheet API 3 | * @author zdying 4 | * @providesModule ActionSheet 5 | */ 6 | import React, { Component } from 'react'; 7 | import { Modal, Text, View, Animated, TouchableWithoutFeedback, TouchableOpacity, Platform, ActionSheetIOS } from 'react-native'; 8 | import styles from './styles'; 9 | 10 | const isIOS = Platform.OS === 'ios'; 11 | const TITLE_HEIGHT = 40; 12 | const BUTTON_HEIGHT = 58; 13 | const GROUP_SPACE_WIDTH = 10; 14 | 15 | export default class ActionSheet extends Component { 16 | constructor (props, state) { 17 | super(props, state); 18 | 19 | if (!global.__action_sheet) { 20 | global.__action_sheet = this; 21 | this.state = { 22 | sheets: [], 23 | marginBottomValue: new Animated.Value(0) 24 | }; 25 | 26 | this.state.marginBottomValue.addListener(({value}) => { this._value = value; }); 27 | } else { 28 | this.__should_not_render = true; 29 | } 30 | } 31 | 32 | show (opts, callback) { 33 | let sheets = this.state.sheets; 34 | let height = this.getHeight(opts); 35 | 36 | this.state.marginBottomValue.setValue(-height); 37 | 38 | this.setState({ 39 | sheets: [ 40 | ...sheets, 41 | { 42 | opts, 43 | callback 44 | } 45 | ], 46 | height 47 | }); 48 | } 49 | 50 | hide () { 51 | Animated.timing( 52 | this.state.marginBottomValue, 53 | {toValue: -this.state.height, duration: 210, delay: 0} 54 | ).start(() => { 55 | this.setState({ 56 | sheets: this.state.sheets.slice(0, -1) 57 | }); 58 | }); 59 | } 60 | 61 | getHeight (opts) { 62 | let {title, cancelButtonIndex, options, destructiveButtonIndex} = opts; 63 | 64 | let height = options.length * BUTTON_HEIGHT + 10; 65 | 66 | if (title) { 67 | height += TITLE_HEIGHT; 68 | } 69 | 70 | if (cancelButtonIndex < options.length && cancelButtonIndex !== destructiveButtonIndex) { 71 | height += GROUP_SPACE_WIDTH; 72 | } 73 | 74 | return height; 75 | } 76 | 77 | render () { 78 | if (this.__should_not_render || this.state.sheets.length === 0) { 79 | return null; 80 | } 81 | 82 | Animated.timing( 83 | this.state.marginBottomValue, 84 | {toValue: 0, duration: 210, delay: 10} 85 | ).start(); 86 | 87 | let onMaskPress = this.onMaskPress.bind(this); 88 | 89 | return ( 90 | 95 | 96 | 97 | 98 | {this.renderActionButtons()} 99 | 100 | 101 | 102 | 103 | ); 104 | } 105 | 106 | renderActionButtons () { 107 | let {sheets} = this.state; 108 | let lastSheet = sheets.slice(-1)[0]; 109 | let {buttonList, cancelButton, title} = this.parseOpitons(lastSheet); 110 | 111 | return [ 112 | this.renderButtonList(buttonList, {}, title), 113 | cancelButton && this.renderButtonList([cancelButton], {marginTop: 10}) 114 | ]; 115 | } 116 | 117 | parseOpitons (sheet) { 118 | let {opts, callback} = sheet; 119 | let buttonList = []; 120 | let cancelButton = null; 121 | let {title, options = [], tintColor = '#157EFB', cancelButtonIndex, destructiveButtonIndex} = opts; 122 | 123 | (options).forEach((option, index) => { 124 | let btnObj = { 125 | title: option, 126 | index, 127 | callback 128 | }; 129 | 130 | if (index === destructiveButtonIndex) { 131 | btnObj.color = 'red'; 132 | } else { 133 | btnObj.color = tintColor; 134 | } 135 | 136 | if (index === cancelButtonIndex && cancelButtonIndex !== destructiveButtonIndex) { 137 | btnObj.fontWeight = 'bold'; 138 | cancelButton = btnObj; 139 | } else { 140 | btnObj.fontWeight = 'normal'; 141 | buttonList.push(btnObj); 142 | } 143 | }); 144 | 145 | return { 146 | buttonList, 147 | cancelButton, 148 | title 149 | }; 150 | } 151 | 152 | renderButtonList (list, style, title) { 153 | return ( 154 | 155 | {title ? 156 | 157 | {title} 158 | 159 | : null} 160 | {list.map((button, bIndex) => { 161 | let {index, title, color, fontWeight, callback} = button; 162 | let borderBottomWidth = Number(bIndex !== list.length - 1); 163 | 164 | return ( 165 | 170 | 171 | {title} 172 | 173 | 174 | ); 175 | })} 176 | 177 | ); 178 | } 179 | 180 | onButtonPress (index, callback) { 181 | callback(index); 182 | this.hide(); 183 | } 184 | 185 | onMaskPress () { 186 | let {sheets} = this.state; 187 | let lastSheet = sheets.slice(-1)[0] || {}; 188 | let {opts, callback} = lastSheet; 189 | 190 | if (!opts || !callback) { 191 | return; 192 | } 193 | 194 | let {options = [], cancelButtonIndex, destructiveButtonIndex} = lastSheet.opts; 195 | 196 | if (cancelButtonIndex < options.length && cancelButtonIndex !== destructiveButtonIndex) { 197 | callback(cancelButtonIndex); 198 | this.hide(); 199 | } 200 | } 201 | } 202 | 203 | /** 204 | * Display an action sheet. The `opts` object must contain one or more 205 | * of: 206 | * 207 | * - `options` (array of strings) - a list of button titles (required) 208 | * - `cancelButtonIndex` (int) - index of cancel button in `options` 209 | * - `destructiveButtonIndex` (int) - index of destructive button in `options` 210 | * - `title` (string) - a title to show above the action sheet 211 | * - `message` (string) - a message to show below the title 212 | * - `tintColor` (string) - button color 213 | */ 214 | ActionSheet.showActionSheetWithOptions = ( 215 | opts, 216 | callback 217 | ) => { 218 | if (isIOS) { 219 | // IOS 直接调用API 220 | ActionSheetIOS.showActionSheetWithOptions(opts, callback); 221 | } else { 222 | // Android 223 | if (global.__action_sheet instanceof ActionSheet) { 224 | global.__action_sheet.show(opts, callback); 225 | } else { 226 | throw Error('ActionSheet has not been initialized. To initialize ActionSheet, you should put `` to your render().'); 227 | } 228 | } 229 | }; 230 | 231 | ActionSheet.hide = () => { 232 | global.__action_sheet.hide(); 233 | }; 234 | 235 | global.ActionSheet = ActionSheet; 236 | --------------------------------------------------------------------------------