├── lib ├── index.js ├── constants.js ├── types.js ├── manager.js ├── area.js ├── scene.js └── router.js ├── .gitignore ├── package.json ├── LICENSE └── README.md /lib/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | Active, 5 | Inactive, 6 | MightActive, 7 | MightInactive, 8 | 9 | FromLeft, 10 | FromRight, 11 | FromTop, 12 | FromBottom, 13 | Static 14 | } from './constants' 15 | 16 | export { Router } from './router' 17 | export { scene } from './manager' 18 | 19 | export const Status = { 20 | Active, 21 | Inactive, 22 | MightActive, 23 | MightInactive 24 | } 25 | 26 | export const Side = { 27 | FromLeft, 28 | FromRight, 29 | FromTop, 30 | FromBottom, 31 | Static 32 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IJ 26 | # 27 | .idea 28 | .gradle 29 | local.properties 30 | 31 | # node.js 32 | # 33 | node_modules/ 34 | npm-debug.log 35 | 36 | .tags 37 | .tags1 38 | 39 | tmp 40 | .vscode 41 | .flowconfig 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scene-router", 3 | "version": "2.0.6", 4 | "description": "A complete scene routing library for react native", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/pressly/scene-router.git" 12 | }, 13 | "dependencies": { 14 | "trie-route": "0.0.5" 15 | }, 16 | "keywords": [ 17 | "react-component", 18 | "react-native", 19 | "scene-manager", 20 | "router" 21 | ], 22 | "author": { 23 | "name": "Ali Najafizadeh", 24 | "email": "ali@pressly.com", 25 | "url": "https://github.com/alinz", 26 | "company": "https://pressly.com" 27 | }, 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/pressly/scene-router/issues" 31 | }, 32 | "homepage": "https://github.com/pressly/scene-router" 33 | } 34 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Platform, Dimensions } from 'react-native' 4 | 5 | // Scene Status /////////////////////////////////////////////////////////////// 6 | 7 | export const Active = 1 8 | export const Inactive = 2 9 | export const MightActive = 3 10 | export const MightInactive = 4 11 | 12 | // Scene Animation Side /////////////////////////////////////////////////////// 13 | 14 | export const FromLeft = 5 15 | export const FromRight = 6 16 | export const FromTop = 7 17 | export const FromBottom = 8 18 | export const Static = 9 19 | 20 | // window and screen ////////////////////////////////////////////////////////// 21 | 22 | export const isAndroid = Platform.OS === 'android' 23 | 24 | export const window = (() => { 25 | let window = Dimensions.get('window') 26 | window.softMenuHeight = 0 27 | 28 | if (isAndroid) { 29 | let screen = Dimensions.get('screen') 30 | window.softMenuHeight = screen.height - window.height 31 | } 32 | 33 | return window 34 | })() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ali Najafizadeh, Pressly Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type GestureStatus 4 | = 'open' 5 | | 'close' 6 | | 'cancel' 7 | 8 | export type Point = { 9 | x: number, 10 | y: number 11 | } 12 | 13 | export type SceneConfig = { 14 | path: string, 15 | side?: number, 16 | threshold?: number, 17 | gesture?: boolean, 18 | reset?: boolean, 19 | backgroundColor?: string 20 | } 21 | 22 | // this object type will be pass as `route` props to 23 | // user's scene 24 | export type Route = { 25 | path: string, 26 | params: Object, 27 | qs: Object, 28 | props: Object, 29 | config: ?SceneConfig // this is just a raw information, it's good for debuging 30 | // config will be set inside scene's render 31 | } 32 | 33 | export type RouterExtra = { 34 | path: string, 35 | props: Object, 36 | customSceneConfig: SceneConfig 37 | } 38 | 39 | export interface Router { 40 | path(path: string, callback: (params: Object, qs: Object, extra: RouterExtra) => void): void; 41 | process(path: string, extra: Object): void; 42 | } 43 | 44 | export type SceneResolver = ( 45 | scene: Function, 46 | route: Route, 47 | originalSceneConfig: SceneConfig, 48 | customSceneConfig: SceneConfig 49 | ) => void 50 | 51 | export type SceneRejecter = (err: string) => void 52 | 53 | 54 | -------------------------------------------------------------------------------- /lib/manager.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import route from 'trie-route' 5 | 6 | import * as constants from './constants' 7 | import { Scene } from './scene' 8 | 9 | import type { SceneConfig, Route, Router, SceneResolver, SceneRejecter, RouterExtra, GestureStatus } from './types' 10 | 11 | // types ////////////////////////////////////////////////////////////////////// 12 | 13 | type SceneWrapProps = { 14 | sceneRef: (ref: any) => void, 15 | customSceneConfig: SceneConfig, 16 | route: Route, 17 | onGesture: (status: GestureStatus) => void 18 | } 19 | 20 | // internal functions ///////////////////////////////////////////////////////// 21 | 22 | const mergeDefaultSceneOptions = (options: SceneConfig): SceneConfig => { 23 | return { 24 | side: constants.FromRight, 25 | threshold: 30, 26 | gesture: true, 27 | reset: false, 28 | backgroundColor: 'white', 29 | ...options 30 | } 31 | } 32 | 33 | // what we have to do is, 34 | const mergeCustomSceneOptions = (currentOpts: SceneConfig, userOpts: SceneConfig): SceneConfig => { 35 | // currentOpts is the one which was configured at scene decorator. 36 | // userOpts is the one which was passed by router to chnage the behaviour 37 | // so, we need to make sure to remove userOpts.path and merge it with currentOpts 38 | delete userOpts.path 39 | 40 | return { 41 | ...currentOpts, 42 | ...userOpts 43 | } 44 | } 45 | 46 | // SceneManager /////////////////////////////////////////////////////////////// 47 | 48 | export class SceneManager { 49 | router: Router 50 | resolver: ?SceneResolver 51 | rejecter: ?SceneRejecter 52 | 53 | constructor() { 54 | this.router = route.create() 55 | } 56 | 57 | register(SceneWrap: Function, originalSceneConfig: SceneConfig) { 58 | this.router.path(originalSceneConfig.path, (params: Object = {}, qs: Object = {}, extra: RouterExtra) => { 59 | const { path, props, customSceneConfig } = extra 60 | const route = { 61 | path, 62 | params, 63 | qs, 64 | props, 65 | config: null 66 | } 67 | this.resolver && this.resolver(SceneWrap, route, originalSceneConfig, customSceneConfig) 68 | }) 69 | } 70 | 71 | request(path: string, props: ?Object = {}, customSceneConfig: SceneConfig) { 72 | const err = this.router.process(path, { path, props, customSceneConfig }) 73 | if (err) { 74 | this.rejecter && this.rejecter(`'${path}' ${err}`) 75 | } 76 | } 77 | 78 | // this callback will be set by Router class 79 | setSceneResolver(sceneResolver: SceneResolver) { 80 | this.resolver = sceneResolver 81 | } 82 | 83 | setSceneRejecter(sceneRejecter: SceneRejecter) { 84 | this.rejecter = sceneRejecter 85 | } 86 | } 87 | 88 | export const sceneManager = new SceneManager() 89 | 90 | // dcecorators ///////////////////////////////////////////////////////////////// 91 | 92 | export const scene = (originalSceneConfig: SceneConfig): Function => { 93 | originalSceneConfig = mergeDefaultSceneOptions(originalSceneConfig) 94 | 95 | return (WrapComponent: Function): Function => { 96 | const SceneWrap = (props: SceneWrapProps): React.Element => { 97 | const { customSceneConfig, route, onGesture } = props 98 | const sceneConfig = mergeCustomSceneOptions(originalSceneConfig, customSceneConfig) 99 | 100 | return ( 101 | 107 | ) 108 | } 109 | 110 | sceneManager.register(SceneWrap, originalSceneConfig) 111 | 112 | return WrapComponent 113 | } 114 | } -------------------------------------------------------------------------------- /lib/area.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { Component } from 'react' 4 | import { View, StyleSheet } from 'react-native' 5 | 6 | import * as constants from './constants' 7 | import { Scene } from './scene' 8 | 9 | import type { SceneConfig, Route, GestureStatus } from './types' 10 | 11 | // types ////////////////////////////////////////////////////////////////////// 12 | 13 | type AreaProps = { 14 | children?: any 15 | } 16 | 17 | type AreaState = { 18 | scenes: Array>, 19 | sceneRefs: Array 20 | } 21 | 22 | // constants ////////////////////////////////////////////////////////////////// 23 | 24 | const sizeOfArea = 3 25 | const window = constants.window 26 | const styles = StyleSheet.create({ 27 | container: { 28 | flex: 1, 29 | position: 'absolute', 30 | width: sizeOfArea * window.width, 31 | height: sizeOfArea * window.height, 32 | backgroundColor: 'transparent', 33 | transform: [{ translateX: -window.width }, { translateY: -window.height }], 34 | overflow: 'hidden' 35 | }, 36 | staticView: { 37 | position: 'absolute', 38 | overflow: 'hidden', 39 | width: window.width, 40 | height: window.height, 41 | transform: [{ translateX: window.width }, { translateY: window.height }], 42 | } 43 | }) 44 | 45 | // Area Component ///////////////////////////////////////////////////////////// 46 | // responsibility 47 | // - add or remove scene 48 | // - call updateSceneStatus on previous scene once a new scene is added or removed 49 | // - provide a callback to each scene to be called once the gesture is happining 50 | 51 | let sceneIdCount: number = 0 52 | 53 | export class Area extends Component { 54 | props: AreaProps 55 | state: AreaState 56 | 57 | constructor(props: AreaProps, context: any) { 58 | super(props, context) 59 | 60 | this.state = { 61 | scenes: [], 62 | sceneRefs: [] 63 | } 64 | } 65 | 66 | get currentIndex(): number { 67 | return this.state.scenes.length - 1 68 | } 69 | 70 | get currentSceneRef(): ?Scene { 71 | const index = this.currentIndex 72 | return index > -1 ? this.state.sceneRefs[index] : null 73 | } 74 | 75 | get previousSceneRef(): ?Scene { 76 | const index = this.currentIndex - 1 77 | return index > -1 ? this.state.sceneRefs[index] : null 78 | } 79 | 80 | onGesture = (status: GestureStatus) => { 81 | const currSceneRef = this.currentSceneRef 82 | const prevSceneRef = this.previousSceneRef 83 | 84 | switch(status) { 85 | case 'open': 86 | currSceneRef && currSceneRef.updateSceneStatus(constants.MightInactive) 87 | prevSceneRef && prevSceneRef.updateSceneStatus(constants.MightActive) 88 | break 89 | 90 | case 'close': 91 | // remove the scene 92 | this.state.scenes.pop() 93 | this.state.sceneRefs.pop() 94 | 95 | // set the new state, this will rerender the area and once it's done, 96 | // we simply call the updateSceneStatus 97 | this.setState(this.state, () => { 98 | prevSceneRef && prevSceneRef.updateSceneStatus(constants.Active) 99 | }) 100 | break 101 | 102 | case 'cancel': 103 | currSceneRef && currSceneRef.updateSceneStatus(constants.Active) 104 | prevSceneRef && prevSceneRef.updateSceneStatus(constants.Inactive) 105 | break 106 | } 107 | } 108 | 109 | // this Scene is SceneWrap function. 110 | // so we need to get the ref of scene itself 111 | push(SceneWrap: Function, route: Route, originalSceneConfig: SceneConfig, customSceneConfig: SceneConfig, done: ?Function) { 112 | let currSceneRef = this.currentSceneRef 113 | currSceneRef && currSceneRef.updateSceneStatus(constants.Inactive) 114 | 115 | this.state.scenes.push( 116 | { 119 | ref && this.state.sceneRefs.push(ref) 120 | }} 121 | onGesture={this.onGesture} 122 | customSceneConfig={customSceneConfig} 123 | route={route} 124 | /> 125 | ) 126 | 127 | this.setState(this.state, () => { 128 | currSceneRef = this.currentSceneRef 129 | if (currSceneRef) { 130 | currSceneRef.open(() => { 131 | // at this point, animation is done, and scene is visible 132 | // we need to reset all the items inside array up to this scene 133 | // if customSceneConfig.reset is true and then call another setState. 134 | if (customSceneConfig.reset) { 135 | this.state.scenes.splice(0, this.state.scenes.length - 1) 136 | this.state.sceneRefs.splice(0, this.state.sceneRefs.length - 1) 137 | // the reason we don't call updateSceneStatus right away, because, 138 | // we still need to wait for render to be done then we can safely 139 | // call updateSceneStatus and done 140 | this.setState(this.state, () => { 141 | currSceneRef && currSceneRef.updateSceneStatus(constants.Active) 142 | done && done() 143 | }) 144 | } else { 145 | // because reset is false, we can call these two methods right away 146 | currSceneRef && currSceneRef.updateSceneStatus(constants.Active) 147 | done && done() 148 | } 149 | }) 150 | } 151 | }) 152 | } 153 | 154 | pop(done: ?Function) { 155 | // if the current index is zero, it means that 156 | // we can't pop the view. This is the first view 157 | if (this.currentIndex < 1) { 158 | return 159 | } 160 | 161 | // because the current scene will be destroyed, 162 | // there is no point of calling `updateSceneStatus(Status.Inactive)` 163 | // we are letting `componentWillUnmount` does the job 164 | //this.previousSceneRef. 165 | const currSceneRef = this.currentSceneRef 166 | const prevSceneRef = this.previousSceneRef 167 | 168 | currSceneRef && currSceneRef.close(() => { 169 | // remove the scene 170 | this.state.scenes.pop() 171 | this.state.sceneRefs.pop() 172 | 173 | // set the new state, this will rerender the area and once it's done, 174 | // we simply call the updateSceneStatus 175 | this.setState(this.state, () => { 176 | prevSceneRef && prevSceneRef.updateSceneStatus(constants.Active) 177 | done && done() 178 | }) 179 | }) 180 | } 181 | 182 | resetAllScenes(done: ?Function) { 183 | this.setState({ 184 | scenes: [], 185 | sceneRefs: [] 186 | }, () => { 187 | done && done() 188 | }) 189 | } 190 | 191 | render() { 192 | const { scenes } = this.state 193 | 194 | return ( 195 | 196 | 197 | {this.props.children} 198 | 199 | {scenes} 200 | 201 | ) 202 | } 203 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## scene-Router (v2) 2 | 3 | A complete scene routing library written in pure JavaScript for React Native. It supports **iOS** and **Android**. 4 | The api is so easy that you just have to learn only 2 simple things, `scene` decorator and `Router` component. 5 | 6 | ## Description 7 | 8 | We, at [Pressly](https://pressly.com), using react-native and our app consists of large number of scenes. We wanted somthing super simple, it went through a lot of internal reversion, until we decided to open source it. 9 | 10 | `scene-router` supports the following out of box: 11 | 12 | - url like path, which can contains `params` and `query strings`. e.g. '/user/:id' 13 | - passing custom props to target scene 14 | - storeless, you can connect to `redux` or `mobx` stores 15 | - reset stack of scenes 16 | - animating scene from all 4 direction 17 | - gesture enable 18 | - configure scene settings at scene difinition and route time 19 | - all animation are being configure to optimize `useNativeDriver` feature 20 | 21 | ## Installation 22 | 23 | ```bash 24 | npm install scene-router 25 | ``` 26 | 27 | ## Prerequisite 28 | 29 | we recommended to use decorator. It makes the code a lot easier to maintain. If you don't want to use it, that's ok too. 30 | 31 | in order to enable decorator in your `react-native` project follow the 3 steps below, 32 | 33 | 1: install `babel-plugin-transform-decorators-legacy` using `yarn` or `npm` and 34 | 2: configure your `.babelrc` file as follows 35 | 36 | ```js 37 | { 38 | "extends": "react-native/packager/react-packager/rn-babelrc.json", 39 | "plugins": ["transform-decorators-legacy"] 40 | } 41 | ``` 42 | 43 | 3: if you are using flowtype, make sure to add `esproposal.decorators=ignore` under `[options]` tags inside `.flowconfig` file 44 | 45 | ## Usage 46 | 47 | There are 2 things you should learn about `scene-router` in oreder to start using it in your project 48 | 49 | ### `scene` decorator 50 | 51 | `scene` is a decorator feature which register your component as a scene. here's an example 52 | 53 | ```js 54 | import React, { Component } from 'react' 55 | import { scene } from 'scene-router' 56 | 57 | @scene({ 58 | path: '/scene1' 59 | }) 60 | class MyFirstScene extends Component { 61 | render() { 62 | ... 63 | } 64 | } 65 | ``` 66 | 67 | a little bit of explaination, what we did was adding `scene` as a decorator on top of our first component `MyFirstScene`. We were passing `path`. This is our path to this scene. 68 | so every time, `Router` wants to render this scene, all it needs a path url. `scene-router` will handle all coordinations and animations behind the scene. 69 | 70 | you might ask, so what if I want to show my scene from top to bottom. As I said, `scene` decorator accepts many arguments. except `path` the rest of the arguments are optional. 71 | here's the list of all options 72 | 73 | | option | type | required | default value | route time change | Description | 74 | | ------- | ---- | -------- | ------------- | ----- | ----- | 75 | | path | String | Yes | N/A | No | register a scene with this unique path | 76 | | side | Side | No | Side.FromRight | Yes | how the scene will animated, from which side | 77 | | threshold | Number | No | 30 | Yes | how far from side you have to swip to make the gesture working | 78 | | gesture | Boolean | No | true | Yes | enable or disable gesture | 79 | | reset | Boolean | No | false | Yes | all scenes prior to this scene will be destroyed | 80 | | backgroundColor | String | No | white | Yes | the back color of each scene | 81 | 82 | ther are 5 differant sides you can choose from 83 | 84 | | name| description | 85 | | --- | ----------- | 86 | | FromLeft | Animate the Scene From Left to Right | 87 | | FromRight | Animate the Scene From Right to Left | 88 | | FromTop | Animate the Scene From Top to Bottom | 89 | | FromBottom | Animate the Scene From Bottom to Top | 90 | | Static | No animation | 91 | 92 | 93 | There is one little thing, every component which decorated with `scene` will have a method called, `updateSceneStatus(status: Status)`. This method will be called based on whether your scene is 94 | `Active`, `InActive`, `MightActive` or `MightInActive`. In other words, We are adding 4 more lifecycles to React. remember this is just a utility to help you! 95 | 96 | ```js 97 | import React, { Component } from 'react' 98 | import { scene, Status } from 'scene-router' 99 | 100 | @scene({ 101 | path: '/scene1' 102 | }) 103 | class MyFirstScene extends Component { 104 | updateSceneStatus(status) { 105 | switch(status) { 106 | case Status.Active: 107 | break 108 | case Status.InActive: 109 | break 110 | case Status.MightActive: 111 | break 112 | case Status.MightInActive: 113 | break 114 | } 115 | } 116 | 117 | render() { 118 | ... 119 | } 120 | } 121 | ``` 122 | 123 | here's a little bit of description: 124 | 125 | | value | Description | 126 | | ----- | ----------- | 127 | | Active | when the animation is done and scene is visible | 128 | | InActive | when a scene is already covered or gone | 129 | | MightInActive | during dragging a scene. the current scene will get this value | 130 | | MightActive | the previous and covered scene by current during dragging with get this value| 131 | 132 | 133 | ### `Router` component 134 | 135 | the second thing to learn is `Router`. This is your entry point of your app. It has only 3 props, `area`, `action` and `config`. 136 | 137 | - `area` is a string which defines an area with a name. if you plan to use tabs, each individual tab must have a unique name, that name can be passed to `area` 138 | - `action` is a string which accepts either `goto` or `goback`. if you pass `goback`, the 3rd prop, `config`, will be ignored. 139 | - `config` is defining which `path` you want to go and if you want to override any `scene` configuration. 140 | 141 | here's an example of `Router` 142 | 143 | ```js 144 | import React, { Component } from 'react' 145 | import { View, Text } from 'react-native' 146 | import { scene, Router } from 'scene-router' 147 | 148 | @scene({ 149 | path: '/scenes/:id' 150 | }) 151 | class Scenes extends Component { 152 | render() { 153 | const { route: { params } } = this.props 154 | 155 | return ( 156 | 157 | {params.id} 158 | 159 | ) 160 | } 161 | } 162 | 163 | class App extends Component { 164 | constructor(props: any, context: any) { 165 | super(props, context) 166 | 167 | this.state = { 168 | area: "default", 169 | action: 'goto', 170 | config: { 171 | path: '/scenes/1' 172 | } 173 | } 174 | } 175 | 176 | render() { 177 | const { area, action, config } = this.state 178 | 179 | return ( 180 | 184 | ) 185 | } 186 | } 187 | ``` 188 | 189 | > NOTE `Router` component also accepts one Componet as a child, This Component is being displayed and renderd first before the first route is triggered. this is a good place to put your splash screen. 190 | 191 | Cheers. 192 | -------------------------------------------------------------------------------- /lib/scene.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { Component } from 'react' 4 | import { Animated, View, StyleSheet, PanResponder } from 'react-native' 5 | 6 | import * as constants from './constants' 7 | 8 | import type { SceneConfig, Point, Route, GestureStatus } from './types' 9 | 10 | // types ////////////////////////////////////////////////////////////////////// 11 | 12 | type SceneProps = { 13 | WrapComponent: Function, 14 | sceneConfig: SceneConfig, 15 | route: Route, 16 | onGesture: (status: GestureStatus) => void 17 | } 18 | 19 | type SceneState = { 20 | ref: any, 21 | position: Animated.ValueXY, 22 | isDragging: boolean 23 | } 24 | 25 | type GestureResponderEvent = { 26 | nativeEvent: { 27 | changedTouches: Array, 28 | identifier: number, 29 | locationX: number, 30 | locationY: number, 31 | pageX: number, 32 | pageY: number, 33 | target: number, 34 | timestamp: number, 35 | touches: Array 36 | } 37 | } 38 | 39 | type PanResponderGestureState = { 40 | stateID: number, 41 | moveX: number, 42 | moveY: number, 43 | x0: number, 44 | y0: number, 45 | dx: number, 46 | dy: number, 47 | vx: number, 48 | vy: number, 49 | numberActiveTouches: number 50 | } 51 | 52 | // constants ////////////////////////////////////////////////////////////////// 53 | const window = constants.window 54 | const pointOfView: Point = { x: window.width, y: window.height } 55 | const startTouchPos: Point = { x: 0, y: 0 } 56 | 57 | const styles = StyleSheet.create({ 58 | container: { 59 | position: 'absolute', 60 | width: window.width, 61 | height: window.height 62 | } 63 | }) 64 | 65 | // internal functions ///////////////////////////////////////////////////////// 66 | 67 | const calcSide = (side: number): Point => { 68 | let y 69 | let x 70 | 71 | switch (side) { 72 | case constants.FromLeft: 73 | y = window.height 74 | x = 0 75 | break 76 | 77 | case constants.FromRight: 78 | y = window.height 79 | x = 2 * window.width 80 | break 81 | 82 | case constants.FromTop: 83 | y = 0 84 | x = window.width 85 | break 86 | 87 | case constants.FromBottom: 88 | y = 2 * window.height - window.softMenuHeight 89 | x = window.width 90 | break 91 | 92 | case constants.Static: 93 | y = window.height 94 | x = window.width 95 | break 96 | 97 | default: 98 | throw new Error('side is unknown') 99 | } 100 | 101 | return { y, x } 102 | } 103 | 104 | const shouldSceneClose = (side: number, threshold: number, x: number, y: number): boolean => { 105 | threshold = Math.abs(threshold) 106 | 107 | switch (side) { 108 | case constants.FromRight: 109 | return (x - threshold) > pointOfView.x 110 | case constants.FromLeft: 111 | return (x + threshold) < pointOfView.x 112 | case constants.FromTop: 113 | return (y + threshold) < pointOfView.y 114 | case constants.FromBottom: 115 | return (y - threshold) > pointOfView.y 116 | default: 117 | return false 118 | } 119 | } 120 | 121 | // Scene Component //////////////////////////////////////////////////////////// 122 | const ignore = () => false 123 | 124 | export class Scene extends Component { 125 | props: SceneProps 126 | state: SceneState 127 | 128 | panResponder: Object 129 | 130 | constructor(props: SceneProps, context: any) { 131 | super(props, context) 132 | 133 | this.state = { 134 | ref: null, 135 | position: new Animated.ValueXY(calcSide(this.side(props.sceneConfig))), 136 | isDragging: false 137 | } 138 | 139 | this.panResponder = props.sceneConfig.gesture ? PanResponder.create({ 140 | onStartShouldSetPanResponderCapture: this.shouldStartDrag, 141 | onStartShouldSetPanResponder: ignore, 142 | onMoveShouldSetPanResponderCapture: ignore, 143 | onMoveShouldSetPanResponder: ignore, 144 | onPanResponderGrant: this.onStartDrag, 145 | onPanResponderMove: this.onMoveDrag, 146 | onPanResponderRelease: this.onEndDrag 147 | }) : {} 148 | } 149 | 150 | shouldStartDrag = (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => { 151 | const { pageX, pageY } = evt.nativeEvent 152 | const { sceneConfig: { threshold, side, gesture } } = this.props 153 | 154 | // this if is only here to remove flow error 155 | if (!threshold || !gesture) { 156 | return false 157 | } 158 | 159 | // we need to know if finger starts at the right side of window 160 | switch (side) { 161 | case constants.FromBottom: 162 | return pageY <= threshold 163 | case constants.FromTop: 164 | return window.height - threshold <= pageY 165 | case constants.FromLeft: 166 | return window.width - threshold <= pageX 167 | case constants.FromRight: 168 | return pageX <= threshold 169 | default: 170 | return false 171 | } 172 | } 173 | 174 | onStartDrag = (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => { 175 | const { onGesture } = this.props 176 | 177 | startTouchPos.x = this.state.position.x.__getValue() 178 | startTouchPos.y = this.state.position.y.__getValue() 179 | 180 | onGesture('open') 181 | } 182 | 183 | onMoveDrag = (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => { 184 | const { dx, dy, moveX, moveY, x0, vx } = gestureState 185 | const { sceneConfig: { side } } = this.props 186 | const { position } = this.state 187 | 188 | switch (side) { 189 | case constants.FromLeft: 190 | if (dx < 0) { 191 | position.x.setValue(startTouchPos.x + dx) 192 | } 193 | break 194 | case constants.FromRight: 195 | if (dx > 0) { 196 | position.x.setValue(startTouchPos.x + dx) 197 | } 198 | break 199 | case constants.FromTop: 200 | if (dy < 0) { 201 | position.y.setValue(startTouchPos.y + dy) 202 | } 203 | break 204 | case constants.FromBottom: 205 | if (dy > 0) { 206 | position.y.setValue(startTouchPos.y + dy) 207 | } 208 | break 209 | } 210 | } 211 | 212 | onEndDrag = (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => { 213 | const { onGesture, sceneConfig: { side, threshold } } = this.props 214 | const { position } = this.state 215 | const x = position.x.__getValue() 216 | const y = position.y.__getValue() 217 | 218 | if (!side || !threshold) { 219 | return 220 | } 221 | 222 | if (shouldSceneClose(side, threshold, x, y)) { 223 | this.close(() => { 224 | onGesture('close') 225 | }) 226 | } else { 227 | this.open(() => { 228 | onGesture('cancel') 229 | }) 230 | } 231 | } 232 | 233 | side(sceneConfig: SceneConfig): number { 234 | // NOTE: `sceneConfig.side` will always have a value as soon as being passed to 235 | // Scene component and the reason, I'm doing this to just remove the flow error 236 | return sceneConfig.side || constants.FromRight 237 | } 238 | 239 | updateSceneStatus = (status: number) => { 240 | const { ref } = this.state 241 | if (ref) { 242 | ref.updateSceneStatus && ref.updateSceneStatus(status) 243 | } 244 | } 245 | 246 | open(done: ?Function) { 247 | Animated.timing( 248 | this.state.position, 249 | { 250 | toValue: pointOfView, 251 | duration: 300 252 | } 253 | ).start(done) 254 | } 255 | 256 | close(done: ?Function) { 257 | const { sceneConfig } = this.props 258 | 259 | Animated.timing( 260 | this.state.position, 261 | { 262 | toValue: calcSide(this.side(sceneConfig)), 263 | duration: 300 264 | } 265 | ).start(done) 266 | } 267 | 268 | render() { 269 | const { route, WrapComponent, sceneConfig } = this.props 270 | const { position } = this.state 271 | const style = [ 272 | styles.container, 273 | { 274 | transform: position.getTranslateTransform(), 275 | backgroundColor: sceneConfig.backgroundColor 276 | } 277 | ] 278 | 279 | // we need to attach sceneConfig to route 280 | route.config = sceneConfig 281 | 282 | return ( 283 | 284 | this.state.ref = ref} 286 | route={route}/> 287 | 288 | ) 289 | } 290 | } -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { Component } from 'react' 4 | import { View } from 'react-native' 5 | 6 | import { Area } from './area' 7 | import { sceneManager } from './manager' 8 | import * as constants from './constants' 9 | 10 | import type { SceneConfig, Route, SceneRejecter } from './types' 11 | 12 | // types ////////////////////////////////////////////////////////////////////// 13 | 14 | // - `area` must be passed by all times, 15 | // - if `action` is not pass, then Router will only change the area 16 | // router will throw an exception if area does not exists. 17 | // - config must be provided if `action` is 'goto'. config on 'goback' 18 | // will be ignored. 19 | // - `props` only being used by 'goto' and it's optional 20 | // - `error` is a function which will be called if path does not found 21 | type RouterProps = { 22 | area: string, 23 | action?: 'goto' | 'goback', 24 | config?: SceneConfig, 25 | props?: Object, 26 | children?: React.Element<*>, 27 | error?: SceneRejecter 28 | } 29 | 30 | type RouterState = { 31 | areas: Array>, 32 | names: Array, 33 | areaRefs: Map, 34 | firstAreaName: string 35 | } 36 | 37 | // Router Component /////////////////////////////////////////////////////////// 38 | 39 | let idCount: number = 0 40 | 41 | export class Router extends Component { 42 | props: RouterProps 43 | state: RouterState 44 | 45 | constructor(props: RouterProps, context: any) { 46 | super(props, context) 47 | 48 | sceneManager.setSceneResolver(this.sceneResolver) 49 | sceneManager.setSceneRejecter(this.sceneRejecter) 50 | 51 | const area = ( 52 | 53 | {props.children} 54 | 55 | ) 56 | 57 | this.state = { 58 | areas: [area], 59 | names: [props.area], 60 | areaRefs: new Map(), 61 | firstAreaName: props.area 62 | } 63 | } 64 | 65 | registerAreaRef = (areaRef: Area) => { 66 | const { area } = this.props 67 | if (areaRef) { 68 | this.state.areaRefs.set(area, areaRef) 69 | } 70 | } 71 | 72 | get currentAreaName(): string { 73 | const { names } = this.state 74 | return names[names.length - 1] 75 | } 76 | 77 | get currentAreRef(): ?Area { 78 | const { areaRefs } = this.state 79 | return areaRefs.get(this.currentAreaName) 80 | } 81 | 82 | // we need this method to drill down to current area 83 | // and current scene and call `updateSceneStatus` 84 | // it is mainly used for updating status once area is being chnaged. 85 | updateSceneStatus(status: number) { 86 | let sceneRef: any 87 | const currArea = this.currentAreRef 88 | if (currArea) { 89 | sceneRef = currArea.currentSceneRef 90 | if (sceneRef) { 91 | sceneRef.updateSceneStatus(status) 92 | } 93 | } 94 | } 95 | 96 | sceneRejecter = (err: string) => { 97 | const { error } = this.props 98 | error && error(err) 99 | } 100 | 101 | sceneResolver = (scene: Function, 102 | route: Route, 103 | originalSceneConfig: SceneConfig, 104 | customSceneConfig: SceneConfig) => { 105 | const ref = this.currentAreRef 106 | if (ref) { 107 | ref.push(scene, route, originalSceneConfig, customSceneConfig) 108 | } 109 | } 110 | 111 | reOrder = (name: string, done: Function) => { 112 | const index = this.state.names.indexOf(name) 113 | if (index === -1) { 114 | throw new Error('should not happen, but happened anyway! FUCK!') 115 | } 116 | 117 | let lastItem = this.state.areas.length - 1 118 | 119 | if (this.state.names[lastItem] === name) { 120 | return done() 121 | } 122 | 123 | this.updateSceneStatus(constants.Inactive) 124 | 125 | let swap: any = this.state.names[lastItem] 126 | this.state.names[lastItem] = this.state.names[index] 127 | this.state.names[index] = swap 128 | 129 | swap = this.state.areas[lastItem] 130 | this.state.areas[lastItem] = this.state.areas[index] 131 | this.state.areas[index] = swap 132 | 133 | this.setState(this.state, () => { 134 | this.updateSceneStatus(constants.Active) 135 | done() 136 | }) 137 | } 138 | 139 | componentWillReceiveProps(nextProps: RouterProps) { 140 | const { area, action, config, props } = nextProps 141 | const currentAreaName = this.props.area 142 | 143 | let areaRef: ?Area = this.state.areaRefs.get(area) 144 | 145 | if (!areaRef) { 146 | if (action !== 'goto') { 147 | throw new Error(`you can't call '${String(action)}' on area that doesn't exist`) 148 | } 149 | 150 | this.updateSceneStatus(constants.Inactive) 151 | 152 | // create a new Area 153 | this.state.areas.push() 154 | this.state.names.push(area) 155 | 156 | this.setState(this.state, () => { 157 | // so by now, area is set, so we can call the sceneManager to process the path 158 | // if a route is found, `sceneResolver` will be called. 159 | // NOTE: because `config` will always be an object, we need extra condition 160 | // to make sure it has right information. that's why you see 161 | // `config && config.path && ...` 162 | config && config.path && sceneManager.request(config.path, props, config) 163 | }) 164 | } else { 165 | if (action === 'goto') { 166 | const requireReset = area === this.state.firstAreaName && currentAreaName !== area 167 | 168 | // we know that areaRef exists and we simply call `reOrder` to switch to requested area 169 | // when the match happens, `sceneResolver` will be called. 170 | this.reOrder(area, () => { 171 | // NOTE: because `config` will always be an object, we need extra condition 172 | // to make sure it has right information. that's why you see 173 | // `config && config.path && ...` 174 | config && config.path && sceneManager.request(config.path, props, config) 175 | if (requireReset) { 176 | this.state.names.forEach((name: string) => { 177 | if (name !== this.state.firstAreaName) { 178 | const ref = this.state.areaRefs.get(name) 179 | if (ref) { 180 | ref.resetAllScenes() 181 | } 182 | 183 | // remove the ref from map 184 | this.state.areaRefs.delete(name) 185 | } 186 | }) 187 | 188 | // clean up the names and ares 189 | this.state.areas.splice(0, this.state.areas.length - 1) 190 | this.state.names.splice(0, this.state.names.length - 1) 191 | 192 | // this make sure all the tabs are destroyed 193 | this.setState(this.state) 194 | } 195 | }) 196 | } else { 197 | // if the type is `goback`, then we simply call the pop on areaRef 198 | // obviously, pop does more than just a pop. It does the animation and hide the 199 | // previous scene and call `updateSceneState` on both previous and current scenes. 200 | this.reOrder(area, () => { 201 | areaRef && areaRef.pop() 202 | }) 203 | } 204 | } 205 | } 206 | 207 | shouldComponentUpdate(nextProps: RouterProps, nextState: RouterState) { 208 | // TODO: reverting it back to normal 209 | // we need to find a way to intercept going into a route. 210 | // e.g. shouldRoute(nextRoute): boolean 211 | // this method should also be called when gesture close happening 212 | // and nextRoute will be prevRoute. 213 | 214 | /* 215 | const currConfig = this.props.config 216 | const nextConfig = nextProps.config 217 | 218 | if (currConfig && nextConfig) { 219 | // if the previous path is the same as next path, 220 | // then you don't want to do the render opration. 221 | // This can be more optimized, for checking props as well. 222 | // but for now it's sufficent enough 223 | return currConfig.path !== nextConfig.path 224 | } 225 | */ 226 | 227 | return true 228 | } 229 | 230 | componentDidMount() { 231 | const { action, config, props } = this.props 232 | 233 | if (action === 'goto') { 234 | config && sceneManager.request(config.path, props, config) 235 | } 236 | } 237 | 238 | render() { 239 | const { areas } = this.state 240 | 241 | return ( 242 | 243 | {areas} 244 | 245 | ) 246 | } 247 | } --------------------------------------------------------------------------------