├── 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 |
--------------------------------------------------------------------------------