├── app ├── actions │ ├── actionTypes.js │ ├── fieldsAction.js │ └── counter.js ├── common │ ├── Toast.js │ ├── FetchLoaderView.js │ ├── Loader.js │ ├── FetchErrorView.js │ ├── Grid.js │ ├── FetchBlankView.js │ ├── FetchLoginView.js │ ├── TabIcon.js │ ├── Error.js │ ├── ModelAlert.js │ ├── CheckBox.js │ ├── MoreMenu.js │ ├── ImageLoader.js │ ├── PraiseCheckBox.js │ ├── SelectPickerView.js │ ├── Password.js │ ├── StarRating.js │ ├── Button.js │ ├── KeyboardSpacer.js │ ├── AutoGrowingTextInput.js │ ├── Search.js │ ├── NavigationBar.js │ ├── DatePicker.js │ ├── PickerAndroid.js │ └── Popover.js ├── reducers │ ├── rootReducer.js │ ├── counter.js │ └── fieldsReducer.js ├── util │ ├── RequestURL.js │ ├── colors.js │ ├── CommonUtil.js │ ├── Cancelable.js │ ├── dismissKeyboradDecorator.js │ ├── GlobalStorage.js │ ├── ArrayUtils.js │ ├── normalizeText.js │ └── FetchUtil.js ├── root.js ├── store │ └── configureStore.js ├── page │ └── Counter.js └── container │ └── app.js └── package.json /app/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | 2 | export const UPDATE_FIELDS = 'UPDATE_FIELDS'; 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/actions/fieldsAction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/6/24. 3 | */ 4 | import * as types from './actionTypes'; 5 | 6 | export let updateFields = (value) => { 7 | return { 8 | type: types.UPDATE_FIELDS, 9 | value: value 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /app/common/Toast.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/6/30. 3 | */ 4 | 5 | import RootToast from 'react-native-root-toast'; 6 | 7 | export default class Toast { 8 | static showTop(message) { 9 | RootToast.show(message, {position: RootToast.positions.TOP}); 10 | } 11 | } -------------------------------------------------------------------------------- /app/reducers/rootReducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/9/18. 3 | */ 4 | 5 | import { combineReducers } from 'redux' 6 | import fields from './fieldsReducer' 7 | import counter from './counter'; 8 | 9 | export default rootReducer = combineReducers({ 10 | fields, 11 | counter 12 | }) -------------------------------------------------------------------------------- /app/util/RequestURL.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 2016/10/21. 3 | */ 4 | const SERVER = 'http://it.sidlu.com/index.php'; 5 | 6 | // ----------------------------------------- ** 公共接口 ** ----------------------------------------------------- 7 | 8 | export const login = `${SERVER}/index/user/login`; //登录 9 | 10 | -------------------------------------------------------------------------------- /app/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { handleActions } from 'redux-actions'; 3 | 4 | const initialState = { counter: 0 }; 5 | 6 | const reducer = handleActions({ 7 | increment: state => ({counter: state.counter + 1}), 8 | decrement: state => ({counter: state.counter - 1}), 9 | }, initialState); 10 | 11 | export default reducer; 12 | -------------------------------------------------------------------------------- /app/reducers/fieldsReducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/6/24. 3 | */ 4 | 'use strict'; 5 | 6 | import * as types from '../actions/actionTypes'; 7 | 8 | const initialState = {}; 9 | 10 | export default function fields(state = initialState, action = {}) { 11 | switch (action.type) { 12 | case types.UPDATE_FIELDS: 13 | return {...state, ...action.value}; 14 | default: 15 | return state; 16 | } 17 | } -------------------------------------------------------------------------------- /app/util/colors.js: -------------------------------------------------------------------------------- 1 | export default { 2 | primary: '#9E9E9E', 3 | primary1: '#4d86f7', 4 | primary2: '#6296f9', 5 | secondary: '#8F0CE8', 6 | secondary2: '#00B233', 7 | secondary3: '#00FF48', 8 | grey0: '#393e42', 9 | grey1: '#43484d', 10 | grey2: '#5e6977', 11 | grey3: '#86939e', 12 | grey4: '#bdc6cf', 13 | grey5: '#e1e8ee', 14 | dkGreyBg: '#232323', 15 | greyOutline: '#cbd2d9', 16 | searchBg: '#303337', 17 | disabled: '#dadee0', 18 | white: '#ffffff', 19 | } 20 | -------------------------------------------------------------------------------- /app/util/CommonUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/7/20. 3 | */ 4 | import { 5 | Dimensions, 6 | PixelRatio, 7 | Platform 8 | } from 'react-native'; 9 | 10 | export const Screen = Dimensions.get('window'); 11 | 12 | export const pixelRation = PixelRatio.get(); 13 | 14 | export const PlatformiOS = Platform.OS === 'ios'; 15 | 16 | export const pixel1 = 1 / pixelRation; 17 | 18 | export function naviGoBack(navigator) { 19 | if (navigator && navigator.getCurrentRoutes().length > 1) { 20 | navigator.pop(); 21 | return true; 22 | } 23 | return false; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /app/util/Cancelable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cancelable 3 | * @flow 4 | **/ 5 | 6 | export default function makeCancelable(promise){ 7 | let hasCanceled_ = false; 8 | const wrappedPromise = new Promise((resolve, reject) => { 9 | promise.then((val) => 10 | hasCanceled_ ? reject({isCanceled: true}) : resolve(val) 11 | ); 12 | promise.catch((error) => 13 | hasCanceled_ ? reject({isCanceled: true}) : reject(error) 14 | ); 15 | }); 16 | 17 | return { 18 | promise: wrappedPromise, 19 | cancel() { 20 | hasCanceled_ = true; 21 | }, 22 | }; 23 | } -------------------------------------------------------------------------------- /app/actions/counter.js: -------------------------------------------------------------------------------- 1 | import {createAction} from 'redux-actions'; 2 | 3 | export const increment = createAction('increment'); 4 | 5 | export const decrement = createAction('decrement'); 6 | 7 | export function incrementIfOdd() { 8 | return (dispatch, getState) => { 9 | const {counter} = getState(); 10 | 11 | if (counter.counter % 2 === 0) { 12 | return; 13 | } 14 | 15 | dispatch(decrement()); 16 | }; 17 | } 18 | 19 | export function incrementAsync(delay = 1000) { 20 | return (dispatch) => { 21 | setTimeout(() => { 22 | dispatch(increment()); 23 | }, delay); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /app/root.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/9/18. 3 | */ 4 | import React from 'react'; 5 | import {Provider} from 'react-redux'; 6 | import App from './container/app'; 7 | 8 | import configureStore from './store/configureStore'; 9 | 10 | const store = configureStore(); 11 | 12 | if (!__DEV__) { 13 | global.console = { 14 | info: () => {}, 15 | log: () => {}, 16 | warn: () => {}, 17 | error: () => {}, 18 | }; 19 | } 20 | 21 | export default class root extends React.Component { 22 | 23 | render() { 24 | return ( 25 | 26 | 27 | 28 | ); 29 | } 30 | } -------------------------------------------------------------------------------- /app/util/dismissKeyboradDecorator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 2017/1/16. 3 | * 点击空白处,自动隐藏键盘高阶组件 4 | */ 5 | 6 | import React, { Component } from 'react' 7 | import {TouchableWithoutFeedback, View} from 'react-native' 8 | import dismissKeyboard from 'react-native/Libraries/Utilities/dismissKeyboard' 9 | 10 | const Decorator = WrappedCompoent => class extends Component { 11 | render() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | }; 21 | 22 | export default Decorator -------------------------------------------------------------------------------- /app/common/FetchLoaderView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/7/18. 3 | */ 4 | import React, { Component } from 'react'; 5 | import { 6 | StyleSheet, 7 | View 8 | } from 'react-native'; 9 | 10 | import Spinner from 'react-native-spinkit' 11 | 12 | //['CircleFlip', 'Bounce', 'Wave', 'WanderingCubes', 'Pulse', 'ChasingDots', 'ThreeBounce', 'Circle', '9CubeGrid', 'WordPress', 'FadingCircle', 'FadingCircleAlt', 'Arc', 'ArcAlt'], 13 | 14 | export default class FetchLoaderView extends Component { 15 | 16 | render() { 17 | return ( 18 | 19 | 20 | 21 | ); 22 | } 23 | } 24 | 25 | const styles = StyleSheet.create({ 26 | loading: { 27 | justifyContent: 'center', 28 | alignItems: 'center', 29 | flex: 1 30 | } 31 | }); -------------------------------------------------------------------------------- /app/util/GlobalStorage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/6/30. 3 | * 本地存储 4 | */ 5 | 'use strict'; 6 | 7 | import Storage from 'react-native-storage'; 8 | import { AsyncStorage } from 'react-native'; 9 | 10 | let defaultStorage = new Storage({ 11 | // 最大容量,默认值1000条数据循环存储 12 | size: 1000, 13 | // 如果不指定则数据只会保存在内存中,重启后即丢失 14 | storageBackend: AsyncStorage, 15 | // 数据过期时间,默认一整天(1000 * 3600 * 24秒) 16 | defaultExpires: null, 17 | // 读写时在内存中缓存数据。默认启用。 18 | enableCache: true 19 | }); 20 | 21 | export default class UserDefaults { 22 | 23 | static storage = defaultStorage; 24 | 25 | static setObject(key, value) { 26 | defaultStorage.save({ 27 | key: key, 28 | rawData: value 29 | }); 30 | } 31 | 32 | static objectForKey(key, action) { 33 | defaultStorage.load({ 34 | key: key 35 | }).then(ret => { 36 | action(ret); 37 | }).catch(err => { 38 | action(null); 39 | }); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/common/Loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/6/13. 3 | * 封装的加载中视图 4 | */ 5 | import { View, Platform} from 'react-native'; 6 | import RootSiblings from 'react-native-root-siblings'; 7 | import Spinner from 'react-native-spinkit' 8 | 9 | export default class Loader { 10 | 11 | static show = (backgroundColor, size, color, type) => { 12 | size = size || 35; 13 | color = color || '#B19372'; 14 | type = type || 'Wave'; 15 | backgroundColor = backgroundColor || 'rgba(230,230,230,0.1)'; 16 | this.sibling = new RootSiblings( 17 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | static hide = () => { 27 | if (this.sibling) { 28 | this.sibling.destroy(); 29 | } 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /app/store/configureStore.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import {Platform, AsyncStorage} from 'react-native'; 3 | import {createStore, applyMiddleware, compose} from 'redux'; 4 | import thunk from 'redux-thunk'; 5 | import {persistStore, autoRehydrate} from 'redux-persist'; 6 | import devTools from 'remote-redux-devtools'; 7 | import rootReducer from '../reducers/rootReducer'; 8 | 9 | let enhancer; 10 | export default function confirgureStore(initialState) { 11 | if (__DEV__) { 12 | enhancer = compose( 13 | applyMiddleware(thunk), 14 | devTools({ 15 | name: Platform.OS, 16 | hostname: 'localhost', 17 | port: 5678, 18 | }), 19 | )(createStore); 20 | } else { 21 | enhancer = compose( 22 | applyMiddleware(thunk), 23 | )(createStore); 24 | } 25 | 26 | const store = autoRehydrate(initialState)(enhancer)(rootReducer); 27 | // const store = createStore(rootReducer, initialState, enhancer); 28 | 29 | let opt = { 30 | storage: AsyncStorage, 31 | transform: [], 32 | //whitelist: ['userStore'], 33 | }; 34 | persistStore(store, opt); 35 | return store; 36 | } 37 | -------------------------------------------------------------------------------- /app/common/FetchErrorView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/7/18. 3 | */ 4 | import React, { Component, PropTypes } from 'react'; 5 | import { 6 | StyleSheet, 7 | Text, 8 | TouchableOpacity, 9 | Image 10 | } from 'react-native'; 11 | 12 | export default class FetchErrorView extends Component { 13 | 14 | static PropTypes = { 15 | text: PropTypes.string, 16 | onPress: PropTypes.fun 17 | }; 18 | 19 | static defaultProps = { 20 | onPress: () => {}, 21 | text: '点击屏幕重新加载' 22 | }; 23 | 24 | render() { 25 | //解构 26 | const {onPress, text} = this.props; 27 | 28 | return ( 29 | 33 | 37 | {text} 38 | 39 | ); 40 | } 41 | } 42 | 43 | const styles = StyleSheet.create({ 44 | button: { 45 | flex:1, 46 | alignItems: 'center', 47 | justifyContent: 'center' 48 | }, 49 | text: { 50 | color: '#cdcdcd', 51 | textAlign: 'center', 52 | fontWeight: 'bold', 53 | marginTop: 20 54 | }, 55 | image: { 56 | width: 180, 57 | height: 186*0.61 58 | } 59 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "IPconnect", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node node_modules/react-native/local-cli/cli.js start", 7 | "test": "jest", 8 | "android-release": "cd android && ./gradlew assembleRelease", 9 | "android-apk-manual-install": "adb install ~/temp/app-release.apk", 10 | "postinstall": "remotedev-debugger --hostname localhost --port 5678 --injectserver" 11 | }, 12 | "dependencies": { 13 | "react": "15.4.2", 14 | "react-native": "0.39.2", 15 | "react-native-button": "^1.8.2", 16 | "react-native-drawer": "^2.3.0", 17 | "react-native-inputscrollview": "^2.0.1", 18 | "react-native-modalbox": "^1.3.9", 19 | "react-native-root-siblings": "^1.1.2", 20 | "react-native-root-toast": "^1.0.3", 21 | "react-native-router-flux": "^3.37.0", 22 | "react-native-scrollable-tab-view": "^0.7.2", 23 | "react-native-spinkit": "^0.1.5", 24 | "react-native-storage": "^0.1.5", 25 | "react-native-vector-icons": "^3.0.0", 26 | "react-redux": "^5.0.2", 27 | "redux": "^3.6.0", 28 | "redux-actions": "^1.2.1", 29 | "redux-persist": "^4.4.0", 30 | "redux-thunk": "^2.2.0" 31 | }, 32 | "devDependencies": { 33 | "babel-jest": "18.0.0", 34 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 35 | "babel-preset-react-native": "^1.9.1", 36 | "jest": "18.1.0", 37 | "react-test-renderer": "15.4.2", 38 | "remote-redux-devtools": "^0.5.7", 39 | "remote-redux-devtools-on-debugger": "^0.7.0" 40 | }, 41 | "jest": { 42 | "preset": "react-native" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/common/Grid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 2016/12/8. 3 | */ 4 | 5 | import React, { 6 | Component, 7 | PropTypes, 8 | } from 'react' 9 | import { 10 | View, 11 | StyleSheet, 12 | Dimensions, 13 | } from 'react-native' 14 | 15 | const {width: deviceWidth} = Dimensions.get('window'); 16 | const styles = StyleSheet.create({ 17 | container: { 18 | flexDirection: 'row', 19 | justifyContent: 'flex-start', 20 | alignItems: 'flex-start', 21 | flexWrap: 'wrap', 22 | }, 23 | }); 24 | 25 | export default class Grid extends Component { 26 | 27 | static propTypes = { 28 | rowWidth: PropTypes.number, 29 | columnCount: PropTypes.number.isRequired, 30 | dataSource: PropTypes.array.isRequired, 31 | renderCell: PropTypes.func.isRequired, 32 | style: View.propTypes.style, 33 | }; 34 | 35 | constructor(props) { 36 | super(props); 37 | this._columnWidth = (props.rowWidth || deviceWidth) / props.columnCount 38 | } 39 | 40 | render() { 41 | return ( 42 | 43 | {this._renderCells()} 44 | 45 | ) 46 | } 47 | 48 | _renderCells() { 49 | return this.props.dataSource.map((data, index, dataList) => { 50 | return ( 51 | 52 | {this.props.renderCell(data, index, dataList)} 53 | 54 | ) 55 | }) 56 | } 57 | } -------------------------------------------------------------------------------- /app/common/FetchBlankView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 2016/12/19. 3 | */ 4 | import React, { Component, PropTypes } from 'react'; 5 | import { 6 | StyleSheet, 7 | Text, 8 | TouchableOpacity, 9 | Image 10 | } from 'react-native'; 11 | 12 | export default class FetchBlankView extends Component { 13 | 14 | static PropTypes = { 15 | onPress: PropTypes.fun, 16 | type: PropTypes.number 17 | }; 18 | 19 | static defaultProps = { 20 | onPress: () => {}, 21 | type: 0 22 | }; 23 | 24 | constructor(props) { 25 | super(props); 26 | 27 | this.blanks = [ 28 | 29 | ] 30 | } 31 | 32 | render() { 33 | 34 | const {onPress, type} = this.props; 35 | 36 | return ( 37 | 41 | 45 | {this.blanks[type].text} 46 | 47 | ); 48 | } 49 | } 50 | 51 | const styles = StyleSheet.create({ 52 | button: { 53 | flex:1, 54 | alignItems: 'center', 55 | justifyContent: 'center' 56 | }, 57 | text: { 58 | color: '#A0C5ED', 59 | textAlign: 'center', 60 | fontWeight: 'bold', 61 | marginTop: 20, 62 | fontSize: 15 63 | }, 64 | image: { 65 | width: 60, 66 | height: 60 67 | } 68 | }); -------------------------------------------------------------------------------- /app/common/FetchLoginView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/7/18. 3 | */ 4 | import React, {Component, PropTypes} from 'react'; 5 | import { 6 | StyleSheet, 7 | Text, 8 | TouchableOpacity, 9 | View 10 | } from 'react-native'; 11 | 12 | export default class FetchLoginView extends Component { 13 | 14 | static PropTypes = { 15 | onPress: PropTypes.fun 16 | }; 17 | 18 | static defaultProps = { 19 | onPress: () => {} 20 | }; 21 | 22 | render() { 23 | 24 | const {onPress} = this.props; 25 | 26 | return ( 27 | 28 | 账号异常 29 | 33 | 登录 34 | 35 | 36 | ); 37 | } 38 | } 39 | 40 | const styles = StyleSheet.create({ 41 | button: { 42 | width: 100, 43 | height: 32, 44 | backgroundColor: '#3A8AE3', 45 | borderRadius: 16, 46 | overflow: 'hidden', 47 | justifyContent: 'center', 48 | alignItems: 'center', 49 | }, 50 | text: { 51 | color: '#FFF', 52 | fontWeight: 'bold', 53 | }, 54 | title: { 55 | color: '#787878', 56 | fontSize: 13, 57 | marginBottom: 10 58 | }, 59 | image: { 60 | width: 180, 61 | marginTop: 68 62 | }, 63 | container: { 64 | justifyContent: 'center', 65 | alignItems: 'center', 66 | flex: 1, 67 | }, 68 | }); -------------------------------------------------------------------------------- /app/common/TabIcon.js: -------------------------------------------------------------------------------- 1 | // import React, { 2 | // PropTypes, 3 | // } from 'react'; 4 | // import { 5 | // Text, 6 | // Image, 7 | // View 8 | // } from 'react-native'; 9 | // 10 | // const propTypes = { 11 | // selected: PropTypes.bool, 12 | // title: PropTypes.string, 13 | // }; 14 | // 15 | // const TabIcon = (props) => ( 16 | // 17 | // {props.image ? : null} 18 | // {props.title ? 21 | // {props.title} 22 | // : null 23 | // } 24 | // 25 | // ); 26 | // 27 | // TabIcon.propTypes = propTypes; 28 | // 29 | // export default TabIcon; 30 | import React from 'react'; 31 | import { 32 | View, 33 | Text, 34 | StyleSheet 35 | } from 'react-native'; 36 | import Icon from 'react-native-vector-icons/Ionicons'; 37 | 38 | export default class TabIcon extends React.Component { 39 | render() { 40 | return ( 41 | 42 | 47 | 48 | {this.props.title} 49 | 50 | 51 | ); 52 | } 53 | } 54 | 55 | const styles = StyleSheet.create({ 56 | container: { 57 | flexDirection: 'column', 58 | alignItems: 'center' 59 | }, 60 | title: { 61 | fontSize: 14 62 | } 63 | }); -------------------------------------------------------------------------------- /app/common/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, Text, StyleSheet, Animated, Dimensions} from "react-native"; 3 | import Button from "react-native-button"; 4 | import {Actions} from "react-native-router-flux"; 5 | 6 | var { 7 | height: deviceHeight 8 | } = Dimensions.get("window"); 9 | 10 | var styles = StyleSheet.create({ 11 | container: { 12 | position: "absolute", 13 | top:0, 14 | bottom:0, 15 | left:0, 16 | right:0, 17 | backgroundColor:"transparent", 18 | justifyContent: "center", 19 | alignItems: "center", 20 | }, 21 | }); 22 | 23 | export default class extends React.Component { 24 | constructor(props){ 25 | super (props); 26 | 27 | this.state = { 28 | offset: new Animated.Value(-deviceHeight) 29 | }; 30 | } 31 | 32 | componentDidMount() { 33 | Animated.timing(this.state.offset, { 34 | duration: 600, 35 | toValue: 0 36 | }).start(); 37 | } 38 | 39 | closeModal() { 40 | Animated.timing(this.state.offset, { 41 | duration: 600, 42 | toValue: -deviceHeight 43 | }).start(Actions.pop); 44 | } 45 | 46 | render(){ 47 | return ( 48 | 50 | 55 | {this.props.data} 56 | 57 | 58 | 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/util/ArrayUtils.js: -------------------------------------------------------------------------------- 1 | export default class ArrayUtils { 2 | /** 3 | * 更新数组,若item已存在则将其从数组中删除,若不存在则将其添加到数组 4 | * **/ 5 | static updateArray(array, item) { 6 | for (let i = 0, len = array.length; i < len; i++) { 7 | let temp = array[i]; 8 | if (item === temp) { 9 | array.splice(i, 1); 10 | return; 11 | } 12 | } 13 | array.push(item); 14 | } 15 | 16 | static unique(arr){ 17 | let tmp = {}; 18 | 19 | arr.map((v) => { 20 | tmp[v] = 1 21 | }); 22 | 23 | let tmparr = []; 24 | 25 | for(let n in tmp){ 26 | tmparr.push(n); 27 | } 28 | return tmparr; 29 | 30 | } 31 | 32 | /** 33 | * 向数组中添加元素,若元素与存在则不添加 34 | * **/ 35 | static add(array, item) { 36 | if (!array)return; 37 | for (let i = 0, l = array.length; i < l; i++) { 38 | if (array === array[i])return; 39 | } 40 | array.push(item); 41 | } 42 | 43 | /** 44 | * 将数组中指定元素移除 45 | * **/ 46 | static remove(array, item) { 47 | if (!array)return; 48 | for (let i = 0, l = array.length; i < l; i++) { 49 | if (item === array[i]) array.splice(i, 1); 50 | } 51 | } 52 | 53 | /** 54 | * clone 数组 55 | * @return Array 新的数组 56 | * */ 57 | static clone(from) { 58 | if (!from)return []; 59 | let newArray = []; 60 | for (let i = 0, l = from.length; i < l; i++) { 61 | newArray[i] = from[i]; 62 | } 63 | return newArray; 64 | } 65 | 66 | /** 67 | * 判断两个数组的是否相等 68 | * @return boolean true 数组长度相等且对应元素相等 69 | * */ 70 | static isEqual(arr1, arr2) { 71 | if (!(arr1 && arr2))return false; 72 | if (arr1.length != arr2.length)return false; 73 | for (let i = 0, l = arr1.length; i < l; i++) { 74 | if (arr1[i] != arr2[i])return false; 75 | } 76 | return true; 77 | } 78 | } -------------------------------------------------------------------------------- /app/common/ModelAlert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 2016/12/22. 3 | */ 4 | import React, {Component} from 'react'; 5 | import { 6 | StyleSheet, 7 | Text, 8 | TouchableOpacity, 9 | Image, 10 | View, 11 | Linking 12 | } from 'react-native'; 13 | 14 | import Modal from 'react-native-modalbox' 15 | 16 | export default class ModelAlert extends Component { 17 | 18 | open = () => { 19 | this.refs.modal.open(); 20 | }; 21 | 22 | close = () => { 23 | this.refs.modal.close(); 24 | }; 25 | 26 | render() { 27 | 28 | const {title, onPress} = this.props; 29 | 30 | return ( 31 | 32 | {title} 33 | 34 | {onPress(true)}} 37 | > 38 | 39 | 40 | 41 | {onPress(false)}} 44 | > 45 | 46 | 47 | 48 | 49 | ); 50 | } 51 | } 52 | 53 | const styles = StyleSheet.create({ 54 | modal: { 55 | justifyContent: 'center', 56 | alignItems: 'center', 57 | height: 200, 58 | width: 260, 59 | backgroundColor: '#FFF', 60 | borderRadius: 5 61 | }, 62 | text: { 63 | color: "#1E1E1E", 64 | fontSize: 14, 65 | fontWeight: '400' 66 | }, 67 | button: { 68 | alignItems: 'center', 69 | justifyContent: 'center', 70 | marginTop: 26, 71 | backgroundColor:'#f4f4f4', 72 | height:40, 73 | width:160, 74 | borderRadius: 5 75 | }, 76 | buttonText: { 77 | color: "#1E1E1E", 78 | fontSize: 14, 79 | } 80 | }); 81 | 82 | -------------------------------------------------------------------------------- /app/page/Counter.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {StyleSheet, Text, View} from 'react-native'; 3 | import {bindActionCreators} from 'redux'; 4 | import {connect} from 'react-redux'; 5 | import Icon from 'react-native-vector-icons/FontAwesome'; 6 | import {Actions} from "react-native-router-flux"; 7 | 8 | import * as CounterActions from '../actions/counter'; 9 | 10 | @connect( 11 | state => ({ 12 | counter: state.counter, 13 | }), 14 | dispatch => bindActionCreators({...CounterActions}, dispatch), 15 | ) 16 | export default class Counter extends Component { 17 | 18 | static propTypes = { 19 | increment: PropTypes.func.isRequired, 20 | incrementIfOdd: PropTypes.func.isRequired, 21 | incrementAsync: PropTypes.func.isRequired, 22 | decrement: PropTypes.func.isRequired, 23 | // counter: PropTypes.instanceOf(Immutable.Map).isRequired, 24 | }; 25 | 26 | render() { 27 | const {increment, incrementIfOdd, decrement, incrementAsync, counter} = this.props; 28 | return ( 29 | 30 | Clicked: {counter.counter} times 31 | 32 | Increment 33 | 34 | 35 | Increment 36 | 37 | 38 | Increment if odd 39 | 40 | 41 | { 43 | incrementAsync(); 44 | Actions.error("Error message") 45 | } 46 | }>Increment async 47 | 48 | 49 | ); 50 | } 51 | } 52 | 53 | const styles = StyleSheet.create({ 54 | text: { 55 | fontSize: 20, 56 | textAlign: 'center', 57 | margin: 10, 58 | }, 59 | icon: { 60 | margin: 10, 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /app/common/CheckBox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/7/21. 3 | */ 4 | import React, {Component, PropTypes} from 'react'; 5 | 6 | import { 7 | View, 8 | StyleSheet, 9 | Text, 10 | TouchableOpacity, 11 | Image 12 | } from 'react-native' 13 | 14 | export default class CheckBox extends Component { 15 | 16 | static PropTypes = { 17 | onCheck: PropTypes.func, 18 | style: PropTypes.object, 19 | imageStyle: PropTypes.object, 20 | checked: PropTypes.bool, 21 | image: PropTypes.object, 22 | selectImage: PropTypes.object, 23 | onPress: PropTypes.func, 24 | isDisabled: PropTypes.bool, 25 | }; 26 | 27 | static defaultProps = { 28 | onCheck: () => { 29 | }, 30 | onPress: () => {}, 31 | checked: false, 32 | isDisabled: false, 33 | }; 34 | 35 | constructor(props) { 36 | super(props); 37 | 38 | this.state = { 39 | checked: this.props.checked, 40 | } 41 | } 42 | 43 | componentWillReceiveProps(nextProps) { 44 | this.setState({ 45 | checked: nextProps.checked, 46 | }) 47 | } 48 | 49 | onSelect = () => { 50 | 51 | this.setState({ 52 | checked: !this.state.checked, 53 | }, ()=> { 54 | this.props.onCheck(this.state.checked); 55 | }); 56 | 57 | }; 58 | 59 | render() { 60 | const {image, selectImage,isDisabled} = this.props; 61 | return ( 62 | 63 | {isDisabled ? 64 | 65 | 69 | : 70 | 75 | 79 | 80 | } 81 | 82 | 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/common/MoreMenu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 更多菜单 3 | * @flow 4 | */ 5 | 'use strict'; 6 | import React, {Component,PropTypes} from 'react' 7 | import { 8 | ListView, 9 | StyleSheet, 10 | RefreshControl, 11 | TouchableHighlight, 12 | Text, 13 | Image, 14 | Linking, 15 | View, 16 | } from 'react-native' 17 | import Popover from "./Popover"; 18 | 19 | export default class MoreMenu extends Component { 20 | constructor(props) { 21 | super(props); 22 | this.state = { 23 | isVisible: false, 24 | buttonRect: {}, 25 | }; 26 | this.MORE_MENU =[ 27 | {id:0, title:'全部'}, 28 | {id:1, title:'理财'}, 29 | {id:2, title:'文化'}, 30 | {id:3, title:'教育'}, 31 | ] 32 | } 33 | 34 | static propTypes = { 35 | contentStyle: View.propTypes.style, 36 | menus:PropTypes.array, 37 | } 38 | 39 | open() { 40 | this.showPopover(); 41 | } 42 | 43 | showPopover() { 44 | 45 | this.setState({ 46 | isVisible: true, 47 | buttonRect: {x: 0, y: 10, width: 100, height: 100} 48 | }); 49 | 50 | } 51 | 52 | closePopover() { 53 | this.setState({ 54 | isVisible: false, 55 | }); 56 | if (typeof(this.props.onClose) == 'function')this.props.onClose(); 57 | } 58 | 59 | onMoreMenuSelect(tab) { 60 | this.closePopover(); 61 | if (typeof(this.props.onMoreMenuSelect) == 'function')this.props.onMoreMenuSelect(tab); 62 | 63 | } 64 | 65 | renderMoreView() { 66 | let view = this.closePopover()} 71 | contentStyle={{opacity:0.82,backgroundColor:'#343434'}} 72 | contentMarginRight={-30} 73 | > 74 | 75 | {this.MORE_MENU.map((result, i) => { 76 | return this.onMoreMenuSelect(result)} 77 | underlayColor='transparent'> 78 | 80 | {result.title} 81 | 82 | 83 | }) 84 | } 85 | 86 | 87 | ; 88 | return view; 89 | } 90 | 91 | render() { 92 | return (this.renderMoreView()); 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /app/util/normalizeText.js: -------------------------------------------------------------------------------- 1 | // 2 | // Method to normalize size of fonts across devices 3 | // 4 | // Some code taken from https://jsfiddle.net/97ty7yjk/ & 5 | // https://stackoverflow.com/questions/34837342/font-size-on-iphone-6s-plus 6 | // 7 | // author: @xiaoneng 8 | // date: 14/10/2016 9 | // version: 03 10 | // 11 | 12 | import { 13 | Dimensions, 14 | PixelRatio 15 | } from 'react-native'; 16 | 17 | const pixelRatio = PixelRatio.get(); 18 | const deviceHeight = Dimensions.get('window').height; 19 | const deviceWidth = Dimensions.get('window').width; 20 | 21 | // -- Testing Only -- 22 | // const fontScale = PixelRatio.getFontScale(); 23 | // const layoutSize = PixelRatio.getPixelSizeForLayoutSize(14); 24 | // console.log('normalizeText getPR ->', pixelRatio); 25 | // console.log('normalizeText getFS ->', fontScale); 26 | // console.log('normalizeText getDH ->', deviceHeight); 27 | // console.log('normalizeText getDW ->', deviceWidth); 28 | // console.log('normalizeText getPSFLS ->', layoutSize); 29 | 30 | const normalize = (size) => { 31 | if (pixelRatio === 2) { 32 | // iphone 5s and older Androids 33 | if (deviceWidth < 360) { 34 | return size * 0.95; 35 | } 36 | // iphone 5 37 | if (deviceHeight < 667) { 38 | return size; 39 | // iphone 6-6s 40 | } else if (deviceHeight >= 667 && deviceHeight <= 735) { 41 | return size * 1.15; 42 | } 43 | // older phablets 44 | return size * 1.25; 45 | } 46 | if (pixelRatio === 3) { 47 | // catch Android font scaling on small machines 48 | // where pixel ratio / font scale ratio => 3:3 49 | if (deviceWidth <= 360) { 50 | return size; 51 | } 52 | // Catch other weird android width sizings 53 | if (deviceHeight < 667) { 54 | return size * 1.15; 55 | // catch in-between size Androids and scale font up 56 | // a tad but not too much 57 | } 58 | if (deviceHeight >= 667 && deviceHeight <= 735) { 59 | return size * 1.2; 60 | } 61 | // catch larger devices 62 | // ie iphone 6s plus / 7 plus / mi note 等等 63 | return size * 1.27; 64 | } 65 | if (pixelRatio === 3.5) { 66 | // catch Android font scaling on small machines 67 | // where pixel ratio / font scale ratio => 3:3 68 | if (deviceWidth <= 360) { 69 | return size; 70 | // Catch other smaller android height sizings 71 | } 72 | if (deviceHeight < 667) { 73 | return size * 1.20; 74 | // catch in-between size Androids and scale font up 75 | // a tad but not too much 76 | } 77 | if(deviceHeight >= 667 && deviceHeight <= 735) { 78 | return size * 1.25; 79 | } 80 | // catch larger phablet devices 81 | return size * 1.40; 82 | } 83 | // if older device ie pixelRatio !== 2 || 3 || 3.5 84 | return size; 85 | } 86 | 87 | module.exports = normalize; 88 | -------------------------------------------------------------------------------- /app/common/ImageLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 2016/12/8. 3 | */ 4 | 5 | import React, {Component, PropTypes} from 'react' 6 | import { 7 | View, 8 | Platform, 9 | Image 10 | } from 'react-native' 11 | 12 | export default class ImageLoader extends Component { 13 | 14 | static propTypes = { 15 | source: PropTypes.object, 16 | defaultSource: PropTypes.number, //默认图片 17 | style: PropTypes.object, 18 | imageStyle: PropTypes.object, 19 | isRadius: PropTypes.bool, //是否圆角 20 | borderColor: PropTypes.string 21 | }; 22 | 23 | static defaultProps = { 24 | defaultSource: require('../image/test1.jpg'), 25 | isRadius: true, 26 | imageStyle: {width: 68, height: 68}, 27 | style: {width: 68, height: 68}, 28 | borderColor:'#FFF' 29 | }; 30 | 31 | constructor(props) { 32 | super(props); 33 | this.state = { 34 | source: props.source, 35 | isLoad: false, 36 | } 37 | } 38 | 39 | componentWillReceiveProps(newProps){ 40 | this.setState({ 41 | source: newProps.source, 42 | }) 43 | } 44 | 45 | onLoad = () => { 46 | this.setState({ 47 | isLoad: true, 48 | }) 49 | }; 50 | 51 | render() { 52 | 53 | const {style, imageStyle, defaultSource, isRadius, borderColor} = this.props; 54 | const {source, isLoad} = this.state; 55 | 56 | if (Platform.OS === 'ios') { 57 | 58 | let borderRadius = isRadius ? style.width / 2 : 0; 59 | 60 | return( 61 | 67 | ) 68 | 69 | } else { 70 | 71 | const a = parseFloat(style.width) / 4; 72 | const b = parseFloat(style.width) / 2; 73 | 74 | let radiusStyle = { 75 | position: 'absolute', 76 | top: - a, 77 | bottom: - a, 78 | right: - a, 79 | left: - a, 80 | borderRadius: b + a, 81 | borderWidth: a, 82 | borderColor: borderColor 83 | }; 84 | 85 | return ( 86 | 87 | 93 | {!isLoad && 94 | 99 | } 100 | 101 | {isRadius && 102 | 103 | } 104 | 105 | ) 106 | } 107 | } 108 | } 109 | 110 | -------------------------------------------------------------------------------- /app/common/PraiseCheckBox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/7/21. 3 | */ 4 | import React, {Component, PropTypes} from 'react'; 5 | 6 | import { 7 | View, 8 | StyleSheet, 9 | Text, 10 | TouchableOpacity, 11 | Image 12 | } from 'react-native' 13 | 14 | export default class PraiseCheckBox extends Component { 15 | 16 | static PropTypes = { 17 | onCheck: PropTypes.func, 18 | style: PropTypes.object, 19 | imageStyle: PropTypes.object, 20 | textStyle: PropTypes.object, 21 | checked: PropTypes.bool, 22 | count: PropTypes.string, 23 | image: PropTypes.object, 24 | selectImage: PropTypes.object, 25 | onPress: PropTypes.func, 26 | isDisabled: PropTypes.bool, 27 | }; 28 | 29 | static defaultProps = { 30 | onCheck: () => { 31 | }, 32 | onPress: () => {}, 33 | checked: false, 34 | isDisabled: false, 35 | count: '0', 36 | style: {}, 37 | imageStyle: {}, 38 | textStyle: {} 39 | }; 40 | 41 | constructor(props) { 42 | super(props); 43 | 44 | this.state = { 45 | checked: this.props.checked, 46 | } 47 | } 48 | 49 | componentWillReceiveProps(nextProps) { 50 | this.setState({ 51 | checked: nextProps.checked, 52 | }) 53 | } 54 | 55 | onSelect = () => { 56 | 57 | this.setState({ 58 | checked: !this.state.checked, 59 | }, ()=> { 60 | this.props.onCheck(this.state.checked); 61 | }); 62 | 63 | }; 64 | 65 | render() { 66 | const {image, count, selectImage,isDisabled, onCheck} = this.props; 67 | return ( 68 | 69 | {isDisabled ? 70 | 71 | 75 | {count} 76 | : 77 | 82 | 86 | {count} 93 | 94 | } 95 | 96 | 97 | ); 98 | } 99 | } 100 | 101 | 102 | const styles = StyleSheet.create({ 103 | button: { 104 | padding:4, 105 | flexDirection:'row', 106 | alignItems:'center', 107 | marginLeft:18 108 | }, 109 | image: { 110 | height:14, 111 | width:14 112 | }, 113 | text: { 114 | fontSize:12, 115 | color:'#787878', 116 | marginLeft:6 117 | } 118 | }); -------------------------------------------------------------------------------- /app/container/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 2017/2/16. 3 | */ 4 | import React, {Component} from 'react'; 5 | import { 6 | StyleSheet, 7 | View 8 | } from 'react-native'; 9 | 10 | import {connect} from 'react-redux' 11 | import { 12 | Scene, 13 | Reducer, 14 | Router, 15 | Switch, 16 | Modal, 17 | Actions, 18 | ActionConst, 19 | } from 'react-native-router-flux'; 20 | import TabIcon from '../common/TabIcon' 21 | import Error from '../common/Error' 22 | import Counter from '../page/Counter' 23 | 24 | const reducerCreate = params => { 25 | const defaultReducer = new Reducer(params); 26 | return (state, action) => { 27 | console.log('ACTION:', action); 28 | return defaultReducer(state, action); 29 | }; 30 | }; 31 | 32 | // define this based on the styles/dimensions you use 33 | const getSceneStyle = (/* NavigationSceneRendererProps */ props, computedProps) => { 34 | const style = { 35 | flex: 1, 36 | backgroundColor: '#fff', 37 | shadowColor: null, 38 | shadowOffset: null, 39 | shadowOpacity: null, 40 | shadowRadius: null, 41 | }; 42 | if (computedProps.isActive) { 43 | style.marginTop = computedProps.hideNavBar ? 0 : 64; 44 | style.marginBottom = computedProps.hideTabBar ? 0 : 49; 45 | } 46 | return style; 47 | }; 48 | 49 | export default class app extends Component { 50 | 51 | render() { 52 | 53 | return ( 54 | 55 | 56 | 57 | 65 | 72 | 80 | 87 | 88 | 89 | 90 | 91 | 92 | ); 93 | } 94 | 95 | } 96 | 97 | const styles = StyleSheet.create({ 98 | container: { 99 | flex: 1, 100 | backgroundColor: '#F4F4F4' 101 | }, 102 | tabBarStyle: { 103 | borderTopWidth: StyleSheet.hairlineWidth, 104 | borderColor: '#b7b7b7', 105 | backgroundColor: 'rgba(250, 251, 252, 1)', 106 | justifyContent: 'center', 107 | opacity: 1, 108 | height: 49, 109 | }, 110 | tabBarSelectedItemStyle: { 111 | // backgroundColor: '#ddd', 112 | }, 113 | }); 114 | 115 | -------------------------------------------------------------------------------- /app/common/SelectPickerView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/7/15. 3 | */ 4 | import React, {Component, PropTypes} from 'react'; 5 | import { 6 | StyleSheet, 7 | Text, 8 | View, 9 | TouchableOpacity, 10 | PickerIOS, 11 | Platform, 12 | } from 'react-native'; 13 | 14 | import Modal from 'react-native-modalbox' 15 | import PickerAndroid from './PickerAndroid'; 16 | 17 | let Picker = Platform.OS === 'ios' ? PickerIOS : PickerAndroid; 18 | 19 | export default class SelectPickerView extends Component { 20 | 21 | static PropTypes = { 22 | defaultValue: PropTypes.object, 23 | defaultTitle: PropTypes.object, 24 | onChange: PropTypes.func, 25 | pickerArr: PropTypes.array, 26 | onPressConfirm: PropTypes.func, 27 | onPressCancel: PropTypes.func, 28 | style: PropTypes.object 29 | }; 30 | 31 | static defaultProps = { 32 | onChange: ()=> {} 33 | }; 34 | 35 | constructor(props) { 36 | super(props); 37 | const {defaultValue,defaultTitle} = this.props; 38 | this.state = { 39 | select: defaultValue, 40 | visible: false, 41 | index: 0, 42 | title:defaultTitle 43 | } 44 | } 45 | 46 | onShow() { 47 | this.refs.modal.open(); 48 | } 49 | 50 | onDismiss() { 51 | this.refs.modal.close(); 52 | } 53 | 54 | onPressCancel() { 55 | this.onDismiss() 56 | } 57 | 58 | onPressConfirm() { 59 | this.onDismiss(); 60 | 61 | this.props.onPressConfirm(this.state.select, this.state.index, this.state.title) 62 | 63 | } 64 | 65 | render() { 66 | 67 | const {pickerArr, onChange} = this.props; 68 | 69 | console.log(pickerArr); 70 | 71 | return ( 72 | 73 | 74 | 75 | 82 | 84 | 取消 85 | 86 | 87 | 89 | 确认 90 | 91 | 92 | { 93 | pickerArr.length > 0 && 94 | { 98 | this.setState({ 99 | select: itemValue, 100 | index: itemPosition, 101 | title:pickerArr[itemPosition].name 102 | }); 103 | onChange(itemValue, itemPosition) 104 | }}> 105 | {pickerArr.map((v, key)=> { 106 | return ( 107 | 108 | ) 109 | } 110 | )} 111 | 112 | } 113 | 114 | 115 | ); 116 | } 117 | } 118 | 119 | const styles = StyleSheet.create({ 120 | modal: { 121 | height:256, 122 | }, 123 | }); -------------------------------------------------------------------------------- /app/common/Password.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 2016/11/23. 3 | */ 4 | import React, { 5 | Component, 6 | PropTypes, 7 | } from 'react'; 8 | import { 9 | StyleSheet, 10 | View, 11 | TextInput, 12 | TouchableHighlight, 13 | InteractionManager, 14 | } from 'react-native'; 15 | 16 | export default class Password extends Component { 17 | static propTypes = { 18 | style: View.propTypes.style, 19 | inputItemStyle: View.propTypes.style, 20 | iconStyle: View.propTypes.style, 21 | maxLength: TextInput.propTypes.maxLength.isRequired, 22 | onChange: PropTypes.func, 23 | onEnd: PropTypes.func, 24 | autoFocus: PropTypes.bool, 25 | }; 26 | 27 | static defaultProps = { 28 | autoFocus: true, 29 | onChange: () => { 30 | }, 31 | onEnd: () => { 32 | }, 33 | }; 34 | 35 | constructor(props) { 36 | super(props); 37 | this.state = { 38 | text: '' 39 | }; 40 | } 41 | 42 | componentDidMount() { 43 | if (this.props.autoFocus) { 44 | InteractionManager.runAfterInteractions(() => { 45 | this._onPress(); 46 | }); 47 | } 48 | } 49 | 50 | render() { 51 | return ( 52 | 56 | 57 | { 65 | this.setState({text}); 66 | this.props.onChange(text); 67 | if (text.length === this.props.maxLength) { 68 | this.props.onEnd(text); 69 | } 70 | } 71 | } 72 | /> 73 | { 74 | this._getInputItem() 75 | } 76 | 77 | 78 | ) 79 | 80 | } 81 | 82 | _getInputItem() { 83 | let inputItem = []; 84 | let {text}=this.state; 85 | for (let i = 0; i < parseInt(this.props.maxLength); i++) { 86 | if (i == 0) { 87 | inputItem.push( 88 | 92 | { 93 | i < text.length ? 94 | 95 | : 96 | } 97 | ) 98 | } 99 | else { 100 | inputItem.push( 101 | 105 | { 106 | i < text.length ? 107 | 108 | : 109 | } 110 | ) 111 | } 112 | } 113 | return inputItem; 114 | } 115 | 116 | _onPress() { 117 | this.refs.textInput.focus(); 118 | } 119 | } 120 | 121 | const styles = StyleSheet.create({ 122 | container: { 123 | alignItems: 'center', 124 | flexDirection: 'row', 125 | // borderWidth: 1, 126 | // borderColor: '#ccc', 127 | // backgroundColor: '#fff' 128 | }, 129 | inputItem: { 130 | height: 45, 131 | width: 45, 132 | justifyContent: 'center', 133 | alignItems: 'center' 134 | }, 135 | iconStyle: { 136 | width: 16, 137 | height: 16, 138 | backgroundColor: '#222', 139 | borderRadius: 8, 140 | }, 141 | }); -------------------------------------------------------------------------------- /app/common/StarRating.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 2016/12/19. 3 | */ 4 | import { 5 | StyleSheet, 6 | View, 7 | TouchableOpacity 8 | } from 'react-native'; 9 | 10 | import React, { 11 | Component, 12 | PropTypes 13 | } from 'react'; 14 | 15 | // Third party imports 16 | import EntypoIcons from 'react-native-vector-icons/Entypo'; 17 | import EvilIconsIcons from 'react-native-vector-icons/EvilIcons'; 18 | import FontAwesomeIcons from 'react-native-vector-icons/FontAwesome'; 19 | import FoundationIcons from 'react-native-vector-icons/Foundation'; 20 | import IoniconsIcons from 'react-native-vector-icons/Ionicons'; 21 | import MaterialIconsIcons from 'react-native-vector-icons/MaterialIcons'; 22 | import OcticonsIcons from 'react-native-vector-icons/Octicons'; 23 | import ZocialIcons from 'react-native-vector-icons/Zocial'; 24 | 25 | const iconSets = { 26 | Entypo: EntypoIcons, 27 | EvilIcons: EvilIconsIcons, 28 | FontAwesome: FontAwesomeIcons, 29 | Foundation: FoundationIcons, 30 | Ionicons: IoniconsIcons, 31 | MaterialIcons: MaterialIconsIcons, 32 | Octicons: OcticonsIcons, 33 | Zocial: ZocialIcons 34 | }; 35 | 36 | class StarRating extends Component { 37 | 38 | constructor(props) { 39 | super(props); 40 | 41 | // Round rating down to nearest .5 star 42 | const roundedRating = this.round(this.props.rating); 43 | this.state = { 44 | maxStars: this.props.maxStars, 45 | rating: this.round(this.props.rating) 46 | }; 47 | } 48 | 49 | componentWillReceiveProps(nextProps) { 50 | this.setState({ 51 | rating: this.round(nextProps.rating) 52 | }); 53 | } 54 | 55 | round(number) { 56 | return (Math.round(number * 2) / 2); 57 | } 58 | 59 | pressStarButton(rating) { 60 | this.props.selectedStar(rating); 61 | this.setState({ 62 | rating: rating 63 | }); 64 | } 65 | 66 | render() { 67 | var starsLeft = this.state.rating; 68 | const starButtons = []; 69 | for (var i = 0; i < this.state.maxStars; i++) { 70 | const Icon = iconSets[this.props.iconSet]; 71 | var starIconName = this.props.emptyStar; 72 | var starColor = this.props.emptyStarColor; 73 | if (starsLeft >= 1) { 74 | starIconName = this.props.fullStar; 75 | starColor = this.props.starColor; 76 | } else if (starsLeft === 0.5) { 77 | starIconName = this.props.halfStar; 78 | starColor = this.props.starColor; 79 | } 80 | starButtons.push( 81 | 92 | 97 | 98 | ); 99 | starsLeft--; 100 | } 101 | return ( 102 | 103 | {starButtons} 104 | 105 | ); 106 | } 107 | }; 108 | 109 | StarRating.propTypes = { 110 | disabled: PropTypes.bool, 111 | emptyStar: PropTypes.string, 112 | fullStar: PropTypes.string, 113 | halfStar: PropTypes.string, 114 | iconSet: PropTypes.string, 115 | maxStars: PropTypes.number, 116 | rating: PropTypes.number, 117 | selectedStar: PropTypes.func.isRequired, 118 | starColor: PropTypes.string, 119 | emptyStarColor: PropTypes.string, 120 | starSize: PropTypes.number 121 | } 122 | 123 | StarRating.defaultProps = { 124 | disabled: false, 125 | emptyStar: 'star-o', 126 | fullStar: 'star', 127 | halfStar: 'star-half-o', 128 | iconSet: 'FontAwesome', 129 | maxStars: 5, 130 | rating: 0, 131 | starColor: 'black', 132 | emptyStarColor: 'gray', 133 | starSize: 40 134 | } 135 | 136 | const styles = StyleSheet.create({ 137 | starRatingContainer: { 138 | flexDirection: 'row', 139 | justifyContent: 'space-between', 140 | } 141 | }); 142 | 143 | export default StarRating; -------------------------------------------------------------------------------- /app/util/FetchUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/7/18. 3 | */ 4 | 5 | import UserDefaults from './GlobalStorage' 6 | 7 | export function requestLogin(url, method = 'GET', params = {}, successBlock = ()=>{}, failBlock = ()=>{}, loginBlock = ()=>{}) { 8 | UserDefaults.objectForKey('userLoginInfo', (info) => { 9 | if (!!info) { 10 | if (method == 'GET') { 11 | requestGET(url, {...info,...params}, successBlock, failBlock, loginBlock); 12 | } else { 13 | console.log('POST'); 14 | requestPOST(url, {...info,...params}, successBlock, failBlock, loginBlock); 15 | } 16 | } else { 17 | loginBlock(); 18 | } 19 | }) 20 | } 21 | 22 | export function requestGET(url, params = {}, successBlock = ()=>{}, failBlock = ()=>{}, loginBlock = ()=>{}) { 23 | 24 | //把传进来的参数加工成GET模式 25 | let newURL = url; 26 | let keys = Object.keys(params); 27 | keys.map((value, index)=> { 28 | if (index == 0) { 29 | newURL = url + '?' 30 | } 31 | newURL = `${newURL}${value}=${params[value]}`; 32 | 33 | if (index != keys.length - 1) { 34 | newURL = newURL + '&' 35 | } 36 | }); 37 | 38 | let map = {method: 'GET', timeout: 30 * 1000}; 39 | 40 | console.log(newURL); 41 | 42 | fetch(newURL, map) 43 | .then((response) => response.json()) 44 | .then( 45 | (responseJson) => { 46 | 47 | console.log(responseJson); 48 | 49 | if (responseJson.errcode == 9) { 50 | loginBlock(); 51 | } else if (responseJson.errcode == 0) { 52 | successBlock(responseJson.data); 53 | } else { 54 | failBlock(responseJson.errmsg); 55 | } 56 | } 57 | ) 58 | .catch( 59 | (error) => { 60 | console.log(error); 61 | failBlock(error); 62 | } 63 | ) 64 | } 65 | 66 | export function requestPOST(url, params = {}, successBlock = ()=>{}, failBlock = ()=>{}, loginBlock = ()=>{}) { 67 | 68 | console.log(url); 69 | console.log(params); 70 | 71 | fetch(url, { 72 | method: 'POST', 73 | headers: { 74 | 'Accept': 'application/json', 75 | 'Content-Type': 'application/json' 76 | }, 77 | body: JSON.stringify(params), 78 | timeout: 30 * 1000 79 | }) 80 | .then((response) => response.json()) 81 | .then( 82 | (responseJson) => { 83 | 84 | console.log(responseJson); 85 | 86 | if (responseJson.errcode == 9) { 87 | loginBlock(); 88 | } else if (responseJson.errcode == 0) { 89 | successBlock(responseJson.data); 90 | } else { 91 | failBlock(responseJson.errmsg); 92 | } 93 | } 94 | ) 95 | .catch( 96 | (error) => { 97 | failBlock(error); 98 | console.log(error); 99 | } 100 | ); 101 | } 102 | 103 | export function upLoadImage(url, params, response, successBlock, failBlack) { 104 | let formData = new FormData(); 105 | formData.append('file', {uri: response, type: 'image/jpeg', name: 'userImage.jpg'}); 106 | formData.append('fileName', 'file'); 107 | formData.append('photoDir', 'userImage'); 108 | formData.append('height', params.height); 109 | formData.append('width', params.width); 110 | fetch(url, { 111 | method: 'POST', 112 | headers: { 113 | 'Content-Type': 'multipart/form-data', 114 | }, 115 | body: formData 116 | }) 117 | .then((response) => response.text()) 118 | .then((responseData) => { 119 | let responseJson = eval("(" + responseData + ")"); 120 | successBlock(responseJson.data); 121 | } 122 | ) 123 | .catch((error) => { 124 | failBlack(error); 125 | } 126 | ) 127 | } 128 | 129 | export function gets(url, successCallback, failCallback) { 130 | let request = new XMLHttpRequest(); 131 | request.onreadystatechange = (e) => { 132 | if (request.readyState !== 4) { 133 | return; 134 | } 135 | 136 | if (request.status === 200) { 137 | successCallback(JSON.parse(request.responseText)) 138 | 139 | } else { 140 | failCallback() 141 | } 142 | }; 143 | 144 | request.open('GET', url); 145 | request.send(); 146 | } -------------------------------------------------------------------------------- /app/common/Button.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/5/19. 3 | */ 4 | 5 | import React, {Component, PropTypes} from 'react'; 6 | import { 7 | View, 8 | TouchableOpacity, 9 | Text, 10 | StyleSheet, 11 | ActivityIndicator, 12 | } from 'react-native'; 13 | 14 | import lodash from 'lodash' 15 | 16 | export default class Button extends Component { 17 | static PropTypes = { 18 | textStyle: Text.propTypes.style, 19 | disabledStyle: View.propTypes.style, 20 | disabledTextStyle: Text.propTypes.style, 21 | children: PropTypes.oneOfType([ 22 | PropTypes.string, 23 | PropTypes.node, 24 | PropTypes.element 25 | ]), 26 | activeOpacity: PropTypes.number, 27 | allowFontScaling: PropTypes.bool, 28 | isLoading: PropTypes.bool, 29 | isDisabled: PropTypes.bool, 30 | activityIndicatorColor: PropTypes.string, 31 | delayLongPress: PropTypes.number, 32 | delayPressIn: PropTypes.number, 33 | delayPressOut: PropTypes.number, 34 | onPress: PropTypes.func, 35 | onLongPress: PropTypes.func, 36 | onPressIn: PropTypes.func, 37 | onPressOut: PropTypes.func, 38 | }; 39 | 40 | _renderChildren() { 41 | let disabledStyle = [styles.textButton, this.props.textStyle]; 42 | if (this.props.isDisabled === true) { 43 | disabledStyle = [styles.textButton, this.props.textStyle, this.props.disabledTextStyle]; 44 | } 45 | let childElements = []; 46 | React.Children.forEach(this.props.children, (item) => { 47 | if (typeof item === 'string' || typeof item === 'number') { 48 | const element = ( 49 | 53 | {item} 54 | 55 | ); 56 | childElements.push(element); 57 | } else if (React.isValidElement(item)) { 58 | childElements.push(item); 59 | } 60 | }); 61 | return (childElements); 62 | } 63 | 64 | shouldComponentUpdate(nextProps, nextState) { 65 | if (!lodash.isEqual(nextProps, this.props)) { 66 | return true; 67 | } 68 | return false; 69 | } 70 | 71 | _renderInnerText() { 72 | if (this.props.isLoading) { 73 | return ( 74 | 80 | ); 81 | } 82 | return this._renderChildren(); 83 | } 84 | 85 | render() { 86 | if (this.props.isDisabled === true || this.props.isLoading === true) { 87 | return ( 88 | 89 | {this._renderInnerText()} 90 | 91 | ); 92 | } else { 93 | // Extract Touchable props 94 | let touchableProps = { 95 | onPress: this.props.onPress, 96 | onPressIn: this.props.onPressIn, 97 | onPressOut: this.props.onPressOut, 98 | onLongPress: this.props.onLongPress, 99 | activeOpacity: this.props.activeOpacity, 100 | delayLongPress: this.props.delayLongPress, 101 | delayPressIn: this.props.delayPressIn, 102 | delayPressOut: this.props.delayPressOut, 103 | }; 104 | 105 | return ( 106 | 109 | {this._renderInnerText()} 110 | 111 | ); 112 | } 113 | 114 | } 115 | }; 116 | 117 | const styles = StyleSheet.create({ 118 | button: { 119 | flexDirection: 'row', 120 | borderRadius: 4, 121 | justifyContent: 'center', 122 | // alignSelf:'center' 123 | }, 124 | textButton: { 125 | fontSize: 16, 126 | alignSelf: 'center', 127 | marginHorizontal:5, 128 | marginVertical:5 129 | }, 130 | spinner: { 131 | alignSelf: 'center', 132 | }, 133 | opacity: { 134 | opacity: 0.7, 135 | }, 136 | }); 137 | 138 | //transparent 139 | //opacity: 0.5, -------------------------------------------------------------------------------- /app/common/KeyboardSpacer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 2016/12/27. 3 | */ 4 | import React, { Component, PropTypes } from 'react'; 5 | import { 6 | Keyboard, 7 | LayoutAnimation, 8 | View, 9 | Dimensions, 10 | Platform, 11 | StyleSheet 12 | } from 'react-native'; 13 | 14 | const Screen = Dimensions.get('window'); 15 | 16 | const styles = StyleSheet.create({ 17 | container: Platform.select({ 18 | ios: {}, 19 | android: {flex:1}, 20 | }), 21 | }); 22 | 23 | // From: https://medium.com/man-moon/writing-modern-react-native-ui-e317ff956f02 24 | const defaultAnimation = { 25 | duration: 500, 26 | create: { 27 | duration: 300, 28 | type: LayoutAnimation.Types.easeInEaseOut, 29 | property: LayoutAnimation.Properties.opacity 30 | }, 31 | update: { 32 | type: LayoutAnimation.Types.spring, 33 | springDamping: 200 34 | } 35 | }; 36 | 37 | export default class KeyboardSpacer extends Component { 38 | static propTypes = { 39 | topSpacing: PropTypes.number, 40 | onToggle: PropTypes.func, 41 | style: View.propTypes.style, 42 | }; 43 | 44 | static defaultProps = { 45 | topSpacing: 0, 46 | onToggle: () => null, 47 | }; 48 | 49 | constructor(props, context) { 50 | super(props, context); 51 | 52 | this.viewHeight = 0; 53 | this.isOneHeight = false; 54 | if (Platform.OS === 'android') { 55 | this.viewHeight = Screen.height - 44 56 | } else { 57 | this.viewHeight = Screen.height - 64 58 | } 59 | 60 | this.state = { 61 | keyboardSpace: this.viewHeight, 62 | isKeyboardOpened: false 63 | }; 64 | this._listeners = null; 65 | this.updateKeyboardSpace = this.updateKeyboardSpace.bind(this); 66 | this.resetKeyboardSpace = this.resetKeyboardSpace.bind(this); 67 | } 68 | 69 | componentDidMount() { 70 | const updateListener = Platform.OS === 'android' ? 'keyboardDidShow' : 'keyboardWillShow'; 71 | const resetListener = Platform.OS === 'android' ? 'keyboardDidHide' : 'keyboardWillHide'; 72 | this._listeners = [ 73 | Keyboard.addListener(updateListener, this.updateKeyboardSpace), 74 | Keyboard.addListener(resetListener, this.resetKeyboardSpace) 75 | ]; 76 | } 77 | 78 | componentWillUnmount() { 79 | this._listeners.forEach(listener => listener.remove()); 80 | } 81 | 82 | updateKeyboardSpace(event) { 83 | if (!event.endCoordinates) { 84 | return; 85 | } 86 | 87 | let animationConfig = defaultAnimation; 88 | if (Platform.OS === 'ios') { 89 | animationConfig = LayoutAnimation.create( 90 | event.duration, 91 | LayoutAnimation.Types[event.easing], 92 | LayoutAnimation.Properties.opacity, 93 | ); 94 | } 95 | LayoutAnimation.configureNext(animationConfig); 96 | 97 | // get updated on rotation 98 | // when external physical keyboard is connected 99 | // event.endCoordinates.height still equals virtual keyboard height 100 | // however only the keyboard toolbar is showing if there should be one 101 | const keyboardSpace = (this.viewHeight - event.endCoordinates.height) + this.props.topSpacing; 102 | console.log(keyboardSpace); 103 | this.setState({ 104 | keyboardSpace: keyboardSpace, 105 | isKeyboardOpened: true 106 | }, this.props.onToggle(true, keyboardSpace)); 107 | } 108 | 109 | resetKeyboardSpace(event) { 110 | let animationConfig = defaultAnimation; 111 | if (Platform.OS === 'ios') { 112 | animationConfig = LayoutAnimation.create( 113 | event.duration, 114 | LayoutAnimation.Types[event.easing], 115 | LayoutAnimation.Properties.opacity, 116 | ); 117 | } 118 | LayoutAnimation.configureNext(animationConfig); 119 | 120 | console.log(this.viewHeight); 121 | 122 | this.setState({ 123 | keyboardSpace: this.viewHeight, 124 | isKeyboardOpened: false 125 | }, this.props.onToggle(false, 0)); 126 | } 127 | 128 | render() { 129 | return ( 130 | { 132 | console.log('-----'+e.nativeEvent.layout.height); 133 | if (!this.isOneHeight){ 134 | this.isOneHeight = true; 135 | this.viewHeight = e.nativeEvent.layout.height; 136 | this.setState({ 137 | keyboardSpace: this.viewHeight 138 | }) 139 | } 140 | }} 141 | > 142 | {this.props.children} 143 | 144 | ); 145 | } 146 | } -------------------------------------------------------------------------------- /app/common/AutoGrowingTextInput.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | 3 | import { 4 | View, 5 | TextInput, 6 | StyleSheet, 7 | LayoutAnimation, 8 | Platform, 9 | } from 'react-native'; 10 | 11 | const ANDROID_PLATFORM = (Platform.OS === 'android'); 12 | 13 | const DEFAULT_ANIM_DURATION = 100; 14 | 15 | export default class AutoGrowingTextInput extends Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | 20 | this.state = { 21 | height: this._getValidHeight(props.initialHeight), 22 | androidFirstContentSizeChange: true 23 | }; 24 | } 25 | 26 | _renderTextInput() { 27 | return ( 28 | this._onContentSizeChange(event)} 36 | ref={(r) => {this._textInput = r;}} 37 | onChange={(event) => this._onChange(event)} 38 | /> 39 | ); 40 | } 41 | 42 | render() { 43 | return this._renderTextInput(); 44 | } 45 | 46 | /* 47 | TextInput onContentSizeChange should fix the issue with "initial value doesn't receive change event" 48 | While this works perfectly on iOS, on Android you only get it on the first time the component is displayed.. 49 | So on Android a height update for the initial value is performed in `onContentSizeChange`, but the rest 50 | of the updates are still performed via `onChange` as it was before 51 | using a flag (androidFirstContentSizeChange) to pervent multiple updates in case both notifications works simultaniously in some cases 52 | */ 53 | _onContentSizeChange(event) { 54 | if (ANDROID_PLATFORM) { 55 | if (!this.state.androidFirstContentSizeChange) { 56 | return; 57 | } 58 | this.setState({androidFirstContentSizeChange: false}); 59 | } 60 | this._handleNativeEvent(event.nativeEvent); 61 | 62 | if (this.props.onContentSizeChange) { 63 | this.props.onContentSizeChange(event); 64 | } 65 | } 66 | 67 | _onChange(event) { 68 | if (ANDROID_PLATFORM && !this.state.androidFirstContentSizeChange) { 69 | this._handleNativeEvent(event.nativeEvent); 70 | } 71 | if (this.props.onChange) { 72 | this.props.onChange(event); 73 | } 74 | } 75 | 76 | _getValidHeight(height) { 77 | const minCappedHeight = Math.max(this.props.minHeight, height); 78 | if (this.props.maxHeight == null) { 79 | return minCappedHeight; 80 | } 81 | return Math.min(this.props.maxHeight, minCappedHeight); 82 | } 83 | 84 | _handleNativeEvent(nativeEvent) { 85 | let newHeight = this.state.height; 86 | if (nativeEvent.contentSize && this.props.autoGrowing) { 87 | newHeight = nativeEvent.contentSize.height; 88 | if (this.state.height !== newHeight && newHeight <= this.props.maxHeight && this.props.onHeightChanged) { 89 | this.props.onHeightChanged(newHeight, this.state.height, newHeight - this.state.height); 90 | } 91 | } 92 | 93 | if (this.props.animation.animated) { 94 | const duration = this.props.animation.duration || DEFAULT_ANIM_DURATION; 95 | LayoutAnimation.configureNext({...LayoutAnimation.Presets.easeInEaseOut, duration: duration}); 96 | } 97 | 98 | this.setState({ 99 | height: newHeight 100 | }); 101 | } 102 | 103 | setNativeProps(nativeProps = {}) { 104 | this._textInput.setNativeProps(nativeProps); 105 | } 106 | 107 | resetHeightToMin() { 108 | this.setState({ 109 | height: this.props.minHeight 110 | }); 111 | } 112 | 113 | clear() { 114 | return this._textInput.clear(); 115 | } 116 | 117 | focus() { 118 | return this._textInput.focus(); 119 | } 120 | 121 | blur() { 122 | return this._textInput.blur(); 123 | } 124 | 125 | isFocused() { 126 | return this._textInput.isFocused(); 127 | } 128 | } 129 | 130 | const styles = StyleSheet.create({ 131 | hiddenOffScreen: { 132 | position: 'absolute', 133 | top: 5000, 134 | left: 5000, 135 | backgroundColor: 'transparent', 136 | borderColor: 'transparent', 137 | color: 'transparent' 138 | }, 139 | textInput: { 140 | // flex: 1, 141 | marginLeft: Platform.select({ 142 | ios: 10, 143 | android: 0, 144 | }), 145 | fontSize: 16, 146 | lineHeight: 16, 147 | marginTop: Platform.select({ 148 | ios: 3, 149 | android: 0, 150 | }), 151 | marginBottom: Platform.select({ 152 | ios: 3, 153 | android: 3, 154 | }), 155 | // backgroundColor:'red' 156 | }, 157 | }); 158 | 159 | AutoGrowingTextInput.propTypes = { 160 | autoGrowing: PropTypes.bool, 161 | initialHeight: PropTypes.number, 162 | minHeight: PropTypes.number, 163 | maxHeight: PropTypes.number, 164 | onHeightChanged: PropTypes.func, 165 | onChange: PropTypes.func, 166 | animation: PropTypes.object 167 | }; 168 | AutoGrowingTextInput.defaultProps = { 169 | autoGrowing: true, 170 | minHeight: Platform.select({ 171 | ios: 28, 172 | android: 41, 173 | }), 174 | initialHeight: Platform.select({ 175 | ios: 28, 176 | android: 41, 177 | }), 178 | maxHeight: null, 179 | animation: {animated: false, duration: DEFAULT_ANIM_DURATION} 180 | }; 181 | -------------------------------------------------------------------------------- /app/common/Search.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 2016/12/21. 3 | */ 4 | import React, {PropTypes, Component} from 'react' 5 | import {View, StyleSheet, TextInput, Platform} from 'react-native' 6 | import Icon from 'react-native-vector-icons/MaterialIcons' 7 | import colors from '../util/colors' 8 | import normalize from '../util/normalizeText' 9 | 10 | class Search extends Component { 11 | focus() { 12 | const ref = this.props.textInputRef; 13 | this.refs[ref].focus() 14 | } 15 | 16 | render() { 17 | const { 18 | containerStyle, 19 | inputStyle, 20 | icon, 21 | noIcon, 22 | lightTheme, 23 | round, 24 | /* inherited props */ 25 | value, 26 | autoCapitalize, 27 | autoCorrect, 28 | autoFocus, 29 | blurOnSubmit, 30 | defaultValue, 31 | editable, 32 | keyboardType, 33 | maxLength, 34 | multiline, 35 | onBlur, 36 | onChange, 37 | onChangeText, 38 | onContentSizeChange, 39 | onEndEditing, 40 | onFocus, 41 | onLayout, 42 | onSelectionChange, 43 | onSubmitEditing, 44 | placeholder, 45 | placeholderTextColor, 46 | returnKeyType, 47 | secureTextEntry, 48 | selectTextOnFocus, 49 | selectionColor, 50 | inlineImageLeft, 51 | inlineImagePadding, 52 | numberOfLines, 53 | returnKeyLabel, 54 | underlineColorAndroid, 55 | clearButtonMode, 56 | clearTextOnFocus, 57 | dataDetectorTypes, 58 | enablesReturnKeyAutomatically, 59 | keyboardAppearance, 60 | onKeyPress, 61 | selectionState, 62 | isFocused, 63 | clear, 64 | textInputRef, 65 | containerRef 66 | } = this.props; 67 | 68 | return ( 69 | 76 | 124 | { 125 | !noIcon && ( 126 | 135 | ) 136 | } 137 | 138 | ) 139 | } 140 | } 141 | 142 | Search.propTypes = { 143 | icon: PropTypes.object, 144 | noIcon: PropTypes.bool, 145 | lightTheme: PropTypes.bool, 146 | containerStyle: PropTypes.any, 147 | inputStyle: PropTypes.any, 148 | round: PropTypes.bool 149 | }; 150 | 151 | Search.defaultProps = { 152 | placeholderTextColor: colors.grey3, 153 | lightTheme: false, 154 | noIcon: false, 155 | round: false, 156 | icon: {} 157 | }; 158 | 159 | const styles = StyleSheet.create({ 160 | container: { 161 | borderTopWidth: 1, 162 | borderBottomWidth: 1, 163 | borderBottomColor: '#000', 164 | borderTopColor: '#000', 165 | backgroundColor: colors.grey0 166 | }, 167 | containerLight: { 168 | backgroundColor: '#FFF', 169 | borderTopColor: '#e1e1e1', 170 | borderBottomColor: '#e1e1e1' 171 | }, 172 | icon: { 173 | backgroundColor: 'transparent', 174 | position: 'absolute', 175 | left: 16, 176 | top: 15.5, 177 | ...Platform.select({ 178 | android: { 179 | top: 20 180 | } 181 | }) 182 | }, 183 | input: { 184 | paddingLeft: 26, 185 | paddingRight: 19, 186 | margin: 8, 187 | borderRadius: 3, 188 | overflow: 'hidden', 189 | backgroundColor: colors.searchBg, 190 | fontSize: normalize(14), 191 | color: colors.grey3, 192 | height: 40, 193 | ...Platform.select({ 194 | ios: { 195 | height: 30 196 | }, 197 | android: { 198 | borderWidth: 0 199 | } 200 | }) 201 | }, 202 | inputLight: { 203 | backgroundColor: '#f4f4f4' 204 | } 205 | }); 206 | 207 | export default Search -------------------------------------------------------------------------------- /app/common/NavigationBar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/7/16. 3 | */ 4 | 'use strict'; 5 | import React, {Component, PropTypes} from 'react'; 6 | import { 7 | StyleSheet, 8 | View, 9 | Image, 10 | Text, 11 | TouchableOpacity, 12 | Dimensions, 13 | Platform, 14 | StatusBar, 15 | PixelRatio 16 | } from 'react-native'; 17 | 18 | let width = Dimensions.get('window').width; 19 | const pixelRation = PixelRatio.get(); 20 | import Icon from 'react-native-vector-icons/Ionicons'; 21 | 22 | export default class NavigationBar extends Component{ 23 | 24 | static propTypes = { 25 | title: PropTypes.string.isRequired, 26 | height: PropTypes.number, 27 | titleColor: PropTypes.string, 28 | backgroundColor: PropTypes.string, 29 | leftButtonTitle: PropTypes.string, 30 | leftButtonTitleColor: PropTypes.string, 31 | onLeftButtonPress: PropTypes.func, 32 | rightButtonTitle: PropTypes.string, 33 | rightButtonTitleColor: PropTypes.string, 34 | onRightButtonPress: PropTypes.func, 35 | showLeftDefault: PropTypes.bool 36 | }; 37 | 38 | static defaultProps = { 39 | height: 44, 40 | titleColor: '#1E1E1E', 41 | backgroundColor: '#FFF', 42 | leftButtonTitle: null, 43 | leftButtonTitleColor: '#1E1E1E', 44 | rightButtonTitle: null, 45 | rightButtonTitleColor: '#1E1E1E', 46 | showLeftDefault: true 47 | }; 48 | 49 | componentWillMount(){ 50 | this.state = this._getStateFromProps(this.props); 51 | } 52 | 53 | componentWillReceiveProps(newProps){ 54 | let newState = this._getStateFromProps(newProps); 55 | this.setState(newState); 56 | } 57 | 58 | shouldComponentUpdate(nextProps, nextState, context) { 59 | return JSON.stringify([nextState, context]) !== JSON.stringify([this.state, context]); 60 | } 61 | 62 | _getStateFromProps(props){ 63 | let title = props.title; 64 | let height = props.height; 65 | let titleColor = props.titleColor; 66 | let backgroundColor = props.backgroundColor; 67 | let leftButtonTitle = props.leftButtonTitle; 68 | let leftButtonTitleColor = props.leftButtonTitleColor; 69 | let onLeftButtonPress = props.onLeftButtonPress; 70 | let rightButtonTitle = props.rightButtonTitle; 71 | let rightButtonTitleColor = props.rightButtonTitleColor; 72 | let onRightButtonPress = props.onRightButtonPress; 73 | let leftButtonIcon = props.leftButtonIcon; 74 | let rightButtonIcon = props.rightButtonIcon; 75 | let showLeftDefault = props.showLeftDefault; 76 | 77 | return { 78 | title, 79 | height, 80 | titleColor, 81 | backgroundColor, 82 | leftButtonTitle, 83 | leftButtonTitleColor, 84 | onLeftButtonPress, 85 | rightButtonTitle, 86 | rightButtonTitleColor, 87 | onRightButtonPress, 88 | leftButtonIcon, 89 | rightButtonIcon, 90 | showLeftDefault 91 | }; 92 | } 93 | 94 | _renderLeftIcon() { 95 | if (this.state.showLeftDefault) { 96 | return ( 97 | 99 | ); 100 | } 101 | if(this.state.leftButtonIcon){ 102 | return ( 103 | 104 | ); 105 | } 106 | return null; 107 | } 108 | 109 | _renderRightIcon() { 110 | if(this.state.rightButtonIcon){ 111 | return ( 112 | 113 | ); 114 | } 115 | return null; 116 | } 117 | 118 | _onLeftButtonPressHandle(event) { 119 | let onPress = this.state.onLeftButtonPress; 120 | typeof onPress === 'function' && onPress(event); 121 | } 122 | 123 | _onRightButtonPressHandle(event) { 124 | let onPress = this.state.onRightButtonPress; 125 | typeof onPress === 'function' && onPress(event); 126 | } 127 | 128 | render() { 129 | let height = Platform.OS === 'ios' ? this.state.height + 20 : this.state.height; 130 | return ( 131 | 135 | 136 | 137 | 138 | 139 | {this._renderLeftIcon()} 140 | 141 | {this.state.leftButtonTitle} 142 | 143 | 144 | 145 | 146 | 147 | 148 | {this.state.title} 149 | 150 | 151 | 152 | 153 | 154 | {this._renderRightIcon()} 155 | 156 | {this.state.rightButtonTitle} 157 | 158 | 159 | 160 | 161 | 162 | ); 163 | } 164 | }; 165 | 166 | let styles = StyleSheet.create({ 167 | container: { 168 | // flex: 1, 169 | // position: 'absolute', 170 | // top: 0, 171 | // left: 0, 172 | flexDirection: 'row', 173 | width: width, 174 | borderBottomWidth: 1 / pixelRation, 175 | borderBottomColor:'#F4F4F4' 176 | }, 177 | leftButton: { 178 | flex: 1, 179 | flexDirection: 'row', 180 | justifyContent: 'flex-start', 181 | alignItems: 'center', 182 | width: 90, 183 | paddingTop: 1, 184 | paddingLeft: 8 185 | }, 186 | leftButtonIcon: { 187 | width: 15, 188 | height: 15, 189 | marginLeft: 6 190 | }, 191 | leftButtonTitle: { 192 | fontSize: 15, 193 | marginLeft: 4 194 | }, 195 | title: { 196 | flex: 1, 197 | alignItems: 'center', 198 | paddingTop: 1, 199 | justifyContent: 'center', 200 | width: width - 200, 201 | overflow: 'hidden' 202 | }, 203 | titleText: { 204 | fontSize: 16, 205 | fontWeight: '500' 206 | }, 207 | rightButton: { 208 | flex: 1, 209 | flexDirection: 'row', 210 | justifyContent: 'flex-end', 211 | alignItems: 'center', 212 | width: 90, 213 | paddingTop: 1, 214 | paddingRight: 8 215 | }, 216 | rightButtonIcon: { 217 | width: 22, 218 | height: 22 219 | }, 220 | rightButtonTitle: { 221 | fontSize: 15 222 | } 223 | }); 224 | 225 | if(Platform.OS === 'ios'){ 226 | styles = { 227 | ...styles, 228 | container: { 229 | // flex: 1, 230 | // position: 'absolute', 231 | // top: 0, 232 | // left: 0, 233 | flexDirection: 'row', 234 | width: width, 235 | paddingTop: 20, 236 | borderBottomWidth: 1 / pixelRation, 237 | borderBottomColor:'#F4F4F4' 238 | } 239 | } 240 | } -------------------------------------------------------------------------------- /app/common/DatePicker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 16/6/22. 3 | * 封装的时间选择器iOS和Android 一起(。・∀・)ノ゙嗨 4 | */ 5 | 'use strict'; 6 | import React, {Component, PropTypes} from 'react'; 7 | import { 8 | View, 9 | Text, 10 | Modal, 11 | TouchableHighlight, 12 | DatePickerAndroid, 13 | TimePickerAndroid, 14 | DatePickerIOS, 15 | Platform, 16 | Animated, 17 | StyleSheet, 18 | TouchableOpacity 19 | } from 'react-native'; 20 | import Moment from 'moment'; 21 | 22 | const FORMATS = { 23 | 'date': 'YYYY-MM-DD', 24 | 'datetime': 'YYYY-MM-DD HH:mm', 25 | 'time': 'HH:mm' 26 | }; 27 | 28 | export default class DatePicker extends Component { 29 | 30 | static propTypes = { 31 | confirmBtnText: PropTypes.string, 32 | cancelBtnText: PropTypes.string, 33 | mode: PropTypes.string, 34 | format: PropTypes.string, 35 | height: PropTypes.number, 36 | duration: PropTypes.number, 37 | defaultDate: PropTypes.string, 38 | maxDate: PropTypes.object, 39 | minDate: PropTypes.object, 40 | onDateChange: PropTypes.func 41 | }; 42 | 43 | static defaultProps = { 44 | confirmBtnText: "确定", 45 | cancelBtnText: "取消", 46 | mode: 'date', 47 | height: 259, // component height: 216(DatePickerIOS) + 1(borderTop) + 42(marginTop), IOS only 48 | duration: 300, // slide animation duration time, default to 300ms, IOS only 49 | }; 50 | 51 | // 构造 52 | constructor(props) { 53 | super(props); 54 | this.format = this.props.format || FORMATS[this.props.mode]; 55 | this.defaultDate = this.props.defaultDate || this.getDateStr(new Date()); 56 | this.state = { 57 | date: this.getDate(), 58 | modalVisible: false, 59 | animatedHeight: new Animated.Value(0), 60 | } 61 | } 62 | 63 | setModalVisible(visible) { 64 | 65 | // slide animation 66 | if (visible) { 67 | Animated.timing( 68 | this.state.animatedHeight, 69 | { 70 | toValue: this.props.height, 71 | duration: this.props.duration 72 | } 73 | ).start(); 74 | this.setState({ 75 | modalVisible: visible 76 | }) 77 | 78 | } else { 79 | 80 | Animated.timing( 81 | this.state.animatedHeight, 82 | { 83 | toValue: 0, 84 | duration: this.props.duration 85 | } 86 | ).start(() => { 87 | this.setState({ 88 | animatedHeight: new Animated.Value(0), 89 | modalVisible: visible 90 | }) 91 | }); 92 | } 93 | } 94 | 95 | onPressCancel() { 96 | this.setModalVisible(false); 97 | } 98 | 99 | onPressConfirm() { 100 | this.datePicked(); 101 | this.setModalVisible(false); 102 | } 103 | 104 | getDate(date = this.defaultDate) { 105 | if (date instanceof Date) { 106 | return date; 107 | } else { 108 | return Moment(date, this.format).toDate(); 109 | } 110 | } 111 | 112 | getDateStr(date = this.defaultDate) { 113 | if (date instanceof Date) { 114 | return Moment(date).format(this.format); 115 | } else { 116 | return Moment(this.getDate(date)).format(this.format); 117 | } 118 | } 119 | 120 | datePicked() { 121 | if (typeof this.props.onDateChange === 'function') { 122 | this.props.onDateChange(this.getDateStr(this.state.date)) 123 | } 124 | } 125 | 126 | onPressDate() { 127 | 128 | // reset state 129 | this.setState({ 130 | date: this.getDate() 131 | }); 132 | 133 | if (Platform.OS === 'ios') { 134 | this.setModalVisible(true); 135 | } else { 136 | 137 | const {minDate, maxDate} = this.props; 138 | // 选日期 139 | if (this.props.mode === 'date') { 140 | 141 | DatePickerAndroid.open({ 142 | date: this.state.date, 143 | minDate: minDate && this.getDate(minDate), 144 | maxDate: maxDate && this.getDate(maxDate) 145 | }).then(({action, year, month, day}) => { 146 | if (action !== DatePickerAndroid.dismissedAction) { 147 | this.setState({ 148 | date: new Date(year, month, day) 149 | }); 150 | this.datePicked(); 151 | } 152 | }); 153 | } else if (this.props.mode === 'time') { 154 | // 选时间 155 | 156 | let timeMoment = Moment(this.state.date); 157 | 158 | TimePickerAndroid.open({ 159 | hour: timeMoment.hour(), 160 | minute: timeMoment.minutes(), 161 | is24Hour: !this.format.match(/h|a/) 162 | }).then(({action, hour, minute}) => { 163 | if (action !== DatePickerAndroid.dismissedAction) { 164 | console.log(Moment().hour(hour).minute(minute).toDate()); 165 | this.setState({ 166 | date: Moment().hour(hour).minute(minute).toDate() 167 | }); 168 | this.datePicked(); 169 | } 170 | }); 171 | } else if (this.props.mode === 'datetime') { 172 | // 选日期和时间 173 | 174 | DatePickerAndroid.open({ 175 | date: this.state.date, 176 | minDate: minDate && this.getDate(minDate), 177 | maxDate: maxDate && this.getDate(maxDate) 178 | }).then(({action, year, month, day}) => { 179 | if (action !== DatePickerAndroid.dismissedAction) { 180 | let timeMoment = Moment(this.state.date); 181 | 182 | TimePickerAndroid.open({ 183 | hour: timeMoment.hour(), 184 | minute: timeMoment.minutes(), 185 | is24Hour: !this.format.match(/h|a/) 186 | }).then(({action, hour, minute}) => { 187 | if (action !== DatePickerAndroid.dismissedAction) { 188 | this.setState({ 189 | date: new Date(year, month, day, hour, minute) 190 | }); 191 | this.datePicked(); 192 | } 193 | }); 194 | } 195 | }); 196 | } else { 197 | new Error('The specified mode is not supported'); 198 | } 199 | } 200 | } 201 | 202 | render() { 203 | 204 | const {confirmBtnText, cancelBtnText, mode, maxDate, minDate} = this.props; 205 | 206 | return ( 207 | 208 | 212 | { 213 | Platform.OS === 'ios' ? 214 | 215 | 220 | 221 | 223 | this.setState({date: date})} 229 | style={[styles.datePicker]} 230 | /> 231 | 235 | {cancelBtnText} 236 | 237 | 241 | {confirmBtnText} 242 | 243 | 244 | : null 245 | } 246 | 247 | 248 | ); 249 | } 250 | } 251 | 252 | const styles = StyleSheet.create({ 253 | datePickerMask: { 254 | flex: 1, 255 | alignItems: 'flex-end', 256 | flexDirection: 'row', 257 | backgroundColor: '#00000077', 258 | }, 259 | datePickerCon: { 260 | backgroundColor: '#fff', 261 | height: 0, 262 | overflow: 'hidden' 263 | }, 264 | btnText: { 265 | position: 'absolute', 266 | top: 0, 267 | height: 42, 268 | padding: 20, 269 | flexDirection: 'row', 270 | alignItems: 'center', 271 | justifyContent: 'center' 272 | }, 273 | btnTextText: { 274 | fontSize: 16, 275 | color: '#B19372' 276 | }, 277 | btnTextCancel: { 278 | color: '#666' 279 | }, 280 | btnCancel: { 281 | left: 0 282 | }, 283 | btnConfirm: { 284 | right: 0 285 | }, 286 | datePicker: { 287 | marginTop: 42, 288 | borderTopColor: '#ccc', 289 | borderTopWidth: 1, 290 | }, 291 | }); 292 | -------------------------------------------------------------------------------- /app/common/PickerAndroid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by DB on 2016/11/4. 3 | */ 4 | import React, {Component, PropTypes} from 'react'; 5 | import { 6 | StyleSheet, 7 | View, 8 | Text, 9 | Image, 10 | Dimensions, 11 | PixelRatio, 12 | PanResponder 13 | } from 'react-native'; 14 | 15 | class PickerAndroidItem extends Component { 16 | 17 | static propTypes = { 18 | value: PropTypes.any, 19 | label: PropTypes.any 20 | }; 21 | 22 | constructor(props, context) { 23 | super(props, context); 24 | } 25 | 26 | render() { 27 | return null; 28 | } 29 | 30 | } 31 | ; 32 | 33 | export default class PickerAndroid extends Component { 34 | 35 | static Item = PickerAndroidItem; 36 | 37 | static propTypes = { 38 | //picker's style 39 | pickerStyle: View.propTypes.style, 40 | //picker item's style 41 | itemStyle: Text.propTypes.style, 42 | //picked value changed then call this function 43 | onValueChange: PropTypes.func, 44 | //default to be selected value 45 | selectedValue: PropTypes.any 46 | }; 47 | 48 | constructor(props, context) { 49 | super(props, context); 50 | this.state = this._stateFromProps(this.props); 51 | } 52 | 53 | componentWillReceiveProps(nextProps) { 54 | this.setState(this._stateFromProps(nextProps)); 55 | } 56 | 57 | shouldComponentUpdate(nextProps, nextState, context) { 58 | return JSON.stringify([{ 59 | selectedIndex: nextState.selectedIndex, 60 | items: nextState.items, 61 | pickerStyle: nextState.pickerStyle, 62 | itemStyle: nextState.itemStyle, 63 | onValueChange: nextState.onValueChange 64 | }, context]) !== JSON.stringify([{ 65 | selectedIndex: this.state.selectedIndex, 66 | items: this.state.items, 67 | pickerStyle: this.state.pickerStyle, 68 | itemStyle: this.state.itemStyle, 69 | onValueChange: this.state.onValueChange 70 | }, this.context]); 71 | } 72 | 73 | _stateFromProps(props) { 74 | let selectedIndex = 0; 75 | let items = []; 76 | let pickerStyle = props.pickerStyle; 77 | let itemStyle = props.itemStyle; 78 | let onValueChange = props.onValueChange; 79 | React.Children.forEach(props.children, (child, index) => { 80 | child.props.value === props.selectedValue && ( selectedIndex = index ); 81 | items.push({value: child.props.value, label: child.props.label}); 82 | }); 83 | //fix issue#https://github.com/beefe/react-native-picker/issues/51 84 | this.index = selectedIndex; 85 | return { 86 | selectedIndex, 87 | items, 88 | pickerStyle, 89 | itemStyle, 90 | onValueChange 91 | }; 92 | } 93 | 94 | _move(dy) { 95 | let index = this.index; 96 | this.middleHeight = Math.abs(-index * 40 + dy); 97 | this.up && this.up.setNativeProps({ 98 | style: { 99 | marginTop: (3 - index) * 30 + dy * .75, 100 | }, 101 | }); 102 | this.middle && this.middle.setNativeProps({ 103 | style: { 104 | marginTop: -index * 40 + dy, 105 | }, 106 | }); 107 | this.down && this.down.setNativeProps({ 108 | style: { 109 | marginTop: (-index - 1) * 30 + dy * .75, 110 | }, 111 | }); 112 | } 113 | 114 | _moveTo(index) { 115 | let _index = this.index; 116 | let diff = _index - index; 117 | let marginValue; 118 | let that = this; 119 | if (diff && !this.isMoving) { 120 | marginValue = diff * 40; 121 | this._move(marginValue); 122 | this.index = index; 123 | this._onValueChange(); 124 | } 125 | } 126 | 127 | //cascade mode will reset the wheel position 128 | moveTo(index) { 129 | this._moveTo(index); 130 | } 131 | 132 | moveUp() { 133 | this._moveTo(Math.max(this.state.items.index - 1, 0)); 134 | } 135 | 136 | moveDown() { 137 | this._moveTo(Math.min(this.index + 1, this.state.items.length - 1)); 138 | } 139 | 140 | _handlePanResponderMove(evt, gestureState) { 141 | let dy = gestureState.dy; 142 | if (this.isMoving) { 143 | return; 144 | } 145 | // turn down 146 | if (dy > 0) { 147 | this._move(dy > this.index * 40 ? this.index * 40 : dy); 148 | } else { 149 | this._move(dy < (this.index - this.state.items.length + 1) * 40 ? (this.index - this.state.items.length + 1) * 40 : dy); 150 | } 151 | } 152 | 153 | _handlePanResponderRelease(evt, gestureState) { 154 | let middleHeight = this.middleHeight; 155 | this.index = middleHeight % 40 >= 20 ? Math.ceil(middleHeight / 40) : Math.floor(middleHeight / 40); 156 | this._move(0); 157 | this._onValueChange(); 158 | } 159 | 160 | componentWillMount() { 161 | this._panResponder = PanResponder.create({ 162 | onMoveShouldSetPanResponder: (evt, gestureState) => true, 163 | onMoveShouldSetPanResponderCapture: (evt, gestureState) => true, 164 | onPanResponderRelease: this._handlePanResponderRelease.bind(this), 165 | onPanResponderMove: this._handlePanResponderMove.bind(this) 166 | }); 167 | this.isMoving = false; 168 | this.index = this.state.selectedIndex; 169 | } 170 | 171 | componentWillUnmount() { 172 | this.timer && clearInterval(this.timer); 173 | } 174 | 175 | _renderItems(items) { 176 | //value was used to watch the change of picker 177 | //label was used to display 178 | let upItems = [], middleItems = [], downItems = []; 179 | items.forEach((item, index) => { 180 | 181 | upItems[index] = { 185 | this._moveTo(index); 186 | }}> 187 | {item.label} 188 | ; 189 | 190 | middleItems[index] = {item.label} 193 | ; 194 | 195 | downItems[index] = { 199 | this._moveTo(index); 200 | }}> 201 | {item.label} 202 | ; 203 | 204 | }); 205 | return {upItems, middleItems, downItems,}; 206 | } 207 | 208 | _onValueChange() { 209 | //the current picked label was more expected to be passed,
 210 | // but PickerIOS only passed value, so we set label to be the second argument
 211 | // add by zooble @2015-12-10
 212 | let curItem = this.state.items[this.index]; 213 | this.state.onValueChange && this.state.onValueChange(curItem.value, this.index); 214 | } 215 | 216 | render() { 217 | let index = this.state.selectedIndex; 218 | let length = this.state.items.length; 219 | let items = this._renderItems(this.state.items); 220 | 221 | let upViewStyle = { 222 | marginTop: (3 - index) * 30, 223 | height: length * 30, 224 | }; 225 | let middleViewStyle = { 226 | marginTop: -index * 40, 227 | }; 228 | let downViewStyle = { 229 | marginTop: (-index - 1) * 30, 230 | height: length * 30, 231 | }; 232 | 233 | return ( 234 | //total to be 90*2+40=220 height 235 | 236 | 237 | 238 | { 239 | this.up = up 240 | }}> 241 | { items.upItems } 242 | 243 | 244 | 245 | 246 | { 247 | this.middle = middle 248 | }}> 249 | { items.middleItems } 250 | 251 | 252 | 253 | 254 | { 255 | this.down = down 256 | }}> 257 | { items.downItems } 258 | 259 | 260 | 261 | 262 | ); 263 | } 264 | 265 | }; 266 | 267 | let width = Dimensions.get('window').width; 268 | let height = Dimensions.get('window').height; 269 | let ratio = PixelRatio.get(); 270 | let styles = StyleSheet.create({ 271 | 272 | container: { 273 | flex: 1, 274 | justifyContent: 'center', 275 | alignItems: 'center', 276 | //this is very important 277 | backgroundColor: null 278 | }, 279 | up: { 280 | height: 90, 281 | overflow: 'hidden' 282 | }, 283 | upView: { 284 | justifyContent: 'flex-start', 285 | alignItems: 'center' 286 | }, 287 | upText: { 288 | paddingTop: 0, 289 | height: 30, 290 | fontSize: 16, 291 | color: '#000', 292 | opacity: .5, 293 | paddingBottom: 0, 294 | marginTop: 0, 295 | marginBottom: 0 296 | }, 297 | middle: { 298 | height: 40, 299 | width: width, 300 | overflow: 'hidden', 301 | borderColor: '#aaa', 302 | borderTopWidth: 1 / ratio, 303 | borderBottomWidth: 1 / ratio 304 | }, 305 | middleView: { 306 | height: 40, 307 | justifyContent: 'flex-start', 308 | alignItems: 'center', 309 | }, 310 | middleText: { 311 | paddingTop: 0, 312 | height: 40, 313 | color: '#000', 314 | fontSize: 24, 315 | paddingBottom: 0, 316 | marginTop: 0, 317 | marginBottom: 0 318 | }, 319 | down: { 320 | height: 90, 321 | overflow: 'hidden', 322 | }, 323 | downView: { 324 | overflow: 'hidden', 325 | justifyContent: 'flex-start', 326 | alignItems: 'center', 327 | }, 328 | downText: { 329 | paddingTop: 0, 330 | height: 30, 331 | fontSize: 16, 332 | color: '#000', 333 | opacity: .5, 334 | paddingBottom: 0, 335 | marginTop: 0, 336 | marginBottom: 0 337 | } 338 | 339 | }); -------------------------------------------------------------------------------- /app/common/Popover.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, { PropTypes } from 'react'; 4 | import { 5 | StyleSheet, 6 | Dimensions, 7 | Animated, 8 | Text, 9 | TouchableWithoutFeedback, 10 | View, 11 | Easing 12 | } from 'react-native'; 13 | 14 | var noop = () => {}; 15 | 16 | var {height: SCREEN_HEIGHT, width: SCREEN_WIDTH} = Dimensions.get('window'); 17 | var DEFAULT_ARROW_SIZE = new Size(10, 5); 18 | 19 | function Point(x, y) { 20 | this.x = x; 21 | this.y = y; 22 | } 23 | 24 | function Size(width, height) { 25 | this.width = width; 26 | this.height = height; 27 | } 28 | 29 | function Rect(x, y, width, height) { 30 | this.x = x; 31 | this.y = y; 32 | this.width = width; 33 | this.height = height; 34 | } 35 | 36 | var Popover = React.createClass({ 37 | propTypes: { 38 | isVisible: PropTypes.bool, 39 | onClose: PropTypes.func, 40 | contentStyle:View.propTypes.style, 41 | }, 42 | getInitialState() { 43 | return { 44 | contentSize: {}, 45 | anchorPoint: {}, 46 | popoverOrigin: {}, 47 | placement: 'auto', 48 | isTransitioning: false, 49 | defaultAnimatedValues: { 50 | scale: new Animated.Value(0), 51 | translate: new Animated.ValueXY(), 52 | fade: new Animated.Value(0), 53 | }, 54 | }; 55 | }, 56 | getDefaultProps() { 57 | return { 58 | isVisible: false, 59 | displayArea: new Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT), 60 | arrowSize: DEFAULT_ARROW_SIZE, 61 | placement: 'auto', 62 | onClose: noop, 63 | }; 64 | }, 65 | measureContent(x) { 66 | var {width, height} = x.nativeEvent.layout; 67 | var contentSize = {width, height}; 68 | var geom = this.computeGeometry({contentSize}); 69 | 70 | var isAwaitingShow = this.state.isAwaitingShow; 71 | this.setState(Object.assign(geom, 72 | {contentSize, isAwaitingShow: undefined}), () => { 73 | // Once state is set, call the showHandler so it can access all the geometry 74 | // from the state 75 | isAwaitingShow && this._startAnimation({show: true}); 76 | }); 77 | }, 78 | computeGeometry({contentSize, placement}) { 79 | placement = placement || this.props.placement; 80 | 81 | var options = { 82 | displayArea: this.props.displayArea, 83 | fromRect: this.props.fromRect, 84 | arrowSize: this.getArrowSize(placement), 85 | contentSize, 86 | } 87 | 88 | switch (placement) { 89 | case 'top': 90 | return this.computeTopGeometry(options); 91 | case 'bottom': 92 | return this.computeBottomGeometry(options); 93 | case 'left': 94 | return this.computeLeftGeometry(options); 95 | case 'right': 96 | return this.computeRightGeometry(options); 97 | default: 98 | return this.computeAutoGeometry(options); 99 | } 100 | }, 101 | computeTopGeometry({displayArea, fromRect, contentSize, arrowSize}) { 102 | var popoverOrigin = new Point( 103 | Math.min(displayArea.x + displayArea.width - contentSize.width, 104 | Math.max(displayArea.x, fromRect.x + (fromRect.width - contentSize.width) / 2)), 105 | fromRect.y - contentSize.height - arrowSize.height); 106 | var anchorPoint = new Point(fromRect.x + fromRect.width / 2.0, fromRect.y); 107 | 108 | return { 109 | popoverOrigin, 110 | anchorPoint, 111 | placement: 'top', 112 | } 113 | }, 114 | computeBottomGeometry({displayArea, fromRect, contentSize, arrowSize}) { 115 | var popoverOrigin = new Point( 116 | Math.min(displayArea.x + displayArea.width - contentSize.width, 117 | Math.max(displayArea.x, fromRect.x + (fromRect.width - contentSize.width) / 2)), 118 | fromRect.y + fromRect.height + arrowSize.height); 119 | var anchorPoint = new Point(fromRect.x + fromRect.width / 2.0, fromRect.y + fromRect.height); 120 | 121 | return { 122 | popoverOrigin, 123 | anchorPoint, 124 | placement: 'bottom', 125 | } 126 | }, 127 | computeLeftGeometry({displayArea, fromRect, contentSize, arrowSize}) { 128 | var popoverOrigin = new Point(fromRect.x - contentSize.width - arrowSize.width, 129 | Math.min(displayArea.y + displayArea.height - contentSize.height, 130 | Math.max(displayArea.y, fromRect.y + (fromRect.height - contentSize.height) / 2))); 131 | var anchorPoint = new Point(fromRect.x, fromRect.y + fromRect.height / 2.0); 132 | 133 | return { 134 | popoverOrigin, 135 | anchorPoint, 136 | placement: 'left', 137 | } 138 | }, 139 | computeRightGeometry({displayArea, fromRect, contentSize, arrowSize}) { 140 | var popoverOrigin = new Point(fromRect.x + fromRect.width + arrowSize.width, 141 | Math.min(displayArea.y + displayArea.height - contentSize.height, 142 | Math.max(displayArea.y, fromRect.y + (fromRect.height - contentSize.height) / 2))); 143 | var anchorPoint = new Point(fromRect.x + fromRect.width, fromRect.y + fromRect.height / 2.0); 144 | 145 | return { 146 | popoverOrigin, 147 | anchorPoint, 148 | placement: 'right', 149 | } 150 | }, 151 | computeAutoGeometry({displayArea, contentSize}) { 152 | var placementsToTry = ['left', 'right', 'bottom', 'top']; 153 | 154 | for (var i = 0; i < placementsToTry.length; i++) { 155 | var placement = placementsToTry[i]; 156 | var geom = this.computeGeometry({contentSize: contentSize, placement: placement}); 157 | var {popoverOrigin} = geom; 158 | 159 | if (popoverOrigin.x >= displayArea.x 160 | && popoverOrigin.x <= displayArea.x + displayArea.width - contentSize.width 161 | && popoverOrigin.y >= displayArea.y 162 | && popoverOrigin.y <= displayArea.y + displayArea.height - contentSize.height) { 163 | break; 164 | } 165 | } 166 | 167 | return geom; 168 | }, 169 | getArrowSize(placement) { 170 | var size = this.props.arrowSize; 171 | switch(placement) { 172 | case 'left': 173 | case 'right': 174 | return new Size(size.height, size.width); 175 | default: 176 | return size; 177 | } 178 | }, 179 | getArrowColorStyle(color) { 180 | return { borderTopColor: color }; 181 | }, 182 | getArrowRotation(placement) { 183 | switch (placement) { 184 | case 'bottom': 185 | return '180deg'; 186 | case 'left': 187 | return '-90deg'; 188 | case 'right': 189 | return '90deg'; 190 | default: 191 | return '0deg'; 192 | } 193 | }, 194 | getArrowDynamicStyle() { 195 | var {anchorPoint, popoverOrigin} = this.state; 196 | var arrowSize = this.props.arrowSize; 197 | 198 | // Create the arrow from a rectangle with the appropriate borderXWidth set 199 | // A rotation is then applied dependending on the placement 200 | // Also make it slightly bigger 201 | // to fix a visual artifact when the popover is animated with a scale 202 | var width = arrowSize.width + 2; 203 | var height = arrowSize.height * 2 + 2; 204 | 205 | return { 206 | left: anchorPoint.x - popoverOrigin.x - width / 2, 207 | top: anchorPoint.y - popoverOrigin.y - height / 2, 208 | width: width, 209 | height: height, 210 | borderTopWidth: height / 2, 211 | borderRightWidth: width / 2, 212 | borderBottomWidth: height / 2, 213 | borderLeftWidth: width / 2, 214 | } 215 | }, 216 | getTranslateOrigin() { 217 | var {contentSize, popoverOrigin, anchorPoint} = this.state; 218 | var popoverCenter = new Point(popoverOrigin.x + contentSize.width / 2, 219 | popoverOrigin.y + contentSize.height / 2); 220 | return new Point(anchorPoint.x - popoverCenter.x, anchorPoint.y - popoverCenter.y); 221 | }, 222 | componentWillReceiveProps(nextProps) { 223 | var willBeVisible = nextProps.isVisible; 224 | var { 225 | isVisible, 226 | } = this.props; 227 | 228 | if (willBeVisible !== isVisible) { 229 | if (willBeVisible) { 230 | // We want to start the show animation only when contentSize is known 231 | // so that we can have some logic depending on the geometry 232 | this.setState({contentSize: {}, isAwaitingShow: true}); 233 | } else { 234 | this._startAnimation({show: false}); 235 | } 236 | } 237 | }, 238 | _startAnimation({show}) { 239 | var handler = this.props.startCustomAnimation || this._startDefaultAnimation; 240 | handler({show, doneCallback: () => this.setState({isTransitioning: false})}); 241 | this.setState({isTransitioning: true}); 242 | }, 243 | _startDefaultAnimation({show, doneCallback}) { 244 | var animDuration = 300; 245 | var values = this.state.defaultAnimatedValues; 246 | var translateOrigin = this.getTranslateOrigin(); 247 | 248 | if (show) { 249 | values.translate.setValue(translateOrigin); 250 | } 251 | 252 | var commonConfig = { 253 | duration: animDuration, 254 | easing: show ? Easing.out(Easing.back()) : Easing.inOut(Easing.quad), 255 | } 256 | 257 | Animated.parallel([ 258 | Animated.timing(values.fade, { 259 | toValue: show ? 1 : 0, 260 | ...commonConfig, 261 | }), 262 | Animated.timing(values.translate, { 263 | toValue: show ? new Point(0, 0) : translateOrigin, 264 | ...commonConfig, 265 | }), 266 | Animated.timing(values.scale, { 267 | toValue: show ? 1 : 0, 268 | ...commonConfig, 269 | }) 270 | ]).start(doneCallback); 271 | }, 272 | _getDefaultAnimatedStyles() { 273 | // If there's a custom animation handler, 274 | // we don't return the default animated styles 275 | if (typeof this.props.startCustomAnimation !== 'undefined') { 276 | return null; 277 | } 278 | 279 | var animatedValues = this.state.defaultAnimatedValues; 280 | 281 | return { 282 | backgroundStyle: { 283 | opacity: animatedValues.fade, 284 | }, 285 | arrowStyle: { 286 | transform: [ 287 | { 288 | scale: animatedValues.scale.interpolate({ 289 | inputRange: [0, 1], 290 | outputRange: [0, 1], 291 | extrapolate: 'clamp', 292 | }), 293 | } 294 | ], 295 | }, 296 | contentStyle: { 297 | transform: [ 298 | {translateX: animatedValues.translate.x}, 299 | {translateY: animatedValues.translate.y}, 300 | {scale: animatedValues.scale}, 301 | ], 302 | } 303 | }; 304 | }, 305 | _getExtendedStyles() { 306 | var background = []; 307 | var popover = []; 308 | var arrow = []; 309 | var content = []; 310 | 311 | [this._getDefaultAnimatedStyles(), this.props].forEach((source) => { 312 | if (source) { 313 | background.push(source.backgroundStyle); 314 | popover.push(source.popoverStyle); 315 | arrow.push(source.arrowStyle); 316 | content.push(source.contentStyle); 317 | } 318 | }); 319 | 320 | return { 321 | background, 322 | popover, 323 | arrow, 324 | content, 325 | } 326 | }, 327 | render() { 328 | if (!this.props.isVisible && !this.state.isTransitioning) { 329 | return null; 330 | } 331 | 332 | var {popoverOrigin, placement} = this.state; 333 | var extendedStyles = this._getExtendedStyles(); 334 | var contentStyle = [styles.content, ...extendedStyles.content]; 335 | var arrowColor = StyleSheet.flatten(contentStyle).backgroundColor; 336 | var arrowColorStyle = this.getArrowColorStyle(arrowColor); 337 | var arrowDynamicStyle = this.getArrowDynamicStyle(); 338 | var contentSizeAvailable = this.state.contentSize.width; 339 | 340 | // Special case, force the arrow rotation even if it was overriden 341 | var arrowStyle = [styles.arrow, arrowDynamicStyle, arrowColorStyle, ...extendedStyles.arrow]; 342 | var arrowTransform = (StyleSheet.flatten(arrowStyle).transform || []).slice(0); 343 | arrowTransform.unshift({rotate: this.getArrowRotation(placement)}); 344 | arrowStyle = [...arrowStyle, {transform: arrowTransform}]; 345 | var contentMarginRight=this.props.contentMarginRight? this.props.contentMarginRight:0; 346 | return ( 347 | 348 | 349 | 350 | 354 | {/* 隐藏箭头*/} 355 | 356 | {this.props.children} 357 | 358 | 359 | 360 | 361 | ); 362 | } 363 | }); 364 | 365 | 366 | var styles = StyleSheet.create({ 367 | container: { 368 | opacity: 0, 369 | top: 0, 370 | bottom: 0, 371 | left: 0, 372 | right: 0, 373 | position: 'absolute', 374 | backgroundColor: 'transparent', 375 | }, 376 | containerVisible: { 377 | opacity: 1, 378 | }, 379 | background: { 380 | top: 0, 381 | bottom: 0, 382 | left: 0, 383 | right: 0, 384 | position: 'absolute', 385 | //隐藏背景 backgroundColor: 'rgba(0,0,0,0.5)', 386 | }, 387 | popover: { 388 | backgroundColor: 'transparent', 389 | position: 'absolute', 390 | }, 391 | content: { 392 | borderRadius: 3, 393 | padding: 6, 394 | backgroundColor: '#fff', 395 | shadowColor: 'gray', 396 | shadowOffset: {width: 2, height: 2}, 397 | shadowRadius: 2, 398 | shadowOpacity: 0.8, 399 | }, 400 | arrow: { 401 | position: 'absolute', 402 | borderTopColor: 'transparent', 403 | borderRightColor: 'transparent', 404 | borderBottomColor: 'transparent', 405 | borderLeftColor: 'transparent', 406 | }, 407 | }); 408 | 409 | module.exports = Popover; 410 | --------------------------------------------------------------------------------