Updates from Thurs July 23rd
This commit is contained in:
commit
bb7aa500d7
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
Image,
|
||||
StyleSheet,
|
||||
View,
|
||||
ScrollView
|
||||
} = React;
|
||||
|
||||
var AssetScaledImageExample = React.createClass({
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
asset: this.props.asset
|
||||
};
|
||||
},
|
||||
|
||||
render() {
|
||||
var image = this.state.asset.node.image;
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.row}>
|
||||
<Image source={image} style={styles.imageWide}/>
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<Image source={image} style={styles.imageThumb}/>
|
||||
<Image source={image} style={styles.imageThumb}/>
|
||||
<Image source={image} style={styles.imageThumb}/>
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<Image source={image} style={styles.imageT1}/>
|
||||
<Image source={image} style={styles.imageT2}/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
row: {
|
||||
padding: 5,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
textColumn: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
},
|
||||
imageWide: {
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
width: 320,
|
||||
height: 240,
|
||||
margin: 5,
|
||||
},
|
||||
imageThumb: {
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
width: 100,
|
||||
height: 100,
|
||||
margin: 5,
|
||||
},
|
||||
imageT1: {
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
width: 212,
|
||||
height: 320,
|
||||
margin: 5,
|
||||
},
|
||||
imageT2: {
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
width: 100,
|
||||
height: 320,
|
||||
margin: 5,
|
||||
},
|
||||
});
|
||||
|
||||
exports.title = '<AssetScaledImageExample>';
|
||||
exports.description = 'Example component that displays the automatic scaling capabilities of the <Image /> tag';
|
||||
module.exports = AssetScaledImageExample;
|
|
@ -24,9 +24,11 @@ var {
|
|||
SwitchIOS,
|
||||
Text,
|
||||
View,
|
||||
TouchableOpacity
|
||||
} = React;
|
||||
|
||||
var CameraRollView = require('./CameraRollView.ios');
|
||||
var AssetScaledImageExampleView = require('./AssetScaledImageExample');
|
||||
|
||||
var CAMERA_ROLL_VIEW = 'camera_roll_view';
|
||||
|
||||
|
@ -54,7 +56,7 @@ var CameraRollExample = React.createClass({
|
|||
<Text>{'Group Type: ' + this.state.groupTypes}</Text>
|
||||
<CameraRollView
|
||||
ref={CAMERA_ROLL_VIEW}
|
||||
batchSize={5}
|
||||
batchSize={20}
|
||||
groupTypes={this.state.groupTypes}
|
||||
renderImage={this._renderImage}
|
||||
/>
|
||||
|
@ -62,24 +64,35 @@ var CameraRollExample = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
loadAsset(asset){
|
||||
this.props.navigator.push({
|
||||
title: 'Camera Roll Image',
|
||||
component: AssetScaledImageExampleView,
|
||||
backButtonTitle: 'Back',
|
||||
passProps: { asset: asset },
|
||||
});
|
||||
},
|
||||
|
||||
_renderImage(asset) {
|
||||
var imageSize = this.state.bigImages ? 150 : 75;
|
||||
var imageStyle = [styles.image, {width: imageSize, height: imageSize}];
|
||||
var location = asset.node.location.longitude ?
|
||||
JSON.stringify(asset.node.location) : 'Unknown location';
|
||||
return (
|
||||
<View key={asset} style={styles.row}>
|
||||
<Image
|
||||
source={asset.node.image}
|
||||
style={imageStyle}
|
||||
/>
|
||||
<View style={styles.info}>
|
||||
<Text style={styles.url}>{asset.node.image.uri}</Text>
|
||||
<Text>{location}</Text>
|
||||
<Text>{asset.node.group_name}</Text>
|
||||
<Text>{new Date(asset.node.timestamp).toString()}</Text>
|
||||
<TouchableOpacity onPress={ this.loadAsset.bind( this, asset ) }>
|
||||
<View key={asset} style={styles.row}>
|
||||
<Image
|
||||
source={asset.node.image}
|
||||
style={imageStyle}
|
||||
/>
|
||||
<View style={styles.info}>
|
||||
<Text style={styles.url}>{asset.node.image.uri}</Text>
|
||||
<Text>{location}</Text>
|
||||
<Text>{asset.node.group_name}</Text>
|
||||
<Text>{new Date(asset.node.timestamp).toString()}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -115,7 +128,7 @@ var styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
exports.title = '<CameraRollView>';
|
||||
exports.title = 'Camera Roll';
|
||||
exports.description = 'Example component that uses CameraRoll to list user\'s photos';
|
||||
exports.examples = [
|
||||
{
|
||||
|
|
|
@ -32,7 +32,7 @@ var NetworkImageExample = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
error: false,
|
||||
loading: true,
|
||||
loading: false,
|
||||
progress: 0
|
||||
};
|
||||
},
|
||||
|
@ -47,10 +47,10 @@ var NetworkImageExample = React.createClass({
|
|||
<Image
|
||||
source={this.props.source}
|
||||
style={[styles.base, {overflow: 'visible'}]}
|
||||
onLoadError={(e) => this.setState({error: e.nativeEvent.error})}
|
||||
onLoadProgress={(e) => this.setState({progress: Math.max(0, Math.round(100 * e.nativeEvent.written / e.nativeEvent.total))}) }
|
||||
onLoadEnd={() => this.setState({loading: false, error: false})}
|
||||
onLoadAbort={() => this.setState({error: 'Loading has aborted'})} >
|
||||
onLoadStart={(e) => this.setState({loading: true})}
|
||||
onError={(e) => this.setState({error: e.nativeEvent.error, loading: false})}
|
||||
onProgress={(e) => this.setState({progress: Math.round(100 * e.nativeEvent.loaded / e.nativeEvent.total)})}
|
||||
onLoad={() => this.setState({loading: false, error: false})}>
|
||||
{loader}
|
||||
</Image>;
|
||||
}
|
||||
|
|
|
@ -48,19 +48,19 @@ var Thumb = React.createClass({
|
|||
},
|
||||
render: function() {
|
||||
return (
|
||||
<TouchableOpacity onPress={this._onPressThumb}>
|
||||
<View style={[styles.buttonContents, {flexDirection: this.state.dir}]}>
|
||||
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
|
||||
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
|
||||
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
|
||||
{this.state.dir === 'column' ?
|
||||
<Text>
|
||||
Oooo, look at this new text! So awesome it may just be crazy.
|
||||
Let me keep typing here so it wraps at least one line.
|
||||
</Text> :
|
||||
<Text />
|
||||
}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={this._onPressThumb}
|
||||
style={[styles.buttonContents, {flexDirection: this.state.dir}]}>
|
||||
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
|
||||
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
|
||||
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
|
||||
{this.state.dir === 'column' ?
|
||||
<Text>
|
||||
Oooo, look at this new text! So awesome it may just be crazy.
|
||||
Let me keep typing here so it wraps at least one line.
|
||||
</Text> :
|
||||
<Text />
|
||||
}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
@ -127,14 +127,12 @@ var ListViewPagingExample = React.createClass({
|
|||
<View><Text style={styles.text}>1 Like</Text></View> :
|
||||
null;
|
||||
return (
|
||||
<TouchableOpacity onPress={this._onPressHeader}>
|
||||
<View style={styles.header}>
|
||||
{headerLikeText}
|
||||
<View>
|
||||
<Text style={styles.text}>
|
||||
Table Header (click me)
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={this._onPressHeader} style={styles.header}>
|
||||
{headerLikeText}
|
||||
<View>
|
||||
<Text style={styles.text}>
|
||||
Table Header (click me)
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
|
|
@ -55,26 +55,24 @@ var BreadcrumbNavSample = React.createClass({
|
|||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => navigator.push(_getRandomRoute())}>
|
||||
<View>
|
||||
<Text style={styles.titleText}>{route.title}</Text>
|
||||
</View>
|
||||
<Text style={styles.titleText}>{route.title}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
},
|
||||
iconForRoute: function(route, navigator) {
|
||||
return (
|
||||
<TouchableOpacity onPress={() => {
|
||||
navigator.popToRoute(route);
|
||||
}}>
|
||||
<View style={styles.crumbIconPlaceholder} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => { navigator.popToRoute(route); }}
|
||||
style={styles.crumbIconPlaceholder}
|
||||
/>
|
||||
);
|
||||
},
|
||||
separatorForRoute: function(route, navigator) {
|
||||
return (
|
||||
<TouchableOpacity onPress={navigator.pop}>
|
||||
<View style={styles.crumbSeparatorPlaceholder} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={navigator.pop}
|
||||
style={styles.crumbSeparatorPlaceholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -51,12 +51,11 @@ var NavigationBarRouteMapper = {
|
|||
var previousRoute = navState.routeStack[index - 1];
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => navigator.pop()}>
|
||||
<View style={styles.navBarLeftButton}>
|
||||
<Text style={[styles.navBarText, styles.navBarButtonText]}>
|
||||
{previousRoute.title}
|
||||
</Text>
|
||||
</View>
|
||||
onPress={() => navigator.pop()}
|
||||
style={styles.navBarLeftButton}>
|
||||
<Text style={[styles.navBarText, styles.navBarButtonText]}>
|
||||
{previousRoute.title}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
},
|
||||
|
@ -64,12 +63,11 @@ var NavigationBarRouteMapper = {
|
|||
RightButton: function(route, navigator, index, navState) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => navigator.push(newRandomRoute())}>
|
||||
<View style={styles.navBarRightButton}>
|
||||
<Text style={[styles.navBarText, styles.navBarButtonText]}>
|
||||
Next
|
||||
</Text>
|
||||
</View>
|
||||
onPress={() => navigator.push(newRandomRoute())}
|
||||
style={styles.navBarRightButton}>
|
||||
<Text style={[styles.navBarText, styles.navBarButtonText]}>
|
||||
Next
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -33,7 +33,7 @@ var WithLabel = React.createClass({
|
|||
{this.props.children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var TextEventsExample = React.createClass({
|
||||
|
@ -41,13 +41,17 @@ var TextEventsExample = React.createClass({
|
|||
return {
|
||||
curText: '<No Event>',
|
||||
prevText: '<No Event>',
|
||||
prev2Text: '<No Event>',
|
||||
};
|
||||
},
|
||||
|
||||
updateText: function(text) {
|
||||
this.setState({
|
||||
curText: text,
|
||||
prevText: this.state.curText,
|
||||
this.setState((state) => {
|
||||
return {
|
||||
curText: text,
|
||||
prevText: state.curText,
|
||||
prev2Text: state.prevText,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -73,13 +77,43 @@ var TextEventsExample = React.createClass({
|
|||
/>
|
||||
<Text style={styles.eventLabel}>
|
||||
{this.state.curText}{'\n'}
|
||||
(prev: {this.state.prevText})
|
||||
(prev: {this.state.prevText}){'\n'}
|
||||
(prev2: {this.state.prev2Text})
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
class RewriteExample extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {text: ''};
|
||||
}
|
||||
render() {
|
||||
var limit = 20;
|
||||
var remainder = limit - this.state.text.length;
|
||||
var remainderColor = remainder > 5 ? 'blue' : 'red';
|
||||
return (
|
||||
<View style={styles.rewriteContainer}>
|
||||
<TextInput
|
||||
multiline={false}
|
||||
maxLength={limit}
|
||||
onChangeText={(text) => {
|
||||
text = text.replace(/ /g, '_');
|
||||
this.setState({text});
|
||||
}}
|
||||
style={styles.default}
|
||||
value={this.state.text}
|
||||
/>
|
||||
<Text style={[styles.remainder, {color: remainderColor}]}>
|
||||
{remainder}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
page: {
|
||||
paddingBottom: 300,
|
||||
|
@ -125,12 +159,19 @@ var styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
},
|
||||
label: {
|
||||
width: 120,
|
||||
justifyContent: 'flex-end',
|
||||
flexDirection: 'row',
|
||||
width: 115,
|
||||
alignItems: 'flex-end',
|
||||
marginRight: 10,
|
||||
paddingTop: 2,
|
||||
},
|
||||
rewriteContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
remainder: {
|
||||
textAlign: 'right',
|
||||
width: 24,
|
||||
},
|
||||
});
|
||||
|
||||
exports.displayName = (undefined: ?string);
|
||||
|
@ -143,6 +184,12 @@ exports.examples = [
|
|||
return <TextInput autoFocus={true} style={styles.default} />;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Live Re-Write (<sp> -> '_') + maxLength",
|
||||
render: function() {
|
||||
return <RewriteExample />;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Auto-capitalize',
|
||||
render: function() {
|
||||
|
@ -268,7 +315,7 @@ exports.examples = [
|
|||
return (
|
||||
<View>
|
||||
<WithLabel label="true">
|
||||
<TextInput password={true} style={styles.default} value="abc" />
|
||||
<TextInput password={true} style={styles.default} defaultValue="abc" />
|
||||
</WithLabel>
|
||||
</View>
|
||||
);
|
||||
|
@ -276,7 +323,7 @@ exports.examples = [
|
|||
},
|
||||
{
|
||||
title: 'Event handling',
|
||||
render: function(): ReactElement { return <TextEventsExample /> },
|
||||
render: function(): ReactElement { return <TextEventsExample />; },
|
||||
},
|
||||
{
|
||||
title: 'Colored input text',
|
||||
|
@ -285,11 +332,11 @@ exports.examples = [
|
|||
<View>
|
||||
<TextInput
|
||||
style={[styles.default, {color: 'blue'}]}
|
||||
value="Blue"
|
||||
defaultValue="Blue"
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.default, {color: 'green'}]}
|
||||
value="Green"
|
||||
defaultValue="Green"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
@ -336,7 +383,7 @@ exports.examples = [
|
|||
<WithLabel label="clearTextOnFocus">
|
||||
<TextInput
|
||||
placeholder="text is cleared on focus"
|
||||
value="text is cleared on focus"
|
||||
defaultValue="text is cleared on focus"
|
||||
style={styles.default}
|
||||
clearTextOnFocus={true}
|
||||
/>
|
||||
|
@ -344,7 +391,7 @@ exports.examples = [
|
|||
<WithLabel label="selectTextOnFocus">
|
||||
<TextInput
|
||||
placeholder="text is selected on focus"
|
||||
value="text is selected on focus"
|
||||
defaultValue="text is selected on focus"
|
||||
style={styles.default}
|
||||
selectTextOnFocus={true}
|
||||
/>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1300627F1B59179B0043FE5A /* RCTGzipTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1300627E1B59179B0043FE5A /* RCTGzipTests.m */; };
|
||||
13417FE91AA91432003F314A /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13417FE81AA91428003F314A /* libRCTImage.a */; };
|
||||
134180011AA9153C003F314A /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13417FEF1AA914B8003F314A /* libRCTText.a */; };
|
||||
1341802C1AA9178B003F314A /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1341802B1AA91779003F314A /* libRCTNetwork.a */; };
|
||||
|
@ -19,9 +20,10 @@
|
|||
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 */; };
|
||||
13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DB03471B5D2ED500C27245 /* RCTJSONTests.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 */; };
|
||||
144D21241B2204C5006DB32B /* RCTClipRectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 144D21231B2204C5006DB32B /* RCTClipRectTests.m */; };
|
||||
147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; };
|
||||
1497CFAC1B21F5E400C1F8F2 /* RCTAllocationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */; };
|
||||
1497CFAD1B21F5E400C1F8F2 /* RCTBridgeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */; };
|
||||
|
@ -156,6 +158,7 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
004D289E1AAF61C70097A701 /* UIExplorerUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIExplorerUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
1300627E1B59179B0043FE5A /* RCTGzipTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGzipTests.m; sourceTree = "<group>"; };
|
||||
13417FE31AA91428003F314A /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = ../../Libraries/Image/RCTImage.xcodeproj; sourceTree = "<group>"; };
|
||||
13417FEA1AA914B8003F314A /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../../Libraries/Text/RCTText.xcodeproj; sourceTree = "<group>"; };
|
||||
134180261AA91779003F314A /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = ../../Libraries/Network/RCTNetwork.xcodeproj; sourceTree = "<group>"; };
|
||||
|
@ -172,6 +175,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>"; };
|
||||
13DB03471B5D2ED500C27245 /* RCTJSONTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSONTests.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>"; };
|
||||
|
@ -183,7 +187,7 @@
|
|||
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 /* 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>"; };
|
||||
144D21231B2204C5006DB32B /* RCTClipRectTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTClipRectTests.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>"; };
|
||||
1497CFA61B21F5E400C1F8F2 /* RCTContextExecutorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTContextExecutorTests.m; sourceTree = "<group>"; };
|
||||
|
@ -348,15 +352,17 @@
|
|||
children = (
|
||||
1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */,
|
||||
1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */,
|
||||
144D21231B2204C5006DB32B /* RCTClippingTests.m */,
|
||||
138D6A151B53CD440074A87E /* RCTCacheTests.m */,
|
||||
144D21231B2204C5006DB32B /* RCTClipRectTests.m */,
|
||||
1497CFA61B21F5E400C1F8F2 /* RCTContextExecutorTests.m */,
|
||||
1497CFA71B21F5E400C1F8F2 /* RCTConvert_NSURLTests.m */,
|
||||
1497CFA81B21F5E400C1F8F2 /* RCTConvert_UIFontTests.m */,
|
||||
1497CFA91B21F5E400C1F8F2 /* RCTEventDispatcherTests.m */,
|
||||
1300627E1B59179B0043FE5A /* RCTGzipTests.m */,
|
||||
13DB03471B5D2ED500C27245 /* RCTJSONTests.m */,
|
||||
138D6A161B53CD440074A87E /* RCTShadowViewTests.m */,
|
||||
1497CFAA1B21F5E400C1F8F2 /* RCTSparseArrayTests.m */,
|
||||
1497CFAB1B21F5E400C1F8F2 /* RCTUIManagerTests.m */,
|
||||
138D6A151B53CD440074A87E /* RCTCacheTests.m */,
|
||||
143BC57E1B21E18100462512 /* Info.plist */,
|
||||
14D6D7101B220EB3001FB087 /* libOCMock.a */,
|
||||
14D6D7011B220AE3001FB087 /* OCMock */,
|
||||
|
@ -781,14 +787,16 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1497CFB01B21F5E400C1F8F2 /* RCTConvert_UIFontTests.m in Sources */,
|
||||
144D21241B2204C5006DB32B /* RCTClippingTests.m in Sources */,
|
||||
144D21241B2204C5006DB32B /* RCTClipRectTests.m in Sources */,
|
||||
1497CFB21B21F5E400C1F8F2 /* RCTSparseArrayTests.m in Sources */,
|
||||
1300627F1B59179B0043FE5A /* RCTGzipTests.m in Sources */,
|
||||
1497CFAF1B21F5E400C1F8F2 /* RCTConvert_NSURLTests.m in Sources */,
|
||||
1497CFAE1B21F5E400C1F8F2 /* RCTContextExecutorTests.m in Sources */,
|
||||
1497CFAD1B21F5E400C1F8F2 /* RCTBridgeTests.m in Sources */,
|
||||
1497CFB11B21F5E400C1F8F2 /* RCTEventDispatcherTests.m in Sources */,
|
||||
1497CFB31B21F5E400C1F8F2 /* RCTUIManagerTests.m in Sources */,
|
||||
138D6A171B53CD440074A87E /* RCTCacheTests.m in Sources */,
|
||||
13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */,
|
||||
1497CFAC1B21F5E400C1F8F2 /* RCTAllocationTests.m in Sources */,
|
||||
138D6A181B53CD440074A87E /* RCTShadowViewTests.m in Sources */,
|
||||
);
|
||||
|
@ -1040,6 +1048,7 @@
|
|||
"$(inherited)",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(SRCROOT)/../../React/**",
|
||||
"$(SRCROOT)/../../Libraries/**",
|
||||
);
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 7.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
|
@ -1094,6 +1103,7 @@
|
|||
"$(inherited)",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(SRCROOT)/../../React/**",
|
||||
"$(SRCROOT)/../../Libraries/**",
|
||||
);
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 7.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
|
|
|
@ -59,12 +59,12 @@ var IntegrationTestsApp = React.createClass({
|
|||
<View style={styles.separator} />
|
||||
<ScrollView>
|
||||
{TESTS.map((test) => [
|
||||
<TouchableOpacity onPress={() => this.setState({test})}>
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.testName}>
|
||||
{test.displayName}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => this.setState({test})}
|
||||
style={styles.row}>
|
||||
<Text style={styles.testName}>
|
||||
{test.displayName}
|
||||
</Text>
|
||||
</TouchableOpacity>,
|
||||
<View style={styles.separator} />
|
||||
])}
|
||||
|
|
|
@ -16,10 +16,7 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIView.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
extern CGRect RCTClipRect(CGSize contentSize, CGFloat contentScale,
|
||||
CGSize targetSize, CGFloat targetScale,
|
||||
UIViewContentMode resizeMode);
|
||||
#import "RCTImageUtils.h"
|
||||
|
||||
#define RCTAssertEqualPoints(a, b) { \
|
||||
XCTAssertEqual(a.x, b.x); \
|
||||
|
@ -36,11 +33,11 @@ RCTAssertEqualPoints(a.origin, b.origin); \
|
|||
RCTAssertEqualSizes(a.size, b.size); \
|
||||
}
|
||||
|
||||
@interface ClippingTests : XCTestCase
|
||||
@interface RCTClipRectTests : XCTestCase
|
||||
|
||||
@end
|
||||
|
||||
@implementation ClippingTests
|
||||
@implementation RCTClipRectTests
|
||||
|
||||
- (void)testLandscapeSourceLandscapeTarget
|
||||
{
|
||||
|
@ -109,6 +106,18 @@ RCTAssertEqualSizes(a.size, b.size); \
|
|||
|
||||
{
|
||||
CGRect expected = {{0, -37.5}, {10, 100}};
|
||||
CGRect result = RCTClipRect(content, 2, target, 2, UIViewContentModeScaleAspectFill);
|
||||
RCTAssertEqualRects(expected, result);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testRounding
|
||||
{
|
||||
CGSize content = {10, 100};
|
||||
CGSize target = {20, 50};
|
||||
|
||||
{
|
||||
CGRect expected = {{0, -38}, {10, 100}};
|
||||
CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFill);
|
||||
RCTAssertEqualRects(expected, result);
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* 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>
|
||||
#import "RCTUtils.h"
|
||||
#import "RCTNetworking.h"
|
||||
|
||||
extern BOOL RCTIsGzippedData(NSData *data);
|
||||
|
||||
@interface RCTNetworking (Private)
|
||||
|
||||
- (void)buildRequest:(NSDictionary *)query
|
||||
completionBlock:(void (^)(NSURLRequest *request))block;
|
||||
|
||||
@end
|
||||
|
||||
@interface RCTGzipTests : XCTestCase
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTGzipTests
|
||||
|
||||
- (void)testGzip
|
||||
{
|
||||
//set up data
|
||||
NSString *inputString = @"Hello World!";
|
||||
NSData *inputData = [inputString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
//compress
|
||||
NSData *outputData = RCTGzipData(inputData, -1);
|
||||
XCTAssertTrue(RCTIsGzippedData(outputData));
|
||||
}
|
||||
|
||||
- (void)testDontRezipZippedData
|
||||
{
|
||||
//set up data
|
||||
NSString *inputString = @"Hello World!";
|
||||
NSData *inputData = [inputString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
//compress
|
||||
NSData *compressedData = RCTGzipData(inputData, -1);
|
||||
inputString = [[NSString alloc] initWithData:compressedData encoding:NSUTF8StringEncoding];
|
||||
|
||||
//compress again
|
||||
NSData *outputData = RCTGzipData(inputData, -1);
|
||||
NSString *outputString = [[NSString alloc] initWithData:outputData encoding:NSUTF8StringEncoding];
|
||||
XCTAssertEqualObjects(outputString, inputString);
|
||||
}
|
||||
|
||||
- (void)testRequestBodyEncoding
|
||||
{
|
||||
NSDictionary *query = @{
|
||||
@"url": @"http://example.com",
|
||||
@"method": @"POST",
|
||||
@"data": @{@"string": @"Hello World"},
|
||||
@"headers": @{@"Content-Encoding": @"gzip"},
|
||||
};
|
||||
|
||||
RCTNetworking *networker = [[RCTNetworking alloc] init];
|
||||
__block NSURLRequest *request = nil;
|
||||
[networker buildRequest:query completionBlock:^(NSURLRequest *_request) {
|
||||
request = _request;
|
||||
}];
|
||||
|
||||
XCTAssertNotNil(request);
|
||||
XCTAssertNotNil(request.HTTPBody);
|
||||
XCTAssertTrue(RCTIsGzippedData(request.HTTPBody));
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* 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>
|
||||
#import "RCTUtils.h"
|
||||
|
||||
@interface RCTJSONTests : XCTestCase
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTJSONTests
|
||||
|
||||
- (void)testEncodingObject
|
||||
{
|
||||
NSDictionary *obj = @{@"foo": @"bar"};
|
||||
NSString *json = @"{\"foo\":\"bar\"}";
|
||||
XCTAssertEqualObjects(json, RCTJSONStringify(obj, NULL));
|
||||
}
|
||||
|
||||
- (void)testEncodingArray
|
||||
{
|
||||
NSArray *array = @[@"foo", @"bar"];
|
||||
NSString *json = @"[\"foo\",\"bar\"]";
|
||||
XCTAssertEqualObjects(json, RCTJSONStringify(array, NULL));
|
||||
}
|
||||
|
||||
- (void)testEncodingString
|
||||
{
|
||||
NSString *text = @"Hello\nWorld";
|
||||
NSString *json = @"\"Hello\\nWorld\"";
|
||||
XCTAssertEqualObjects(json, RCTJSONStringify(text, NULL));
|
||||
}
|
||||
|
||||
- (void)testDecodingObject
|
||||
{
|
||||
NSDictionary *obj = @{@"foo": @"bar"};
|
||||
NSString *json = @"{\"foo\":\"bar\"}";
|
||||
XCTAssertEqualObjects(obj, RCTJSONParse(json, NULL));
|
||||
}
|
||||
|
||||
- (void)testDecodingArray
|
||||
{
|
||||
NSArray *array = @[@"foo", @"bar"];
|
||||
NSString *json = @"[\"foo\",\"bar\"]";
|
||||
XCTAssertEqualObjects(array, RCTJSONParse(json, NULL));
|
||||
}
|
||||
|
||||
- (void)testDecodingString
|
||||
{
|
||||
NSString *text = @"Hello\nWorld";
|
||||
NSString *json = @"\"Hello\\nWorld\"";
|
||||
XCTAssertEqualObjects(text, RCTJSONParse(json, NULL));
|
||||
}
|
||||
|
||||
- (void)testDecodingMutableArray
|
||||
{
|
||||
NSString *json = @"[1,2,3]";
|
||||
NSMutableArray *array = RCTJSONParseMutable(json, NULL);
|
||||
XCTAssertNoThrow([array addObject:@4]);
|
||||
XCTAssertEqualObjects(array, (@[@1, @2, @3, @4]));
|
||||
}
|
||||
|
||||
- (void)testLeadingWhitespace
|
||||
{
|
||||
NSDictionary *obj = @{@"foo": @"bar"};
|
||||
NSString *json = @" \r\n\t{\"foo\":\"bar\"}";
|
||||
XCTAssertEqualObjects(obj, RCTJSONParse(json, NULL));
|
||||
}
|
||||
|
||||
@end
|
|
@ -58,19 +58,19 @@ var WebViewExample = React.createClass({
|
|||
return (
|
||||
<View style={[styles.container]}>
|
||||
<View style={[styles.addressBarRow]}>
|
||||
<TouchableOpacity onPress={this.goBack}>
|
||||
<View style={this.state.backButtonEnabled ? styles.navButton : styles.disabledButton}>
|
||||
<Text>
|
||||
{'<'}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={this.goBack}
|
||||
style={this.state.backButtonEnabled ? styles.navButton : styles.disabledButton}>
|
||||
<Text>
|
||||
{'<'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={this.goForward}>
|
||||
<View style={this.state.forwardButtonEnabled ? styles.navButton : styles.disabledButton}>
|
||||
<Text>
|
||||
{'>'}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={this.goForward}
|
||||
style={this.state.forwardButtonEnabled ? styles.navButton : styles.disabledButton}>
|
||||
<Text>
|
||||
{'>'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TextInput
|
||||
ref={TEXT_INPUT_REF}
|
||||
|
|
|
@ -128,6 +128,7 @@ class FormUploader extends React.Component {
|
|||
super(props);
|
||||
this.state = {
|
||||
isUploading: false,
|
||||
uploadProgress: null,
|
||||
randomPhoto: null,
|
||||
textParams: [],
|
||||
};
|
||||
|
@ -217,6 +218,14 @@ class FormUploader extends React.Component {
|
|||
this.state.textParams.forEach(
|
||||
(param) => formdata.append(param.name, param.value)
|
||||
);
|
||||
if (xhr.upload) {
|
||||
xhr.upload.onprogress = (event) => {
|
||||
console.log('upload onprogress', event);
|
||||
if (event.lengthComputable) {
|
||||
this.setState({uploadProgress: event.loaded / event.total});
|
||||
}
|
||||
};
|
||||
}
|
||||
xhr.send(formdata);
|
||||
this.setState({isUploading: true});
|
||||
}
|
||||
|
@ -251,6 +260,10 @@ class FormUploader extends React.Component {
|
|||
</View>
|
||||
));
|
||||
var uploadButtonLabel = this.state.isUploading ? 'Uploading...' : 'Upload';
|
||||
var uploadProgress = this.state.uploadProgress;
|
||||
if (uploadProgress !== null) {
|
||||
uploadButtonLabel += ' ' + Math.round(uploadProgress * 100) + '%';
|
||||
}
|
||||
var uploadButton = (
|
||||
<View style={styles.uploadButtonBox}>
|
||||
<Text style={styles.uploadButtonLabel}>{uploadButtonLabel}</Text>
|
||||
|
|
|
@ -55,6 +55,7 @@ var createExamplePage = function(title: ?string, exampleModule: ExampleModule)
|
|||
var result = example.render(null);
|
||||
if (result) {
|
||||
renderedComponent = result;
|
||||
result.props.navigator = this.props.navigator;
|
||||
}
|
||||
(React: Object).render = originalRender;
|
||||
(React: Object).renderComponent = originalRenderComponent;
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#import "JSContextRef.h"
|
||||
|
||||
extern "C" {
|
||||
|
||||
JSValueRef nativeProfilerStart(
|
||||
JSContextRef ctx,
|
||||
JSObjectRef function,
|
||||
JSObjectRef thisObject,
|
||||
size_t argumentCount,
|
||||
const JSValueRef arguments[],
|
||||
JSValueRef *exception);
|
||||
|
||||
JSValueRef nativeProfilerEnd(
|
||||
JSContextRef ctx,
|
||||
JSObjectRef function,
|
||||
JSObjectRef thisObject,
|
||||
size_t argumentCount,
|
||||
const JSValueRef arguments[],
|
||||
JSValueRef *exception);
|
||||
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
//#include "config.h"
|
||||
|
||||
#include "JSCLegacyProfiler.h"
|
||||
|
||||
#include "APICast.h"
|
||||
#include "LegacyProfiler.h"
|
||||
#include "OpaqueJSString.h"
|
||||
#include "JSProfilerPrivate.h"
|
||||
#include "JSStringRef.h"
|
||||
|
||||
#include <yajl/yajl_gen.h>
|
||||
|
||||
#define GEN_AND_CHECK(expr) \
|
||||
do { \
|
||||
yajl_gen_status GEN_AND_CHECK_status = (expr); \
|
||||
if (GEN_AND_CHECK_status != yajl_gen_status_ok) { \
|
||||
return GEN_AND_CHECK_status; \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
static inline yajl_gen_status yajl_gen_cstring(yajl_gen gen, const char *str) {
|
||||
return yajl_gen_string(gen, (const unsigned char*)str, strlen(str));
|
||||
}
|
||||
|
||||
static yajl_gen_status append_children_array_json(yajl_gen gen, const JSC::ProfileNode *node);
|
||||
static yajl_gen_status append_node_json(yajl_gen gen, const JSC::ProfileNode *node);
|
||||
|
||||
static yajl_gen_status append_root_json(yajl_gen gen, const JSC::Profile *profile) {
|
||||
GEN_AND_CHECK(yajl_gen_map_open(gen));
|
||||
GEN_AND_CHECK(yajl_gen_cstring(gen, "rootNodes"));
|
||||
GEN_AND_CHECK(append_children_array_json(gen, profile->head()));
|
||||
GEN_AND_CHECK(yajl_gen_map_close(gen));
|
||||
|
||||
return yajl_gen_status_ok;
|
||||
}
|
||||
|
||||
static yajl_gen_status append_children_array_json(yajl_gen gen, const JSC::ProfileNode *node) {
|
||||
GEN_AND_CHECK(yajl_gen_array_open(gen));
|
||||
for (RefPtr<JSC::ProfileNode> child : node->children()) {
|
||||
GEN_AND_CHECK(append_node_json(gen, child.get()));
|
||||
}
|
||||
GEN_AND_CHECK(yajl_gen_array_close(gen));
|
||||
|
||||
return yajl_gen_status_ok;
|
||||
}
|
||||
|
||||
static yajl_gen_status append_node_json(yajl_gen gen, const JSC::ProfileNode *node) {
|
||||
GEN_AND_CHECK(yajl_gen_map_open(gen));
|
||||
GEN_AND_CHECK(yajl_gen_cstring(gen, "id"));
|
||||
GEN_AND_CHECK(yajl_gen_integer(gen, node->id()));
|
||||
|
||||
if (!node->functionName().isEmpty()) {
|
||||
GEN_AND_CHECK(yajl_gen_cstring(gen, "functionName"));
|
||||
GEN_AND_CHECK(yajl_gen_cstring(gen, node->functionName().utf8().data()));
|
||||
}
|
||||
|
||||
if (!node->url().isEmpty()) {
|
||||
GEN_AND_CHECK(yajl_gen_cstring(gen, "url"));
|
||||
GEN_AND_CHECK(yajl_gen_cstring(gen, node->url().utf8().data()));
|
||||
GEN_AND_CHECK(yajl_gen_cstring(gen, "lineNumber"));
|
||||
GEN_AND_CHECK(yajl_gen_integer(gen, node->lineNumber()));
|
||||
GEN_AND_CHECK(yajl_gen_cstring(gen, "columnNumber"));
|
||||
GEN_AND_CHECK(yajl_gen_integer(gen, node->columnNumber()));
|
||||
}
|
||||
|
||||
GEN_AND_CHECK(yajl_gen_cstring(gen, "calls"));
|
||||
GEN_AND_CHECK(yajl_gen_array_open(gen));
|
||||
for (const JSC::ProfileNode::Call &call : node->calls()) {
|
||||
GEN_AND_CHECK(yajl_gen_map_open(gen));
|
||||
GEN_AND_CHECK(yajl_gen_cstring(gen, "startTime"));
|
||||
GEN_AND_CHECK(yajl_gen_double(gen, call.startTime()));
|
||||
GEN_AND_CHECK(yajl_gen_cstring(gen, "totalTime"));
|
||||
GEN_AND_CHECK(yajl_gen_double(gen, call.totalTime()));
|
||||
GEN_AND_CHECK(yajl_gen_map_close(gen));
|
||||
}
|
||||
GEN_AND_CHECK(yajl_gen_array_close(gen));
|
||||
|
||||
if (!node->children().isEmpty()) {
|
||||
GEN_AND_CHECK(yajl_gen_cstring(gen, "children"));
|
||||
GEN_AND_CHECK(append_children_array_json(gen, node));
|
||||
}
|
||||
|
||||
GEN_AND_CHECK(yajl_gen_map_close(gen));
|
||||
|
||||
return yajl_gen_status_ok;
|
||||
}
|
||||
|
||||
static char *render_error_code(yajl_gen_status status) {
|
||||
char err[1024];
|
||||
snprintf(err, sizeof(err), "{\"error\": %d}", (int)status);
|
||||
return strdup(err);
|
||||
}
|
||||
|
||||
static char *convert_to_json(const JSC::Profile *profile) {
|
||||
yajl_gen_status status;
|
||||
yajl_gen gen = yajl_gen_alloc(NULL);
|
||||
|
||||
status = append_root_json(gen, profile);
|
||||
if (status != yajl_gen_status_ok) {
|
||||
yajl_gen_free(gen);
|
||||
return render_error_code(status);
|
||||
}
|
||||
|
||||
const unsigned char *buf;
|
||||
size_t buf_size;
|
||||
status = yajl_gen_get_buf(gen, &buf, &buf_size);
|
||||
if (status != yajl_gen_status_ok) {
|
||||
yajl_gen_free(gen);
|
||||
return render_error_code(status);
|
||||
}
|
||||
|
||||
char *json_copy = strdup((const char*)buf);
|
||||
yajl_gen_free(gen);
|
||||
return json_copy;
|
||||
}
|
||||
|
||||
static char *JSEndProfilingAndRender(JSContextRef ctx, JSStringRef title)
|
||||
{
|
||||
JSC::ExecState *exec = toJS(ctx);
|
||||
JSC::LegacyProfiler *profiler = JSC::LegacyProfiler::profiler();
|
||||
RefPtr<JSC::Profile> rawProfile = profiler->stopProfiling(exec, title->string());
|
||||
return convert_to_json(rawProfile.get());
|
||||
}
|
||||
|
||||
JSValueRef nativeProfilerStart(
|
||||
JSContextRef ctx,
|
||||
JSObjectRef function,
|
||||
JSObjectRef thisObject,
|
||||
size_t argumentCount,
|
||||
const JSValueRef arguments[],
|
||||
JSValueRef *exception) {
|
||||
if (argumentCount < 1) {
|
||||
// Could raise an exception here.
|
||||
return JSValueMakeUndefined(ctx);
|
||||
}
|
||||
|
||||
JSStringRef title = JSValueToStringCopy(ctx, arguments[0], NULL);
|
||||
JSStartProfiling(ctx, title);
|
||||
JSStringRelease(title);
|
||||
return JSValueMakeUndefined(ctx);
|
||||
}
|
||||
|
||||
JSValueRef nativeProfilerEnd(
|
||||
JSContextRef ctx,
|
||||
JSObjectRef function,
|
||||
JSObjectRef thisObject,
|
||||
size_t argumentCount,
|
||||
const JSValueRef arguments[],
|
||||
JSValueRef *exception) {
|
||||
if (argumentCount < 1) {
|
||||
// Could raise an exception here.
|
||||
return JSValueMakeUndefined(ctx);
|
||||
}
|
||||
|
||||
JSStringRef title = JSValueToStringCopy(ctx, arguments[0], NULL);
|
||||
char *rendered = JSEndProfilingAndRender(ctx, title);
|
||||
JSStringRelease(title);
|
||||
JSStringRef profile = JSStringCreateWithUTF8CString(rendered);
|
||||
free(rendered);
|
||||
return JSValueMakeString(ctx, profile);
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
HEADER_PATHS := `find ./tmp/JavaScriptCore -name '*.h' | xargs -I{} dirname {} | uniq | xargs -I{} echo "-I {}"`
|
||||
CERT ?= "iPhone Developer"
|
||||
|
||||
ios8: prepare build generate
|
||||
|
||||
prepare: clean create download
|
||||
|
||||
build: x86_64 arm64 armv7
|
||||
|
||||
generate: lipo codesign
|
||||
|
||||
clean:
|
||||
@rm -rf tmp/ /tmp/RCTJSCProfiler
|
||||
|
||||
lipo:
|
||||
lipo -create -output /tmp/RCTJSCProfiler/RCTJSCProfiler.ios8.dylib ./tmp/RCTJSCProfiler_x86_64 ./tmp/RCTJSCProfiler_arm64 ./tmp/RCTJSCProfiler_armv7
|
||||
|
||||
codesign:
|
||||
codesign -f -s ${CERT} /tmp/RCTJSCProfiler/RCTJSCProfiler.ios8.dylib
|
||||
|
||||
create:
|
||||
mkdir -p ./tmp /tmp/RCTJSCProfiler/ ./tmp/CoreFoundation ./tmp/Foundation
|
||||
for file in ./tmp/CoreFoundation/CFUserNotification.h ./tmp/CoreFoundation/CFXMLNode.h ./tmp/CoreFoundation/CFXMLParser.h ./tmp/Foundation/Foundation.h; do echo '' > "$$file"; done
|
||||
|
||||
download: wtf jsc webcore yajl
|
||||
|
||||
wtf:
|
||||
curl -o tmp/WTF.tar.gz http://www.opensource.apple.com/tarballs/WTF/WTF-7600.1.24.tar.gz
|
||||
tar -zxvf tmp/WTF.tar.gz -C tmp
|
||||
|
||||
jsc:
|
||||
curl -o tmp/JSC.tar.gz http://www.opensource.apple.com/tarballs/JavaScriptCore/JavaScriptCore-7600.1.17.tar.gz
|
||||
tar -zxvf tmp/JSC.tar.gz -C tmp
|
||||
mv ./tmp/JavaScriptCore-7600.1.17 ./tmp/JavaScriptCore
|
||||
python ./tmp/JavaScriptCore/generate-bytecode-files --bytecodes_h ./tmp/JavaScriptCore/Bytecodes.h ./tmp/JavaScriptCore/bytecode/BytecodeList.json
|
||||
|
||||
webcore:
|
||||
curl -o tmp/WebCore.tar.gz http://www.opensource.apple.com/tarballs/WebCore/WebCore-7600.1.25.tar.gz
|
||||
tar -zxvf tmp/WebCore.tar.gz -C tmp
|
||||
|
||||
yajl:
|
||||
curl -o tmp/yajl.tar.gz https://codeload.github.com/lloyd/yajl/tar.gz/2.1.0
|
||||
tar -zxvf tmp/yajl.tar.gz -C tmp
|
||||
mkdir -p ./tmp/yajl-2.1.0/build && cd ./tmp/yajl-2.1.0/build && cmake .. && make
|
||||
echo `find . -name '*.c'`
|
||||
cd ./tmp/yajl-2.1.0/src && \
|
||||
clang -arch arm64 -arch armv7 -std=c99 \
|
||||
-I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/include/ \
|
||||
-I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/include/machine \
|
||||
-I ../build/yajl-2.1.0/include \
|
||||
-c `find . -name '*.c'`
|
||||
libtool -static -o ./tmp/yajl.a `find ./tmp/yajl-2.1.0/src/ -name '*.o'`
|
||||
|
||||
x86_64:
|
||||
clang -w -dynamiclib -o ./tmp/RCTJSCProfiler_x86_64 -std=c++11 \
|
||||
-install_name RCTJSCProfiler.ios8.dylib \
|
||||
-include ./tmp/JavaScriptCore/config.h \
|
||||
-I ./tmp \
|
||||
-I ./tmp/WebCore-7600.1.25/icu \
|
||||
-I ./tmp/WTF-7600.1.24 \
|
||||
-I ./tmp/yajl-2.1.0/build/yajl-2.1.0/include \
|
||||
-DNDEBUG=1\
|
||||
-miphoneos-version-min=8.0 \
|
||||
-L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib \
|
||||
-L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/system \
|
||||
${HEADER_PATHS} \
|
||||
-undefined dynamic_lookup \
|
||||
./JSCLegacyProfiler.mm ./tmp/yajl-2.1.0/build/yajl-2.1.0/lib/libyajl_s.a
|
||||
|
||||
arm64:
|
||||
echo $(HEADER_PATHS)
|
||||
clang -w -dynamiclib -o ./tmp/RCTJSCProfiler_arm64 -std=c++11 \
|
||||
-install_name RCTJSCProfiler.ios8.dylib \
|
||||
-arch arm64 \
|
||||
-include ./tmp/JavaScriptCore/config.h \
|
||||
-I ./tmp \
|
||||
-I ./tmp/WebCore-7600.1.25/icu \
|
||||
-I ./tmp/WTF-7600.1.24 \
|
||||
-I ./tmp/yajl-2.1.0/build/yajl-2.1.0/include \
|
||||
-I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/include \
|
||||
-I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/include/machine \
|
||||
-DNDEBUG=1\
|
||||
-miphoneos-version-min=8.0 \
|
||||
-L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/lib \
|
||||
-L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/lib/system \
|
||||
${HEADER_PATHS} \
|
||||
-undefined dynamic_lookup \
|
||||
./JSCLegacyProfiler.mm ./tmp/yajl.a
|
||||
|
||||
armv7:
|
||||
clang -w -dynamiclib -o ./tmp/RCTJSCProfiler_armv7 -std=c++11 \
|
||||
-install_name RCTJSCProfiler.ios8.dylib \
|
||||
-arch armv7 \
|
||||
-include ./tmp/JavaScriptCore/config.h \
|
||||
-I ./tmp \
|
||||
-I ./tmp/WebCore-7600.1.25/icu \
|
||||
-I ./tmp/WTF-7600.1.24 \
|
||||
-I ./tmp/yajl-2.1.0/build/yajl-2.1.0/include \
|
||||
-I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/include \
|
||||
-DNDEBUG=1\
|
||||
-miphoneos-version-min=8.0 \
|
||||
-L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/lib \
|
||||
-L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/lib/system \
|
||||
${HEADER_PATHS} \
|
||||
-undefined dynamic_lookup \
|
||||
./JSCLegacyProfiler.mm ./tmp/yajl.a
|
||||
|
||||
.PHONY: ios8
|
|
@ -1177,7 +1177,7 @@ var parallel = function(
|
|||
}
|
||||
|
||||
animations.forEach((animation, idx) => {
|
||||
animation.start(endResult => {
|
||||
var cb = function(endResult) {
|
||||
hasEnded[idx] = true;
|
||||
doneCount++;
|
||||
if (doneCount === animations.length) {
|
||||
|
@ -1189,7 +1189,13 @@ var parallel = function(
|
|||
if (!endResult.finished && stopTogether) {
|
||||
result.stop();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!animation) {
|
||||
cb({finished: true});
|
||||
} else {
|
||||
animation.start(cb);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -205,6 +205,16 @@ describe('Animated Parallel', () => {
|
|||
expect(cb).toBeCalledWith({finished: true});
|
||||
});
|
||||
|
||||
it('works with an empty element in array', () => {
|
||||
var anim1 = {start: jest.genMockFunction()};
|
||||
var cb = jest.genMockFunction();
|
||||
Animated.parallel([null, anim1]).start(cb);
|
||||
|
||||
expect(anim1.start).toBeCalled();
|
||||
anim1.start.mock.calls[0][0]({finished: true});
|
||||
|
||||
expect(cb).toBeCalledWith({finished: true});
|
||||
});
|
||||
|
||||
it('parellelizes well', () => {
|
||||
var anim1 = {start: jest.genMockFunction()};
|
||||
|
|
|
@ -31,8 +31,8 @@ var invariant = require('invariant');
|
|||
var requireNativeComponent = require('requireNativeComponent');
|
||||
|
||||
var onlyMultiline = {
|
||||
onSelectionChange: true,
|
||||
onTextInput: true,
|
||||
onSelectionChange: true, // not supported in Open Source yet
|
||||
onTextInput: true, // not supported in Open Source yet
|
||||
children: true,
|
||||
};
|
||||
|
||||
|
@ -64,10 +64,6 @@ var viewConfigAndroid = {
|
|||
var RCTTextView = requireNativeComponent('RCTTextView', null);
|
||||
var RCTTextField = requireNativeComponent('RCTTextField', null);
|
||||
|
||||
type DefaultProps = {
|
||||
bufferDelay: number;
|
||||
};
|
||||
|
||||
type Event = Object;
|
||||
|
||||
/**
|
||||
|
@ -77,30 +73,29 @@ type Event = Object;
|
|||
* types, such as a numeric keypad.
|
||||
*
|
||||
* The simplest use case is to plop down a `TextInput` and subscribe to the
|
||||
* `onChangeText` events to read the user input. There are also other events, such
|
||||
* as `onSubmitEditing` and `onFocus` that can be subscribed to. A simple
|
||||
* `onChangeText` events to read the user input. There are also other events,
|
||||
* such as `onSubmitEditing` and `onFocus` that can be subscribed to. A simple
|
||||
* example:
|
||||
*
|
||||
* ```
|
||||
* <View>
|
||||
* <TextInput
|
||||
* style={{height: 40, borderColor: 'gray', borderWidth: 1}}
|
||||
* onChangeText={(text) => this.setState({input: text})}
|
||||
* onChangeText={(text) => this.setState({text})}
|
||||
* value={this.state.text}
|
||||
* />
|
||||
* <Text>{'user input: ' + this.state.input}</Text>
|
||||
* </View>
|
||||
* ```
|
||||
*
|
||||
* The `value` prop can be used to set the value of the input in order to make
|
||||
* the state of the component clear, but <TextInput> does not behave as a true
|
||||
* controlled component by default because all operations are asynchronous.
|
||||
* Setting `value` once is like setting the default value, but you can change it
|
||||
* continuously based on `onChangeText` events as well. If you really want to
|
||||
* force the component to always revert to the value you are setting, you can
|
||||
* set `controlled={true}`.
|
||||
* Note that some props are only available with multiline={true/false}:
|
||||
*
|
||||
* The `multiline` prop is not supported in all releases, and some props are
|
||||
* multiline only.
|
||||
* var onlyMultiline = {
|
||||
* onSelectionChange: true, // not supported in Open Source yet
|
||||
* onTextInput: true, // not supported in Open Source yet
|
||||
* children: true,
|
||||
* };
|
||||
*
|
||||
* var notMultiline = {
|
||||
* onSubmitEditing: true,
|
||||
* };
|
||||
*/
|
||||
|
||||
var TextInput = React.createClass({
|
||||
|
@ -179,6 +174,11 @@ var TextInput = React.createClass({
|
|||
'done',
|
||||
'emergency-call',
|
||||
]),
|
||||
/**
|
||||
* Limits the maximum number of characters that can be entered. Use this
|
||||
* instead of implementing the logic in JS to avoid flicker.
|
||||
*/
|
||||
maxLength: PropTypes.number,
|
||||
/**
|
||||
* If true, the keyboard disables the return key when there is no text and
|
||||
* automatically enables it when there is text. Default value is false.
|
||||
|
@ -236,22 +236,21 @@ var TextInput = React.createClass({
|
|||
*/
|
||||
selectionState: PropTypes.instanceOf(DocumentSelectionState),
|
||||
/**
|
||||
* The default value for the text input
|
||||
* The value to show for the text input. TextInput is a controlled
|
||||
* component, which means the native value will be forced to match this
|
||||
* value prop if provided. For most uses this works great, but in some
|
||||
* cases this may cause flickering - one common cause is preventing edits
|
||||
* by keeping value the same. In addition to simply setting the same value,
|
||||
* either set `editable={false}`, or set/update `maxLength` to prevent
|
||||
* unwanted edits without flicker.
|
||||
*/
|
||||
value: PropTypes.string,
|
||||
/**
|
||||
* This helps avoid drops characters due to race conditions between JS and
|
||||
* the native text input. The default should be fine, but if you're
|
||||
* potentially doing very slow operations on every keystroke then you may
|
||||
* want to try increasing this.
|
||||
* Provides an initial value that will change when the user starts typing.
|
||||
* Useful for simple use-cases where you don't want to deal with listening
|
||||
* to events and updating the value prop to keep the controlled state in sync.
|
||||
*/
|
||||
bufferDelay: PropTypes.number,
|
||||
/**
|
||||
* If you really want this to behave as a controlled component, you can set
|
||||
* this true, but you will probably see flickering, dropped keystrokes,
|
||||
* and/or laggy typing, depending on how you process onChange events.
|
||||
*/
|
||||
controlled: PropTypes.bool,
|
||||
defaultValue: PropTypes.string,
|
||||
/**
|
||||
* When the clear button should appear on the right side of the text view
|
||||
*/
|
||||
|
@ -297,16 +296,9 @@ var TextInput = React.createClass({
|
|||
React.findNodeHandle(this.refs.input);
|
||||
},
|
||||
|
||||
getDefaultProps: function(): DefaultProps {
|
||||
return {
|
||||
bufferDelay: 100,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
mostRecentEventCounter: 0,
|
||||
bufferedValue: this.props.value,
|
||||
mostRecentEventCount: 0,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -346,52 +338,6 @@ var TextInput = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_bufferTimeout: (undefined: ?number),
|
||||
|
||||
componentWillReceiveProps: function(newProps: {value: any}) {
|
||||
if (newProps.value !== this.props.value) {
|
||||
if (!this.isFocused()) {
|
||||
// Set the value immediately if the input is not focused since that
|
||||
// means there is no risk of the user typing immediately.
|
||||
this.setState({bufferedValue: newProps.value});
|
||||
} else {
|
||||
// The following clear and setTimeout buffers the value such that if more
|
||||
// characters are typed in quick succession, generating new values, the
|
||||
// out of date values will get cancelled before they are ever sent to
|
||||
// native.
|
||||
//
|
||||
// If we don't do this, it's likely the out of date values will blow
|
||||
// away recently typed characters in the native input that JS was not
|
||||
// yet aware of (since it is informed asynchronously), then the next
|
||||
// character will be appended to the older value, dropping the
|
||||
// characters in between. Here is a potential sequence of events
|
||||
// (recall we have multiple independently serial, interleaved queues):
|
||||
//
|
||||
// 1) User types 'R' => send 'R' to JS queue.
|
||||
// 2) User types 'e' => send 'Re' to JS queue.
|
||||
// 3) JS processes 'R' and sends 'R' back to native.
|
||||
// 4) Native recieves 'R' and changes input from 'Re' back to 'R'.
|
||||
// 5) User types 'a' => send 'Ra' to JS queue.
|
||||
// 6) JS processes 'Re' and sends 'Re' back to native.
|
||||
// 7) Native recieves 'Re' and changes input from 'R' back to 'Re'.
|
||||
// 8) JS processes 'Ra' and sends 'Ra' back to native.
|
||||
// 9) Native recieves final 'Ra' from JS - 'e' has been dropped!
|
||||
//
|
||||
// This isn't 100% foolproop (e.g. if it takes longer than
|
||||
// `props.bufferDelay` ms to process one keystroke), and there are of
|
||||
// course other potential algorithms to deal with this, but this is a
|
||||
// simple solution that seems to reduce the chance of dropped characters
|
||||
// drastically without compromising native input responsiveness (e.g. by
|
||||
// introducing delay from a synchronization protocol).
|
||||
this.clearTimeout(this._bufferTimeout);
|
||||
this._bufferTimeout = this.setTimeout(
|
||||
() => this.setState({bufferedValue: newProps.value}),
|
||||
this.props.bufferDelay
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getChildContext: function(): Object {
|
||||
return {isInAParentText: true};
|
||||
},
|
||||
|
@ -408,12 +354,17 @@ var TextInput = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_getText: function(): ?string {
|
||||
return typeof this.props.value === 'string' ?
|
||||
this.props.value :
|
||||
this.props.defaultValue;
|
||||
},
|
||||
|
||||
_renderIOS: function() {
|
||||
var textContainer;
|
||||
|
||||
var props = Object.assign({},this.props);
|
||||
var props = Object.assign({}, this.props);
|
||||
props.style = [styles.input, this.props.style];
|
||||
|
||||
if (!props.multiline) {
|
||||
for (var propKey in onlyMultiline) {
|
||||
if (props[propKey]) {
|
||||
|
@ -430,7 +381,8 @@ var TextInput = React.createClass({
|
|||
onBlur={this._onBlur}
|
||||
onChange={this._onChange}
|
||||
onSelectionChangeShouldSetResponder={() => true}
|
||||
text={this.state.bufferedValue}
|
||||
text={this._getText()}
|
||||
mostRecentEventCount={this.state.mostRecentEventCount}
|
||||
/>;
|
||||
} else {
|
||||
for (var propKey in notMultiline) {
|
||||
|
@ -459,14 +411,14 @@ var TextInput = React.createClass({
|
|||
ref="input"
|
||||
{...props}
|
||||
children={children}
|
||||
mostRecentEventCounter={this.state.mostRecentEventCounter}
|
||||
mostRecentEventCount={this.state.mostRecentEventCount}
|
||||
onFocus={this._onFocus}
|
||||
onBlur={this._onBlur}
|
||||
onChange={this._onChange}
|
||||
onSelectionChange={this._onSelectionChange}
|
||||
onTextInput={this._onTextInput}
|
||||
onSelectionChangeShouldSetResponder={emptyFunction.thatReturnsTrue}
|
||||
text={this.state.bufferedValue}
|
||||
text={this._getText()}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -516,7 +468,7 @@ var TextInput = React.createClass({
|
|||
password={this.props.password || this.props.secureTextEntry}
|
||||
placeholder={this.props.placeholder}
|
||||
placeholderTextColor={this.props.placeholderTextColor}
|
||||
text={this.state.bufferedValue}
|
||||
text={this._getText()}
|
||||
underlineColorAndroid={this.props.underlineColorAndroid}
|
||||
children={children}
|
||||
/>;
|
||||
|
@ -543,11 +495,20 @@ var TextInput = React.createClass({
|
|||
},
|
||||
|
||||
_onChange: function(event: Event) {
|
||||
if (this.props.controlled && event.nativeEvent.text !== this.props.value) {
|
||||
this.refs.input.setNativeProps({text: this.props.value});
|
||||
}
|
||||
var text = event.nativeEvent.text;
|
||||
var eventCount = event.nativeEvent.eventCount;
|
||||
this.props.onChange && this.props.onChange(event);
|
||||
this.props.onChangeText && this.props.onChangeText(event.nativeEvent.text);
|
||||
this.props.onChangeText && this.props.onChangeText(text);
|
||||
this.setState({mostRecentEventCount: eventCount}, () => {
|
||||
// This is a controlled component, so make sure to force the native value
|
||||
// to match. Most usage shouldn't need this, but if it does this will be
|
||||
// more correct but might flicker a bit and/or cause the cursor to jump.
|
||||
if (text !== this.props.value && typeof this.props.value === 'string') {
|
||||
this.refs.input.setNativeProps({
|
||||
text: this.props.value,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_onBlur: function(event: Event) {
|
||||
|
@ -567,10 +528,6 @@ var TextInput = React.createClass({
|
|||
|
||||
_onTextInput: function(event: Event) {
|
||||
this.props.onTextInput && this.props.onTextInput(event);
|
||||
var counter = event.nativeEvent.eventCounter;
|
||||
if (counter > this.state.mostRecentEventCounter) {
|
||||
this.setState({mostRecentEventCounter: counter});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -181,6 +181,10 @@ var TouchableHighlight = React.createClass({
|
|||
},
|
||||
|
||||
_showUnderlay: function() {
|
||||
if (!this.isMounted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refs[UNDERLAY_REF].setNativeProps(this.state.activeUnderlayProps);
|
||||
this.refs[CHILD_REF].setNativeProps(this.state.activeProps);
|
||||
this.props.onShowUnderlay && this.props.onShowUnderlay();
|
||||
|
|
|
@ -12,19 +12,16 @@
|
|||
|
||||
// Note (avik): add @flow when Flow supports spread properties in propTypes
|
||||
|
||||
var Animated = require('Animated');
|
||||
var NativeMethodsMixin = require('NativeMethodsMixin');
|
||||
var POPAnimationMixin = require('POPAnimationMixin');
|
||||
var React = require('React');
|
||||
var TimerMixin = require('react-timer-mixin');
|
||||
var Touchable = require('Touchable');
|
||||
var TouchableWithoutFeedback = require('TouchableWithoutFeedback');
|
||||
|
||||
var cloneWithProps = require('cloneWithProps');
|
||||
var ensureComponentIsNative = require('ensureComponentIsNative');
|
||||
var ensurePositiveDelayProps = require('ensurePositiveDelayProps');
|
||||
var flattenStyle = require('flattenStyle');
|
||||
var keyOf = require('keyOf');
|
||||
var onlyChild = require('onlyChild');
|
||||
|
||||
/**
|
||||
* A wrapper for making views respond properly to touches.
|
||||
|
@ -52,7 +49,7 @@ var onlyChild = require('onlyChild');
|
|||
*/
|
||||
|
||||
var TouchableOpacity = React.createClass({
|
||||
mixins: [TimerMixin, Touchable.Mixin, NativeMethodsMixin, POPAnimationMixin],
|
||||
mixins: [TimerMixin, Touchable.Mixin, NativeMethodsMixin],
|
||||
|
||||
propTypes: {
|
||||
...TouchableWithoutFeedback.propTypes,
|
||||
|
@ -70,16 +67,17 @@ var TouchableOpacity = React.createClass({
|
|||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return this.touchableGetInitialState();
|
||||
return {
|
||||
...this.touchableGetInitialState(),
|
||||
anim: new Animated.Value(1),
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
ensurePositiveDelayProps(this.props);
|
||||
ensureComponentIsNative(this.refs[CHILD_REF]);
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
ensureComponentIsNative(this.refs[CHILD_REF]);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
|
@ -87,22 +85,10 @@ var TouchableOpacity = React.createClass({
|
|||
},
|
||||
|
||||
setOpacityTo: function(value) {
|
||||
if (POPAnimationMixin) {
|
||||
// Reset with animation if POP is available
|
||||
this.stopAllAnimations();
|
||||
var anim = {
|
||||
type: this.AnimationTypes.linear,
|
||||
property: this.AnimationProperties.opacity,
|
||||
duration: 0.15,
|
||||
toValue: value,
|
||||
};
|
||||
this.startAnimation(CHILD_REF, anim);
|
||||
} else {
|
||||
// Reset immediately if POP is unavailable
|
||||
this.refs[CHILD_REF].setNativeProps({
|
||||
opacity: value
|
||||
});
|
||||
}
|
||||
Animated.timing(
|
||||
this.state.anim,
|
||||
{toValue: value, duration: 150}
|
||||
).start();
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -161,25 +147,27 @@ var TouchableOpacity = React.createClass({
|
|||
_opacityInactive: function() {
|
||||
this.clearTimeout(this._hideTimeout);
|
||||
this._hideTimeout = null;
|
||||
var child = onlyChild(this.props.children);
|
||||
var childStyle = flattenStyle(child.props.style) || {};
|
||||
var childStyle = flattenStyle(this.props.style) || {};
|
||||
this.setOpacityTo(
|
||||
childStyle.opacity === undefined ? 1 : childStyle.opacity
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return cloneWithProps(onlyChild(this.props.children), {
|
||||
ref: CHILD_REF,
|
||||
accessible: true,
|
||||
testID: this.props.testID,
|
||||
onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder,
|
||||
onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest,
|
||||
onResponderGrant: this.touchableHandleResponderGrant,
|
||||
onResponderMove: this.touchableHandleResponderMove,
|
||||
onResponderRelease: this.touchableHandleResponderRelease,
|
||||
onResponderTerminate: this.touchableHandleResponderTerminate,
|
||||
});
|
||||
return (
|
||||
<Animated.View
|
||||
accessible={true}
|
||||
style={[this.props.style, {opacity: this.state.anim}]}
|
||||
testID={this.props.testID}
|
||||
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
|
||||
onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
|
||||
onResponderGrant={this.touchableHandleResponderGrant}
|
||||
onResponderMove={this.touchableHandleResponderMove}
|
||||
onResponderRelease={this.touchableHandleResponderRelease}
|
||||
onResponderTerminate={this.touchableHandleResponderTerminate}>
|
||||
{this.props.children}
|
||||
</Animated.View>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -191,6 +179,5 @@ var TouchableOpacity = React.createClass({
|
|||
*/
|
||||
var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
|
||||
|
||||
var CHILD_REF = keyOf({childRef: null});
|
||||
|
||||
module.exports = TouchableOpacity;
|
||||
|
|
|
@ -67,10 +67,10 @@ class NavigationContext {
|
|||
}
|
||||
}
|
||||
|
||||
emit(eventType: String, data: any): void {
|
||||
emit(eventType: String, data: any, didEmitCallback: ?Function): void {
|
||||
var emitter = this._eventEmitter;
|
||||
if (emitter) {
|
||||
emitter.emit(eventType, data);
|
||||
emitter.emit(eventType, data, didEmitCallback);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ class NavigationEventPool {
|
|||
this._list = [];
|
||||
}
|
||||
|
||||
get(type: String, target: Object, data: any): NavigationEvent {
|
||||
get(type: string, target: Object, data: any): NavigationEvent {
|
||||
var event;
|
||||
if (this._list.length > 0) {
|
||||
event = this._list.pop();
|
||||
|
@ -59,13 +59,13 @@ class NavigationEvent {
|
|||
_defaultPrevented: boolean;
|
||||
_disposed: boolean;
|
||||
_target: ?Object;
|
||||
_type: ?String;
|
||||
_type: ?string;
|
||||
|
||||
static pool(type: String, target: Object, data: any): NavigationEvent {
|
||||
static pool(type: string, target: Object, data: any): NavigationEvent {
|
||||
return _navigationEventPool.get(type, target, data);
|
||||
}
|
||||
|
||||
constructor(type: String, target: Object, data: any) {
|
||||
constructor(type: string, target: Object, data: any) {
|
||||
this._type = type;
|
||||
this._target = target;
|
||||
this._data = data;
|
||||
|
|
|
@ -31,8 +31,9 @@ var EventEmitter = require('EventEmitter');
|
|||
var NavigationEvent = require('NavigationEvent');
|
||||
|
||||
type EventParams = {
|
||||
eventType: String;
|
||||
data: any;
|
||||
didEmitCallback: ?Function;
|
||||
eventType: string;
|
||||
};
|
||||
|
||||
class NavigationEventEmitter extends EventEmitter {
|
||||
|
@ -47,22 +48,36 @@ class NavigationEventEmitter extends EventEmitter {
|
|||
this._target = target;
|
||||
}
|
||||
|
||||
emit(eventType: String, data: any): void {
|
||||
emit(
|
||||
eventType: string,
|
||||
data: any,
|
||||
didEmitCallback: ?Function
|
||||
): void {
|
||||
if (this._emitting) {
|
||||
// An event cycle that was previously created hasn't finished yet.
|
||||
// Put this event cycle into the queue and will finish them later.
|
||||
this._emitQueue.push({eventType, data});
|
||||
this._emitQueue.push({eventType, data, didEmitCallback});
|
||||
return;
|
||||
}
|
||||
|
||||
this._emitting = true;
|
||||
|
||||
var event = new NavigationEvent(eventType, this._target, data);
|
||||
super.emit(eventType, event);
|
||||
|
||||
// EventEmitter#emit only takes `eventType` as `String`. Casting `eventType`
|
||||
// to `String` to make @flow happy.
|
||||
super.emit(String(eventType), event);
|
||||
|
||||
if (typeof didEmitCallback === 'function') {
|
||||
didEmitCallback.call(this._target, event);
|
||||
}
|
||||
event.dispose();
|
||||
|
||||
this._emitting = false;
|
||||
|
||||
while (this._emitQueue.length) {
|
||||
var arg = this._emitQueue.shift();
|
||||
this.emit(arg.eventType, arg.data);
|
||||
this.emit(arg.eventType, arg.data, arg.didEmitCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,16 @@ class RouteStack {
|
|||
return this._routes.get(index);
|
||||
}
|
||||
|
||||
indexOf(route: any): number {
|
||||
return this._routes.indexOf(route);
|
||||
}
|
||||
|
||||
slice(begin: ?number, end: ?number): RouteStack {
|
||||
var routes = this._routes.slice(begin, end);
|
||||
var index = Math.min(this._index, routes.size - 1);
|
||||
return this._update(index, routes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new stack with the provided route appended,
|
||||
* starting at this stack size.
|
||||
|
@ -71,7 +81,7 @@ class RouteStack {
|
|||
list.slice(0, this._index + 1).push(route);
|
||||
});
|
||||
|
||||
return new RouteStack(routes.size - 1, routes);
|
||||
return this._update(routes.size - 1, routes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,7 +93,7 @@ class RouteStack {
|
|||
|
||||
// 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);
|
||||
return this._update(routes.size - 1, routes);
|
||||
}
|
||||
|
||||
jumpToIndex(index: number): RouteStack {
|
||||
|
@ -92,11 +102,7 @@ class RouteStack {
|
|||
'index out of bound'
|
||||
);
|
||||
|
||||
if (index === this._index) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return new RouteStack(index, this._routes);
|
||||
return this._update(index, this._routes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -129,7 +135,14 @@ class RouteStack {
|
|||
);
|
||||
|
||||
var routes = this._routes.set(index, route);
|
||||
return new RouteStack(this._index, routes);
|
||||
return this._update(this._index, routes);
|
||||
}
|
||||
|
||||
_update(index: number, routes: List): RouteStack {
|
||||
if (this._index === index && this._routes === routes) {
|
||||
return this;
|
||||
}
|
||||
return new RouteStack(index, routes);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,27 +34,48 @@ jest
|
|||
var NavigationEventEmitter = require('NavigationEventEmitter');
|
||||
|
||||
describe('NavigationEventEmitter', () => {
|
||||
it('emit event', () => {
|
||||
var target = {};
|
||||
var emitter = new NavigationEventEmitter(target);
|
||||
var focusCounter = 0;
|
||||
var focusTarget;
|
||||
it('emits event', () => {
|
||||
var context = {};
|
||||
var emitter = new NavigationEventEmitter(context);
|
||||
var logs = [];
|
||||
|
||||
emitter.addListener('ping', (event) => {
|
||||
var {type, data, target, defaultPrevented} = event;
|
||||
|
||||
logs.push({
|
||||
data,
|
||||
defaultPrevented,
|
||||
target,
|
||||
type,
|
||||
});
|
||||
|
||||
emitter.addListener('focus', (event) => {
|
||||
focusCounter++;
|
||||
focusTarget = event.target;
|
||||
});
|
||||
|
||||
emitter.emit('focus');
|
||||
emitter.emit('blur');
|
||||
emitter.emit('ping', 'hello');
|
||||
|
||||
expect(focusCounter).toBe(1);
|
||||
expect(focusTarget).toBe(target);
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].target).toBe(context);
|
||||
expect(logs[0].type).toBe('ping');
|
||||
expect(logs[0].data).toBe('hello');
|
||||
expect(logs[0].defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
it('put nested emit call in queue', () => {
|
||||
var target = {};
|
||||
var emitter = new NavigationEventEmitter(target);
|
||||
it('does not emit event that has no listeners', () => {
|
||||
var context = {};
|
||||
var emitter = new NavigationEventEmitter(context);
|
||||
var pinged = false;
|
||||
|
||||
emitter.addListener('ping', () => {
|
||||
pinged = true;
|
||||
});
|
||||
|
||||
emitter.emit('yo', 'bo');
|
||||
expect(pinged).toBe(false);
|
||||
});
|
||||
|
||||
it('puts nested emit call in a queue', () => {
|
||||
var context = {};
|
||||
var emitter = new NavigationEventEmitter(context);
|
||||
var logs = [];
|
||||
|
||||
emitter.addListener('one', () => {
|
||||
|
@ -77,4 +98,63 @@ describe('NavigationEventEmitter', () => {
|
|||
|
||||
expect(logs).toEqual([1, 2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
it('calls callback after emitting', () => {
|
||||
var context = {};
|
||||
var emitter = new NavigationEventEmitter(context);
|
||||
var logs = [];
|
||||
|
||||
emitter.addListener('ping', (event) => {
|
||||
var {type, data, target, defaultPrevented} = event;
|
||||
logs.push({
|
||||
data,
|
||||
defaultPrevented,
|
||||
target,
|
||||
type,
|
||||
});
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
emitter.emit('ping', 'hello', (event) => {
|
||||
var {type, data, target, defaultPrevented} = event;
|
||||
logs.push({
|
||||
data,
|
||||
defaultPrevented,
|
||||
target,
|
||||
type,
|
||||
});
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(2);
|
||||
expect(logs[1].target).toBe(context);
|
||||
expect(logs[1].type).toBe('ping');
|
||||
expect(logs[1].data).toBe('hello');
|
||||
expect(logs[1].defaultPrevented).toBe(true);
|
||||
});
|
||||
|
||||
it('calls callback after emitting the current event and before ' +
|
||||
'emitting the next event', () => {
|
||||
var context = {};
|
||||
var emitter = new NavigationEventEmitter(context);
|
||||
var logs = [];
|
||||
|
||||
emitter.addListener('ping', (event) => {
|
||||
logs.push('ping');
|
||||
emitter.emit('pong');
|
||||
});
|
||||
|
||||
emitter.addListener('pong', (event) => {
|
||||
logs.push('pong');
|
||||
});
|
||||
|
||||
emitter.emit('ping', null, () => {
|
||||
logs.push('did-ping');
|
||||
});
|
||||
|
||||
expect(logs).toEqual([
|
||||
'ping',
|
||||
'did-ping',
|
||||
'pong',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,17 +36,17 @@ describe('NavigationRouteStack:', () => {
|
|||
// Basic
|
||||
it('gets index', () => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b', 'c']);
|
||||
expect(stack.index).toEqual(1);
|
||||
expect(stack.index).toBe(1);
|
||||
});
|
||||
|
||||
it('gets size', () => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b', 'c']);
|
||||
expect(stack.size).toEqual(3);
|
||||
expect(stack.size).toBe(3);
|
||||
});
|
||||
|
||||
it('gets route', () => {
|
||||
var stack = new NavigationRouteStack(0, ['a', 'b', 'c']);
|
||||
expect(stack.get(2)).toEqual('c');
|
||||
expect(stack.get(2)).toBe('c');
|
||||
});
|
||||
|
||||
it('converts to an array', () => {
|
||||
|
@ -57,7 +57,7 @@ describe('NavigationRouteStack:', () => {
|
|||
it('creates a new stack after mutation', () => {
|
||||
var stack1 = new NavigationRouteStack(0, ['a', 'b']);
|
||||
var stack2 = stack1.push('c');
|
||||
expect(stack1).not.toEqual(stack2);
|
||||
expect(stack1).not.toBe(stack2);
|
||||
});
|
||||
|
||||
it('throws at index out of bound', () => {
|
||||
|
@ -70,15 +70,57 @@ describe('NavigationRouteStack:', () => {
|
|||
}).toThrow();
|
||||
});
|
||||
|
||||
it('finds index', () => {
|
||||
var stack = new NavigationRouteStack(0, ['a', 'b']);
|
||||
expect(stack.indexOf('b')).toBe(1);
|
||||
expect(stack.indexOf('c')).toBe(-1);
|
||||
});
|
||||
|
||||
it('slices', () => {
|
||||
var stack1 = new NavigationRouteStack(1, ['a', 'b', 'c', 'd']);
|
||||
var stack2 = stack1.slice(1, 3);
|
||||
expect(stack2).not.toBe(stack1);
|
||||
expect(stack2.toArray()).toEqual(['b', 'c']);
|
||||
});
|
||||
|
||||
it('may update index after slicing', () => {
|
||||
var stack = new NavigationRouteStack(2, ['a', 'b', 'c']);
|
||||
expect(stack.slice().index).toBe(2);
|
||||
expect(stack.slice(0, 1).index).toBe(0);
|
||||
expect(stack.slice(0, 2).index).toBe(1);
|
||||
expect(stack.slice(0, 3).index).toBe(2);
|
||||
expect(stack.slice(0, 100).index).toBe(2);
|
||||
expect(stack.slice(-2).index).toBe(1);
|
||||
});
|
||||
|
||||
it('slices without specifying params', () => {
|
||||
var stack1 = new NavigationRouteStack(1, ['a', 'b', 'c']);
|
||||
var stack2 = stack1.slice();
|
||||
expect(stack2).toBe(stack1);
|
||||
});
|
||||
|
||||
it('slices to from the end', () => {
|
||||
var stack1 = new NavigationRouteStack(1, ['a', 'b', 'c', 'd']);
|
||||
var stack2 = stack1.slice(-2);
|
||||
expect(stack2.toArray()).toEqual(['c', 'd']);
|
||||
});
|
||||
|
||||
it('throws when slicing to empty', () => {
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
stack.slice(100);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
// Push
|
||||
it('pushes route', () => {
|
||||
var stack1 = new NavigationRouteStack(1, ['a', 'b']);
|
||||
var stack2 = stack1.push('c');
|
||||
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2).not.toBe(stack1);
|
||||
expect(stack2.toArray()).toEqual(['a', 'b', 'c']);
|
||||
expect(stack2.index).toEqual(2);
|
||||
expect(stack2.size).toEqual(3);
|
||||
expect(stack2.index).toBe(2);
|
||||
expect(stack2.size).toBe(3);
|
||||
});
|
||||
|
||||
it('throws when pushing empty route', () => {
|
||||
|
@ -101,27 +143,27 @@ describe('NavigationRouteStack:', () => {
|
|||
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).not.toBe(stack1);
|
||||
expect(stack2.toArray()).toEqual(['a', 'b', 'd']);
|
||||
expect(stack2.index).toEqual(2);
|
||||
expect(stack2.index).toBe(2);
|
||||
});
|
||||
|
||||
// Pop
|
||||
it('pops route', () => {
|
||||
var stack1 = new NavigationRouteStack(2, ['a', 'b', 'c']);
|
||||
var stack2 = stack1.pop();
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2).not.toBe(stack1);
|
||||
expect(stack2.toArray()).toEqual(['a', 'b']);
|
||||
expect(stack2.index).toEqual(1);
|
||||
expect(stack2.size).toEqual(2);
|
||||
expect(stack2.index).toBe(1);
|
||||
expect(stack2.size).toBe(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).not.toBe(stack1);
|
||||
expect(stack2.toArray()).toEqual(['a']);
|
||||
expect(stack2.index).toEqual(0);
|
||||
expect(stack2.index).toBe(0);
|
||||
});
|
||||
|
||||
it('throws when popping to empty stack', () => {
|
||||
|
@ -136,8 +178,8 @@ describe('NavigationRouteStack:', () => {
|
|||
var stack1 = new NavigationRouteStack(0, ['a', 'b', 'c']);
|
||||
var stack2 = stack1.jumpToIndex(2);
|
||||
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2.index).toEqual(2);
|
||||
expect(stack2).not.toBe(stack1);
|
||||
expect(stack2.index).toBe(2);
|
||||
});
|
||||
|
||||
it('throws then jumping to index out of bound', () => {
|
||||
|
@ -157,21 +199,20 @@ describe('NavigationRouteStack:', () => {
|
|||
var stack1 = new NavigationRouteStack(1, ['a', 'b']);
|
||||
var stack2 = stack1.replaceAtIndex(0, 'x');
|
||||
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2).not.toBe(stack1);
|
||||
expect(stack2.toArray()).toEqual(['x', 'b']);
|
||||
expect(stack2.index).toEqual(1);
|
||||
expect(stack2.index).toBe(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).not.toBe(stack1);
|
||||
expect(stack2.toArray()).toEqual(['a', 'x']);
|
||||
expect(stack2.index).toEqual(1);
|
||||
expect(stack2.index).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
it('throws when replacing empty route', () => {
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
|
|
|
@ -67,6 +67,24 @@ function getuid() {
|
|||
return __uid++;
|
||||
}
|
||||
|
||||
function getRouteID(route) {
|
||||
if (route === null || typeof route !== 'object') {
|
||||
return String(route);
|
||||
}
|
||||
|
||||
var key = '__navigatorRouteID';
|
||||
|
||||
if (!route.hasOwnProperty(key)) {
|
||||
Object.defineProperty(route, key, {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: getuid(),
|
||||
});
|
||||
}
|
||||
return route[key];
|
||||
}
|
||||
|
||||
// styles moved to the top of the file so getDefaultProps can refer to it
|
||||
var styles = StyleSheet.create({
|
||||
container: {
|
||||
|
@ -220,11 +238,6 @@ var Navigator = React.createClass({
|
|||
*/
|
||||
onDidFocus: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Will be called with (ref, indexInStack, route) when the scene ref changes
|
||||
*/
|
||||
onItemRef: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Optionally provide a navigation bar that persists across scene
|
||||
* transitions
|
||||
|
@ -277,7 +290,6 @@ var Navigator = React.createClass({
|
|||
sceneConfigStack: routeStack.map(
|
||||
(route) => this.props.configureScene(route)
|
||||
),
|
||||
idStack: routeStack.map(() => getuid()),
|
||||
routeStack,
|
||||
presentedIndex: initialRouteIndex,
|
||||
transitionFromIndex: null,
|
||||
|
@ -318,7 +330,6 @@ var Navigator = React.createClass({
|
|||
onPanResponderMove: this._handlePanResponderMove,
|
||||
onPanResponderTerminate: this._handlePanResponderTerminate,
|
||||
});
|
||||
this._itemRefs = {};
|
||||
this._interactionHandle = null;
|
||||
this._emitWillFocus(this.state.routeStack[this.state.presentedIndex]);
|
||||
},
|
||||
|
@ -345,7 +356,6 @@ var Navigator = React.createClass({
|
|||
immediatelyResetRouteStack: function(nextRouteStack) {
|
||||
var destIndex = nextRouteStack.length - 1;
|
||||
this.setState({
|
||||
idStack: nextRouteStack.map(getuid),
|
||||
routeStack: nextRouteStack,
|
||||
sceneConfigStack: nextRouteStack.map(
|
||||
this.props.configureScene
|
||||
|
@ -870,17 +880,14 @@ var Navigator = React.createClass({
|
|||
invariant(!!route, 'Must supply route to push');
|
||||
var activeLength = this.state.presentedIndex + 1;
|
||||
var activeStack = this.state.routeStack.slice(0, activeLength);
|
||||
var activeIDStack = this.state.idStack.slice(0, activeLength);
|
||||
var activeAnimationConfigStack = this.state.sceneConfigStack.slice(0, activeLength);
|
||||
var nextStack = activeStack.concat([route]);
|
||||
var destIndex = nextStack.length - 1;
|
||||
var nextIDStack = activeIDStack.concat([getuid()]);
|
||||
var nextAnimationConfigStack = activeAnimationConfigStack.concat([
|
||||
this.props.configureScene(route),
|
||||
]);
|
||||
this._emitWillFocus(nextStack[destIndex]);
|
||||
this.setState({
|
||||
idStack: nextIDStack,
|
||||
routeStack: nextStack,
|
||||
sceneConfigStack: nextAnimationConfigStack,
|
||||
}, () => {
|
||||
|
@ -930,12 +937,8 @@ var Navigator = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
// I don't believe we need to lock for a replace since there's no
|
||||
// navigation actually happening
|
||||
var nextIDStack = this.state.idStack.slice();
|
||||
var nextRouteStack = this.state.routeStack.slice();
|
||||
var nextAnimationModeStack = this.state.sceneConfigStack.slice();
|
||||
nextIDStack[index] = getuid();
|
||||
nextRouteStack[index] = route;
|
||||
nextAnimationModeStack[index] = this.props.configureScene(route);
|
||||
|
||||
|
@ -943,7 +946,6 @@ var Navigator = React.createClass({
|
|||
this._emitWillFocus(route);
|
||||
}
|
||||
this.setState({
|
||||
idStack: nextIDStack,
|
||||
routeStack: nextRouteStack,
|
||||
sceneConfigStack: nextAnimationModeStack,
|
||||
}, () => {
|
||||
|
@ -1006,63 +1008,34 @@ var Navigator = React.createClass({
|
|||
return this.state.routeStack.slice();
|
||||
},
|
||||
|
||||
_handleItemRef: function(itemId, route, ref) {
|
||||
this._itemRefs[itemId] = ref;
|
||||
var itemIndex = this.state.idStack.indexOf(itemId);
|
||||
if (itemIndex === -1) {
|
||||
return;
|
||||
}
|
||||
this.props.onItemRef && this.props.onItemRef(ref, itemIndex, route);
|
||||
},
|
||||
|
||||
_cleanScenesPastIndex: function(index) {
|
||||
var newStackLength = index + 1;
|
||||
// Remove any unneeded rendered routes.
|
||||
if (newStackLength < this.state.routeStack.length) {
|
||||
this.state.idStack.slice(newStackLength).map((removingId) => {
|
||||
this._itemRefs[removingId] = null;
|
||||
});
|
||||
this.setState({
|
||||
sceneConfigStack: this.state.sceneConfigStack.slice(0, newStackLength),
|
||||
idStack: this.state.idStack.slice(0, newStackLength),
|
||||
routeStack: this.state.routeStack.slice(0, newStackLength),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_renderScene: function(route, i) {
|
||||
var child = this.props.renderScene(
|
||||
route,
|
||||
this
|
||||
);
|
||||
var disabledSceneStyle = null;
|
||||
if (i !== this.state.presentedIndex) {
|
||||
disabledSceneStyle = styles.disabledScene;
|
||||
}
|
||||
var originalRef = child.ref;
|
||||
if (originalRef != null && typeof originalRef !== 'function') {
|
||||
console.warn(
|
||||
'String refs are not supported for navigator scenes. Use a callback ' +
|
||||
'ref instead. Ignoring ref: ' + originalRef
|
||||
);
|
||||
originalRef = null;
|
||||
}
|
||||
return (
|
||||
<View
|
||||
key={this.state.idStack[i]}
|
||||
key={'scene_' + getRouteID(route)}
|
||||
ref={'scene_' + i}
|
||||
onStartShouldSetResponderCapture={() => {
|
||||
return (this.state.transitionFromIndex != null) || (this.state.transitionFromIndex != null);
|
||||
}}
|
||||
style={[styles.baseScene, this.props.sceneStyle, disabledSceneStyle]}>
|
||||
{React.cloneElement(child, {
|
||||
ref: component => {
|
||||
this._handleItemRef(this.state.idStack[i], route, component);
|
||||
if (originalRef) {
|
||||
originalRef(component);
|
||||
}
|
||||
}
|
||||
})}
|
||||
{this.props.renderScene(
|
||||
route,
|
||||
this
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -33,6 +33,8 @@ var StaticContainer = require('StaticContainer.react');
|
|||
var StyleSheet = require('StyleSheet');
|
||||
var View = require('View');
|
||||
|
||||
var { Map } = require('immutable');
|
||||
|
||||
var invariant = require('invariant');
|
||||
|
||||
var Interpolators = NavigatorBreadcrumbNavigationBarStyles.Interpolators;
|
||||
|
@ -86,7 +88,6 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({
|
|||
}),
|
||||
navState: React.PropTypes.shape({
|
||||
routeStack: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
idStack: React.PropTypes.arrayOf(React.PropTypes.number),
|
||||
presentedIndex: React.PropTypes.number,
|
||||
}),
|
||||
style: View.propTypes.style,
|
||||
|
@ -173,11 +174,19 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._descriptors = {
|
||||
crumb: new Map(),
|
||||
title: new Map(),
|
||||
right: new Map(),
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var navState = this.props.navState;
|
||||
var icons = navState && navState.routeStack.map(this._renderOrReturnBreadcrumb);
|
||||
var titles = navState.routeStack.map(this._renderOrReturnTitle);
|
||||
var buttons = navState.routeStack.map(this._renderOrReturnRightButton);
|
||||
var icons = navState && navState.routeStack.map(this._getBreadcrumb);
|
||||
var titles = navState.routeStack.map(this._getTitle);
|
||||
var buttons = navState.routeStack.map(this._getRightButton);
|
||||
return (
|
||||
<View style={[styles.breadCrumbContainer, this.props.style]}>
|
||||
{titles}
|
||||
|
@ -187,104 +196,69 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
_renderOrReturnBreadcrumb: function(route, index) {
|
||||
var uid = this.props.navState.idStack[index];
|
||||
var navBarRouteMapper = this.props.routeMapper;
|
||||
var navOps = this.props.navigator;
|
||||
var alreadyRendered = this.refs['crumbContainer' + uid];
|
||||
if (alreadyRendered) {
|
||||
// Don't bother re-calculating the children
|
||||
return (
|
||||
<StaticContainer
|
||||
ref={'crumbContainer' + uid}
|
||||
key={'crumbContainer' + uid}
|
||||
shouldUpdate={false}
|
||||
/>
|
||||
);
|
||||
_getBreadcrumb: function(route, index) {
|
||||
if (this._descriptors.crumb.has(route)) {
|
||||
return this._descriptors.crumb.get(route);
|
||||
}
|
||||
|
||||
var navBarRouteMapper = this.props.routeMapper;
|
||||
var firstStyles = initStyle(index, navStatePresentedIndex(this.props.navState));
|
||||
return (
|
||||
<StaticContainer
|
||||
ref={'crumbContainer' + uid}
|
||||
key={'crumbContainer' + uid}
|
||||
shouldUpdate={false}>
|
||||
<View ref={'crumb_' + index} style={firstStyles.Crumb}>
|
||||
<View ref={'icon_' + index} style={firstStyles.Icon}>
|
||||
{navBarRouteMapper.iconForRoute(route, navOps)}
|
||||
</View>
|
||||
<View ref={'separator_' + index} style={firstStyles.Separator}>
|
||||
{navBarRouteMapper.separatorForRoute(route, navOps)}
|
||||
</View>
|
||||
|
||||
var breadcrumbDescriptor = (
|
||||
<View ref={'crumb_' + index} style={firstStyles.Crumb}>
|
||||
<View ref={'icon_' + index} style={firstStyles.Icon}>
|
||||
{navBarRouteMapper.iconForRoute(route, this.props.navigator)}
|
||||
</View>
|
||||
</StaticContainer>
|
||||
<View ref={'separator_' + index} style={firstStyles.Separator}>
|
||||
{navBarRouteMapper.separatorForRoute(route, this.props.navigator)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
this._descriptors.crumb = this._descriptors.crumb.set(route, breadcrumbDescriptor);
|
||||
return breadcrumbDescriptor;
|
||||
},
|
||||
|
||||
_renderOrReturnTitle: function(route, index) {
|
||||
var navState = this.props.navState;
|
||||
var uid = navState.idStack[index];
|
||||
var alreadyRendered = this.refs['titleContainer' + uid];
|
||||
if (alreadyRendered) {
|
||||
// Don't bother re-calculating the children
|
||||
return (
|
||||
<StaticContainer
|
||||
ref={'titleContainer' + uid}
|
||||
key={'titleContainer' + uid}
|
||||
shouldUpdate={false}
|
||||
/>
|
||||
);
|
||||
_getTitle: function(route, index) {
|
||||
if (this._descriptors.title.has(route)) {
|
||||
return this._descriptors.title.get(route);
|
||||
}
|
||||
var navBarRouteMapper = this.props.routeMapper;
|
||||
var titleContent = navBarRouteMapper.titleContentForRoute(
|
||||
navState.routeStack[index],
|
||||
|
||||
var titleContent = this.props.routeMapper.titleContentForRoute(
|
||||
this.props.navState.routeStack[index],
|
||||
this.props.navigator
|
||||
);
|
||||
var firstStyles = initStyle(index, navStatePresentedIndex(this.props.navState));
|
||||
return (
|
||||
<StaticContainer
|
||||
ref={'titleContainer' + uid}
|
||||
key={'titleContainer' + uid}
|
||||
shouldUpdate={false}>
|
||||
<View ref={'title_' + index} style={firstStyles.Title}>
|
||||
{titleContent}
|
||||
</View>
|
||||
</StaticContainer>
|
||||
|
||||
var titleDescriptor = (
|
||||
<View ref={'title_' + index} style={firstStyles.Title}>
|
||||
{titleContent}
|
||||
</View>
|
||||
);
|
||||
this._descriptors.title = this._descriptors.title.set(route, titleDescriptor);
|
||||
return titleDescriptor;
|
||||
},
|
||||
|
||||
_renderOrReturnRightButton: function(route, index) {
|
||||
var navState = this.props.navState;
|
||||
var navBarRouteMapper = this.props.routeMapper;
|
||||
var uid = navState.idStack[index];
|
||||
var alreadyRendered = this.refs['rightContainer' + uid];
|
||||
if (alreadyRendered) {
|
||||
// Don't bother re-calculating the children
|
||||
return (
|
||||
<StaticContainer
|
||||
ref={'rightContainer' + uid}
|
||||
key={'rightContainer' + uid}
|
||||
shouldUpdate={false}
|
||||
/>
|
||||
);
|
||||
_getRightButton: function(route, index) {
|
||||
if (this._descriptors.right.has(route)) {
|
||||
return this._descriptors.right.get(route);
|
||||
}
|
||||
var rightContent = navBarRouteMapper.rightContentForRoute(
|
||||
navState.routeStack[index],
|
||||
var rightContent = this.props.routeMapper.rightContentForRoute(
|
||||
this.props.navState.routeStack[index],
|
||||
this.props.navigator
|
||||
);
|
||||
if (!rightContent) {
|
||||
this._descriptors.right = this._descriptors.right.set(route, null);
|
||||
return null;
|
||||
}
|
||||
var firstStyles = initStyle(index, navStatePresentedIndex(this.props.navState));
|
||||
return (
|
||||
<StaticContainer
|
||||
ref={'rightContainer' + uid}
|
||||
key={'rightContainer' + uid}
|
||||
shouldUpdate={false}>
|
||||
<View ref={'right_' + index} style={firstStyles.RightItem}>
|
||||
{rightContent}
|
||||
</View>
|
||||
</StaticContainer>
|
||||
var rightButtonDescriptor = (
|
||||
<View ref={'right_' + index} style={firstStyles.RightItem}>
|
||||
{rightContent}
|
||||
</View>
|
||||
);
|
||||
this._descriptors.right = this._descriptors.right.set(route, rightButtonDescriptor);
|
||||
return rightButtonDescriptor;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -32,6 +32,8 @@ var StaticContainer = require('StaticContainer.react');
|
|||
var StyleSheet = require('StyleSheet');
|
||||
var View = require('View');
|
||||
|
||||
var { Map } = require('immutable');
|
||||
|
||||
var COMPONENT_NAMES = ['Title', 'LeftButton', 'RightButton'];
|
||||
|
||||
var navStatePresentedIndex = function(navState) {
|
||||
|
@ -53,7 +55,6 @@ var NavigatorNavigationBar = React.createClass({
|
|||
}),
|
||||
navState: React.PropTypes.shape({
|
||||
routeStack: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
idStack: React.PropTypes.arrayOf(React.PropTypes.number),
|
||||
presentedIndex: React.PropTypes.number,
|
||||
}),
|
||||
style: View.propTypes.style,
|
||||
|
@ -63,6 +64,16 @@ var NavigatorNavigationBar = React.createClass({
|
|||
Styles: NavigatorNavigationBarStyles,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._components = {};
|
||||
this._descriptors = {};
|
||||
|
||||
COMPONENT_NAMES.forEach(componentName => {
|
||||
this._components[componentName] = new Map();
|
||||
this._descriptors[componentName] = new Map();
|
||||
});
|
||||
},
|
||||
|
||||
_getReusableProps: function(
|
||||
/*string*/componentName,
|
||||
/*number*/index
|
||||
|
@ -104,7 +115,7 @@ var NavigatorNavigationBar = React.createClass({
|
|||
}
|
||||
|
||||
COMPONENT_NAMES.forEach(function (componentName) {
|
||||
var component = this.refs[componentName + index];
|
||||
var component = this._components[componentName].get(this.props.navState.routeStack[index]);
|
||||
var props = this._getReusableProps(componentName, index);
|
||||
if (component && interpolate[componentName](props.style, amount)) {
|
||||
component.setNativeProps(props);
|
||||
|
@ -128,7 +139,7 @@ var NavigatorNavigationBar = React.createClass({
|
|||
var navState = this.props.navState;
|
||||
var components = COMPONENT_NAMES.map(function (componentName) {
|
||||
return navState.routeStack.map(
|
||||
this._renderOrReturnComponent.bind(this, componentName)
|
||||
this._getComponent.bind(this, componentName)
|
||||
);
|
||||
}, this);
|
||||
|
||||
|
@ -139,28 +150,19 @@ var NavigatorNavigationBar = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
_renderOrReturnComponent: function(
|
||||
_getComponent: function(
|
||||
/*string*/componentName,
|
||||
/*object*/route,
|
||||
/*number*/index
|
||||
) /*object*/ {
|
||||
var navState = this.props.navState;
|
||||
var uid = navState.idStack[index];
|
||||
var containerRef = componentName + 'Container' + uid;
|
||||
var alreadyRendered = this.refs[containerRef];
|
||||
if (alreadyRendered) {
|
||||
// Don't bother re-calculating the children
|
||||
return (
|
||||
<StaticContainer
|
||||
ref={containerRef}
|
||||
key={containerRef}
|
||||
shouldUpdate={false}
|
||||
/>
|
||||
);
|
||||
) /*?Object*/ {
|
||||
if (this._descriptors[componentName].includes(route)) {
|
||||
return this._descriptors[componentName].get(route);
|
||||
}
|
||||
|
||||
var rendered = null;
|
||||
|
||||
var content = this.props.routeMapper[componentName](
|
||||
navState.routeStack[index],
|
||||
this.props.navState.routeStack[index],
|
||||
this.props.navigator,
|
||||
index,
|
||||
this.props.navState
|
||||
|
@ -171,16 +173,18 @@ var NavigatorNavigationBar = React.createClass({
|
|||
|
||||
var initialStage = index === navStatePresentedIndex(this.props.navState) ?
|
||||
NavigatorNavigationBarStyles.Stages.Center : NavigatorNavigationBarStyles.Stages.Left;
|
||||
return (
|
||||
<StaticContainer
|
||||
ref={containerRef}
|
||||
key={containerRef}
|
||||
shouldUpdate={false}>
|
||||
<View ref={componentName + index} style={initialStage[componentName]}>
|
||||
{content}
|
||||
</View>
|
||||
</StaticContainer>
|
||||
rendered = (
|
||||
<View
|
||||
ref={(ref) => {
|
||||
this._components[componentName] = this._components[componentName].set(route, ref);
|
||||
}}
|
||||
style={initialStage[componentName]}>
|
||||
{content}
|
||||
</View>
|
||||
);
|
||||
|
||||
this._descriptors[componentName] = this._descriptors[componentName].set(route, rendered);
|
||||
return rendered;
|
||||
},
|
||||
|
||||
});
|
||||
|
|
|
@ -163,6 +163,56 @@ var ToTheLeft = {
|
|||
},
|
||||
};
|
||||
|
||||
var ToTheUp = {
|
||||
transformTranslate: {
|
||||
from: {x: 0, y: 0, z: 0},
|
||||
to: {x: 0, y: -Dimensions.get('window').height, z: 0},
|
||||
min: 0,
|
||||
max: 1,
|
||||
type: 'linear',
|
||||
extrapolate: true,
|
||||
round: PixelRatio.get(),
|
||||
},
|
||||
opacity: {
|
||||
value: 1.0,
|
||||
type: 'constant',
|
||||
},
|
||||
translateY: {
|
||||
from: 0,
|
||||
to: -Dimensions.get('window').height,
|
||||
min: 0,
|
||||
max: 1,
|
||||
type: 'linear',
|
||||
extrapolate: true,
|
||||
round: PixelRatio.get(),
|
||||
},
|
||||
};
|
||||
|
||||
var ToTheDown = {
|
||||
transformTranslate: {
|
||||
from: {x: 0, y: 0, z: 0},
|
||||
to: {x: 0, y: Dimensions.get('window').height, z: 0},
|
||||
min: 0,
|
||||
max: 1,
|
||||
type: 'linear',
|
||||
extrapolate: true,
|
||||
round: PixelRatio.get(),
|
||||
},
|
||||
opacity: {
|
||||
value: 1.0,
|
||||
type: 'constant',
|
||||
},
|
||||
translateY: {
|
||||
from: 0,
|
||||
to: Dimensions.get('window').height,
|
||||
min: 0,
|
||||
max: 1,
|
||||
type: 'linear',
|
||||
extrapolate: true,
|
||||
round: PixelRatio.get(),
|
||||
},
|
||||
};
|
||||
|
||||
var FromTheRight = {
|
||||
opacity: {
|
||||
value: 1.0,
|
||||
|
@ -221,6 +271,50 @@ var FromTheLeft = {
|
|||
},
|
||||
};
|
||||
|
||||
var FromTheDown = {
|
||||
...FromTheRight,
|
||||
transformTranslate: {
|
||||
from: {y: SCREEN_HEIGHT, x: 0, z: 0},
|
||||
to: {x: 0, y: 0, z: 0},
|
||||
min: 0,
|
||||
max: 1,
|
||||
type: 'linear',
|
||||
extrapolate: true,
|
||||
round: PixelRatio.get(),
|
||||
},
|
||||
translateY: {
|
||||
from: SCREEN_HEIGHT,
|
||||
to: 0,
|
||||
min: 0,
|
||||
max: 1,
|
||||
type: 'linear',
|
||||
extrapolate: true,
|
||||
round: PixelRatio.get(),
|
||||
},
|
||||
};
|
||||
|
||||
var FromTheTop = {
|
||||
...FromTheRight,
|
||||
transformTranslate: {
|
||||
from: {y: -SCREEN_HEIGHT, x: 0, z: 0},
|
||||
to: {x: 0, y: 0, z: 0},
|
||||
min: 0,
|
||||
max: 1,
|
||||
type: 'linear',
|
||||
extrapolate: true,
|
||||
round: PixelRatio.get(),
|
||||
},
|
||||
translateY: {
|
||||
from: -SCREEN_HEIGHT,
|
||||
to: 0,
|
||||
min: 0,
|
||||
max: 1,
|
||||
type: 'linear',
|
||||
extrapolate: true,
|
||||
round: PixelRatio.get(),
|
||||
},
|
||||
};
|
||||
|
||||
var ToTheBack = {
|
||||
// Rotate *requires* you to break out each individual component of
|
||||
// rotation (x, y, z, w)
|
||||
|
@ -378,6 +472,18 @@ var BaseRightToLeftGesture = {
|
|||
direction: 'right-to-left',
|
||||
};
|
||||
|
||||
var BaseDownUpGesture = {
|
||||
...BaseLeftToRightGesture,
|
||||
fullDistance: SCREEN_HEIGHT,
|
||||
direction: 'down-to-up',
|
||||
};
|
||||
|
||||
var BaseUpDownGesture = {
|
||||
...BaseLeftToRightGesture,
|
||||
fullDistance: SCREEN_HEIGHT,
|
||||
direction: 'up-to-down',
|
||||
};
|
||||
|
||||
var BaseConfig = {
|
||||
// A list of all gestures that are enabled on this scene
|
||||
gestures: {
|
||||
|
@ -468,6 +574,48 @@ var NavigatorSceneConfigs = {
|
|||
out: buildStyleInterpolator(ToTheLeft),
|
||||
},
|
||||
},
|
||||
VerticalUpSwipeJump: {
|
||||
...BaseConfig,
|
||||
gestures: {
|
||||
jumpBack: {
|
||||
...BaseDownUpGesture,
|
||||
overswipe: BaseOverswipeConfig,
|
||||
edgeHitWidth: null,
|
||||
isDetachable: true,
|
||||
},
|
||||
jumpForward: {
|
||||
...BaseDownUpGesture,
|
||||
overswipe: BaseOverswipeConfig,
|
||||
edgeHitWidth: null,
|
||||
isDetachable: true,
|
||||
},
|
||||
},
|
||||
animationInterpolators: {
|
||||
into: buildStyleInterpolator(FromTheDown),
|
||||
out: buildStyleInterpolator(ToTheUp),
|
||||
},
|
||||
},
|
||||
VerticalDownSwipeJump: {
|
||||
...BaseConfig,
|
||||
gestures: {
|
||||
jumpBack: {
|
||||
...BaseUpDownGesture,
|
||||
overswipe: BaseOverswipeConfig,
|
||||
edgeHitWidth: null,
|
||||
isDetachable: true,
|
||||
},
|
||||
jumpForward: {
|
||||
...BaseUpDownGesture,
|
||||
overswipe: BaseOverswipeConfig,
|
||||
edgeHitWidth: null,
|
||||
isDetachable: true,
|
||||
},
|
||||
},
|
||||
animationInterpolators: {
|
||||
into: buildStyleInterpolator(FromTheTop),
|
||||
out: buildStyleInterpolator(ToTheDown),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = NavigatorSceneConfigs;
|
||||
|
|
|
@ -24,7 +24,6 @@ var StyleSheetPropType = require('StyleSheetPropType');
|
|||
|
||||
var flattenStyle = require('flattenStyle');
|
||||
var invariant = require('invariant');
|
||||
var merge = require('merge');
|
||||
var requireNativeComponent = require('requireNativeComponent');
|
||||
var resolveAssetSource = require('resolveAssetSource');
|
||||
var verifyPropTypes = require('verifyPropTypes');
|
||||
|
@ -57,6 +56,7 @@ var warning = require('warning');
|
|||
|
||||
var Image = React.createClass({
|
||||
propTypes: {
|
||||
style: StyleSheetPropType(ImageStylePropTypes),
|
||||
/**
|
||||
* `uri` is a string representing the resource identifier for the image, which
|
||||
* could be an http address, a local file path, or the name of a static image
|
||||
|
@ -93,7 +93,6 @@ var Image = React.createClass({
|
|||
* image dimensions.
|
||||
*/
|
||||
resizeMode: PropTypes.oneOf(['cover', 'contain', 'stretch']),
|
||||
style: StyleSheetPropType(ImageStylePropTypes),
|
||||
/**
|
||||
* A unique identifier for this element to be used in UI Automation
|
||||
* testing scripts.
|
||||
|
@ -102,7 +101,7 @@ var Image = React.createClass({
|
|||
/**
|
||||
* Invoked on mount and layout changes with
|
||||
*
|
||||
* {nativeEvent: { layout: {x, y, width, height}}}.
|
||||
* {nativeEvent: {layout: {x, y, width, height}}}.
|
||||
*/
|
||||
onLayout: PropTypes.func,
|
||||
/**
|
||||
|
@ -112,25 +111,23 @@ var Image = React.createClass({
|
|||
/**
|
||||
* Invoked on download progress with
|
||||
*
|
||||
* {nativeEvent: { written, total}}.
|
||||
* {nativeEvent: {loaded, total}}.
|
||||
*/
|
||||
onLoadProgress: PropTypes.func,
|
||||
/**
|
||||
* Invoked on load abort
|
||||
*/
|
||||
onLoadAbort: PropTypes.func,
|
||||
onProgress: PropTypes.func,
|
||||
/**
|
||||
* Invoked on load error
|
||||
*
|
||||
* {nativeEvent: { error}}.
|
||||
* {nativeEvent: {error}}.
|
||||
*/
|
||||
onLoadError: PropTypes.func,
|
||||
onError: PropTypes.func,
|
||||
/**
|
||||
* Invoked on load end
|
||||
*
|
||||
* Invoked when load completes successfully
|
||||
*/
|
||||
onLoaded: PropTypes.func
|
||||
|
||||
onLoad: PropTypes.func,
|
||||
/**
|
||||
* Invoked when load either succeeds or fails
|
||||
*/
|
||||
onLoadEnd: PropTypes.func,
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
@ -149,46 +146,27 @@ var Image = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
for (var prop in nativeOnlyProps) {
|
||||
if (this.props[prop] !== undefined) {
|
||||
console.warn('Prop `' + prop + ' = ' + this.props[prop] + '` should ' +
|
||||
'not be set directly on Image.');
|
||||
}
|
||||
}
|
||||
var source = resolveAssetSource(this.props.source) || {};
|
||||
var defaultSource = (this.props.defaultSource && resolveAssetSource(this.props.defaultSource)) || {};
|
||||
|
||||
var {width, height} = source;
|
||||
var style = flattenStyle([{width, height}, styles.base, this.props.style]);
|
||||
invariant(style, 'style must be initialized');
|
||||
var style = flattenStyle([{width, height}, styles.base, this.props.style]) || {};
|
||||
|
||||
var isNetwork = source.uri && source.uri.match(/^https?:/);
|
||||
invariant(
|
||||
!(isNetwork && source.isStatic),
|
||||
'static image uris cannot start with "http": "' + source.uri + '"'
|
||||
var RawImage = isNetwork ? RCTNetworkImageView : RCTImageView;
|
||||
var resizeMode = this.props.resizeMode || (style || {}).resizeMode || 'cover'; // Workaround for flow bug t7737108
|
||||
var tintColor = (style || {}).tintColor; // Workaround for flow bug t7737108
|
||||
|
||||
return (
|
||||
<RawImage
|
||||
{...this.props}
|
||||
style={style}
|
||||
resizeMode={resizeMode}
|
||||
tintColor={tintColor}
|
||||
src={source.uri}
|
||||
defaultSrc={defaultSource.uri}
|
||||
/>
|
||||
);
|
||||
var isStored = !source.isStatic && !isNetwork;
|
||||
var RawImage = isNetwork ? RCTNetworkImage : RCTStaticImage;
|
||||
|
||||
if (this.props.style && this.props.style.tintColor) {
|
||||
warning(RawImage === RCTStaticImage, 'tintColor style only supported on static images.');
|
||||
}
|
||||
var resizeMode = this.props.resizeMode || style.resizeMode || 'cover';
|
||||
|
||||
var nativeProps = merge(this.props, {
|
||||
style,
|
||||
resizeMode,
|
||||
tintColor: style.tintColor,
|
||||
});
|
||||
if (isStored) {
|
||||
nativeProps.imageTag = source.uri;
|
||||
} else {
|
||||
nativeProps.src = source.uri;
|
||||
}
|
||||
if (this.props.defaultSource) {
|
||||
nativeProps.defaultImageSrc = this.props.defaultSource.uri;
|
||||
}
|
||||
nativeProps.progressHandlerRegistered = isNetwork && this.props.onLoadProgress;
|
||||
return <RawImage {...nativeProps} />;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -198,18 +176,7 @@ var styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
var RCTNetworkImage = requireNativeComponent('RCTNetworkImageView', null);
|
||||
var RCTStaticImage = requireNativeComponent('RCTStaticImage', null);
|
||||
|
||||
var nativeOnlyProps = {
|
||||
src: true,
|
||||
defaultImageSrc: true,
|
||||
imageTag: true,
|
||||
progressHandlerRegistered: true
|
||||
};
|
||||
if (__DEV__) {
|
||||
verifyPropTypes(Image, RCTStaticImage.viewConfig, nativeOnlyProps);
|
||||
verifyPropTypes(Image, RCTNetworkImage.viewConfig, nativeOnlyProps);
|
||||
}
|
||||
var RCTImageView = requireNativeComponent('RCTImageView', null);
|
||||
var RCTNetworkImageView = (NativeModules.NetworkImageViewManager) ? requireNativeComponent('RCTNetworkImageView', null) : RCTImageView;
|
||||
|
||||
module.exports = Image;
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "RCTBridge.h"
|
||||
#import "RCTImageLoader.h"
|
||||
#import "RCTLog.h"
|
||||
#import "RCTUtils.h"
|
||||
|
@ -22,11 +23,13 @@
|
|||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
@synthesize bridge = _bridge;
|
||||
|
||||
RCT_EXPORT_METHOD(saveImageWithTag:(NSString *)imageTag
|
||||
successCallback:(RCTResponseSenderBlock)successCallback
|
||||
errorCallback:(RCTResponseErrorBlock)errorCallback)
|
||||
{
|
||||
[RCTImageLoader loadImageWithTag:imageTag callback:^(NSError *loadError, UIImage *loadedImage) {
|
||||
[RCTImageLoader loadImageWithTag:imageTag bridge:_bridge callback:^(NSError *loadError, UIImage *loadedImage) {
|
||||
if (loadError) {
|
||||
errorCallback(loadError);
|
||||
return;
|
||||
|
|
|
@ -8,17 +8,16 @@
|
|||
|
||||
/* 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 */; };
|
||||
1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5A81AA8C4A30002E2BE /* RCTImageView.m */; };
|
||||
1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTImageViewManager.m */; };
|
||||
1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */; };
|
||||
1345A8391B26592900583190 /* RCTImageRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 1345A8381B26592900583190 /* RCTImageRequestHandler.m */; };
|
||||
134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */; };
|
||||
137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137620341B31C53500677FF0 /* RCTImagePickerManager.m */; };
|
||||
143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */; };
|
||||
143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.m */; };
|
||||
35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */; };
|
||||
58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */; };
|
||||
58B511901A9E6BD600147676 /* RCTNetworkImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118C1A9E6BD600147676 /* RCTNetworkImageView.m */; };
|
||||
58B511911A9E6BD600147676 /* RCTNetworkImageViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118E1A9E6BD600147676 /* RCTNetworkImageViewManager.m */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
|
@ -36,10 +35,10 @@
|
|||
/* 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>"; };
|
||||
1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStaticImageManager.m; sourceTree = "<group>"; };
|
||||
1304D5A71AA8C4A30002E2BE /* RCTImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageView.h; sourceTree = "<group>"; };
|
||||
1304D5A81AA8C4A30002E2BE /* RCTImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageView.m; sourceTree = "<group>"; };
|
||||
1304D5A91AA8C4A30002E2BE /* RCTImageViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageViewManager.h; sourceTree = "<group>"; };
|
||||
1304D5AA1AA8C4A30002E2BE /* RCTImageViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageViewManager.m; sourceTree = "<group>"; };
|
||||
1304D5B01AA8C50D0002E2BE /* RCTGIFImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTGIFImage.h; sourceTree = "<group>"; };
|
||||
1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGIFImage.m; sourceTree = "<group>"; };
|
||||
1345A8371B26592900583190 /* RCTImageRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageRequestHandler.h; sourceTree = "<group>"; };
|
||||
|
@ -52,13 +51,11 @@
|
|||
143879341AAD238D00F088A5 /* RCTCameraRollManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCameraRollManager.m; sourceTree = "<group>"; };
|
||||
143879361AAD32A300F088A5 /* RCTImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageLoader.h; sourceTree = "<group>"; };
|
||||
143879371AAD32A300F088A5 /* RCTImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoader.m; sourceTree = "<group>"; };
|
||||
35123E691B59C99D00EBAD80 /* RCTImageStoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageStoreManager.h; sourceTree = "<group>"; };
|
||||
35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageStoreManager.m; sourceTree = "<group>"; };
|
||||
58B5115D1A9E6B3D00147676 /* libRCTImage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTImage.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
58B511891A9E6BD600147676 /* RCTImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageDownloader.h; sourceTree = "<group>"; };
|
||||
58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageDownloader.m; sourceTree = "<group>"; };
|
||||
58B5118B1A9E6BD600147676 /* RCTNetworkImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNetworkImageView.h; sourceTree = "<group>"; };
|
||||
58B5118C1A9E6BD600147676 /* RCTNetworkImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNetworkImageView.m; sourceTree = "<group>"; };
|
||||
58B5118D1A9E6BD600147676 /* RCTNetworkImageViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNetworkImageViewManager.h; sourceTree = "<group>"; };
|
||||
58B5118E1A9E6BD600147676 /* RCTNetworkImageViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNetworkImageViewManager.m; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -89,14 +86,12 @@
|
|||
137620341B31C53500677FF0 /* RCTImagePickerManager.m */,
|
||||
1345A8371B26592900583190 /* RCTImageRequestHandler.h */,
|
||||
1345A8381B26592900583190 /* RCTImageRequestHandler.m */,
|
||||
58B5118B1A9E6BD600147676 /* RCTNetworkImageView.h */,
|
||||
58B5118C1A9E6BD600147676 /* RCTNetworkImageView.m */,
|
||||
58B5118D1A9E6BD600147676 /* RCTNetworkImageViewManager.h */,
|
||||
58B5118E1A9E6BD600147676 /* RCTNetworkImageViewManager.m */,
|
||||
1304D5A71AA8C4A30002E2BE /* RCTStaticImage.h */,
|
||||
1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */,
|
||||
1304D5A91AA8C4A30002E2BE /* RCTStaticImageManager.h */,
|
||||
1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */,
|
||||
1304D5A71AA8C4A30002E2BE /* RCTImageView.h */,
|
||||
1304D5A81AA8C4A30002E2BE /* RCTImageView.m */,
|
||||
1304D5A91AA8C4A30002E2BE /* RCTImageViewManager.h */,
|
||||
1304D5AA1AA8C4A30002E2BE /* RCTImageViewManager.m */,
|
||||
35123E691B59C99D00EBAD80 /* RCTImageStoreManager.h */,
|
||||
35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */,
|
||||
134B00A01B54232B00EC8DFB /* RCTImageUtils.h */,
|
||||
134B00A11B54232B00EC8DFB /* RCTImageUtils.m */,
|
||||
58B5115E1A9E6B3D00147676 /* Products */,
|
||||
|
@ -169,17 +164,16 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */,
|
||||
58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */,
|
||||
137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */,
|
||||
58B511911A9E6BD600147676 /* RCTNetworkImageViewManager.m in Sources */,
|
||||
1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */,
|
||||
1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */,
|
||||
1345A8391B26592900583190 /* RCTImageRequestHandler.m in Sources */,
|
||||
58B511901A9E6BD600147676 /* RCTNetworkImageView.m in Sources */,
|
||||
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 */,
|
||||
1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */,
|
||||
134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -43,11 +43,4 @@ typedef void (^RCTImageDownloadCancellationBlock)(void);
|
|||
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:(RCTImageDownloadCancellationBlock)downloadToken;
|
||||
|
||||
@end
|
||||
|
|
|
@ -52,7 +52,9 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
|
|||
return self;
|
||||
}
|
||||
|
||||
- (RCTImageDownloadCancellationBlock)_downloadDataForURL:(NSURL *)url progressBlock:progressBlock block:(RCTCachedDataDownloadBlock)block
|
||||
- (RCTImageDownloadCancellationBlock)_downloadDataForURL:(NSURL *)url
|
||||
progressBlock:progressBlock
|
||||
block:(RCTCachedDataDownloadBlock)block
|
||||
{
|
||||
NSString *const cacheKey = url.absoluteString;
|
||||
|
||||
|
@ -134,7 +136,9 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
|
|||
return [cancel copy];
|
||||
}
|
||||
|
||||
- (RCTImageDownloadCancellationBlock)downloadDataForURL:(NSURL *)url progressBlock:(RCTDataProgressBlock)progressBlock block:(RCTDataDownloadBlock)block
|
||||
- (RCTImageDownloadCancellationBlock)downloadDataForURL:(NSURL *)url
|
||||
progressBlock:(RCTDataProgressBlock)progressBlock
|
||||
block:(RCTDataDownloadBlock)block
|
||||
{
|
||||
return [self _downloadDataForURL:url progressBlock:progressBlock block:^(BOOL cached, NSURLResponse *response, NSData *data, NSError *error) {
|
||||
block(data, error);
|
||||
|
@ -150,24 +154,19 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
|
|||
progressBlock:(RCTDataProgressBlock)progressBlock
|
||||
block:(RCTImageDownloadBlock)block
|
||||
{
|
||||
scale = scale ?: RCTScreenScale();
|
||||
|
||||
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) {
|
||||
if (image && !CGSizeEqualToSize(size, CGSizeZero)) {
|
||||
|
||||
// Get scale and size
|
||||
CGFloat destScale = scale ?: RCTScreenScale();
|
||||
CGRect imageRect = RCTClipRect(image.size, image.scale, size, destScale, resizeMode);
|
||||
CGRect imageRect = RCTClipRect(image.size, scale, size, scale, resizeMode);
|
||||
CGSize destSize = RCTTargetSizeForClipRect(imageRect);
|
||||
|
||||
// Opacity optimizations
|
||||
|
@ -183,7 +182,7 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
|
|||
}
|
||||
|
||||
// Decompress image at required size
|
||||
UIGraphicsBeginImageContextWithOptions(destSize, opaque, destScale);
|
||||
UIGraphicsBeginImageContextWithOptions(destSize, opaque, scale);
|
||||
if (blendColor) {
|
||||
[blendColor setFill];
|
||||
UIRectFill((CGRect){CGPointZero, destSize});
|
||||
|
@ -201,11 +200,4 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
|
|||
}];
|
||||
}
|
||||
|
||||
- (void)cancelDownload:(RCTImageDownloadCancellationBlock)downloadToken
|
||||
{
|
||||
if (downloadToken) {
|
||||
downloadToken();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -10,6 +10,11 @@
|
|||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class ALAssetsLibrary;
|
||||
@class RCTBridge;
|
||||
|
||||
typedef void (^RCTImageLoaderProgressBlock)(int64_t written, int64_t total);
|
||||
typedef void (^RCTImageLoaderCompletionBlock)(NSError *error, id /* UIImage or CAAnimation */);
|
||||
typedef void (^RCTImageLoaderCancellationBlock)(void);
|
||||
|
||||
@interface RCTImageLoader : NSObject
|
||||
|
||||
|
@ -22,22 +27,30 @@
|
|||
* Can be called from any thread.
|
||||
* Will always call callback on main thread.
|
||||
*/
|
||||
+ (void)loadImageWithTag:(NSString *)imageTag
|
||||
callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback;
|
||||
+ (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
|
||||
bridge:(RCTBridge *)bridge
|
||||
callback:(RCTImageLoaderCompletionBlock)callback;
|
||||
|
||||
/**
|
||||
* As above, but includes target size, scale and resizeMode, which are used to
|
||||
* select the optimal dimensions for the loaded image.
|
||||
*/
|
||||
+ (void)loadImageWithTag:(NSString *)imageTag
|
||||
size:(CGSize)size
|
||||
scale:(CGFloat)scale
|
||||
resizeMode:(UIViewContentMode)resizeMode
|
||||
callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback;
|
||||
+ (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
|
||||
size:(CGSize)size
|
||||
scale:(CGFloat)scale
|
||||
resizeMode:(UIViewContentMode)resizeMode
|
||||
bridge:(RCTBridge *)bridge
|
||||
progressBlock:(RCTImageLoaderProgressBlock)progress
|
||||
completionBlock:(RCTImageLoaderCompletionBlock)completion;
|
||||
|
||||
/**
|
||||
* Is the specified image tag an asset library image?
|
||||
*/
|
||||
+ (BOOL)isAssetLibraryImage:(NSString *)imageTag;
|
||||
|
||||
/**
|
||||
* Is the specified image tag a remote image?
|
||||
*/
|
||||
+ (BOOL)isRemoteImage:(NSString *)imageTag;
|
||||
|
||||
@end
|
||||
|
|
|
@ -15,10 +15,12 @@
|
|||
#import <Photos/PHImageManager.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "RCTBridge.h"
|
||||
#import "RCTConvert.h"
|
||||
#import "RCTDefines.h"
|
||||
#import "RCTGIFImage.h"
|
||||
#import "RCTImageDownloader.h"
|
||||
#import "RCTImageStoreManager.h"
|
||||
#import "RCTImageUtils.h"
|
||||
#import "RCTLog.h"
|
||||
#import "RCTUtils.h"
|
||||
|
@ -57,24 +59,73 @@ static dispatch_queue_t RCTImageLoaderQueue(void)
|
|||
return assetsLibrary;
|
||||
}
|
||||
|
||||
+ (void)loadImageWithTag:(NSString *)imageTag
|
||||
callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback
|
||||
+ (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
|
||||
bridge:(RCTBridge *)bridge
|
||||
callback:(RCTImageLoaderCompletionBlock)callback
|
||||
{
|
||||
return [self loadImageWithTag:imageTag
|
||||
size:CGSizeZero
|
||||
scale:0
|
||||
resizeMode:UIViewContentModeScaleToFill
|
||||
callback:callback];
|
||||
bridge:bridge
|
||||
progressBlock:nil
|
||||
completionBlock:callback];
|
||||
}
|
||||
|
||||
+ (void)loadImageWithTag:(NSString *)imageTag
|
||||
size:(CGSize)size
|
||||
scale:(CGFloat)scale
|
||||
resizeMode:(UIViewContentMode)resizeMode
|
||||
callback:(void (^)(NSError *error, id image))callback
|
||||
// Why use a custom scaling method? Greater efficiency, reduced memory overhead:
|
||||
// http://www.mindsea.com/2012/12/downscaling-huge-alassets-without-fear-of-sigkill
|
||||
|
||||
static UIImage *RCTScaledImageForAsset(ALAssetRepresentation *representation,
|
||||
CGSize size, CGFloat scale,
|
||||
UIViewContentMode resizeMode,
|
||||
NSError **error)
|
||||
{
|
||||
NSUInteger length = (NSUInteger)representation.size;
|
||||
NSMutableData *data = [NSMutableData dataWithLength:length];
|
||||
if (![representation getBytes:data.mutableBytes
|
||||
fromOffset:0
|
||||
length:length
|
||||
error:error]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
CGSize sourceSize = representation.dimensions;
|
||||
CGRect targetRect = RCTClipRect(sourceSize, representation.scale, size, scale, resizeMode);
|
||||
CGSize targetSize = targetRect.size;
|
||||
|
||||
NSDictionary *options = @{
|
||||
(id)kCGImageSourceShouldAllowFloat: @YES,
|
||||
(id)kCGImageSourceCreateThumbnailWithTransform: @YES,
|
||||
(id)kCGImageSourceCreateThumbnailFromImageAlways: @YES,
|
||||
(id)kCGImageSourceThumbnailMaxPixelSize: @(MAX(targetSize.width, targetSize.height) * scale)
|
||||
};
|
||||
|
||||
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil);
|
||||
CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(sourceRef, 0, (__bridge CFDictionaryRef)options);
|
||||
if (sourceRef) {
|
||||
CFRelease(sourceRef);
|
||||
}
|
||||
|
||||
if (imageRef) {
|
||||
UIImage *image = [UIImage imageWithCGImage:imageRef scale:scale
|
||||
orientation:(UIImageOrientation)representation.orientation];
|
||||
CGImageRelease(imageRef);
|
||||
return image;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
|
||||
size:(CGSize)size
|
||||
scale:(CGFloat)scale
|
||||
resizeMode:(UIViewContentMode)resizeMode
|
||||
bridge:(RCTBridge *)bridge
|
||||
progressBlock:(RCTImageLoaderProgressBlock)progress
|
||||
completionBlock:(RCTImageLoaderCompletionBlock)completion
|
||||
{
|
||||
if ([imageTag hasPrefix:@"assets-library://"]) {
|
||||
[[RCTImageLoader assetsLibrary] assetForURL:[NSURL URLWithString:imageTag] resultBlock:^(ALAsset *asset) {
|
||||
[[self assetsLibrary] assetForURL:[NSURL URLWithString:imageTag] resultBlock:^(ALAsset *asset) {
|
||||
if (asset) {
|
||||
// ALAssetLibrary API is async and will be multi-threaded. Loading a few full
|
||||
// resolution images at once will spike the memory up to store the image data,
|
||||
|
@ -86,42 +137,32 @@ static dispatch_queue_t RCTImageLoaderQueue(void)
|
|||
@autoreleasepool {
|
||||
|
||||
BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero);
|
||||
ALAssetOrientation orientation = ALAssetOrientationUp;
|
||||
CGImageRef imageRef = NULL;
|
||||
ALAssetRepresentation *representation = [asset defaultRepresentation];
|
||||
|
||||
if (!useMaximumSize) {
|
||||
imageRef = asset.thumbnail;
|
||||
}
|
||||
if (RCTUpscalingRequired((CGSize){CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)}, 1, size, scale, resizeMode)) {
|
||||
if (!useMaximumSize) {
|
||||
imageRef = asset.aspectRatioThumbnail;
|
||||
}
|
||||
if (RCTUpscalingRequired((CGSize){CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)}, 1, size, scale, resizeMode)) {
|
||||
ALAssetRepresentation *representation = [asset defaultRepresentation];
|
||||
orientation = [representation orientation];
|
||||
if (!useMaximumSize) {
|
||||
imageRef = [representation fullScreenImage];
|
||||
}
|
||||
if (RCTUpscalingRequired((CGSize){CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)}, 1, size, scale, resizeMode)) {
|
||||
imageRef = [representation fullResolutionImage];
|
||||
}
|
||||
}
|
||||
UIImage *image;
|
||||
NSError *error = nil;
|
||||
if (useMaximumSize) {
|
||||
image = [UIImage imageWithCGImage:representation.fullResolutionImage
|
||||
scale:scale
|
||||
orientation:(UIImageOrientation)representation.orientation];
|
||||
} else {
|
||||
image = RCTScaledImageForAsset(representation, size, scale, resizeMode, &error);
|
||||
}
|
||||
|
||||
UIImage *image = [UIImage imageWithCGImage:imageRef scale:scale orientation:(UIImageOrientation)orientation];
|
||||
RCTDispatchCallbackOnMainQueue(callback, nil, image);
|
||||
RCTDispatchCallbackOnMainQueue(completion, error, image);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
NSString *errorText = [NSString stringWithFormat:@"Failed to load asset at URL %@ with no error message.", imageTag];
|
||||
NSError *error = RCTErrorWithMessage(errorText);
|
||||
RCTDispatchCallbackOnMainQueue(callback, error, nil);
|
||||
RCTDispatchCallbackOnMainQueue(completion, error, nil);
|
||||
}
|
||||
} failureBlock:^(NSError *loadError) {
|
||||
NSString *errorText = [NSString stringWithFormat:@"Failed to load asset at URL %@.\niOS Error: %@", imageTag, loadError];
|
||||
NSError *error = RCTErrorWithMessage(errorText);
|
||||
RCTDispatchCallbackOnMainQueue(callback, error, nil);
|
||||
RCTDispatchCallbackOnMainQueue(completion, error, nil);
|
||||
}];
|
||||
return ^{};
|
||||
} else if ([imageTag hasPrefix:@"ph://"]) {
|
||||
// Using PhotoKit for iOS 8+
|
||||
// The 'ph://' prefix is used by FBMediaKit to differentiate between
|
||||
|
@ -132,71 +173,103 @@ static dispatch_queue_t RCTImageLoaderQueue(void)
|
|||
if (results.count == 0) {
|
||||
NSString *errorText = [NSString stringWithFormat:@"Failed to fetch PHAsset with local identifier %@ with no error message.", phAssetID];
|
||||
NSError *error = RCTErrorWithMessage(errorText);
|
||||
RCTDispatchCallbackOnMainQueue(callback, error, nil);
|
||||
return;
|
||||
RCTDispatchCallbackOnMainQueue(completion, error, nil);
|
||||
return ^{};
|
||||
}
|
||||
|
||||
PHAsset *asset = [results firstObject];
|
||||
CGSize targetSize = CGSizeEqualToSize(size, CGSizeZero) ? PHImageManagerMaximumSize : size;
|
||||
|
||||
PHImageRequestOptions *imageOptions = [[PHImageRequestOptions alloc] init];
|
||||
|
||||
BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero);
|
||||
CGSize targetSize;
|
||||
|
||||
if ( useMaximumSize ){
|
||||
targetSize = PHImageManagerMaximumSize;
|
||||
imageOptions.resizeMode = PHImageRequestOptionsResizeModeNone;
|
||||
} else {
|
||||
targetSize = size;
|
||||
imageOptions.resizeMode = PHImageRequestOptionsResizeModeFast;
|
||||
}
|
||||
|
||||
PHImageContentMode contentMode = PHImageContentModeAspectFill;
|
||||
if (resizeMode == UIViewContentModeScaleAspectFit) {
|
||||
contentMode = PHImageContentModeAspectFit;
|
||||
}
|
||||
[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:targetSize contentMode:contentMode options:nil resultHandler:^(UIImage *result, NSDictionary *info) {
|
||||
[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:targetSize contentMode:contentMode options:imageOptions resultHandler:^(UIImage *result, NSDictionary *info) {
|
||||
if (result) {
|
||||
RCTDispatchCallbackOnMainQueue(callback, nil, result);
|
||||
RCTDispatchCallbackOnMainQueue(completion, nil, result);
|
||||
} else {
|
||||
NSString *errorText = [NSString stringWithFormat:@"Failed to load PHAsset with local identifier %@ with no error message.", phAssetID];
|
||||
NSError *error = RCTErrorWithMessage(errorText);
|
||||
RCTDispatchCallbackOnMainQueue(callback, error, nil);
|
||||
RCTDispatchCallbackOnMainQueue(completion, error, nil);
|
||||
return;
|
||||
}
|
||||
}];
|
||||
return ^{};
|
||||
} else if ([imageTag hasPrefix:@"http"]) {
|
||||
NSURL *url = [NSURL URLWithString:imageTag];
|
||||
if (!url) {
|
||||
NSString *errorMessage = [NSString stringWithFormat:@"Invalid URL: %@", imageTag];
|
||||
RCTDispatchCallbackOnMainQueue(callback, RCTErrorWithMessage(errorMessage), nil);
|
||||
return;
|
||||
RCTDispatchCallbackOnMainQueue(completion, RCTErrorWithMessage(errorMessage), nil);
|
||||
return ^{};
|
||||
}
|
||||
if ([[imageTag lowercaseString] hasSuffix:@".gif"]) {
|
||||
[[RCTImageDownloader sharedInstance] downloadDataForURL:url progressBlock:nil block:^(NSData *data, NSError *error) {
|
||||
if ([imageTag.lowercaseString hasSuffix:@".gif"]) {
|
||||
return [[RCTImageDownloader sharedInstance] downloadDataForURL:url progressBlock:progress block:^(NSData *data, NSError *error) {
|
||||
id image = RCTGIFImageWithFileURL([RCTConvert NSURL:imageTag]);
|
||||
if (!image && !error) {
|
||||
NSString *errorMessage = [NSString stringWithFormat:@"Unable to load GIF image: %@", imageTag];
|
||||
error = RCTErrorWithMessage(errorMessage);
|
||||
}
|
||||
RCTDispatchCallbackOnMainQueue(callback, error, image);
|
||||
RCTDispatchCallbackOnMainQueue(completion, error, image);
|
||||
}];
|
||||
} else {
|
||||
[[RCTImageDownloader sharedInstance] downloadImageForURL:url size:size scale:scale resizeMode:resizeMode tintColor:nil backgroundColor:nil progressBlock:NULL block:^(UIImage *image, NSError *error) {
|
||||
RCTDispatchCallbackOnMainQueue(callback, error, image);
|
||||
return [[RCTImageDownloader sharedInstance] downloadImageForURL:url size:size scale:scale resizeMode:resizeMode tintColor:nil backgroundColor:nil progressBlock:progress block:^(UIImage *image, NSError *error) {
|
||||
RCTDispatchCallbackOnMainQueue(completion, error, image);
|
||||
}];
|
||||
}
|
||||
} else if ([[imageTag lowercaseString] hasSuffix:@".gif"]) {
|
||||
} else if ([imageTag hasPrefix:@"rct-image-store://"]) {
|
||||
[bridge.imageStoreManager getImageForTag:imageTag withBlock:^(UIImage *image) {
|
||||
if (image) {
|
||||
RCTDispatchCallbackOnMainQueue(completion, nil, image);
|
||||
} else {
|
||||
NSString *errorMessage = [NSString stringWithFormat:@"Unable to load image from image store: %@", imageTag];
|
||||
NSError *error = RCTErrorWithMessage(errorMessage);
|
||||
RCTDispatchCallbackOnMainQueue(completion, error, nil);
|
||||
}
|
||||
}];
|
||||
return ^{};
|
||||
} else if ([imageTag.lowercaseString hasSuffix:@".gif"]) {
|
||||
id image = RCTGIFImageWithFileURL([RCTConvert NSURL:imageTag]);
|
||||
if (image) {
|
||||
RCTDispatchCallbackOnMainQueue(callback, nil, image);
|
||||
RCTDispatchCallbackOnMainQueue(completion, nil, image);
|
||||
} else {
|
||||
NSString *errorMessage = [NSString stringWithFormat:@"Unable to load GIF image: %@", imageTag];
|
||||
NSError *error = RCTErrorWithMessage(errorMessage);
|
||||
RCTDispatchCallbackOnMainQueue(callback, error, nil);
|
||||
RCTDispatchCallbackOnMainQueue(completion, error, nil);
|
||||
}
|
||||
return ^{};
|
||||
} else {
|
||||
UIImage *image = [RCTConvert UIImage:imageTag];
|
||||
if (image) {
|
||||
RCTDispatchCallbackOnMainQueue(callback, nil, image);
|
||||
RCTDispatchCallbackOnMainQueue(completion, nil, image);
|
||||
} else {
|
||||
NSString *errorMessage = [NSString stringWithFormat:@"Unrecognized tag protocol: %@", imageTag];
|
||||
NSError *error = RCTErrorWithMessage(errorMessage);
|
||||
RCTDispatchCallbackOnMainQueue(callback, error, nil);
|
||||
RCTDispatchCallbackOnMainQueue(completion, error, nil);
|
||||
}
|
||||
return ^{};
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)isAssetLibraryImage:(NSString *)imageTag
|
||||
{
|
||||
return [imageTag hasPrefix:@"assets-library://"] || [imageTag hasPrefix:@"ph:"];
|
||||
return [imageTag hasPrefix:@"assets-library://"] || [imageTag hasPrefix:@"ph://"];
|
||||
}
|
||||
|
||||
+ (BOOL)isRemoteImage:(NSString *)imageTag
|
||||
{
|
||||
return [imageTag hasPrefix:@"http://"] || [imageTag hasPrefix:@"https://"];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "RCTBridge.h"
|
||||
#import "RCTImageLoader.h"
|
||||
#import "RCTUtils.h"
|
||||
|
||||
|
@ -20,6 +21,8 @@
|
|||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
@synthesize bridge = _bridge;
|
||||
|
||||
- (BOOL)canHandleRequest:(NSURLRequest *)request
|
||||
{
|
||||
return [@[@"assets-library", @"ph"] containsObject:[request.URL.scheme lowercaseString]];
|
||||
|
@ -30,7 +33,7 @@ RCT_EXPORT_MODULE()
|
|||
{
|
||||
NSNumber *requestToken = @(++_currentToken);
|
||||
NSString *URLString = [request.URL absoluteString];
|
||||
[RCTImageLoader loadImageWithTag:URLString callback:^(NSError *error, UIImage *image) {
|
||||
[RCTImageLoader loadImageWithTag:URLString bridge:_bridge callback:^(NSError *error, UIImage *image) {
|
||||
if (error) {
|
||||
[delegate URLRequest:requestToken didCompleteWithError:error];
|
||||
return;
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "RCTBridge.h"
|
||||
#import "RCTURLRequestHandler.h"
|
||||
|
||||
@interface RCTImageStoreManager : NSObject<RCTURLRequestHandler>
|
||||
|
||||
/**
|
||||
* Set and get cached images. These must be called from the main thread.
|
||||
*/
|
||||
- (NSString *)storeImage:(UIImage *)image;
|
||||
- (UIImage *)imageForTag:(NSString *)imageTag;
|
||||
|
||||
/**
|
||||
* Set and get cached images asynchronously. It is safe to call these from any
|
||||
* thread. The callbacks will be called on the main thread.
|
||||
*/
|
||||
- (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block;
|
||||
- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block;
|
||||
|
||||
@end
|
||||
|
||||
@interface RCTBridge (RCTImageStoreManager)
|
||||
|
||||
@property (nonatomic, readonly) RCTImageStoreManager *imageStoreManager;
|
||||
|
||||
@end
|
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* 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 "RCTImageStoreManager.h"
|
||||
|
||||
#import "RCTAssert.h"
|
||||
#import "RCTUtils.h"
|
||||
|
||||
@implementation RCTImageStoreManager
|
||||
{
|
||||
NSMutableDictionary *_store;
|
||||
}
|
||||
|
||||
@synthesize methodQueue = _methodQueue;
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if ((self = [super init])) {
|
||||
|
||||
// TODO: need a way to clear this store
|
||||
_store = [[NSMutableDictionary alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)storeImage:(UIImage *)image
|
||||
{
|
||||
RCTAssertMainThread();
|
||||
NSString *tag = [NSString stringWithFormat:@"rct-image-store://%tu", [_store count]];
|
||||
_store[tag] = image;
|
||||
return tag;
|
||||
}
|
||||
|
||||
- (UIImage *)imageForTag:(NSString *)imageTag
|
||||
{
|
||||
RCTAssertMainThread();
|
||||
return _store[imageTag];
|
||||
}
|
||||
|
||||
- (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSString *imageTag = [self storeImage:image];
|
||||
if (block) {
|
||||
block(imageTag);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block
|
||||
{
|
||||
RCTAssert(block != nil, @"block must not be nil");
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
block([self imageForTag:imageTag]);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO (#5906496): Name could be more explicit - something like getBase64EncodedJPEGDataForTag:?
|
||||
RCT_EXPORT_METHOD(getBase64ForTag:(NSString *)imageTag
|
||||
successCallback:(RCTResponseSenderBlock)successCallback
|
||||
errorCallback:(RCTResponseErrorBlock)errorCallback)
|
||||
{
|
||||
[self getImageForTag:imageTag withBlock:^(UIImage *image) {
|
||||
if (!image) {
|
||||
errorCallback(RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]));
|
||||
return;
|
||||
}
|
||||
dispatch_async(_methodQueue, ^{
|
||||
NSData *imageData = UIImageJPEGRepresentation(image, 1.0);
|
||||
NSString *base64 = [imageData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
|
||||
successCallback(@[[base64 stringByReplacingOccurrencesOfString:@"\n" withString:@""]]);
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(addImageFromBase64:(NSString *)base64String
|
||||
successCallback:(RCTResponseSenderBlock)successCallback
|
||||
errorCallback:(RCTResponseErrorBlock)errorCallback)
|
||||
|
||||
{
|
||||
NSData *imageData = [[NSData alloc] initWithBase64EncodedString:base64String options:0];
|
||||
if (imageData) {
|
||||
UIImage *image = [[UIImage alloc] initWithData:imageData];
|
||||
[self storeImage:image withBlock:^(NSString *imageTag) {
|
||||
successCallback(@[imageTag]);
|
||||
}];
|
||||
} else {
|
||||
errorCallback(RCTErrorWithMessage(@"Failed to add image from base64String"));
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - RCTURLRequestHandler
|
||||
|
||||
- (BOOL)canHandleRequest:(NSURLRequest *)request
|
||||
{
|
||||
return [@[@"rct-image-store"] containsObject:[request.URL.scheme lowercaseString]];
|
||||
}
|
||||
|
||||
- (id)sendRequest:(NSURLRequest *)request
|
||||
withDelegate:(id<RCTURLRequestDelegate>)delegate
|
||||
{
|
||||
NSString *imageTag = [request.URL absoluteString];
|
||||
[self getImageForTag:imageTag withBlock:^(UIImage *image) {
|
||||
if (!image) {
|
||||
NSError *error = RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]);
|
||||
[delegate URLRequest:request didCompleteWithError:error];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *mimeType = nil;
|
||||
NSData *imageData = nil;
|
||||
if (RCTImageHasAlpha(image.CGImage)) {
|
||||
mimeType = @"image/png";
|
||||
imageData = UIImagePNGRepresentation(image);
|
||||
} else {
|
||||
mimeType = @"image/jpeg";
|
||||
imageData = UIImageJPEGRepresentation(image, 1.0);
|
||||
}
|
||||
|
||||
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL
|
||||
MIMEType:mimeType
|
||||
expectedContentLength:imageData.length
|
||||
textEncodingName:nil];
|
||||
|
||||
[delegate URLRequest:request didReceiveResponse:response];
|
||||
[delegate URLRequest:request didReceiveData:imageData];
|
||||
[delegate URLRequest:request didCompleteWithError:nil];
|
||||
}];
|
||||
return request;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTBridge (RCTImageStoreManager)
|
||||
|
||||
- (RCTImageStoreManager *)imageStoreManager
|
||||
{
|
||||
return self.modules[RCTBridgeModuleNameForClass([RCTImageStoreManager class])];
|
||||
}
|
||||
|
||||
@end
|
|
@ -11,6 +11,24 @@
|
|||
|
||||
#import "RCTLog.h"
|
||||
|
||||
static CGFloat RCTCeilValue(CGFloat value, CGFloat scale)
|
||||
{
|
||||
return ceil(value * scale) / scale;
|
||||
}
|
||||
|
||||
static CGFloat RCTFloorValue(CGFloat value, CGFloat scale)
|
||||
{
|
||||
return floor(value * scale) / scale;
|
||||
}
|
||||
|
||||
static CGSize RCTCeilSize(CGSize size, CGFloat scale)
|
||||
{
|
||||
return (CGSize){
|
||||
RCTCeilValue(size.width, scale),
|
||||
RCTCeilValue(size.height, scale)
|
||||
};
|
||||
}
|
||||
|
||||
CGSize RCTTargetSizeForClipRect(CGRect clipRect)
|
||||
{
|
||||
return (CGSize){
|
||||
|
@ -48,7 +66,7 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale,
|
|||
|
||||
sourceSize.width = MIN(destSize.width, sourceSize.width);
|
||||
sourceSize.height = MIN(destSize.height, sourceSize.height);
|
||||
return (CGRect){CGPointZero, sourceSize};
|
||||
return (CGRect){CGPointZero, RCTCeilSize(sourceSize, destScale)};
|
||||
|
||||
case UIViewContentModeScaleAspectFit: // contain
|
||||
|
||||
|
@ -62,7 +80,7 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale,
|
|||
sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height);
|
||||
sourceSize.width = sourceSize.height * aspect;
|
||||
}
|
||||
return (CGRect){CGPointZero, sourceSize};
|
||||
return (CGRect){CGPointZero, RCTCeilSize(sourceSize, destScale)};
|
||||
|
||||
case UIViewContentModeScaleAspectFill: // cover
|
||||
|
||||
|
@ -71,20 +89,26 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale,
|
|||
sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height);
|
||||
sourceSize.width = sourceSize.height * aspect;
|
||||
destSize.width = destSize.height * targetAspect;
|
||||
return (CGRect){{(destSize.width - sourceSize.width) / 2, 0}, sourceSize};
|
||||
return (CGRect){
|
||||
{RCTFloorValue((destSize.width - sourceSize.width) / 2, destScale), 0},
|
||||
RCTCeilSize(sourceSize, destScale)
|
||||
};
|
||||
|
||||
} else { // target is wider than content
|
||||
|
||||
sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width);
|
||||
sourceSize.height = sourceSize.width / aspect;
|
||||
destSize.height = destSize.width / targetAspect;
|
||||
return (CGRect){{0, (destSize.height - sourceSize.height) / 2}, sourceSize};
|
||||
return (CGRect){
|
||||
{0, RCTFloorValue((destSize.height - sourceSize.height) / 2, destScale)},
|
||||
RCTCeilSize(sourceSize, destScale)
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
RCTLogError(@"A resizeMode value of %zd is not supported", resizeMode);
|
||||
return (CGRect){CGPointZero, destSize};
|
||||
return (CGRect){CGPointZero, RCTCeilSize(destSize, destScale)};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,9 +9,14 @@
|
|||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface RCTStaticImage : UIImageView
|
||||
@class RCTBridge;
|
||||
|
||||
@interface RCTImageView : UIImageView
|
||||
|
||||
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@property (nonatomic, assign) UIEdgeInsets capInsets;
|
||||
@property (nonatomic, strong) UIImage *defaultImage;
|
||||
@property (nonatomic, assign) UIImageRenderingMode renderingMode;
|
||||
@property (nonatomic, copy) NSString *src;
|
||||
|
|
@ -7,16 +7,41 @@
|
|||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
#import "RCTStaticImage.h"
|
||||
#import "RCTImageView.h"
|
||||
|
||||
#import "RCTBridge.h"
|
||||
#import "RCTConvert.h"
|
||||
#import "RCTEventDispatcher.h"
|
||||
#import "RCTGIFImage.h"
|
||||
#import "RCTImageLoader.h"
|
||||
#import "RCTUtils.h"
|
||||
|
||||
#import "UIView+React.h"
|
||||
|
||||
@implementation RCTStaticImage
|
||||
@interface RCTImageView ()
|
||||
|
||||
@property (nonatomic, assign) BOOL onLoadStart;
|
||||
@property (nonatomic, assign) BOOL onProgress;
|
||||
@property (nonatomic, assign) BOOL onError;
|
||||
@property (nonatomic, assign) BOOL onLoad;
|
||||
@property (nonatomic, assign) BOOL onLoadEnd;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTImageView
|
||||
{
|
||||
RCTBridge *_bridge;
|
||||
}
|
||||
|
||||
- (instancetype)initWithBridge:(RCTBridge *)bridge
|
||||
{
|
||||
if ((self = [super init])) {
|
||||
_bridge = bridge;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
RCT_NOT_IMPLEMENTED(-init)
|
||||
|
||||
- (void)_updateImage
|
||||
{
|
||||
|
@ -45,7 +70,7 @@
|
|||
- (void)setImage:(UIImage *)image
|
||||
{
|
||||
if (image != super.image) {
|
||||
super.image = image;
|
||||
super.image = image ?: _defaultImage;
|
||||
[self _updateImage];
|
||||
}
|
||||
}
|
||||
|
@ -77,19 +102,56 @@
|
|||
- (void)reloadImage
|
||||
{
|
||||
if (_src && !CGSizeEqualToSize(self.frame.size, CGSizeZero)) {
|
||||
|
||||
if (_onLoadStart) {
|
||||
NSDictionary *event = @{ @"target": self.reactTag };
|
||||
[_bridge.eventDispatcher sendInputEventWithName:@"loadStart" body:event];
|
||||
}
|
||||
|
||||
RCTImageLoaderProgressBlock progressHandler = nil;
|
||||
if (_onProgress) {
|
||||
progressHandler = ^(int64_t loaded, int64_t total) {
|
||||
NSDictionary *event = @{
|
||||
@"target": self.reactTag,
|
||||
@"loaded": @(loaded),
|
||||
@"total": @(total),
|
||||
};
|
||||
[_bridge.eventDispatcher sendInputEventWithName:@"progress" body:event];
|
||||
};
|
||||
}
|
||||
|
||||
[RCTImageLoader loadImageWithTag:_src
|
||||
size:self.bounds.size
|
||||
scale:RCTScreenScale()
|
||||
resizeMode:self.contentMode callback:^(NSError *error, id image) {
|
||||
if (error) {
|
||||
RCTLogWarn(@"%@", error.localizedDescription);
|
||||
}
|
||||
resizeMode:self.contentMode
|
||||
bridge:_bridge
|
||||
progressBlock:progressHandler
|
||||
completionBlock:^(NSError *error, id image) {
|
||||
|
||||
if ([image isKindOfClass:[CAAnimation class]]) {
|
||||
[self.layer addAnimation:image forKey:@"contents"];
|
||||
} else {
|
||||
[self.layer removeAnimationForKey:@"contents"];
|
||||
self.image = image;
|
||||
}
|
||||
if (error) {
|
||||
if (_onError) {
|
||||
NSDictionary *event = @{
|
||||
@"target": self.reactTag,
|
||||
@"error": error.localizedDescription,
|
||||
};
|
||||
[_bridge.eventDispatcher sendInputEventWithName:@"error" body:event];
|
||||
}
|
||||
} else {
|
||||
if (_onLoad) {
|
||||
NSDictionary *event = @{ @"target": self.reactTag };
|
||||
[_bridge.eventDispatcher sendInputEventWithName:@"load" body:event];
|
||||
}
|
||||
}
|
||||
if (_onLoadEnd) {
|
||||
NSDictionary *event = @{ @"target": self.reactTag };
|
||||
[_bridge.eventDispatcher sendInputEventWithName:@"loadEnd" body:event];
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
[self.layer removeAnimationForKey:@"contents"];
|
||||
|
@ -102,7 +164,7 @@
|
|||
[super reactSetFrame:frame];
|
||||
if (self.image == nil) {
|
||||
[self reloadImage];
|
||||
} else if ([RCTImageLoader isAssetLibraryImage:_src]) {
|
||||
} else if ([RCTImageLoader isAssetLibraryImage:_src] || [RCTImageLoader isRemoteImage:_src]) {
|
||||
CGSize imageSize = {
|
||||
self.image.size.width / RCTScreenScale(),
|
||||
self.image.size.height / RCTScreenScale()
|
||||
|
@ -116,4 +178,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
- (void)willMoveToSuperview:(UIView *)newSuperview
|
||||
{
|
||||
[super willMoveToSuperview:newSuperview];
|
||||
|
||||
if (!newSuperview) {
|
||||
[self.layer removeAnimationForKey:@"contents"];
|
||||
self.image = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didMoveToSuperview
|
||||
{
|
||||
[super didMoveToSuperview];
|
||||
|
||||
if (self.superview && self.src) {
|
||||
[self reloadImage];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -9,6 +9,6 @@
|
|||
|
||||
#import "RCTViewManager.h"
|
||||
|
||||
@interface RCTStaticImageManager : RCTViewManager
|
||||
@interface RCTImageViewManager : RCTViewManager
|
||||
|
||||
@end
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* 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 "RCTImageViewManager.h"
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "RCTConvert.h"
|
||||
#import "RCTImageView.h"
|
||||
|
||||
@implementation RCTImageViewManager
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
- (UIView *)view
|
||||
{
|
||||
return [[RCTImageView alloc] initWithBridge:self.bridge];
|
||||
}
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(capInsets, UIEdgeInsets)
|
||||
RCT_REMAP_VIEW_PROPERTY(defaultImageSrc, defaultImage, UIImage)
|
||||
RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode)
|
||||
RCT_EXPORT_VIEW_PROPERTY(src, NSString)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onLoadStart, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onProgress, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onError, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onLoad, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onLoadEnd, BOOL)
|
||||
RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTImageView)
|
||||
{
|
||||
if (json) {
|
||||
view.renderingMode = UIImageRenderingModeAlwaysTemplate;
|
||||
view.tintColor = [RCTConvert UIColor:json];
|
||||
} else {
|
||||
view.renderingMode = defaultView.renderingMode;
|
||||
view.tintColor = defaultView.tintColor;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSDictionary *)customDirectEventTypes
|
||||
{
|
||||
return @{
|
||||
@"loadStart": @{ @"registrationName": @"onLoadStart" },
|
||||
@"progress": @{ @"registrationName": @"onProgress" },
|
||||
@"error": @{ @"registrationName": @"onError" },
|
||||
@"load": @{ @"registrationName": @"onLoad" },
|
||||
@"loadEnd": @{ @"registrationName": @"onLoadEnd" },
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,46 +0,0 @@
|
|||
/**
|
||||
* 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 <UIKit/UIKit.h>
|
||||
|
||||
@class RCTEventDispatcher;
|
||||
@class RCTImageDownloader;
|
||||
|
||||
@interface RCTNetworkImageView : UIView
|
||||
|
||||
- (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,
|
||||
* or when imageURL is nil. Defaults to nil.
|
||||
*/
|
||||
@property (nonatomic, strong) UIImage *defaultImage;
|
||||
|
||||
/**
|
||||
* Specify a URL for an image. The image will be asynchronously loaded and displayed.
|
||||
*/
|
||||
@property (nonatomic, strong) NSURL *imageURL;
|
||||
|
||||
/**
|
||||
* Whether the image should be masked with this view's tint color.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL tinted;
|
||||
|
||||
/**
|
||||
* By default, changing imageURL will reset whatever existing image was present
|
||||
* and revert to defaultImage while the new image loads. In certain obscure cases you
|
||||
* may want to disable this behavior and instead keep displaying the previous image
|
||||
* while the new one loads. In this case, pass NO for resetToDefaultImageWhileLoading.
|
||||
* (If you set imageURL to nil, however, resetToDefaultImageWhileLoading is ignored;
|
||||
* that will always reset to the default image.)
|
||||
*/
|
||||
- (void)setImageURL:(NSURL *)imageURL resetToDefaultImageWhileLoading:(BOOL)reset;
|
||||
|
||||
@end
|
|
@ -1,220 +0,0 @@
|
|||
/**
|
||||
* 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 "RCTNetworkImageView.h"
|
||||
|
||||
#import "RCTAssert.h"
|
||||
#import "RCTConvert.h"
|
||||
#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)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher imageDownloader:(RCTImageDownloader *)imageDownloader
|
||||
{
|
||||
if ((self = [super initWithFrame:CGRectZero])) {
|
||||
_eventDispatcher = eventDispatcher;
|
||||
_progressHandlerRegistered = NO;
|
||||
_deferSentinel = 0;
|
||||
_imageDownloader = imageDownloader;
|
||||
self.userInteractionEnabled = NO;
|
||||
self.contentMode = UIViewContentModeScaleAspectFill;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame)
|
||||
RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
|
||||
|
||||
- (NSURL *)imageURL
|
||||
{
|
||||
// We clear our imageURL when we are not in a window for a while,
|
||||
// to make sure we don't consume network resources while offscreen.
|
||||
// However we don't want to expose this hackery externally.
|
||||
return _deferred ? _deferredImageURL : _imageURL;
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
super.backgroundColor = backgroundColor;
|
||||
[self _updateImage];
|
||||
}
|
||||
|
||||
- (void)setTintColor:(UIColor *)tintColor
|
||||
{
|
||||
super.tintColor = tintColor;
|
||||
[self _updateImage];
|
||||
}
|
||||
|
||||
- (void)setProgressHandlerRegistered:(BOOL)progressHandlerRegistered
|
||||
{
|
||||
_progressHandlerRegistered = progressHandlerRegistered;
|
||||
}
|
||||
|
||||
- (void)reactSetFrame:(CGRect)frame
|
||||
{
|
||||
[super reactSetFrame:frame];
|
||||
[self _updateImage];
|
||||
}
|
||||
|
||||
- (void)_updateImage
|
||||
{
|
||||
[self setImageURL:_imageURL resetToDefaultImageWhileLoading:NO];
|
||||
}
|
||||
|
||||
- (void)setImageURL:(NSURL *)imageURL resetToDefaultImageWhileLoading:(BOOL)reset
|
||||
{
|
||||
if (![_imageURL isEqual:imageURL] && _downloadToken) {
|
||||
[_imageDownloader cancelDownload:_downloadToken];
|
||||
NSDictionary *event = @{ @"target": self.reactTag };
|
||||
[_eventDispatcher sendInputEventWithName:@"loadAbort" body:event];
|
||||
_downloadToken = nil;
|
||||
}
|
||||
|
||||
_imageURL = imageURL;
|
||||
|
||||
if (_deferred) {
|
||||
_deferredImageURL = imageURL;
|
||||
} else {
|
||||
if (!imageURL) {
|
||||
self.layer.contents = nil;
|
||||
return;
|
||||
}
|
||||
if (reset) {
|
||||
self.layer.contentsScale = _defaultImage.scale;
|
||||
self.layer.contents = (__bridge id)_defaultImage.CGImage;
|
||||
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 progressBlock:progressHandler block:^(NSData *data, NSError *error) {
|
||||
if (data) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (imageURL != self.imageURL) {
|
||||
// Image has changed
|
||||
return;
|
||||
}
|
||||
CAKeyframeAnimation *animation = RCTGIFImageWithData(data);
|
||||
self.layer.contentsScale = 1.0;
|
||||
self.layer.minificationFilter = kCAFilterLinear;
|
||||
self.layer.magnificationFilter = kCAFilterLinear;
|
||||
[self.layer addAnimation:animation forKey:@"contents"];
|
||||
loadEndHandler();
|
||||
});
|
||||
} else if (error) {
|
||||
errorHandler([error localizedDescription]);
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
_downloadToken = [_imageDownloader downloadImageForURL:imageURL
|
||||
size:self.bounds.size
|
||||
scale:RCTScreenScale()
|
||||
resizeMode:self.contentMode
|
||||
tintColor:_tinted ? self.tintColor : nil
|
||||
backgroundColor:self.backgroundColor
|
||||
progressBlock:progressHandler
|
||||
block:^(UIImage *image, NSError *error) {
|
||||
if (image) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (imageURL != self.imageURL) {
|
||||
// Image has changed
|
||||
return;
|
||||
}
|
||||
[self.layer removeAnimationForKey:@"contents"];
|
||||
self.layer.contentsScale = image.scale;
|
||||
self.layer.contents = (__bridge id)image.CGImage;
|
||||
loadEndHandler();
|
||||
});
|
||||
} else if (error) {
|
||||
errorHandler([error localizedDescription]);
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setImageURL:(NSURL *)imageURL
|
||||
{
|
||||
[self setImageURL:imageURL resetToDefaultImageWhileLoading:YES];
|
||||
}
|
||||
|
||||
- (void)willMoveToWindow:(UIWindow *)newWindow
|
||||
{
|
||||
[super willMoveToWindow:newWindow];
|
||||
if (newWindow != nil && _deferredImageURL) {
|
||||
// Immediately exit deferred mode and restore the imageURL that we saved when we went offscreen.
|
||||
[self setImageURL:_deferredImageURL resetToDefaultImageWhileLoading:YES];
|
||||
_deferredImageURL = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_enterDeferredModeIfNeededForSentinel:(NSUInteger)sentinel
|
||||
{
|
||||
if (self.window == nil && _deferSentinel == sentinel) {
|
||||
_deferred = YES;
|
||||
[_imageDownloader cancelDownload:_downloadToken];
|
||||
_downloadToken = nil;
|
||||
_deferredImageURL = _imageURL;
|
||||
_imageURL = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow
|
||||
{
|
||||
[super didMoveToWindow];
|
||||
if (self.window == nil) {
|
||||
__weak RCTNetworkImageView *weakSelf = self;
|
||||
NSUInteger sentinelAtDispatchTime = ++_deferSentinel;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC), dispatch_get_main_queue(), ^(void){
|
||||
[weakSelf _enterDeferredModeIfNeededForSentinel:sentinelAtDispatchTime];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,15 +0,0 @@
|
|||
/**
|
||||
* 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 "RCTViewManager.h"
|
||||
|
||||
@interface RCTNetworkImageViewManager : RCTViewManager
|
||||
|
||||
@end
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
/**
|
||||
* 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 "RCTNetworkImageViewManager.h"
|
||||
|
||||
#import "RCTBridge.h"
|
||||
#import "RCTConvert.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] 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)
|
||||
RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTNetworkImageView)
|
||||
{
|
||||
if (json) {
|
||||
view.tinted = YES;
|
||||
view.tintColor = [RCTConvert UIColor:json];
|
||||
} else {
|
||||
view.tinted = defaultView.tinted;
|
||||
view.tintColor = defaultView.tintColor;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSDictionary *)customDirectEventTypes
|
||||
{
|
||||
return @{
|
||||
@"loadStart": @{ @"registrationName": @"onLoadStart" },
|
||||
@"loadProgress": @{ @"registrationName": @"onLoadProgress" },
|
||||
@"loaded": @{ @"registrationName": @"onLoadEnd" },
|
||||
@"loadError": @{ @"registrationName": @"onLoadError" },
|
||||
@"loadAbort": @{ @"registrationName": @"onLoadAbort" },
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,41 +0,0 @@
|
|||
/**
|
||||
* 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 "RCTStaticImageManager.h"
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "RCTConvert.h"
|
||||
#import "RCTStaticImage.h"
|
||||
|
||||
@implementation RCTStaticImageManager
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
- (UIView *)view
|
||||
{
|
||||
return [[RCTStaticImage alloc] init];
|
||||
}
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(capInsets, UIEdgeInsets)
|
||||
RCT_REMAP_VIEW_PROPERTY(imageTag, src, NSString)
|
||||
RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode)
|
||||
RCT_EXPORT_VIEW_PROPERTY(src, NSString)
|
||||
RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTStaticImage)
|
||||
{
|
||||
if (json) {
|
||||
view.renderingMode = UIImageRenderingModeAlwaysTemplate;
|
||||
view.tintColor = [RCTConvert UIColor:json];
|
||||
} else {
|
||||
view.renderingMode = defaultView.renderingMode;
|
||||
view.tintColor = defaultView.tintColor;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -75,6 +75,16 @@ RCT_EXPORT_MODULE()
|
|||
|
||||
#pragma mark - NSURLSession delegate
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
task:(NSURLSessionTask *)task
|
||||
didSendBodyData:(int64_t)bytesSent
|
||||
totalBytesSent:(int64_t)totalBytesSent
|
||||
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
|
||||
{
|
||||
[[_delegates objectForKey:task] URLRequest:task didUploadProgress:(double)totalBytesSent total:(double)totalBytesExpectedToSend];
|
||||
}
|
||||
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
dataTask:(NSURLSessionDataTask *)task
|
||||
didReceiveResponse:(NSURLResponse *)response
|
||||
|
|
|
@ -184,6 +184,11 @@ typedef void (^RCTDataLoaderCallback)(NSData *data, NSString *MIMEType, NSError
|
|||
return [self initWithRequest:nil handler:nil callback:nil];
|
||||
}
|
||||
|
||||
- (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total
|
||||
{
|
||||
RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen");
|
||||
}
|
||||
|
||||
- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response
|
||||
{
|
||||
RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen");
|
||||
|
@ -230,15 +235,13 @@ RCT_EXPORT_MODULE()
|
|||
}
|
||||
|
||||
- (void)buildRequest:(NSDictionary *)query
|
||||
responseSender:(RCTResponseSenderBlock)responseSender
|
||||
completionBlock:(void (^)(NSURLRequest *request))block
|
||||
{
|
||||
NSURL *URL = [RCTConvert NSURL:query[@"url"]];
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
|
||||
request.HTTPMethod = [[RCTConvert NSString:query[@"method"]] uppercaseString] ?: @"GET";
|
||||
request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]];
|
||||
|
||||
BOOL incrementalUpdates = [RCTConvert BOOL:query[@"incrementalUpdates"]];
|
||||
|
||||
NSDictionary *data = [RCTConvert NSDictionary:query[@"data"]];
|
||||
[self processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary *result) {
|
||||
if (error) {
|
||||
|
@ -258,9 +261,7 @@ RCT_EXPORT_MODULE()
|
|||
[request setValue:[@(request.HTTPBody.length) description] forHTTPHeaderField:@"Content-Length"];
|
||||
}
|
||||
|
||||
[self sendRequest:request
|
||||
incrementalUpdates:incrementalUpdates
|
||||
responseSender:responseSender];
|
||||
block(request);
|
||||
}];
|
||||
}
|
||||
|
||||
|
@ -399,6 +400,17 @@ RCT_EXPORT_MODULE()
|
|||
|
||||
#pragma mark - RCTURLRequestDelegate
|
||||
|
||||
- (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total
|
||||
{
|
||||
dispatch_async(_methodQueue, ^{
|
||||
RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken];
|
||||
RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken);
|
||||
|
||||
NSArray *responseJSON = @[request.requestID, @(progress), @(total)];
|
||||
[_bridge.eventDispatcher sendDeviceEventWithName:@"didUploadProgress" body:responseJSON];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response
|
||||
{
|
||||
dispatch_async(_methodQueue, ^{
|
||||
|
@ -464,7 +476,12 @@ RCT_EXPORT_MODULE()
|
|||
RCT_EXPORT_METHOD(sendRequest:(NSDictionary *)query
|
||||
responseSender:(RCTResponseSenderBlock)responseSender)
|
||||
{
|
||||
[self buildRequest:query responseSender:responseSender];
|
||||
[self buildRequest:query completionBlock:^(NSURLRequest *request) {
|
||||
|
||||
BOOL incrementalUpdates = [RCTConvert BOOL:query[@"incrementalUpdates"]];
|
||||
[self sendRequest:request incrementalUpdates:incrementalUpdates
|
||||
responseSender:responseSender];
|
||||
}];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(cancelRequest:(NSNumber *)requestID)
|
||||
|
|
|
@ -21,15 +21,23 @@ class XMLHttpRequest extends XMLHttpRequestBase {
|
|||
|
||||
_requestId: ?number;
|
||||
_subscriptions: [any];
|
||||
upload: {
|
||||
onprogress?: (event: Object) => void;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._requestId = null;
|
||||
this._subscriptions = [];
|
||||
this.upload = {};
|
||||
}
|
||||
|
||||
_didCreateRequest(requestId: number): void {
|
||||
this._requestId = requestId;
|
||||
this._subscriptions.push(RCTDeviceEventEmitter.addListener(
|
||||
'didUploadProgress',
|
||||
(args) => this._didUploadProgress.call(this, args[0], args[1], args[2])
|
||||
));
|
||||
this._subscriptions.push(RCTDeviceEventEmitter.addListener(
|
||||
'didReceiveNetworkResponse',
|
||||
(args) => this._didReceiveResponse.call(this, args[0], args[1], args[2])
|
||||
|
@ -44,6 +52,17 @@ class XMLHttpRequest extends XMLHttpRequestBase {
|
|||
));
|
||||
}
|
||||
|
||||
_didUploadProgress(requestId: number, progress: number, total: number): void {
|
||||
if (requestId === this._requestId && this.upload.onprogress) {
|
||||
var event = {
|
||||
lengthComputable: true,
|
||||
loaded: progress,
|
||||
total,
|
||||
};
|
||||
this.upload.onprogress(event);
|
||||
}
|
||||
}
|
||||
|
||||
_didReceiveResponse(requestId: number, status: number, responseHeaders: ?Object): void {
|
||||
if (requestId === this._requestId) {
|
||||
this.status = status;
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
* Copyright 2004-present Facebook. All Rights Reserved.
|
||||
*
|
||||
* @providesModule Portal
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('React');
|
||||
var StyleSheet = require('StyleSheet');
|
||||
var View = require('View');
|
||||
|
||||
var _portalRef: any;
|
||||
|
||||
// Unique identifiers for modals.
|
||||
var lastUsedTag = 0;
|
||||
|
||||
/*
|
||||
* A container that renders all the modals on top of everything else in the application.
|
||||
*
|
||||
* Portal makes it possible for application code to pass modal views all the way up to
|
||||
* the root element created in `renderApplication`.
|
||||
*
|
||||
* Never use `<Portal>` in your code. There is only one Portal instance rendered
|
||||
* by the top-level `renderApplication`.
|
||||
*/
|
||||
var Portal = React.createClass({
|
||||
statics: {
|
||||
/**
|
||||
* Use this to create a new unique tag for your component that renders
|
||||
* modals. A good place to allocate a tag is in `componentWillMount`
|
||||
* of your component.
|
||||
* See `showModal` and `closeModal`.
|
||||
*/
|
||||
allocateTag: function(): string {
|
||||
return '__modal_' + (++lastUsedTag);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render a new modal.
|
||||
* @param tag A unique tag identifying the React component to render.
|
||||
* This tag can be later used in `closeModal`.
|
||||
* @param component A React component to be rendered.
|
||||
*/
|
||||
showModal: function(tag: string, component: any) {
|
||||
if (!_portalRef) {
|
||||
console.error('Calling showModal but no Portal has been rendered.');
|
||||
return;
|
||||
}
|
||||
_portalRef._showModal(tag, component);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a modal from the collection of modals to be rendered.
|
||||
* @param tag A unique tag identifying the React component to remove.
|
||||
* Must exactly match the tag previously passed to `showModal`.
|
||||
*/
|
||||
closeModal: function(tag: string) {
|
||||
if (!_portalRef) {
|
||||
console.error('Calling closeModal but no Portal has been rendered.');
|
||||
return;
|
||||
}
|
||||
_portalRef._closeModal(tag);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an array of all the open modals, as identified by their tag string.
|
||||
*/
|
||||
getOpenModals: function(): Array<string> {
|
||||
if (!_portalRef) {
|
||||
console.error('Calling getOpenModals but no Portal has been rendered.');
|
||||
return [];
|
||||
}
|
||||
return _portalRef._getOpenModals();
|
||||
}
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {modals: {}};
|
||||
},
|
||||
|
||||
_showModal: function(tag: string, component: any) {
|
||||
// This way state is chained through multiple calls to
|
||||
// _showModal, _closeModal correctly.
|
||||
this.setState((state) => {
|
||||
var modals = state.modals;
|
||||
modals[tag] = component;
|
||||
return {modals};
|
||||
});
|
||||
},
|
||||
|
||||
_closeModal: function(tag: string) {
|
||||
if (!this.state.modals.hasOwnProperty(tag)) {
|
||||
return;
|
||||
}
|
||||
// This way state is chained through multiple calls to
|
||||
// _showModal, _closeModal correctly.
|
||||
this.setState((state) => {
|
||||
var modals = state.modals;
|
||||
delete modals[tag];
|
||||
return {modals};
|
||||
});
|
||||
},
|
||||
|
||||
_getOpenModals: function(): Array<string> {
|
||||
return Object.keys(this.state.modals);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
_portalRef = this;
|
||||
if (!this.state.modals) {
|
||||
return null;
|
||||
}
|
||||
var modals = [];
|
||||
for (var tag in this.state.modals) {
|
||||
modals.push(this.state.modals[tag]);
|
||||
}
|
||||
if (modals.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View style={styles.modalsContainer}>
|
||||
{modals}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
modalsContainer: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = Portal;
|
|
@ -174,15 +174,13 @@ var WarningRow = React.createClass({
|
|||
{...this.panGesture.panHandlers}>
|
||||
<TouchableOpacity
|
||||
onPress={this.props.onOpened}>
|
||||
<View>
|
||||
<Text
|
||||
style={styles.warningText}
|
||||
numberOfLines={2}
|
||||
ref={text => { this.text = text; }}>
|
||||
{countText}
|
||||
{this.props.warning}
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
style={styles.warningText}
|
||||
numberOfLines={2}
|
||||
ref={text => { this.text = text; }}>
|
||||
{countText}
|
||||
{this.props.warning}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View
|
||||
ref={closeButton => { this.closeButton = closeButton; }}
|
||||
|
@ -212,30 +210,27 @@ var WarningBoxOpened = React.createClass({
|
|||
return (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={this.props.onClose}>
|
||||
<View style={styles.yellowBox}>
|
||||
<Text style={styles.yellowBoxText}>
|
||||
{countText}
|
||||
{this.props.warning}
|
||||
</Text>
|
||||
<View style={styles.yellowBoxButtons}>
|
||||
<View style={styles.yellowBoxButton}>
|
||||
<TouchableOpacity
|
||||
onPress={this.props.onDismissed}>
|
||||
<Text style={styles.yellowBoxButtonText}>
|
||||
Dismiss
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.yellowBoxButton}>
|
||||
<TouchableOpacity
|
||||
onPress={this.props.onIgnored}>
|
||||
<Text style={styles.yellowBoxButtonText}>
|
||||
Ignore
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
onPress={this.props.onClose}
|
||||
style={styles.yellowBox}>
|
||||
<Text style={styles.yellowBoxText}>
|
||||
{countText}
|
||||
{this.props.warning}
|
||||
</Text>
|
||||
<View style={styles.yellowBoxButtons}>
|
||||
<TouchableOpacity
|
||||
onPress={this.props.onDismissed}
|
||||
style={styles.yellowBoxButton}>
|
||||
<Text style={styles.yellowBoxButtonText}>
|
||||
Dismiss
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={this.props.onIgnored}
|
||||
style={styles.yellowBoxButton}>
|
||||
<Text style={styles.yellowBoxButtonText}>
|
||||
Ignore
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
|
|
@ -11,13 +11,15 @@
|
|||
|
||||
@class RCTEventDispatcher;
|
||||
|
||||
@interface RCTTextField : UITextField
|
||||
@interface RCTTextField : UITextField<UITextFieldDelegate>
|
||||
|
||||
@property (nonatomic, assign) BOOL caretHidden;
|
||||
@property (nonatomic, assign) BOOL autoCorrect;
|
||||
@property (nonatomic, assign) BOOL selectTextOnFocus;
|
||||
@property (nonatomic, assign) UIEdgeInsets contentInset;
|
||||
@property (nonatomic, strong) UIColor *placeholderTextColor;
|
||||
@property (nonatomic, assign) NSInteger mostRecentEventCount;
|
||||
@property (nonatomic, strong) NSNumber *maxLength;
|
||||
|
||||
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
RCTEventDispatcher *_eventDispatcher;
|
||||
NSMutableArray *_reactSubviews;
|
||||
BOOL _jsRequestingFirstResponder;
|
||||
NSInteger _nativeEventCount;
|
||||
}
|
||||
|
||||
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
|
||||
|
@ -31,6 +32,7 @@
|
|||
[self addTarget:self action:@selector(_textFieldEndEditing) forControlEvents:UIControlEventEditingDidEnd];
|
||||
[self addTarget:self action:@selector(_textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit];
|
||||
_reactSubviews = [[NSMutableArray alloc] init];
|
||||
self.delegate = self;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
@ -38,10 +40,40 @@
|
|||
RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame)
|
||||
RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
|
||||
|
||||
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
|
||||
{
|
||||
if (_maxLength == nil || [string isEqualToString:@"\n"]) { // Make sure forms can be submitted via return
|
||||
return YES;
|
||||
}
|
||||
NSUInteger allowedLength = _maxLength.integerValue - textField.text.length + range.length;
|
||||
if (string.length > allowedLength) {
|
||||
if (string.length > 1) {
|
||||
// Truncate the input string so the result is exactly maxLength
|
||||
NSString *limitedString = [string substringToIndex:allowedLength];
|
||||
NSMutableString *newString = textField.text.mutableCopy;
|
||||
[newString replaceCharactersInRange:range withString:limitedString];
|
||||
textField.text = newString;
|
||||
// Collapse selection at end of insert to match normal paste behavior
|
||||
UITextPosition *insertEnd = [textField positionFromPosition:textField.beginningOfDocument
|
||||
offset:(range.location + allowedLength)];
|
||||
textField.selectedTextRange = [textField textRangeFromPosition:insertEnd toPosition:insertEnd];
|
||||
[self _textFieldDidChange];
|
||||
}
|
||||
return NO;
|
||||
} else {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setText:(NSString *)text
|
||||
{
|
||||
if (![text isEqualToString:self.text]) {
|
||||
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
|
||||
if (eventLag == 0 && ![text isEqualToString:self.text]) {
|
||||
UITextRange *selection = self.selectedTextRange;
|
||||
[super setText:text];
|
||||
self.selectedTextRange = selection; // maintain cursor position/selection - this is robust to out of bounds
|
||||
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
|
||||
RCTLogWarn(@"Native TextInput(%@) is %ld events ahead of JS - try to make your JS faster.", self.text, (long)eventLag);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,17 +154,29 @@ static void RCTUpdatePlaceholder(RCTTextField *self)
|
|||
return self.autocorrectionType == UITextAutocorrectionTypeYes;
|
||||
}
|
||||
|
||||
#define RCT_TEXT_EVENT_HANDLER(delegateMethod, eventName) \
|
||||
- (void)delegateMethod \
|
||||
{ \
|
||||
[_eventDispatcher sendTextEventWithType:eventName \
|
||||
reactTag:self.reactTag \
|
||||
text:self.text]; \
|
||||
- (void)_textFieldDidChange
|
||||
{
|
||||
_nativeEventCount++;
|
||||
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange
|
||||
reactTag:self.reactTag
|
||||
text:self.text
|
||||
eventCount:_nativeEventCount];
|
||||
}
|
||||
|
||||
RCT_TEXT_EVENT_HANDLER(_textFieldDidChange, RCTTextEventTypeChange)
|
||||
RCT_TEXT_EVENT_HANDLER(_textFieldEndEditing, RCTTextEventTypeEnd)
|
||||
RCT_TEXT_EVENT_HANDLER(_textFieldSubmitEditing, RCTTextEventTypeSubmit)
|
||||
- (void)_textFieldEndEditing
|
||||
{
|
||||
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd
|
||||
reactTag:self.reactTag
|
||||
text:self.text
|
||||
eventCount:_nativeEventCount];
|
||||
}
|
||||
- (void)_textFieldSubmitEditing
|
||||
{
|
||||
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit
|
||||
reactTag:self.reactTag
|
||||
text:self.text
|
||||
eventCount:_nativeEventCount];
|
||||
}
|
||||
|
||||
- (void)_textFieldBeginEditing
|
||||
{
|
||||
|
@ -143,11 +187,10 @@ RCT_TEXT_EVENT_HANDLER(_textFieldSubmitEditing, RCTTextEventTypeSubmit)
|
|||
}
|
||||
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
|
||||
reactTag:self.reactTag
|
||||
text:self.text];
|
||||
text:self.text
|
||||
eventCount:_nativeEventCount];
|
||||
}
|
||||
|
||||
// TODO: we should support shouldChangeTextInRect (see UITextFieldDelegate)
|
||||
|
||||
- (BOOL)becomeFirstResponder
|
||||
{
|
||||
_jsRequestingFirstResponder = YES;
|
||||
|
@ -163,7 +206,8 @@ RCT_TEXT_EVENT_HANDLER(_textFieldSubmitEditing, RCTTextEventTypeSubmit)
|
|||
{
|
||||
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur
|
||||
reactTag:self.reactTag
|
||||
text:self.text];
|
||||
text:self.text
|
||||
eventCount:_nativeEventCount];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ RCT_REMAP_VIEW_PROPERTY(editable, enabled, BOOL)
|
|||
RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
|
||||
RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor)
|
||||
RCT_EXPORT_VIEW_PROPERTY(text, NSString)
|
||||
RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber)
|
||||
RCT_EXPORT_VIEW_PROPERTY(clearButtonMode, UITextFieldViewMode)
|
||||
RCT_REMAP_VIEW_PROPERTY(clearTextOnFocus, clearsOnBeginEditing, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL)
|
||||
|
@ -56,6 +57,7 @@ RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextField)
|
|||
{
|
||||
view.font = [RCTConvert UIFont:view.font withFamily:json ?: defaultView.font.familyName];
|
||||
}
|
||||
RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger)
|
||||
|
||||
- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView
|
||||
{
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
@property (nonatomic, strong) UIColor *textColor;
|
||||
@property (nonatomic, strong) UIColor *placeholderTextColor;
|
||||
@property (nonatomic, strong) UIFont *font;
|
||||
@property (nonatomic, assign) NSInteger mostRecentEventCount;
|
||||
@property (nonatomic, strong) NSNumber *maxLength;
|
||||
|
||||
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
NSString *_placeholder;
|
||||
UITextView *_placeholderView;
|
||||
UITextView *_textView;
|
||||
NSInteger _nativeEventCount;
|
||||
}
|
||||
|
||||
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
|
||||
|
@ -124,11 +125,41 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
|
|||
return _textView.text;
|
||||
}
|
||||
|
||||
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
|
||||
{
|
||||
if (_maxLength == nil) {
|
||||
return YES;
|
||||
}
|
||||
NSUInteger allowedLength = _maxLength.integerValue - textView.text.length + range.length;
|
||||
if (text.length > allowedLength) {
|
||||
if (text.length > 1) {
|
||||
// Truncate the input string so the result is exactly maxLength
|
||||
NSString *limitedString = [text substringToIndex:allowedLength];
|
||||
NSMutableString *newString = textView.text.mutableCopy;
|
||||
[newString replaceCharactersInRange:range withString:limitedString];
|
||||
textView.text = newString;
|
||||
// Collapse selection at end of insert to match normal paste behavior
|
||||
UITextPosition *insertEnd = [textView positionFromPosition:textView.beginningOfDocument
|
||||
offset:(range.location + allowedLength)];
|
||||
textView.selectedTextRange = [textView textRangeFromPosition:insertEnd toPosition:insertEnd];
|
||||
[self textViewDidChange:textView];
|
||||
}
|
||||
return NO;
|
||||
} else {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setText:(NSString *)text
|
||||
{
|
||||
if (![text isEqualToString:_textView.text]) {
|
||||
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
|
||||
if (eventLag == 0 && ![text isEqualToString:_textView.text]) {
|
||||
UITextRange *selection = _textView.selectedTextRange;
|
||||
[_textView setText:text];
|
||||
[self _setPlaceholderVisibility];
|
||||
_textView.selectedTextRange = selection; // maintain cursor position/selection - this is robust to out of bounds
|
||||
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
|
||||
RCTLogWarn(@"Native TextInput(%@) is %ld events ahead of JS - try to make your JS faster.", self.text, (long)eventLag);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,15 +201,18 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
|
|||
|
||||
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
|
||||
reactTag:self.reactTag
|
||||
text:textView.text];
|
||||
text:textView.text
|
||||
eventCount:_nativeEventCount];
|
||||
}
|
||||
|
||||
- (void)textViewDidChange:(UITextView *)textView
|
||||
{
|
||||
[self _setPlaceholderVisibility];
|
||||
_nativeEventCount++;
|
||||
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange
|
||||
reactTag:self.reactTag
|
||||
text:textView.text];
|
||||
text:textView.text
|
||||
eventCount:_nativeEventCount];
|
||||
|
||||
}
|
||||
|
||||
|
@ -186,7 +220,8 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
|
|||
{
|
||||
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd
|
||||
reactTag:self.reactTag
|
||||
text:textView.text];
|
||||
text:textView.text
|
||||
eventCount:_nativeEventCount];
|
||||
}
|
||||
|
||||
- (BOOL)becomeFirstResponder
|
||||
|
@ -204,7 +239,8 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
|
|||
if (result) {
|
||||
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur
|
||||
reactTag:self.reactTag
|
||||
text:_textView.text];
|
||||
text:_textView.text
|
||||
eventCount:_nativeEventCount];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ RCT_REMAP_VIEW_PROPERTY(editable, textView.editable, BOOL)
|
|||
RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
|
||||
RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor)
|
||||
RCT_EXPORT_VIEW_PROPERTY(text, NSString)
|
||||
RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber)
|
||||
RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL)
|
||||
RCT_REMAP_VIEW_PROPERTY(keyboardType, textView.keyboardType, UIKeyboardType)
|
||||
|
@ -52,6 +53,7 @@ RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextView)
|
|||
{
|
||||
view.font = [RCTConvert UIFont:view.font withFamily:json ?: defaultView.font.familyName];
|
||||
}
|
||||
RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger)
|
||||
|
||||
- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView
|
||||
{
|
||||
|
|
|
@ -106,6 +106,13 @@ id<RCTJavaScriptExecutor> RCTGetLatestExecutor(void)
|
|||
*/
|
||||
[self registerModules];
|
||||
|
||||
/**
|
||||
* If currently profiling, hook into the current instance
|
||||
*/
|
||||
if (RCTProfileIsProfiling()) {
|
||||
RCTProfileHookModules(self);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the application script
|
||||
*/
|
||||
|
@ -235,18 +242,13 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL
|
|||
NSString *configJSON = RCTJSONStringify(@{
|
||||
@"remoteModuleConfig": config,
|
||||
}, NULL);
|
||||
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
||||
[_javaScriptExecutor injectJSONText:configJSON
|
||||
asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback:
|
||||
^(NSError *error) {
|
||||
if (error) {
|
||||
[[RCTRedBox sharedInstance] showError:error];
|
||||
}
|
||||
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
}];
|
||||
|
||||
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW);
|
||||
asGlobalObjectNamed:@"__fbBatchedBridgeConfig"
|
||||
callback:^(NSError *error) {
|
||||
if (error) {
|
||||
[[RCTRedBox sharedInstance] showError:error];
|
||||
}
|
||||
}];
|
||||
|
||||
NSURL *bundleURL = _parentBridge.bundleURL;
|
||||
if (_javaScriptExecutor == nil) {
|
||||
|
@ -349,53 +351,41 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL
|
|||
RCTLatestExecutor = nil;
|
||||
}
|
||||
|
||||
void (^mainThreadInvalidate)(void) = ^{
|
||||
RCTAssertMainThread();
|
||||
[_mainDisplayLink invalidate];
|
||||
_mainDisplayLink = nil;
|
||||
|
||||
[_mainDisplayLink invalidate];
|
||||
_mainDisplayLink = nil;
|
||||
|
||||
// Invalidate modules
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
for (RCTModuleData *moduleData in _modules) {
|
||||
if ([moduleData.instance respondsToSelector:@selector(invalidate)]) {
|
||||
[moduleData dispatchBlock:^{
|
||||
[(id<RCTInvalidating>)moduleData.instance invalidate];
|
||||
} dispatchGroup:group];
|
||||
}
|
||||
moduleData.queue = nil;
|
||||
// Invalidate modules
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
for (RCTModuleData *moduleData in _modules) {
|
||||
if (moduleData.instance == _javaScriptExecutor) {
|
||||
continue;
|
||||
}
|
||||
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
||||
|
||||
if ([moduleData.instance respondsToSelector:@selector(invalidate)]) {
|
||||
[moduleData dispatchBlock:^{
|
||||
[(id<RCTInvalidating>)moduleData.instance invalidate];
|
||||
} dispatchGroup:group];
|
||||
}
|
||||
moduleData.queue = nil;
|
||||
}
|
||||
|
||||
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
||||
[_javaScriptExecutor executeBlockOnJavaScriptQueue:^{
|
||||
[_jsDisplayLink invalidate];
|
||||
_jsDisplayLink = nil;
|
||||
|
||||
[_javaScriptExecutor invalidate];
|
||||
_javaScriptExecutor = nil;
|
||||
|
||||
if (RCTProfileIsProfiling()) {
|
||||
RCTProfileUnhookModules(self);
|
||||
}
|
||||
_modules = nil;
|
||||
_modulesByName = nil;
|
||||
_frameUpdateObservers = nil;
|
||||
});
|
||||
};
|
||||
|
||||
if (!_javaScriptExecutor) {
|
||||
|
||||
// No JS thread running
|
||||
mainThreadInvalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
[_javaScriptExecutor executeBlockOnJavaScriptQueue:^{
|
||||
|
||||
/**
|
||||
* JS Thread deallocations
|
||||
*/
|
||||
[_javaScriptExecutor invalidate];
|
||||
_javaScriptExecutor = nil;
|
||||
|
||||
[_jsDisplayLink invalidate];
|
||||
_jsDisplayLink = nil;
|
||||
|
||||
/**
|
||||
* Main Thread deallocations
|
||||
*/
|
||||
dispatch_async(dispatch_get_main_queue(), mainThreadInvalidate);
|
||||
|
||||
}];
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - RCTBridge methods
|
||||
|
|
|
@ -28,6 +28,8 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) {
|
|||
RCTScrollEventTypeEndAnimation,
|
||||
};
|
||||
|
||||
extern const NSInteger RCTTextUpdateLagWarningThreshold;
|
||||
|
||||
@protocol RCTEvent <NSObject>
|
||||
|
||||
@required
|
||||
|
@ -76,12 +78,14 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) {
|
|||
*/
|
||||
- (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body;
|
||||
|
||||
|
||||
/**
|
||||
* Send a text input/focus event.
|
||||
*/
|
||||
- (void)sendTextEventWithType:(RCTTextEventType)type
|
||||
reactTag:(NSNumber *)reactTag
|
||||
text:(NSString *)text;
|
||||
text:(NSString *)text
|
||||
eventCount:(NSInteger)eventCount;
|
||||
|
||||
- (void)sendEvent:(id<RCTEvent>)event;
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
#import "RCTAssert.h"
|
||||
#import "RCTBridge.h"
|
||||
|
||||
const NSInteger RCTTextUpdateLagWarningThreshold = 3;
|
||||
|
||||
static NSNumber *RCTGetEventID(id<RCTEvent> event)
|
||||
{
|
||||
return @(
|
||||
|
@ -113,6 +115,7 @@ RCT_EXPORT_MODULE()
|
|||
- (void)sendTextEventWithType:(RCTTextEventType)type
|
||||
reactTag:(NSNumber *)reactTag
|
||||
text:(NSString *)text
|
||||
eventCount:(NSInteger)eventCount
|
||||
{
|
||||
static NSString *events[] = {
|
||||
@"topFocus",
|
||||
|
@ -124,8 +127,10 @@ RCT_EXPORT_MODULE()
|
|||
|
||||
[self sendInputEventWithName:events[type] body:text ? @{
|
||||
@"text": text,
|
||||
@"eventCount": @(eventCount),
|
||||
@"target": reactTag
|
||||
} : @{
|
||||
@"eventCount": @(eventCount),
|
||||
@"target": reactTag
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -103,6 +103,16 @@ RCT_EXTERN void RCTProfileImmediateEvent(NSString *, NSTimeInterval , NSString *
|
|||
RCTProfileEndEvent([NSString stringWithFormat:@"[%@ %@]", NSStringFromClass([self class]), NSStringFromSelector(_cmd)], category, arguments); \
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into a bridge instance to log all bridge module's method calls
|
||||
*/
|
||||
RCT_EXTERN void RCTProfileHookModules(RCTBridge *);
|
||||
|
||||
/**
|
||||
* Unhook from a given bridge instance's modules
|
||||
*/
|
||||
RCT_EXTERN void RCTProfileUnhookModules(RCTBridge *);
|
||||
|
||||
#else
|
||||
|
||||
#define RCTProfileBeginFlowEvent()
|
||||
|
@ -125,4 +135,7 @@ RCT_EXTERN void RCTProfileImmediateEvent(NSString *, NSTimeInterval , NSString *
|
|||
|
||||
#define RCTProfileBlock(block, ...) block
|
||||
|
||||
#define RCTProfileHookModules(...)
|
||||
#define RCTProfileUnhookModules(...)
|
||||
|
||||
#endif
|
||||
|
|
|
@ -124,6 +124,8 @@ static void RCTProfileForwardInvocation(NSObject *self, __unused SEL cmd, NSInvo
|
|||
RCTProfileBeginEvent();
|
||||
[invocation invoke];
|
||||
RCTProfileEndEvent(name, @"objc_call,modules,auto", nil);
|
||||
} else if ([self respondsToSelector:invocation.selector]) {
|
||||
[invocation invoke];
|
||||
} else {
|
||||
// Use original selector to don't change error message
|
||||
[self doesNotRecognizeSelector:invocation.selector];
|
||||
|
@ -144,14 +146,17 @@ static IMP RCTProfileMsgForward(NSObject *self, SEL selector)
|
|||
return imp;
|
||||
}
|
||||
|
||||
static void RCTProfileHookModules(RCTBridge *);
|
||||
static void RCTProfileHookModules(RCTBridge *bridge)
|
||||
void RCTProfileHookModules(RCTBridge *bridge)
|
||||
{
|
||||
for (RCTModuleData *moduleData in [bridge valueForKey:@"_modules"]) {
|
||||
[moduleData dispatchBlock:^{
|
||||
Class moduleClass = moduleData.cls;
|
||||
Class proxyClass = objc_allocateClassPair(moduleClass, RCTProfileProxyClassName(moduleClass), 0);
|
||||
|
||||
if (!proxyClass) {
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned int methodCount;
|
||||
Method *methods = class_copyMethodList(moduleClass, &methodCount);
|
||||
for (NSUInteger i = 0; i < methodCount; i++) {
|
||||
|
@ -185,20 +190,17 @@ static void RCTProfileHookModules(RCTBridge *bridge)
|
|||
}
|
||||
}
|
||||
|
||||
void RCTProfileUnhookModules(RCTBridge *);
|
||||
void RCTProfileUnhookModules(RCTBridge *bridge)
|
||||
{
|
||||
for (RCTModuleData *moduleData in [bridge valueForKey:@"_modules"]) {
|
||||
[moduleData dispatchBlock:^{
|
||||
RCTProfileLock(
|
||||
Class proxyClass = object_getClass(moduleData.instance);
|
||||
if (moduleData.cls != proxyClass) {
|
||||
object_setClass(moduleData.instance, moduleData.cls);
|
||||
objc_disposeClassPair(proxyClass);
|
||||
}
|
||||
);
|
||||
}];
|
||||
};
|
||||
RCTProfileLock(
|
||||
for (RCTModuleData *moduleData in [bridge valueForKey:@"_modules"]) {
|
||||
Class proxyClass = object_getClass(moduleData.instance);
|
||||
if (moduleData.cls != proxyClass) {
|
||||
object_setClass(moduleData.instance, moduleData.cls);
|
||||
objc_disposeClassPair(proxyClass);
|
||||
}
|
||||
};
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
{
|
||||
if ((self = [super initWithFrame:frame])) {
|
||||
_redColor = [UIColor colorWithRed:0.8 green:0 blue:0 alpha:1];
|
||||
self.windowLevel = UIWindowLevelStatusBar + 5;
|
||||
self.windowLevel = CGFLOAT_MAX;
|
||||
self.backgroundColor = _redColor;
|
||||
self.hidden = YES;
|
||||
|
||||
|
|
|
@ -15,6 +15,12 @@
|
|||
*/
|
||||
@protocol RCTURLRequestDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* Call this when you first receives a response from the server. This should
|
||||
* include response headers, etc.
|
||||
*/
|
||||
- (void)URLRequest:(id)requestToken didUploadProgress:(double)progress total:(double)total;
|
||||
|
||||
/**
|
||||
* Call this when you first receives a response from the server. This should
|
||||
* include response headers, etc.
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
RCT_EXTERN NSString *RCTJSONStringify(id jsonObject, NSError **error);
|
||||
RCT_EXTERN id RCTJSONParse(NSString *jsonString, NSError **error);
|
||||
RCT_EXTERN id RCTJSONParseMutable(NSString *jsonString, NSError **error);
|
||||
RCT_EXTERN id RCTJSONParseWithOptions(NSString *jsonString, NSError **error, NSJSONReadingOptions options);
|
||||
|
||||
// Strip non JSON-safe values from an object graph
|
||||
RCT_EXTERN id RCTJSONClean(id object);
|
||||
|
|
|
@ -23,34 +23,99 @@
|
|||
|
||||
NSString *RCTJSONStringify(id jsonObject, NSError **error)
|
||||
{
|
||||
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObject options:(NSJSONWritingOptions)NSJSONReadingAllowFragments error:error];
|
||||
static SEL JSONKitSelector = NULL;
|
||||
static NSSet *collectionTypes;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
SEL selector = NSSelectorFromString(@"JSONStringWithOptions:error:");
|
||||
if ([NSDictionary instancesRespondToSelector:selector]) {
|
||||
JSONKitSelector = selector;
|
||||
collectionTypes = [NSSet setWithObjects:
|
||||
[NSArray class], [NSMutableArray class],
|
||||
[NSDictionary class], [NSMutableDictionary class], nil];
|
||||
}
|
||||
});
|
||||
|
||||
// Use JSONKit if available and object is not a fragment
|
||||
if (JSONKitSelector && [collectionTypes containsObject:[jsonObject classForCoder]]) {
|
||||
return ((NSString *(*)(id, SEL, int, NSError **))objc_msgSend)(jsonObject, JSONKitSelector, 0, error);
|
||||
}
|
||||
|
||||
// Use Foundation JSON method
|
||||
NSData *jsonData = [NSJSONSerialization
|
||||
dataWithJSONObject:jsonObject
|
||||
options:(NSJSONWritingOptions)NSJSONReadingAllowFragments
|
||||
error:error];
|
||||
return jsonData ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] : nil;
|
||||
}
|
||||
|
||||
id RCTJSONParseWithOptions(NSString *jsonString, NSError **error, NSJSONReadingOptions options)
|
||||
static id _RCTJSONParse(NSString *jsonString, BOOL mutable, NSError **error)
|
||||
{
|
||||
if (!jsonString) {
|
||||
return nil;
|
||||
}
|
||||
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO];
|
||||
if (!jsonData) {
|
||||
jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
|
||||
if (jsonData) {
|
||||
RCTLogWarn(@"RCTJSONParse received the following string, which could not be losslessly converted to UTF8 data: '%@'", jsonString);
|
||||
} else {
|
||||
RCTLogError(@"RCTJSONParse received invalid UTF8 data");
|
||||
return nil;
|
||||
static SEL JSONKitSelector = NULL;
|
||||
static SEL JSONKitMutableSelector = NULL;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
SEL selector = NSSelectorFromString(@"objectFromJSONStringWithParseOptions:error:");
|
||||
if ([NSString instancesRespondToSelector:selector]) {
|
||||
JSONKitSelector = selector;
|
||||
JSONKitMutableSelector = NSSelectorFromString(@"mutableObjectFromJSONStringWithParseOptions:error:");
|
||||
}
|
||||
});
|
||||
|
||||
if (jsonString) {
|
||||
|
||||
// Use JSONKit if available and string is not a fragment
|
||||
if (JSONKitSelector) {
|
||||
NSInteger length = jsonString.length;
|
||||
for (NSInteger i = 0; i < length; i++) {
|
||||
unichar c = [jsonString characterAtIndex:i];
|
||||
if (strchr("{[", c)) {
|
||||
static const int options = (1 << 2); // loose unicode
|
||||
SEL selector = mutable ? JSONKitMutableSelector : JSONKitSelector;
|
||||
return ((id (*)(id, SEL, int, NSError **))objc_msgSend)(jsonString, selector, options, error);
|
||||
}
|
||||
if (!strchr(" \r\n\t", c)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use Foundation JSON method
|
||||
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
if (!jsonData) {
|
||||
jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
|
||||
if (jsonData) {
|
||||
RCTLogWarn(@"RCTJSONParse received the following string, which could "
|
||||
"not be losslessly converted to UTF8 data: '%@'", jsonString);
|
||||
} else {
|
||||
NSString *errorMessage = @"RCTJSONParse received invalid UTF8 data";
|
||||
if (error) {
|
||||
*error = RCTErrorWithMessage(errorMessage);
|
||||
} else {
|
||||
RCTLogError(@"%@", errorMessage);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
NSJSONReadingOptions options = NSJSONReadingAllowFragments;
|
||||
if (mutable) {
|
||||
options |= NSJSONReadingMutableContainers;
|
||||
}
|
||||
return [NSJSONSerialization JSONObjectWithData:jsonData
|
||||
options:options
|
||||
error:error];
|
||||
}
|
||||
return [NSJSONSerialization JSONObjectWithData:jsonData options:options error:error];
|
||||
return nil;
|
||||
}
|
||||
|
||||
id RCTJSONParse(NSString *jsonString, NSError **error) {
|
||||
return RCTJSONParseWithOptions(jsonString, error, NSJSONReadingAllowFragments);
|
||||
id RCTJSONParse(NSString *jsonString, NSError **error)
|
||||
{
|
||||
return _RCTJSONParse(jsonString, NO, error);
|
||||
}
|
||||
|
||||
id RCTJSONParseMutable(NSString *jsonString, NSError **error) {
|
||||
return RCTJSONParseWithOptions(jsonString, error, NSJSONReadingMutableContainers|NSJSONReadingMutableLeaves);
|
||||
id RCTJSONParseMutable(NSString *jsonString, NSError **error)
|
||||
{
|
||||
return _RCTJSONParse(jsonString, YES, error);
|
||||
}
|
||||
|
||||
id RCTJSONClean(id object)
|
||||
|
@ -308,7 +373,8 @@ NSURL *RCTDataURL(NSString *mimeType, NSData *data)
|
|||
[data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]]];
|
||||
}
|
||||
|
||||
static BOOL RCTIsGzippedData(NSData *data)
|
||||
BOOL RCTIsGzippedData(NSData *); // exposed for unit testing purposes
|
||||
BOOL RCTIsGzippedData(NSData *data)
|
||||
{
|
||||
UInt8 *bytes = (UInt8 *)data.bytes;
|
||||
return (data.length >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b);
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#import <pthread.h>
|
||||
|
||||
#import <JavaScriptCore/JavaScriptCore.h>
|
||||
#import <UIKit/UIDevice.h>
|
||||
|
||||
#import "RCTAssert.h"
|
||||
#import "RCTDefines.h"
|
||||
|
@ -20,6 +21,22 @@
|
|||
#import "RCTPerformanceLogger.h"
|
||||
#import "RCTUtils.h"
|
||||
|
||||
#ifndef RCT_JSC_PROFILER
|
||||
#if RCT_DEV && DEBUG
|
||||
#define RCT_JSC_PROFILER 1
|
||||
#else
|
||||
#define RCT_JSC_PROFILER 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if RCT_JSC_PROFILER
|
||||
#include <dlfcn.h>
|
||||
|
||||
#ifndef RCT_JSC_PROFILER_DYLIB
|
||||
#define RCT_JSC_PROFILER_DYLIB [[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"RCTJSCProfiler.ios%zd", [[[UIDevice currentDevice] systemVersion] integerValue]] ofType:@"dylib" inDirectory:@"Frameworks"] UTF8String]
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@interface RCTJavaScriptContext : NSObject <RCTInvalidating>
|
||||
|
||||
@property (nonatomic, assign, readonly) JSGlobalContextRef ctx;
|
||||
|
@ -269,6 +286,18 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
|
|||
[strongSelf _addNativeHook:RCTConsoleProfile withName:"consoleProfile"];
|
||||
[strongSelf _addNativeHook:RCTConsoleProfileEnd withName:"consoleProfileEnd"];
|
||||
|
||||
#if RCT_JSC_PROFILER
|
||||
void *JSCProfiler = dlopen(RCT_JSC_PROFILER_DYLIB, RTLD_NOW);
|
||||
if (JSCProfiler != NULL) {
|
||||
JSObjectCallAsFunctionCallback nativeProfilerStart = dlsym(JSCProfiler, "nativeProfilerStart");
|
||||
JSObjectCallAsFunctionCallback nativeProfilerEnd = dlsym(JSCProfiler, "nativeProfilerEnd");
|
||||
if (nativeProfilerStart != NULL && nativeProfilerEnd != NULL) {
|
||||
[strongSelf _addNativeHook:nativeProfilerStart withName:"nativeProfilerStart"];
|
||||
[strongSelf _addNativeHook:nativeProfilerEnd withName:"nativeProfilerStop"];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
for (NSString *event in @[RCTProfileDidStartProfiling, RCTProfileDidEndProfiling]) {
|
||||
[[NSNotificationCenter defaultCenter] addObserver:strongSelf
|
||||
selector:@selector(toggleProfilingFlag:)
|
||||
|
@ -320,6 +349,7 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
|
|||
onThread:_javaScriptThread
|
||||
withObject:nil
|
||||
waitUntilDone:NO];
|
||||
_context = nil;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
|
|
|
@ -1408,8 +1408,13 @@ RCT_EXPORT_METHOD(clearJSResponder)
|
|||
for (RCTViewManager *manager in _viewManagers.allValues) {
|
||||
if (RCTClassOverridesInstanceMethod([manager class], @selector(customDirectEventTypes))) {
|
||||
NSDictionary *eventTypes = [manager customDirectEventTypes];
|
||||
for (NSString *eventName in eventTypes) {
|
||||
RCTAssert(!customDirectEventTypes[eventName], @"Event '%@' registered multiple times.", eventName);
|
||||
if (RCT_DEV) {
|
||||
for (NSString *eventName in eventTypes) {
|
||||
id eventType = customDirectEventTypes[eventName];
|
||||
RCTAssert(!eventType || [eventType isEqual:eventTypes[eventName]],
|
||||
@"Event '%@' registered multiple times with different "
|
||||
"properties.", eventName);
|
||||
}
|
||||
}
|
||||
[customDirectEventTypes addEntriesFromDictionary:eventTypes];
|
||||
}
|
||||
|
|
|
@ -472,6 +472,7 @@
|
|||
83CBBA2A1A601D0E00E9B192 /* Sources */,
|
||||
83CBBA2B1A601D0E00E9B192 /* Frameworks */,
|
||||
83CBBA2C1A601D0E00E9B192 /* Copy Files */,
|
||||
142C4F7F1B582EA6001F0B58 /* ShellScript */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
@ -528,6 +529,20 @@
|
|||
shellPath = /bin/sh;
|
||||
shellScript = "if nc -w 5 -z localhost 8081 ; then\n if ! curl -s \"http://localhost:8081/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port 8081 already in use, packager is either not running or not running correctly\"\n exit 2\n fi\nelse\n open $SRCROOT/../packager/launchPackager.command || echo \"Can't start packager automatically\"\nfi";
|
||||
};
|
||||
142C4F7F1B582EA6001F0B58 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [[ \"$CONFIGURATION\" == \"Debug\" ]] && [[ -d \"/tmp/RCTJSCProfiler\" ]]; then\n find \"${CONFIGURATION_BUILD_DIR}\" -name '*.app' | xargs -I{} sh -c 'mkdir -p \"$1/Frameworks\" && cp -r /tmp/RCTJSCProfiler/* \"$1/Frameworks\"' -- {}\nfi";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
|
|
@ -436,7 +436,7 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
|
|||
*/
|
||||
- (UIView *)reactSuperview
|
||||
{
|
||||
RCTAssert(self.superview != nil, @"put reactNavSuperviewLink back");
|
||||
RCTAssert(!_bridge.isValid || self.superview != nil, @"put reactNavSuperviewLink back");
|
||||
return self.superview ? self.superview : self.reactNavSuperviewLink;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ RCT_EXPORT_MODULE()
|
|||
RCTSlider *slider = [[RCTSlider alloc] init];
|
||||
[slider addTarget:self action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged];
|
||||
[slider addTarget:self action:@selector(sliderTouchEnd:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[slider addTarget:self action:@selector(sliderTouchEnd:) forControlEvents:UIControlEventTouchUpOutside];
|
||||
return slider;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue