├── .eslintrc ├── .flowconfig ├── .gitignore ├── LICENSE ├── README.md ├── components ├── NativeButton.js └── btn.js ├── index.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", // https://github.com/babel/babel-eslint 3 | "plugins": [ 4 | "react" // https://github.com/yannickcr/eslint-plugin-react 5 | ], 6 | "env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments 7 | "browser": true, // browser global variables 8 | "node": true, // Node.js global variables and Node.js-specific rules 9 | "es6": true 10 | }, 11 | "ecmaFeatures": { 12 | "arrowFunctions": true, 13 | "blockBindings": true, 14 | "classes": true, 15 | "defaultParams": true, 16 | "destructuring": true, 17 | "forOf": true, 18 | "generators": false, 19 | "modules": true, 20 | "objectLiteralComputedProperties": true, 21 | "objectLiteralDuplicateProperties": false, 22 | "objectLiteralShorthandMethods": true, 23 | "objectLiteralShorthandProperties": true, 24 | "spread": true, 25 | "superInFunctions": true, 26 | "templateStrings": true, 27 | "jsx": true 28 | }, 29 | "rules": { 30 | "no-multi-spaces": 0, 31 | /** 32 | * Strict mode 33 | */ 34 | // babel inserts "use strict"; for us 35 | "strict": [2, "never"], // http://eslint.org/docs/rules/strict 36 | 37 | /** 38 | * ES6 39 | */ 40 | "no-var": 2, // http://eslint.org/docs/rules/no-var 41 | "prefer-const": 2, // http://eslint.org/docs/rules/prefer-const 42 | 43 | /** 44 | * Variables 45 | */ 46 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow 47 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 48 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars 49 | "vars": "local", 50 | "args": "after-used" 51 | }], 52 | "no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define 53 | 54 | /** 55 | * Possible errors 56 | */ 57 | "comma-dangle": [2, "always-multiline"], // http://eslint.org/docs/rules/comma-dangle 58 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign 59 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger 60 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert 61 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition 62 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 63 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case 64 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 65 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 66 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast 67 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 68 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 69 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 70 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 71 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 72 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 73 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 74 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 75 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 76 | "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var 77 | 78 | /** 79 | * Best practices 80 | */ 81 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 82 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly 83 | "default-case": 0, // http://eslint.org/docs/rules/default-case 84 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation 85 | "allowKeywords": true 86 | }], 87 | "no-undef": 2, 88 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq 89 | "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in 90 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 91 | "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return 92 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null 93 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval 94 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 95 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind 96 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough 97 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 98 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 99 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 100 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 101 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str 102 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 103 | "no-new": 2, // http://eslint.org/docs/rules/no-new 104 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 105 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 106 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 107 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 108 | /*"no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign*/ 109 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 110 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 111 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 112 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 113 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 114 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 115 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 116 | "no-with": 2, // http://eslint.org/docs/rules/no-with 117 | "radix": 2, // http://eslint.org/docs/rules/radix 118 | "vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top 119 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife 120 | "yoda": 2, // http://eslint.org/docs/rules/yoda 121 | 122 | /** 123 | * Style 124 | */ 125 | "indent": [2, 2, {"SwitchCase": 1}], // http://eslint.org/docs/rules/indent 126 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style 127 | "1tbs", { 128 | "allowSingleLine": true 129 | }], 130 | "quotes": [ 131 | 2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes 132 | ], 133 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase 134 | "properties": "never" 135 | }], 136 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing 137 | "before": false, 138 | "after": true 139 | }], 140 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style 141 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last 142 | "func-names": 1, // http://eslint.org/docs/rules/func-names 143 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing 144 | "beforeColon": false, 145 | "afterColon": true 146 | }], 147 | "new-cap": 0, 148 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines 149 | "max": 2 150 | }], 151 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary 152 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 153 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 154 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 155 | "no-extra-parens": [2, "functions"],// http://eslint.org/docs/rules/no-wrap-func 156 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 157 | "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var 158 | "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks 159 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi 160 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing 161 | "before": false, 162 | "after": true 163 | }], 164 | "keyword-spacing": 2, // http://eslint.org/docs/rules/keyword-spacing 165 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 166 | "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren 167 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 168 | "spaced-comment": 2, // http://eslint.org/docs/rules/spaced-line-comment 169 | 170 | /** 171 | * JSX style 172 | */ 173 | "react/display-name": 0, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/display-name.md 174 | "jsx-quotes": [2,"prefer-double"], // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-quotes.md 175 | "react/jsx-no-undef": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-undef.md 176 | "react/jsx-sort-props": 0, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-sort-props.md 177 | "react/jsx-sort-prop-types": 0, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-sort-prop-types.md 178 | "react/jsx-uses-react": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-uses-react.md 179 | "react/jsx-uses-vars": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-uses-vars.md 180 | "react/no-did-mount-set-state": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-did-mount-set-state.md 181 | "react/no-did-update-set-state": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-did-update-set-state.md 182 | "react/no-multi-comp": 0, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-multi-comp.md 183 | "react/no-unknown-property": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-unknown-property.md 184 | /*"react/prop-types": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/prop-types.md*/ 185 | "react/react-in-jsx-scope": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/react-in-jsx-scope.md 186 | "react/self-closing-comp": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/self-closing-comp.md 187 | "react/wrap-multilines": 2 // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/wrap-multilines.md 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | .*/Libraries/react-native/React.js 15 | .*/Libraries/react-native/ReactNative.js 16 | 17 | [include] 18 | 19 | [libs] 20 | node_modules/react-native/Libraries/react-native/react-native-interface.js 21 | node_modules/react-native/flow 22 | flow/ 23 | 24 | [options] 25 | emoji=true 26 | 27 | module.system=haste 28 | 29 | experimental.strict_type_args=true 30 | 31 | munge_underscores=true 32 | 33 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 34 | 35 | suppress_type=$FlowIssue 36 | suppress_type=$FlowFixMe 37 | suppress_type=$FixMe 38 | 39 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-8]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 40 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-8]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 41 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 42 | 43 | unsafe.enable_getters_and_setters=true 44 | 45 | [version] 46 | ^0.38.0 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | 33 | # node.js 34 | # 35 | node_modules/ 36 | npm-debug.log 37 | yarn-error.log 38 | 39 | # BUCK 40 | buck-out/ 41 | \.buckd/ 42 | *.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 50 | 51 | fastlane/report.xml 52 | fastlane/Preview.html 53 | fastlane/screenshots 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dan Cormier 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-swipeable-view 2 | 3 | This library allow you to create swipeable component, by exemple for a row in a list view, or anywhere you want. 4 | The code is based on the experimental SwipeableListView of react-native. 5 | 6 | ![swipe](https://cloud.githubusercontent.com/assets/3551795/25225308/45494d9a-25c1-11e7-830f-defea7a8262d.gif) 7 | 8 | ## Installation 9 | 10 | ``` 11 | npm install --save react-native-swipeable-view 12 | ``` 13 | 14 | ## Usage example 15 | 16 | ``` 17 | import SwipeableView from 'react-native-swipeable-view'; 18 | 19 | // Buttons 20 | var btnsArray = [ 21 | { 22 | text: 'Button', 23 | }, 24 | ]; 25 | 26 | // SwipeableView component 27 | 28 | 29 | Swipe me left 30 | 31 | 32 | 33 | ``` 34 | 35 | ## Props 36 | 37 | Prop | Type | Optional | Default | Description 38 | ------------------- | ------ | -------- | --------- | ----------- 39 | isOpen         | bool   | Yes | false | Swipeout is open or not 40 | autoClose       | bool   | Yes     | false   | Auto-Close on button press 41 | btnsArray | array | No | [] | Swipe buttons array 42 | onOpen | func | Yes | | Callback when swipe is opened 43 | onClose | func | Yes | | Callback when swipe is closed 44 | onSwipeStart   | func   | Yes      | | Callback when swipe start 45 | onSwipeEnd   | func   | Yes      | | Callback when swipe end 46 | shouldBounceOnMount | bool | Yes | false | Bounce component on mount 47 | swipeThreshold | number | Yes | 30 | The minimum swipe distance required before fully animating the swipe 48 | isRTL | bool | Yes | false | True/false if the current language is right to left 49 | 50 | ##### Button props 51 | 52 | Prop | Type | Optional | Default | Description 53 | --------------- | ------ | -------- | --------- | ----------- 54 | props           | object | Yes     |           | Pass custom props to button component 55 | component       | string | Yes     | null   | Pass custom component to button 56 | onPress | func | Yes | null | Function executed onPress 57 | text | string | Yes | 'Click Me'| Text 58 | type | string | Yes | 'default' | Default, primary, secondary 59 | 60 | ## To Do 61 | 62 | If you have any amelioration: 63 | 64 | [https://github.com/magrinj/react-native-swipeable-view/issues](https://github.com/magrinj/react-native-swipeable-view/issues) 65 | -------------------------------------------------------------------------------- /components/NativeButton.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component, 3 | } from 'react'; 4 | 5 | import { 6 | TouchableWithoutFeedback, 7 | TouchableNativeFeedback, 8 | TouchableHighlight, 9 | Text, 10 | StyleSheet, 11 | Platform, 12 | View, 13 | } from 'react-native'; 14 | 15 | import PropTypes from 'prop-types'; 16 | 17 | const styles = StyleSheet.create({ 18 | button: { 19 | flexDirection: 'row', 20 | alignSelf: 'stretch', 21 | justifyContent: 'center', 22 | }, 23 | textButton: { 24 | fontSize: 14, 25 | alignSelf: 'center', 26 | }, 27 | opacity: { 28 | opacity: 0.8, 29 | }, 30 | }); 31 | 32 | class NativeButton extends Component { 33 | 34 | static propTypes = { 35 | // Extract parent props 36 | ...TouchableWithoutFeedback.propTypes, 37 | textStyle: Text.propTypes.style, 38 | disabledStyle: Text.propTypes.style, 39 | children: PropTypes.node.isRequired, 40 | underlayColor: PropTypes.string, 41 | background: (TouchableNativeFeedback.propTypes) ? TouchableNativeFeedback.propTypes.background : PropTypes.any, 42 | }; 43 | 44 | static defaultProps = { 45 | textStyle: null, 46 | disabledStyle: null, 47 | underlayColor: null, 48 | }; 49 | 50 | static isAndroid = (Platform.OS === 'android'); 51 | 52 | constructor(props) { 53 | super(props); 54 | } 55 | 56 | setNativeProps(props) { 57 | if (this.refs.TouchableNativeFeedbackChild) { 58 | this.refs.TouchableNativeFeedbackChild.setNativeProps(props); 59 | } 60 | 61 | if (this.refs.TouchableHighlightChild) { 62 | this.refs.TouchableHighlightChild.setNativeProps(props); 63 | } 64 | } 65 | 66 | _renderText() { 67 | // If children is not a string don't wrapp it in a Text component 68 | if (typeof this.props.children !== 'string') { 69 | return this.props.children; 70 | } 71 | 72 | return ( 73 | 74 | { this.props.children } 75 | 76 | ); 77 | } 78 | 79 | render() { 80 | const disabledStyle = this.props.disabled ? (this.props.disabledStyle || styles.opacity) : {}; 81 | 82 | // Extract Button props 83 | let buttonProps = { 84 | accessibilityComponentType: this.props.accessibilityComponentType, 85 | accessibilityTraits: this.props.accessibilityTraits, 86 | accessible: this.props.accessible, 87 | delayLongPress: this.props.delayLongPress, 88 | delayPressIn: this.props.delayPressIn, 89 | delayPressOut: this.props.delayPressOut, 90 | disabled: this.props.disabled, 91 | hitSlop: this.props.hitSlop, 92 | onLayout: this.props.onLayout, 93 | onPress: this.props.onPress, 94 | onPressIn: this.props.onPressIn, 95 | onPressOut: this.props.onPressOut, 96 | onLongPress: this.props.onLongPress, 97 | pressRetentionOffset: this.props.pressRetentionOffset, 98 | }; 99 | 100 | // Render Native Android Button 101 | if (NativeButton.isAndroid) { 102 | buttonProps = Object.assign(buttonProps, { 103 | background: this.props.background || TouchableNativeFeedback.SelectableBackground(), 104 | }); 105 | 106 | return ( 107 | 109 | 110 | {this._renderText()} 111 | 112 | 113 | ); 114 | } 115 | 116 | // Render default button 117 | return ( 118 | 122 | 123 | { this._renderText() } 124 | 125 | 126 | ); 127 | } 128 | 129 | } 130 | 131 | export default NativeButton; 132 | -------------------------------------------------------------------------------- /components/btn.js: -------------------------------------------------------------------------------- 1 | import NativeButton from './NativeButton'; 2 | 3 | import React, { 4 | Component, 5 | } from 'react'; 6 | 7 | import { 8 | StyleSheet, 9 | Text, 10 | TouchableNativeFeedback, 11 | TouchableWithoutFeedback, 12 | View, 13 | } from 'react-native'; 14 | 15 | import PropTypes from 'prop-types'; 16 | 17 | const styles = StyleSheet.create({ 18 | btn: { 19 | backgroundColor: '#b6bec0', 20 | flex: 1, 21 | justifyContent: 'center', 22 | overflow: 'hidden', 23 | }, 24 | btnDanger: { 25 | backgroundColor: '#FF3B30', 26 | }, 27 | btnPrimary: { 28 | backgroundColor: '#006fff', 29 | }, 30 | btnSecondary: { 31 | backgroundColor: '#fd9427', 32 | }, 33 | btnSuccess: { 34 | backgroundColor: '#4cd965', 35 | }, 36 | btnText: { 37 | color: '#fff', 38 | }, 39 | }); 40 | 41 | class Btn extends Component { 42 | 43 | static propTypes = { 44 | ...TouchableWithoutFeedback.propTypes, 45 | textStyle: Text.propTypes.style, 46 | disabledStyle: Text.propTypes.style, 47 | underlayColor: PropTypes.string, 48 | background: (TouchableNativeFeedback.propTypes) ? TouchableNativeFeedback.propTypes.background : PropTypes.any, 49 | panDimensions: PropTypes.object.isRequired, 50 | component: PropTypes.node, 51 | text: PropTypes.string, 52 | type: PropTypes.string, 53 | }; 54 | 55 | static defaultProps = { 56 | component: null, 57 | text: 'Click Me', 58 | type: null, 59 | }; 60 | 61 | setTypeStyle() { 62 | const { type } = this.props; 63 | 64 | switch (type) { 65 | case 'danger': 66 | case 'delete': 67 | return styles.btnDanger; 68 | case 'primary': 69 | return styles.btnPrimary; 70 | case 'secondary': 71 | return styles.btnSecondary; 72 | case 'success': 73 | return styles.btnSuccess; 74 | default: 75 | return {}; 76 | } 77 | } 78 | 79 | render() { 80 | const { panDimensions, style, text, component, type, ...btnProps } = this.props; 81 | 82 | // unused, but remove from btnProps 83 | type; 84 | 85 | return ( 86 | 87 | 91 | { 92 | component ? component : text 93 | } 94 | 95 | 96 | ); 97 | } 98 | } 99 | 100 | export default Btn; 101 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Btn from './components/btn'; 2 | 3 | import React, { 4 | Component, 5 | } from 'react'; 6 | 7 | import { 8 | Animated, 9 | PanResponder, 10 | StyleSheet, 11 | View, 12 | } from 'react-native'; 13 | 14 | import PropTypes from 'prop-types'; 15 | 16 | const styles = StyleSheet.create({ 17 | slideOutContainer: { 18 | position: 'absolute', 19 | top: 0, bottom: 0, 20 | left: 0, right: 0, 21 | overflow: 'hidden', 22 | }, 23 | btnsContainer: { 24 | flex: 1, 25 | flexDirection: 'row', 26 | justifyContent: 'flex-end', 27 | overflow: 'hidden', 28 | }, 29 | container: { 30 | flex: 1, 31 | flexDirection: 'row', 32 | }, 33 | }); 34 | 35 | // Position of the left of the swipable item when closed 36 | const CLOSED_LEFT_POSITION = 0; 37 | // Minimum swipe distance before we recognize it as such 38 | const HORIZONTAL_SWIPE_DISTANCE_THRESHOLD = 10; 39 | // Minimum swipe speed before we fully animate the user's action (open/close) 40 | const HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD = 0.3; 41 | // Factor to divide by to get slow speed; i.e. 4 means 1/4 of full speed 42 | const SLOW_SPEED_SWIPE_FACTOR = 4; 43 | // Time, in milliseconds, of how long the animated swipe should be 44 | const SWIPE_DURATION = 300; 45 | 46 | /** 47 | * On SwipeableListView mount, the 1st item will bounce to show users it's 48 | * possible to swipe 49 | */ 50 | const ON_MOUNT_BOUNCE_DELAY = 700; 51 | const ON_MOUNT_BOUNCE_DURATION = 400; 52 | 53 | // Distance left of closed position to bounce back when right-swiping from closed 54 | const RIGHT_SWIPE_BOUNCE_BACK_DISTANCE = 30; 55 | const RIGHT_SWIPE_BOUNCE_BACK_DURATION = 300; 56 | /** 57 | * Max distance of right swipe to allow (right swipes do functionally nothing). 58 | * Must be multiplied by SLOW_SPEED_SWIPE_FACTOR because gestureState.dx tracks 59 | * how far the finger swipes, and not the actual animation distance. 60 | */ 61 | const RIGHT_SWIPE_THRESHOLD = 30 * SLOW_SPEED_SWIPE_FACTOR; 62 | 63 | class SwipeableView extends Component { 64 | 65 | static propTypes = { 66 | children: PropTypes.any, 67 | isOpen: PropTypes.bool, 68 | onOpen: PropTypes.func, 69 | onClose: PropTypes.func, 70 | onSwipeEnd: PropTypes.func, 71 | onSwipeStart: PropTypes.func, 72 | autoClose: PropTypes.bool, 73 | // Should bounce the row on mount 74 | shouldBounceOnMount: PropTypes.bool, 75 | /** 76 | * A ReactElement that is unveiled when the user swipes 77 | */ 78 | btnsArray: PropTypes.array.isRequired, 79 | /** 80 | * The minimum swipe distance required before fully animating the swipe. If 81 | * the user swipes less than this distance, the item will return to its 82 | * previous (open/close) position. 83 | */ 84 | swipeThreshold: PropTypes.number, 85 | /** 86 | * True/false if the current language is right to left 87 | */ 88 | isRTL: PropTypes.bool, 89 | }; 90 | 91 | static defaultProps = { 92 | isOpen: false, 93 | onOpen: () => {}, 94 | onClose: () => {}, 95 | onSwipeEnd: () => {}, 96 | onSwipeStart: () => {}, 97 | swipeThreshold: 30, 98 | shouldBounceOnMount: false, 99 | autoClose: false, 100 | isRTL: false, 101 | }; 102 | 103 | constructor(props) { 104 | super(props); 105 | 106 | this._panResponder = {}; 107 | 108 | this._previousLeft = CLOSED_LEFT_POSITION; 109 | 110 | this.state = { 111 | btnWidthDefault: 0, 112 | btnWidths: [], 113 | width: 0, 114 | height: 0, 115 | currentLeft: new Animated.Value(this._previousLeft), 116 | /** 117 | * In order to render component A beneath component B, A must be rendered 118 | * before B. However, this will cause "flickering", aka we see A briefly 119 | * then B. To counter this, _isSwipeableViewRendered flag is used to set 120 | * component A to be transparent until component B is loaded. 121 | */ 122 | isSwipeableViewRendered: false, 123 | rowHeight: null, 124 | maxSwipeDistance: 0, 125 | }; 126 | 127 | this._swipeoutRef = null; 128 | } 129 | 130 | componentWillMount() { 131 | this._panResponder = PanResponder.create({ 132 | onMoveShouldSetPanResponderCapture: this._handleMoveShouldSetPanResponderCapture.bind(this), 133 | onPanResponderGrant: this._handlePanResponderGrant.bind(this), 134 | onPanResponderMove: this._handlePanResponderMove.bind(this), 135 | onPanResponderRelease: this._handlePanResponderEnd.bind(this), 136 | onPanResponderTerminationRequest: this._onPanResponderTerminationRequest.bind(this), 137 | onPanResponderTerminate: this._handlePanResponderEnd.bind(this), 138 | onShouldBlockNativeResponder: () => false, 139 | }); 140 | } 141 | 142 | componentDidMount() { 143 | const { shouldBounceOnMount } = this.props; 144 | 145 | if (shouldBounceOnMount) { 146 | /** 147 | * Do the on mount bounce after a delay because if we animate when other 148 | * components are loading, the animation will be laggy 149 | */ 150 | this._timeout = setTimeout(() => { 151 | this._animateBounceBack(ON_MOUNT_BOUNCE_DURATION); 152 | }, ON_MOUNT_BOUNCE_DELAY); 153 | } 154 | 155 | setTimeout(this._measureSwipeout.bind(this)); 156 | } 157 | 158 | componentWillUnmount() { 159 | if (this._timeout) { 160 | clearTimeout(this._timeout); 161 | } 162 | } 163 | 164 | componentWillReceiveProps(nextProps) { 165 | const { isOpen } = this.props; 166 | /** 167 | * We do not need an "animateOpen(noCallback)" because this animation is 168 | * handled internally by this component. 169 | */ 170 | if (isOpen && !nextProps.isOpen) { 171 | this._animateToClosedPosition(); 172 | } 173 | } 174 | 175 | shouldComponentUpdate(nextProps) { 176 | const { shouldBounceOnMount } = this.props; 177 | 178 | if (shouldBounceOnMount && !nextProps.shouldBounceOnMount) { 179 | // No need to rerender if SwipeableListView is disabling the bounce flag 180 | return false; 181 | } 182 | 183 | return true; 184 | } 185 | 186 | _onPanResponderTerminationRequest() { 187 | return false; 188 | } 189 | 190 | _animateTo(toValue, duration = SWIPE_DURATION, callback = () => {}) { 191 | Animated.timing( 192 | this.state.currentLeft, 193 | { 194 | duration, 195 | toValue, 196 | }, 197 | ).start(() => { 198 | this._previousLeft = toValue; 199 | callback(); 200 | }); 201 | } 202 | 203 | _animateToClosedPosition(duration = SWIPE_DURATION) { 204 | this._animateTo(CLOSED_LEFT_POSITION, duration); 205 | } 206 | 207 | _animateToClosedPositionDuringBounce() { 208 | this._animateToClosedPosition(RIGHT_SWIPE_BOUNCE_BACK_DURATION); 209 | } 210 | 211 | _animateToOpenPosition() { 212 | const { isRTL } = this.props; 213 | const { maxSwipeDistance } = this.state; 214 | this._animateTo(isRTL ? maxSwipeDistance : -maxSwipeDistance); 215 | } 216 | 217 | _animateToOpenPositionWith(speed, distMoved) { 218 | const { isRTL } = this.props; 219 | const { maxSwipeDistance } = this.state; 220 | /** 221 | * Ensure the speed is at least the set speed threshold to prevent a slow 222 | * swiping animation 223 | */ 224 | speed = ( 225 | speed > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD ? 226 | speed : 227 | HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD 228 | ); 229 | /** 230 | * Calculate the duration the row should take to swipe the remaining distance 231 | * at the same speed the user swiped (or the speed threshold) 232 | */ 233 | const duration = Math.abs((maxSwipeDistance - Math.abs(distMoved)) / speed); 234 | this._animateTo(isRTL ? maxSwipeDistance : -maxSwipeDistance, duration); 235 | } 236 | 237 | _animateBounceBack(duration) { 238 | /** 239 | * When swiping right, we want to bounce back past closed position on release 240 | * so users know they should swipe right to get content. 241 | */ 242 | const { isRTL } = this.props; 243 | const swipeBounceBackDistance = isRTL ? 244 | -RIGHT_SWIPE_BOUNCE_BACK_DISTANCE : 245 | RIGHT_SWIPE_BOUNCE_BACK_DISTANCE; 246 | this._animateTo( 247 | -swipeBounceBackDistance, 248 | duration, 249 | this._animateToClosedPositionDuringBounce.bind(this), 250 | ); 251 | } 252 | 253 | _shouldAnimateRemainder(gestureState) { 254 | /** 255 | * If user has swiped past a certain distance, animate the rest of the way 256 | * if they let go 257 | */ 258 | return ( 259 | Math.abs(gestureState.dx) > this.props.swipeThreshold || 260 | gestureState.vx > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD 261 | ); 262 | } 263 | 264 | _handlePanResponderEnd(event, gestureState) { 265 | const { onOpen, onClose, onSwipeEnd, isRTL } = this.props; 266 | const horizontalDistance = isRTL ? -gestureState.dx : gestureState.dx; 267 | 268 | if (this._isSwipingRightFromClosed(gestureState)) { 269 | onOpen && onOpen(); 270 | this._animateBounceBack(RIGHT_SWIPE_BOUNCE_BACK_DURATION); 271 | } else if (this._shouldAnimateRemainder(gestureState)) { 272 | if (horizontalDistance < 0) { 273 | // Swiped left 274 | onOpen && onOpen(); 275 | this._animateToOpenPositionWith(gestureState.vx, horizontalDistance); 276 | } else { 277 | // Swiped right 278 | onClose && onClose(); 279 | this._animateToClosedPosition(); 280 | } 281 | } else { 282 | if (this._previousLeft === CLOSED_LEFT_POSITION) { 283 | this._animateToClosedPosition(); 284 | } else { 285 | this._animateToOpenPosition(); 286 | } 287 | } 288 | 289 | onSwipeEnd && onSwipeEnd(); 290 | } 291 | 292 | _swipeFullSpeed(gestureState) { 293 | this.state.currentLeft.setValue(this._previousLeft + gestureState.dx); 294 | } 295 | 296 | _swipeSlowSpeed(gestureState) { 297 | this.state.currentLeft.setValue( 298 | this._previousLeft + gestureState.dx / SLOW_SPEED_SWIPE_FACTOR 299 | ); 300 | } 301 | 302 | _isSwipingRightFromClosed(gestureState) { 303 | const { isRTL } = this.props; 304 | const gestureStateDx = isRTL ? -gestureState.dx : gestureState.dx; 305 | return this._previousLeft === CLOSED_LEFT_POSITION && gestureStateDx > 0; 306 | } 307 | 308 | _isSwipingExcessivelyRightFromClosedPosition(gestureState) { 309 | /** 310 | * We want to allow a BIT of right swipe, to allow users to know that 311 | * swiping is available, but swiping right does not do anything 312 | * functionally. 313 | */ 314 | const { isRTL } = this.props; 315 | const gestureStateDx = isRTL ? -gestureState.dx : gestureState.dx; 316 | return ( 317 | this._isSwipingRightFromClosed(gestureState) && 318 | gestureStateDx > RIGHT_SWIPE_THRESHOLD 319 | ); 320 | } 321 | 322 | _handlePanResponderMove(event, gestureState) { 323 | const { onSwipeStart } = this.props; 324 | 325 | if (this._isSwipingExcessivelyRightFromClosedPosition(gestureState)) { 326 | return; 327 | } 328 | 329 | onSwipeStart && onSwipeStart(); 330 | 331 | if (this._isSwipingRightFromClosed(gestureState)) { 332 | this._swipeSlowSpeed(gestureState); 333 | } else { 334 | this._swipeFullSpeed(gestureState); 335 | } 336 | } 337 | 338 | _handlePanResponderGrant() { 339 | } 340 | 341 | _handleMoveShouldSetPanResponderCapture(event, gestureState) { 342 | // Decides whether a swipe is responded to by this component or its child 343 | return gestureState.dy < 10 && this._isValidSwipe(gestureState); 344 | } 345 | 346 | _isValidSwipe(gestureState) { 347 | return Math.abs(gestureState.dx) > HORIZONTAL_SWIPE_DISTANCE_THRESHOLD; 348 | } 349 | 350 | _btnWidth(btn) { 351 | const hasCustomWidth = btn.props && btn.props.style && btn.props.style.width; 352 | return hasCustomWidth ? btn.props.style.width : false; 353 | } 354 | 355 | _btnsWidthTotal(width, group) { 356 | const customWidths = []; 357 | 358 | group && group.forEach(btn => { 359 | this._btnWidth(btn) ? customWidths.push(this._btnWidth(btn)) : null; 360 | }); 361 | 362 | const customWidthTotal = customWidths.reduce((a, b) => a + b, 0); 363 | const defaultWidth = (width - customWidthTotal) / (5 - customWidths.length); 364 | const defaultWidthsTotal = ((group ? group.length : 0) - customWidths.length) * defaultWidth; 365 | 366 | this.setState({ 367 | btnWidthDefault: defaultWidth, 368 | }); 369 | 370 | return customWidthTotal + defaultWidthsTotal; 371 | } 372 | 373 | _setBtnsWidth(btns) { 374 | const { btnWidthDefault } = this.state; 375 | const btnWidths = []; 376 | 377 | btns && btns.forEach(btn => { 378 | btnWidths.push(this._btnWidth(btn) ? this._btnWidth(btn) : btnWidthDefault); 379 | }); 380 | 381 | this.setState({ 382 | btnWidths, 383 | }); 384 | } 385 | 386 | _handleBtnPress(btn) { 387 | const { autoClose } = this.props; 388 | 389 | if (btn) { 390 | btn.onPress && btn.onPress(); 391 | (btn.autoClose || autoClose) && this._animateToClosedPosition(); 392 | } 393 | } 394 | 395 | _measureSwipeout() { 396 | if (this._swipeoutRef) { 397 | this._swipeoutRef.measure((a, b, width, height) => { 398 | const { btnsArray } = this.props; 399 | 400 | this.setState({ 401 | height: height, 402 | width: width, 403 | maxSwipeDistance: this._btnsWidthTotal(width, btnsArray), 404 | }); 405 | 406 | this._setBtnsWidth(btnsArray); 407 | }); 408 | } 409 | } 410 | 411 | _returnBtnDimensions() { 412 | const { height, width } = this.state; 413 | 414 | return { 415 | height: height, 416 | width: width, 417 | }; 418 | } 419 | 420 | _onSwipeableViewLayout(event) { 421 | this.setState({ 422 | isSwipeableViewRendered: true, 423 | rowHeight: event.nativeEvent.layout.height, 424 | }); 425 | } 426 | 427 | _renderSlideoutBtns() { 428 | const { btnWidths } = this.state; 429 | 430 | if (btnWidths.length <= 0) { 431 | return false; 432 | } 433 | 434 | return this.props.btnsArray.map((btn, i) => { 435 | const btnProps = btn.props ? btn.props : []; 436 | 437 | return ( 438 | this._handleBtnPress(btn)}/> 446 | ); 447 | }); 448 | } 449 | 450 | render() { 451 | const { isRTL } = this.props; 452 | // The view hidden behind the main view 453 | let btnsArray; 454 | if (this.state.isSwipeableViewRendered && this.state.rowHeight) { 455 | btnsArray = ( 456 | 457 | 458 | { this._renderSlideoutBtns() } 459 | 460 | 461 | ); 462 | } 463 | 464 | // The swipeable item 465 | const swipeableView = ( 466 | 469 | {this.props.children} 470 | 471 | ); 472 | 473 | return ( 474 | (this._swipeoutRef = ref) } 476 | {...this._panResponder.panHandlers} 477 | collapsable={false}> 478 | {btnsArray} 479 | {swipeableView} 480 | 481 | ); 482 | } 483 | 484 | } 485 | 486 | module.exports = SwipeableView; 487 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-swipeable-view", 3 | "version": "1.0.0", 4 | "description": "iOS-style swipeout buttons behind component", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+ssh://git@github.com:magrinj/react-native-swipeable-view.git" 12 | }, 13 | "keywords": [ 14 | "react-native-component", 15 | "react-native", 16 | "react-component", 17 | "swipeable", 18 | "ios", 19 | "android", 20 | "swipeout", 21 | "button", 22 | "swipe", 23 | "ui" 24 | ], 25 | "author": "Jeremy Magrin", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/magrinj/react-native-swipeable-view/issues" 29 | }, 30 | "homepage": "https://github.com/magrinj/react-native-swipeable-view", 31 | "peerDependencies": { 32 | "react-native": ">=0.20.0" 33 | }, 34 | "devDependencies": { 35 | "babel-eslint": "^7.1.1", 36 | "eslint": "^3.18.0", 37 | "eslint-plugin-flowtype": "^2.30.3", 38 | "eslint-plugin-react": "^6.10.2" 39 | } 40 | } 41 | --------------------------------------------------------------------------------