Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a767c7b5f3 | ||
|
|
c6cf3d9405 | ||
|
|
8ef7a50af4 | ||
|
|
2858a10d08 | ||
|
|
d4af948261 | ||
|
|
95608dac9b | ||
|
|
b59f173863 | ||
|
|
e5358b5a9b | ||
|
|
12cae9a724 | ||
|
|
0cb1312370 | ||
|
|
2b916f3cef | ||
|
|
98a00f4840 | ||
|
|
b54594e42b | ||
|
|
f6d058dfea | ||
|
|
7fa08e5c3f | ||
|
|
9229eadff0 | ||
|
|
4f464962a0 | ||
|
|
ea959af850 | ||
|
|
72b50dc32d | ||
|
|
8e70c7f003 | ||
|
|
54c21ac651 | ||
|
|
ffaf16283f | ||
|
|
40a043109d | ||
|
|
86c815f1d4 | ||
|
|
572157d14b | ||
|
|
fc059857e2 | ||
|
|
10bb054b62 | ||
|
|
23909cd6f6 | ||
|
|
e1f53c043c | ||
|
|
1419941f8e | ||
|
|
8a5fa6cb3a | ||
|
|
bda7f7389a | ||
|
|
a2578fed3c | ||
|
|
233f80d6c2 | ||
|
|
7ce8cd7222 | ||
|
|
fd2cb763df | ||
|
|
04fe80b7a0 | ||
|
|
7d4b1877ae | ||
|
|
9b162d7659 | ||
|
|
21b98b23c9 | ||
|
|
bb07817023 | ||
|
|
b5c1cd7918 | ||
|
|
4b5b952c32 | ||
|
|
bfbc280fb4 | ||
|
|
c66b1c64b5 | ||
|
|
a886e4c66b | ||
|
|
57513fd24a | ||
|
|
2829be7de6 | ||
|
|
0d0b4c947b | ||
|
|
b3e0a702a7 | ||
|
|
98521eb16e | ||
|
|
f21e79d5e1 | ||
|
|
0ffb2d36eb | ||
|
|
a251316a5f | ||
|
|
bb141e3a3c | ||
|
|
1b7699f671 | ||
|
|
90022023c9 | ||
|
|
5c01b1e1a0 | ||
|
|
566ca9e58d | ||
|
|
b770310555 | ||
|
|
d56ff42596 | ||
|
|
c928d9495b | ||
|
|
c27defee7b | ||
|
|
3c541ca540 | ||
|
|
66d3f3c616 | ||
|
|
b57a14d07c | ||
|
|
b13e2e8c2e | ||
|
|
02db374e50 | ||
|
|
fe2155e238 | ||
|
|
3325e8f768 | ||
|
|
0f2d8e662e | ||
|
|
65027e8e29 | ||
|
|
3955236b0f | ||
|
|
652ffa28b2 | ||
|
|
c065d98112 | ||
|
|
c045c566e9 | ||
|
|
ca791dfe6f | ||
|
|
7e70ee2e03 | ||
|
|
b4cd019f39 | ||
|
|
f5ad9c2103 | ||
|
|
aba148061f | ||
|
|
fe9af98a28 | ||
|
|
60b56455c4 | ||
|
|
e37e794ccf | ||
|
|
93d3cc135d | ||
|
|
c1d7c0df47 | ||
|
|
16cb41a24e | ||
|
|
4fdeed0214 | ||
|
|
88cfc1aaa6 | ||
|
|
3441847aa1 | ||
|
|
66e32dc406 | ||
|
|
4e650f05d1 | ||
|
|
a97a866570 | ||
|
|
00e85cbc85 | ||
|
|
1d1386e735 | ||
|
|
13be9454cc | ||
|
|
54825b304a | ||
|
|
b45e2ed7ed | ||
|
|
d0cb80c072 | ||
|
|
4886b3dc57 | ||
|
|
d161640f52 | ||
|
|
5aa27586f0 | ||
|
|
7a8398b956 | ||
|
|
212bd2250c | ||
|
|
44c587e828 | ||
|
|
776dc97437 | ||
|
|
5418cdf071 | ||
|
|
14fef6474d | ||
|
|
d7ddff7554 | ||
|
|
301c01260d | ||
|
|
4e6f48c650 | ||
|
|
5af8849aa4 | ||
|
|
eee630827a | ||
|
|
5a206d2fee | ||
|
|
82ca3364c6 | ||
|
|
e5ba0f388b | ||
|
|
97bf9a6b4d | ||
|
|
cae410bda0 | ||
|
|
e3225f3403 | ||
|
|
7d184adf1a | ||
|
|
9f22f67599 | ||
|
|
555236865b | ||
|
|
cf6ff3f815 | ||
|
|
454b5f3c0b | ||
|
|
c953aa7e0b | ||
|
|
7196445cc6 | ||
|
|
8708c35702 | ||
|
|
73b032ab87 | ||
|
|
19e32399b0 | ||
|
|
de020b44c9 | ||
|
|
1461e4a17e | ||
|
|
951b5f9517 | ||
|
|
843e181542 | ||
|
|
29e49bdb91 | ||
|
|
fe7edf0860 | ||
|
|
1cca4fb769 | ||
|
|
3ec3312c7c | ||
|
|
e3e60983e6 | ||
|
|
7159a4e947 | ||
|
|
7963add0d5 | ||
|
|
5e71d352a6 | ||
|
|
f383bf2b83 | ||
|
|
99bc08cf61 | ||
|
|
3c5b4b0a9f | ||
|
|
ffe80fa10a |
@@ -40,9 +40,9 @@ suppress_type=$FlowIssue
|
||||
suppress_type=$FlowFixMe
|
||||
suppress_type=$FixMe
|
||||
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-2]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-2]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-3]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-3]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
|
||||
|
||||
[version]
|
||||
0.12.0
|
||||
0.13.1
|
||||
|
||||
@@ -28,3 +28,4 @@ project.xcworkspace
|
||||
# Node
|
||||
node_modules
|
||||
*.log
|
||||
.nvm
|
||||
|
||||
@@ -15,13 +15,13 @@ install:
|
||||
- cp $(brew --prefix nvm)/nvm-exec .nvm/
|
||||
- export NVM_DIR=.nvm
|
||||
- source $(brew --prefix nvm)/nvm.sh
|
||||
- nvm install v0.10
|
||||
- nvm install iojs-v2
|
||||
- npm config set spin=false
|
||||
- npm install
|
||||
|
||||
script:
|
||||
- |
|
||||
nvm use v0.10
|
||||
nvm use iojs-v2
|
||||
|
||||
if [ "$TEST_TYPE" = objc ]
|
||||
then
|
||||
|
||||
@@ -26,6 +26,8 @@ var {
|
||||
} = React;
|
||||
var TimerMixin = require('react-timer-mixin');
|
||||
|
||||
var invariant = require('invariant');
|
||||
|
||||
var MovieCell = require('./MovieCell');
|
||||
var MovieScreen = require('./MovieScreen');
|
||||
|
||||
@@ -73,18 +75,16 @@ var SearchScreen = React.createClass({
|
||||
this.searchMovies('');
|
||||
},
|
||||
|
||||
_urlForQueryAndPage: function(query: string, pageNumber: ?number): string {
|
||||
_urlForQueryAndPage: function(query: string, pageNumber: number): string {
|
||||
var apiKey = API_KEYS[this.state.queryNumber % API_KEYS.length];
|
||||
if (query) {
|
||||
return (
|
||||
// $FlowFixMe(>=0.13.0) - pageNumber may be null or undefined
|
||||
API_URL + 'movies.json?apikey=' + apiKey + '&q=' +
|
||||
encodeURIComponent(query) + '&page_limit=20&page=' + pageNumber
|
||||
);
|
||||
} else {
|
||||
// With no query, load latest movies
|
||||
return (
|
||||
// $FlowFixMe(>=0.13.0) - pageNumber may be null or undefined
|
||||
API_URL + 'lists/movies/in_theaters.json?apikey=' + apiKey +
|
||||
'&page_limit=20&page=' + pageNumber
|
||||
);
|
||||
@@ -176,6 +176,7 @@ var SearchScreen = React.createClass({
|
||||
});
|
||||
|
||||
var page = resultsCache.nextPageNumberForQuery[query];
|
||||
invariant(page != null, 'Next page number for "%s" is missing', query);
|
||||
fetch(this._urlForQueryAndPage(query, page))
|
||||
.then((response) => response.json())
|
||||
.catch((error) => {
|
||||
|
||||
@@ -32,5 +32,13 @@ node_modules/react-native/Libraries/react-native/react-native-interface.js
|
||||
[options]
|
||||
module.system=haste
|
||||
|
||||
suppress_type=$FlowIssue
|
||||
suppress_type=$FlowFixMe
|
||||
suppress_type=$FixMe
|
||||
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-3]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-3]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
|
||||
|
||||
[version]
|
||||
0.12.0
|
||||
0.13.1
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
144C5F691AC3E5E300B004E7 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 144C5F681AC3E5D800B004E7 /* libReact.a */; };
|
||||
58C572511AA6229D00CDF9C8 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58C5724D1AA6224400CDF9C8 /* libRCTText.a */; };
|
||||
832044981B492C2500E297FC /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832044951B492C1E00E297FC /* libRCTSettings.a */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -38,6 +39,13 @@
|
||||
remoteGlobalIDString = 58B5119B1A9E6C1200147676;
|
||||
remoteInfo = RCTText;
|
||||
};
|
||||
832044941B492C1E00E297FC /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 8320448F1B492C1E00E297FC /* RCTSettings.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 134814201AA4EA6300B7C361;
|
||||
remoteInfo = RCTSettings;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -51,6 +59,7 @@
|
||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = TicTacToe/main.m; sourceTree = "<group>"; };
|
||||
144C5F631AC3E5D800B004E7 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = ../../React/React.xcodeproj; sourceTree = "<group>"; };
|
||||
587650DA1A9EB0DB008B8F17 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../../Libraries/Text/RCTText.xcodeproj; sourceTree = "<group>"; };
|
||||
8320448F1B492C1E00E297FC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../../Libraries/Settings/RCTSettings.xcodeproj; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -61,6 +70,7 @@
|
||||
144C5F691AC3E5E300B004E7 /* libReact.a in Frameworks */,
|
||||
1341803E1AA91802003F314A /* libRCTImage.a in Frameworks */,
|
||||
58C572511AA6229D00CDF9C8 /* libRCTText.a in Frameworks */,
|
||||
832044981B492C2500E297FC /* libRCTSettings.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -101,6 +111,7 @@
|
||||
children = (
|
||||
144C5F631AC3E5D800B004E7 /* React.xcodeproj */,
|
||||
134180381AA917ED003F314A /* RCTImage.xcodeproj */,
|
||||
8320448F1B492C1E00E297FC /* RCTSettings.xcodeproj */,
|
||||
587650DA1A9EB0DB008B8F17 /* RCTText.xcodeproj */,
|
||||
);
|
||||
name = Libraries;
|
||||
@@ -114,6 +125,14 @@
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
832044901B492C1E00E297FC /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
832044951B492C1E00E297FC /* libRCTSettings.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -178,6 +197,10 @@
|
||||
ProductGroup = 134180391AA917ED003F314A /* Products */;
|
||||
ProjectRef = 134180381AA917ED003F314A /* RCTImage.xcodeproj */;
|
||||
},
|
||||
{
|
||||
ProductGroup = 832044901B492C1E00E297FC /* Products */;
|
||||
ProjectRef = 8320448F1B492C1E00E297FC /* RCTSettings.xcodeproj */;
|
||||
},
|
||||
{
|
||||
ProductGroup = 58C572481AA6224300CDF9C8 /* Products */;
|
||||
ProjectRef = 587650DA1A9EB0DB008B8F17 /* RCTText.xcodeproj */;
|
||||
@@ -216,6 +239,13 @@
|
||||
remoteRef = 58C5724C1AA6224400CDF9C8 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
832044951B492C1E00E297FC /* libRCTSettings.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = libRCTSettings.a;
|
||||
remoteRef = 832044941B492C1E00E297FC /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
/* End PBXReferenceProxy section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
var React = require('react-native');
|
||||
var {
|
||||
AppRegistry,
|
||||
Image,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
@@ -118,17 +117,6 @@ var Cell = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
imageStyle() {
|
||||
switch (this.props.player) {
|
||||
case 1:
|
||||
return styles.imageX;
|
||||
case 2:
|
||||
return styles.imageO;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
textContents() {
|
||||
switch (this.props.player) {
|
||||
case 1:
|
||||
@@ -140,17 +128,6 @@ var Cell = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
imageContents() {
|
||||
switch (this.props.player) {
|
||||
case 1:
|
||||
return 'http://www.picgifs.com/alphabets/alphabets/children-5/alphabets-children-5-277623.gif';
|
||||
case 2:
|
||||
return 'http://www.picgifs.com/alphabets/alphabets/children-5/alphabets-children-5-730492.gif';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TouchableHighlight
|
||||
@@ -158,7 +135,9 @@ var Cell = React.createClass({
|
||||
underlayColor="transparent"
|
||||
activeOpacity={0.5}>
|
||||
<View style={[styles.cell, this.cellStyle()]}>
|
||||
<Image source={{uri: this.imageContents()}} style={this.imageStyle()}/>
|
||||
<Text style={[styles.cellText, this.textStyle()]}>
|
||||
{this.textContents()}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
@@ -304,17 +283,6 @@ var styles = StyleSheet.create({
|
||||
color: '#b9dc2f',
|
||||
},
|
||||
|
||||
// CELL IMAGE
|
||||
|
||||
imageX: {
|
||||
width: 34,
|
||||
height: 42,
|
||||
},
|
||||
imageO: {
|
||||
width: 45,
|
||||
height: 41,
|
||||
},
|
||||
|
||||
// GAME OVER
|
||||
|
||||
overlay: {
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
ActionSheetIOS,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} = React;
|
||||
var ActionSheetIOS = require('ActionSheetIOS');
|
||||
|
||||
var BUTTONS = [
|
||||
'Button Index: 0',
|
||||
'Button Index: 1',
|
||||
|
||||
@@ -15,10 +15,9 @@
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var AdSupportIOS = require('AdSupportIOS');
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
AdSupportIOS,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* 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 NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK 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.
|
||||
*
|
||||
* @providesModule AnExApp
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
Animated,
|
||||
LayoutAnimation,
|
||||
PanResponder,
|
||||
StyleSheet,
|
||||
View,
|
||||
} = React;
|
||||
|
||||
var AnExSet = require('AnExSet');
|
||||
|
||||
var CIRCLE_SIZE = 80;
|
||||
var CIRCLE_MARGIN = 18;
|
||||
var NUM_CIRCLES = 30;
|
||||
|
||||
class Circle extends React.Component {
|
||||
_onLongPress: () => void;
|
||||
_toggleIsActive: () => void;
|
||||
constructor(props: Object): void {
|
||||
super();
|
||||
this._onLongPress = this._onLongPress.bind(this);
|
||||
this._toggleIsActive = this._toggleIsActive.bind(this);
|
||||
this.state = {isActive: false};
|
||||
this.state.pan = new Animated.ValueXY(); // Vectors reduce boilerplate. (step1: uncomment)
|
||||
this.state.pop = new Animated.Value(0); // Initial value. (step2a: uncomment)
|
||||
}
|
||||
|
||||
_onLongPress(): void {
|
||||
var config = {tension: 40, friction: 3};
|
||||
this.state.pan.addListener((value) => { // Async listener for state changes (step1: uncomment)
|
||||
this.props.onMove && this.props.onMove(value);
|
||||
});
|
||||
Animated.spring(this.state.pop, {
|
||||
toValue: 1, // Pop to larger size. (step2b: uncomment)
|
||||
...config, // Reuse config for convenient consistency (step2b: uncomment)
|
||||
}).start();
|
||||
this.setState({panResponder: PanResponder.create({
|
||||
onPanResponderMove: Animated.event([
|
||||
null, // native event - ignore (step1: uncomment)
|
||||
{dx: this.state.pan.x, dy: this.state.pan.y}, // links pan to gestureState (step1: uncomment)
|
||||
]),
|
||||
onPanResponderRelease: (e, gestureState) => {
|
||||
LayoutAnimation.easeInEaseOut(); // @flowfixme animates layout update as one batch (step3: uncomment)
|
||||
Animated.spring(this.state.pop, {
|
||||
toValue: 0, // Pop back to 0 (step2c: uncomment)
|
||||
...config,
|
||||
}).start();
|
||||
this.setState({panResponder: undefined});
|
||||
this.props.onMove && this.props.onMove({
|
||||
x: gestureState.dx + this.props.restLayout.x,
|
||||
y: gestureState.dy + this.props.restLayout.y,
|
||||
});
|
||||
this.props.onDeactivate();
|
||||
},
|
||||
})}, () => {
|
||||
this.props.onActivate();
|
||||
});
|
||||
}
|
||||
|
||||
render(): ReactElement {
|
||||
if (this.state.panResponder) {
|
||||
var handlers = this.state.panResponder.panHandlers;
|
||||
var dragStyle = { // Used to position while dragging
|
||||
position: 'absolute', // Hoist out of layout (step1: uncomment)
|
||||
...this.state.pan.getLayout(), // Convenience converter (step1: uncomment)
|
||||
};
|
||||
} else {
|
||||
handlers = {
|
||||
onStartShouldSetResponder: () => !this.state.isActive,
|
||||
onResponderGrant: () => {
|
||||
this.state.pan.setValue({x: 0, y: 0}); // reset (step1: uncomment)
|
||||
this.state.pan.setOffset(this.props.restLayout); // offset from onLayout (step1: uncomment)
|
||||
this.longTimer = setTimeout(this._onLongPress, 300);
|
||||
},
|
||||
onResponderRelease: () => {
|
||||
if (!this.state.panResponder) {
|
||||
clearTimeout(this.longTimer);
|
||||
this._toggleIsActive();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
var animatedStyle: Object = {
|
||||
shadowOpacity: this.state.pop, // no need for interpolation (step2d: uncomment)
|
||||
transform: [
|
||||
{scale: this.state.pop.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, 1.3] // scale up from 1 to 1.3 (step2d: uncomment)
|
||||
})},
|
||||
],
|
||||
};
|
||||
var openVal = this.props.openVal;
|
||||
if (this.props.dummy) {
|
||||
animatedStyle.opacity = 0;
|
||||
} else if (this.state.isActive) {
|
||||
var innerOpenStyle = [styles.open, { // (step4: uncomment)
|
||||
left: openVal.interpolate({inputRange: [0, 1], outputRange: [this.props.restLayout.x, 0]}),
|
||||
top: openVal.interpolate({inputRange: [0, 1], outputRange: [this.props.restLayout.y, 0]}),
|
||||
width: openVal.interpolate({inputRange: [0, 1], outputRange: [CIRCLE_SIZE, this.props.containerLayout.width]}),
|
||||
height: openVal.interpolate({inputRange: [0, 1], outputRange: [CIRCLE_SIZE, this.props.containerLayout.height]}),
|
||||
margin: openVal.interpolate({inputRange: [0, 1], outputRange: [CIRCLE_MARGIN, 0]}),
|
||||
borderRadius: openVal.interpolate({inputRange: [-0.15, 0, 0.5, 1], outputRange: [0, CIRCLE_SIZE / 2, CIRCLE_SIZE * 1.3, 0]}),
|
||||
}];
|
||||
}
|
||||
return (
|
||||
<Animated.View
|
||||
onLayout={this.props.onLayout}
|
||||
style={[styles.dragView, dragStyle, animatedStyle, this.state.isActive ? styles.open : null]}
|
||||
{...handlers}>
|
||||
<Animated.View style={[styles.circle, innerOpenStyle]}>
|
||||
<AnExSet
|
||||
containerLayout={this.props.containerLayout}
|
||||
id={this.props.id}
|
||||
isActive={this.state.isActive}
|
||||
openVal={this.props.openVal}
|
||||
onDismiss={this._toggleIsActive}
|
||||
/>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
_toggleIsActive(velocity) {
|
||||
var config = {tension: 30, friction: 7};
|
||||
if (this.state.isActive) {
|
||||
Animated.spring(this.props.openVal, {toValue: 0, ...config}).start(() => { // (step4: uncomment)
|
||||
this.setState({isActive: false}, this.props.onDeactivate);
|
||||
}); // (step4: uncomment)
|
||||
} else {
|
||||
this.props.onActivate();
|
||||
this.setState({isActive: true, panResponder: undefined}, () => {
|
||||
// this.props.openVal.setValue(1); // (step4: comment)
|
||||
Animated.spring(this.props.openVal, {toValue: 1, ...config}).start(); // (step4: uncomment)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AnExApp extends React.Component {
|
||||
_onMove: (position: Point) => void;
|
||||
constructor(props: any): void {
|
||||
super(props);
|
||||
var keys = [];
|
||||
for (var idx = 0; idx < NUM_CIRCLES; idx++) {
|
||||
keys.push('E' + idx);
|
||||
}
|
||||
this.state = {
|
||||
keys,
|
||||
restLayouts: [],
|
||||
openVal: new Animated.Value(0),
|
||||
};
|
||||
this._onMove = this._onMove.bind(this);
|
||||
}
|
||||
|
||||
render(): ReactElement {
|
||||
var circles = this.state.keys.map((key, idx) => {
|
||||
if (key === this.state.activeKey) {
|
||||
return <Circle key={key + 'd'} dummy={true} />;
|
||||
} else {
|
||||
if (!this.state.restLayouts[idx]) {
|
||||
var onLayout = function(index, e) {
|
||||
var layout = e.nativeEvent.layout;
|
||||
this.setState((state) => {
|
||||
state.restLayouts[index] = layout;
|
||||
return state;
|
||||
});
|
||||
}.bind(this, idx);
|
||||
}
|
||||
return (
|
||||
<Circle
|
||||
key={key}
|
||||
id={key}
|
||||
openVal={this.state.openVal}
|
||||
onLayout={onLayout}
|
||||
restLayout={this.state.restLayouts[idx]}
|
||||
onActivate={this.setState.bind(this, {
|
||||
activeKey: key,
|
||||
activeInitialLayout: this.state.restLayouts[idx],
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
if (this.state.activeKey) {
|
||||
circles.push(
|
||||
<Animated.View key="dark" style={[styles.darkening, {opacity: this.state.openVal}]} />
|
||||
);
|
||||
circles.push(
|
||||
<Circle
|
||||
openVal={this.state.openVal}
|
||||
key={this.state.activeKey}
|
||||
id={this.state.activeKey}
|
||||
restLayout={this.state.activeInitialLayout}
|
||||
containerLayout={this.state.layout}
|
||||
onMove={this._onMove}
|
||||
onDeactivate={() => { this.setState({activeKey: undefined}); }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.grid} onLayout={(e) => this.setState({layout: e.nativeEvent.layout})}>
|
||||
{circles}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_onMove(position: Point): void {
|
||||
var newKeys = moveToClosest(this.state, position);
|
||||
if (newKeys !== this.state.keys) {
|
||||
LayoutAnimation.easeInEaseOut(); // animates layout update as one batch (step3: uncomment)
|
||||
this.setState({keys: newKeys});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Point = {x: number, y: number};
|
||||
function distance(p1: Point, p2: Point): number {
|
||||
var dx = p1.x - p2.x;
|
||||
var dy = p1.y - p2.y;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
function moveToClosest({activeKey, keys, restLayouts}, position) {
|
||||
var activeIdx = -1;
|
||||
var closestIdx = activeIdx;
|
||||
var minDist = Infinity;
|
||||
var newKeys = [];
|
||||
keys.forEach((key, idx) => {
|
||||
var dist = distance(position, restLayouts[idx]);
|
||||
if (key === activeKey) {
|
||||
idx = activeIdx;
|
||||
} else {
|
||||
newKeys.push(key);
|
||||
}
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
closestIdx = idx;
|
||||
}
|
||||
});
|
||||
if (closestIdx === activeIdx) {
|
||||
return keys; // nothing changed
|
||||
} else {
|
||||
newKeys.splice(closestIdx, 0, activeKey);
|
||||
return newKeys;
|
||||
}
|
||||
}
|
||||
|
||||
AnExApp.title = 'Animated - Gratuitous App';
|
||||
AnExApp.description = 'Bunch of Animations - tap a circle to ' +
|
||||
'open a view with more animations, or longPress and drag to reorder circles.';
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingTop: 64, // push content below nav bar
|
||||
},
|
||||
grid: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
circle: {
|
||||
width: CIRCLE_SIZE,
|
||||
height: CIRCLE_SIZE,
|
||||
borderRadius: CIRCLE_SIZE / 2,
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
margin: CIRCLE_MARGIN,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
dragView: {
|
||||
shadowRadius: 10,
|
||||
shadowColor: 'rgba(0,0,0,0.7)',
|
||||
shadowOffset: {height: 8},
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
open: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: undefined, // unset value from styles.circle
|
||||
height: undefined, // unset value from styles.circle
|
||||
borderRadius: 0, // unset value from styles.circle
|
||||
},
|
||||
darkening: {
|
||||
backgroundColor: 'black',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = AnExApp;
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* 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 NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK 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.
|
||||
*
|
||||
* @providesModule AnExBobble
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
Animated,
|
||||
Image,
|
||||
PanResponder,
|
||||
StyleSheet,
|
||||
View,
|
||||
} = React;
|
||||
|
||||
var NUM_BOBBLES = 5;
|
||||
var RAD_EACH = Math.PI / 2 / (NUM_BOBBLES - 2);
|
||||
var RADIUS = 160;
|
||||
var BOBBLE_SPOTS = [...Array(NUM_BOBBLES)].map((_, i) => { // static positions
|
||||
return i === 0 ? {x: 0, y: 0} : { // first bobble is the selector
|
||||
x: -Math.cos(RAD_EACH * (i - 1)) * RADIUS,
|
||||
y: -Math.sin(RAD_EACH * (i - 1)) * RADIUS,
|
||||
};
|
||||
});
|
||||
|
||||
class AnExBobble extends React.Component {
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this.state.bobbles = BOBBLE_SPOTS.map((_, i) => {
|
||||
return new Animated.ValueXY();
|
||||
});
|
||||
this.state.selectedBobble = null;
|
||||
var bobblePanListener = (e, gestureState) => { // async events => change selection
|
||||
var newSelected = computeNewSelected(gestureState);
|
||||
if (this.state.selectedBobble !== newSelected) {
|
||||
if (this.state.selectedBobble !== null) {
|
||||
var restSpot = BOBBLE_SPOTS[this.state.selectedBobble];
|
||||
Animated.spring(this.state.bobbles[this.state.selectedBobble], {
|
||||
toValue: restSpot, // return previously selected bobble to rest position
|
||||
}).start();
|
||||
}
|
||||
if (newSelected !== null && newSelected !== 0) {
|
||||
Animated.spring(this.state.bobbles[newSelected], {
|
||||
toValue: this.state.bobbles[0], // newly selected should track the selector
|
||||
}).start();
|
||||
}
|
||||
this.state.selectedBobble = newSelected;
|
||||
}
|
||||
};
|
||||
var releaseBobble = () => {
|
||||
this.state.bobbles.forEach((bobble, i) => {
|
||||
Animated.spring(bobble, {
|
||||
toValue: {x: 0, y: 0} // all bobbles return to zero
|
||||
}).start();
|
||||
});
|
||||
};
|
||||
this.state.bobbleResponder = PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onPanResponderGrant: () => {
|
||||
BOBBLE_SPOTS.forEach((spot, idx) => {
|
||||
Animated.spring(this.state.bobbles[idx], {
|
||||
toValue: spot, // spring each bobble to its spot
|
||||
friction: 3, // less friction => bouncier
|
||||
}).start();
|
||||
});
|
||||
},
|
||||
onPanResponderMove: Animated.event(
|
||||
[ null, {dx: this.state.bobbles[0].x, dy: this.state.bobbles[0].y} ],
|
||||
{listener: bobblePanListener} // async state changes with arbitrary logic
|
||||
),
|
||||
onPanResponderRelease: releaseBobble,
|
||||
onPanResponderTerminate: releaseBobble,
|
||||
});
|
||||
}
|
||||
|
||||
render(): ReactElement {
|
||||
return (
|
||||
<View style={styles.bobbleContainer}>
|
||||
{this.state.bobbles.map((_, i) => {
|
||||
var j = this.state.bobbles.length - i - 1; // reverse so lead on top
|
||||
var handlers = j > 0 ? {} : this.state.bobbleResponder.panHandlers;
|
||||
return (
|
||||
<Animated.Image
|
||||
{...handlers}
|
||||
source={{uri: BOBBLE_IMGS[j]}}
|
||||
style={[styles.circle, {
|
||||
backgroundColor: randColor(), // re-renders are obvious
|
||||
transform: this.state.bobbles[j].getTranslateTransform(), // simple conversion
|
||||
}]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
circle: {
|
||||
position: 'absolute',
|
||||
height: 60,
|
||||
width: 60,
|
||||
borderRadius: 30,
|
||||
borderWidth: 0.5,
|
||||
},
|
||||
bobbleContainer: {
|
||||
top: -68,
|
||||
paddingRight: 66,
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
function computeNewSelected(
|
||||
gestureState: Object,
|
||||
): ?number {
|
||||
var {dx, dy} = gestureState;
|
||||
var minDist = Infinity;
|
||||
var newSelected = null;
|
||||
var pointRadius = Math.sqrt(dx * dx + dy * dy);
|
||||
if (Math.abs(RADIUS - pointRadius) < 80) {
|
||||
BOBBLE_SPOTS.forEach((spot, idx) => {
|
||||
var delta = {x: spot.x - dx, y: spot.y - dy};
|
||||
var dist = delta.x * delta.x + delta.y * delta.y;
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
newSelected = idx;
|
||||
}
|
||||
});
|
||||
}
|
||||
return newSelected;
|
||||
}
|
||||
|
||||
function randColor(): string {
|
||||
var colors = [0,1,2].map(() => Math.floor(Math.random() * 150 + 100));
|
||||
return 'rgb(' + colors.join(',') + ')';
|
||||
}
|
||||
|
||||
var BOBBLE_IMGS = [
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xpf1/t39.1997-6/10173489_272703316237267_1025826781_n.png',
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/l/t39.1997-6/p240x240/851578_631487400212668_2087073502_n.png',
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/p240x240/851583_654446917903722_178118452_n.png',
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/p240x240/851565_641023175913294_875343096_n.png',
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/851562_575284782557566_1188781517_n.png',
|
||||
];
|
||||
|
||||
module.exports = AnExBobble;
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* 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 NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK 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.
|
||||
*
|
||||
* @providesModule AnExChained
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
Animated,
|
||||
Image,
|
||||
PanResponder,
|
||||
StyleSheet,
|
||||
View,
|
||||
} = React;
|
||||
|
||||
class AnExChained extends React.Component {
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
this.state = {
|
||||
stickers: [new Animated.ValueXY()], // 1 leader
|
||||
};
|
||||
var stickerConfig = {tension: 2, friction: 3}; // soft spring
|
||||
for (var i = 0; i < 4; i++) { // 4 followers
|
||||
var sticker = new Animated.ValueXY();
|
||||
Animated.spring(sticker, {
|
||||
...stickerConfig,
|
||||
toValue: this.state.stickers[i], // Animated toValue's are tracked
|
||||
}).start();
|
||||
this.state.stickers.push(sticker); // push on the followers
|
||||
}
|
||||
var releaseChain = (e, gestureState) => {
|
||||
this.state.stickers[0].flattenOffset(); // merges offset into value and resets
|
||||
Animated.sequence([ // spring to start after decay finishes
|
||||
Animated.decay(this.state.stickers[0], { // coast to a stop
|
||||
velocity: {x: gestureState.vx, y: gestureState.vy},
|
||||
deceleration: 0.997,
|
||||
}),
|
||||
Animated.spring(this.state.stickers[0], {
|
||||
toValue: {x: 0, y: 0} // return to start
|
||||
}),
|
||||
]).start();
|
||||
};
|
||||
this.state.chainResponder = PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onPanResponderGrant: () => {
|
||||
this.state.stickers[0].stopAnimation((value) => {
|
||||
this.state.stickers[0].setOffset(value); // start where sticker animated to
|
||||
this.state.stickers[0].setValue({x: 0, y: 0}); // avoid flicker before next event
|
||||
});
|
||||
},
|
||||
onPanResponderMove: Animated.event(
|
||||
[null, {dx: this.state.stickers[0].x, dy: this.state.stickers[0].y}] // map gesture to leader
|
||||
),
|
||||
onPanResponderRelease: releaseChain,
|
||||
onPanResponderTerminate: releaseChain,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.chained}>
|
||||
{this.state.stickers.map((_, i) => {
|
||||
var j = this.state.stickers.length - i - 1; // reverse so leader is on top
|
||||
var handlers = (j === 0) ? this.state.chainResponder.panHandlers : {};
|
||||
return (
|
||||
<Animated.Image
|
||||
{...handlers}
|
||||
source={{uri: CHAIN_IMGS[j]}}
|
||||
style={[styles.sticker, {
|
||||
transform: this.state.stickers[j].getTranslateTransform(), // simple conversion
|
||||
}]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
chained: {
|
||||
alignSelf: 'flex-end',
|
||||
top: -160,
|
||||
right: 126
|
||||
},
|
||||
sticker: {
|
||||
position: 'absolute',
|
||||
height: 120,
|
||||
width: 120,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
var CHAIN_IMGS = [
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xpf1/t39.1997-6/p160x160/10574705_1529175770666007_724328156_n.png',
|
||||
'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xfa1/t39.1997-6/p160x160/851575_392309884199657_1917957497_n.png',
|
||||
'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xfa1/t39.1997-6/p160x160/851567_555288911225630_1628791128_n.png',
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xfa1/t39.1997-6/p160x160/851583_531111513625557_903469595_n.png',
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xpa1/t39.1997-6/p160x160/851577_510515972354399_2147096990_n.png',
|
||||
];
|
||||
|
||||
module.exports = AnExChained;
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* 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 NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK 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.
|
||||
*
|
||||
* @providesModule AnExScroll
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
Animated,
|
||||
Image,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} = React;
|
||||
|
||||
class AnExScroll extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
scrollX: new Animated.Value(0),
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
var width = this.props.panelWidth;
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView
|
||||
automaticallyAdjustContentInsets={false}
|
||||
scrollEventThrottle={16 /* get all events */ }
|
||||
onScroll={Animated.event(
|
||||
[{nativeEvent: {contentOffset: {x: this.state.scrollX}}}] // nested event mapping
|
||||
)}
|
||||
contentContainerStyle={{flex: 1, padding: 10}}
|
||||
pagingEnabled={true}
|
||||
horizontal={true}>
|
||||
<View style={[styles.page, {width}]}>
|
||||
<Image
|
||||
style={{width: 180, height: 180}}
|
||||
source={HAWK_PIC}
|
||||
/>
|
||||
<Text style={styles.text}>
|
||||
{'I\'ll find something to put here.'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.page, {width}]}>
|
||||
<Text style={styles.text}>{'And here.'}</Text>
|
||||
</View>
|
||||
<View style={[styles.page, {width}]}>
|
||||
<Text>{'But not here.'}</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<Animated.Image
|
||||
pointerEvents="none"
|
||||
style={[styles.bunny, {transform: [
|
||||
{translateX: this.state.scrollX.interpolate({
|
||||
inputRange: [0, width, 2 * width],
|
||||
outputRange: [0, 0, width / 3]}), // multi-part ranges
|
||||
extrapolate: 'clamp'}, // default is 'extend'
|
||||
{translateY: this.state.scrollX.interpolate({
|
||||
inputRange: [0, width, 2 * width],
|
||||
outputRange: [0, -200, -260]}),
|
||||
extrapolate: 'clamp'},
|
||||
{scale: this.state.scrollX.interpolate({
|
||||
inputRange: [0, width, 2 * width],
|
||||
outputRange: [0.5, 0.5, 2]}),
|
||||
extrapolate: 'clamp'},
|
||||
]}]}
|
||||
source={BUNNY_PIC}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: 'transparent',
|
||||
flex: 1,
|
||||
},
|
||||
text: {
|
||||
padding: 4,
|
||||
paddingBottom: 10,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 18,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
bunny: {
|
||||
backgroundColor: 'transparent',
|
||||
position: 'absolute',
|
||||
height: 160,
|
||||
width: 160,
|
||||
},
|
||||
page: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
});
|
||||
|
||||
var HAWK_PIC = {uri: 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xfa1/t39.1997-6/10734304_1562225620659674_837511701_n.png'};
|
||||
var BUNNY_PIC = {uri: 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/851564_531111380292237_1898871086_n.png'};
|
||||
|
||||
module.exports = AnExScroll;
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* 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 NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK 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.
|
||||
*
|
||||
* @providesModule AnExSet
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
Animated,
|
||||
PanResponder,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} = React;
|
||||
|
||||
var AnExBobble = require('./AnExBobble');
|
||||
var AnExChained = require('./AnExChained');
|
||||
var AnExScroll = require('./AnExScroll');
|
||||
var AnExTilt = require('./AnExTilt');
|
||||
|
||||
class AnExSet extends React.Component {
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
function randColor() {
|
||||
var colors = [0,1,2].map(() => Math.floor(Math.random() * 150 + 100));
|
||||
return 'rgb(' + colors.join(',') + ')';
|
||||
}
|
||||
this.state = {
|
||||
closeColor: randColor(),
|
||||
openColor: randColor(),
|
||||
};
|
||||
}
|
||||
render(): ReactElement {
|
||||
var backgroundColor = this.props.openVal ?
|
||||
this.props.openVal.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [
|
||||
this.state.closeColor, // interpolates color strings
|
||||
this.state.openColor
|
||||
],
|
||||
}) :
|
||||
this.state.closeColor;
|
||||
var panelWidth = this.props.containerLayout && this.props.containerLayout.width || 320;
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Animated.View
|
||||
style={[styles.header, { backgroundColor }]}
|
||||
{...this.state.dismissResponder.panHandlers}>
|
||||
<Text style={[styles.text, styles.headerText]}>
|
||||
{this.props.id}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
{this.props.isActive &&
|
||||
<View style={styles.stream}>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.text}>
|
||||
July 2nd
|
||||
</Text>
|
||||
<AnExTilt isActive={this.props.isActive} />
|
||||
<AnExBobble />
|
||||
</View>
|
||||
<AnExScroll panelWidth={panelWidth}/>
|
||||
<AnExChained />
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.state.dismissY = new Animated.Value(0);
|
||||
this.state.dismissResponder = PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => this.props.isActive,
|
||||
onPanResponderGrant: () => {
|
||||
Animated.spring(this.props.openVal, { // Animated value passed in.
|
||||
toValue: this.state.dismissY.interpolate({ // Track dismiss gesture
|
||||
inputRange: [0, 300], // and interpolate pixel distance
|
||||
outputRange: [1, 0], // to a fraction.
|
||||
})
|
||||
}).start();
|
||||
},
|
||||
onPanResponderMove: Animated.event(
|
||||
[null, {dy: this.state.dismissY}] // track pan gesture
|
||||
),
|
||||
onPanResponderRelease: (e, gestureState) => {
|
||||
if (gestureState.dy > 100) {
|
||||
this.props.onDismiss(gestureState.vy); // delegates dismiss action to parent
|
||||
} else {
|
||||
Animated.spring(this.props.openVal, {
|
||||
toValue: 1, // animate back open if released early
|
||||
}).start();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
paddingTop: 18,
|
||||
height: 90,
|
||||
},
|
||||
stream: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgb(230, 230, 230)',
|
||||
},
|
||||
card: {
|
||||
margin: 8,
|
||||
padding: 8,
|
||||
borderRadius: 6,
|
||||
backgroundColor: 'white',
|
||||
shadowRadius: 2,
|
||||
shadowColor: 'black',
|
||||
shadowOpacity: 0.2,
|
||||
shadowOffset: {height: 0.5},
|
||||
},
|
||||
text: {
|
||||
padding: 4,
|
||||
paddingBottom: 10,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 18,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
headerText: {
|
||||
fontSize: 25,
|
||||
color: 'white',
|
||||
shadowRadius: 3,
|
||||
shadowColor: 'black',
|
||||
shadowOpacity: 1,
|
||||
shadowOffset: {height: 1},
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = AnExSet;
|
||||
@@ -0,0 +1,107 @@
|
||||
<br /><br />
|
||||
# React Native: Animated
|
||||
|
||||
ReactEurope 2015, Paris - Spencer Ahrens - Facebook
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Fluid Interactions
|
||||
|
||||
- People expect smooth, delightful experiences
|
||||
- Complex interactions are hard
|
||||
- Common patterns can be optimized
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## Declarative Interactions
|
||||
|
||||
- Wire up inputs (events) to outputs (props) + transforms (springs, easing, etc.)
|
||||
- Arbitrary code can define/update this config
|
||||
- Config can be serialized -> native/main thread
|
||||
- No refs or lifecycle to worry about
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## var { Animated } = require('react-native');
|
||||
|
||||
- New library soon to be released for React Native
|
||||
- 100% JS implementation -> X-Platform
|
||||
- Per-platform native optimizations planned
|
||||
- This talk -> usage examples, not implementation
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## Gratuitous Animation Demo App
|
||||
|
||||
- Layout uses `flexWrap: 'wrap'`
|
||||
- longPress -> drag to reorder
|
||||
- Tap to open example sets
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Gratuitous Animation Codez
|
||||
|
||||
- Step 1: 2D tracking pan gesture
|
||||
- Step 2: Simple pop-out spring on select
|
||||
- Step 3: Animate grid reordering with `LayoutAnimation`
|
||||
- Step 4: Opening animation
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Animation Example Set
|
||||
|
||||
- `Animated.Value` `this.props.open` passed in from parent
|
||||
- `interpolate` works with string "shapes," e.g. `'rgb(0, 0, 255)'`, `'45deg'`
|
||||
- Examples easily composed as separate components
|
||||
- Dismissing tracks interpolated gesture
|
||||
- Custom release logic
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## Tilting Photo
|
||||
|
||||
- Pan -> translateX * 2, rotate, opacity (via tracking)
|
||||
- Gesture release triggers separate animations
|
||||
- `addListener` for async, arbitrary logic on animation progress
|
||||
- `interpolate` easily creates parallax and other effects
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Bobbles
|
||||
|
||||
- Static positions defined
|
||||
- Listens to events to maybe change selection
|
||||
- Springs previous selection back
|
||||
- New selection tracks selector
|
||||
- `getTranslateTransform` adds convenience
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Chained
|
||||
|
||||
- Classic "Chat Heads" animation
|
||||
- Each sticker tracks the one before it with a soft spring
|
||||
- `decay` maintains gesture velocity, followed by `spring` to home
|
||||
- `stopAnimation` provides the last value for `setOffset`
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Scrolling
|
||||
|
||||
- `Animated.event` can track all sorts of stuff
|
||||
- Multi-part ranges and extrapolation options
|
||||
- Transforms decompose into ordered components
|
||||
|
||||
<br /><br />
|
||||
|
||||
# React Native: Animated
|
||||
|
||||
- Landing soon in master (days)
|
||||
- GitHub: @vjeux, @sahrens
|
||||
- Questions?
|
||||
|
||||
<br />
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* 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 NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK 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.
|
||||
*
|
||||
* @providesModule AnExTilt
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
Animated,
|
||||
Image,
|
||||
PanResponder,
|
||||
StyleSheet,
|
||||
View,
|
||||
} = React;
|
||||
|
||||
class AnExTilt extends React.Component {
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
this.state = {
|
||||
panX: new Animated.Value(0),
|
||||
opacity: new Animated.Value(1),
|
||||
burns: new Animated.Value(1.15),
|
||||
};
|
||||
this.state.tiltPanResponder = PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onPanResponderGrant: () => {
|
||||
Animated.timing(this.state.opacity, {
|
||||
toValue: this.state.panX.interpolate({
|
||||
inputRange: [-300, 0, 300], // pan is in pixels
|
||||
outputRange: [0, 1, 0], // goes to zero at both edges
|
||||
}),
|
||||
duration: 0, // direct tracking
|
||||
}).start();
|
||||
},
|
||||
onPanResponderMove: Animated.event(
|
||||
[null, {dx: this.state.panX}] // panX is linked to the gesture
|
||||
),
|
||||
onPanResponderRelease: (e, gestureState) => {
|
||||
var toValue = 0;
|
||||
if (gestureState.dx > 100) {
|
||||
toValue = 500;
|
||||
} else if (gestureState.dx < -100) {
|
||||
toValue = -500;
|
||||
}
|
||||
Animated.spring(this.state.panX, {
|
||||
toValue, // animate back to center or off screen
|
||||
velocity: gestureState.vx, // maintain gesture velocity
|
||||
tension: 10,
|
||||
friction: 3,
|
||||
}).start();
|
||||
this.state.panX.removeAllListeners();
|
||||
var id = this.state.panX.addListener(({value}) => { // listen until offscreen
|
||||
if (Math.abs(value) > 400) {
|
||||
this.state.panX.removeListener(id); // offscreen, so stop listening
|
||||
Animated.timing(this.state.opacity, {
|
||||
toValue: 1, // Fade back in. This unlinks it from tracking this.state.panX
|
||||
}).start();
|
||||
this.state.panX.setValue(0); // Note: stops the spring animation
|
||||
toValue !== 0 && this._startBurnsZoom();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
_startBurnsZoom() {
|
||||
this.state.burns.setValue(1); // reset to beginning
|
||||
Animated.decay(this.state.burns, {
|
||||
velocity: 1, // sublte zoom
|
||||
deceleration: 0.9999, // slow decay
|
||||
}).start();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._startBurnsZoom();
|
||||
}
|
||||
|
||||
render(): ReactElement {
|
||||
return (
|
||||
<Animated.View
|
||||
{...this.state.tiltPanResponder.panHandlers}
|
||||
style={[styles.tilt, {
|
||||
opacity: this.state.opacity,
|
||||
transform: [
|
||||
{rotate: this.state.panX.interpolate({
|
||||
inputRange: [-320, 320],
|
||||
outputRange: ['-15deg', '15deg']})}, // interpolate string "shapes"
|
||||
{translateX: this.state.panX},
|
||||
],
|
||||
}]}>
|
||||
<Animated.Image
|
||||
pointerEvents="none"
|
||||
style={{
|
||||
flex: 1,
|
||||
transform: [
|
||||
{translateX: this.state.panX.interpolate({
|
||||
inputRange: [-3, 3], // small range is extended by default
|
||||
outputRange: [2, -2]}) // parallax
|
||||
},
|
||||
{scale: this.state.burns.interpolate({
|
||||
inputRange: [1, 3000],
|
||||
outputRange: [1, 1.25]}) // simple multiplier
|
||||
},
|
||||
],
|
||||
}}
|
||||
source={NATURE_IMAGE}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
tilt: {
|
||||
overflow: 'hidden',
|
||||
height: 200,
|
||||
marginBottom: 4,
|
||||
backgroundColor: 'rgb(130, 130, 255)',
|
||||
borderColor: 'rgba(0, 0, 0, 0.2)',
|
||||
borderWidth: 1,
|
||||
borderRadius: 20,
|
||||
},
|
||||
});
|
||||
|
||||
var NATURE_IMAGE = {uri: 'http://www.deshow.net/d/file/travel/2009-04/scenic-beauty-of-nature-photography-2-504-4.jpg'};
|
||||
|
||||
module.exports = AnExTilt;
|
||||
@@ -62,8 +62,11 @@ var CircleBlock = React.createClass({
|
||||
var LayoutExample = React.createClass({
|
||||
statics: {
|
||||
title: 'Layout - Flexbox',
|
||||
description: 'Examples of using the flexbox API to layout views.'
|
||||
description: 'Examples of using the flexbox API to layout views.',
|
||||
},
|
||||
|
||||
displayName: 'LayoutExample',
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<UIExplorerPage title={this.props.navigator ? null : 'Layout'}>
|
||||
|
||||
@@ -212,6 +212,7 @@ var styles = StyleSheet.create({
|
||||
width: 64,
|
||||
height: 64,
|
||||
marginHorizontal: 10,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
section: {
|
||||
flexDirection: 'column',
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var StyleSheet = require('StyleSheet');
|
||||
var {
|
||||
MapView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
|
||||
@@ -66,6 +66,7 @@ var NavigatorIOSColors = React.createClass({
|
||||
tintColor="#FFFFFF"
|
||||
barTintColor="#183E63"
|
||||
titleTextColor="#FFFFFF"
|
||||
translucent="true"
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -118,7 +118,7 @@ var EventSegmentedControlExample = React.createClass({
|
||||
|
||||
_onChange(event) {
|
||||
this.setState({
|
||||
selectedIndex: event.nativeEvent.selectedIndex,
|
||||
selectedIndex: event.nativeEvent.selectedSegmentIndex,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -26,9 +26,11 @@ var {
|
||||
var TabBarExample = React.createClass({
|
||||
statics: {
|
||||
title: '<TabBarIOS>',
|
||||
description: 'Tab-based navigation.'
|
||||
description: 'Tab-based navigation.',
|
||||
},
|
||||
|
||||
displayName: 'TabBarExample',
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
selectedTab: 'redTab',
|
||||
|
||||
@@ -185,6 +185,44 @@ exports.examples = [
|
||||
</View>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: 'Text Decoration',
|
||||
render: function() {
|
||||
return (
|
||||
<View>
|
||||
<Text style={{textDecorationLine: 'underline', textDecorationStyle: 'solid'}}>
|
||||
Solid underline
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'underline', textDecorationStyle: 'double', textDecorationColor: '#ff0000'}}>
|
||||
Double underline with custom color
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'underline', textDecorationStyle: 'dashed', textDecorationColor: '#9CDC40'}}>
|
||||
Dashed underline with custom color
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'underline', textDecorationStyle: 'dotted', textDecorationColor: 'blue'}}>
|
||||
Dotted underline with custom color
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'none'}}>
|
||||
None textDecoration
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'line-through', textDecorationStyle: 'solid'}}>
|
||||
Solid line-through
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'line-through', textDecorationStyle: 'double', textDecorationColor: '#ff0000'}}>
|
||||
Double line-through with custom color
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'line-through', textDecorationStyle: 'dashed', textDecorationColor: '#9CDC40'}}>
|
||||
Dashed line-through with custom color
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'line-through', textDecorationStyle: 'dotted', textDecorationColor: 'blue'}}>
|
||||
Dotted line-through with custom color
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'underline line-through'}}>
|
||||
Both underline and line-through
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: 'Nested',
|
||||
description: 'Nested text components will inherit the styles of their ' +
|
||||
|
||||
@@ -130,10 +130,11 @@ var TouchableFeedbackEvents = React.createClass({
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<View>
|
||||
<View testID="touchable_feedback_events">
|
||||
<View style={[styles.row, {justifyContent: 'center'}]}>
|
||||
<TouchableOpacity
|
||||
style={styles.wrapper}
|
||||
testID="touchable_feedback_events_button"
|
||||
onPress={() => this._appendEvent('press')}
|
||||
onPressIn={() => this._appendEvent('pressIn')}
|
||||
onPressOut={() => this._appendEvent('pressOut')}
|
||||
@@ -143,7 +144,7 @@ var TouchableFeedbackEvents = React.createClass({
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.eventLogBox}>
|
||||
<View testID="touchable_feedback_events_console" style={styles.eventLogBox}>
|
||||
{this.state.eventLog.map((e, ii) => <Text key={ii}>{e}</Text>)}
|
||||
</View>
|
||||
</View>
|
||||
@@ -165,10 +166,11 @@ var TouchableDelayEvents = React.createClass({
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<View>
|
||||
<View testID="touchable_delay_events">
|
||||
<View style={[styles.row, {justifyContent: 'center'}]}>
|
||||
<TouchableOpacity
|
||||
style={styles.wrapper}
|
||||
testID="touchable_delay_events_button"
|
||||
onPress={() => this._appendEvent('press')}
|
||||
delayPressIn={400}
|
||||
onPressIn={() => this._appendEvent('pressIn - 400ms delay')}
|
||||
@@ -181,7 +183,7 @@ var TouchableDelayEvents = React.createClass({
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.eventLogBox}>
|
||||
<View style={styles.eventLogBox} testID="touchable_delay_events_console">
|
||||
{this.state.eventLog.map((e, ii) => <Text key={ii}>{e}</Text>)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
'use strict';
|
||||
|
||||
var React = require('React');
|
||||
var {
|
||||
StyleSheet,
|
||||
View,
|
||||
} = React;
|
||||
|
||||
var StyleSheet = require('StyleSheet');
|
||||
var TimerMixin = require('react-timer-mixin');
|
||||
var UIExplorerBlock = require('UIExplorerBlock');
|
||||
var UIExplorerPage = require('UIExplorerPage');
|
||||
var View = require('View');
|
||||
var UIExplorerBlock = require('./UIExplorerBlock');
|
||||
var UIExplorerPage = require('./UIExplorerPage');
|
||||
|
||||
var TransformExample = React.createClass({
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
141FC1211B222EBB004D5FFB /* IntegrationTestsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 141FC1201B222EBB004D5FFB /* IntegrationTestsTests.m */; };
|
||||
143BC5A11B21E45C00462512 /* UIExplorerIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 143BC5A01B21E45C00462512 /* UIExplorerIntegrationTests.m */; };
|
||||
141FC1211B222EBB004D5FFB /* IntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 141FC1201B222EBB004D5FFB /* IntegrationTests.m */; };
|
||||
143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */; };
|
||||
144D21241B2204C5006DB32B /* RCTClippingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 144D21231B2204C5006DB32B /* RCTClippingTests.m */; };
|
||||
147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; };
|
||||
1497CFAC1B21F5E400C1F8F2 /* RCTAllocationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */; };
|
||||
@@ -167,7 +167,7 @@
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = UIExplorer/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = UIExplorer/main.m; sourceTree = "<group>"; };
|
||||
13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../../Libraries/Settings/RCTSettings.xcodeproj; sourceTree = "<group>"; };
|
||||
141FC1201B222EBB004D5FFB /* IntegrationTestsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IntegrationTestsTests.m; sourceTree = "<group>"; };
|
||||
141FC1201B222EBB004D5FFB /* IntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IntegrationTests.m; sourceTree = "<group>"; };
|
||||
143BC57E1B21E18100462512 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
143BC5811B21E18100462512 /* testLayoutExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testLayoutExampleSnapshot_1@2x.png"; sourceTree = "<group>"; };
|
||||
143BC5821B21E18100462512 /* testSliderExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testSliderExampleSnapshot_1@2x.png"; sourceTree = "<group>"; };
|
||||
@@ -177,7 +177,7 @@
|
||||
143BC5861B21E18100462512 /* testViewExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testViewExampleSnapshot_1@2x.png"; sourceTree = "<group>"; };
|
||||
143BC5951B21E3E100462512 /* UIExplorerIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIExplorerIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
143BC5981B21E3E100462512 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
143BC5A01B21E45C00462512 /* UIExplorerIntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIExplorerIntegrationTests.m; sourceTree = "<group>"; };
|
||||
143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIExplorerSnapshotTests.m; sourceTree = "<group>"; };
|
||||
144D21231B2204C5006DB32B /* RCTClippingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTClippingTests.m; sourceTree = "<group>"; };
|
||||
1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAllocationTests.m; sourceTree = "<group>"; };
|
||||
1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBridgeTests.m; sourceTree = "<group>"; };
|
||||
@@ -381,8 +381,8 @@
|
||||
143BC5961B21E3E100462512 /* UIExplorerIntegrationTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
141FC1201B222EBB004D5FFB /* IntegrationTestsTests.m */,
|
||||
143BC5A01B21E45C00462512 /* UIExplorerIntegrationTests.m */,
|
||||
141FC1201B222EBB004D5FFB /* IntegrationTests.m */,
|
||||
143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */,
|
||||
143BC5971B21E3E100462512 /* Supporting Files */,
|
||||
);
|
||||
path = UIExplorerIntegrationTests;
|
||||
@@ -796,8 +796,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
141FC1211B222EBB004D5FFB /* IntegrationTestsTests.m in Sources */,
|
||||
143BC5A11B21E45C00462512 /* UIExplorerIntegrationTests.m in Sources */,
|
||||
141FC1211B222EBB004D5FFB /* IntegrationTests.m in Sources */,
|
||||
143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x",
|
||||
"filename" : "uie_thumb_big.png"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
@@ -17,14 +17,16 @@
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var Dimensions = require('Dimensions');
|
||||
var DrawerLayoutAndroid = require('DrawerLayoutAndroid');
|
||||
var ToolbarAndroid = require('ToolbarAndroid');
|
||||
var UIExplorerList = require('./UIExplorerList');
|
||||
var {
|
||||
Dimensions,
|
||||
StyleSheet,
|
||||
View,
|
||||
} = React;
|
||||
var UIExplorerList = require('./UIExplorerList');
|
||||
|
||||
// TODO: these should be exposed by the 'react-native' module.
|
||||
var DrawerLayoutAndroid = require('DrawerLayoutAndroid');
|
||||
var ToolbarAndroid = require('ToolbarAndroid');
|
||||
|
||||
var DRAWER_WIDTH_LEFT = 56;
|
||||
|
||||
|
||||
@@ -43,22 +43,23 @@ var UIExplorerApp = React.createClass({
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<NavigatorIOS
|
||||
style={styles.container}
|
||||
initialRoute={{
|
||||
title: 'UIExplorer',
|
||||
component: UIExplorerList,
|
||||
passProps: {
|
||||
onExternalExampleRequested: (example) => {
|
||||
this.setState({ openExternalExample: example, });
|
||||
},
|
||||
}
|
||||
}}
|
||||
itemWrapperStyle={styles.itemWrapper}
|
||||
tintColor="#008888"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<NavigatorIOS
|
||||
style={styles.container}
|
||||
initialRoute={{
|
||||
title: 'UIExplorer',
|
||||
component: UIExplorerList,
|
||||
passProps: {
|
||||
onExternalExampleRequested: (example) => {
|
||||
this.setState({ openExternalExample: example, });
|
||||
},
|
||||
}
|
||||
}}
|
||||
itemWrapperStyle={styles.itemWrapper}
|
||||
tintColor="#008888"
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
|
||||
#import "RCTAssert.h"
|
||||
|
||||
@interface IntegrationTestsTests : XCTestCase
|
||||
@interface IntegrationTests : XCTestCase
|
||||
|
||||
@end
|
||||
|
||||
@implementation IntegrationTestsTests
|
||||
@implementation IntegrationTests
|
||||
{
|
||||
RCTTestRunner *_runner;
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion];
|
||||
RCTAssert(version.majorVersion == 8 || version.minorVersion == 3, @"Tests should be run on iOS 8.3, found %zd.%zd.%zd", version.majorVersion, version.minorVersion, version.patchVersion);
|
||||
_runner = RCTInitRunnerForApp(@"Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestsApp");
|
||||
_runner = RCTInitRunnerForApp(@"Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestsApp", nil);
|
||||
}
|
||||
|
||||
#pragma mark Logic Tests
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* 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 NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK 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.
|
||||
*/
|
||||
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#import "RCTSparseArray.h"
|
||||
#import "RCTUIManager.h"
|
||||
#import "UIView+React.h"
|
||||
|
||||
@interface RCTUIManager (Testing)
|
||||
|
||||
- (void)_manageChildren:(NSNumber *)containerReactTag
|
||||
moveFromIndices:(NSArray *)moveFromIndices
|
||||
moveToIndices:(NSArray *)moveToIndices
|
||||
addChildReactTags:(NSArray *)addChildReactTags
|
||||
addAtIndices:(NSArray *)addAtIndices
|
||||
removeAtIndices:(NSArray *)removeAtIndices
|
||||
registry:(RCTSparseArray *)registry;
|
||||
|
||||
@property (nonatomic, readonly) RCTSparseArray *viewRegistry;
|
||||
|
||||
@end
|
||||
|
||||
@interface RCTUIManagerScenarioTests : XCTestCase
|
||||
|
||||
@property (nonatomic, readwrite, strong) RCTUIManager *uiManager;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTUIManagerScenarioTests
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
[super setUp];
|
||||
|
||||
_uiManager = [[RCTUIManager alloc] init];
|
||||
|
||||
// Register 20 views to use in the tests
|
||||
for (NSInteger i = 1; i <= 20; i++) {
|
||||
UIView *registeredView = [[UIView alloc] init];
|
||||
[registeredView setReactTag:@(i)];
|
||||
_uiManager.viewRegistry[i] = registeredView;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testManagingChildrenToAddViews
|
||||
{
|
||||
UIView *containerView = _uiManager.viewRegistry[20];
|
||||
NSMutableArray *addedViews = [NSMutableArray array];
|
||||
|
||||
NSArray *tagsToAdd = @[@1, @2, @3, @4, @5];
|
||||
NSArray *addAtIndices = @[@0, @1, @2, @3, @4];
|
||||
for (NSNumber *tag in tagsToAdd) {
|
||||
[addedViews addObject:_uiManager.viewRegistry[tag]];
|
||||
}
|
||||
|
||||
// Add views 1-5 to view 20
|
||||
[_uiManager _manageChildren:@20
|
||||
moveFromIndices:nil
|
||||
moveToIndices:nil
|
||||
addChildReactTags:tagsToAdd
|
||||
addAtIndices:addAtIndices
|
||||
removeAtIndices:nil
|
||||
registry:_uiManager.viewRegistry];
|
||||
|
||||
XCTAssertTrue([[containerView reactSubviews] count] == 5,
|
||||
@"Expect to have 5 react subviews after calling manage children \
|
||||
with 5 tags to add, instead have %lu", (unsigned long)[[containerView reactSubviews] count]);
|
||||
for (UIView *view in addedViews) {
|
||||
XCTAssertTrue([view superview] == containerView,
|
||||
@"Expected to have manage children successfully add children");
|
||||
[view removeFromSuperview];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testManagingChildrenToRemoveViews
|
||||
{
|
||||
UIView *containerView = _uiManager.viewRegistry[20];
|
||||
NSMutableArray *removedViews = [NSMutableArray array];
|
||||
|
||||
NSArray *removeAtIndices = @[@0, @4, @8, @12, @16];
|
||||
for (NSNumber *index in removeAtIndices) {
|
||||
NSNumber *reactTag = @([index integerValue] + 2);
|
||||
[removedViews addObject:_uiManager.viewRegistry[reactTag]];
|
||||
}
|
||||
for (NSInteger i = 2; i < 20; i++) {
|
||||
UIView *view = _uiManager.viewRegistry[i];
|
||||
[containerView addSubview:view];
|
||||
}
|
||||
|
||||
// Remove views 1-5 from view 20
|
||||
[_uiManager _manageChildren:@20
|
||||
moveFromIndices:nil
|
||||
moveToIndices:nil
|
||||
addChildReactTags:nil
|
||||
addAtIndices:nil
|
||||
removeAtIndices:removeAtIndices
|
||||
registry:_uiManager.viewRegistry];
|
||||
|
||||
XCTAssertEqual(containerView.reactSubviews.count, (NSUInteger)13,
|
||||
@"Expect to have 13 react subviews after calling manage children\
|
||||
with 5 tags to remove and 18 prior children, instead have %zd",
|
||||
containerView.reactSubviews.count);
|
||||
for (UIView *view in removedViews) {
|
||||
XCTAssertTrue([view superview] == nil,
|
||||
@"Expected to have manage children successfully remove children");
|
||||
// After removing views are unregistered - we need to reregister
|
||||
_uiManager.viewRegistry[view.reactTag] = view;
|
||||
}
|
||||
for (NSInteger i = 2; i < 20; i++) {
|
||||
UIView *view = _uiManager.viewRegistry[i];
|
||||
if (![removedViews containsObject:view]) {
|
||||
XCTAssertTrue([view superview] == containerView,
|
||||
@"Should not have removed view with react tag %ld during delete but did", (long)i);
|
||||
[view removeFromSuperview];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We want to start with views 1-10 added at indices 0-9
|
||||
// Then we'll remove indices 2, 3, 5 and 8
|
||||
// Add views 11 and 12 to indices 0 and 6
|
||||
// And move indices 4 and 9 to 1 and 7
|
||||
// So in total it goes from:
|
||||
// [1,2,3,4,5,6,7,8,9,10]
|
||||
// to
|
||||
// [11,5,1,2,7,8,12,10]
|
||||
- (void)testManagingChildrenToAddRemoveAndMove
|
||||
{
|
||||
UIView *containerView = _uiManager.viewRegistry[20];
|
||||
|
||||
NSArray *removeAtIndices = @[@2, @3, @5, @8];
|
||||
NSArray *addAtIndices = @[@0, @6];
|
||||
NSArray *tagsToAdd = @[@11, @12];
|
||||
NSArray *moveFromIndices = @[@4, @9];
|
||||
NSArray *moveToIndices = @[@1, @7];
|
||||
|
||||
// We need to keep these in array to keep them around
|
||||
NSMutableArray *viewsToRemove = [NSMutableArray array];
|
||||
for (NSUInteger i = 0; i < removeAtIndices.count; i++) {
|
||||
NSNumber *reactTagToRemove = @([removeAtIndices[i] integerValue] + 1);
|
||||
UIView *viewToRemove = _uiManager.viewRegistry[reactTagToRemove];
|
||||
[viewsToRemove addObject:viewToRemove];
|
||||
}
|
||||
|
||||
for (NSInteger i = 1; i < 11; i++) {
|
||||
UIView *view = _uiManager.viewRegistry[i];
|
||||
[containerView addSubview:view];
|
||||
}
|
||||
|
||||
[_uiManager _manageChildren:@20
|
||||
moveFromIndices:moveFromIndices
|
||||
moveToIndices:moveToIndices
|
||||
addChildReactTags:tagsToAdd
|
||||
addAtIndices:addAtIndices
|
||||
removeAtIndices:removeAtIndices
|
||||
registry:_uiManager.viewRegistry];
|
||||
|
||||
XCTAssertTrue([[containerView reactSubviews] count] == 8,
|
||||
@"Expect to have 8 react subviews after calling manage children,\
|
||||
instead have the following subviews %@", [containerView reactSubviews]);
|
||||
|
||||
NSArray *expectedReactTags = @[@11, @5, @1, @2, @7, @8, @12, @10];
|
||||
for (NSUInteger i = 0; i < containerView.subviews.count; i++) {
|
||||
XCTAssertEqualObjects([[containerView reactSubviews][i] reactTag], expectedReactTags[i],
|
||||
@"Expected subview at index %ld to have react tag #%@ but has tag #%@",
|
||||
(long)i, expectedReactTags[i], [[containerView reactSubviews][i] reactTag]);
|
||||
}
|
||||
|
||||
// Clean up after ourselves
|
||||
for (NSInteger i = 1; i < 13; i++) {
|
||||
UIView *view = _uiManager.viewRegistry[i];
|
||||
[view removeFromSuperview];
|
||||
}
|
||||
for (UIView *view in viewsToRemove) {
|
||||
_uiManager.viewRegistry[view.reactTag] = view;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
Before Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 97 KiB |
@@ -21,14 +21,14 @@
|
||||
#import "RCTRedBox.h"
|
||||
#import "RCTRootView.h"
|
||||
|
||||
@interface UIExplorerTests : XCTestCase
|
||||
@interface UIExplorerSnapshotTests : XCTestCase
|
||||
{
|
||||
RCTTestRunner *_runner;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation UIExplorerTests
|
||||
@implementation UIExplorerSnapshotTests
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
@@ -37,21 +37,26 @@
|
||||
#endif
|
||||
NSString *version = [[UIDevice currentDevice] systemVersion];
|
||||
RCTAssert([version isEqualToString:@"8.3"], @"Snapshot tests should be run on iOS 8.3, found %@", version);
|
||||
_runner = RCTInitRunnerForApp(@"Examples/UIExplorer/UIExplorerApp.ios");
|
||||
_runner = RCTInitRunnerForApp(@"Examples/UIExplorer/UIExplorerApp.ios", nil);
|
||||
_runner.recordMode = NO;
|
||||
}
|
||||
|
||||
#define RCT_SNAPSHOT_TEST(name, reRecord) \
|
||||
- (void)test##name##Snapshot \
|
||||
{ \
|
||||
_runner.recordMode |= reRecord; \
|
||||
[_runner runTest:_cmd module:@#name]; \
|
||||
#define RCT_TEST(name) \
|
||||
- (void)test##name \
|
||||
{ \
|
||||
[_runner runTest:_cmd module:@#name]; \
|
||||
}
|
||||
|
||||
RCT_SNAPSHOT_TEST(ViewExample, NO)
|
||||
RCT_SNAPSHOT_TEST(LayoutExample, NO)
|
||||
RCT_SNAPSHOT_TEST(TextExample, NO)
|
||||
RCT_SNAPSHOT_TEST(SwitchExample, NO)
|
||||
RCT_SNAPSHOT_TEST(SliderExample, NO)
|
||||
RCT_SNAPSHOT_TEST(TabBarExample, NO)
|
||||
RCT_TEST(ViewExample)
|
||||
RCT_TEST(LayoutExample)
|
||||
RCT_TEST(TextExample)
|
||||
RCT_TEST(SwitchExample)
|
||||
RCT_TEST(SliderExample)
|
||||
RCT_TEST(TabBarExample)
|
||||
|
||||
- (void)testZZZNotInRecordMode
|
||||
{
|
||||
XCTAssertFalse(_runner.recordMode, @"Don't forget to turn record mode back to off");
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -53,7 +53,7 @@ var IntegrationTestsApp = React.createClass({
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.row}>
|
||||
Click on a test to run it in this shell for easier debugging and
|
||||
development. Run all tests in the testing envirnment with cmd+U in
|
||||
development. Run all tests in the testing environment with cmd+U in
|
||||
Xcode.
|
||||
</Text>
|
||||
<View style={styles.separator} />
|
||||
|
||||
@@ -121,7 +121,7 @@ var LayoutEventsTest = React.createClass({
|
||||
ref="img"
|
||||
onLayout={this.onImageLayout}
|
||||
style={styles.image}
|
||||
source={{uri: 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851561_767334496626293_1958532586_n.png'}}
|
||||
source={{uri: 'uie_thumb_big.png'}}
|
||||
/>
|
||||
<Text>
|
||||
ViewLayout: {JSON.stringify(this.state.viewLayout, null, ' ') + '\n\n'}
|
||||
|
||||
@@ -21,6 +21,7 @@ var {
|
||||
ListView,
|
||||
PixelRatio,
|
||||
Platform,
|
||||
Settings,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
@@ -29,18 +30,28 @@ var {
|
||||
} = React;
|
||||
|
||||
var { TestModule } = React.addons;
|
||||
var Settings = require('Settings');
|
||||
|
||||
import type { ExampleModule } from 'ExampleTypes';
|
||||
import type { NavigationContext } from 'NavigationContext';
|
||||
|
||||
var createExamplePage = require('./createExamplePage');
|
||||
|
||||
var COMMON_COMPONENTS = [
|
||||
require('./ImageExample'),
|
||||
require('./LayoutEventsExample'),
|
||||
require('./ListViewExample'),
|
||||
require('./ListViewPagingExample'),
|
||||
require('./MapViewExample'),
|
||||
require('./Navigator/NavigatorExample'),
|
||||
require('./ScrollViewExample'),
|
||||
require('./TextInputExample'),
|
||||
require('./TouchableExample'),
|
||||
require('./ViewExample'),
|
||||
require('./WebViewExample'),
|
||||
];
|
||||
|
||||
var COMMON_APIS = [
|
||||
require('./AnimationExample/AnExApp'),
|
||||
require('./GeolocationExample'),
|
||||
require('./LayoutExample'),
|
||||
require('./PanResponderExample'),
|
||||
@@ -51,23 +62,15 @@ if (Platform.OS === 'ios') {
|
||||
var COMPONENTS = COMMON_COMPONENTS.concat([
|
||||
require('./ActivityIndicatorIOSExample'),
|
||||
require('./DatePickerIOSExample'),
|
||||
require('./ImageExample'),
|
||||
require('./ListViewExample'),
|
||||
require('./ListViewPagingExample'),
|
||||
require('./MapViewExample'),
|
||||
require('./Navigator/NavigatorExample'),
|
||||
require('./NavigatorIOSColorsExample'),
|
||||
require('./NavigatorIOSExample'),
|
||||
require('./PickerIOSExample'),
|
||||
require('./ProgressViewIOSExample'),
|
||||
require('./ScrollViewExample'),
|
||||
require('./SegmentedControlIOSExample'),
|
||||
require('./SliderIOSExample'),
|
||||
require('./SwitchIOSExample'),
|
||||
require('./TabBarIOSExample'),
|
||||
require('./TextExample.ios'),
|
||||
require('./TextInputExample'),
|
||||
require('./TouchableExample'),
|
||||
]);
|
||||
|
||||
var APIS = COMMON_APIS.concat([
|
||||
@@ -79,7 +82,6 @@ if (Platform.OS === 'ios') {
|
||||
require('./AsyncStorageExample'),
|
||||
require('./BorderExample'),
|
||||
require('./CameraRollExample.ios'),
|
||||
require('./LayoutEventsExample'),
|
||||
require('./NetInfoExample'),
|
||||
require('./PushNotificationIOSExample'),
|
||||
require('./StatusBarIOSExample'),
|
||||
@@ -129,7 +131,10 @@ COMPONENTS.concat(APIS).forEach((Example) => {
|
||||
});
|
||||
|
||||
type Props = {
|
||||
navigator: Array<{title: string, component: ReactClass<any,any,any>}>,
|
||||
navigator: {
|
||||
navigationContext: NavigationContext,
|
||||
push: (route: {title: string, component: ReactClass<any,any,any>}) => void,
|
||||
},
|
||||
onExternalExampleRequested: Function,
|
||||
onSelectExample: Function,
|
||||
isInDrawer: bool,
|
||||
@@ -149,8 +154,25 @@ class UIExplorerList extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.navigator.navigationContext.addListener('didfocus', function(event) {
|
||||
if (event.data.route.title === 'UIExplorer') {
|
||||
Settings.set({visibleExample: null});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._search(this.state.searchText);
|
||||
|
||||
var visibleExampleTitle = Settings.get('visibleExample');
|
||||
if (visibleExampleTitle) {
|
||||
var predicate = (example) => example.title === visibleExampleTitle;
|
||||
var foundExample = APIS.find(predicate) || COMPONENTS.find(predicate);
|
||||
if (foundExample) {
|
||||
setTimeout(() => this._openExample(foundExample), 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -168,6 +190,7 @@ class UIExplorerList extends React.Component {
|
||||
onChangeText={this._search.bind(this)}
|
||||
placeholder="Search..."
|
||||
style={[styles.searchTextInput, platformTextInputStyle]}
|
||||
testID="explorer_search"
|
||||
value={this.state.searchText}
|
||||
/>
|
||||
</View>);
|
||||
@@ -240,11 +263,12 @@ class UIExplorerList extends React.Component {
|
||||
Settings.set({searchText: text});
|
||||
}
|
||||
|
||||
_onPressRow(example: any) {
|
||||
_openExample(example: any) {
|
||||
if (example.external) {
|
||||
this.props.onExternalExampleRequested(example);
|
||||
return;
|
||||
}
|
||||
|
||||
var Component = makeRenderable(example);
|
||||
if (Platform.OS === 'ios') {
|
||||
this.props.navigator.push({
|
||||
@@ -258,6 +282,11 @@ class UIExplorerList extends React.Component {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onPressRow(example: any) {
|
||||
Settings.set({visibleExample: example.title});
|
||||
this._openExample(example);
|
||||
}
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
|
||||
@@ -40,6 +40,7 @@ var UIExplorerPage = React.createClass({
|
||||
ContentWrapper = (View: ReactClass<any, any, any>);
|
||||
} else {
|
||||
ContentWrapper = (ScrollView: ReactClass<any, any, any>);
|
||||
wrapperProps.automaticallyAdjustContentInsets = !this.props.title;
|
||||
wrapperProps.keyboardShouldPersistTaps = true;
|
||||
wrapperProps.keyboardDismissMode = 'interactive';
|
||||
}
|
||||
@@ -64,7 +65,6 @@ var UIExplorerPage = React.createClass({
|
||||
var styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: '#e9eaed',
|
||||
paddingTop: 15,
|
||||
flex: 1,
|
||||
},
|
||||
spacer: {
|
||||
@@ -72,6 +72,7 @@ var styles = StyleSheet.create({
|
||||
},
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
paddingTop: 10,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ var styles = StyleSheet.create({
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#d6d7da',
|
||||
margin: 10,
|
||||
marginBottom: 0,
|
||||
height: 45,
|
||||
padding: 10,
|
||||
backgroundColor: 'white',
|
||||
|
||||
@@ -14,25 +14,25 @@
|
||||
XCTAssertEqualObjects(font1, font2); \
|
||||
}
|
||||
|
||||
- (void)DISABLED_testWeight // task #7118691
|
||||
- (void)testWeight
|
||||
{
|
||||
{
|
||||
UIFont *expected = [UIFont fontWithName:@"HelveticaNeue-Bold" size:14];
|
||||
UIFont *expected = [UIFont systemFontOfSize:14 weight:UIFontWeightBold];
|
||||
UIFont *result = [RCTConvert UIFont:@{@"fontWeight": @"bold"}];
|
||||
RCTAssertEqualFonts(expected, result);
|
||||
}
|
||||
{
|
||||
UIFont *expected = [UIFont fontWithName:@"HelveticaNeue-Medium" size:14];
|
||||
UIFont *expected = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
|
||||
UIFont *result = [RCTConvert UIFont:@{@"fontWeight": @"500"}];
|
||||
RCTAssertEqualFonts(expected, result);
|
||||
}
|
||||
{
|
||||
UIFont *expected = [UIFont fontWithName:@"HelveticaNeue-UltraLight" size:14];
|
||||
UIFont *expected = [UIFont systemFontOfSize:14 weight:UIFontWeightUltraLight];
|
||||
UIFont *result = [RCTConvert UIFont:@{@"fontWeight": @"100"}];
|
||||
RCTAssertEqualFonts(expected, result);
|
||||
}
|
||||
{
|
||||
UIFont *expected = [UIFont fontWithName:@"HelveticaNeue" size:14];
|
||||
UIFont *expected = [UIFont systemFontOfSize:14 weight:UIFontWeightRegular];
|
||||
UIFont *result = [RCTConvert UIFont:@{@"fontWeight": @"normal"}];
|
||||
RCTAssertEqualFonts(expected, result);
|
||||
}
|
||||
@@ -41,7 +41,7 @@
|
||||
- (void)testSize
|
||||
{
|
||||
{
|
||||
UIFont *expected = [UIFont fontWithName:@"HelveticaNeue" size:18.5];
|
||||
UIFont *expected = [UIFont systemFontOfSize:18.5];
|
||||
UIFont *result = [RCTConvert UIFont:@{@"fontSize": @18.5}];
|
||||
RCTAssertEqualFonts(expected, result);
|
||||
}
|
||||
@@ -69,32 +69,47 @@
|
||||
- (void)testStyle
|
||||
{
|
||||
{
|
||||
UIFont *expected = [UIFont fontWithName:@"HelveticaNeue-Italic" size:14];
|
||||
UIFont *font = [UIFont systemFontOfSize:14];
|
||||
UIFontDescriptor *fontDescriptor = [font fontDescriptor];
|
||||
UIFontDescriptorSymbolicTraits symbolicTraits = fontDescriptor.symbolicTraits;
|
||||
symbolicTraits |= UIFontDescriptorTraitItalic;
|
||||
fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits];
|
||||
UIFont *expected = [UIFont fontWithDescriptor:fontDescriptor size:14];
|
||||
UIFont *result = [RCTConvert UIFont:@{@"fontStyle": @"italic"}];
|
||||
RCTAssertEqualFonts(expected, result);
|
||||
}
|
||||
{
|
||||
UIFont *expected = [UIFont fontWithName:@"HelveticaNeue" size:14];
|
||||
UIFont *expected = [UIFont systemFontOfSize:14];
|
||||
UIFont *result = [RCTConvert UIFont:@{@"fontStyle": @"normal"}];
|
||||
RCTAssertEqualFonts(expected, result);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)DISABLED_testStyleAndWeight // task #7118691
|
||||
- (void)testStyleAndWeight
|
||||
{
|
||||
{
|
||||
UIFont *expected = [UIFont fontWithName:@"HelveticaNeue-UltraLightItalic" size:14];
|
||||
UIFont *font = [UIFont systemFontOfSize:14 weight:UIFontWeightUltraLight];
|
||||
UIFontDescriptor *fontDescriptor = [font fontDescriptor];
|
||||
UIFontDescriptorSymbolicTraits symbolicTraits = fontDescriptor.symbolicTraits;
|
||||
symbolicTraits |= UIFontDescriptorTraitItalic;
|
||||
fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits];
|
||||
UIFont *expected = [UIFont fontWithDescriptor:fontDescriptor size:14];
|
||||
UIFont *result = [RCTConvert UIFont:@{@"fontStyle": @"italic", @"fontWeight": @"100"}];
|
||||
RCTAssertEqualFonts(expected, result);
|
||||
}
|
||||
{
|
||||
UIFont *expected = [UIFont fontWithName:@"HelveticaNeue-BoldItalic" size:14];
|
||||
UIFont *font = [UIFont systemFontOfSize:14 weight:UIFontWeightBold];
|
||||
UIFontDescriptor *fontDescriptor = [font fontDescriptor];
|
||||
UIFontDescriptorSymbolicTraits symbolicTraits = fontDescriptor.symbolicTraits;
|
||||
symbolicTraits |= UIFontDescriptorTraitItalic;
|
||||
fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits];
|
||||
UIFont *expected = [UIFont fontWithDescriptor:fontDescriptor size:14];
|
||||
UIFont *result = [RCTConvert UIFont:@{@"fontStyle": @"italic", @"fontWeight": @"bold"}];
|
||||
RCTAssertEqualFonts(expected, result);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)DISABLED_testFamilyAndWeight // task #7118691
|
||||
- (void)testFamilyAndWeight
|
||||
{
|
||||
{
|
||||
UIFont *expected = [UIFont fontWithName:@"HelveticaNeue-Bold" size:14];
|
||||
@@ -111,11 +126,6 @@
|
||||
UIFont *result = [RCTConvert UIFont:@{@"fontFamily": @"Cochin", @"fontWeight": @"700"}];
|
||||
RCTAssertEqualFonts(expected, result);
|
||||
}
|
||||
{
|
||||
UIFont *expected = [UIFont fontWithName:@"Cochin" size:14];
|
||||
UIFont *result = [RCTConvert UIFont:@{@"fontFamily": @"Cochin", @"fontWeight": @"500"}]; // regular Cochin is actually medium bold
|
||||
RCTAssertEqualFonts(expected, result);
|
||||
}
|
||||
{
|
||||
UIFont *expected = [UIFont fontWithName:@"Cochin" size:14];
|
||||
UIFont *result = [RCTConvert UIFont:@{@"fontFamily": @"Cochin", @"fontWeight": @"100"}];
|
||||
@@ -137,7 +147,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)DISABLED_testFamilyStyleAndWeight // task #7118691
|
||||
- (void)testFamilyStyleAndWeight
|
||||
{
|
||||
{
|
||||
UIFont *expected = [UIFont fontWithName:@"HelveticaNeue-UltraLightItalic" size:14];
|
||||
@@ -156,4 +166,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testInvalidFont
|
||||
{
|
||||
{
|
||||
UIFont *expected = [UIFont systemFontOfSize:14];
|
||||
UIFont *result = [RCTConvert UIFont:@{@"fontFamily": @"foobar"}];
|
||||
RCTAssertEqualFonts(expected, result);
|
||||
}
|
||||
{
|
||||
UIFont *expected = [UIFont boldSystemFontOfSize:14];
|
||||
UIFont *result = [RCTConvert UIFont:@{@"fontFamily": @"foobar", @"fontWeight": @"bold"}];
|
||||
RCTAssertEqualFonts(expected, result);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* 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 NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK 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.
|
||||
*/
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
|
||||
@@ -15,12 +15,14 @@
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var Platform = require('Platform');
|
||||
var React = require('react-native');
|
||||
var {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} = React;
|
||||
var TouchableWithoutFeedback = require('TouchableWithoutFeedback');
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
box: {
|
||||
@@ -30,6 +32,58 @@ var styles = StyleSheet.create({
|
||||
}
|
||||
});
|
||||
|
||||
var ViewBorderStyleExample = React.createClass({
|
||||
getInitialState() {
|
||||
return {
|
||||
showBorder: true
|
||||
};
|
||||
},
|
||||
|
||||
render() {
|
||||
if (Platform.OS !== 'android') {
|
||||
return (
|
||||
<View style={{backgroundColor: 'red'}}>
|
||||
<Text style={{color: 'white'}}>
|
||||
borderStyle is only supported on android for now.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={this._handlePress}>
|
||||
<View>
|
||||
<View style={{
|
||||
borderWidth: 1,
|
||||
borderRadius: 5,
|
||||
borderStyle: this.state.showBorder ? 'dashed' : null,
|
||||
padding: 5
|
||||
}}>
|
||||
<Text style={{fontSize: 11}}>
|
||||
Dashed border style
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{
|
||||
marginTop: 5,
|
||||
borderWidth: 1,
|
||||
borderRadius: 5,
|
||||
borderStyle: this.state.showBorder ? 'dotted' : null,
|
||||
padding: 5
|
||||
}}>
|
||||
<Text style={{fontSize: 11}}>
|
||||
Dotted border style
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
},
|
||||
|
||||
_handlePress() {
|
||||
this.setState({showBorder: !this.state.showBorder});
|
||||
}
|
||||
});
|
||||
|
||||
exports.title = '<View>';
|
||||
exports.description = 'Basic building block of all UI.';
|
||||
exports.displayName = 'ViewExample';
|
||||
@@ -89,6 +143,11 @@ exports.examples = [
|
||||
</View>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: 'Border Style',
|
||||
render: function() {
|
||||
return <ViewBorderStyleExample />;
|
||||
},
|
||||
}, {
|
||||
title: 'Circle with Border Radius',
|
||||
render: function() {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var StyleSheet = require('StyleSheet');
|
||||
var {
|
||||
StyleSheet,
|
||||
Text,
|
||||
|
||||
@@ -96,6 +96,8 @@
|
||||
0CF68AB81AF0540F00FF9E5C = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CF68AEA1AF0549300FF9E5C /* Brushes */,
|
||||
0CF68AF81AF0549300FF9E5C /* ViewManagers */,
|
||||
0CF68ADB1AF0549300FF9E5C /* ARTCGFloatArray.h */,
|
||||
0CF68ADC1AF0549300FF9E5C /* ARTContainer.h */,
|
||||
0CF68ADD1AF0549300FF9E5C /* ARTGroup.h */,
|
||||
@@ -111,10 +113,8 @@
|
||||
0CF68AE71AF0549300FF9E5C /* ARTText.h */,
|
||||
0CF68AE81AF0549300FF9E5C /* ARTText.m */,
|
||||
0CF68AE91AF0549300FF9E5C /* ARTTextFrame.h */,
|
||||
0CF68AEA1AF0549300FF9E5C /* Brushes */,
|
||||
0CF68AF61AF0549300FF9E5C /* RCTConvert+ART.h */,
|
||||
0CF68AF71AF0549300FF9E5C /* RCTConvert+ART.m */,
|
||||
0CF68AF81AF0549300FF9E5C /* ViewManagers */,
|
||||
0CF68AC21AF0540F00FF9E5C /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
|
||||
@@ -19,15 +19,22 @@
|
||||
_alignment = alignment;
|
||||
}
|
||||
|
||||
static void ARTFreeTextFrame(ARTTextFrame frame)
|
||||
{
|
||||
if (frame.count) {
|
||||
// We must release each line before freeing up this struct
|
||||
for (int i = 0; i < frame.count; i++) {
|
||||
CFRelease(frame.lines[i]);
|
||||
}
|
||||
free(frame.lines);
|
||||
free(frame.widths);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setTextFrame:(ARTTextFrame)frame
|
||||
{
|
||||
if (frame.lines != _textFrame.lines && _textFrame.count) {
|
||||
// We must release each line before overriding the old one
|
||||
for (int i = 0; i < _textFrame.count; i++) {
|
||||
CFRelease(_textFrame.lines[0]);
|
||||
}
|
||||
free(_textFrame.lines);
|
||||
free(_textFrame.widths);
|
||||
if (frame.lines != _textFrame.lines) {
|
||||
ARTFreeTextFrame(_textFrame);
|
||||
}
|
||||
[self invalidate];
|
||||
_textFrame = frame;
|
||||
@@ -35,14 +42,7 @@
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if (_textFrame.count) {
|
||||
// We must release each line before freeing up this struct
|
||||
for (int i = 0; i < _textFrame.count; i++) {
|
||||
CFRelease(_textFrame.lines[0]);
|
||||
}
|
||||
free(_textFrame.lines);
|
||||
free(_textFrame.widths);
|
||||
}
|
||||
ARTFreeTextFrame(_textFrame);
|
||||
}
|
||||
|
||||
- (void)renderLayerTo:(CGContextRef)context
|
||||
|
||||
@@ -10,30 +10,27 @@
|
||||
#import <AdSupport/ASIdentifierManager.h>
|
||||
|
||||
#import "RCTAdSupport.h"
|
||||
#import "RCTUtils.h"
|
||||
|
||||
@implementation RCTAdSupport
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
RCT_EXPORT_METHOD(getAdvertisingId:(RCTResponseSenderBlock)callback
|
||||
withErrorCallback:(RCTResponseSenderBlock)errorCallback)
|
||||
withErrorCallback:(RCTResponseErrorBlock)errorCallback)
|
||||
{
|
||||
if ([ASIdentifierManager class]) {
|
||||
callback(@[[[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString]]);
|
||||
NSUUID *advertisingIdentifier = [ASIdentifierManager sharedManager].advertisingIdentifier;
|
||||
if (advertisingIdentifier) {
|
||||
callback(@[advertisingIdentifier.UUIDString]);
|
||||
} else {
|
||||
return errorCallback(@[@"as_identifier_unavailable"]);
|
||||
errorCallback(RCTErrorWithMessage(@"Advertising identifier is unavailable."));
|
||||
}
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(getAdvertisingTrackingEnabled:(RCTResponseSenderBlock)callback
|
||||
withErrorCallback:(RCTResponseSenderBlock)errorCallback)
|
||||
withErrorCallback:(__unused RCTResponseSenderBlock)errorCallback)
|
||||
{
|
||||
if ([ASIdentifierManager class]) {
|
||||
BOOL hasTracking = [[ASIdentifierManager sharedManager] isAdvertisingTrackingEnabled];
|
||||
callback(@[@(hasTracking)]);
|
||||
} else {
|
||||
return errorCallback(@[@"as_identifier_unavailable"]);
|
||||
}
|
||||
callback(@[@([ASIdentifierManager sharedManager].advertisingTrackingEnabled)]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule Easing
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var bezier = require('bezier');
|
||||
|
||||
/**
|
||||
* This class implements common easing functions. The math is pretty obscure,
|
||||
* but this cool website has nice visual illustrations of what they represent:
|
||||
* http://xaedes.de/dev/transitions/
|
||||
*/
|
||||
class Easing {
|
||||
static step0(n) {
|
||||
return n > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
static step1(n) {
|
||||
return n >= 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
static linear(t) {
|
||||
return t;
|
||||
}
|
||||
|
||||
static ease(t: number): number {
|
||||
return ease(t);
|
||||
}
|
||||
|
||||
static quad(t) {
|
||||
return t * t;
|
||||
}
|
||||
|
||||
static cubic(t) {
|
||||
return t * t * t;
|
||||
}
|
||||
|
||||
static poly(n) {
|
||||
return (t) => Math.pow(t, n);
|
||||
}
|
||||
|
||||
static sin(t) {
|
||||
return 1 - Math.cos(t * Math.PI / 2);
|
||||
}
|
||||
|
||||
static circle(t) {
|
||||
return 1 - Math.sqrt(1 - t * t);
|
||||
}
|
||||
|
||||
static exp(t) {
|
||||
return Math.pow(2, 10 * (t - 1));
|
||||
}
|
||||
|
||||
static elastic(a: number, p: number): (t: number) => number {
|
||||
var tau = Math.PI * 2;
|
||||
// flow isn't smart enough to figure out that s is always assigned to a
|
||||
// number before being used in the returned function
|
||||
var s: any;
|
||||
if (arguments.length < 2) {
|
||||
p = 0.45;
|
||||
}
|
||||
if (arguments.length) {
|
||||
s = p / tau * Math.asin(1 / a);
|
||||
} else {
|
||||
a = 1;
|
||||
s = p / 4;
|
||||
}
|
||||
return (t) => 1 + a * Math.pow(2, -10 * t) * Math.sin((t - s) * tau / p);
|
||||
};
|
||||
|
||||
static back(s: number): (t: number) => number {
|
||||
if (s === undefined) {
|
||||
s = 1.70158;
|
||||
}
|
||||
return (t) => t * t * ((s + 1) * t - s);
|
||||
};
|
||||
|
||||
static bounce(t: number): number {
|
||||
if (t < 1 / 2.75) {
|
||||
return 7.5625 * t * t;
|
||||
}
|
||||
|
||||
if (t < 2 / 2.75) {
|
||||
t -= 1.5 / 2.75;
|
||||
return 7.5625 * t * t + 0.75;
|
||||
}
|
||||
|
||||
if (t < 2.5 / 2.75) {
|
||||
t -= 2.25 / 2.75;
|
||||
return 7.5625 * t * t + 0.9375;
|
||||
}
|
||||
|
||||
t -= 2.625 / 2.75;
|
||||
return 7.5625 * t * t + 0.984375;
|
||||
};
|
||||
|
||||
static bezier(
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
epsilon?: ?number,
|
||||
): (t: number) => number {
|
||||
if (epsilon === undefined) {
|
||||
// epsilon determines the precision of the solved values
|
||||
// a good approximation is:
|
||||
var duration = 500; // duration of animation in milliseconds.
|
||||
epsilon = (1000 / 60 / duration) / 4;
|
||||
}
|
||||
|
||||
return bezier(x1, y1, x2, y2, epsilon);
|
||||
}
|
||||
|
||||
static in(
|
||||
easing: (t: number) => number,
|
||||
): (t: number) => number {
|
||||
return easing;
|
||||
}
|
||||
|
||||
static out(
|
||||
easing: (t: number) => number,
|
||||
): (t: number) => number {
|
||||
return (t) => 1 - easing(1 - t);
|
||||
}
|
||||
|
||||
static inOut(
|
||||
easing: (t: number) => number,
|
||||
): (t: number) => number {
|
||||
return (t) => {
|
||||
if (t < 0.5) {
|
||||
return easing(t * 2) / 2;
|
||||
}
|
||||
return 1 - easing((1 - t) * 2) / 2;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var ease = Easing.bezier(0.42, 0, 1, 1);
|
||||
|
||||
module.exports = Easing;
|
||||
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule Interpolation
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
// TODO(#7644673): fix this hack once github jest actually checks invariants
|
||||
var invariant = function(condition, message) {
|
||||
if (!condition) {
|
||||
var error = new Error(message);
|
||||
(error: any).framesToPop = 1; // $FlowIssue
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
type ExtrapolateType = 'extend' | 'identity' | 'clamp';
|
||||
|
||||
// $FlowFixMe D2163827
|
||||
export type InterpolationConfigType = {
|
||||
inputRange: Array<number>;
|
||||
outputRange: (Array<number> | Array<string>);
|
||||
easing?: ((input: number) => number);
|
||||
extrapolate?: ExtrapolateType;
|
||||
extrapolateLeft?: ExtrapolateType;
|
||||
extrapolateRight?: ExtrapolateType;
|
||||
};
|
||||
|
||||
var linear = (t) => t;
|
||||
|
||||
/**
|
||||
* Very handy helper to map input ranges to output ranges with an easing
|
||||
* function and custom behavior outside of the ranges.
|
||||
*/
|
||||
class Interpolation {
|
||||
static create(config: InterpolationConfigType): (input: number) => number | string {
|
||||
|
||||
if (config.outputRange && typeof config.outputRange[0] === 'string') {
|
||||
return createInterpolationFromStringOutputRange(config);
|
||||
}
|
||||
|
||||
var outputRange: Array<number> = (config.outputRange: any);
|
||||
checkInfiniteRange('outputRange', outputRange);
|
||||
|
||||
var inputRange = config.inputRange;
|
||||
checkInfiniteRange('inputRange', inputRange);
|
||||
checkValidInputRange(inputRange);
|
||||
|
||||
invariant(
|
||||
inputRange.length === outputRange.length,
|
||||
'inputRange (' + inputRange.length + ') and outputRange (' +
|
||||
outputRange.length + ') must have the same length'
|
||||
);
|
||||
|
||||
var easing = config.easing || linear;
|
||||
|
||||
var extrapolateLeft: ExtrapolateType = 'extend';
|
||||
if (config.extrapolateLeft !== undefined) {
|
||||
extrapolateLeft = config.extrapolateLeft;
|
||||
} else if (config.extrapolate !== undefined) {
|
||||
extrapolateLeft = config.extrapolate;
|
||||
}
|
||||
|
||||
var extrapolateRight: ExtrapolateType = 'extend';
|
||||
if (config.extrapolateRight !== undefined) {
|
||||
extrapolateRight = config.extrapolateRight;
|
||||
} else if (config.extrapolate !== undefined) {
|
||||
extrapolateRight = config.extrapolate;
|
||||
}
|
||||
|
||||
return (input) => {
|
||||
invariant(
|
||||
typeof input === 'number',
|
||||
'Cannot interpolation an input which is not a number'
|
||||
);
|
||||
|
||||
var range = findRange(input, inputRange);
|
||||
return interpolate(
|
||||
input,
|
||||
inputRange[range],
|
||||
inputRange[range + 1],
|
||||
outputRange[range],
|
||||
outputRange[range + 1],
|
||||
easing,
|
||||
extrapolateLeft,
|
||||
extrapolateRight,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function interpolate(
|
||||
input: number,
|
||||
inputMin: number,
|
||||
inputMax: number,
|
||||
outputMin: number,
|
||||
outputMax: number,
|
||||
easing: ((input: number) => number),
|
||||
extrapolateLeft: ExtrapolateType,
|
||||
extrapolateRight: ExtrapolateType,
|
||||
) {
|
||||
var result = input;
|
||||
|
||||
// Extrapolate
|
||||
if (result < inputMin) {
|
||||
if (extrapolateLeft === 'identity') {
|
||||
return result;
|
||||
} else if (extrapolateLeft === 'clamp') {
|
||||
result = inputMin;
|
||||
} else if (extrapolateLeft === 'extend') {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
if (result > inputMax) {
|
||||
if (extrapolateRight === 'identity') {
|
||||
return result;
|
||||
} else if (extrapolateRight === 'clamp') {
|
||||
result = inputMax;
|
||||
} else if (extrapolateRight === 'extend') {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
if (outputMin === outputMax) {
|
||||
return outputMin;
|
||||
}
|
||||
|
||||
if (inputMin === inputMax) {
|
||||
if (input <= inputMin) {
|
||||
return outputMin;
|
||||
}
|
||||
return outputMax;
|
||||
}
|
||||
|
||||
// Input Range
|
||||
if (inputMin === -Infinity) {
|
||||
result = -result;
|
||||
} else if (inputMax === Infinity) {
|
||||
result = result - inputMin;
|
||||
} else {
|
||||
result = (result - inputMin) / (inputMax - inputMin);
|
||||
}
|
||||
|
||||
// Easing
|
||||
result = easing(result);
|
||||
|
||||
// Output Range
|
||||
if (outputMin === -Infinity) {
|
||||
result = -result;
|
||||
} else if (outputMax === Infinity) {
|
||||
result = result + outputMin;
|
||||
} else {
|
||||
result = result * (outputMax - outputMin) + outputMin;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
var stringShapeRegex = /[0-9\.-]+/g;
|
||||
|
||||
/**
|
||||
* Supports string shapes by extracting numbers so new values can be computed,
|
||||
* and recombines those values into new strings of the same shape. Supports
|
||||
* things like:
|
||||
*
|
||||
* rgba(123, 42, 99, 0.36) // colors
|
||||
* -45deg // values with units
|
||||
*/
|
||||
function createInterpolationFromStringOutputRange(
|
||||
config: InterpolationConfigType,
|
||||
): (input: number) => string {
|
||||
var outputRange: Array<string> = (config.outputRange: any);
|
||||
invariant(outputRange.length >= 2, 'Bad output range');
|
||||
checkPattern(outputRange);
|
||||
|
||||
// ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)']
|
||||
// ->
|
||||
// [
|
||||
// [0, 50],
|
||||
// [100, 150],
|
||||
// [200, 250],
|
||||
// [0, 0.5],
|
||||
// ]
|
||||
var outputRanges = outputRange[0].match(stringShapeRegex).map(() => []);
|
||||
outputRange.forEach(value => {
|
||||
value.match(stringShapeRegex).forEach((number, i) => {
|
||||
outputRanges[i].push(+number);
|
||||
});
|
||||
});
|
||||
|
||||
var interpolations = outputRange[0].match(stringShapeRegex).map((value, i) => {
|
||||
return Interpolation.create({
|
||||
...config,
|
||||
outputRange: outputRanges[i],
|
||||
});
|
||||
});
|
||||
|
||||
return (input) => {
|
||||
var i = 0;
|
||||
// 'rgba(0, 100, 200, 0)'
|
||||
// ->
|
||||
// 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
|
||||
return outputRange[0].replace(stringShapeRegex, () => {
|
||||
return String(interpolations[i++](input));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function checkPattern(arr: Array<string>) {
|
||||
var pattern = arr[0].replace(stringShapeRegex, '');
|
||||
for (var i = 1; i < arr.length; ++i) {
|
||||
invariant(
|
||||
pattern === arr[i].replace(stringShapeRegex, ''),
|
||||
'invalid pattern ' + arr[0] + ' and ' + arr[i],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function findRange(input: number, inputRange: Array<number>) {
|
||||
for (var i = 1; i < inputRange.length - 1; ++i) {
|
||||
if (inputRange[i] >= input) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return i - 1;
|
||||
}
|
||||
|
||||
function checkValidInputRange(arr: Array<number>) {
|
||||
invariant(arr.length >= 2, 'inputRange must have at least 2 elements');
|
||||
for (var i = 1; i < arr.length; ++i) {
|
||||
invariant(
|
||||
arr[i] >= arr[i - 1],
|
||||
/* $FlowFixMe(>=0.13.0) - In the addition expression below this comment,
|
||||
* one or both of the operands may be something that doesn't cleanly
|
||||
* convert to a string, like undefined, null, and object, etc. If you really
|
||||
* mean this implicit string conversion, you can do something like
|
||||
* String(myThing)
|
||||
*/
|
||||
'inputRange must be monotonically increasing ' + arr
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function checkInfiniteRange(name: string, arr: Array<number>) {
|
||||
invariant(arr.length >= 2, name + ' must have at least 2 elements');
|
||||
invariant(
|
||||
arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity,
|
||||
/* $FlowFixMe(>=0.13.0) - In the addition expression below this comment,
|
||||
* one or both of the operands may be something that doesn't cleanly convert
|
||||
* to a string, like undefined, null, and object, etc. If you really mean
|
||||
* this implicit string conversion, you can do something like
|
||||
* String(myThing)
|
||||
*/
|
||||
name + 'cannot be ]-infinity;+infinity[ ' + arr
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = Interpolation;
|
||||
@@ -0,0 +1,493 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest
|
||||
.autoMockOff()
|
||||
.setMock('Text', {})
|
||||
.setMock('View', {})
|
||||
.setMock('Image', {})
|
||||
.setMock('React', {Component: class {}});
|
||||
|
||||
var Animated = require('Animated');
|
||||
|
||||
describe('Animated', () => {
|
||||
it('works end to end', () => {
|
||||
var anim = new Animated.Value(0);
|
||||
|
||||
var callback = jest.genMockFunction();
|
||||
|
||||
var node = new Animated.__PropsOnlyForTests({
|
||||
style: {
|
||||
backgroundColor: 'red',
|
||||
opacity: anim,
|
||||
transform: [
|
||||
{translateX: anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [100, 200],
|
||||
})},
|
||||
{scale: anim},
|
||||
]
|
||||
}
|
||||
}, callback);
|
||||
|
||||
expect(anim.getChildren().length).toBe(3);
|
||||
|
||||
expect(node.__getValue()).toEqual({
|
||||
style: {
|
||||
backgroundColor: 'red',
|
||||
opacity: 0,
|
||||
transform: [
|
||||
{translateX: 100},
|
||||
{scale: 0},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
anim.setValue(0.5);
|
||||
|
||||
expect(callback).toBeCalled();
|
||||
|
||||
expect(node.__getValue()).toEqual({
|
||||
style: {
|
||||
backgroundColor: 'red',
|
||||
opacity: 0.5,
|
||||
transform: [
|
||||
{translateX: 150},
|
||||
{scale: 0.5},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
node.detach();
|
||||
expect(anim.getChildren().length).toBe(0);
|
||||
|
||||
anim.setValue(1);
|
||||
expect(callback.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('does not detach on updates', () => {
|
||||
var anim = new Animated.Value(0);
|
||||
anim.detach = jest.genMockFunction();
|
||||
|
||||
var c = new Animated.View();
|
||||
c.props = {
|
||||
style: {
|
||||
opacity: anim,
|
||||
},
|
||||
};
|
||||
c.componentWillMount();
|
||||
|
||||
expect(anim.detach).not.toBeCalled();
|
||||
c.componentWillReceiveProps({
|
||||
style: {
|
||||
opacity: anim,
|
||||
},
|
||||
});
|
||||
expect(anim.detach).not.toBeCalled();
|
||||
|
||||
c.componentWillUnmount();
|
||||
expect(anim.detach).toBeCalled();
|
||||
});
|
||||
|
||||
|
||||
it('stops animation when detached', () => {
|
||||
// jest environment doesn't have requestAnimationFrame :(
|
||||
window.requestAnimationFrame = jest.genMockFunction();
|
||||
window.cancelAnimationFrame = jest.genMockFunction();
|
||||
|
||||
var anim = new Animated.Value(0);
|
||||
var callback = jest.genMockFunction();
|
||||
|
||||
var c = new Animated.View();
|
||||
c.props = {
|
||||
style: {
|
||||
opacity: anim,
|
||||
},
|
||||
};
|
||||
c.componentWillMount();
|
||||
|
||||
Animated.timing(anim, {toValue: 10, duration: 1000}).start(callback);
|
||||
|
||||
c.componentWillUnmount();
|
||||
|
||||
expect(callback).toBeCalledWith({finished: false});
|
||||
expect(callback).toBeCalledWith({finished: false});
|
||||
});
|
||||
|
||||
it('triggers callback when spring is at rest', () => {
|
||||
var anim = new Animated.Value(0);
|
||||
var callback = jest.genMockFunction();
|
||||
Animated.spring(anim, {toValue: 0, velocity: 0}).start(callback);
|
||||
expect(callback).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Animated Sequence', () => {
|
||||
|
||||
it('works with an empty sequence', () => {
|
||||
var cb = jest.genMockFunction();
|
||||
Animated.sequence([]).start(cb);
|
||||
expect(cb).toBeCalledWith({finished: true});
|
||||
});
|
||||
|
||||
it('sequences well', () => {
|
||||
var anim1 = {start: jest.genMockFunction()};
|
||||
var anim2 = {start: jest.genMockFunction()};
|
||||
var cb = jest.genMockFunction();
|
||||
|
||||
var seq = Animated.sequence([anim1, anim2]);
|
||||
|
||||
expect(anim1.start).not.toBeCalled();
|
||||
expect(anim2.start).not.toBeCalled();
|
||||
|
||||
seq.start(cb);
|
||||
|
||||
expect(anim1.start).toBeCalled();
|
||||
expect(anim2.start).not.toBeCalled();
|
||||
expect(cb).not.toBeCalled();
|
||||
|
||||
anim1.start.mock.calls[0][0]({finished: true});
|
||||
|
||||
expect(anim2.start).toBeCalled();
|
||||
expect(cb).not.toBeCalled();
|
||||
|
||||
anim2.start.mock.calls[0][0]({finished: true});
|
||||
expect(cb).toBeCalledWith({finished: true});
|
||||
});
|
||||
|
||||
it('supports interrupting sequence', () => {
|
||||
var anim1 = {start: jest.genMockFunction()};
|
||||
var anim2 = {start: jest.genMockFunction()};
|
||||
var cb = jest.genMockFunction();
|
||||
|
||||
Animated.sequence([anim1, anim2]).start(cb);
|
||||
|
||||
anim1.start.mock.calls[0][0]({finished: false});
|
||||
|
||||
expect(anim1.start).toBeCalled();
|
||||
expect(anim2.start).not.toBeCalled();
|
||||
expect(cb).toBeCalledWith({finished: false});
|
||||
});
|
||||
|
||||
it('supports stopping sequence', () => {
|
||||
var anim1 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var anim2 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var cb = jest.genMockFunction();
|
||||
|
||||
var seq = Animated.sequence([anim1, anim2]);
|
||||
seq.start(cb);
|
||||
seq.stop();
|
||||
|
||||
expect(anim1.stop).toBeCalled();
|
||||
expect(anim2.stop).not.toBeCalled();
|
||||
expect(cb).not.toBeCalled();
|
||||
|
||||
anim1.start.mock.calls[0][0]({finished: false});
|
||||
|
||||
expect(cb).toBeCalledWith({finished: false});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Animated Parallel', () => {
|
||||
|
||||
it('works with an empty parallel', () => {
|
||||
var cb = jest.genMockFunction();
|
||||
Animated.parallel([]).start(cb);
|
||||
expect(cb).toBeCalledWith({finished: true});
|
||||
});
|
||||
|
||||
|
||||
it('parellelizes well', () => {
|
||||
var anim1 = {start: jest.genMockFunction()};
|
||||
var anim2 = {start: jest.genMockFunction()};
|
||||
var cb = jest.genMockFunction();
|
||||
|
||||
var par = Animated.parallel([anim1, anim2]);
|
||||
|
||||
expect(anim1.start).not.toBeCalled();
|
||||
expect(anim2.start).not.toBeCalled();
|
||||
|
||||
par.start(cb);
|
||||
|
||||
expect(anim1.start).toBeCalled();
|
||||
expect(anim2.start).toBeCalled();
|
||||
expect(cb).not.toBeCalled();
|
||||
|
||||
anim1.start.mock.calls[0][0]({finished: true});
|
||||
expect(cb).not.toBeCalled();
|
||||
|
||||
anim2.start.mock.calls[0][0]({finished: true});
|
||||
expect(cb).toBeCalledWith({finished: true});
|
||||
});
|
||||
|
||||
it('supports stopping parallel', () => {
|
||||
var anim1 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var anim2 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var cb = jest.genMockFunction();
|
||||
|
||||
var seq = Animated.parallel([anim1, anim2]);
|
||||
seq.start(cb);
|
||||
seq.stop();
|
||||
|
||||
expect(anim1.stop).toBeCalled();
|
||||
expect(anim2.stop).toBeCalled();
|
||||
expect(cb).not.toBeCalled();
|
||||
|
||||
anim1.start.mock.calls[0][0]({finished: false});
|
||||
expect(cb).not.toBeCalled();
|
||||
|
||||
anim2.start.mock.calls[0][0]({finished: false});
|
||||
expect(cb).toBeCalledWith({finished: false});
|
||||
});
|
||||
|
||||
|
||||
it('does not call stop more than once when stopping', () => {
|
||||
var anim1 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var anim2 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var anim3 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var cb = jest.genMockFunction();
|
||||
|
||||
var seq = Animated.parallel([anim1, anim2, anim3]);
|
||||
seq.start(cb);
|
||||
|
||||
anim1.start.mock.calls[0][0]({finished: false});
|
||||
|
||||
expect(anim1.stop.mock.calls.length).toBe(0);
|
||||
expect(anim2.stop.mock.calls.length).toBe(1);
|
||||
expect(anim3.stop.mock.calls.length).toBe(1);
|
||||
|
||||
anim2.start.mock.calls[0][0]({finished: false});
|
||||
|
||||
expect(anim1.stop.mock.calls.length).toBe(0);
|
||||
expect(anim2.stop.mock.calls.length).toBe(1);
|
||||
expect(anim3.stop.mock.calls.length).toBe(1);
|
||||
|
||||
anim3.start.mock.calls[0][0]({finished: false});
|
||||
|
||||
expect(anim1.stop.mock.calls.length).toBe(0);
|
||||
expect(anim2.stop.mock.calls.length).toBe(1);
|
||||
expect(anim3.stop.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Animated delays', () => {
|
||||
it('should call anim after delay in sequence', () => {
|
||||
var anim = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var cb = jest.genMockFunction();
|
||||
Animated.sequence([
|
||||
Animated.delay(1000),
|
||||
anim,
|
||||
]).start(cb);
|
||||
jest.runAllTimers();
|
||||
expect(anim.start.mock.calls.length).toBe(1);
|
||||
expect(cb).not.toBeCalled();
|
||||
anim.start.mock.calls[0][0]({finished: true});
|
||||
expect(cb).toBeCalledWith({finished: true});
|
||||
});
|
||||
it('should run stagger to end', () => {
|
||||
var cb = jest.genMockFunction();
|
||||
Animated.stagger(1000, [
|
||||
Animated.delay(1000),
|
||||
Animated.delay(1000),
|
||||
Animated.delay(1000),
|
||||
]).start(cb);
|
||||
jest.runAllTimers();
|
||||
expect(cb).toBeCalledWith({finished: true});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Animated Events', () => {
|
||||
it('should map events', () => {
|
||||
var value = new Animated.Value(0);
|
||||
var handler = Animated.event(
|
||||
[null, {state: {foo: value}}],
|
||||
);
|
||||
handler({bar: 'ignoreBar'}, {state: {baz: 'ignoreBaz', foo: 42}});
|
||||
expect(value.__getValue()).toBe(42);
|
||||
});
|
||||
it('should call listeners', () => {
|
||||
var value = new Animated.Value(0);
|
||||
var listener = jest.genMockFunction();
|
||||
var handler = Animated.event(
|
||||
[{foo: value}],
|
||||
{listener},
|
||||
);
|
||||
handler({foo: 42});
|
||||
expect(value.__getValue()).toBe(42);
|
||||
expect(listener.mock.calls.length).toBe(1);
|
||||
expect(listener).toBeCalledWith({foo: 42});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Animated Tracking', () => {
|
||||
it('should track values', () => {
|
||||
var value1 = new Animated.Value(0);
|
||||
var value2 = new Animated.Value(0);
|
||||
Animated.timing(value2, {
|
||||
toValue: value1,
|
||||
duration: 0,
|
||||
}).start();
|
||||
value1.setValue(42);
|
||||
expect(value2.__getValue()).toBe(42);
|
||||
value1.setValue(7);
|
||||
expect(value2.__getValue()).toBe(7);
|
||||
});
|
||||
|
||||
it('should track interpolated values', () => {
|
||||
var value1 = new Animated.Value(0);
|
||||
var value2 = new Animated.Value(0);
|
||||
Animated.timing(value2, {
|
||||
toValue: value1.interpolate({
|
||||
inputRange: [0, 2],
|
||||
outputRange: [0, 1]
|
||||
}),
|
||||
duration: 0,
|
||||
}).start();
|
||||
value1.setValue(42);
|
||||
expect(value2.__getValue()).toBe(42 / 2);
|
||||
});
|
||||
|
||||
it('should stop tracking when animated', () => {
|
||||
var value1 = new Animated.Value(0);
|
||||
var value2 = new Animated.Value(0);
|
||||
Animated.timing(value2, {
|
||||
toValue: value1,
|
||||
duration: 0,
|
||||
}).start();
|
||||
value1.setValue(42);
|
||||
expect(value2.__getValue()).toBe(42);
|
||||
Animated.timing(value2, {
|
||||
toValue: 7,
|
||||
duration: 0,
|
||||
}).start();
|
||||
value1.setValue(1492);
|
||||
expect(value2.__getValue()).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Animated Vectors', () => {
|
||||
it('should animate vectors', () => {
|
||||
var vec = new Animated.ValueXY();
|
||||
|
||||
var callback = jest.genMockFunction();
|
||||
|
||||
var node = new Animated.__PropsOnlyForTests({
|
||||
style: {
|
||||
opacity: vec.x.interpolate({
|
||||
inputRange: [0, 42],
|
||||
outputRange: [0.2, 0.8],
|
||||
}),
|
||||
transform: vec.getTranslateTransform(),
|
||||
...vec.getLayout(),
|
||||
}
|
||||
}, callback);
|
||||
|
||||
expect(node.__getValue()).toEqual({
|
||||
style: {
|
||||
opacity: 0.2,
|
||||
transform: [
|
||||
{translateX: 0},
|
||||
{translateY: 0},
|
||||
],
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
});
|
||||
|
||||
vec.setValue({x: 42, y: 1492});
|
||||
|
||||
expect(callback.mock.calls.length).toBe(2); // once each for x, y
|
||||
|
||||
expect(node.__getValue()).toEqual({
|
||||
style: {
|
||||
opacity: 0.8,
|
||||
transform: [
|
||||
{translateX: 42},
|
||||
{translateY: 1492},
|
||||
],
|
||||
left: 42,
|
||||
top: 1492,
|
||||
},
|
||||
});
|
||||
|
||||
node.detach();
|
||||
|
||||
vec.setValue({x: 1, y: 1});
|
||||
expect(callback.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should track vectors', () => {
|
||||
var value1 = new Animated.ValueXY();
|
||||
var value2 = new Animated.ValueXY();
|
||||
Animated.timing(value2, {
|
||||
toValue: value1,
|
||||
duration: 0,
|
||||
}).start();
|
||||
value1.setValue({x: 42, y: 1492});
|
||||
expect(value2.__getValue()).toEqual({x: 42, y: 1492});
|
||||
|
||||
// Make sure tracking keeps working (see stopTogether in ParallelConfig used
|
||||
// by maybeVectorAnim).
|
||||
value1.setValue({x: 3, y: 4});
|
||||
expect(value2.__getValue()).toEqual({x: 3, y: 4});
|
||||
});
|
||||
|
||||
it('should track with springs', () => {
|
||||
var value1 = new Animated.ValueXY();
|
||||
var value2 = new Animated.ValueXY();
|
||||
Animated.spring(value2, {
|
||||
toValue: value1,
|
||||
tension: 3000, // faster spring for faster test
|
||||
friction: 60,
|
||||
}).start();
|
||||
value1.setValue({x: 1, y: 1});
|
||||
jest.runAllTimers();
|
||||
expect(Math.round(value2.__getValue().x)).toEqual(1);
|
||||
expect(Math.round(value2.__getValue().y)).toEqual(1);
|
||||
value1.setValue({x: 2, y: 2});
|
||||
jest.runAllTimers();
|
||||
expect(Math.round(value2.__getValue().x)).toEqual(2);
|
||||
expect(Math.round(value2.__getValue().y)).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Animated Listeners', () => {
|
||||
it('should get updates', () => {
|
||||
var value1 = new Animated.Value(0);
|
||||
var listener = jest.genMockFunction();
|
||||
var id = value1.addListener(listener);
|
||||
value1.setValue(42);
|
||||
expect(listener.mock.calls.length).toBe(1);
|
||||
expect(listener).toBeCalledWith({value: 42});
|
||||
expect(value1.__getValue()).toBe(42);
|
||||
value1.setValue(7);
|
||||
expect(listener.mock.calls.length).toBe(2);
|
||||
expect(listener).toBeCalledWith({value: 7});
|
||||
expect(value1.__getValue()).toBe(7);
|
||||
value1.removeListener(id);
|
||||
value1.setValue(1492);
|
||||
expect(listener.mock.calls.length).toBe(2);
|
||||
expect(value1.__getValue()).toBe(1492);
|
||||
});
|
||||
|
||||
it('should removeAll', () => {
|
||||
var value1 = new Animated.Value(0);
|
||||
var listener = jest.genMockFunction();
|
||||
[1,2,3,4].forEach(() => value1.addListener(listener));
|
||||
value1.setValue(42);
|
||||
expect(listener.mock.calls.length).toBe(4);
|
||||
expect(listener).toBeCalledWith({value: 42});
|
||||
value1.removeAllListeners();
|
||||
value1.setValue(7);
|
||||
expect(listener.mock.calls.length).toBe(4);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest.dontMock('Easing');
|
||||
|
||||
var Easing = require('Easing');
|
||||
describe('Easing', () => {
|
||||
it('should work with linear', () => {
|
||||
var easing = Easing.linear;
|
||||
|
||||
expect(easing(0)).toBe(0);
|
||||
expect(easing(0.5)).toBe(0.5);
|
||||
expect(easing(0.8)).toBe(0.8);
|
||||
expect(easing(1)).toBe(1);
|
||||
});
|
||||
|
||||
it('should work with ease in linear', () => {
|
||||
var easing = Easing.in(Easing.linear);
|
||||
expect(easing(0)).toBe(0);
|
||||
expect(easing(0.5)).toBe(0.5);
|
||||
expect(easing(0.8)).toBe(0.8);
|
||||
expect(easing(1)).toBe(1);
|
||||
});
|
||||
|
||||
it('should work with easy out linear', () => {
|
||||
var easing = Easing.out(Easing.linear);
|
||||
expect(easing(0)).toBe(0);
|
||||
expect(easing(0.5)).toBe(0.5);
|
||||
expect(easing(0.6)).toBe(0.6);
|
||||
expect(easing(1)).toBe(1);
|
||||
});
|
||||
|
||||
it('should work with ease in quad', () => {
|
||||
function easeInQuad(t) {
|
||||
return t * t;
|
||||
}
|
||||
var easing = Easing.in(Easing.quad);
|
||||
for (var t = -0.5; t < 1.5; t += 0.1) {
|
||||
expect(easing(t)).toBe(easeInQuad(t));
|
||||
}
|
||||
});
|
||||
|
||||
it('should work with ease out quad', () => {
|
||||
function easeOutQuad(t) {
|
||||
return -t * (t - 2);
|
||||
}
|
||||
var easing = Easing.out(Easing.quad);
|
||||
for (var t = 0; t <= 1; t += 0.1) {
|
||||
expect(easing(1)).toBe(easeOutQuad(1));
|
||||
}
|
||||
});
|
||||
|
||||
it('should work with ease in-out quad', () => {
|
||||
function easeInOutQuad(t) {
|
||||
t = t * 2;
|
||||
if (t < 1) {
|
||||
return 0.5 * t * t;
|
||||
}
|
||||
return -((t - 1) * (t - 3) - 1) / 2;
|
||||
}
|
||||
var easing = Easing.inOut(Easing.quad);
|
||||
for (var t = -0.5; t < 1.5; t += 0.1) {
|
||||
expect(easing(t)).toBeCloseTo(easeInOutQuad(t), 4);
|
||||
}
|
||||
});
|
||||
|
||||
function sampleEasingFunction(easing) {
|
||||
var DURATION = 300;
|
||||
var tickCount = Math.round(DURATION * 60 / 1000);
|
||||
var samples = [];
|
||||
for (var i = 0; i <= tickCount; i++) {
|
||||
samples.push(easing(i / tickCount));
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
var Samples = {
|
||||
in_quad: [0,0.0030864197530864196,0.012345679012345678,0.027777777777777776,0.04938271604938271,0.0771604938271605,0.1111111111111111,0.15123456790123457,0.19753086419753085,0.25,0.308641975308642,0.37345679012345684,0.4444444444444444,0.5216049382716049,0.6049382716049383,0.6944444444444445,0.7901234567901234,0.8919753086419753,1],
|
||||
out_quad: [0,0.10802469135802469,0.20987654320987653,0.3055555555555555,0.3950617283950617,0.47839506172839513,0.5555555555555556,0.6265432098765432,0.691358024691358,0.75,0.8024691358024691,0.8487654320987654,0.888888888888889,0.9228395061728394,0.9506172839506174,0.9722222222222221,0.9876543209876543,0.9969135802469136,1],
|
||||
inOut_quad: [0,0.006172839506172839,0.024691358024691357,0.05555555555555555,0.09876543209876543,0.154320987654321,0.2222222222222222,0.30246913580246915,0.3950617283950617,0.5,0.6049382716049383,0.697530864197531,0.7777777777777777,0.845679012345679,0.9012345679012346,0.9444444444444444,0.9753086419753086,0.9938271604938271,1],
|
||||
in_cubic: [0,0.00017146776406035664,0.0013717421124828531,0.004629629629629629,0.010973936899862825,0.021433470507544586,0.037037037037037035,0.05881344307270234,0.0877914951989026,0.125,0.1714677640603567,0.22822359396433475,0.2962962962962963,0.37671467764060357,0.4705075445816187,0.5787037037037038,0.7023319615912208,0.8424211248285322,1],
|
||||
out_cubic: [0,0.15757887517146785,0.2976680384087792,0.42129629629629617,0.5294924554183813,0.6232853223593964,0.7037037037037036,0.7717764060356652,0.8285322359396433,0.875,0.9122085048010974,0.9411865569272977,0.9629629629629629,0.9785665294924554,0.9890260631001372,0.9953703703703703,0.9986282578875172,0.9998285322359396,1],
|
||||
inOut_cubic: [0,0.0006858710562414266,0.0054869684499314125,0.018518518518518517,0.0438957475994513,0.08573388203017834,0.14814814814814814,0.23525377229080935,0.3511659807956104,0.5,0.6488340192043895,0.7647462277091908,0.8518518518518519,0.9142661179698217,0.9561042524005487,0.9814814814814815,0.9945130315500685,0.9993141289437586,1],
|
||||
in_sin: [0,0.003805301908254455,0.01519224698779198,0.03407417371093169,0.06030737921409157,0.09369221296335006,0.1339745962155613,0.1808479557110082,0.233955556881022,0.2928932188134524,0.35721239031346064,0.42642356364895384,0.4999999999999999,0.5773817382593005,0.6579798566743311,0.7411809548974793,0.8263518223330696,0.9128442572523416,0.9999999999999999],
|
||||
out_sin: [0,0.08715574274765817,0.17364817766693033,0.25881904510252074,0.3420201433256687,0.42261826174069944,0.49999999999999994,0.573576436351046,0.6427876096865393,0.7071067811865475,0.766044443118978,0.8191520442889918,0.8660254037844386,0.9063077870366499,0.9396926207859083,0.9659258262890683,0.984807753012208,0.9961946980917455,1],
|
||||
inOut_sin: [0,0.00759612349389599,0.030153689607045786,0.06698729810778065,0.116977778440511,0.17860619515673032,0.24999999999999994,0.32898992833716556,0.4131759111665348,0.49999999999999994,0.5868240888334652,0.6710100716628343,0.7499999999999999,0.8213938048432696,0.883022221559489,0.9330127018922194,0.9698463103929542,0.9924038765061041,1],
|
||||
in_exp: [0,0.0014352875901128893,0.002109491677524035,0.0031003926796253885,0.004556754060844206,0.006697218616039631,0.009843133202303688,0.014466792379488908,0.021262343752724643,0.03125,0.045929202883612456,0.06750373368076916,0.09921256574801243,0.1458161299470146,0.2143109957132682,0.31498026247371835,0.46293735614364506,0.6803950000871883,1],
|
||||
out_exp: [0,0.31960499991281155,0.5370626438563548,0.6850197375262816,0.7856890042867318,0.8541838700529854,0.9007874342519875,0.9324962663192309,0.9540707971163875,0.96875,0.9787376562472754,0.9855332076205111,0.9901568667976963,0.9933027813839603,0.9954432459391558,0.9968996073203746,0.9978905083224759,0.9985647124098871,1],
|
||||
inOut_exp: [0,0.0010547458387620175,0.002278377030422103,0.004921566601151844,0.010631171876362321,0.022964601441806228,0.049606282874006216,0.1071554978566341,0.23146867807182253,0.5,0.7685313219281775,0.892844502143366,0.9503937171259937,0.9770353985581938,0.9893688281236377,0.9950784333988482,0.9977216229695779,0.998945254161238,1],
|
||||
in_circle: [0,0.0015444024660317135,0.006192010000093506,0.013986702816730645,0.025003956956430873,0.03935464078941209,0.057190958417936644,0.07871533601238889,0.10419358352238339,0.1339745962155614,0.1685205807169019,0.20845517506805522,0.2546440075000701,0.3083389112228482,0.37146063894529113,0.4472292016074334,0.5418771527091488,0.6713289009389102,1],
|
||||
out_circle: [0,0.3286710990610898,0.45812284729085123,0.5527707983925666,0.6285393610547089,0.6916610887771518,0.7453559924999298,0.7915448249319448,0.8314794192830981,0.8660254037844386,0.8958064164776166,0.9212846639876111,0.9428090415820634,0.9606453592105879,0.9749960430435691,0.9860132971832694,0.9938079899999065,0.9984555975339683,1],
|
||||
inOut_circle: [0,0.003096005000046753,0.012501978478215436,0.028595479208968322,0.052096791761191696,0.08426029035845095,0.12732200375003505,0.18573031947264557,0.2709385763545744,0.5,0.7290614236454256,0.8142696805273546,0.8726779962499649,0.915739709641549,0.9479032082388084,0.9714045207910317,0.9874980215217846,0.9969039949999532,1],
|
||||
in_back_: [0,-0.004788556241426612,-0.017301289437585736,-0.0347587962962963,-0.05438167352537723,-0.07339051783264748,-0.08900592592592595,-0.09844849451303156,-0.0989388203017833,-0.08769750000000004,-0.06194513031550073,-0.018902307956104283,0.044210370370370254,0.13017230795610413,0.2417629080932785,0.3817615740740742,0.5529477091906719,0.7581007167352535,0.9999999999999998],
|
||||
out_back_: [2.220446049250313e-16,0.24189928326474652,0.44705229080932807,0.6182384259259258,0.7582370919067215,0.8698276920438959,0.9557896296296297,1.0189023079561044,1.0619451303155008,1.0876975,1.0989388203017834,1.0984484945130315,1.089005925925926,1.0733905178326475,1.0543816735253773,1.0347587962962963,1.0173012894375857,1.0047885562414267,1],
|
||||
};
|
||||
|
||||
Object.keys(Samples).forEach(function(type) {
|
||||
it('should ease ' + type, function() {
|
||||
var [modeName, easingName, isFunction] = type.split('_');
|
||||
var easing = Easing[easingName];
|
||||
if (isFunction !== undefined) {
|
||||
easing = easing();
|
||||
}
|
||||
var computed = sampleEasingFunction(Easing[modeName](easing));
|
||||
var samples = Samples[type];
|
||||
|
||||
computed.forEach((value, key) => {
|
||||
expect(value).toBeCloseTo(samples[key], 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest
|
||||
.dontMock('Interpolation')
|
||||
.dontMock('Easing');
|
||||
|
||||
var Interpolation = require('Interpolation');
|
||||
var Easing = require('Easing');
|
||||
|
||||
describe('Interpolation', () => {
|
||||
it('should work with defaults', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
});
|
||||
|
||||
expect(interpolation(0)).toBe(0);
|
||||
expect(interpolation(0.5)).toBe(0.5);
|
||||
expect(interpolation(0.8)).toBe(0.8);
|
||||
expect(interpolation(1)).toBe(1);
|
||||
});
|
||||
|
||||
it('should work with output range', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [100, 200],
|
||||
});
|
||||
|
||||
expect(interpolation(0)).toBe(100);
|
||||
expect(interpolation(0.5)).toBe(150);
|
||||
expect(interpolation(0.8)).toBe(180);
|
||||
expect(interpolation(1)).toBe(200);
|
||||
});
|
||||
|
||||
it('should work with input range', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [100, 200],
|
||||
outputRange: [0, 1],
|
||||
});
|
||||
|
||||
expect(interpolation(100)).toBe(0);
|
||||
expect(interpolation(150)).toBe(0.5);
|
||||
expect(interpolation(180)).toBe(0.8);
|
||||
expect(interpolation(200)).toBe(1);
|
||||
});
|
||||
|
||||
it('should throw for non monotonic input ranges', () => {
|
||||
expect(() => Interpolation.create({
|
||||
inputRange: [0, 2, 1],
|
||||
outputRange: [0, 1, 2],
|
||||
})).toThrow();
|
||||
|
||||
expect(() => Interpolation.create({
|
||||
inputRange: [0, 1, 2],
|
||||
outputRange: [0, 3, 1],
|
||||
})).not.toThrow();
|
||||
});
|
||||
|
||||
it('should work with empty input range', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 10, 10],
|
||||
outputRange: [1, 2, 3],
|
||||
extrapolate: 'extend',
|
||||
});
|
||||
|
||||
expect(interpolation(0)).toBe(1);
|
||||
expect(interpolation(5)).toBe(1.5);
|
||||
expect(interpolation(10)).toBe(2);
|
||||
expect(interpolation(10.1)).toBe(3);
|
||||
expect(interpolation(15)).toBe(3);
|
||||
});
|
||||
|
||||
it('should work with empty output range', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [1, 2, 3],
|
||||
outputRange: [0, 10, 10],
|
||||
extrapolate: 'extend',
|
||||
});
|
||||
|
||||
expect(interpolation(0)).toBe(-10);
|
||||
expect(interpolation(1.5)).toBe(5);
|
||||
expect(interpolation(2)).toBe(10);
|
||||
expect(interpolation(2.5)).toBe(10);
|
||||
expect(interpolation(3)).toBe(10);
|
||||
expect(interpolation(4)).toBe(10);
|
||||
});
|
||||
|
||||
it('should work with easing', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
easing: Easing.quad,
|
||||
});
|
||||
|
||||
expect(interpolation(0)).toBe(0);
|
||||
expect(interpolation(0.5)).toBe(0.25);
|
||||
expect(interpolation(0.9)).toBe(0.81);
|
||||
expect(interpolation(1)).toBe(1);
|
||||
});
|
||||
|
||||
it('should work with extrapolate', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
extrapolate: 'extend',
|
||||
easing: Easing.quad,
|
||||
});
|
||||
|
||||
expect(interpolation(-2)).toBe(4);
|
||||
expect(interpolation(2)).toBe(4);
|
||||
|
||||
interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
extrapolate: 'clamp',
|
||||
easing: Easing.quad,
|
||||
});
|
||||
|
||||
expect(interpolation(-2)).toBe(0);
|
||||
expect(interpolation(2)).toBe(1);
|
||||
|
||||
interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
extrapolate: 'identity',
|
||||
easing: Easing.quad,
|
||||
});
|
||||
|
||||
expect(interpolation(-2)).toBe(-2);
|
||||
expect(interpolation(2)).toBe(2);
|
||||
});
|
||||
|
||||
it('should work with keyframes with extrapolate', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 10, 100, 1000],
|
||||
outputRange: [0, 5, 50, 500],
|
||||
extrapolate: true,
|
||||
});
|
||||
|
||||
expect(interpolation(-5)).toBe(-2.5);
|
||||
expect(interpolation(0)).toBe(0);
|
||||
expect(interpolation(5)).toBe(2.5);
|
||||
expect(interpolation(10)).toBe(5);
|
||||
expect(interpolation(50)).toBe(25);
|
||||
expect(interpolation(100)).toBe(50);
|
||||
expect(interpolation(500)).toBe(250);
|
||||
expect(interpolation(1000)).toBe(500);
|
||||
expect(interpolation(2000)).toBe(1000);
|
||||
});
|
||||
|
||||
it('should work with keyframes without extrapolate', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1, 2],
|
||||
outputRange: [0.2, 1, 0.2],
|
||||
extrapolate: 'clamp',
|
||||
});
|
||||
|
||||
expect(interpolation(5)).toBeCloseTo(0.2);
|
||||
});
|
||||
|
||||
it('should throw for an infinite input range', () => {
|
||||
expect(() => Interpolation.create({
|
||||
inputRange: [-Infinity, Infinity],
|
||||
outputRange: [0, 1],
|
||||
})).toThrow();
|
||||
|
||||
expect(() => Interpolation.create({
|
||||
inputRange: [-Infinity, 0, Infinity],
|
||||
outputRange: [1, 2, 3],
|
||||
})).not.toThrow();
|
||||
});
|
||||
|
||||
it('should work with negative infinite', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [-Infinity, 0],
|
||||
outputRange: [-Infinity, 0],
|
||||
easing: Easing.quad,
|
||||
extrapolate: 'identity',
|
||||
});
|
||||
|
||||
expect(interpolation(-Infinity)).toBe(-Infinity);
|
||||
expect(interpolation(-100)).toBeCloseTo(-10000);
|
||||
expect(interpolation(-10)).toBeCloseTo(-100);
|
||||
expect(interpolation(0)).toBeCloseTo(0);
|
||||
expect(interpolation(1)).toBeCloseTo(1);
|
||||
expect(interpolation(100)).toBeCloseTo(100);
|
||||
});
|
||||
|
||||
it('should work with positive infinite', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [5, Infinity],
|
||||
outputRange: [5, Infinity],
|
||||
easing: Easing.quad,
|
||||
extrapolate: 'identity',
|
||||
});
|
||||
|
||||
expect(interpolation(-100)).toBeCloseTo(-100);
|
||||
expect(interpolation(-10)).toBeCloseTo(-10);
|
||||
expect(interpolation(0)).toBeCloseTo(0);
|
||||
expect(interpolation(5)).toBeCloseTo(5);
|
||||
expect(interpolation(6)).toBeCloseTo(5 + 1);
|
||||
expect(interpolation(10)).toBeCloseTo(5 + 25);
|
||||
expect(interpolation(100)).toBeCloseTo(5 + (95 * 95));
|
||||
expect(interpolation(Infinity)).toBe(Infinity);
|
||||
});
|
||||
|
||||
it('should work with output ranges as string', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)'],
|
||||
});
|
||||
|
||||
expect(interpolation(0)).toBe('rgba(0, 100, 200, 0)');
|
||||
expect(interpolation(0.5)).toBe('rgba(25, 125, 225, 0.25)');
|
||||
expect(interpolation(1)).toBe('rgba(50, 150, 250, 0.5)');
|
||||
});
|
||||
|
||||
it('should work with negative and decimal values in string ranges', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['-100.5deg', '100deg'],
|
||||
});
|
||||
|
||||
expect(interpolation(0)).toBe('-100.5deg');
|
||||
expect(interpolation(0.5)).toBe('-0.25deg');
|
||||
expect(interpolation(1)).toBe('100deg');
|
||||
});
|
||||
|
||||
it('should crash when chaining an interpolation that returns a string', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
});
|
||||
expect(() => { interpolation('45rad'); }).toThrow();
|
||||
});
|
||||
|
||||
it('should crash when defining output range with different pattern', () => {
|
||||
expect(() => Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['rgba(0, 100, 200, 0)', 'rgb(50, 150, 250)'],
|
||||
})).toThrow();
|
||||
|
||||
expect(() => Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['20deg', '30rad'],
|
||||
})).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "react-animated",
|
||||
"description": "Animated provides powerful mechanisms for animating your React views",
|
||||
"version": "0.1.0",
|
||||
"keywords": [
|
||||
"react",
|
||||
"animated",
|
||||
"animation"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"main": "Animated.js",
|
||||
"readmeFilename": "README.md"
|
||||
}
|
||||
@@ -7,11 +7,18 @@
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule AnimationExperimental
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var RCTAnimationManager = require('NativeModules').AnimationExperimentalManager;
|
||||
if (!RCTAnimationManager) {
|
||||
// AnimationExperimental isn't available internally - this is a temporary
|
||||
// workaround to enable its availability to be determined at runtime.
|
||||
// For Flow let's pretend like we always export AnimationExperimental
|
||||
// so all our users don't need to do null checks
|
||||
module.exports = null;
|
||||
} else {
|
||||
|
||||
var React = require('React');
|
||||
var AnimationUtils = require('AnimationUtils');
|
||||
|
||||
@@ -88,3 +95,5 @@ if (__DEV__) {
|
||||
}
|
||||
|
||||
module.exports = AnimationExperimental;
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ var TypesEnum = {
|
||||
easeInEaseOut: true,
|
||||
easeIn: true,
|
||||
easeOut: true,
|
||||
keyboard: true,
|
||||
};
|
||||
var Types = keyMirror(TypesEnum);
|
||||
|
||||
@@ -86,31 +87,42 @@ function create(duration: number, type, creationProp): Config {
|
||||
};
|
||||
}
|
||||
|
||||
var Presets = {
|
||||
easeInEaseOut: create(
|
||||
300, Types.easeInEaseOut, Properties.opacity
|
||||
),
|
||||
linear: create(
|
||||
500, Types.linear, Properties.opacity
|
||||
),
|
||||
spring: {
|
||||
duration: 700,
|
||||
create: {
|
||||
type: Types.linear,
|
||||
property: Properties.opacity,
|
||||
},
|
||||
update: {
|
||||
type: Types.spring,
|
||||
springDamping: 0.4,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var LayoutAnimation = {
|
||||
configureNext,
|
||||
create,
|
||||
Types,
|
||||
Properties,
|
||||
configChecker: configChecker,
|
||||
Presets: {
|
||||
easeInEaseOut: create(
|
||||
300, Types.easeInEaseOut, Properties.opacity
|
||||
),
|
||||
linear: create(
|
||||
500, Types.linear, Properties.opacity
|
||||
),
|
||||
spring: {
|
||||
duration: 700,
|
||||
create: {
|
||||
type: Types.linear,
|
||||
property: Properties.opacity,
|
||||
},
|
||||
update: {
|
||||
type: Types.spring,
|
||||
springDamping: 0.4,
|
||||
},
|
||||
},
|
||||
}
|
||||
Presets,
|
||||
easeInEaseOut: configureNext.bind(
|
||||
null, Presets.easeInEaseOut
|
||||
),
|
||||
linear: configureNext.bind(
|
||||
null, Presets.linear
|
||||
),
|
||||
spring: configureNext.bind(
|
||||
null, Presets.spring
|
||||
),
|
||||
};
|
||||
|
||||
module.exports = LayoutAnimation;
|
||||
|
||||
@@ -36,6 +36,7 @@ var POPAnimationMixin = {
|
||||
AnimationProperties: POPAnimation.Properties,
|
||||
|
||||
getInitialState: function(): Object {
|
||||
this._popAnimationEnqueuedAnimationTimeouts = [];
|
||||
return {
|
||||
_currentAnimationsByNodeHandle: {},
|
||||
};
|
||||
@@ -120,7 +121,11 @@ var POPAnimationMixin = {
|
||||
}
|
||||
doneCallback && doneCallback(finished);
|
||||
};
|
||||
POPAnimation.addAnimation(nodeHandle, animID, cleanupWrapper);
|
||||
// Hack to aviod race condition in POP:
|
||||
var animationTimeoutHandler = setTimeout(() => {
|
||||
POPAnimation.addAnimation(nodeHandle, animID, cleanupWrapper);
|
||||
}, 1);
|
||||
this._popAnimationEnqueuedAnimationTimeouts.push(animationTimeoutHandler);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -251,6 +256,10 @@ var POPAnimationMixin = {
|
||||
// Cleanup any potentially leaked animations.
|
||||
componentWillUnmount: function() {
|
||||
this.stopAllAnimations();
|
||||
this._popAnimationEnqueuedAnimationTimeouts.forEach(animationTimeoutHandler => {
|
||||
clearTimeout(animationTimeoutHandler);
|
||||
});
|
||||
this._popAnimationEnqueuedAnimationTimeouts = [];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* https://github.com/arian/cubic-bezier
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2013 Arian Stolwijk
|
||||
*
|
||||
* 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:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @providesModule bezier
|
||||
* @nolint
|
||||
*/
|
||||
|
||||
module.exports = function(x1, y1, x2, y2, epsilon){
|
||||
|
||||
var curveX = function(t){
|
||||
var v = 1 - t;
|
||||
return 3 * v * v * t * x1 + 3 * v * t * t * x2 + t * t * t;
|
||||
};
|
||||
|
||||
var curveY = function(t){
|
||||
var v = 1 - t;
|
||||
return 3 * v * v * t * y1 + 3 * v * t * t * y2 + t * t * t;
|
||||
};
|
||||
|
||||
var derivativeCurveX = function(t){
|
||||
var v = 1 - t;
|
||||
return 3 * (2 * (t - 1) * t + v * v) * x1 + 3 * (- t * t * t + 2 * v * t) * x2;
|
||||
};
|
||||
|
||||
return function(t){
|
||||
|
||||
var x = t, t0, t1, t2, x2, d2, i;
|
||||
|
||||
// First try a few iterations of Newton's method -- normally very fast.
|
||||
for (t2 = x, i = 0; i < 8; i++){
|
||||
x2 = curveX(t2) - x;
|
||||
if (Math.abs(x2) < epsilon) return curveY(t2);
|
||||
d2 = derivativeCurveX(t2);
|
||||
if (Math.abs(d2) < 1e-6) break;
|
||||
t2 = t2 - x2 / d2;
|
||||
}
|
||||
|
||||
t0 = 0, t1 = 1, t2 = x;
|
||||
|
||||
if (t2 < t0) return curveY(t0);
|
||||
if (t2 > t1) return curveY(t1);
|
||||
|
||||
// Fallback to the bisection method for reliability.
|
||||
while (t0 < t1){
|
||||
x2 = curveX(t2);
|
||||
if (Math.abs(x2 - x) < epsilon) return curveY(t2);
|
||||
if (x > x2) t0 = t2;
|
||||
else t1 = t2;
|
||||
t2 = (t1 - t0) * .5 + t0;
|
||||
}
|
||||
|
||||
// Failure
|
||||
return curveY(t2);
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
@@ -35,6 +35,34 @@ type MapRegion = {
|
||||
var MapView = React.createClass({
|
||||
mixins: [NativeMethodsMixin],
|
||||
|
||||
checkAnnotationIds: function (annotations: Array<Object>) {
|
||||
|
||||
var newAnnotations = annotations.map(function (annotation) {
|
||||
if (!annotation.id) {
|
||||
// TODO: add a base64 (or similar) encoder here
|
||||
annotation.id = encodeURIComponent(JSON.stringify(annotation));
|
||||
}
|
||||
|
||||
return annotation;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
annotations: newAnnotations
|
||||
});
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
if (this.props.annotations) {
|
||||
this.checkAnnotationIds(this.props.annotations);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps: Object) {
|
||||
if (nextProps.annotations) {
|
||||
this.checkAnnotationIds(nextProps.annotations);
|
||||
}
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
/**
|
||||
* Used to style and layout the `MapView`. See `StyleSheet.js` and
|
||||
@@ -84,14 +112,14 @@ var MapView = React.createClass({
|
||||
|
||||
/**
|
||||
* The map type to be displayed.
|
||||
*
|
||||
*
|
||||
* - standard: standard road map (default)
|
||||
* - satellite: satellite view
|
||||
* - hybrid: satellite view with roads and points of interest overlayed
|
||||
*/
|
||||
mapType: React.PropTypes.oneOf([
|
||||
'standard',
|
||||
'satellite',
|
||||
'standard',
|
||||
'satellite',
|
||||
'hybrid',
|
||||
]),
|
||||
|
||||
@@ -126,11 +154,34 @@ var MapView = React.createClass({
|
||||
latitude: React.PropTypes.number.isRequired,
|
||||
longitude: React.PropTypes.number.isRequired,
|
||||
|
||||
/**
|
||||
* Whether the pin drop should be animated or not
|
||||
*/
|
||||
animateDrop: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Annotation title/subtile.
|
||||
*/
|
||||
title: React.PropTypes.string,
|
||||
subtitle: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* Whether the Annotation has callout buttons.
|
||||
*/
|
||||
hasLeftCallout: React.PropTypes.bool,
|
||||
hasRightCallout: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Event handlers for callout buttons.
|
||||
*/
|
||||
onLeftCalloutPress: React.PropTypes.func,
|
||||
onRightCalloutPress: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* annotation id
|
||||
*/
|
||||
id: React.PropTypes.string
|
||||
|
||||
})),
|
||||
|
||||
/**
|
||||
@@ -158,6 +209,11 @@ var MapView = React.createClass({
|
||||
* Callback that is called once, when the user is done moving the map.
|
||||
*/
|
||||
onRegionChangeComplete: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Callback that is called once, when the user is clicked on a annotation.
|
||||
*/
|
||||
onAnnotationPress: React.PropTypes.func,
|
||||
},
|
||||
|
||||
_onChange: function(event: Event) {
|
||||
@@ -170,8 +226,34 @@ var MapView = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
_onPress: function(event: Event) {
|
||||
if (event.nativeEvent.action === 'annotation-click') {
|
||||
this.props.onAnnotationPress && this.props.onAnnotationPress(event.nativeEvent.annotation);
|
||||
}
|
||||
|
||||
if (event.nativeEvent.action === 'callout-click') {
|
||||
if (!this.props.annotations) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the annotation with the id of what has been pressed
|
||||
for (var i = 0; i < this.props.annotations.length; i++) {
|
||||
var annotation = this.props.annotations[i];
|
||||
if (annotation.id === event.nativeEvent.annotationId) {
|
||||
// Pass the right function
|
||||
if (event.nativeEvent.side === 'left') {
|
||||
annotation.onLeftCalloutPress && annotation.onLeftCalloutPress(event.nativeEvent);
|
||||
} else if (event.nativeEvent.side === 'right') {
|
||||
annotation.onRightCalloutPress && annotation.onRightCalloutPress(event.nativeEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <RCTMap {...this.props} onChange={this._onChange} />;
|
||||
return <RCTMap {...this.props} onPress={this._onPress} onChange={this._onChange} />;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -179,6 +261,7 @@ if (Platform.OS === 'android') {
|
||||
var RCTMap = createReactNativeComponentClass({
|
||||
validAttributes: merge(
|
||||
ReactNativeViewAttributes.UIView, {
|
||||
active: true,
|
||||
showsUserLocation: true,
|
||||
zoomEnabled: true,
|
||||
rotateEnabled: true,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
var EventEmitter = require('EventEmitter');
|
||||
var Image = require('Image');
|
||||
var NavigationContext = require('NavigationContext');
|
||||
var React = require('React');
|
||||
var ReactNativeViewAttributes = require('ReactNativeViewAttributes');
|
||||
var RCTNavigatorManager = require('NativeModules').NavigatorManager;
|
||||
@@ -57,6 +58,7 @@ var RCTNavigatorItem = createReactNativeComponentClass({
|
||||
backButtonIcon: true,
|
||||
backButtonTitle: true,
|
||||
tintColor: true,
|
||||
translucent: true,
|
||||
navigationBarHidden: true,
|
||||
titleTextColor: true,
|
||||
style: true,
|
||||
@@ -300,9 +302,15 @@ var NavigatorIOS = React.createClass({
|
||||
*/
|
||||
titleTextColor: PropTypes.string,
|
||||
|
||||
/**
|
||||
* A Boolean value that indicates whether the navigation bar is translucent
|
||||
*/
|
||||
translucent: PropTypes.bool,
|
||||
|
||||
},
|
||||
|
||||
navigator: (undefined: ?Object),
|
||||
navigationContext: new NavigationContext(),
|
||||
|
||||
componentWillMount: function() {
|
||||
// Precompute a pack of callbacks that's frequently generated and passed to
|
||||
@@ -317,7 +325,18 @@ var NavigatorIOS = React.createClass({
|
||||
resetTo: this.resetTo,
|
||||
popToRoute: this.popToRoute,
|
||||
popToTop: this.popToTop,
|
||||
navigationContext: this.navigationContext,
|
||||
};
|
||||
this._emitWillFocus(this.state.routeStack[this.state.observedTopOfStack]);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._emitDidFocus(this.state.routeStack[this.state.observedTopOfStack]);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.navigationContext.dispose();
|
||||
this.navigationContext = new NavigationContext();
|
||||
},
|
||||
|
||||
getInitialState: function(): State {
|
||||
@@ -391,6 +410,8 @@ var NavigatorIOS = React.createClass({
|
||||
|
||||
_handleNavigatorStackChanged: function(e: Event) {
|
||||
var newObservedTopOfStack = e.nativeEvent.stackLength - 1;
|
||||
this._emitDidFocus(this.state.routeStack[newObservedTopOfStack]);
|
||||
|
||||
invariant(
|
||||
newObservedTopOfStack <= this.state.requestedTopOfStack,
|
||||
'No navigator item should be pushed without JS knowing about it %s %s', newObservedTopOfStack, this.state.requestedTopOfStack
|
||||
@@ -442,11 +463,21 @@ var NavigatorIOS = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
_emitDidFocus: function(route: Route) {
|
||||
this.navigationContext.emit('didfocus', {route: route});
|
||||
},
|
||||
|
||||
_emitWillFocus: function(route: Route) {
|
||||
this.navigationContext.emit('willfocus', {route: route});
|
||||
},
|
||||
|
||||
push: function(route: Route) {
|
||||
invariant(!!route, 'Must supply route to push');
|
||||
// Make sure all previous requests are caught up first. Otherwise reject.
|
||||
if (this.state.requestedTopOfStack === this.state.observedTopOfStack) {
|
||||
this._tryLockNavigator(() => {
|
||||
this._emitWillFocus(route);
|
||||
|
||||
var nextStack = this.state.routeStack.concat([route]);
|
||||
var nextIDStack = this.state.idStack.concat([getuid()]);
|
||||
this.setState({
|
||||
@@ -470,12 +501,11 @@ var NavigatorIOS = React.createClass({
|
||||
if (this.state.requestedTopOfStack === this.state.observedTopOfStack) {
|
||||
if (this.state.requestedTopOfStack > 0) {
|
||||
this._tryLockNavigator(() => {
|
||||
invariant(
|
||||
this.state.requestedTopOfStack - n >= 0,
|
||||
'Cannot pop below 0'
|
||||
);
|
||||
var newRequestedTopOfStack = this.state.requestedTopOfStack - n;
|
||||
invariant(newRequestedTopOfStack >= 0, 'Cannot pop below 0');
|
||||
this._emitWillFocus(this.state.routeStack[newRequestedTopOfStack]);
|
||||
this.setState({
|
||||
requestedTopOfStack: this.state.requestedTopOfStack - n,
|
||||
requestedTopOfStack: newRequestedTopOfStack,
|
||||
makingNavigatorRequest: true,
|
||||
// Not actually updating the indices yet until we get the native
|
||||
// `onNavigationComplete`.
|
||||
@@ -519,6 +549,9 @@ var NavigatorIOS = React.createClass({
|
||||
makingNavigatorRequest: false,
|
||||
updatingAllIndicesAtOrBeyond: index,
|
||||
});
|
||||
|
||||
this._emitWillFocus(route);
|
||||
this._emitDidFocus(route);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -609,6 +642,7 @@ var NavigatorIOS = React.createClass({
|
||||
navigationBarHidden={this.props.navigationBarHidden}
|
||||
tintColor={this.props.tintColor}
|
||||
barTintColor={this.props.barTintColor}
|
||||
translucent={this.props.translucent !== false}
|
||||
titleTextColor={this.props.titleTextColor}>
|
||||
<Component
|
||||
navigator={this.navigator}
|
||||
@@ -652,7 +686,7 @@ var NavigatorIOS = React.createClass({
|
||||
{this.renderNavigationStackItems()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
|
||||
@@ -411,7 +411,7 @@ var TextInput = React.createClass({
|
||||
_renderIOS: function() {
|
||||
var textContainer;
|
||||
|
||||
var props = this.props;
|
||||
var props = Object.assign({},this.props);
|
||||
props.style = [styles.input, this.props.style];
|
||||
|
||||
if (!props.multiline) {
|
||||
|
||||
@@ -20,6 +20,12 @@ var Touchable = require('Touchable');
|
||||
var merge = require('merge');
|
||||
var onlyChild = require('onlyChild');
|
||||
|
||||
var invariant = require('invariant');
|
||||
invariant(
|
||||
AnimationExperimental || POPAnimation,
|
||||
'Please add the RCTAnimationExperimental framework to your project, or add //Libraries/FBReactKit:RCTPOPAnimation to your BUCK file if running internally within Facebook.'
|
||||
);
|
||||
|
||||
type State = {
|
||||
animationID: ?number;
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ var ViewStylePropTypes = {
|
||||
borderTopRightRadius: ReactPropTypes.number,
|
||||
borderBottomLeftRadius: ReactPropTypes.number,
|
||||
borderBottomRightRadius: ReactPropTypes.number,
|
||||
borderStyle: ReactPropTypes.oneOf(['solid', 'dotted', 'dashed']),
|
||||
opacity: ReactPropTypes.number,
|
||||
overflow: ReactPropTypes.oneOf(['visible', 'hidden']),
|
||||
shadowColor: ReactPropTypes.string,
|
||||
|
||||
@@ -43,6 +43,12 @@ var WebView = React.createClass({
|
||||
startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load
|
||||
style: View.propTypes.style,
|
||||
javaScriptEnabledAndroid: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Sets the JS to be injected when the webpage loads.
|
||||
*/
|
||||
injectedJavaScript: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Sets the user-agent for this WebView. The user-agent can also be set in native through
|
||||
* WebViewConfig, but this can and will overwrite that config.
|
||||
@@ -96,6 +102,7 @@ var WebView = React.createClass({
|
||||
key="webViewKey"
|
||||
style={webViewStyles}
|
||||
url={this.props.url}
|
||||
injectedJavaScript={this.props.injectedJavaScript}
|
||||
userAgent={this.props.userAgent}
|
||||
javaScriptEnabledAndroid={this.props.javaScriptEnabledAndroid}
|
||||
contentInset={this.props.contentInset}
|
||||
@@ -176,8 +183,9 @@ var WebView = React.createClass({
|
||||
|
||||
var RCTWebView = createReactNativeComponentClass({
|
||||
validAttributes: merge(ReactNativeViewAttributes.UIView, {
|
||||
url: true,
|
||||
injectedJavaScript: true,
|
||||
javaScriptEnabledAndroid: true,
|
||||
url: true,
|
||||
userAgent: true,
|
||||
}),
|
||||
uiViewClassName: 'RCTWebView',
|
||||
|
||||
@@ -43,6 +43,8 @@ var NavigationType = {
|
||||
other: RCTWebViewManager.NavigationType.Other,
|
||||
};
|
||||
|
||||
var JSNavigationScheme = RCTWebViewManager.JSNavigationScheme;
|
||||
|
||||
type ErrorEvent = {
|
||||
domain: any;
|
||||
code: any;
|
||||
@@ -75,6 +77,7 @@ var defaultRenderError = (errorDomain, errorCode, errorDesc) => (
|
||||
|
||||
var WebView = React.createClass({
|
||||
statics: {
|
||||
JSNavigationScheme: JSNavigationScheme,
|
||||
NavigationType: NavigationType,
|
||||
},
|
||||
|
||||
@@ -86,7 +89,6 @@ var WebView = React.createClass({
|
||||
bounces: PropTypes.bool,
|
||||
scrollEnabled: PropTypes.bool,
|
||||
automaticallyAdjustContentInsets: PropTypes.bool,
|
||||
shouldInjectAJAXHandler: PropTypes.bool,
|
||||
contentInset: EdgeInsetsPropType,
|
||||
onNavigationStateChange: PropTypes.func,
|
||||
startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load
|
||||
@@ -95,6 +97,11 @@ var WebView = React.createClass({
|
||||
* Used for android only, JS is enabled by default for WebView on iOS
|
||||
*/
|
||||
javaScriptEnabledAndroid: PropTypes.bool,
|
||||
/**
|
||||
* Sets the JS to be injected when the webpage loads.
|
||||
*/
|
||||
injectedJavaScript: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Used for iOS only, sets whether the webpage scales to fit the view and the
|
||||
* user can change the scale
|
||||
@@ -152,9 +159,9 @@ var WebView = React.createClass({
|
||||
style={webViewStyles}
|
||||
url={this.props.url}
|
||||
html={this.props.html}
|
||||
injectedJavaScript={this.props.injectedJavaScript}
|
||||
bounces={this.props.bounces}
|
||||
scrollEnabled={this.props.scrollEnabled}
|
||||
shouldInjectAJAXHandler={this.props.shouldInjectAJAXHandler}
|
||||
contentInset={this.props.contentInset}
|
||||
automaticallyAdjustContentInsets={this.props.automaticallyAdjustContentInsets}
|
||||
onLoadingStart={this.onLoadingStart}
|
||||
@@ -172,15 +179,15 @@ var WebView = React.createClass({
|
||||
},
|
||||
|
||||
goForward: function() {
|
||||
RCTWebViewManager.goForward(this.getWebWiewHandle());
|
||||
RCTWebViewManager.goForward(this.getWebViewHandle());
|
||||
},
|
||||
|
||||
goBack: function() {
|
||||
RCTWebViewManager.goBack(this.getWebWiewHandle());
|
||||
RCTWebViewManager.goBack(this.getWebViewHandle());
|
||||
},
|
||||
|
||||
reload: function() {
|
||||
RCTWebViewManager.reload(this.getWebWiewHandle());
|
||||
RCTWebViewManager.reload(this.getWebViewHandle());
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -193,7 +200,7 @@ var WebView = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
getWebWiewHandle: function(): any {
|
||||
getWebViewHandle: function(): any {
|
||||
return React.findNodeHandle(this.refs[RCT_WEBVIEW_REF]);
|
||||
},
|
||||
|
||||
|
||||
@@ -29,14 +29,15 @@
|
||||
var ListViewDataSource = require('ListViewDataSource');
|
||||
var React = require('React');
|
||||
var RCTUIManager = require('NativeModules').UIManager;
|
||||
var RKScrollViewManager = require('NativeModules').ScrollViewManager;
|
||||
var ScrollView = require('ScrollView');
|
||||
var ScrollResponder = require('ScrollResponder');
|
||||
var StaticRenderer = require('StaticRenderer');
|
||||
var TimerMixin = require('react-timer-mixin');
|
||||
|
||||
var isEmpty = require('isEmpty');
|
||||
var logError = require('logError');
|
||||
var merge = require('merge');
|
||||
var isEmpty = require('isEmpty');
|
||||
|
||||
var PropTypes = React.PropTypes;
|
||||
|
||||
@@ -173,6 +174,13 @@ var ListView = React.createClass({
|
||||
* header.
|
||||
*/
|
||||
renderSectionHeader: PropTypes.func,
|
||||
/**
|
||||
* (props) => renderable
|
||||
*
|
||||
* A function that returns the scrollable component in which the list rows
|
||||
* are rendered. Defaults to returning a ScrollView with the given props.
|
||||
*/
|
||||
renderScrollComponent: React.PropTypes.func.isRequired,
|
||||
/**
|
||||
* How early to start rendering rows before they come on screen, in
|
||||
* pixels.
|
||||
@@ -213,7 +221,9 @@ var ListView = React.createClass({
|
||||
* such as scrollTo.
|
||||
*/
|
||||
getScrollResponder: function() {
|
||||
return this.refs[SCROLLVIEW_REF];
|
||||
return this.refs[SCROLLVIEW_REF] &&
|
||||
this.refs[SCROLLVIEW_REF].getScrollResponder &&
|
||||
this.refs[SCROLLVIEW_REF].getScrollResponder();
|
||||
},
|
||||
|
||||
setNativeProps: function(props) {
|
||||
@@ -228,6 +238,7 @@ var ListView = React.createClass({
|
||||
return {
|
||||
initialListSize: DEFAULT_INITIAL_ROWS,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
renderScrollComponent: props => <ScrollView {...props} />,
|
||||
scrollRenderAheadDistance: DEFAULT_SCROLL_RENDER_AHEAD,
|
||||
onEndReachedThreshold: DEFAULT_END_REACHED_THRESHOLD,
|
||||
};
|
||||
@@ -276,7 +287,9 @@ var ListView = React.createClass({
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
this._measureAndUpdateScrollProps();
|
||||
this.requestAnimationFrame(() => {
|
||||
this._measureAndUpdateScrollProps();
|
||||
});
|
||||
},
|
||||
|
||||
onRowHighlighted: function(sectionID, rowID) {
|
||||
@@ -363,23 +376,24 @@ var ListView = React.createClass({
|
||||
}
|
||||
}
|
||||
|
||||
var props = merge(
|
||||
this.props, {
|
||||
onScroll: this._onScroll,
|
||||
stickyHeaderIndices: sectionHeaderIndices,
|
||||
}
|
||||
);
|
||||
var {
|
||||
renderScrollComponent,
|
||||
...props,
|
||||
} = this.props;
|
||||
if (!props.scrollEventThrottle) {
|
||||
props.scrollEventThrottle = DEFAULT_SCROLL_CALLBACK_THROTTLE;
|
||||
}
|
||||
return (
|
||||
<ScrollView {...props}
|
||||
ref={SCROLLVIEW_REF}>
|
||||
{header}
|
||||
{bodyComponents}
|
||||
{footer}
|
||||
</ScrollView>
|
||||
);
|
||||
Object.assign(props, {
|
||||
onScroll: this._onScroll,
|
||||
stickyHeaderIndices: sectionHeaderIndices,
|
||||
children: [header, bodyComponents, footer],
|
||||
});
|
||||
|
||||
// TODO(ide): Use function refs so we can compose with the scroll
|
||||
// component's original ref instead of clobbering it
|
||||
return React.cloneElement(renderScrollComponent(props), {
|
||||
ref: SCROLLVIEW_REF,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -387,17 +401,28 @@ var ListView = React.createClass({
|
||||
*/
|
||||
|
||||
_measureAndUpdateScrollProps: function() {
|
||||
var scrollComponent = this.getScrollResponder();
|
||||
if (!scrollComponent || !scrollComponent.getInnerViewNode) {
|
||||
return;
|
||||
}
|
||||
RCTUIManager.measureLayout(
|
||||
this.refs[SCROLLVIEW_REF].getInnerViewNode(),
|
||||
React.findNodeHandle(this.refs[SCROLLVIEW_REF]),
|
||||
scrollComponent.getInnerViewNode(),
|
||||
React.findNodeHandle(scrollComponent),
|
||||
logError,
|
||||
this._setScrollContentHeight
|
||||
);
|
||||
RCTUIManager.measureLayoutRelativeToParent(
|
||||
React.findNodeHandle(this.refs[SCROLLVIEW_REF]),
|
||||
React.findNodeHandle(scrollComponent),
|
||||
logError,
|
||||
this._setScrollVisibleHeight
|
||||
);
|
||||
|
||||
// RKScrollViewManager.calculateChildFrames not available on every platform
|
||||
RKScrollViewManager && RKScrollViewManager.calculateChildFrames &&
|
||||
RKScrollViewManager.calculateChildFrames(
|
||||
React.findNodeHandle(scrollComponent),
|
||||
this._updateChildFrames,
|
||||
);
|
||||
},
|
||||
|
||||
_setScrollContentHeight: function(left, top, width, height) {
|
||||
@@ -410,6 +435,10 @@ var ListView = React.createClass({
|
||||
this._renderMoreRowsIfNeeded();
|
||||
},
|
||||
|
||||
_updateChildFrames: function(childFrames) {
|
||||
this._updateVisibleRows(childFrames);
|
||||
},
|
||||
|
||||
_renderMoreRowsIfNeeded: function() {
|
||||
if (this.scrollProperties.contentHeight === null ||
|
||||
this.scrollProperties.visibleHeight === null ||
|
||||
@@ -447,11 +476,10 @@ var ListView = React.createClass({
|
||||
scrollProperties.offsetY;
|
||||
},
|
||||
|
||||
_updateVisibleRows: function(e) {
|
||||
_updateVisibleRows: function(updatedFrames) {
|
||||
if (!this.props.onChangeVisibleRows) {
|
||||
return; // No need to compute visible rows if there is no callback
|
||||
}
|
||||
var updatedFrames = e && e.nativeEvent.updatedChildFrames;
|
||||
if (updatedFrames) {
|
||||
updatedFrames.forEach((newFrame) => {
|
||||
this._childFrames[newFrame.index] = merge(newFrame);
|
||||
@@ -520,7 +548,7 @@ var ListView = React.createClass({
|
||||
this.scrollProperties.visibleHeight = e.nativeEvent.layoutMeasurement.height;
|
||||
this.scrollProperties.contentHeight = e.nativeEvent.contentSize.height;
|
||||
this.scrollProperties.offsetY = e.nativeEvent.contentOffset.y;
|
||||
this._updateVisibleRows(e);
|
||||
this._updateVisibleRows(e.nativeEvent.updatedChildFrames);
|
||||
var nearEnd = this._getDistanceFromEnd(this.scrollProperties) < this.props.onEndReachedThreshold;
|
||||
if (nearEnd &&
|
||||
this.props.onEndReached &&
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Copyright 2004-present Facebook. All Rights Reserved.
|
||||
*
|
||||
* @providesModule NavigationRouteStack
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var immutable = require('immutable');
|
||||
var invariant = require('invariant');
|
||||
|
||||
var {List} = immutable;
|
||||
|
||||
/**
|
||||
* The immutable routes stack.
|
||||
*/
|
||||
class RouteStack {
|
||||
_index: number;
|
||||
|
||||
_routes: List;
|
||||
|
||||
constructor(index: number, routes: List) {
|
||||
invariant(
|
||||
routes.size > 0,
|
||||
'size must not be empty'
|
||||
);
|
||||
|
||||
invariant(
|
||||
index > -1 && index <= routes.size - 1,
|
||||
'index out of bound'
|
||||
);
|
||||
|
||||
this._routes = routes;
|
||||
this._index = index;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this._routes.size;
|
||||
}
|
||||
|
||||
get index(): number {
|
||||
return this._index;
|
||||
}
|
||||
|
||||
toArray(): Array {
|
||||
return this._routes.toJS();
|
||||
}
|
||||
|
||||
get(index: number): any {
|
||||
if (index < 0 || index > this._routes.size - 1) {
|
||||
return null;
|
||||
}
|
||||
return this._routes.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new stack with the provided route appended,
|
||||
* starting at this stack size.
|
||||
*/
|
||||
push(route: any): RouteStack {
|
||||
invariant(
|
||||
route === 0 ||
|
||||
route === false ||
|
||||
!!route,
|
||||
'Must supply route to push'
|
||||
);
|
||||
|
||||
invariant(this._routes.indexOf(route) === -1, 'route must be unique');
|
||||
|
||||
// When pushing, removes the rest of the routes past the current index.
|
||||
var routes = this._routes.withMutations((list: List) => {
|
||||
list.slice(0, this._index + 1).push(route);
|
||||
});
|
||||
|
||||
return new RouteStack(routes.size - 1, routes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new stack a size ones less than this stack,
|
||||
* excluding the last index in this stack.
|
||||
*/
|
||||
pop(): RouteStack {
|
||||
invariant(this._routes.size > 1, 'shoud not pop routes stack to empty');
|
||||
|
||||
// When popping, removes the rest of the routes past the current index.
|
||||
var routes = this._routes.slice(0, this._index);
|
||||
return new RouteStack(routes.size - 1, routes);
|
||||
}
|
||||
|
||||
jumpToIndex(index: number): RouteStack {
|
||||
invariant(
|
||||
index > -1 && index < this._routes.size,
|
||||
'index out of bound'
|
||||
);
|
||||
|
||||
if (index === this._index) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return new RouteStack(index, this._routes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a route in the navigation stack.
|
||||
*
|
||||
* `index` specifies the route in the stack that should be replaced.
|
||||
* If it's negative, it counts from the back.
|
||||
*/
|
||||
replaceAtIndex(index: number, route: any): RouteStack {
|
||||
invariant(
|
||||
route === 0 ||
|
||||
route === false ||
|
||||
!!route,
|
||||
'Must supply route to replace'
|
||||
);
|
||||
|
||||
if (this.get(index) === route) {
|
||||
return this;
|
||||
}
|
||||
|
||||
invariant(this._routes.indexOf(route) === -1, 'route must be unique');
|
||||
|
||||
if (index < 0) {
|
||||
index += this._routes.size;
|
||||
}
|
||||
|
||||
invariant(
|
||||
index > -1 && index < this._routes.size,
|
||||
'index out of bound'
|
||||
);
|
||||
|
||||
var routes = this._routes.set(index, route);
|
||||
return new RouteStack(this._index, routes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The first class data structure for NavigationContext to manage the navigation
|
||||
* stack of routes.
|
||||
*/
|
||||
class NavigationRouteStack extends RouteStack {
|
||||
constructor(index: number, routes: Array) {
|
||||
// For now, `RouteStack` internally, uses an immutable `List` to keep
|
||||
// track of routes. Since using `List` is really just the implementation
|
||||
// detail, we don't want to accept `routes` as `list` from constructor
|
||||
// for developer.
|
||||
super(index, new List(routes));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NavigationRouteStack;
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Copyright (c) 2015, Facebook, Inc. All rights reserved.
|
||||
*
|
||||
* Facebook, Inc. (“Facebook”) owns all right, title and interest, including
|
||||
* all intellectual property and other proprietary rights, in and to the React
|
||||
* Native CustomComponents software (the “Software”). Subject to your
|
||||
* compliance with these terms, you are hereby granted a non-exclusive,
|
||||
* worldwide, royalty-free copyright license to (1) use and copy the Software;
|
||||
* and (2) reproduce and distribute the Software as part of your own software
|
||||
* (“Your Software”). Facebook reserves all rights not expressly granted to
|
||||
* you in this license agreement.
|
||||
*
|
||||
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
|
||||
* OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
|
||||
* EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
|
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest
|
||||
.dontMock('NavigationRouteStack')
|
||||
.dontMock('clamp')
|
||||
.dontMock('invariant')
|
||||
.dontMock('immutable');
|
||||
|
||||
var NavigationRouteStack = require('NavigationRouteStack');
|
||||
|
||||
describe('NavigationRouteStack:', () => {
|
||||
// Basic
|
||||
it('gets index', () => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b', 'c']);
|
||||
expect(stack.index).toEqual(1);
|
||||
});
|
||||
|
||||
it('gets size', () => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b', 'c']);
|
||||
expect(stack.size).toEqual(3);
|
||||
});
|
||||
|
||||
it('gets route', () => {
|
||||
var stack = new NavigationRouteStack(0, ['a', 'b', 'c']);
|
||||
expect(stack.get(2)).toEqual('c');
|
||||
});
|
||||
|
||||
it('converts to an array', () => {
|
||||
var stack = new NavigationRouteStack(0, ['a', 'b']);
|
||||
expect(stack.toArray()).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('creates a new stack after mutation', () => {
|
||||
var stack1 = new NavigationRouteStack(0, ['a', 'b']);
|
||||
var stack2 = stack1.push('c');
|
||||
expect(stack1).not.toEqual(stack2);
|
||||
});
|
||||
|
||||
it('throws at index out of bound', () => {
|
||||
expect(() => {
|
||||
new NavigationRouteStack(-1, ['a', 'b']);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
new NavigationRouteStack(100, ['a', 'b']);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
// Push
|
||||
it('pushes route', () => {
|
||||
var stack1 = new NavigationRouteStack(1, ['a', 'b']);
|
||||
var stack2 = stack1.push('c');
|
||||
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2.toArray()).toEqual(['a', 'b', 'c']);
|
||||
expect(stack2.index).toEqual(2);
|
||||
expect(stack2.size).toEqual(3);
|
||||
});
|
||||
|
||||
it('throws when pushing empty route', () => {
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
stack.push(null);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
stack.push('');
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
stack.push(undefined);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('replaces routes on push', () => {
|
||||
var stack1 = new NavigationRouteStack(1, ['a', 'b', 'c']);
|
||||
var stack2 = stack1.push('d');
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2.toArray()).toEqual(['a', 'b', 'd']);
|
||||
expect(stack2.index).toEqual(2);
|
||||
});
|
||||
|
||||
// Pop
|
||||
it('pops route', () => {
|
||||
var stack1 = new NavigationRouteStack(2, ['a', 'b', 'c']);
|
||||
var stack2 = stack1.pop();
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2.toArray()).toEqual(['a', 'b']);
|
||||
expect(stack2.index).toEqual(1);
|
||||
expect(stack2.size).toEqual(2);
|
||||
});
|
||||
|
||||
it('replaces routes on pop', () => {
|
||||
var stack1 = new NavigationRouteStack(1, ['a', 'b', 'c']);
|
||||
var stack2 = stack1.pop();
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2.toArray()).toEqual(['a']);
|
||||
expect(stack2.index).toEqual(0);
|
||||
});
|
||||
|
||||
it('throws when popping to empty stack', () => {
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(0, ['a']);
|
||||
stack.pop();
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
// Jump
|
||||
it('jumps to index', () => {
|
||||
var stack1 = new NavigationRouteStack(0, ['a', 'b', 'c']);
|
||||
var stack2 = stack1.jumpToIndex(2);
|
||||
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2.index).toEqual(2);
|
||||
});
|
||||
|
||||
it('throws then jumping to index out of bound', () => {
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
stack.jumpToIndex(2);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
stack.jumpToIndex(-1);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
// Replace
|
||||
it('replaces route at index', () => {
|
||||
var stack1 = new NavigationRouteStack(1, ['a', 'b']);
|
||||
var stack2 = stack1.replaceAtIndex(0, 'x');
|
||||
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2.toArray()).toEqual(['x', 'b']);
|
||||
expect(stack2.index).toEqual(1);
|
||||
});
|
||||
|
||||
it('replaces route at negative index', () => {
|
||||
var stack1 = new NavigationRouteStack(1, ['a', 'b']);
|
||||
var stack2 = stack1.replaceAtIndex(-1, 'x');
|
||||
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2.toArray()).toEqual(['a', 'x']);
|
||||
expect(stack2.index).toEqual(1);
|
||||
});
|
||||
|
||||
|
||||
it('throws when replacing empty route', () => {
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
stack.replaceAtIndex(1, null);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('throws when replacing at index out of bound', () => {
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
stack.replaceAtIndex(100, 'x');
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -30,6 +30,7 @@
|
||||
var AnimationsDebugModule = require('NativeModules').AnimationsDebugModule;
|
||||
var Dimensions = require('Dimensions');
|
||||
var InteractionMixin = require('InteractionMixin');
|
||||
var Map = require('Map');
|
||||
var NavigationContext = require('NavigationContext');
|
||||
var NavigatorBreadcrumbNavigationBar = require('NavigatorBreadcrumbNavigationBar');
|
||||
var NavigatorNavigationBar = require('NavigatorNavigationBar');
|
||||
@@ -257,6 +258,8 @@ var Navigator = React.createClass({
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
this._renderedSceneMap = new Map();
|
||||
|
||||
var routeStack = this.props.initialRouteStack || [this.props.initialRoute];
|
||||
invariant(
|
||||
routeStack.length >= 1,
|
||||
@@ -276,10 +279,6 @@ var Navigator = React.createClass({
|
||||
),
|
||||
idStack: routeStack.map(() => getuid()),
|
||||
routeStack,
|
||||
// `updatingRange*` allows us to only render the visible or staged scenes
|
||||
// On first render, we will render every scene in the initialRouteStack
|
||||
updatingRangeStart: 0,
|
||||
updatingRangeLength: routeStack.length,
|
||||
presentedIndex: initialRouteIndex,
|
||||
transitionFromIndex: null,
|
||||
activeGesture: null,
|
||||
@@ -351,8 +350,6 @@ var Navigator = React.createClass({
|
||||
sceneConfigStack: nextRouteStack.map(
|
||||
this.props.configureScene
|
||||
),
|
||||
updatingRangeStart: 0,
|
||||
updatingRangeLength: nextRouteStack.length,
|
||||
presentedIndex: destIndex,
|
||||
activeGesture: null,
|
||||
transitionFromIndex: null,
|
||||
@@ -395,7 +392,6 @@ var Navigator = React.createClass({
|
||||
this.spring.getSpringConfig().tension = sceneConfig.springTension;
|
||||
this.spring.setVelocity(velocity || sceneConfig.defaultTransitionVelocity);
|
||||
this.spring.setEndValue(1);
|
||||
this._emitWillFocus(this.state.routeStack[this.state.presentedIndex]);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -461,6 +457,7 @@ var Navigator = React.createClass({
|
||||
if (this.state.transitionQueue.length) {
|
||||
var queuedTransition = this.state.transitionQueue.shift();
|
||||
this._enableScene(queuedTransition.destIndex);
|
||||
this._emitWillFocus(this.state.routeStack[queuedTransition.destIndex]);
|
||||
this._transitionTo(
|
||||
queuedTransition.destIndex,
|
||||
queuedTransition.velocity,
|
||||
@@ -660,6 +657,7 @@ var Navigator = React.createClass({
|
||||
}
|
||||
} else {
|
||||
// The gesture has enough velocity to complete, so we transition to the gesture's destination
|
||||
this._emitWillFocus(this.state.routeStack[destIndex]);
|
||||
this._transitionTo(
|
||||
destIndex,
|
||||
transitionVelocity,
|
||||
@@ -829,11 +827,6 @@ var Navigator = React.createClass({
|
||||
return false;
|
||||
},
|
||||
|
||||
_resetUpdatingRange: function() {
|
||||
this.state.updatingRangeStart = 0;
|
||||
this.state.updatingRangeLength = this.state.routeStack.length;
|
||||
},
|
||||
|
||||
_getDestIndexWithinBounds: function(n) {
|
||||
var currentIndex = this.state.presentedIndex;
|
||||
var destIndex = currentIndex + n;
|
||||
@@ -851,15 +844,9 @@ var Navigator = React.createClass({
|
||||
|
||||
_jumpN: function(n) {
|
||||
var destIndex = this._getDestIndexWithinBounds(n);
|
||||
var requestTransitionAndResetUpdatingRange = () => {
|
||||
this._enableScene(destIndex);
|
||||
this._transitionTo(destIndex);
|
||||
this._resetUpdatingRange();
|
||||
};
|
||||
this.setState({
|
||||
updatingRangeStart: destIndex,
|
||||
updatingRangeLength: 1,
|
||||
}, requestTransitionAndResetUpdatingRange);
|
||||
this._enableScene(destIndex);
|
||||
this._emitWillFocus(this.state.routeStack[destIndex]);
|
||||
this._transitionTo(destIndex);
|
||||
},
|
||||
|
||||
jumpTo: function(route) {
|
||||
@@ -891,18 +878,15 @@ var Navigator = React.createClass({
|
||||
var nextAnimationConfigStack = activeAnimationConfigStack.concat([
|
||||
this.props.configureScene(route),
|
||||
]);
|
||||
var requestTransitionAndResetUpdatingRange = () => {
|
||||
this._enableScene(destIndex);
|
||||
this._transitionTo(destIndex);
|
||||
this._resetUpdatingRange();
|
||||
};
|
||||
this._emitWillFocus(nextStack[destIndex]);
|
||||
this.setState({
|
||||
idStack: nextIDStack,
|
||||
routeStack: nextStack,
|
||||
sceneConfigStack: nextAnimationConfigStack,
|
||||
updatingRangeStart: nextStack.length - 1,
|
||||
updatingRangeLength: 1,
|
||||
}, requestTransitionAndResetUpdatingRange);
|
||||
}, () => {
|
||||
this._enableScene(destIndex);
|
||||
this._transitionTo(destIndex);
|
||||
});
|
||||
},
|
||||
|
||||
_popN: function(n) {
|
||||
@@ -915,6 +899,7 @@ var Navigator = React.createClass({
|
||||
);
|
||||
var popIndex = this.state.presentedIndex - n;
|
||||
this._enableScene(popIndex);
|
||||
this._emitWillFocus(this.state.routeStack[popIndex]);
|
||||
this._transitionTo(
|
||||
popIndex,
|
||||
null, // default velocity
|
||||
@@ -954,16 +939,15 @@ var Navigator = React.createClass({
|
||||
nextRouteStack[index] = route;
|
||||
nextAnimationModeStack[index] = this.props.configureScene(route);
|
||||
|
||||
if (index === this.state.presentedIndex) {
|
||||
this._emitWillFocus(route);
|
||||
}
|
||||
this.setState({
|
||||
idStack: nextIDStack,
|
||||
routeStack: nextRouteStack,
|
||||
sceneConfigStack: nextAnimationModeStack,
|
||||
updatingRangeStart: index,
|
||||
updatingRangeLength: 1,
|
||||
}, () => {
|
||||
this._resetUpdatingRange();
|
||||
if (index === this.state.presentedIndex) {
|
||||
this._emitWillFocus(route);
|
||||
this._emitDidFocus(route);
|
||||
}
|
||||
cb && cb();
|
||||
@@ -1018,7 +1002,8 @@ var Navigator = React.createClass({
|
||||
},
|
||||
|
||||
getCurrentRoutes: function() {
|
||||
return this.state.routeStack;
|
||||
// Clone before returning to avoid caller mutating the stack
|
||||
return this.state.routeStack.slice();
|
||||
},
|
||||
|
||||
_handleItemRef: function(itemId, route, ref) {
|
||||
@@ -1034,69 +1019,17 @@ var Navigator = React.createClass({
|
||||
var newStackLength = index + 1;
|
||||
// Remove any unneeded rendered routes.
|
||||
if (newStackLength < this.state.routeStack.length) {
|
||||
var updatingRangeStart = newStackLength; // One past the top
|
||||
var updatingRangeLength = this.state.routeStack.length - newStackLength + 1;
|
||||
this.state.idStack.slice(newStackLength).map((removingId) => {
|
||||
this._itemRefs[removingId] = null;
|
||||
});
|
||||
this.setState({
|
||||
updatingRangeStart: updatingRangeStart,
|
||||
updatingRangeLength: updatingRangeLength,
|
||||
sceneConfigStack: this.state.sceneConfigStack.slice(0, newStackLength),
|
||||
idStack: this.state.idStack.slice(0, newStackLength),
|
||||
routeStack: this.state.routeStack.slice(0, newStackLength),
|
||||
}, this._resetUpdatingRange);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_renderOptimizedScenes: function() {
|
||||
// To avoid rendering scenes that are not visible, we use
|
||||
// updatingRangeStart and updatingRangeLength to track the scenes that need
|
||||
// to be updated.
|
||||
|
||||
// To avoid visual glitches, we never re-render scenes during a transition.
|
||||
// We assume that `state.updatingRangeLength` will have a length during the
|
||||
// initial render of any scene
|
||||
var shouldRenderScenes = this.state.updatingRangeLength !== 0;
|
||||
if (shouldRenderScenes) {
|
||||
return (
|
||||
<StaticContainer shouldUpdate={true}>
|
||||
<View
|
||||
style={styles.transitioner}
|
||||
{...this.panGesture.panHandlers}
|
||||
onTouchStart={this._handleTouchStart}
|
||||
onResponderTerminationRequest={
|
||||
this._handleResponderTerminationRequest
|
||||
}>
|
||||
{this.state.routeStack.map(this._renderOptimizedScene)}
|
||||
</View>
|
||||
</StaticContainer>
|
||||
);
|
||||
}
|
||||
// If no scenes are changing, we can save render time. React will notice
|
||||
// that we are rendering a StaticContainer in the same place, so the
|
||||
// existing element will be updated. When React asks the element
|
||||
// shouldComponentUpdate, the StaticContainer will return false, and the
|
||||
// children from the previous reconciliation will remain.
|
||||
return (
|
||||
<StaticContainer shouldUpdate={false} />
|
||||
);
|
||||
},
|
||||
|
||||
_renderOptimizedScene: function(route, i) {
|
||||
var shouldRenderScene =
|
||||
i >= this.state.updatingRangeStart &&
|
||||
i <= this.state.updatingRangeStart + this.state.updatingRangeLength;
|
||||
var scene = shouldRenderScene ? this._renderScene(route, i) : null;
|
||||
return (
|
||||
<StaticContainer
|
||||
key={'nav' + i}
|
||||
shouldUpdate={shouldRenderScene}>
|
||||
{scene}
|
||||
</StaticContainer>
|
||||
);
|
||||
},
|
||||
|
||||
_renderScene: function(route, i) {
|
||||
var child = this.props.renderScene(
|
||||
route,
|
||||
@@ -1146,9 +1079,30 @@ var Navigator = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var newRenderedSceneMap = new Map();
|
||||
var scenes = this.state.routeStack.map((route, index) => {
|
||||
var renderedScene;
|
||||
if (this._renderedSceneMap.has(route) &&
|
||||
index !== this.state.presentedIndex) {
|
||||
renderedScene = this._renderedSceneMap.get(route);
|
||||
} else {
|
||||
renderedScene = this._renderScene(route, index);
|
||||
}
|
||||
newRenderedSceneMap.set(route, renderedScene);
|
||||
return renderedScene;
|
||||
});
|
||||
this._renderedSceneMap = newRenderedSceneMap;
|
||||
return (
|
||||
<View style={[styles.container, this.props.style]}>
|
||||
{this._renderOptimizedScenes()}
|
||||
<View
|
||||
style={styles.transitioner}
|
||||
{...this.panGesture.panHandlers}
|
||||
onTouchStart={this._handleTouchStart}
|
||||
onResponderTerminationRequest={
|
||||
this._handleResponderTerminationRequest
|
||||
}>
|
||||
{scenes}
|
||||
</View>
|
||||
{this._renderNavigationBar()}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -104,7 +104,33 @@ var Image = React.createClass({
|
||||
*
|
||||
* {nativeEvent: { layout: {x, y, width, height}}}.
|
||||
*/
|
||||
onLayout: PropTypes.func,
|
||||
onLayout: PropTypes.func,
|
||||
/**
|
||||
* Invoked on load start
|
||||
*/
|
||||
onLoadStart: PropTypes.func,
|
||||
/**
|
||||
* Invoked on download progress with
|
||||
*
|
||||
* {nativeEvent: { written, total}}.
|
||||
*/
|
||||
onLoadProgress: PropTypes.func,
|
||||
/**
|
||||
* Invoked on load abort
|
||||
*/
|
||||
onLoadAbort: PropTypes.func,
|
||||
/**
|
||||
* Invoked on load error
|
||||
*
|
||||
* {nativeEvent: { error}}.
|
||||
*/
|
||||
onLoadError: PropTypes.func,
|
||||
/**
|
||||
* Invoked on load end
|
||||
*
|
||||
*/
|
||||
onLoaded: PropTypes.func
|
||||
|
||||
},
|
||||
|
||||
statics: {
|
||||
@@ -161,6 +187,7 @@ var Image = React.createClass({
|
||||
if (this.props.defaultSource) {
|
||||
nativeProps.defaultImageSrc = this.props.defaultSource.uri;
|
||||
}
|
||||
nativeProps.progressHandlerRegistered = isNetwork && this.props.onLoadProgress;
|
||||
return <RawImage {...nativeProps} />;
|
||||
}
|
||||
});
|
||||
@@ -178,6 +205,7 @@ var nativeOnlyProps = {
|
||||
src: true,
|
||||
defaultImageSrc: true,
|
||||
imageTag: true,
|
||||
progressHandlerRegistered: true
|
||||
};
|
||||
if (__DEV__) {
|
||||
verifyPropTypes(Image, RCTStaticImage.viewConfig, nativeOnlyProps);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#import "RCTImageLoader.h"
|
||||
#import "RCTLog.h"
|
||||
#import "RCTUtils.h"
|
||||
|
||||
@implementation RCTCameraRollManager
|
||||
|
||||
@@ -23,21 +24,20 @@ RCT_EXPORT_MODULE()
|
||||
|
||||
RCT_EXPORT_METHOD(saveImageWithTag:(NSString *)imageTag
|
||||
successCallback:(RCTResponseSenderBlock)successCallback
|
||||
errorCallback:(RCTResponseSenderBlock)errorCallback)
|
||||
errorCallback:(RCTResponseErrorBlock)errorCallback)
|
||||
{
|
||||
[RCTImageLoader loadImageWithTag:imageTag callback:^(NSError *loadError, UIImage *loadedImage) {
|
||||
if (loadError) {
|
||||
errorCallback(@[[loadError localizedDescription]]);
|
||||
errorCallback(loadError);
|
||||
return;
|
||||
}
|
||||
[[RCTImageLoader assetsLibrary] writeImageToSavedPhotosAlbum:[loadedImage CGImage] metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) {
|
||||
if (saveError) {
|
||||
NSString *errorMessage = [NSString stringWithFormat:@"Error saving cropped image: %@", saveError];
|
||||
RCTLogWarn(@"%@", errorMessage);
|
||||
errorCallback(@[errorMessage]);
|
||||
return;
|
||||
RCTLogWarn(@"Error saving cropped image: %@", saveError);
|
||||
errorCallback(saveError);
|
||||
} else {
|
||||
successCallback(@[[assetURL absoluteString]]);
|
||||
}
|
||||
successCallback(@[[assetURL absoluteString]]);
|
||||
}];
|
||||
}];
|
||||
}
|
||||
@@ -63,7 +63,7 @@ RCT_EXPORT_METHOD(saveImageWithTag:(NSString *)imageTag
|
||||
|
||||
RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
||||
callback:(RCTResponseSenderBlock)callback
|
||||
errorCallback:(RCTResponseSenderBlock)errorCallback)
|
||||
errorCallback:(RCTResponseErrorBlock)errorCallback)
|
||||
{
|
||||
NSUInteger first = [params[@"first"] integerValue];
|
||||
NSString *afterCursor = params[@"after"];
|
||||
@@ -160,7 +160,7 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
||||
if (error.code != ALAssetsLibraryAccessUserDeniedError) {
|
||||
RCTLogError(@"Failure while iterating through asset groups %@", error);
|
||||
}
|
||||
errorCallback(@[error.description]);
|
||||
errorCallback(error);
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef void (^RCTDataCompletionBlock)(NSURLResponse *response, NSData *data, NSError *error);
|
||||
typedef void (^RCTDataProgressBlock)(int64_t written, int64_t total);
|
||||
|
||||
@interface RCTDownloadTaskWrapper : NSObject <NSURLSessionDownloadDelegate>
|
||||
|
||||
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration delegateQueue:(NSOperationQueue *)delegateQueue;
|
||||
|
||||
- (NSURLSessionDownloadTask *)downloadData:(NSURL *)url progressBlock:(RCTDataProgressBlock)progressBlock completionBlock:(RCTDataCompletionBlock)completionBlock;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
|
||||
#import "RCTDownloadTaskWrapper.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
static void *const RCTDownloadTaskWrapperCompletionBlockKey = (void *)&RCTDownloadTaskWrapperCompletionBlockKey;
|
||||
static void *const RCTDownloadTaskWrapperProgressBlockKey = (void *)&RCTDownloadTaskWrapperProgressBlockKey;
|
||||
|
||||
@interface NSURLSessionTask (RCTDownloadTaskWrapper)
|
||||
|
||||
@property (nonatomic, copy, setter=rct_setCompletionBlock:) RCTDataCompletionBlock rct_completionBlock;
|
||||
@property (nonatomic, copy, setter=rct_setProgressBlock:) RCTDataProgressBlock rct_progressBlock;
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSURLSessionTask (RCTDownloadTaskWrapper)
|
||||
|
||||
- (RCTDataCompletionBlock)rct_completionBlock
|
||||
{
|
||||
return objc_getAssociatedObject(self, RCTDownloadTaskWrapperCompletionBlockKey);
|
||||
}
|
||||
|
||||
- (void)rct_setCompletionBlock:(RCTDataCompletionBlock)completionBlock
|
||||
{
|
||||
objc_setAssociatedObject(self, RCTDownloadTaskWrapperCompletionBlockKey, completionBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
|
||||
}
|
||||
|
||||
- (RCTDataProgressBlock)rct_progressBlock
|
||||
{
|
||||
return objc_getAssociatedObject(self, RCTDownloadTaskWrapperProgressBlockKey);
|
||||
}
|
||||
|
||||
- (void)rct_setProgressBlock:(RCTDataProgressBlock)progressBlock
|
||||
{
|
||||
objc_setAssociatedObject(self, RCTDownloadTaskWrapperProgressBlockKey, progressBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTDownloadTaskWrapper
|
||||
{
|
||||
NSURLSession *_URLSession;
|
||||
}
|
||||
|
||||
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration delegateQueue:(NSOperationQueue *)delegateQueue
|
||||
{
|
||||
if ((self = [super init])) {
|
||||
_URLSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSURLSessionDownloadTask *)downloadData:(NSURL *)url progressBlock:(RCTDataProgressBlock)progressBlock completionBlock:(RCTDataCompletionBlock)completionBlock
|
||||
{
|
||||
NSURLSessionDownloadTask *task = [_URLSession downloadTaskWithURL:url completionHandler:nil];
|
||||
task.rct_completionBlock = completionBlock;
|
||||
task.rct_progressBlock = progressBlock;
|
||||
|
||||
[task resume];
|
||||
return task;
|
||||
}
|
||||
|
||||
#pragma mark - NSURLSessionTaskDelegate methods
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
|
||||
{
|
||||
if (downloadTask.rct_completionBlock) {
|
||||
NSData *data = [NSData dataWithContentsOfURL:location];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
downloadTask.rct_completionBlock(downloadTask.response, data, nil);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)didWriteData totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;
|
||||
{
|
||||
if (downloadTask.rct_progressBlock) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
downloadTask.rct_progressBlock(totalBytesWritten, totalBytesExpectedToWrite);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
|
||||
{
|
||||
if (error && task.rct_completionBlock) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
task.rct_completionBlock(nil, nil, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -7,6 +7,7 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
03559E7F1B064DAF00730281 /* RCTDownloadTaskWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 03559E7E1B064DAF00730281 /* RCTDownloadTaskWrapper.m */; };
|
||||
1304D5AB1AA8C4A30002E2BE /* RCTStaticImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */; };
|
||||
1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */; };
|
||||
1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */; };
|
||||
@@ -32,6 +33,8 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
03559E7D1B064D3A00730281 /* RCTDownloadTaskWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTDownloadTaskWrapper.h; sourceTree = "<group>"; };
|
||||
03559E7E1B064DAF00730281 /* RCTDownloadTaskWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDownloadTaskWrapper.m; sourceTree = "<group>"; };
|
||||
1304D5A71AA8C4A30002E2BE /* RCTStaticImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStaticImage.h; sourceTree = "<group>"; };
|
||||
1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStaticImage.m; sourceTree = "<group>"; };
|
||||
1304D5A91AA8C4A30002E2BE /* RCTStaticImageManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStaticImageManager.h; sourceTree = "<group>"; };
|
||||
@@ -69,14 +72,16 @@
|
||||
58B511541A9E6B3D00147676 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
143879361AAD32A300F088A5 /* RCTImageLoader.h */,
|
||||
143879371AAD32A300F088A5 /* RCTImageLoader.m */,
|
||||
143879331AAD238D00F088A5 /* RCTCameraRollManager.h */,
|
||||
143879341AAD238D00F088A5 /* RCTCameraRollManager.m */,
|
||||
03559E7D1B064D3A00730281 /* RCTDownloadTaskWrapper.h */,
|
||||
03559E7E1B064DAF00730281 /* RCTDownloadTaskWrapper.m */,
|
||||
1304D5B01AA8C50D0002E2BE /* RCTGIFImage.h */,
|
||||
1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */,
|
||||
58B511891A9E6BD600147676 /* RCTImageDownloader.h */,
|
||||
58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */,
|
||||
143879361AAD32A300F088A5 /* RCTImageLoader.h */,
|
||||
143879371AAD32A300F088A5 /* RCTImageLoader.m */,
|
||||
137620331B31C53500677FF0 /* RCTImagePickerManager.h */,
|
||||
137620341B31C53500677FF0 /* RCTImagePickerManager.m */,
|
||||
1345A8371B26592900583190 /* RCTImageRequestHandler.h */,
|
||||
@@ -168,6 +173,7 @@
|
||||
1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */,
|
||||
143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */,
|
||||
143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */,
|
||||
03559E7F1B064DAF00730281 /* RCTDownloadTaskWrapper.m in Sources */,
|
||||
1304D5AB1AA8C4A30002E2BE /* RCTStaticImage.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
||||
@@ -9,20 +9,24 @@
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "RCTDownloadTaskWrapper.h"
|
||||
|
||||
typedef void (^RCTDataDownloadBlock)(NSData *data, NSError *error);
|
||||
typedef void (^RCTImageDownloadBlock)(UIImage *image, NSError *error);
|
||||
typedef void (^RCTImageDownloadCancellationBlock)(void);
|
||||
|
||||
@interface RCTImageDownloader : NSObject
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
+ (RCTImageDownloader *)sharedInstance;
|
||||
|
||||
/**
|
||||
* Downloads a block of raw data and returns it. Note that the callback block
|
||||
* will not be executed on the same thread you called the method from, nor on
|
||||
* the main thread. Returns a token that can be used to cancel the download.
|
||||
*/
|
||||
- (id)downloadDataForURL:(NSURL *)url
|
||||
block:(RCTDataDownloadBlock)block;
|
||||
- (RCTImageDownloadCancellationBlock)downloadDataForURL:(NSURL *)url
|
||||
progressBlock:(RCTDataProgressBlock)progressBlock
|
||||
block:(RCTDataDownloadBlock)block;
|
||||
|
||||
/**
|
||||
* Downloads an image and decompresses it a the size specified. The compressed
|
||||
@@ -30,18 +34,19 @@ typedef void (^RCTImageDownloadBlock)(UIImage *image, NSError *error);
|
||||
* will not be executed on the same thread you called the method from, nor on
|
||||
* the main thread. Returns a token that can be used to cancel the download.
|
||||
*/
|
||||
- (id)downloadImageForURL:(NSURL *)url
|
||||
size:(CGSize)size
|
||||
scale:(CGFloat)scale
|
||||
resizeMode:(UIViewContentMode)resizeMode
|
||||
backgroundColor:(UIColor *)backgroundColor
|
||||
block:(RCTImageDownloadBlock)block;
|
||||
- (RCTImageDownloadCancellationBlock)downloadImageForURL:(NSURL *)url
|
||||
size:(CGSize)size
|
||||
scale:(CGFloat)scale
|
||||
resizeMode:(UIViewContentMode)resizeMode
|
||||
backgroundColor:(UIColor *)backgroundColor
|
||||
progressBlock:(RCTDataProgressBlock)progressBlock
|
||||
block:(RCTImageDownloadBlock)block;
|
||||
|
||||
/**
|
||||
* Cancel an in-flight download. If multiple requets have been made for the
|
||||
* same image, only the request that relates to the token passed will be
|
||||
* cancelled.
|
||||
*/
|
||||
- (void)cancelDownload:(id)downloadToken;
|
||||
- (void)cancelDownload:(RCTImageDownloadCancellationBlock)downloadToken;
|
||||
|
||||
@end
|
||||
|
||||
@@ -9,25 +9,29 @@
|
||||
|
||||
#import "RCTImageDownloader.h"
|
||||
|
||||
#import "RCTCache.h"
|
||||
#import "RCTDownloadTaskWrapper.h"
|
||||
#import "RCTLog.h"
|
||||
#import "RCTUtils.h"
|
||||
|
||||
typedef void (^RCTCachedDataDownloadBlock)(BOOL cached, NSData *data, NSError *error);
|
||||
|
||||
CGSize RCTTargetSizeForClipRect(CGRect);
|
||||
CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
|
||||
|
||||
@implementation RCTImageDownloader
|
||||
{
|
||||
RCTCache *_cache;
|
||||
NSURLCache *_cache;
|
||||
dispatch_queue_t _processingQueue;
|
||||
NSMutableDictionary *_pendingBlocks;
|
||||
RCTDownloadTaskWrapper *_downloadTaskWrapper;
|
||||
}
|
||||
|
||||
+ (instancetype)sharedInstance
|
||||
+ (RCTImageDownloader *)sharedInstance
|
||||
{
|
||||
static RCTImageDownloader *sharedInstance;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedInstance = [[self alloc] init];
|
||||
sharedInstance = [[RCTImageDownloader alloc] init];
|
||||
});
|
||||
return sharedInstance;
|
||||
}
|
||||
@@ -35,27 +39,25 @@ typedef void (^RCTCachedDataDownloadBlock)(BOOL cached, NSData *data, NSError *e
|
||||
- (instancetype)init
|
||||
{
|
||||
if ((self = [super init])) {
|
||||
_cache = [[RCTCache alloc] initWithName:@"RCTImageDownloader"];
|
||||
_cache = [[NSURLCache alloc] initWithMemoryCapacity:5 * 1024 * 1024 diskCapacity:200 * 1024 * 1024 diskPath:@"React/RCTImageDownloader"];
|
||||
_processingQueue = dispatch_queue_create("com.facebook.React.DownloadProcessingQueue", DISPATCH_QUEUE_SERIAL);
|
||||
_pendingBlocks = [[NSMutableDictionary alloc] init];
|
||||
|
||||
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
|
||||
_downloadTaskWrapper = [[RCTDownloadTaskWrapper alloc] initWithSessionConfiguration:config delegateQueue:nil];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
static NSString *RCTCacheKeyForURL(NSURL *url)
|
||||
- (RCTImageDownloadCancellationBlock)_downloadDataForURL:(NSURL *)url progressBlock:progressBlock block:(RCTCachedDataDownloadBlock)block
|
||||
{
|
||||
return url.absoluteString;
|
||||
}
|
||||
|
||||
- (id)_downloadDataForURL:(NSURL *)url block:(RCTCachedDataDownloadBlock)block
|
||||
{
|
||||
NSString *cacheKey = RCTCacheKeyForURL(url);
|
||||
NSString *const cacheKey = url.absoluteString;
|
||||
|
||||
__block BOOL cancelled = NO;
|
||||
__block NSURLSessionDataTask *task = nil;
|
||||
|
||||
dispatch_block_t cancel = ^{
|
||||
__block NSURLSessionDownloadTask *task = nil;
|
||||
|
||||
RCTImageDownloadCancellationBlock cancel = ^{
|
||||
cancelled = YES;
|
||||
|
||||
dispatch_async(_processingQueue, ^{
|
||||
@@ -88,45 +90,112 @@ static NSString *RCTCacheKeyForURL(NSURL *url)
|
||||
});
|
||||
};
|
||||
|
||||
if ([_cache hasDataForKey:cacheKey]) {
|
||||
[_cache fetchDataForKey:cacheKey completionHandler:^(NSData *data) {
|
||||
if (!cancelled) {
|
||||
runBlocks(YES, data, nil);
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||
if (!cancelled) {
|
||||
runBlocks(NO, data, error);
|
||||
}
|
||||
}];
|
||||
NSURLRequest *request = [NSURLRequest requestWithURL:url];
|
||||
task = [_downloadTaskWrapper downloadData:url progressBlock:progressBlock completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) {
|
||||
if (!cancelled) {
|
||||
runBlocks(NO, data, error);
|
||||
}
|
||||
|
||||
if (response) {
|
||||
RCTImageDownloader *strongSelf = weakSelf;
|
||||
NSCachedURLResponse *cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:data userInfo:nil storagePolicy:NSURLCacheStorageAllowed];
|
||||
[strongSelf->_cache storeCachedResponse:cachedResponse forRequest:request];
|
||||
}
|
||||
task = nil;
|
||||
}];
|
||||
|
||||
NSCachedURLResponse *cachedResponse = [_cache cachedResponseForRequest:request];
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cachedResponse) {
|
||||
runBlocks(YES, cachedResponse.data, nil);
|
||||
} else {
|
||||
[task resume];
|
||||
}
|
||||
|
||||
[task resume];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return [cancel copy];
|
||||
}
|
||||
|
||||
- (id)downloadDataForURL:(NSURL *)url block:(RCTDataDownloadBlock)block
|
||||
- (RCTImageDownloadCancellationBlock)downloadDataForURL:(NSURL *)url progressBlock:(RCTDataProgressBlock)progressBlock block:(RCTDataDownloadBlock)block
|
||||
{
|
||||
NSString *cacheKey = RCTCacheKeyForURL(url);
|
||||
__weak RCTImageDownloader *weakSelf = self;
|
||||
return [self _downloadDataForURL:url block:^(BOOL cached, NSData *data, NSError *error) {
|
||||
if (!cached) {
|
||||
RCTImageDownloader *strongSelf = weakSelf;
|
||||
[strongSelf->_cache setData:data forKey:cacheKey];
|
||||
}
|
||||
return [self _downloadDataForURL:url progressBlock:progressBlock block:^(BOOL cached, NSData *data, NSError *error) {
|
||||
block(data, error);
|
||||
}];
|
||||
}
|
||||
|
||||
- (RCTImageDownloadCancellationBlock)downloadImageForURL:(NSURL *)url
|
||||
size:(CGSize)size
|
||||
scale:(CGFloat)scale
|
||||
resizeMode:(UIViewContentMode)resizeMode
|
||||
backgroundColor:(UIColor *)backgroundColor
|
||||
progressBlock:(RCTDataProgressBlock)progressBlock
|
||||
block:(RCTImageDownloadBlock)block
|
||||
{
|
||||
return [self downloadDataForURL:url progressBlock:progressBlock block:^(NSData *data, NSError *error) {
|
||||
if (!data || error) {
|
||||
block(nil, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (CGSizeEqualToSize(size, CGSizeZero)) {
|
||||
// Target size wasn't available yet, so abort image drawing
|
||||
block(nil, nil);
|
||||
return;
|
||||
}
|
||||
|
||||
UIImage *image = [UIImage imageWithData:data scale:scale];
|
||||
if (image) {
|
||||
|
||||
// Get scale and size
|
||||
CGFloat destScale = scale ?: RCTScreenScale();
|
||||
CGRect imageRect = RCTClipRect(image.size, image.scale, size, destScale, resizeMode);
|
||||
CGSize destSize = RCTTargetSizeForClipRect(imageRect);
|
||||
|
||||
// Opacity optimizations
|
||||
UIColor *blendColor = nil;
|
||||
BOOL opaque = !RCTImageHasAlpha(image.CGImage);
|
||||
if (!opaque && backgroundColor) {
|
||||
CGFloat alpha;
|
||||
[backgroundColor getRed:NULL green:NULL blue:NULL alpha:&alpha];
|
||||
if (alpha > 0.999) { // no benefit to blending if background is translucent
|
||||
opaque = YES;
|
||||
blendColor = backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
// Decompress image at required size
|
||||
UIGraphicsBeginImageContextWithOptions(destSize, opaque, destScale);
|
||||
if (blendColor) {
|
||||
[blendColor setFill];
|
||||
UIRectFill((CGRect){CGPointZero, destSize});
|
||||
}
|
||||
[image drawInRect:imageRect];
|
||||
image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
}
|
||||
|
||||
block(image, nil);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)cancelDownload:(RCTImageDownloadCancellationBlock)downloadToken
|
||||
{
|
||||
if (downloadToken) {
|
||||
downloadToken();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
* Returns the optimal context size for an image drawn using the clip rect
|
||||
* returned by RCTClipRect.
|
||||
*/
|
||||
CGSize RCTTargetSizeForClipRect(CGRect);
|
||||
CGSize RCTTargetSizeForClipRect(CGRect clipRect)
|
||||
{
|
||||
return (CGSize){
|
||||
@@ -141,7 +210,6 @@ CGSize RCTTargetSizeForClipRect(CGRect clipRect)
|
||||
* then calculates the optimal rectangle to draw the image into so that it will
|
||||
* be sized and positioned correctly if drawn using the specified content mode.
|
||||
*/
|
||||
CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
|
||||
CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale,
|
||||
CGSize destSize, CGFloat destScale,
|
||||
UIViewContentMode resizeMode)
|
||||
@@ -202,66 +270,3 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale,
|
||||
return (CGRect){CGPointZero, destSize};
|
||||
}
|
||||
}
|
||||
|
||||
- (id)downloadImageForURL:(NSURL *)url
|
||||
size:(CGSize)size
|
||||
scale:(CGFloat)scale
|
||||
resizeMode:(UIViewContentMode)resizeMode
|
||||
backgroundColor:(UIColor *)backgroundColor
|
||||
block:(RCTImageDownloadBlock)block
|
||||
{
|
||||
return [self downloadDataForURL:url block:^(NSData *data, NSError *error) {
|
||||
|
||||
if (!data || error) {
|
||||
block(nil, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (CGSizeEqualToSize(size, CGSizeZero)) {
|
||||
// Target size wasn't available yet, so abort image drawing
|
||||
block(nil, nil);
|
||||
return;
|
||||
}
|
||||
|
||||
UIImage *image = [UIImage imageWithData:data scale:scale];
|
||||
if (image) {
|
||||
|
||||
// Get scale and size
|
||||
CGFloat destScale = scale ?: RCTScreenScale();
|
||||
CGRect imageRect = RCTClipRect(image.size, image.scale, size, destScale, resizeMode);
|
||||
CGSize destSize = RCTTargetSizeForClipRect(imageRect);
|
||||
|
||||
// Opacity optimizations
|
||||
UIColor *blendColor = nil;
|
||||
BOOL opaque = !RCTImageHasAlpha(image.CGImage);
|
||||
if (!opaque && backgroundColor) {
|
||||
CGFloat alpha;
|
||||
[backgroundColor getRed:NULL green:NULL blue:NULL alpha:&alpha];
|
||||
if (alpha > 0.999) { // no benefit to blending if background is translucent
|
||||
opaque = YES;
|
||||
blendColor = backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
// Decompress image at required size
|
||||
UIGraphicsBeginImageContextWithOptions(destSize, opaque, destScale);
|
||||
if (blendColor) {
|
||||
[blendColor setFill];
|
||||
UIRectFill((CGRect){CGPointZero, destSize});
|
||||
}
|
||||
[image drawInRect:imageRect];
|
||||
image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
}
|
||||
block(image, nil);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)cancelDownload:(id)downloadToken
|
||||
{
|
||||
if (downloadToken) {
|
||||
((dispatch_block_t)downloadToken)();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -121,7 +121,7 @@ static dispatch_queue_t RCTImageLoaderQueue(void)
|
||||
RCTDispatchCallbackOnMainQueue(callback, RCTErrorWithMessage(errorMessage), nil);
|
||||
return;
|
||||
}
|
||||
[[RCTImageDownloader sharedInstance] downloadDataForURL:url block:^(NSData *data, NSError *error) {
|
||||
[[RCTImageDownloader sharedInstance] downloadDataForURL:url progressBlock:nil block:^(NSData *data, NSError *error) {
|
||||
if (error) {
|
||||
RCTDispatchCallbackOnMainQueue(callback, error, nil);
|
||||
} else {
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class RCTEventDispatcher;
|
||||
@class RCTImageDownloader;
|
||||
|
||||
@interface RCTNetworkImageView : UIView
|
||||
|
||||
- (instancetype)initWithImageDownloader:(RCTImageDownloader *)imageDownloader NS_DESIGNATED_INITIALIZER;
|
||||
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
|
||||
imageDownloader:(RCTImageDownloader *)imageDownloader NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
/**
|
||||
* An image that will appear while the view is loading the image from the network,
|
||||
|
||||
@@ -14,23 +14,27 @@
|
||||
#import "RCTGIFImage.h"
|
||||
#import "RCTImageDownloader.h"
|
||||
#import "RCTUtils.h"
|
||||
#import "RCTBridgeModule.h"
|
||||
#import "RCTEventDispatcher.h"
|
||||
#import "UIView+React.h"
|
||||
|
||||
@implementation RCTNetworkImageView
|
||||
{
|
||||
BOOL _deferred;
|
||||
BOOL _progressHandlerRegistered;
|
||||
NSURL *_imageURL;
|
||||
NSURL *_deferredImageURL;
|
||||
NSUInteger _deferSentinel;
|
||||
RCTImageDownloader *_imageDownloader;
|
||||
id _downloadToken;
|
||||
RCTEventDispatcher *_eventDispatcher;
|
||||
}
|
||||
|
||||
- (instancetype)initWithImageDownloader:(RCTImageDownloader *)imageDownloader
|
||||
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher imageDownloader:(RCTImageDownloader *)imageDownloader
|
||||
{
|
||||
RCTAssertParam(imageDownloader);
|
||||
|
||||
if ((self = [super initWithFrame:CGRectZero])) {
|
||||
_eventDispatcher = eventDispatcher;
|
||||
_progressHandlerRegistered = NO;
|
||||
_deferSentinel = 0;
|
||||
_imageDownloader = imageDownloader;
|
||||
self.userInteractionEnabled = NO;
|
||||
@@ -56,6 +60,11 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
|
||||
[self _updateImage];
|
||||
}
|
||||
|
||||
- (void)setProgressHandlerRegistered:(BOOL)progressHandlerRegistered
|
||||
{
|
||||
_progressHandlerRegistered = progressHandlerRegistered;
|
||||
}
|
||||
|
||||
- (void)reactSetFrame:(CGRect)frame
|
||||
{
|
||||
[super reactSetFrame:frame];
|
||||
@@ -89,8 +98,34 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
|
||||
self.layer.minificationFilter = kCAFilterTrilinear;
|
||||
self.layer.magnificationFilter = kCAFilterTrilinear;
|
||||
}
|
||||
[_eventDispatcher sendInputEventWithName:@"loadStart" body:@{ @"target": self.reactTag }];
|
||||
|
||||
RCTDataProgressBlock progressHandler = ^(int64_t written, int64_t total) {
|
||||
if (_progressHandlerRegistered) {
|
||||
NSDictionary *event = @{
|
||||
@"target": self.reactTag,
|
||||
@"written": @(written),
|
||||
@"total": @(total),
|
||||
};
|
||||
[_eventDispatcher sendInputEventWithName:@"loadProgress" body:event];
|
||||
}
|
||||
};
|
||||
|
||||
void (^errorHandler)(NSString *errorDescription) = ^(NSString *errorDescription) {
|
||||
NSDictionary *event = @{
|
||||
@"target": self.reactTag,
|
||||
@"error": errorDescription,
|
||||
};
|
||||
[_eventDispatcher sendInputEventWithName:@"loadError" body:event];
|
||||
};
|
||||
|
||||
void (^loadEndHandler)(void) = ^(void) {
|
||||
NSDictionary *event = @{ @"target": self.reactTag };
|
||||
[_eventDispatcher sendInputEventWithName:@"loaded" body:event];
|
||||
};
|
||||
|
||||
if ([imageURL.pathExtension caseInsensitiveCompare:@"gif"] == NSOrderedSame) {
|
||||
_downloadToken = [_imageDownloader downloadDataForURL:imageURL block:^(NSData *data, NSError *error) {
|
||||
_downloadToken = [_imageDownloader downloadDataForURL:imageURL progressBlock:progressHandler block:^(NSData *data, NSError *error) {
|
||||
if (data) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (imageURL != self.imageURL) {
|
||||
@@ -102,13 +137,16 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
|
||||
self.layer.minificationFilter = kCAFilterLinear;
|
||||
self.layer.magnificationFilter = kCAFilterLinear;
|
||||
[self.layer addAnimation:animation forKey:@"contents"];
|
||||
loadEndHandler();
|
||||
});
|
||||
} else if (error) {
|
||||
RCTLogWarn(@"Unable to download image data. Error: %@", error);
|
||||
errorHandler([error description]);
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
_downloadToken = [_imageDownloader downloadImageForURL:imageURL size:self.bounds.size scale:RCTScreenScale() resizeMode:self.contentMode backgroundColor:self.backgroundColor block:^(UIImage *image, NSError *error) {
|
||||
_downloadToken = [_imageDownloader downloadImageForURL:imageURL size:self.bounds.size scale:RCTScreenScale()
|
||||
resizeMode:self.contentMode backgroundColor:self.backgroundColor
|
||||
progressBlock:progressHandler block:^(UIImage *image, NSError *error) {
|
||||
if (image) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (imageURL != self.imageURL) {
|
||||
@@ -118,9 +156,10 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
|
||||
[self.layer removeAnimationForKey:@"contents"];
|
||||
self.layer.contentsScale = image.scale;
|
||||
self.layer.contents = (__bridge id)image.CGImage;
|
||||
loadEndHandler();
|
||||
});
|
||||
} else if (error) {
|
||||
RCTLogWarn(@"Unable to download image. Error: %@", error);
|
||||
errorHandler([error description]);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -9,24 +9,38 @@
|
||||
|
||||
#import "RCTNetworkImageViewManager.h"
|
||||
|
||||
#import "RCTNetworkImageView.h"
|
||||
|
||||
#import "RCTBridge.h"
|
||||
#import "RCTConvert.h"
|
||||
#import "RCTUtils.h"
|
||||
|
||||
#import "RCTImageDownloader.h"
|
||||
#import "RCTNetworkImageView.h"
|
||||
#import "RCTUtils.h"
|
||||
|
||||
@implementation RCTNetworkImageViewManager
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
@synthesize bridge = _bridge;
|
||||
@synthesize methodQueue = _methodQueue;
|
||||
|
||||
- (UIView *)view
|
||||
{
|
||||
return [[RCTNetworkImageView alloc] initWithImageDownloader:[RCTImageDownloader sharedInstance]];
|
||||
return [[RCTNetworkImageView alloc] initWithEventDispatcher:self.bridge.eventDispatcher imageDownloader:[RCTImageDownloader sharedInstance]];
|
||||
}
|
||||
|
||||
RCT_REMAP_VIEW_PROPERTY(defaultImageSrc, defaultImage, UIImage)
|
||||
RCT_REMAP_VIEW_PROPERTY(src, imageURL, NSURL)
|
||||
RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode)
|
||||
RCT_EXPORT_VIEW_PROPERTY(progressHandlerRegistered, BOOL)
|
||||
|
||||
- (NSDictionary *)customDirectEventTypes
|
||||
{
|
||||
return @{
|
||||
@"loadStart": @{ @"registrationName": @"onLoadStart" },
|
||||
@"loadProgress": @{ @"registrationName": @"onLoadProgress" },
|
||||
@"loaded": @{ @"registrationName": @"onLoaded" },
|
||||
@"loadError": @{ @"registrationName": @"onLoadError" },
|
||||
@"loadAbort": @{ @"registrationName": @"onLoadAbort" },
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -21,11 +21,6 @@ var setImmediate = require('setImmediate');
|
||||
|
||||
type Handle = number;
|
||||
|
||||
/**
|
||||
* Maximum time a handle can be open before warning in DEV.
|
||||
*/
|
||||
var DEV_TIMEOUT = 2000;
|
||||
|
||||
var _emitter = new EventEmitter();
|
||||
var _interactionSet = new Set();
|
||||
var _addInteractionSet = new Set();
|
||||
@@ -94,14 +89,6 @@ var InteractionManager = {
|
||||
scheduleUpdate();
|
||||
var handle = ++_inc;
|
||||
_addInteractionSet.add(handle);
|
||||
if (__DEV__) {
|
||||
// Capture the stack trace of what created the handle.
|
||||
var error = new Error(
|
||||
'InteractionManager: interaction handle not cleared within ' +
|
||||
DEV_TIMEOUT + ' ms.'
|
||||
);
|
||||
setDevTimeoutHandle(handle, error, DEV_TIMEOUT);
|
||||
}
|
||||
return handle;
|
||||
},
|
||||
|
||||
@@ -166,19 +153,4 @@ function processUpdate() {
|
||||
_deleteInteractionSet.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until `timeout` has passed and warn if the handle has not been cleared.
|
||||
*/
|
||||
function setDevTimeoutHandle(
|
||||
handle: Handle,
|
||||
error: Error,
|
||||
timeout: number
|
||||
): void {
|
||||
setTimeout(() => {
|
||||
if (_interactionSet.has(handle)) {
|
||||
console.warn(error.message + '\n' + error.stack);
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
module.exports = InteractionManager;
|
||||
|
||||
@@ -9,13 +9,6 @@
|
||||
|
||||
#import "RCTHTTPRequestHandler.h"
|
||||
|
||||
#import "RCTAssert.h"
|
||||
#import "RCTConvert.h"
|
||||
#import "RCTEventDispatcher.h"
|
||||
#import "RCTImageLoader.h"
|
||||
#import "RCTLog.h"
|
||||
#import "RCTUtils.h"
|
||||
|
||||
@interface RCTHTTPRequestHandler () <NSURLSessionDataDelegate>
|
||||
|
||||
@end
|
||||
|
||||
@@ -48,10 +48,10 @@
|
||||
58B511D21A9E6C8500147676 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
58B512061A9E6CE300147676 /* RCTNetworking.h */,
|
||||
58B512071A9E6CE300147676 /* RCTNetworking.m */,
|
||||
352DA0B71B17855800AA15A8 /* RCTHTTPRequestHandler.h */,
|
||||
352DA0B81B17855800AA15A8 /* RCTHTTPRequestHandler.m */,
|
||||
58B512061A9E6CE300147676 /* RCTNetworking.h */,
|
||||
58B512071A9E6CE300147676 /* RCTNetworking.m */,
|
||||
1372B7351AB03E7B00659ED6 /* RCTReachability.h */,
|
||||
1372B7361AB03E7B00659ED6 /* RCTReachability.m */,
|
||||
58B511DC1A9E6C8500147676 /* Products */,
|
||||
|
||||
@@ -381,7 +381,7 @@ RCT_EXPORT_MODULE()
|
||||
|
||||
NSString *responseText = [[NSString alloc] initWithData:data encoding:encoding];
|
||||
if (!responseText && data.length) {
|
||||
RCTLogError(@"Received data was invalid.");
|
||||
RCTLogWarn(@"Received data was invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ class XMLHttpRequestBase {
|
||||
this._lowerCaseResponseHeaders =
|
||||
Object.keys(headers).reduce((lcaseHeaders, headerName) => {
|
||||
lcaseHeaders[headerName.toLowerCase()] = headers[headerName];
|
||||
return headers;
|
||||
return lcaseHeaders;
|
||||
}, {});
|
||||
}
|
||||
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
580C37661AB104AF0015E709 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
58E64FE31AB964CD007446E2 /* FBSnapshotTestCase */,
|
||||
585135331AB3C56F00882537 /* RCTTestModule.h */,
|
||||
585135341AB3C56F00882537 /* RCTTestModule.m */,
|
||||
585135351AB3C56F00882537 /* RCTTestRunner.h */,
|
||||
585135361AB3C56F00882537 /* RCTTestRunner.m */,
|
||||
58E64FE31AB964CD007446E2 /* FBSnapshotTestCase */,
|
||||
580C37701AB104AF0015E709 /* Products */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
|
||||
@@ -25,12 +25,12 @@ typedef NS_ENUM(NSInteger, RCTTestStatus) {
|
||||
/**
|
||||
* The snapshot test controller for this module.
|
||||
*/
|
||||
@property (nonatomic, weak) FBSnapshotTestController *controller;
|
||||
@property (nonatomic, strong) FBSnapshotTestController *controller;
|
||||
|
||||
/**
|
||||
* This is the view to be snapshotted.
|
||||
*/
|
||||
@property (nonatomic, weak) UIView *view;
|
||||
@property (nonatomic, strong) UIView *view;
|
||||
|
||||
/**
|
||||
* This is used to give meaningful names to snapshot image files.
|
||||
|
||||
@@ -55,7 +55,7 @@ RCT_EXPORT_METHOD(verifySnapshot:(RCTResponseSenderBlock)callback)
|
||||
}];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(sendAppEvent:(NSString *)name body:(id)body)
|
||||
RCT_EXPORT_METHOD(sendAppEvent:(NSString *)name body:(nullable id)body)
|
||||
{
|
||||
[_bridge.eventDispatcher sendAppEventWithName:name body:body];
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
*
|
||||
* FB_REFERENCE_IMAGE_DIR="\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\""
|
||||
*/
|
||||
#define RCTInitRunnerForApp(app__) [[RCTTestRunner alloc] initWithApp:(app__) referenceDir:@FB_REFERENCE_IMAGE_DIR]
|
||||
#define RCTInitRunnerForApp(app__, moduleProvider__) \
|
||||
[[RCTTestRunner alloc] initWithApp:(app__) \
|
||||
referenceDirectory:@FB_REFERENCE_IMAGE_DIR \
|
||||
moduleProvider:(moduleProvider__)]
|
||||
|
||||
@interface RCTTestRunner : NSObject
|
||||
|
||||
@@ -28,10 +31,12 @@
|
||||
* macro instead of calling this directly.
|
||||
*
|
||||
* @param app The path to the app bundle without suffixes, e.g. IntegrationTests/IntegrationTestsApp
|
||||
* @param referenceDir The path for snapshot references images. The RCTInitRunnerForApp macro uses
|
||||
* FB_REFERENCE_IMAGE_DIR for this automatically.
|
||||
* @param referenceDirectory The path for snapshot references images. The RCTInitRunnerForApp macro uses FB_REFERENCE_IMAGE_DIR for this automatically.
|
||||
* @param block A block that returns an array of extra modules to be used by the test runner.
|
||||
*/
|
||||
- (instancetype)initWithApp:(NSString *)app referenceDir:(NSString *)referenceDir NS_DESIGNATED_INITIALIZER;
|
||||
- (instancetype)initWithApp:(NSString *)app
|
||||
referenceDirectory:(NSString *)referenceDirectory
|
||||
moduleProvider:(NSArray *(^)(void))block NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
/**
|
||||
* Simplest runTest function simply mounts the specified JS module with no
|
||||
|
||||
@@ -27,20 +27,27 @@
|
||||
@implementation RCTTestRunner
|
||||
{
|
||||
FBSnapshotTestController *_testController;
|
||||
RCTBridgeModuleProviderBlock _moduleProvider;
|
||||
}
|
||||
|
||||
- (instancetype)initWithApp:(NSString *)app referenceDir:(NSString *)referenceDir
|
||||
- (instancetype)initWithApp:(NSString *)app
|
||||
referenceDirectory:(NSString *)referenceDirectory
|
||||
moduleProvider:(RCTBridgeModuleProviderBlock)block
|
||||
{
|
||||
RCTAssertParam(app);
|
||||
RCTAssertParam(referenceDir);
|
||||
RCTAssertParam(referenceDirectory);
|
||||
|
||||
if ((self = [super init])) {
|
||||
|
||||
NSString *sanitizedAppName = [app stringByReplacingOccurrencesOfString:@"/" withString:@"-"];
|
||||
sanitizedAppName = [sanitizedAppName stringByReplacingOccurrencesOfString:@"\\" withString:@"-"];
|
||||
_testController = [[FBSnapshotTestController alloc] initWithTestName:sanitizedAppName];
|
||||
_testController.referenceImagesDirectory = referenceDir;
|
||||
_testController.referenceImagesDirectory = referenceDirectory;
|
||||
_moduleProvider = [block copy];
|
||||
|
||||
#if RUNNING_ON_CI
|
||||
_scriptURL = [[NSBundle bundleForClass:[RCTBridge class]] URLForResource:@"main" withExtension:@"jsbundle"];
|
||||
RCTAssert(_scriptURL != nil, @"Could not locate main.jsBundle");
|
||||
#else
|
||||
_scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.includeRequire.runModule.bundle?dev=true", app]];
|
||||
#endif
|
||||
@@ -73,16 +80,20 @@ RCT_NOT_IMPLEMENTED(-init)
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock
|
||||
- (void)runTest:(SEL)test module:(NSString *)moduleName
|
||||
initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock
|
||||
{
|
||||
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:_scriptURL
|
||||
moduleName:moduleName
|
||||
launchOptions:nil];
|
||||
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:_scriptURL
|
||||
moduleProvider:_moduleProvider
|
||||
launchOptions:nil];
|
||||
|
||||
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName];
|
||||
rootView.initialProperties = initialProps;
|
||||
rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices
|
||||
|
||||
NSString *testModuleName = RCTBridgeModuleNameForClass([RCTTestModule class]);
|
||||
RCTTestModule *testModule = rootView.bridge.batchedBridge.modules[testModuleName];
|
||||
RCTAssert(_testController != nil, @"_testController should not be nil");
|
||||
testModule.controller = _testController;
|
||||
testModule.testSelector = test;
|
||||
testModule.view = rootView;
|
||||
@@ -100,14 +111,11 @@ RCT_NOT_IMPLEMENTED(-init)
|
||||
}
|
||||
[rootView removeFromSuperview];
|
||||
|
||||
|
||||
NSArray *nonLayoutSubviews = [vc.view.subviews filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id subview, NSDictionary *bindings) {
|
||||
return ![NSStringFromClass([subview class]) isEqualToString:@"_UILayoutGuide"];
|
||||
}]];
|
||||
RCTAssert(nonLayoutSubviews.count == 0, @"There shouldn't be any other views: %@", nonLayoutSubviews);
|
||||
|
||||
|
||||
vc.view = nil;
|
||||
[[RCTRedBox sharedInstance] dismiss];
|
||||
if (expectErrorBlock) {
|
||||
RCTAssert(expectErrorBlock(error), @"Expected an error but nothing matched.");
|
||||
@@ -116,7 +124,6 @@ RCT_NOT_IMPLEMENTED(-init)
|
||||
RCTAssert(testModule.status != RCTTestStatusPending, @"Test didn't finish within %d seconds", TIMEOUT_SECONDS);
|
||||
RCTAssert(testModule.status == RCTTestStatusPassed, @"Test failed");
|
||||
}
|
||||
RCTAssert(self.recordMode == NO, @"Don't forget to turn record mode back to NO before commit.");
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||