Merge pull request #1900 from sahrens/Update_Tue_Jul_7_Animated
Updates from Tue July 7 and Wed July 8th - includes new Animated API
|
@ -28,3 +28,4 @@ project.xcworkspace
|
|||
# Node
|
||||
node_modules
|
||||
*.log
|
||||
.nvm
|
||||
|
|
|
@ -32,5 +32,13 @@ node_modules/react-native/Libraries/react-native/react-native-interface.js
|
|||
[options]
|
||||
module.system=haste
|
||||
|
||||
suppress_type=$FlowIssue
|
||||
suppress_type=$FlowFixMe
|
||||
suppress_type=$FixMe
|
||||
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-3]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-3]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
|
||||
|
||||
[version]
|
||||
0.13.1
|
||||
|
|
|
@ -0,0 +1,319 @@
|
|||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* @providesModule AnExApp
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
Animated,
|
||||
LayoutAnimation,
|
||||
PanResponder,
|
||||
StyleSheet,
|
||||
View,
|
||||
} = React;
|
||||
|
||||
var AnExSet = require('AnExSet');
|
||||
|
||||
var CIRCLE_SIZE = 80;
|
||||
var CIRCLE_MARGIN = 18;
|
||||
var NUM_CIRCLES = 30;
|
||||
|
||||
class Circle extends React.Component {
|
||||
_onLongPress: () => void;
|
||||
_toggleIsActive: () => void;
|
||||
constructor(props: Object): void {
|
||||
super();
|
||||
this._onLongPress = this._onLongPress.bind(this);
|
||||
this._toggleIsActive = this._toggleIsActive.bind(this);
|
||||
this.state = {isActive: false};
|
||||
this.state.pan = new Animated.ValueXY(); // Vectors reduce boilerplate. (step1: uncomment)
|
||||
this.state.pop = new Animated.Value(0); // Initial value. (step2a: uncomment)
|
||||
}
|
||||
|
||||
_onLongPress(): void {
|
||||
var config = {tension: 40, friction: 3};
|
||||
this.state.pan.addListener((value) => { // Async listener for state changes (step1: uncomment)
|
||||
this.props.onMove && this.props.onMove(value);
|
||||
});
|
||||
Animated.spring(this.state.pop, {
|
||||
toValue: 1, // Pop to larger size. (step2b: uncomment)
|
||||
...config, // Reuse config for convenient consistency (step2b: uncomment)
|
||||
}).start();
|
||||
this.setState({panResponder: PanResponder.create({
|
||||
onPanResponderMove: Animated.event([
|
||||
null, // native event - ignore (step1: uncomment)
|
||||
{dx: this.state.pan.x, dy: this.state.pan.y}, // links pan to gestureState (step1: uncomment)
|
||||
]),
|
||||
onPanResponderRelease: (e, gestureState) => {
|
||||
LayoutAnimation.easeInEaseOut(); // @flowfixme animates layout update as one batch (step3: uncomment)
|
||||
Animated.spring(this.state.pop, {
|
||||
toValue: 0, // Pop back to 0 (step2c: uncomment)
|
||||
...config,
|
||||
}).start();
|
||||
this.setState({panResponder: undefined});
|
||||
this.props.onMove && this.props.onMove({
|
||||
x: gestureState.dx + this.props.restLayout.x,
|
||||
y: gestureState.dy + this.props.restLayout.y,
|
||||
});
|
||||
this.props.onDeactivate();
|
||||
},
|
||||
})}, () => {
|
||||
this.props.onActivate();
|
||||
});
|
||||
}
|
||||
|
||||
render(): ReactElement {
|
||||
if (this.state.panResponder) {
|
||||
var handlers = this.state.panResponder.panHandlers;
|
||||
var dragStyle = { // Used to position while dragging
|
||||
position: 'absolute', // Hoist out of layout (step1: uncomment)
|
||||
...this.state.pan.getLayout(), // Convenience converter (step1: uncomment)
|
||||
};
|
||||
} else {
|
||||
handlers = {
|
||||
onStartShouldSetResponder: () => !this.state.isActive,
|
||||
onResponderGrant: () => {
|
||||
this.state.pan.setValue({x: 0, y: 0}); // reset (step1: uncomment)
|
||||
this.state.pan.setOffset(this.props.restLayout); // offset from onLayout (step1: uncomment)
|
||||
this.longTimer = setTimeout(this._onLongPress, 300);
|
||||
},
|
||||
onResponderRelease: () => {
|
||||
if (!this.state.panResponder) {
|
||||
clearTimeout(this.longTimer);
|
||||
this._toggleIsActive();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
var animatedStyle: Object = {
|
||||
shadowOpacity: this.state.pop, // no need for interpolation (step2d: uncomment)
|
||||
transform: [
|
||||
{scale: this.state.pop.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, 1.3] // scale up from 1 to 1.3 (step2d: uncomment)
|
||||
})},
|
||||
],
|
||||
};
|
||||
var openVal = this.props.openVal;
|
||||
if (this.props.dummy) {
|
||||
animatedStyle.opacity = 0;
|
||||
} else if (this.state.isActive) {
|
||||
var innerOpenStyle = [styles.open, { // (step4: uncomment)
|
||||
left: openVal.interpolate({inputRange: [0, 1], outputRange: [this.props.restLayout.x, 0]}),
|
||||
top: openVal.interpolate({inputRange: [0, 1], outputRange: [this.props.restLayout.y, 0]}),
|
||||
width: openVal.interpolate({inputRange: [0, 1], outputRange: [CIRCLE_SIZE, this.props.containerLayout.width]}),
|
||||
height: openVal.interpolate({inputRange: [0, 1], outputRange: [CIRCLE_SIZE, this.props.containerLayout.height]}),
|
||||
margin: openVal.interpolate({inputRange: [0, 1], outputRange: [CIRCLE_MARGIN, 0]}),
|
||||
borderRadius: openVal.interpolate({inputRange: [-0.15, 0, 0.5, 1], outputRange: [0, CIRCLE_SIZE / 2, CIRCLE_SIZE * 1.3, 0]}),
|
||||
}];
|
||||
}
|
||||
return (
|
||||
<Animated.View
|
||||
onLayout={this.props.onLayout}
|
||||
style={[styles.dragView, dragStyle, animatedStyle, this.state.isActive ? styles.open : null]}
|
||||
{...handlers}>
|
||||
<Animated.View style={[styles.circle, innerOpenStyle]}>
|
||||
<AnExSet
|
||||
containerLayout={this.props.containerLayout}
|
||||
id={this.props.id}
|
||||
isActive={this.state.isActive}
|
||||
openVal={this.props.openVal}
|
||||
onDismiss={this._toggleIsActive}
|
||||
/>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
_toggleIsActive(velocity) {
|
||||
var config = {tension: 30, friction: 7};
|
||||
if (this.state.isActive) {
|
||||
Animated.spring(this.props.openVal, {toValue: 0, ...config}).start(() => { // (step4: uncomment)
|
||||
this.setState({isActive: false}, this.props.onDeactivate);
|
||||
}); // (step4: uncomment)
|
||||
} else {
|
||||
this.props.onActivate();
|
||||
this.setState({isActive: true, panResponder: undefined}, () => {
|
||||
// this.props.openVal.setValue(1); // (step4: comment)
|
||||
Animated.spring(this.props.openVal, {toValue: 1, ...config}).start(); // (step4: uncomment)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AnExApp extends React.Component {
|
||||
_onMove: (position: Point) => void;
|
||||
constructor(props: any): void {
|
||||
super(props);
|
||||
var keys = [];
|
||||
for (var idx = 0; idx < NUM_CIRCLES; idx++) {
|
||||
keys.push('E' + idx);
|
||||
}
|
||||
this.state = {
|
||||
keys,
|
||||
restLayouts: [],
|
||||
openVal: new Animated.Value(0),
|
||||
};
|
||||
this._onMove = this._onMove.bind(this);
|
||||
}
|
||||
|
||||
render(): ReactElement {
|
||||
var circles = this.state.keys.map((key, idx) => {
|
||||
if (key === this.state.activeKey) {
|
||||
return <Circle key={key + 'd'} dummy={true} />;
|
||||
} else {
|
||||
if (!this.state.restLayouts[idx]) {
|
||||
var onLayout = function(index, e) {
|
||||
var layout = e.nativeEvent.layout;
|
||||
this.setState((state) => {
|
||||
state.restLayouts[index] = layout;
|
||||
return state;
|
||||
});
|
||||
}.bind(this, idx);
|
||||
}
|
||||
return (
|
||||
<Circle
|
||||
key={key}
|
||||
id={key}
|
||||
openVal={this.state.openVal}
|
||||
onLayout={onLayout}
|
||||
restLayout={this.state.restLayouts[idx]}
|
||||
onActivate={this.setState.bind(this, {
|
||||
activeKey: key,
|
||||
activeInitialLayout: this.state.restLayouts[idx],
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
if (this.state.activeKey) {
|
||||
circles.push(
|
||||
<Animated.View key="dark" style={[styles.darkening, {opacity: this.state.openVal}]} />
|
||||
);
|
||||
circles.push(
|
||||
<Circle
|
||||
openVal={this.state.openVal}
|
||||
key={this.state.activeKey}
|
||||
id={this.state.activeKey}
|
||||
restLayout={this.state.activeInitialLayout}
|
||||
containerLayout={this.state.layout}
|
||||
onMove={this._onMove}
|
||||
onDeactivate={() => { this.setState({activeKey: undefined}); }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.grid} onLayout={(e) => this.setState({layout: e.nativeEvent.layout})}>
|
||||
{circles}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_onMove(position: Point): void {
|
||||
var newKeys = moveToClosest(this.state, position);
|
||||
if (newKeys !== this.state.keys) {
|
||||
LayoutAnimation.easeInEaseOut(); // animates layout update as one batch (step3: uncomment)
|
||||
this.setState({keys: newKeys});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Point = {x: number, y: number};
|
||||
function distance(p1: Point, p2: Point): number {
|
||||
var dx = p1.x - p2.x;
|
||||
var dy = p1.y - p2.y;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
function moveToClosest({activeKey, keys, restLayouts}, position) {
|
||||
var activeIdx = -1;
|
||||
var closestIdx = activeIdx;
|
||||
var minDist = Infinity;
|
||||
var newKeys = [];
|
||||
keys.forEach((key, idx) => {
|
||||
var dist = distance(position, restLayouts[idx]);
|
||||
if (key === activeKey) {
|
||||
idx = activeIdx;
|
||||
} else {
|
||||
newKeys.push(key);
|
||||
}
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
closestIdx = idx;
|
||||
}
|
||||
});
|
||||
if (closestIdx === activeIdx) {
|
||||
return keys; // nothing changed
|
||||
} else {
|
||||
newKeys.splice(closestIdx, 0, activeKey);
|
||||
return newKeys;
|
||||
}
|
||||
}
|
||||
|
||||
AnExApp.title = 'Animated - Gratuitous App';
|
||||
AnExApp.description = 'Bunch of Animations - tap a circle to ' +
|
||||
'open a view with more animations, or longPress and drag to reorder circles.';
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingTop: 64, // push content below nav bar
|
||||
},
|
||||
grid: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
circle: {
|
||||
width: CIRCLE_SIZE,
|
||||
height: CIRCLE_SIZE,
|
||||
borderRadius: CIRCLE_SIZE / 2,
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
margin: CIRCLE_MARGIN,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
dragView: {
|
||||
shadowRadius: 10,
|
||||
shadowColor: 'rgba(0,0,0,0.7)',
|
||||
shadowOffset: {height: 8},
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
open: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: undefined, // unset value from styles.circle
|
||||
height: undefined, // unset value from styles.circle
|
||||
borderRadius: 0, // unset value from styles.circle
|
||||
},
|
||||
darkening: {
|
||||
backgroundColor: 'black',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = AnExApp;
|
|
@ -0,0 +1,162 @@
|
|||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* @providesModule AnExBobble
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
Animated,
|
||||
Image,
|
||||
PanResponder,
|
||||
StyleSheet,
|
||||
View,
|
||||
} = React;
|
||||
|
||||
var NUM_BOBBLES = 5;
|
||||
var RAD_EACH = Math.PI / 2 / (NUM_BOBBLES - 2);
|
||||
var RADIUS = 160;
|
||||
var BOBBLE_SPOTS = [...Array(NUM_BOBBLES)].map((_, i) => { // static positions
|
||||
return i === 0 ? {x: 0, y: 0} : { // first bobble is the selector
|
||||
x: -Math.cos(RAD_EACH * (i - 1)) * RADIUS,
|
||||
y: -Math.sin(RAD_EACH * (i - 1)) * RADIUS,
|
||||
};
|
||||
});
|
||||
|
||||
class AnExBobble extends React.Component {
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this.state.bobbles = BOBBLE_SPOTS.map((_, i) => {
|
||||
return new Animated.ValueXY();
|
||||
});
|
||||
this.state.selectedBobble = null;
|
||||
var bobblePanListener = (e, gestureState) => { // async events => change selection
|
||||
var newSelected = computeNewSelected(gestureState);
|
||||
if (this.state.selectedBobble !== newSelected) {
|
||||
if (this.state.selectedBobble !== null) {
|
||||
var restSpot = BOBBLE_SPOTS[this.state.selectedBobble];
|
||||
Animated.spring(this.state.bobbles[this.state.selectedBobble], {
|
||||
toValue: restSpot, // return previously selected bobble to rest position
|
||||
}).start();
|
||||
}
|
||||
if (newSelected !== null && newSelected !== 0) {
|
||||
Animated.spring(this.state.bobbles[newSelected], {
|
||||
toValue: this.state.bobbles[0], // newly selected should track the selector
|
||||
}).start();
|
||||
}
|
||||
this.state.selectedBobble = newSelected;
|
||||
}
|
||||
};
|
||||
var releaseBobble = () => {
|
||||
this.state.bobbles.forEach((bobble, i) => {
|
||||
Animated.spring(bobble, {
|
||||
toValue: {x: 0, y: 0} // all bobbles return to zero
|
||||
}).start();
|
||||
});
|
||||
};
|
||||
this.state.bobbleResponder = PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onPanResponderGrant: () => {
|
||||
BOBBLE_SPOTS.forEach((spot, idx) => {
|
||||
Animated.spring(this.state.bobbles[idx], {
|
||||
toValue: spot, // spring each bobble to its spot
|
||||
friction: 3, // less friction => bouncier
|
||||
}).start();
|
||||
});
|
||||
},
|
||||
onPanResponderMove: Animated.event(
|
||||
[ null, {dx: this.state.bobbles[0].x, dy: this.state.bobbles[0].y} ],
|
||||
{listener: bobblePanListener} // async state changes with arbitrary logic
|
||||
),
|
||||
onPanResponderRelease: releaseBobble,
|
||||
onPanResponderTerminate: releaseBobble,
|
||||
});
|
||||
}
|
||||
|
||||
render(): ReactElement {
|
||||
return (
|
||||
<View style={styles.bobbleContainer}>
|
||||
{this.state.bobbles.map((_, i) => {
|
||||
var j = this.state.bobbles.length - i - 1; // reverse so lead on top
|
||||
var handlers = j > 0 ? {} : this.state.bobbleResponder.panHandlers;
|
||||
return (
|
||||
<Animated.Image
|
||||
{...handlers}
|
||||
source={{uri: BOBBLE_IMGS[j]}}
|
||||
style={[styles.circle, {
|
||||
backgroundColor: randColor(), // re-renders are obvious
|
||||
transform: this.state.bobbles[j].getTranslateTransform(), // simple conversion
|
||||
}]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
circle: {
|
||||
position: 'absolute',
|
||||
height: 60,
|
||||
width: 60,
|
||||
borderRadius: 30,
|
||||
borderWidth: 0.5,
|
||||
},
|
||||
bobbleContainer: {
|
||||
top: -68,
|
||||
paddingRight: 66,
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
function computeNewSelected(
|
||||
gestureState: Object,
|
||||
): ?number {
|
||||
var {dx, dy} = gestureState;
|
||||
var minDist = Infinity;
|
||||
var newSelected = null;
|
||||
var pointRadius = Math.sqrt(dx * dx + dy * dy);
|
||||
if (Math.abs(RADIUS - pointRadius) < 80) {
|
||||
BOBBLE_SPOTS.forEach((spot, idx) => {
|
||||
var delta = {x: spot.x - dx, y: spot.y - dy};
|
||||
var dist = delta.x * delta.x + delta.y * delta.y;
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
newSelected = idx;
|
||||
}
|
||||
});
|
||||
}
|
||||
return newSelected;
|
||||
}
|
||||
|
||||
function randColor(): string {
|
||||
var colors = [0,1,2].map(() => Math.floor(Math.random() * 150 + 100));
|
||||
return 'rgb(' + colors.join(',') + ')';
|
||||
}
|
||||
|
||||
var BOBBLE_IMGS = [
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xpf1/t39.1997-6/10173489_272703316237267_1025826781_n.png',
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/l/t39.1997-6/p240x240/851578_631487400212668_2087073502_n.png',
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/p240x240/851583_654446917903722_178118452_n.png',
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/p240x240/851565_641023175913294_875343096_n.png',
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/851562_575284782557566_1188781517_n.png',
|
||||
];
|
||||
|
||||
module.exports = AnExBobble;
|
|
@ -0,0 +1,114 @@
|
|||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* @providesModule AnExChained
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
Animated,
|
||||
Image,
|
||||
PanResponder,
|
||||
StyleSheet,
|
||||
View,
|
||||
} = React;
|
||||
|
||||
class AnExChained extends React.Component {
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
this.state = {
|
||||
stickers: [new Animated.ValueXY()], // 1 leader
|
||||
};
|
||||
var stickerConfig = {tension: 2, friction: 3}; // soft spring
|
||||
for (var i = 0; i < 4; i++) { // 4 followers
|
||||
var sticker = new Animated.ValueXY();
|
||||
Animated.spring(sticker, {
|
||||
...stickerConfig,
|
||||
toValue: this.state.stickers[i], // Animated toValue's are tracked
|
||||
}).start();
|
||||
this.state.stickers.push(sticker); // push on the followers
|
||||
}
|
||||
var releaseChain = (e, gestureState) => {
|
||||
this.state.stickers[0].flattenOffset(); // merges offset into value and resets
|
||||
Animated.sequence([ // spring to start after decay finishes
|
||||
Animated.decay(this.state.stickers[0], { // coast to a stop
|
||||
velocity: {x: gestureState.vx, y: gestureState.vy},
|
||||
deceleration: 0.997,
|
||||
}),
|
||||
Animated.spring(this.state.stickers[0], {
|
||||
toValue: {x: 0, y: 0} // return to start
|
||||
}),
|
||||
]).start();
|
||||
};
|
||||
this.state.chainResponder = PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onPanResponderGrant: () => {
|
||||
this.state.stickers[0].stopAnimation((value) => {
|
||||
this.state.stickers[0].setOffset(value); // start where sticker animated to
|
||||
this.state.stickers[0].setValue({x: 0, y: 0}); // avoid flicker before next event
|
||||
});
|
||||
},
|
||||
onPanResponderMove: Animated.event(
|
||||
[null, {dx: this.state.stickers[0].x, dy: this.state.stickers[0].y}] // map gesture to leader
|
||||
),
|
||||
onPanResponderRelease: releaseChain,
|
||||
onPanResponderTerminate: releaseChain,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.chained}>
|
||||
{this.state.stickers.map((_, i) => {
|
||||
var j = this.state.stickers.length - i - 1; // reverse so leader is on top
|
||||
var handlers = (j === 0) ? this.state.chainResponder.panHandlers : {};
|
||||
return (
|
||||
<Animated.Image
|
||||
{...handlers}
|
||||
source={{uri: CHAIN_IMGS[j]}}
|
||||
style={[styles.sticker, {
|
||||
transform: this.state.stickers[j].getTranslateTransform(), // simple conversion
|
||||
}]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
chained: {
|
||||
alignSelf: 'flex-end',
|
||||
top: -160,
|
||||
right: 126
|
||||
},
|
||||
sticker: {
|
||||
position: 'absolute',
|
||||
height: 120,
|
||||
width: 120,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
var CHAIN_IMGS = [
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xpf1/t39.1997-6/p160x160/10574705_1529175770666007_724328156_n.png',
|
||||
'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xfa1/t39.1997-6/p160x160/851575_392309884199657_1917957497_n.png',
|
||||
'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xfa1/t39.1997-6/p160x160/851567_555288911225630_1628791128_n.png',
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xfa1/t39.1997-6/p160x160/851583_531111513625557_903469595_n.png',
|
||||
'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xpa1/t39.1997-6/p160x160/851577_510515972354399_2147096990_n.png',
|
||||
];
|
||||
|
||||
module.exports = AnExChained;
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* @providesModule AnExScroll
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
Animated,
|
||||
Image,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} = React;
|
||||
|
||||
class AnExScroll extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
scrollX: new Animated.Value(0),
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
var width = this.props.panelWidth;
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView
|
||||
automaticallyAdjustContentInsets={false}
|
||||
scrollEventThrottle={16 /* get all events */ }
|
||||
onScroll={Animated.event(
|
||||
[{nativeEvent: {contentOffset: {x: this.state.scrollX}}}] // nested event mapping
|
||||
)}
|
||||
contentContainerStyle={{flex: 1, padding: 10}}
|
||||
pagingEnabled={true}
|
||||
horizontal={true}>
|
||||
<View style={[styles.page, {width}]}>
|
||||
<Image
|
||||
style={{width: 180, height: 180}}
|
||||
source={HAWK_PIC}
|
||||
/>
|
||||
<Text style={styles.text}>
|
||||
{'I\'ll find something to put here.'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.page, {width}]}>
|
||||
<Text style={styles.text}>{'And here.'}</Text>
|
||||
</View>
|
||||
<View style={[styles.page, {width}]}>
|
||||
<Text>{'But not here.'}</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<Animated.Image
|
||||
pointerEvents="none"
|
||||
style={[styles.bunny, {transform: [
|
||||
{translateX: this.state.scrollX.interpolate({
|
||||
inputRange: [0, width, 2 * width],
|
||||
outputRange: [0, 0, width / 3]}), // multi-part ranges
|
||||
extrapolate: 'clamp'}, // default is 'extend'
|
||||
{translateY: this.state.scrollX.interpolate({
|
||||
inputRange: [0, width, 2 * width],
|
||||
outputRange: [0, -200, -260]}),
|
||||
extrapolate: 'clamp'},
|
||||
{scale: this.state.scrollX.interpolate({
|
||||
inputRange: [0, width, 2 * width],
|
||||
outputRange: [0.5, 0.5, 2]}),
|
||||
extrapolate: 'clamp'},
|
||||
]}]}
|
||||
source={BUNNY_PIC}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: 'transparent',
|
||||
flex: 1,
|
||||
},
|
||||
text: {
|
||||
padding: 4,
|
||||
paddingBottom: 10,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 18,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
bunny: {
|
||||
backgroundColor: 'transparent',
|
||||
position: 'absolute',
|
||||
height: 160,
|
||||
width: 160,
|
||||
},
|
||||
page: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
});
|
||||
|
||||
var HAWK_PIC = {uri: 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xfa1/t39.1997-6/10734304_1562225620659674_837511701_n.png'};
|
||||
var BUNNY_PIC = {uri: 'https://scontent-sea1-1.xx.fbcdn.net/hphotos-xaf1/t39.1997-6/851564_531111380292237_1898871086_n.png'};
|
||||
|
||||
module.exports = AnExScroll;
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* @providesModule AnExSet
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
Animated,
|
||||
PanResponder,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} = React;
|
||||
|
||||
var AnExBobble = require('./AnExBobble');
|
||||
var AnExChained = require('./AnExChained');
|
||||
var AnExScroll = require('./AnExScroll');
|
||||
var AnExTilt = require('./AnExTilt');
|
||||
|
||||
class AnExSet extends React.Component {
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
function randColor() {
|
||||
var colors = [0,1,2].map(() => Math.floor(Math.random() * 150 + 100));
|
||||
return 'rgb(' + colors.join(',') + ')';
|
||||
}
|
||||
this.state = {
|
||||
closeColor: randColor(),
|
||||
openColor: randColor(),
|
||||
};
|
||||
}
|
||||
render(): ReactElement {
|
||||
var backgroundColor = this.props.openVal ?
|
||||
this.props.openVal.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [
|
||||
this.state.closeColor, // interpolates color strings
|
||||
this.state.openColor
|
||||
],
|
||||
}) :
|
||||
this.state.closeColor;
|
||||
var panelWidth = this.props.containerLayout && this.props.containerLayout.width || 320;
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Animated.View
|
||||
style={[styles.header, { backgroundColor }]}
|
||||
{...this.state.dismissResponder.panHandlers}>
|
||||
<Text style={[styles.text, styles.headerText]}>
|
||||
{this.props.id}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
{this.props.isActive &&
|
||||
<View style={styles.stream}>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.text}>
|
||||
July 2nd
|
||||
</Text>
|
||||
<AnExTilt isActive={this.props.isActive} />
|
||||
<AnExBobble />
|
||||
</View>
|
||||
<AnExScroll panelWidth={panelWidth}/>
|
||||
<AnExChained />
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.state.dismissY = new Animated.Value(0);
|
||||
this.state.dismissResponder = PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => this.props.isActive,
|
||||
onPanResponderGrant: () => {
|
||||
Animated.spring(this.props.openVal, { // Animated value passed in.
|
||||
toValue: this.state.dismissY.interpolate({ // Track dismiss gesture
|
||||
inputRange: [0, 300], // and interpolate pixel distance
|
||||
outputRange: [1, 0], // to a fraction.
|
||||
})
|
||||
}).start();
|
||||
},
|
||||
onPanResponderMove: Animated.event(
|
||||
[null, {dy: this.state.dismissY}] // track pan gesture
|
||||
),
|
||||
onPanResponderRelease: (e, gestureState) => {
|
||||
if (gestureState.dy > 100) {
|
||||
this.props.onDismiss(gestureState.vy); // delegates dismiss action to parent
|
||||
} else {
|
||||
Animated.spring(this.props.openVal, {
|
||||
toValue: 1, // animate back open if released early
|
||||
}).start();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
paddingTop: 18,
|
||||
height: 90,
|
||||
},
|
||||
stream: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgb(230, 230, 230)',
|
||||
},
|
||||
card: {
|
||||
margin: 8,
|
||||
padding: 8,
|
||||
borderRadius: 6,
|
||||
backgroundColor: 'white',
|
||||
shadowRadius: 2,
|
||||
shadowColor: 'black',
|
||||
shadowOpacity: 0.2,
|
||||
shadowOffset: {height: 0.5},
|
||||
},
|
||||
text: {
|
||||
padding: 4,
|
||||
paddingBottom: 10,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 18,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
headerText: {
|
||||
fontSize: 25,
|
||||
color: 'white',
|
||||
shadowRadius: 3,
|
||||
shadowColor: 'black',
|
||||
shadowOpacity: 1,
|
||||
shadowOffset: {height: 1},
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = AnExSet;
|
|
@ -0,0 +1,107 @@
|
|||
<br /><br />
|
||||
# React Native: Animated
|
||||
|
||||
ReactEurope 2015, Paris - Spencer Ahrens - Facebook
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Fluid Interactions
|
||||
|
||||
- People expect smooth, delightful experiences
|
||||
- Complex interactions are hard
|
||||
- Common patterns can be optimized
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## Declarative Interactions
|
||||
|
||||
- Wire up inputs (events) to outputs (props) + transforms (springs, easing, etc.)
|
||||
- Arbitrary code can define/update this config
|
||||
- Config can be serialized -> native/main thread
|
||||
- No refs or lifecycle to worry about
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## var { Animated } = require('react-native');
|
||||
|
||||
- New library soon to be released for React Native
|
||||
- 100% JS implementation -> X-Platform
|
||||
- Per-platform native optimizations planned
|
||||
- This talk -> usage examples, not implementation
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## Gratuitous Animation Demo App
|
||||
|
||||
- Layout uses `flexWrap: 'wrap'`
|
||||
- longPress -> drag to reorder
|
||||
- Tap to open example sets
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Gratuitous Animation Codez
|
||||
|
||||
- Step 1: 2D tracking pan gesture
|
||||
- Step 2: Simple pop-out spring on select
|
||||
- Step 3: Animate grid reordering with `LayoutAnimation`
|
||||
- Step 4: Opening animation
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Animation Example Set
|
||||
|
||||
- `Animated.Value` `this.props.open` passed in from parent
|
||||
- `interpolate` works with string "shapes," e.g. `'rgb(0, 0, 255)'`, `'45deg'`
|
||||
- Examples easily composed as separate components
|
||||
- Dismissing tracks interpolated gesture
|
||||
- Custom release logic
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## Tilting Photo
|
||||
|
||||
- Pan -> translateX * 2, rotate, opacity (via tracking)
|
||||
- Gesture release triggers separate animations
|
||||
- `addListener` for async, arbitrary logic on animation progress
|
||||
- `interpolate` easily creates parallax and other effects
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Bobbles
|
||||
|
||||
- Static positions defined
|
||||
- Listens to events to maybe change selection
|
||||
- Springs previous selection back
|
||||
- New selection tracks selector
|
||||
- `getTranslateTransform` adds convenience
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Chained
|
||||
|
||||
- Classic "Chat Heads" animation
|
||||
- Each sticker tracks the one before it with a soft spring
|
||||
- `decay` maintains gesture velocity, followed by `spring` to home
|
||||
- `stopAnimation` provides the last value for `setOffset`
|
||||
|
||||
<br /><br />
|
||||
|
||||
## Scrolling
|
||||
|
||||
- `Animated.event` can track all sorts of stuff
|
||||
- Multi-part ranges and extrapolation options
|
||||
- Transforms decompose into ordered components
|
||||
|
||||
<br /><br />
|
||||
|
||||
# React Native: Animated
|
||||
|
||||
- Landing soon in master (days)
|
||||
- GitHub: @vjeux, @sahrens
|
||||
- Questions?
|
||||
|
||||
<br />
|
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* @providesModule AnExTilt
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
Animated,
|
||||
Image,
|
||||
PanResponder,
|
||||
StyleSheet,
|
||||
View,
|
||||
} = React;
|
||||
|
||||
class AnExTilt extends React.Component {
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
this.state = {
|
||||
panX: new Animated.Value(0),
|
||||
opacity: new Animated.Value(1),
|
||||
burns: new Animated.Value(1.15),
|
||||
};
|
||||
this.state.tiltPanResponder = PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onPanResponderGrant: () => {
|
||||
Animated.timing(this.state.opacity, {
|
||||
toValue: this.state.panX.interpolate({
|
||||
inputRange: [-300, 0, 300], // pan is in pixels
|
||||
outputRange: [0, 1, 0], // goes to zero at both edges
|
||||
}),
|
||||
duration: 0, // direct tracking
|
||||
}).start();
|
||||
},
|
||||
onPanResponderMove: Animated.event(
|
||||
[null, {dx: this.state.panX}] // panX is linked to the gesture
|
||||
),
|
||||
onPanResponderRelease: (e, gestureState) => {
|
||||
var toValue = 0;
|
||||
if (gestureState.dx > 100) {
|
||||
toValue = 500;
|
||||
} else if (gestureState.dx < -100) {
|
||||
toValue = -500;
|
||||
}
|
||||
Animated.spring(this.state.panX, {
|
||||
toValue, // animate back to center or off screen
|
||||
velocity: gestureState.vx, // maintain gesture velocity
|
||||
tension: 10,
|
||||
friction: 3,
|
||||
}).start();
|
||||
this.state.panX.removeAllListeners();
|
||||
var id = this.state.panX.addListener(({value}) => { // listen until offscreen
|
||||
if (Math.abs(value) > 400) {
|
||||
this.state.panX.removeListener(id); // offscreen, so stop listening
|
||||
Animated.timing(this.state.opacity, {
|
||||
toValue: 1, // Fade back in. This unlinks it from tracking this.state.panX
|
||||
}).start();
|
||||
this.state.panX.setValue(0); // Note: stops the spring animation
|
||||
toValue !== 0 && this._startBurnsZoom();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
_startBurnsZoom() {
|
||||
this.state.burns.setValue(1); // reset to beginning
|
||||
Animated.decay(this.state.burns, {
|
||||
velocity: 1, // sublte zoom
|
||||
deceleration: 0.9999, // slow decay
|
||||
}).start();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._startBurnsZoom();
|
||||
}
|
||||
|
||||
render(): ReactElement {
|
||||
return (
|
||||
<Animated.View
|
||||
{...this.state.tiltPanResponder.panHandlers}
|
||||
style={[styles.tilt, {
|
||||
opacity: this.state.opacity,
|
||||
transform: [
|
||||
{rotate: this.state.panX.interpolate({
|
||||
inputRange: [-320, 320],
|
||||
outputRange: ['-15deg', '15deg']})}, // interpolate string "shapes"
|
||||
{translateX: this.state.panX},
|
||||
],
|
||||
}]}>
|
||||
<Animated.Image
|
||||
pointerEvents="none"
|
||||
style={{
|
||||
flex: 1,
|
||||
transform: [
|
||||
{translateX: this.state.panX.interpolate({
|
||||
inputRange: [-3, 3], // small range is extended by default
|
||||
outputRange: [2, -2]}) // parallax
|
||||
},
|
||||
{scale: this.state.burns.interpolate({
|
||||
inputRange: [1, 3000],
|
||||
outputRange: [1, 1.25]}) // simple multiplier
|
||||
},
|
||||
],
|
||||
}}
|
||||
source={NATURE_IMAGE}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
tilt: {
|
||||
overflow: 'hidden',
|
||||
height: 200,
|
||||
marginBottom: 4,
|
||||
backgroundColor: 'rgb(130, 130, 255)',
|
||||
borderColor: 'rgba(0, 0, 0, 0.2)',
|
||||
borderWidth: 1,
|
||||
borderRadius: 20,
|
||||
},
|
||||
});
|
||||
|
||||
var NATURE_IMAGE = {uri: 'http://www.deshow.net/d/file/travel/2009-04/scenic-beauty-of-nature-photography-2-504-4.jpg'};
|
||||
|
||||
module.exports = AnExTilt;
|
|
@ -62,8 +62,11 @@ var CircleBlock = React.createClass({
|
|||
var LayoutExample = React.createClass({
|
||||
statics: {
|
||||
title: 'Layout - Flexbox',
|
||||
description: 'Examples of using the flexbox API to layout views.'
|
||||
description: 'Examples of using the flexbox API to layout views.',
|
||||
},
|
||||
|
||||
displayName: 'LayoutExample',
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<UIExplorerPage title={this.props.navigator ? null : 'Layout'}>
|
||||
|
|
|
@ -212,6 +212,7 @@ var styles = StyleSheet.create({
|
|||
width: 64,
|
||||
height: 64,
|
||||
marginHorizontal: 10,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
section: {
|
||||
flexDirection: 'column',
|
||||
|
|
|
@ -118,7 +118,7 @@ var EventSegmentedControlExample = React.createClass({
|
|||
|
||||
_onChange(event) {
|
||||
this.setState({
|
||||
selectedIndex: event.nativeEvent.selectedIndex,
|
||||
selectedIndex: event.nativeEvent.selectedSegmentIndex,
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -26,9 +26,11 @@ var {
|
|||
var TabBarExample = React.createClass({
|
||||
statics: {
|
||||
title: '<TabBarIOS>',
|
||||
description: 'Tab-based navigation.'
|
||||
description: 'Tab-based navigation.',
|
||||
},
|
||||
|
||||
displayName: 'TabBarExample',
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
selectedTab: 'redTab',
|
||||
|
|
|
@ -185,6 +185,44 @@ exports.examples = [
|
|||
</View>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: 'Text Decoration',
|
||||
render: function() {
|
||||
return (
|
||||
<View>
|
||||
<Text style={{textDecorationLine: 'underline', textDecorationStyle: 'solid'}}>
|
||||
Solid underline
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'underline', textDecorationStyle: 'double', textDecorationColor: '#ff0000'}}>
|
||||
Double underline with custom color
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'underline', textDecorationStyle: 'dashed', textDecorationColor: '#9CDC40'}}>
|
||||
Dashed underline with custom color
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'underline', textDecorationStyle: 'dotted', textDecorationColor: 'blue'}}>
|
||||
Dotted underline with custom color
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'none'}}>
|
||||
None textDecoration
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'line-through', textDecorationStyle: 'solid'}}>
|
||||
Solid line-through
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'line-through', textDecorationStyle: 'double', textDecorationColor: '#ff0000'}}>
|
||||
Double line-through with custom color
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'line-through', textDecorationStyle: 'dashed', textDecorationColor: '#9CDC40'}}>
|
||||
Dashed line-through with custom color
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'line-through', textDecorationStyle: 'dotted', textDecorationColor: 'blue'}}>
|
||||
Dotted line-through with custom color
|
||||
</Text>
|
||||
<Text style={{textDecorationLine: 'underline line-through'}}>
|
||||
Both underline and line-through
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: 'Nested',
|
||||
description: 'Nested text components will inherit the styles of their ' +
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
141FC1211B222EBB004D5FFB /* IntegrationTestsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 141FC1201B222EBB004D5FFB /* IntegrationTestsTests.m */; };
|
||||
143BC5A11B21E45C00462512 /* UIExplorerIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 143BC5A01B21E45C00462512 /* UIExplorerIntegrationTests.m */; };
|
||||
141FC1211B222EBB004D5FFB /* IntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 141FC1201B222EBB004D5FFB /* IntegrationTests.m */; };
|
||||
143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */; };
|
||||
144D21241B2204C5006DB32B /* RCTClippingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 144D21231B2204C5006DB32B /* RCTClippingTests.m */; };
|
||||
147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; };
|
||||
1497CFAC1B21F5E400C1F8F2 /* RCTAllocationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */; };
|
||||
|
@ -167,7 +167,7 @@
|
|||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = UIExplorer/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = UIExplorer/main.m; sourceTree = "<group>"; };
|
||||
13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../../Libraries/Settings/RCTSettings.xcodeproj; sourceTree = "<group>"; };
|
||||
141FC1201B222EBB004D5FFB /* IntegrationTestsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IntegrationTestsTests.m; sourceTree = "<group>"; };
|
||||
141FC1201B222EBB004D5FFB /* IntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IntegrationTests.m; sourceTree = "<group>"; };
|
||||
143BC57E1B21E18100462512 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
143BC5811B21E18100462512 /* testLayoutExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testLayoutExampleSnapshot_1@2x.png"; sourceTree = "<group>"; };
|
||||
143BC5821B21E18100462512 /* testSliderExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testSliderExampleSnapshot_1@2x.png"; sourceTree = "<group>"; };
|
||||
|
@ -177,7 +177,7 @@
|
|||
143BC5861B21E18100462512 /* testViewExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testViewExampleSnapshot_1@2x.png"; sourceTree = "<group>"; };
|
||||
143BC5951B21E3E100462512 /* UIExplorerIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIExplorerIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
143BC5981B21E3E100462512 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
143BC5A01B21E45C00462512 /* UIExplorerIntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIExplorerIntegrationTests.m; sourceTree = "<group>"; };
|
||||
143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIExplorerSnapshotTests.m; sourceTree = "<group>"; };
|
||||
144D21231B2204C5006DB32B /* RCTClippingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTClippingTests.m; sourceTree = "<group>"; };
|
||||
1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAllocationTests.m; sourceTree = "<group>"; };
|
||||
1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBridgeTests.m; sourceTree = "<group>"; };
|
||||
|
@ -381,8 +381,8 @@
|
|||
143BC5961B21E3E100462512 /* UIExplorerIntegrationTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
141FC1201B222EBB004D5FFB /* IntegrationTestsTests.m */,
|
||||
143BC5A01B21E45C00462512 /* UIExplorerIntegrationTests.m */,
|
||||
141FC1201B222EBB004D5FFB /* IntegrationTests.m */,
|
||||
143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */,
|
||||
143BC5971B21E3E100462512 /* Supporting Files */,
|
||||
);
|
||||
path = UIExplorerIntegrationTests;
|
||||
|
@ -796,8 +796,8 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
141FC1211B222EBB004D5FFB /* IntegrationTestsTests.m in Sources */,
|
||||
143BC5A11B21E45C00462512 /* UIExplorerIntegrationTests.m in Sources */,
|
||||
141FC1211B222EBB004D5FFB /* IntegrationTests.m in Sources */,
|
||||
143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
13
Examples/UIExplorer/UIExplorer/Images.xcassets/uie_thumb_big.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x",
|
||||
"filename" : "uie_thumb_big.png"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Examples/UIExplorer/UIExplorer/Images.xcassets/uie_thumb_big.imageset/uie_thumb_big.png
vendored
Normal file
After Width: | Height: | Size: 6.6 KiB |
|
@ -14,11 +14,11 @@
|
|||
|
||||
#import "RCTAssert.h"
|
||||
|
||||
@interface IntegrationTestsTests : XCTestCase
|
||||
@interface IntegrationTests : XCTestCase
|
||||
|
||||
@end
|
||||
|
||||
@implementation IntegrationTestsTests
|
||||
@implementation IntegrationTests
|
||||
{
|
||||
RCTTestRunner *_runner;
|
||||
}
|
||||
|
@ -31,7 +31,7 @@
|
|||
|
||||
NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion];
|
||||
RCTAssert(version.majorVersion == 8 || version.minorVersion == 3, @"Tests should be run on iOS 8.3, found %zd.%zd.%zd", version.majorVersion, version.minorVersion, version.patchVersion);
|
||||
_runner = RCTInitRunnerForApp(@"Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestsApp");
|
||||
_runner = RCTInitRunnerForApp(@"Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestsApp", nil);
|
||||
}
|
||||
|
||||
#pragma mark Logic Tests
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 87 KiB |
After Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 267 KiB |
After Width: | Height: | Size: 265 KiB |
Before Width: | Height: | Size: 95 KiB |
After Width: | Height: | Size: 95 KiB |
|
@ -21,14 +21,14 @@
|
|||
#import "RCTRedBox.h"
|
||||
#import "RCTRootView.h"
|
||||
|
||||
@interface UIExplorerTests : XCTestCase
|
||||
@interface UIExplorerSnapshotTests : XCTestCase
|
||||
{
|
||||
RCTTestRunner *_runner;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation UIExplorerTests
|
||||
@implementation UIExplorerSnapshotTests
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
|
@ -37,21 +37,26 @@
|
|||
#endif
|
||||
NSString *version = [[UIDevice currentDevice] systemVersion];
|
||||
RCTAssert([version isEqualToString:@"8.3"], @"Snapshot tests should be run on iOS 8.3, found %@", version);
|
||||
_runner = RCTInitRunnerForApp(@"Examples/UIExplorer/UIExplorerApp.ios");
|
||||
_runner = RCTInitRunnerForApp(@"Examples/UIExplorer/UIExplorerApp.ios", nil);
|
||||
_runner.recordMode = NO;
|
||||
}
|
||||
|
||||
#define RCT_SNAPSHOT_TEST(name, reRecord) \
|
||||
- (void)test##name##Snapshot \
|
||||
{ \
|
||||
_runner.recordMode |= reRecord; \
|
||||
[_runner runTest:_cmd module:@#name]; \
|
||||
#define RCT_TEST(name) \
|
||||
- (void)test##name \
|
||||
{ \
|
||||
[_runner runTest:_cmd module:@#name]; \
|
||||
}
|
||||
|
||||
RCT_SNAPSHOT_TEST(ViewExample, NO)
|
||||
RCT_SNAPSHOT_TEST(LayoutExample, NO)
|
||||
RCT_SNAPSHOT_TEST(TextExample, NO)
|
||||
RCT_SNAPSHOT_TEST(SwitchExample, NO)
|
||||
RCT_SNAPSHOT_TEST(SliderExample, NO)
|
||||
RCT_SNAPSHOT_TEST(TabBarExample, NO)
|
||||
RCT_TEST(ViewExample)
|
||||
RCT_TEST(LayoutExample)
|
||||
RCT_TEST(TextExample)
|
||||
RCT_TEST(SwitchExample)
|
||||
RCT_TEST(SliderExample)
|
||||
RCT_TEST(TabBarExample)
|
||||
|
||||
- (void)testZZZNotInRecordMode
|
||||
{
|
||||
XCTAssertFalse(_runner.recordMode, @"Don't forget to turn record mode back to off");
|
||||
}
|
||||
|
||||
@end
|
|
@ -53,7 +53,7 @@ var IntegrationTestsApp = React.createClass({
|
|||
<View style={styles.container}>
|
||||
<Text style={styles.row}>
|
||||
Click on a test to run it in this shell for easier debugging and
|
||||
development. Run all tests in the testing envirnment with cmd+U in
|
||||
development. Run all tests in the testing environment with cmd+U in
|
||||
Xcode.
|
||||
</Text>
|
||||
<View style={styles.separator} />
|
||||
|
|
|
@ -121,7 +121,7 @@ var LayoutEventsTest = React.createClass({
|
|||
ref="img"
|
||||
onLayout={this.onImageLayout}
|
||||
style={styles.image}
|
||||
source={{uri: 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851561_767334496626293_1958532586_n.png'}}
|
||||
source={{uri: 'uie_thumb_big.png'}}
|
||||
/>
|
||||
<Text>
|
||||
ViewLayout: {JSON.stringify(this.state.viewLayout, null, ' ') + '\n\n'}
|
||||
|
|
|
@ -38,6 +38,7 @@ var createExamplePage = require('./createExamplePage');
|
|||
|
||||
var COMMON_COMPONENTS = [
|
||||
require('./ImageExample'),
|
||||
require('./LayoutEventsExample'),
|
||||
require('./ListViewExample'),
|
||||
require('./ListViewPagingExample'),
|
||||
require('./MapViewExample'),
|
||||
|
@ -50,6 +51,7 @@ var COMMON_COMPONENTS = [
|
|||
];
|
||||
|
||||
var COMMON_APIS = [
|
||||
require('./AnimationExample/AnExApp'),
|
||||
require('./GeolocationExample'),
|
||||
require('./LayoutExample'),
|
||||
require('./PanResponderExample'),
|
||||
|
@ -80,7 +82,6 @@ if (Platform.OS === 'ios') {
|
|||
require('./AsyncStorageExample'),
|
||||
require('./BorderExample'),
|
||||
require('./CameraRollExample.ios'),
|
||||
require('./LayoutEventsExample'),
|
||||
require('./NetInfoExample'),
|
||||
require('./PushNotificationIOSExample'),
|
||||
require('./StatusBarIOSExample'),
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "RCTRootView.h"
|
||||
#import "RCTShadowView.h"
|
||||
#import "RCTSparseArray.h"
|
||||
#import "RCTUIManager.h"
|
||||
#import "UIView+React.h"
|
||||
|
@ -9,14 +11,36 @@
|
|||
@interface RCTUIManager (Testing)
|
||||
|
||||
- (void)_manageChildren:(NSNumber *)containerReactTag
|
||||
moveFromIndices:(NSArray *)moveFromIndices
|
||||
moveToIndices:(NSArray *)moveToIndices
|
||||
addChildReactTags:(NSArray *)addChildReactTags
|
||||
addAtIndices:(NSArray *)addAtIndices
|
||||
removeAtIndices:(NSArray *)removeAtIndices
|
||||
registry:(RCTSparseArray *)registry;
|
||||
|
||||
- (void)modifyManageChildren:(NSNumber *)containerReactTag
|
||||
addChildReactTags:(NSMutableArray *)mutableAddChildReactTags
|
||||
addAtIndices:(NSMutableArray *)mutableAddAtIndices
|
||||
removeAtIndices:(NSMutableArray *)mutableRemoveAtIndices;
|
||||
|
||||
- (void)createView:(NSNumber *)reactTag
|
||||
viewName:(NSString *)viewName
|
||||
rootTag:(NSNumber *)rootTag
|
||||
props:(NSDictionary *)props;
|
||||
|
||||
- (void)updateView:(NSNumber *)reactTag
|
||||
viewName:(NSString *)viewName
|
||||
props:(NSDictionary *)props;
|
||||
|
||||
- (void)manageChildren:(NSNumber *)containerReactTag
|
||||
moveFromIndices:(NSArray *)moveFromIndices
|
||||
moveToIndices:(NSArray *)moveToIndices
|
||||
addChildReactTags:(NSArray *)addChildReactTags
|
||||
addAtIndices:(NSArray *)addAtIndices
|
||||
removeAtIndices:(NSArray *)removeAtIndices;
|
||||
|
||||
- (void)flushUIBlocks;
|
||||
|
||||
@property (nonatomic, readonly) RCTSparseArray *viewRegistry;
|
||||
@property (nonatomic, readonly) RCTSparseArray *shadowViewRegistry; // RCT thread only
|
||||
|
||||
@end
|
||||
|
||||
|
@ -39,6 +63,11 @@
|
|||
UIView *registeredView = [[UIView alloc] init];
|
||||
[registeredView setReactTag:@(i)];
|
||||
_uiManager.viewRegistry[i] = registeredView;
|
||||
|
||||
RCTShadowView *registeredShadowView = [[RCTShadowView alloc] init];
|
||||
registeredShadowView.viewName = @"RCTView";
|
||||
[registeredShadowView setReactTag:@(i)];
|
||||
_uiManager.shadowViewRegistry[i] = registeredShadowView;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,8 +84,6 @@
|
|||
|
||||
// Add views 1-5 to view 20
|
||||
[_uiManager _manageChildren:@20
|
||||
moveFromIndices:nil
|
||||
moveToIndices:nil
|
||||
addChildReactTags:tagsToAdd
|
||||
addAtIndices:addAtIndices
|
||||
removeAtIndices:nil
|
||||
|
@ -89,8 +116,6 @@
|
|||
|
||||
// Remove views 1-5 from view 20
|
||||
[_uiManager _manageChildren:@20
|
||||
moveFromIndices:nil
|
||||
moveToIndices:nil
|
||||
addChildReactTags:nil
|
||||
addAtIndices:nil
|
||||
removeAtIndices:removeAtIndices
|
||||
|
@ -128,11 +153,9 @@
|
|||
{
|
||||
UIView *containerView = _uiManager.viewRegistry[20];
|
||||
|
||||
NSArray *removeAtIndices = @[@2, @3, @5, @8];
|
||||
NSArray *addAtIndices = @[@0, @6];
|
||||
NSArray *tagsToAdd = @[@11, @12];
|
||||
NSArray *moveFromIndices = @[@4, @9];
|
||||
NSArray *moveToIndices = @[@1, @7];
|
||||
NSArray *removeAtIndices = @[@2, @3, @5, @8, @4, @9];
|
||||
NSArray *addAtIndices = @[@0, @6, @1, @7];
|
||||
NSArray *tagsToAdd = @[@11, @12, @5, @10];
|
||||
|
||||
// We need to keep these in array to keep them around
|
||||
NSMutableArray *viewsToRemove = [NSMutableArray array];
|
||||
|
@ -148,8 +171,6 @@
|
|||
}
|
||||
|
||||
[_uiManager _manageChildren:@20
|
||||
moveFromIndices:moveFromIndices
|
||||
moveToIndices:moveToIndices
|
||||
addChildReactTags:tagsToAdd
|
||||
addAtIndices:addAtIndices
|
||||
removeAtIndices:removeAtIndices
|
||||
|
@ -176,4 +197,329 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* +-----------------------------------------------------------+ +----------------------+
|
||||
* | Shadow Hierarchy | | Legend |
|
||||
* +-----------------------------------------------------------+ +----------------------+
|
||||
* | | | |
|
||||
* | +---+ ****** | | ******************** |
|
||||
* | | 1 | * 11 * | | * Layout-only View * |
|
||||
* | +---+ ****** | | ******************** |
|
||||
* | | | | | |
|
||||
* | +-------+---+---+----------+ +---+---+ | | +----+ |
|
||||
* | | | | | | | | | |View| Subview |
|
||||
* | v v v v v v | | +----+ -----------> |
|
||||
* | ***** +---+ ***** +---+ +----+ +----+ | | |
|
||||
* | * 2 * | 3 | * 4 * | 5 | | 12 | | 13 | | +----------------------+
|
||||
* | ***** +---+ ***** +---+ +----+ +----+ |
|
||||
* | | | | |
|
||||
* | +---+--+ | +---+---+ |
|
||||
* | | | | | | |
|
||||
* | v v v v v |
|
||||
* | +---+ +---+ +---+ +---+ ****** |
|
||||
* | | 6 | | 7 | | 8 | | 9 | * 10 * |
|
||||
* | +---+ +---+ +---+ +---+ ****** |
|
||||
* | |
|
||||
* +-----------------------------------------------------------+
|
||||
*
|
||||
* +-----------------------------------------------------------+
|
||||
* | View Hierarchy |
|
||||
* +-----------------------------------------------------------+
|
||||
* | |
|
||||
* | +---+ ****** |
|
||||
* | | 1 | * 11 * |
|
||||
* | +---+ ****** |
|
||||
* | | | |
|
||||
* | +------+------+------+------+ +---+---+ |
|
||||
* | | | | | | | | |
|
||||
* | v v v v v v v |
|
||||
* | +---+ +---+ +---+ +---+ +---+ +----+ +----+ |
|
||||
* | | 6 | | 7 | | 3 | | 8 | | 5 | | 12 | | 13 | |
|
||||
* | +---+ +---+ +---+ +---+ +---+ +----+ +----+ |
|
||||
* | | |
|
||||
* | v |
|
||||
* | +---+ |
|
||||
* | | 9 | |
|
||||
* | +---+ |
|
||||
* | |
|
||||
* +-----------------------------------------------------------+
|
||||
*/
|
||||
|
||||
- (void)updateShadowViewWithReactTag:(NSNumber *)reactTag layoutOnly:(BOOL)isLayoutOnly childTags:(NSArray *)childTags
|
||||
{
|
||||
RCTShadowView *shadowView = _uiManager.shadowViewRegistry[reactTag];
|
||||
shadowView.allProps = isLayoutOnly ? @{} : @{@"collapsible": @NO};
|
||||
[childTags enumerateObjectsUsingBlock:^(NSNumber *childTag, NSUInteger idx, __unused BOOL *stop) {
|
||||
[shadowView insertReactSubview:_uiManager.shadowViewRegistry[childTag] atIndex:idx];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)setUpShadowViewHierarchy
|
||||
{
|
||||
[self updateShadowViewWithReactTag:@1 layoutOnly:NO childTags:@[@2, @3, @4, @5]];
|
||||
[self updateShadowViewWithReactTag:@2 layoutOnly:YES childTags:@[@6, @7]];
|
||||
[self updateShadowViewWithReactTag:@3 layoutOnly:NO childTags:nil];
|
||||
[self updateShadowViewWithReactTag:@4 layoutOnly:YES childTags:@[@8]];
|
||||
[self updateShadowViewWithReactTag:@5 layoutOnly:NO childTags:@[@9, @10]];
|
||||
[self updateShadowViewWithReactTag:@6 layoutOnly:NO childTags:nil];
|
||||
[self updateShadowViewWithReactTag:@7 layoutOnly:NO childTags:nil];
|
||||
[self updateShadowViewWithReactTag:@8 layoutOnly:NO childTags:nil];
|
||||
[self updateShadowViewWithReactTag:@9 layoutOnly:NO childTags:nil];
|
||||
[self updateShadowViewWithReactTag:@10 layoutOnly:YES childTags:nil];
|
||||
[self updateShadowViewWithReactTag:@11 layoutOnly:YES childTags:@[@12, @13]];
|
||||
[self updateShadowViewWithReactTag:@12 layoutOnly:NO childTags:nil];
|
||||
[self updateShadowViewWithReactTag:@13 layoutOnly:NO childTags:nil];
|
||||
}
|
||||
|
||||
- (void)testModifyIndices1
|
||||
{
|
||||
[self setUpShadowViewHierarchy];
|
||||
|
||||
NSMutableArray *addTags = [@[@2] mutableCopy];
|
||||
NSMutableArray *addIndices = [@[@3] mutableCopy];
|
||||
NSMutableArray *removeIndices = [@[@0] mutableCopy];
|
||||
[self.uiManager modifyManageChildren:@1
|
||||
addChildReactTags:addTags
|
||||
addAtIndices:addIndices
|
||||
removeAtIndices:removeIndices];
|
||||
XCTAssertEqualObjects(addTags, (@[@6, @7]));
|
||||
XCTAssertEqualObjects(addIndices, (@[@3, @4]));
|
||||
XCTAssertEqualObjects(removeIndices, (@[@0, @1]));
|
||||
}
|
||||
|
||||
- (void)testModifyIndices2
|
||||
{
|
||||
[self setUpShadowViewHierarchy];
|
||||
|
||||
NSMutableArray *addTags = [@[@11] mutableCopy];
|
||||
NSMutableArray *addIndices = [@[@4] mutableCopy];
|
||||
NSMutableArray *removeIndices = [@[] mutableCopy];
|
||||
[self.uiManager modifyManageChildren:@1
|
||||
addChildReactTags:addTags
|
||||
addAtIndices:addIndices
|
||||
removeAtIndices:removeIndices];
|
||||
XCTAssertEqualObjects(addTags, (@[@12, @13]));
|
||||
XCTAssertEqualObjects(addIndices, (@[@5, @6]));
|
||||
XCTAssertEqualObjects(removeIndices, (@[]));
|
||||
}
|
||||
|
||||
- (void)testModifyIndices3
|
||||
{
|
||||
[self setUpShadowViewHierarchy];
|
||||
|
||||
NSMutableArray *addTags = [@[] mutableCopy];
|
||||
NSMutableArray *addIndices = [@[] mutableCopy];
|
||||
NSMutableArray *removeIndices = [@[@2] mutableCopy];
|
||||
[self.uiManager modifyManageChildren:@1
|
||||
addChildReactTags:addTags
|
||||
addAtIndices:addIndices
|
||||
removeAtIndices:removeIndices];
|
||||
XCTAssertEqualObjects(addTags, (@[]));
|
||||
XCTAssertEqualObjects(addIndices, (@[]));
|
||||
XCTAssertEqualObjects(removeIndices, (@[@3]));
|
||||
}
|
||||
|
||||
- (void)testModifyIndices4
|
||||
{
|
||||
[self setUpShadowViewHierarchy];
|
||||
|
||||
NSMutableArray *addTags = [@[@11] mutableCopy];
|
||||
NSMutableArray *addIndices = [@[@3] mutableCopy];
|
||||
NSMutableArray *removeIndices = [@[@2] mutableCopy];
|
||||
[self.uiManager modifyManageChildren:@1
|
||||
addChildReactTags:addTags
|
||||
addAtIndices:addIndices
|
||||
removeAtIndices:removeIndices];
|
||||
XCTAssertEqualObjects(addTags, (@[@12, @13]));
|
||||
XCTAssertEqualObjects(addIndices, (@[@4, @5]));
|
||||
XCTAssertEqualObjects(removeIndices, (@[@3]));
|
||||
}
|
||||
|
||||
- (void)testModifyIndices5
|
||||
{
|
||||
[self setUpShadowViewHierarchy];
|
||||
|
||||
NSMutableArray *addTags = [@[] mutableCopy];
|
||||
NSMutableArray *addIndices = [@[] mutableCopy];
|
||||
NSMutableArray *removeIndices = [@[@0, @1, @2, @3] mutableCopy];
|
||||
[self.uiManager modifyManageChildren:@1
|
||||
addChildReactTags:addTags
|
||||
addAtIndices:addIndices
|
||||
removeAtIndices:removeIndices];
|
||||
XCTAssertEqualObjects(addTags, (@[]));
|
||||
XCTAssertEqualObjects(addIndices, (@[]));
|
||||
XCTAssertEqualObjects(removeIndices, (@[@0, @1, @2, @3, @4]));
|
||||
}
|
||||
|
||||
- (void)testModifyIndices6
|
||||
{
|
||||
[self setUpShadowViewHierarchy];
|
||||
|
||||
NSMutableArray *addTags = [@[@11] mutableCopy];
|
||||
NSMutableArray *addIndices = [@[@0] mutableCopy];
|
||||
NSMutableArray *removeIndices = [@[@0, @1, @2, @3] mutableCopy];
|
||||
[self.uiManager modifyManageChildren:@1
|
||||
addChildReactTags:addTags
|
||||
addAtIndices:addIndices
|
||||
removeAtIndices:removeIndices];
|
||||
XCTAssertEqualObjects(addTags, (@[@12, @13]));
|
||||
XCTAssertEqualObjects(addIndices, (@[@0, @1]));
|
||||
XCTAssertEqualObjects(removeIndices, (@[@0, @1, @2, @3, @4]));
|
||||
}
|
||||
|
||||
- (void)testModifyIndices7
|
||||
{
|
||||
[self setUpShadowViewHierarchy];
|
||||
|
||||
NSMutableArray *addTags = [@[@11] mutableCopy];
|
||||
NSMutableArray *addIndices = [@[@1] mutableCopy];
|
||||
NSMutableArray *removeIndices = [@[@0, @2, @3] mutableCopy];
|
||||
[self.uiManager modifyManageChildren:@1
|
||||
addChildReactTags:addTags
|
||||
addAtIndices:addIndices
|
||||
removeAtIndices:removeIndices];
|
||||
XCTAssertEqualObjects(addTags, (@[@12, @13]));
|
||||
XCTAssertEqualObjects(addIndices, (@[@1, @2]));
|
||||
XCTAssertEqualObjects(removeIndices, (@[@0, @1, @3, @4]));
|
||||
}
|
||||
|
||||
- (void)testScenario1
|
||||
{
|
||||
RCTUIManager *uiManager = [[RCTUIManager alloc] init];
|
||||
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[uiManager]; } launchOptions:nil];
|
||||
NS_VALID_UNTIL_END_OF_SCOPE RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"Test"];
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
||||
|
||||
dispatch_queue_t shadowQueue = [uiManager valueForKey:@"shadowQueue"];
|
||||
dispatch_async(shadowQueue, ^{
|
||||
// Make sure root view finishes loading.
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{});
|
||||
|
||||
/* */[uiManager createView:@2 viewName:@"RCTView" rootTag:@1 props:@{@"bottom":@0,@"left":@0,@"position":@"absolute",@"right":@0,@"top":@0}];
|
||||
/* */[uiManager createView:@3 viewName:@"RCTView" rootTag:@1 props:@{@"bottom":@0,@"left":@0,@"position":@"absolute",@"right":@0,@"top":@0}];
|
||||
/* V */[uiManager createView:@4 viewName:@"RCTView" rootTag:@1 props:@{@"alignItems":@"center",@"backgroundColor":@"#F5FCFF",@"flex":@1,@"justifyContent":@"center"}];
|
||||
/* V */[uiManager createView:@5 viewName:@"RCTView" rootTag:@1 props:@{@"backgroundColor":@"blue",@"height":@50,@"width":@50}];
|
||||
/* */[uiManager createView:@6 viewName:@"RCTView" rootTag:@1 props:@{@"width":@250}];
|
||||
/* V */[uiManager createView:@7 viewName:@"RCTView" rootTag:@1 props:@{@"borderWidth":@10,@"margin":@50}];
|
||||
/* V */[uiManager createView:@8 viewName:@"RCTView" rootTag:@1 props:@{@"backgroundColor":@"yellow",@"height":@50}];
|
||||
/* V */[uiManager createView:@9 viewName:@"RCTText" rootTag:@1 props:@{@"accessible":@1,@"fontSize":@20,@"isHighlighted":@0,@"margin":@10,@"textAlign":@"center"}];
|
||||
/* */[uiManager createView:@10 viewName:@"RCTRawText" rootTag:@1 props:@{@"text":@"This tests removal of layout-only views."}];
|
||||
/* */[uiManager manageChildren:@9 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@10] addAtIndices:@[@0] removeAtIndices:nil];
|
||||
/* V */[uiManager createView:@12 viewName:@"RCTView" rootTag:@1 props:@{@"backgroundColor":@"green",@"height":@50}];
|
||||
/* */[uiManager manageChildren:@7 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@8,@9,@12] addAtIndices:@[@0,@1,@2] removeAtIndices:nil];
|
||||
/* */[uiManager manageChildren:@6 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@7] addAtIndices:@[@0] removeAtIndices:nil];
|
||||
/* V */[uiManager createView:@13 viewName:@"RCTView" rootTag:@1 props:@{@"backgroundColor":@"red",@"height":@50,@"width":@50}];
|
||||
/* */[uiManager manageChildren:@4 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@5,@6,@13] addAtIndices:@[@0,@1,@2] removeAtIndices:nil];
|
||||
/* */[uiManager manageChildren:@3 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@4] addAtIndices:@[@0] removeAtIndices:nil];
|
||||
/* */[uiManager manageChildren:@2 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@3] addAtIndices:@[@0] removeAtIndices:nil];
|
||||
/* */[uiManager manageChildren:@1 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@2] addAtIndices:@[@0] removeAtIndices:nil];
|
||||
|
||||
[uiManager addUIBlock:^(RCTUIManager *uiManager_, __unused RCTSparseArray *viewRegistry) {
|
||||
XCTAssertEqual(uiManager_.shadowViewRegistry.count, (NSUInteger)12);
|
||||
XCTAssertEqual(uiManager_.viewRegistry.count, (NSUInteger)8);
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
[uiManager flushUIBlocks];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:1 handler:nil];
|
||||
|
||||
expectation = [self expectationWithDescription:@""];
|
||||
dispatch_async(shadowQueue, ^{
|
||||
[uiManager updateView:@7 viewName:@"RCTView" props:@{@"borderWidth":[NSNull null]}];
|
||||
[uiManager addUIBlock:^(RCTUIManager *uiManager_, __unused RCTSparseArray *viewRegistry) {
|
||||
XCTAssertEqual(uiManager_.shadowViewRegistry.count, (NSUInteger)12);
|
||||
XCTAssertEqual(uiManager_.viewRegistry.count, (NSUInteger)7);
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
[uiManager flushUIBlocks];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:1 handler:nil];
|
||||
|
||||
expectation = [self expectationWithDescription:@""];
|
||||
dispatch_async(shadowQueue, ^{
|
||||
[uiManager updateView:@7 viewName:@"RCTView" props:@{@"borderWidth":@10}];
|
||||
[uiManager addUIBlock:^(RCTUIManager *uiManager_, __unused RCTSparseArray *viewRegistry) {
|
||||
XCTAssertEqual(uiManager_.shadowViewRegistry.count, (NSUInteger)12);
|
||||
XCTAssertEqual(uiManager_.viewRegistry.count, (NSUInteger)8);
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
[uiManager flushUIBlocks];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:1 handler:nil];
|
||||
}
|
||||
|
||||
- (void)testScenario2
|
||||
{
|
||||
RCTUIManager *uiManager = [[RCTUIManager alloc] init];
|
||||
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[uiManager]; } launchOptions:nil];
|
||||
NS_VALID_UNTIL_END_OF_SCOPE RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"Test"];
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@""];
|
||||
|
||||
dispatch_queue_t shadowQueue = [uiManager valueForKey:@"shadowQueue"];
|
||||
dispatch_async(shadowQueue, ^{
|
||||
// Make sure root view finishes loading.
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{});
|
||||
|
||||
/* */[uiManager createView:@2 viewName:@"RCTView" rootTag:@1 props:@{@"bottom":@0,@"left":@0,@"position":@"absolute",@"right":@0,@"top":@0}];
|
||||
/* */[uiManager createView:@3 viewName:@"RCTView" rootTag:@1 props:@{@"bottom":@0,@"left":@0,@"position":@"absolute",@"right":@0,@"top":@0}];
|
||||
/* V */[uiManager createView:@4 viewName:@"RCTView" rootTag:@1 props:@{@"alignItems":@"center",@"backgroundColor":@"#F5FCFF",@"flex":@1,@"justifyContent":@"center"}];
|
||||
/* */[uiManager createView:@5 viewName:@"RCTView" rootTag:@1 props:@{@"width":@250}];
|
||||
/* V */[uiManager createView:@6 viewName:@"RCTView" rootTag:@1 props:@{@"borderWidth":@1}];
|
||||
/* V */[uiManager createView:@7 viewName:@"RCTText" rootTag:@1 props:@{@"accessible":@1,@"fontSize":@20,@"isHighlighted":@0,@"margin":@10,@"textAlign":@"center"}];
|
||||
/* */[uiManager createView:@8 viewName:@"RCTRawText" rootTag:@1 props:@{@"text":@"This tests removal of layout-only views."}];
|
||||
/* */[uiManager manageChildren:@7 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@8] addAtIndices:@[@0] removeAtIndices:nil];
|
||||
/* */[uiManager manageChildren:@6 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@7] addAtIndices:@[@0] removeAtIndices:nil];
|
||||
/* */[uiManager manageChildren:@5 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@6] addAtIndices:@[@0] removeAtIndices:nil];
|
||||
/* */[uiManager manageChildren:@4 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@5] addAtIndices:@[@0] removeAtIndices:nil];
|
||||
/* */[uiManager manageChildren:@3 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@4] addAtIndices:@[@0] removeAtIndices:nil];
|
||||
/* */[uiManager manageChildren:@2 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@3] addAtIndices:@[@0] removeAtIndices:nil];
|
||||
/* */[uiManager manageChildren:@1 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@2] addAtIndices:@[@0] removeAtIndices:nil];
|
||||
|
||||
[uiManager addUIBlock:^(RCTUIManager *uiManager_, __unused RCTSparseArray *viewRegistry) {
|
||||
XCTAssertEqual(uiManager_.shadowViewRegistry.count, (NSUInteger)8);
|
||||
XCTAssertEqual(uiManager_.viewRegistry.count, (NSUInteger)4);
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
[uiManager flushUIBlocks];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:1 handler:nil];
|
||||
|
||||
expectation = [self expectationWithDescription:@""];
|
||||
dispatch_async(shadowQueue, ^{
|
||||
[uiManager updateView:@6 viewName:@"RCTView" props:@{@"borderWidth":[NSNull null]}];
|
||||
[uiManager addUIBlock:^(RCTUIManager *uiManager_, __unused RCTSparseArray *viewRegistry) {
|
||||
XCTAssertEqual(uiManager_.shadowViewRegistry.count, (NSUInteger)8);
|
||||
XCTAssertEqual(uiManager_.viewRegistry.count, (NSUInteger)3);
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
[uiManager flushUIBlocks];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:1 handler:nil];
|
||||
|
||||
expectation = [self expectationWithDescription:@""];
|
||||
dispatch_async(shadowQueue, ^{
|
||||
[uiManager updateView:@6 viewName:@"RCTView" props:@{@"borderWidth":@1}];
|
||||
[uiManager addUIBlock:^(RCTUIManager *uiManager_, __unused RCTSparseArray *viewRegistry) {
|
||||
XCTAssertEqual(uiManager_.shadowViewRegistry.count, (NSUInteger)8);
|
||||
XCTAssertEqual(uiManager_.viewRegistry.count, (NSUInteger)4);
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
[uiManager flushUIBlocks];
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:1 handler:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -10,19 +10,20 @@
|
|||
#import <AdSupport/ASIdentifierManager.h>
|
||||
|
||||
#import "RCTAdSupport.h"
|
||||
#import "RCTUtils.h"
|
||||
|
||||
@implementation RCTAdSupport
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
RCT_EXPORT_METHOD(getAdvertisingId:(RCTResponseSenderBlock)callback
|
||||
withErrorCallback:(RCTResponseSenderBlock)errorCallback)
|
||||
withErrorCallback:(RCTResponseErrorBlock)errorCallback)
|
||||
{
|
||||
NSUUID *advertisingIdentifier = [ASIdentifierManager sharedManager].advertisingIdentifier;
|
||||
if (advertisingIdentifier) {
|
||||
callback(@[advertisingIdentifier.UUIDString]);
|
||||
} else {
|
||||
errorCallback(@[@"as_identifier_unavailable"]);
|
||||
errorCallback(RCTErrorWithMessage(@"Advertising identifier is unavailable."));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule Easing
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var bezier = require('bezier');
|
||||
|
||||
/**
|
||||
* This class implements common easing functions. The math is pretty obscure,
|
||||
* but this cool website has nice visual illustrations of what they represent:
|
||||
* http://xaedes.de/dev/transitions/
|
||||
*/
|
||||
class Easing {
|
||||
static step0(n) {
|
||||
return n > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
static step1(n) {
|
||||
return n >= 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
static linear(t) {
|
||||
return t;
|
||||
}
|
||||
|
||||
static ease(t: number): number {
|
||||
return ease(t);
|
||||
}
|
||||
|
||||
static quad(t) {
|
||||
return t * t;
|
||||
}
|
||||
|
||||
static cubic(t) {
|
||||
return t * t * t;
|
||||
}
|
||||
|
||||
static poly(n) {
|
||||
return (t) => Math.pow(t, n);
|
||||
}
|
||||
|
||||
static sin(t) {
|
||||
return 1 - Math.cos(t * Math.PI / 2);
|
||||
}
|
||||
|
||||
static circle(t) {
|
||||
return 1 - Math.sqrt(1 - t * t);
|
||||
}
|
||||
|
||||
static exp(t) {
|
||||
return Math.pow(2, 10 * (t - 1));
|
||||
}
|
||||
|
||||
static elastic(a: number, p: number): (t: number) => number {
|
||||
var tau = Math.PI * 2;
|
||||
// flow isn't smart enough to figure out that s is always assigned to a
|
||||
// number before being used in the returned function
|
||||
var s: any;
|
||||
if (arguments.length < 2) {
|
||||
p = 0.45;
|
||||
}
|
||||
if (arguments.length) {
|
||||
s = p / tau * Math.asin(1 / a);
|
||||
} else {
|
||||
a = 1;
|
||||
s = p / 4;
|
||||
}
|
||||
return (t) => 1 + a * Math.pow(2, -10 * t) * Math.sin((t - s) * tau / p);
|
||||
};
|
||||
|
||||
static back(s: number): (t: number) => number {
|
||||
if (s === undefined) {
|
||||
s = 1.70158;
|
||||
}
|
||||
return (t) => t * t * ((s + 1) * t - s);
|
||||
};
|
||||
|
||||
static bounce(t: number): number {
|
||||
if (t < 1 / 2.75) {
|
||||
return 7.5625 * t * t;
|
||||
}
|
||||
|
||||
if (t < 2 / 2.75) {
|
||||
t -= 1.5 / 2.75;
|
||||
return 7.5625 * t * t + 0.75;
|
||||
}
|
||||
|
||||
if (t < 2.5 / 2.75) {
|
||||
t -= 2.25 / 2.75;
|
||||
return 7.5625 * t * t + 0.9375;
|
||||
}
|
||||
|
||||
t -= 2.625 / 2.75;
|
||||
return 7.5625 * t * t + 0.984375;
|
||||
};
|
||||
|
||||
static bezier(
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
epsilon?: ?number,
|
||||
): (t: number) => number {
|
||||
if (epsilon === undefined) {
|
||||
// epsilon determines the precision of the solved values
|
||||
// a good approximation is:
|
||||
var duration = 500; // duration of animation in milliseconds.
|
||||
epsilon = (1000 / 60 / duration) / 4;
|
||||
}
|
||||
|
||||
return bezier(x1, y1, x2, y2, epsilon);
|
||||
}
|
||||
|
||||
static in(
|
||||
easing: (t: number) => number,
|
||||
): (t: number) => number {
|
||||
return easing;
|
||||
}
|
||||
|
||||
static out(
|
||||
easing: (t: number) => number,
|
||||
): (t: number) => number {
|
||||
return (t) => 1 - easing(1 - t);
|
||||
}
|
||||
|
||||
static inOut(
|
||||
easing: (t: number) => number,
|
||||
): (t: number) => number {
|
||||
return (t) => {
|
||||
if (t < 0.5) {
|
||||
return easing(t * 2) / 2;
|
||||
}
|
||||
return 1 - easing((1 - t) * 2) / 2;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var ease = Easing.bezier(0.42, 0, 1, 1);
|
||||
|
||||
module.exports = Easing;
|
|
@ -0,0 +1,265 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule Interpolation
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
// TODO(#7644673): fix this hack once github jest actually checks invariants
|
||||
var invariant = function(condition, message) {
|
||||
if (!condition) {
|
||||
var error = new Error(message);
|
||||
(error: any).framesToPop = 1; // $FlowIssue
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
type ExtrapolateType = 'extend' | 'identity' | 'clamp';
|
||||
|
||||
// $FlowFixMe D2163827
|
||||
export type InterpolationConfigType = {
|
||||
inputRange: Array<number>;
|
||||
outputRange: (Array<number> | Array<string>);
|
||||
easing?: ((input: number) => number);
|
||||
extrapolate?: ExtrapolateType;
|
||||
extrapolateLeft?: ExtrapolateType;
|
||||
extrapolateRight?: ExtrapolateType;
|
||||
};
|
||||
|
||||
var linear = (t) => t;
|
||||
|
||||
/**
|
||||
* Very handy helper to map input ranges to output ranges with an easing
|
||||
* function and custom behavior outside of the ranges.
|
||||
*/
|
||||
class Interpolation {
|
||||
static create(config: InterpolationConfigType): (input: number) => number | string {
|
||||
|
||||
if (config.outputRange && typeof config.outputRange[0] === 'string') {
|
||||
return createInterpolationFromStringOutputRange(config);
|
||||
}
|
||||
|
||||
var outputRange: Array<number> = (config.outputRange: any);
|
||||
checkInfiniteRange('outputRange', outputRange);
|
||||
|
||||
var inputRange = config.inputRange;
|
||||
checkInfiniteRange('inputRange', inputRange);
|
||||
checkValidInputRange(inputRange);
|
||||
|
||||
invariant(
|
||||
inputRange.length === outputRange.length,
|
||||
'inputRange (' + inputRange.length + ') and outputRange (' +
|
||||
outputRange.length + ') must have the same length'
|
||||
);
|
||||
|
||||
var easing = config.easing || linear;
|
||||
|
||||
var extrapolateLeft: ExtrapolateType = 'extend';
|
||||
if (config.extrapolateLeft !== undefined) {
|
||||
extrapolateLeft = config.extrapolateLeft;
|
||||
} else if (config.extrapolate !== undefined) {
|
||||
extrapolateLeft = config.extrapolate;
|
||||
}
|
||||
|
||||
var extrapolateRight: ExtrapolateType = 'extend';
|
||||
if (config.extrapolateRight !== undefined) {
|
||||
extrapolateRight = config.extrapolateRight;
|
||||
} else if (config.extrapolate !== undefined) {
|
||||
extrapolateRight = config.extrapolate;
|
||||
}
|
||||
|
||||
return (input) => {
|
||||
invariant(
|
||||
typeof input === 'number',
|
||||
'Cannot interpolation an input which is not a number'
|
||||
);
|
||||
|
||||
var range = findRange(input, inputRange);
|
||||
return interpolate(
|
||||
input,
|
||||
inputRange[range],
|
||||
inputRange[range + 1],
|
||||
outputRange[range],
|
||||
outputRange[range + 1],
|
||||
easing,
|
||||
extrapolateLeft,
|
||||
extrapolateRight,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function interpolate(
|
||||
input: number,
|
||||
inputMin: number,
|
||||
inputMax: number,
|
||||
outputMin: number,
|
||||
outputMax: number,
|
||||
easing: ((input: number) => number),
|
||||
extrapolateLeft: ExtrapolateType,
|
||||
extrapolateRight: ExtrapolateType,
|
||||
) {
|
||||
var result = input;
|
||||
|
||||
// Extrapolate
|
||||
if (result < inputMin) {
|
||||
if (extrapolateLeft === 'identity') {
|
||||
return result;
|
||||
} else if (extrapolateLeft === 'clamp') {
|
||||
result = inputMin;
|
||||
} else if (extrapolateLeft === 'extend') {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
if (result > inputMax) {
|
||||
if (extrapolateRight === 'identity') {
|
||||
return result;
|
||||
} else if (extrapolateRight === 'clamp') {
|
||||
result = inputMax;
|
||||
} else if (extrapolateRight === 'extend') {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
if (outputMin === outputMax) {
|
||||
return outputMin;
|
||||
}
|
||||
|
||||
if (inputMin === inputMax) {
|
||||
if (input <= inputMin) {
|
||||
return outputMin;
|
||||
}
|
||||
return outputMax;
|
||||
}
|
||||
|
||||
// Input Range
|
||||
if (inputMin === -Infinity) {
|
||||
result = -result;
|
||||
} else if (inputMax === Infinity) {
|
||||
result = result - inputMin;
|
||||
} else {
|
||||
result = (result - inputMin) / (inputMax - inputMin);
|
||||
}
|
||||
|
||||
// Easing
|
||||
result = easing(result);
|
||||
|
||||
// Output Range
|
||||
if (outputMin === -Infinity) {
|
||||
result = -result;
|
||||
} else if (outputMax === Infinity) {
|
||||
result = result + outputMin;
|
||||
} else {
|
||||
result = result * (outputMax - outputMin) + outputMin;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
var stringShapeRegex = /[0-9\.-]+/g;
|
||||
|
||||
/**
|
||||
* Supports string shapes by extracting numbers so new values can be computed,
|
||||
* and recombines those values into new strings of the same shape. Supports
|
||||
* things like:
|
||||
*
|
||||
* rgba(123, 42, 99, 0.36) // colors
|
||||
* -45deg // values with units
|
||||
*/
|
||||
function createInterpolationFromStringOutputRange(
|
||||
config: InterpolationConfigType,
|
||||
): (input: number) => string {
|
||||
var outputRange: Array<string> = (config.outputRange: any);
|
||||
invariant(outputRange.length >= 2, 'Bad output range');
|
||||
checkPattern(outputRange);
|
||||
|
||||
// ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)']
|
||||
// ->
|
||||
// [
|
||||
// [0, 50],
|
||||
// [100, 150],
|
||||
// [200, 250],
|
||||
// [0, 0.5],
|
||||
// ]
|
||||
var outputRanges = outputRange[0].match(stringShapeRegex).map(() => []);
|
||||
outputRange.forEach(value => {
|
||||
value.match(stringShapeRegex).forEach((number, i) => {
|
||||
outputRanges[i].push(+number);
|
||||
});
|
||||
});
|
||||
|
||||
var interpolations = outputRange[0].match(stringShapeRegex).map((value, i) => {
|
||||
return Interpolation.create({
|
||||
...config,
|
||||
outputRange: outputRanges[i],
|
||||
});
|
||||
});
|
||||
|
||||
return (input) => {
|
||||
var i = 0;
|
||||
// 'rgba(0, 100, 200, 0)'
|
||||
// ->
|
||||
// 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
|
||||
return outputRange[0].replace(stringShapeRegex, () => {
|
||||
return String(interpolations[i++](input));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function checkPattern(arr: Array<string>) {
|
||||
var pattern = arr[0].replace(stringShapeRegex, '');
|
||||
for (var i = 1; i < arr.length; ++i) {
|
||||
invariant(
|
||||
pattern === arr[i].replace(stringShapeRegex, ''),
|
||||
'invalid pattern ' + arr[0] + ' and ' + arr[i],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function findRange(input: number, inputRange: Array<number>) {
|
||||
for (var i = 1; i < inputRange.length - 1; ++i) {
|
||||
if (inputRange[i] >= input) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return i - 1;
|
||||
}
|
||||
|
||||
function checkValidInputRange(arr: Array<number>) {
|
||||
invariant(arr.length >= 2, 'inputRange must have at least 2 elements');
|
||||
for (var i = 1; i < arr.length; ++i) {
|
||||
invariant(
|
||||
arr[i] >= arr[i - 1],
|
||||
/* $FlowFixMe(>=0.13.0) - In the addition expression below this comment,
|
||||
* one or both of the operands may be something that doesn't cleanly
|
||||
* convert to a string, like undefined, null, and object, etc. If you really
|
||||
* mean this implicit string conversion, you can do something like
|
||||
* String(myThing)
|
||||
*/
|
||||
'inputRange must be monolithically increasing ' + arr
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function checkInfiniteRange(name: string, arr: Array<number>) {
|
||||
invariant(arr.length >= 2, name + ' must have at least 2 elements');
|
||||
invariant(
|
||||
arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity,
|
||||
/* $FlowFixMe(>=0.13.0) - In the addition expression below this comment,
|
||||
* one or both of the operands may be something that doesn't cleanly convert
|
||||
* to a string, like undefined, null, and object, etc. If you really mean
|
||||
* this implicit string conversion, you can do something like
|
||||
* String(myThing)
|
||||
*/
|
||||
name + 'cannot be ]-infinity;+infinity[ ' + arr
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = Interpolation;
|
|
@ -0,0 +1,493 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest
|
||||
.autoMockOff()
|
||||
.setMock('Text', {})
|
||||
.setMock('View', {})
|
||||
.setMock('Image', {})
|
||||
.setMock('React', {Component: class {}});
|
||||
|
||||
var Animated = require('Animated');
|
||||
|
||||
describe('Animated', () => {
|
||||
it('works end to end', () => {
|
||||
var anim = new Animated.Value(0);
|
||||
|
||||
var callback = jest.genMockFunction();
|
||||
|
||||
var node = new Animated.__PropsOnlyForTests({
|
||||
style: {
|
||||
backgroundColor: 'red',
|
||||
opacity: anim,
|
||||
transform: [
|
||||
{translateX: anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [100, 200],
|
||||
})},
|
||||
{scale: anim},
|
||||
]
|
||||
}
|
||||
}, callback);
|
||||
|
||||
expect(anim.getChildren().length).toBe(3);
|
||||
|
||||
expect(node.__getValue()).toEqual({
|
||||
style: {
|
||||
backgroundColor: 'red',
|
||||
opacity: 0,
|
||||
transform: [
|
||||
{translateX: 100},
|
||||
{scale: 0},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
anim.setValue(0.5);
|
||||
|
||||
expect(callback).toBeCalled();
|
||||
|
||||
expect(node.__getValue()).toEqual({
|
||||
style: {
|
||||
backgroundColor: 'red',
|
||||
opacity: 0.5,
|
||||
transform: [
|
||||
{translateX: 150},
|
||||
{scale: 0.5},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
node.detach();
|
||||
expect(anim.getChildren().length).toBe(0);
|
||||
|
||||
anim.setValue(1);
|
||||
expect(callback.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('does not detach on updates', () => {
|
||||
var anim = new Animated.Value(0);
|
||||
anim.detach = jest.genMockFunction();
|
||||
|
||||
var c = new Animated.View();
|
||||
c.props = {
|
||||
style: {
|
||||
opacity: anim,
|
||||
},
|
||||
};
|
||||
c.componentWillMount();
|
||||
|
||||
expect(anim.detach).not.toBeCalled();
|
||||
c.componentWillReceiveProps({
|
||||
style: {
|
||||
opacity: anim,
|
||||
},
|
||||
});
|
||||
expect(anim.detach).not.toBeCalled();
|
||||
|
||||
c.componentWillUnmount();
|
||||
expect(anim.detach).toBeCalled();
|
||||
});
|
||||
|
||||
|
||||
it('stops animation when detached', () => {
|
||||
// jest environment doesn't have requestAnimationFrame :(
|
||||
window.requestAnimationFrame = jest.genMockFunction();
|
||||
window.cancelAnimationFrame = jest.genMockFunction();
|
||||
|
||||
var anim = new Animated.Value(0);
|
||||
var callback = jest.genMockFunction();
|
||||
|
||||
var c = new Animated.View();
|
||||
c.props = {
|
||||
style: {
|
||||
opacity: anim,
|
||||
},
|
||||
};
|
||||
c.componentWillMount();
|
||||
|
||||
Animated.timing(anim, {toValue: 10, duration: 1000}).start(callback);
|
||||
|
||||
c.componentWillUnmount();
|
||||
|
||||
expect(callback).toBeCalledWith({finished: false});
|
||||
expect(callback).toBeCalledWith({finished: false});
|
||||
});
|
||||
|
||||
it('triggers callback when spring is at rest', () => {
|
||||
var anim = new Animated.Value(0);
|
||||
var callback = jest.genMockFunction();
|
||||
Animated.spring(anim, {toValue: 0, velocity: 0}).start(callback);
|
||||
expect(callback).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Animated Sequence', () => {
|
||||
|
||||
it('works with an empty sequence', () => {
|
||||
var cb = jest.genMockFunction();
|
||||
Animated.sequence([]).start(cb);
|
||||
expect(cb).toBeCalledWith({finished: true});
|
||||
});
|
||||
|
||||
it('sequences well', () => {
|
||||
var anim1 = {start: jest.genMockFunction()};
|
||||
var anim2 = {start: jest.genMockFunction()};
|
||||
var cb = jest.genMockFunction();
|
||||
|
||||
var seq = Animated.sequence([anim1, anim2]);
|
||||
|
||||
expect(anim1.start).not.toBeCalled();
|
||||
expect(anim2.start).not.toBeCalled();
|
||||
|
||||
seq.start(cb);
|
||||
|
||||
expect(anim1.start).toBeCalled();
|
||||
expect(anim2.start).not.toBeCalled();
|
||||
expect(cb).not.toBeCalled();
|
||||
|
||||
anim1.start.mock.calls[0][0]({finished: true});
|
||||
|
||||
expect(anim2.start).toBeCalled();
|
||||
expect(cb).not.toBeCalled();
|
||||
|
||||
anim2.start.mock.calls[0][0]({finished: true});
|
||||
expect(cb).toBeCalledWith({finished: true});
|
||||
});
|
||||
|
||||
it('supports interrupting sequence', () => {
|
||||
var anim1 = {start: jest.genMockFunction()};
|
||||
var anim2 = {start: jest.genMockFunction()};
|
||||
var cb = jest.genMockFunction();
|
||||
|
||||
Animated.sequence([anim1, anim2]).start(cb);
|
||||
|
||||
anim1.start.mock.calls[0][0]({finished: false});
|
||||
|
||||
expect(anim1.start).toBeCalled();
|
||||
expect(anim2.start).not.toBeCalled();
|
||||
expect(cb).toBeCalledWith({finished: false});
|
||||
});
|
||||
|
||||
it('supports stopping sequence', () => {
|
||||
var anim1 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var anim2 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var cb = jest.genMockFunction();
|
||||
|
||||
var seq = Animated.sequence([anim1, anim2]);
|
||||
seq.start(cb);
|
||||
seq.stop();
|
||||
|
||||
expect(anim1.stop).toBeCalled();
|
||||
expect(anim2.stop).not.toBeCalled();
|
||||
expect(cb).not.toBeCalled();
|
||||
|
||||
anim1.start.mock.calls[0][0]({finished: false});
|
||||
|
||||
expect(cb).toBeCalledWith({finished: false});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Animated Parallel', () => {
|
||||
|
||||
it('works with an empty parallel', () => {
|
||||
var cb = jest.genMockFunction();
|
||||
Animated.parallel([]).start(cb);
|
||||
expect(cb).toBeCalledWith({finished: true});
|
||||
});
|
||||
|
||||
|
||||
it('parellelizes well', () => {
|
||||
var anim1 = {start: jest.genMockFunction()};
|
||||
var anim2 = {start: jest.genMockFunction()};
|
||||
var cb = jest.genMockFunction();
|
||||
|
||||
var par = Animated.parallel([anim1, anim2]);
|
||||
|
||||
expect(anim1.start).not.toBeCalled();
|
||||
expect(anim2.start).not.toBeCalled();
|
||||
|
||||
par.start(cb);
|
||||
|
||||
expect(anim1.start).toBeCalled();
|
||||
expect(anim2.start).toBeCalled();
|
||||
expect(cb).not.toBeCalled();
|
||||
|
||||
anim1.start.mock.calls[0][0]({finished: true});
|
||||
expect(cb).not.toBeCalled();
|
||||
|
||||
anim2.start.mock.calls[0][0]({finished: true});
|
||||
expect(cb).toBeCalledWith({finished: true});
|
||||
});
|
||||
|
||||
it('supports stopping parallel', () => {
|
||||
var anim1 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var anim2 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var cb = jest.genMockFunction();
|
||||
|
||||
var seq = Animated.parallel([anim1, anim2]);
|
||||
seq.start(cb);
|
||||
seq.stop();
|
||||
|
||||
expect(anim1.stop).toBeCalled();
|
||||
expect(anim2.stop).toBeCalled();
|
||||
expect(cb).not.toBeCalled();
|
||||
|
||||
anim1.start.mock.calls[0][0]({finished: false});
|
||||
expect(cb).not.toBeCalled();
|
||||
|
||||
anim2.start.mock.calls[0][0]({finished: false});
|
||||
expect(cb).toBeCalledWith({finished: false});
|
||||
});
|
||||
|
||||
|
||||
it('does not call stop more than once when stopping', () => {
|
||||
var anim1 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var anim2 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var anim3 = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var cb = jest.genMockFunction();
|
||||
|
||||
var seq = Animated.parallel([anim1, anim2, anim3]);
|
||||
seq.start(cb);
|
||||
|
||||
anim1.start.mock.calls[0][0]({finished: false});
|
||||
|
||||
expect(anim1.stop.mock.calls.length).toBe(0);
|
||||
expect(anim2.stop.mock.calls.length).toBe(1);
|
||||
expect(anim3.stop.mock.calls.length).toBe(1);
|
||||
|
||||
anim2.start.mock.calls[0][0]({finished: false});
|
||||
|
||||
expect(anim1.stop.mock.calls.length).toBe(0);
|
||||
expect(anim2.stop.mock.calls.length).toBe(1);
|
||||
expect(anim3.stop.mock.calls.length).toBe(1);
|
||||
|
||||
anim3.start.mock.calls[0][0]({finished: false});
|
||||
|
||||
expect(anim1.stop.mock.calls.length).toBe(0);
|
||||
expect(anim2.stop.mock.calls.length).toBe(1);
|
||||
expect(anim3.stop.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Animated delays', () => {
|
||||
it('should call anim after delay in sequence', () => {
|
||||
var anim = {start: jest.genMockFunction(), stop: jest.genMockFunction()};
|
||||
var cb = jest.genMockFunction();
|
||||
Animated.sequence([
|
||||
Animated.delay(1000),
|
||||
anim,
|
||||
]).start(cb);
|
||||
jest.runAllTimers();
|
||||
expect(anim.start.mock.calls.length).toBe(1);
|
||||
expect(cb).not.toBeCalled();
|
||||
anim.start.mock.calls[0][0]({finished: true});
|
||||
expect(cb).toBeCalledWith({finished: true});
|
||||
});
|
||||
it('should run stagger to end', () => {
|
||||
var cb = jest.genMockFunction();
|
||||
Animated.stagger(1000, [
|
||||
Animated.delay(1000),
|
||||
Animated.delay(1000),
|
||||
Animated.delay(1000),
|
||||
]).start(cb);
|
||||
jest.runAllTimers();
|
||||
expect(cb).toBeCalledWith({finished: true});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Animated Events', () => {
|
||||
it('should map events', () => {
|
||||
var value = new Animated.Value(0);
|
||||
var handler = Animated.event(
|
||||
[null, {state: {foo: value}}],
|
||||
);
|
||||
handler({bar: 'ignoreBar'}, {state: {baz: 'ignoreBaz', foo: 42}});
|
||||
expect(value.__getValue()).toBe(42);
|
||||
});
|
||||
it('should call listeners', () => {
|
||||
var value = new Animated.Value(0);
|
||||
var listener = jest.genMockFunction();
|
||||
var handler = Animated.event(
|
||||
[{foo: value}],
|
||||
{listener},
|
||||
);
|
||||
handler({foo: 42});
|
||||
expect(value.__getValue()).toBe(42);
|
||||
expect(listener.mock.calls.length).toBe(1);
|
||||
expect(listener).toBeCalledWith({foo: 42});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Animated Tracking', () => {
|
||||
it('should track values', () => {
|
||||
var value1 = new Animated.Value(0);
|
||||
var value2 = new Animated.Value(0);
|
||||
Animated.timing(value2, {
|
||||
toValue: value1,
|
||||
duration: 0,
|
||||
}).start();
|
||||
value1.setValue(42);
|
||||
expect(value2.__getValue()).toBe(42);
|
||||
value1.setValue(7);
|
||||
expect(value2.__getValue()).toBe(7);
|
||||
});
|
||||
|
||||
it('should track interpolated values', () => {
|
||||
var value1 = new Animated.Value(0);
|
||||
var value2 = new Animated.Value(0);
|
||||
Animated.timing(value2, {
|
||||
toValue: value1.interpolate({
|
||||
inputRange: [0, 2],
|
||||
outputRange: [0, 1]
|
||||
}),
|
||||
duration: 0,
|
||||
}).start();
|
||||
value1.setValue(42);
|
||||
expect(value2.__getValue()).toBe(42 / 2);
|
||||
});
|
||||
|
||||
it('should stop tracking when animated', () => {
|
||||
var value1 = new Animated.Value(0);
|
||||
var value2 = new Animated.Value(0);
|
||||
Animated.timing(value2, {
|
||||
toValue: value1,
|
||||
duration: 0,
|
||||
}).start();
|
||||
value1.setValue(42);
|
||||
expect(value2.__getValue()).toBe(42);
|
||||
Animated.timing(value2, {
|
||||
toValue: 7,
|
||||
duration: 0,
|
||||
}).start();
|
||||
value1.setValue(1492);
|
||||
expect(value2.__getValue()).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Animated Vectors', () => {
|
||||
it('should animate vectors', () => {
|
||||
var vec = new Animated.ValueXY();
|
||||
|
||||
var callback = jest.genMockFunction();
|
||||
|
||||
var node = new Animated.__PropsOnlyForTests({
|
||||
style: {
|
||||
opacity: vec.x.interpolate({
|
||||
inputRange: [0, 42],
|
||||
outputRange: [0.2, 0.8],
|
||||
}),
|
||||
transform: vec.getTranslateTransform(),
|
||||
...vec.getLayout(),
|
||||
}
|
||||
}, callback);
|
||||
|
||||
expect(node.__getValue()).toEqual({
|
||||
style: {
|
||||
opacity: 0.2,
|
||||
transform: [
|
||||
{translateX: 0},
|
||||
{translateY: 0},
|
||||
],
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
});
|
||||
|
||||
vec.setValue({x: 42, y: 1492});
|
||||
|
||||
expect(callback.mock.calls.length).toBe(2); // once each for x, y
|
||||
|
||||
expect(node.__getValue()).toEqual({
|
||||
style: {
|
||||
opacity: 0.8,
|
||||
transform: [
|
||||
{translateX: 42},
|
||||
{translateY: 1492},
|
||||
],
|
||||
left: 42,
|
||||
top: 1492,
|
||||
},
|
||||
});
|
||||
|
||||
node.detach();
|
||||
|
||||
vec.setValue({x: 1, y: 1});
|
||||
expect(callback.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should track vectors', () => {
|
||||
var value1 = new Animated.ValueXY();
|
||||
var value2 = new Animated.ValueXY();
|
||||
Animated.timing(value2, {
|
||||
toValue: value1,
|
||||
duration: 0,
|
||||
}).start();
|
||||
value1.setValue({x: 42, y: 1492});
|
||||
expect(value2.__getValue()).toEqual({x: 42, y: 1492});
|
||||
|
||||
// Make sure tracking keeps working (see stopTogether in ParallelConfig used
|
||||
// by maybeVectorAnim).
|
||||
value1.setValue({x: 3, y: 4});
|
||||
expect(value2.__getValue()).toEqual({x: 3, y: 4});
|
||||
});
|
||||
|
||||
it('should track with springs', () => {
|
||||
var value1 = new Animated.ValueXY();
|
||||
var value2 = new Animated.ValueXY();
|
||||
Animated.spring(value2, {
|
||||
toValue: value1,
|
||||
tension: 3000, // faster spring for faster test
|
||||
friction: 60,
|
||||
}).start();
|
||||
value1.setValue({x: 1, y: 1});
|
||||
jest.runAllTimers();
|
||||
expect(Math.round(value2.__getValue().x)).toEqual(1);
|
||||
expect(Math.round(value2.__getValue().y)).toEqual(1);
|
||||
value1.setValue({x: 2, y: 2});
|
||||
jest.runAllTimers();
|
||||
expect(Math.round(value2.__getValue().x)).toEqual(2);
|
||||
expect(Math.round(value2.__getValue().y)).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Animated Listeners', () => {
|
||||
it('should get updates', () => {
|
||||
var value1 = new Animated.Value(0);
|
||||
var listener = jest.genMockFunction();
|
||||
var id = value1.addListener(listener);
|
||||
value1.setValue(42);
|
||||
expect(listener.mock.calls.length).toBe(1);
|
||||
expect(listener).toBeCalledWith({value: 42});
|
||||
expect(value1.__getValue()).toBe(42);
|
||||
value1.setValue(7);
|
||||
expect(listener.mock.calls.length).toBe(2);
|
||||
expect(listener).toBeCalledWith({value: 7});
|
||||
expect(value1.__getValue()).toBe(7);
|
||||
value1.removeListener(id);
|
||||
value1.setValue(1492);
|
||||
expect(listener.mock.calls.length).toBe(2);
|
||||
expect(value1.__getValue()).toBe(1492);
|
||||
});
|
||||
|
||||
it('should removeAll', () => {
|
||||
var value1 = new Animated.Value(0);
|
||||
var listener = jest.genMockFunction();
|
||||
[1,2,3,4].forEach(() => value1.addListener(listener));
|
||||
value1.setValue(42);
|
||||
expect(listener.mock.calls.length).toBe(4);
|
||||
expect(listener).toBeCalledWith({value: 42});
|
||||
value1.removeAllListeners();
|
||||
value1.setValue(7);
|
||||
expect(listener.mock.calls.length).toBe(4);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest.dontMock('Easing');
|
||||
|
||||
var Easing = require('Easing');
|
||||
describe('Easing', () => {
|
||||
it('should work with linear', () => {
|
||||
var easing = Easing.linear;
|
||||
|
||||
expect(easing(0)).toBe(0);
|
||||
expect(easing(0.5)).toBe(0.5);
|
||||
expect(easing(0.8)).toBe(0.8);
|
||||
expect(easing(1)).toBe(1);
|
||||
});
|
||||
|
||||
it('should work with ease in linear', () => {
|
||||
var easing = Easing.in(Easing.linear);
|
||||
expect(easing(0)).toBe(0);
|
||||
expect(easing(0.5)).toBe(0.5);
|
||||
expect(easing(0.8)).toBe(0.8);
|
||||
expect(easing(1)).toBe(1);
|
||||
});
|
||||
|
||||
it('should work with easy out linear', () => {
|
||||
var easing = Easing.out(Easing.linear);
|
||||
expect(easing(0)).toBe(0);
|
||||
expect(easing(0.5)).toBe(0.5);
|
||||
expect(easing(0.6)).toBe(0.6);
|
||||
expect(easing(1)).toBe(1);
|
||||
});
|
||||
|
||||
it('should work with ease in quad', () => {
|
||||
function easeInQuad(t) {
|
||||
return t * t;
|
||||
}
|
||||
var easing = Easing.in(Easing.quad);
|
||||
for (var t = -0.5; t < 1.5; t += 0.1) {
|
||||
expect(easing(t)).toBe(easeInQuad(t));
|
||||
}
|
||||
});
|
||||
|
||||
it('should work with ease out quad', () => {
|
||||
function easeOutQuad(t) {
|
||||
return -t * (t - 2);
|
||||
}
|
||||
var easing = Easing.out(Easing.quad);
|
||||
for (var t = 0; t <= 1; t += 0.1) {
|
||||
expect(easing(1)).toBe(easeOutQuad(1));
|
||||
}
|
||||
});
|
||||
|
||||
it('should work with ease in-out quad', () => {
|
||||
function easeInOutQuad(t) {
|
||||
t = t * 2;
|
||||
if (t < 1) {
|
||||
return 0.5 * t * t;
|
||||
}
|
||||
return -((t - 1) * (t - 3) - 1) / 2;
|
||||
}
|
||||
var easing = Easing.inOut(Easing.quad);
|
||||
for (var t = -0.5; t < 1.5; t += 0.1) {
|
||||
expect(easing(t)).toBeCloseTo(easeInOutQuad(t), 4);
|
||||
}
|
||||
});
|
||||
|
||||
function sampleEasingFunction(easing) {
|
||||
var DURATION = 300;
|
||||
var tickCount = Math.round(DURATION * 60 / 1000);
|
||||
var samples = [];
|
||||
for (var i = 0; i <= tickCount; i++) {
|
||||
samples.push(easing(i / tickCount));
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
var Samples = {
|
||||
in_quad: [0,0.0030864197530864196,0.012345679012345678,0.027777777777777776,0.04938271604938271,0.0771604938271605,0.1111111111111111,0.15123456790123457,0.19753086419753085,0.25,0.308641975308642,0.37345679012345684,0.4444444444444444,0.5216049382716049,0.6049382716049383,0.6944444444444445,0.7901234567901234,0.8919753086419753,1],
|
||||
out_quad: [0,0.10802469135802469,0.20987654320987653,0.3055555555555555,0.3950617283950617,0.47839506172839513,0.5555555555555556,0.6265432098765432,0.691358024691358,0.75,0.8024691358024691,0.8487654320987654,0.888888888888889,0.9228395061728394,0.9506172839506174,0.9722222222222221,0.9876543209876543,0.9969135802469136,1],
|
||||
inOut_quad: [0,0.006172839506172839,0.024691358024691357,0.05555555555555555,0.09876543209876543,0.154320987654321,0.2222222222222222,0.30246913580246915,0.3950617283950617,0.5,0.6049382716049383,0.697530864197531,0.7777777777777777,0.845679012345679,0.9012345679012346,0.9444444444444444,0.9753086419753086,0.9938271604938271,1],
|
||||
in_cubic: [0,0.00017146776406035664,0.0013717421124828531,0.004629629629629629,0.010973936899862825,0.021433470507544586,0.037037037037037035,0.05881344307270234,0.0877914951989026,0.125,0.1714677640603567,0.22822359396433475,0.2962962962962963,0.37671467764060357,0.4705075445816187,0.5787037037037038,0.7023319615912208,0.8424211248285322,1],
|
||||
out_cubic: [0,0.15757887517146785,0.2976680384087792,0.42129629629629617,0.5294924554183813,0.6232853223593964,0.7037037037037036,0.7717764060356652,0.8285322359396433,0.875,0.9122085048010974,0.9411865569272977,0.9629629629629629,0.9785665294924554,0.9890260631001372,0.9953703703703703,0.9986282578875172,0.9998285322359396,1],
|
||||
inOut_cubic: [0,0.0006858710562414266,0.0054869684499314125,0.018518518518518517,0.0438957475994513,0.08573388203017834,0.14814814814814814,0.23525377229080935,0.3511659807956104,0.5,0.6488340192043895,0.7647462277091908,0.8518518518518519,0.9142661179698217,0.9561042524005487,0.9814814814814815,0.9945130315500685,0.9993141289437586,1],
|
||||
in_sin: [0,0.003805301908254455,0.01519224698779198,0.03407417371093169,0.06030737921409157,0.09369221296335006,0.1339745962155613,0.1808479557110082,0.233955556881022,0.2928932188134524,0.35721239031346064,0.42642356364895384,0.4999999999999999,0.5773817382593005,0.6579798566743311,0.7411809548974793,0.8263518223330696,0.9128442572523416,0.9999999999999999],
|
||||
out_sin: [0,0.08715574274765817,0.17364817766693033,0.25881904510252074,0.3420201433256687,0.42261826174069944,0.49999999999999994,0.573576436351046,0.6427876096865393,0.7071067811865475,0.766044443118978,0.8191520442889918,0.8660254037844386,0.9063077870366499,0.9396926207859083,0.9659258262890683,0.984807753012208,0.9961946980917455,1],
|
||||
inOut_sin: [0,0.00759612349389599,0.030153689607045786,0.06698729810778065,0.116977778440511,0.17860619515673032,0.24999999999999994,0.32898992833716556,0.4131759111665348,0.49999999999999994,0.5868240888334652,0.6710100716628343,0.7499999999999999,0.8213938048432696,0.883022221559489,0.9330127018922194,0.9698463103929542,0.9924038765061041,1],
|
||||
in_exp: [0,0.0014352875901128893,0.002109491677524035,0.0031003926796253885,0.004556754060844206,0.006697218616039631,0.009843133202303688,0.014466792379488908,0.021262343752724643,0.03125,0.045929202883612456,0.06750373368076916,0.09921256574801243,0.1458161299470146,0.2143109957132682,0.31498026247371835,0.46293735614364506,0.6803950000871883,1],
|
||||
out_exp: [0,0.31960499991281155,0.5370626438563548,0.6850197375262816,0.7856890042867318,0.8541838700529854,0.9007874342519875,0.9324962663192309,0.9540707971163875,0.96875,0.9787376562472754,0.9855332076205111,0.9901568667976963,0.9933027813839603,0.9954432459391558,0.9968996073203746,0.9978905083224759,0.9985647124098871,1],
|
||||
inOut_exp: [0,0.0010547458387620175,0.002278377030422103,0.004921566601151844,0.010631171876362321,0.022964601441806228,0.049606282874006216,0.1071554978566341,0.23146867807182253,0.5,0.7685313219281775,0.892844502143366,0.9503937171259937,0.9770353985581938,0.9893688281236377,0.9950784333988482,0.9977216229695779,0.998945254161238,1],
|
||||
in_circle: [0,0.0015444024660317135,0.006192010000093506,0.013986702816730645,0.025003956956430873,0.03935464078941209,0.057190958417936644,0.07871533601238889,0.10419358352238339,0.1339745962155614,0.1685205807169019,0.20845517506805522,0.2546440075000701,0.3083389112228482,0.37146063894529113,0.4472292016074334,0.5418771527091488,0.6713289009389102,1],
|
||||
out_circle: [0,0.3286710990610898,0.45812284729085123,0.5527707983925666,0.6285393610547089,0.6916610887771518,0.7453559924999298,0.7915448249319448,0.8314794192830981,0.8660254037844386,0.8958064164776166,0.9212846639876111,0.9428090415820634,0.9606453592105879,0.9749960430435691,0.9860132971832694,0.9938079899999065,0.9984555975339683,1],
|
||||
inOut_circle: [0,0.003096005000046753,0.012501978478215436,0.028595479208968322,0.052096791761191696,0.08426029035845095,0.12732200375003505,0.18573031947264557,0.2709385763545744,0.5,0.7290614236454256,0.8142696805273546,0.8726779962499649,0.915739709641549,0.9479032082388084,0.9714045207910317,0.9874980215217846,0.9969039949999532,1],
|
||||
in_back_: [0,-0.004788556241426612,-0.017301289437585736,-0.0347587962962963,-0.05438167352537723,-0.07339051783264748,-0.08900592592592595,-0.09844849451303156,-0.0989388203017833,-0.08769750000000004,-0.06194513031550073,-0.018902307956104283,0.044210370370370254,0.13017230795610413,0.2417629080932785,0.3817615740740742,0.5529477091906719,0.7581007167352535,0.9999999999999998],
|
||||
out_back_: [2.220446049250313e-16,0.24189928326474652,0.44705229080932807,0.6182384259259258,0.7582370919067215,0.8698276920438959,0.9557896296296297,1.0189023079561044,1.0619451303155008,1.0876975,1.0989388203017834,1.0984484945130315,1.089005925925926,1.0733905178326475,1.0543816735253773,1.0347587962962963,1.0173012894375857,1.0047885562414267,1],
|
||||
};
|
||||
|
||||
Object.keys(Samples).forEach(function(type) {
|
||||
it('should ease ' + type, function() {
|
||||
var [modeName, easingName, isFunction] = type.split('_');
|
||||
var easing = Easing[easingName];
|
||||
if (isFunction !== undefined) {
|
||||
easing = easing();
|
||||
}
|
||||
var computed = sampleEasingFunction(Easing[modeName](easing));
|
||||
var samples = Samples[type];
|
||||
|
||||
computed.forEach((value, key) => {
|
||||
expect(value).toBeCloseTo(samples[key], 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,256 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest
|
||||
.dontMock('Interpolation')
|
||||
.dontMock('Easing');
|
||||
|
||||
var Interpolation = require('Interpolation');
|
||||
var Easing = require('Easing');
|
||||
|
||||
describe('Interpolation', () => {
|
||||
it('should work with defaults', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
});
|
||||
|
||||
expect(interpolation(0)).toBe(0);
|
||||
expect(interpolation(0.5)).toBe(0.5);
|
||||
expect(interpolation(0.8)).toBe(0.8);
|
||||
expect(interpolation(1)).toBe(1);
|
||||
});
|
||||
|
||||
it('should work with output range', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [100, 200],
|
||||
});
|
||||
|
||||
expect(interpolation(0)).toBe(100);
|
||||
expect(interpolation(0.5)).toBe(150);
|
||||
expect(interpolation(0.8)).toBe(180);
|
||||
expect(interpolation(1)).toBe(200);
|
||||
});
|
||||
|
||||
it('should work with input range', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [100, 200],
|
||||
outputRange: [0, 1],
|
||||
});
|
||||
|
||||
expect(interpolation(100)).toBe(0);
|
||||
expect(interpolation(150)).toBe(0.5);
|
||||
expect(interpolation(180)).toBe(0.8);
|
||||
expect(interpolation(200)).toBe(1);
|
||||
});
|
||||
|
||||
it('should throw for non monotonic input ranges', () => {
|
||||
expect(() => Interpolation.create({
|
||||
inputRange: [0, 2, 1],
|
||||
outputRange: [0, 1, 2],
|
||||
})).toThrow();
|
||||
|
||||
expect(() => Interpolation.create({
|
||||
inputRange: [0, 1, 2],
|
||||
outputRange: [0, 3, 1],
|
||||
})).not.toThrow();
|
||||
});
|
||||
|
||||
it('should work with empty input range', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 10, 10],
|
||||
outputRange: [1, 2, 3],
|
||||
extrapolate: 'extend',
|
||||
});
|
||||
|
||||
expect(interpolation(0)).toBe(1);
|
||||
expect(interpolation(5)).toBe(1.5);
|
||||
expect(interpolation(10)).toBe(2);
|
||||
expect(interpolation(10.1)).toBe(3);
|
||||
expect(interpolation(15)).toBe(3);
|
||||
});
|
||||
|
||||
it('should work with empty output range', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [1, 2, 3],
|
||||
outputRange: [0, 10, 10],
|
||||
extrapolate: 'extend',
|
||||
});
|
||||
|
||||
expect(interpolation(0)).toBe(-10);
|
||||
expect(interpolation(1.5)).toBe(5);
|
||||
expect(interpolation(2)).toBe(10);
|
||||
expect(interpolation(2.5)).toBe(10);
|
||||
expect(interpolation(3)).toBe(10);
|
||||
expect(interpolation(4)).toBe(10);
|
||||
});
|
||||
|
||||
it('should work with easing', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
easing: Easing.quad,
|
||||
});
|
||||
|
||||
expect(interpolation(0)).toBe(0);
|
||||
expect(interpolation(0.5)).toBe(0.25);
|
||||
expect(interpolation(0.9)).toBe(0.81);
|
||||
expect(interpolation(1)).toBe(1);
|
||||
});
|
||||
|
||||
it('should work with extrapolate', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
extrapolate: 'extend',
|
||||
easing: Easing.quad,
|
||||
});
|
||||
|
||||
expect(interpolation(-2)).toBe(4);
|
||||
expect(interpolation(2)).toBe(4);
|
||||
|
||||
interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
extrapolate: 'clamp',
|
||||
easing: Easing.quad,
|
||||
});
|
||||
|
||||
expect(interpolation(-2)).toBe(0);
|
||||
expect(interpolation(2)).toBe(1);
|
||||
|
||||
interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
extrapolate: 'identity',
|
||||
easing: Easing.quad,
|
||||
});
|
||||
|
||||
expect(interpolation(-2)).toBe(-2);
|
||||
expect(interpolation(2)).toBe(2);
|
||||
});
|
||||
|
||||
it('should work with keyframes with extrapolate', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 10, 100, 1000],
|
||||
outputRange: [0, 5, 50, 500],
|
||||
extrapolate: true,
|
||||
});
|
||||
|
||||
expect(interpolation(-5)).toBe(-2.5);
|
||||
expect(interpolation(0)).toBe(0);
|
||||
expect(interpolation(5)).toBe(2.5);
|
||||
expect(interpolation(10)).toBe(5);
|
||||
expect(interpolation(50)).toBe(25);
|
||||
expect(interpolation(100)).toBe(50);
|
||||
expect(interpolation(500)).toBe(250);
|
||||
expect(interpolation(1000)).toBe(500);
|
||||
expect(interpolation(2000)).toBe(1000);
|
||||
});
|
||||
|
||||
it('should work with keyframes without extrapolate', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1, 2],
|
||||
outputRange: [0.2, 1, 0.2],
|
||||
extrapolate: 'clamp',
|
||||
});
|
||||
|
||||
expect(interpolation(5)).toBeCloseTo(0.2);
|
||||
});
|
||||
|
||||
it('should throw for an infinite input range', () => {
|
||||
expect(() => Interpolation.create({
|
||||
inputRange: [-Infinity, Infinity],
|
||||
outputRange: [0, 1],
|
||||
})).toThrow();
|
||||
|
||||
expect(() => Interpolation.create({
|
||||
inputRange: [-Infinity, 0, Infinity],
|
||||
outputRange: [1, 2, 3],
|
||||
})).not.toThrow();
|
||||
});
|
||||
|
||||
it('should work with negative infinite', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [-Infinity, 0],
|
||||
outputRange: [-Infinity, 0],
|
||||
easing: Easing.quad,
|
||||
extrapolate: 'identity',
|
||||
});
|
||||
|
||||
expect(interpolation(-Infinity)).toBe(-Infinity);
|
||||
expect(interpolation(-100)).toBeCloseTo(-10000);
|
||||
expect(interpolation(-10)).toBeCloseTo(-100);
|
||||
expect(interpolation(0)).toBeCloseTo(0);
|
||||
expect(interpolation(1)).toBeCloseTo(1);
|
||||
expect(interpolation(100)).toBeCloseTo(100);
|
||||
});
|
||||
|
||||
it('should work with positive infinite', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [5, Infinity],
|
||||
outputRange: [5, Infinity],
|
||||
easing: Easing.quad,
|
||||
extrapolate: 'identity',
|
||||
});
|
||||
|
||||
expect(interpolation(-100)).toBeCloseTo(-100);
|
||||
expect(interpolation(-10)).toBeCloseTo(-10);
|
||||
expect(interpolation(0)).toBeCloseTo(0);
|
||||
expect(interpolation(5)).toBeCloseTo(5);
|
||||
expect(interpolation(6)).toBeCloseTo(5 + 1);
|
||||
expect(interpolation(10)).toBeCloseTo(5 + 25);
|
||||
expect(interpolation(100)).toBeCloseTo(5 + (95 * 95));
|
||||
expect(interpolation(Infinity)).toBe(Infinity);
|
||||
});
|
||||
|
||||
it('should work with output ranges as string', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)'],
|
||||
});
|
||||
|
||||
expect(interpolation(0)).toBe('rgba(0, 100, 200, 0)');
|
||||
expect(interpolation(0.5)).toBe('rgba(25, 125, 225, 0.25)');
|
||||
expect(interpolation(1)).toBe('rgba(50, 150, 250, 0.5)');
|
||||
});
|
||||
|
||||
it('should work with negative and decimal values in string ranges', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['-100.5deg', '100deg'],
|
||||
});
|
||||
|
||||
expect(interpolation(0)).toBe('-100.5deg');
|
||||
expect(interpolation(0.5)).toBe('-0.25deg');
|
||||
expect(interpolation(1)).toBe('100deg');
|
||||
});
|
||||
|
||||
it('should crash when chaining an interpolation that returns a string', () => {
|
||||
var interpolation = Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
});
|
||||
expect(() => { interpolation('45rad'); }).toThrow();
|
||||
});
|
||||
|
||||
it('should crash when defining output range with different pattern', () => {
|
||||
expect(() => Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['rgba(0, 100, 200, 0)', 'rgb(50, 150, 250)'],
|
||||
})).toThrow();
|
||||
|
||||
expect(() => Interpolation.create({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['20deg', '30rad'],
|
||||
})).toThrow();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "react-animated",
|
||||
"description": "Animated provides powerful mechanisms for animating your React views",
|
||||
"version": "0.1.0",
|
||||
"keywords": [
|
||||
"react",
|
||||
"animated",
|
||||
"animation"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"main": "Animated.js",
|
||||
"readmeFilename": "README.md"
|
||||
}
|
|
@ -7,11 +7,18 @@
|
|||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule AnimationExperimental
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var RCTAnimationManager = require('NativeModules').AnimationExperimentalManager;
|
||||
if (!RCTAnimationManager) {
|
||||
// AnimationExperimental isn't available internally - this is a temporary
|
||||
// workaround to enable its availability to be determined at runtime.
|
||||
// For Flow let's pretend like we always export AnimationExperimental
|
||||
// so all our users don't need to do null checks
|
||||
module.exports = null;
|
||||
} else {
|
||||
|
||||
var React = require('React');
|
||||
var AnimationUtils = require('AnimationUtils');
|
||||
|
||||
|
@ -88,3 +95,5 @@ if (__DEV__) {
|
|||
}
|
||||
|
||||
module.exports = AnimationExperimental;
|
||||
|
||||
}
|
||||
|
|
|
@ -87,37 +87,42 @@ function create(duration: number, type, creationProp): Config {
|
|||
};
|
||||
}
|
||||
|
||||
var Presets = {
|
||||
easeInEaseOut: create(
|
||||
300, Types.easeInEaseOut, Properties.opacity
|
||||
),
|
||||
linear: create(
|
||||
500, Types.linear, Properties.opacity
|
||||
),
|
||||
spring: {
|
||||
duration: 700,
|
||||
create: {
|
||||
type: Types.linear,
|
||||
property: Properties.opacity,
|
||||
},
|
||||
update: {
|
||||
type: Types.spring,
|
||||
springDamping: 0.4,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var LayoutAnimation = {
|
||||
configureNext,
|
||||
create,
|
||||
Types,
|
||||
Properties,
|
||||
configChecker: configChecker,
|
||||
Presets: {
|
||||
easeInEaseOut: create(
|
||||
300, Types.easeInEaseOut, Properties.opacity
|
||||
),
|
||||
linear: create(
|
||||
500, Types.linear, Properties.opacity
|
||||
),
|
||||
spring: {
|
||||
duration: 700,
|
||||
create: {
|
||||
type: Types.linear,
|
||||
property: Properties.opacity,
|
||||
},
|
||||
update: {
|
||||
type: Types.spring,
|
||||
springDamping: 0.4,
|
||||
},
|
||||
},
|
||||
}
|
||||
Presets,
|
||||
easeInEaseOut: configureNext.bind(
|
||||
null, Presets.easeInEaseOut
|
||||
),
|
||||
linear: configureNext.bind(
|
||||
null, Presets.linear
|
||||
),
|
||||
spring: configureNext.bind(
|
||||
null, Presets.spring
|
||||
),
|
||||
};
|
||||
|
||||
for (var key in LayoutAnimation.Presets) {
|
||||
LayoutAnimation[key] = LayoutAnimation.configureNext.bind(
|
||||
null, LayoutAnimation.Presets[key]
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = LayoutAnimation;
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* https://github.com/arian/cubic-bezier
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2013 Arian Stolwijk
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* @providesModule bezier
|
||||
* @nolint
|
||||
*/
|
||||
|
||||
module.exports = function(x1, y1, x2, y2, epsilon){
|
||||
|
||||
var curveX = function(t){
|
||||
var v = 1 - t;
|
||||
return 3 * v * v * t * x1 + 3 * v * t * t * x2 + t * t * t;
|
||||
};
|
||||
|
||||
var curveY = function(t){
|
||||
var v = 1 - t;
|
||||
return 3 * v * v * t * y1 + 3 * v * t * t * y2 + t * t * t;
|
||||
};
|
||||
|
||||
var derivativeCurveX = function(t){
|
||||
var v = 1 - t;
|
||||
return 3 * (2 * (t - 1) * t + v * v) * x1 + 3 * (- t * t * t + 2 * v * t) * x2;
|
||||
};
|
||||
|
||||
return function(t){
|
||||
|
||||
var x = t, t0, t1, t2, x2, d2, i;
|
||||
|
||||
// First try a few iterations of Newton's method -- normally very fast.
|
||||
for (t2 = x, i = 0; i < 8; i++){
|
||||
x2 = curveX(t2) - x;
|
||||
if (Math.abs(x2) < epsilon) return curveY(t2);
|
||||
d2 = derivativeCurveX(t2);
|
||||
if (Math.abs(d2) < 1e-6) break;
|
||||
t2 = t2 - x2 / d2;
|
||||
}
|
||||
|
||||
t0 = 0, t1 = 1, t2 = x;
|
||||
|
||||
if (t2 < t0) return curveY(t0);
|
||||
if (t2 > t1) return curveY(t1);
|
||||
|
||||
// Fallback to the bisection method for reliability.
|
||||
while (t0 < t1){
|
||||
x2 = curveX(t2);
|
||||
if (Math.abs(x2 - x) < epsilon) return curveY(t2);
|
||||
if (x > x2) t0 = t2;
|
||||
else t1 = t2;
|
||||
t2 = (t1 - t0) * .5 + t0;
|
||||
}
|
||||
|
||||
// Failure
|
||||
return curveY(t2);
|
||||
|
||||
};
|
||||
|
||||
};
|
|
@ -17,6 +17,7 @@ var PointPropType = require('PointPropType');
|
|||
var RCTScrollView = require('NativeModules').UIManager.RCTScrollView;
|
||||
var RCTScrollViewConsts = RCTScrollView.Constants;
|
||||
var React = require('React');
|
||||
var ReactChildren = require('ReactChildren');
|
||||
var ReactNativeViewAttributes = require('ReactNativeViewAttributes');
|
||||
var RCTUIManager = require('NativeModules').UIManager;
|
||||
var ScrollResponder = require('ScrollResponder');
|
||||
|
@ -277,12 +278,24 @@ var ScrollView = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
var children = this.props.children;
|
||||
if (this.props.stickyHeaderIndices) {
|
||||
children = ReactChildren.map(children, (child) => {
|
||||
if (child) {
|
||||
return <View collapsible={false}>{child}</View>;
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var contentContainer =
|
||||
<View
|
||||
collapsible={false}
|
||||
ref={INNERVIEW}
|
||||
style={contentContainerStyle}
|
||||
removeClippedSubviews={this.props.removeClippedSubviews}>
|
||||
{this.props.children}
|
||||
{children}
|
||||
</View>;
|
||||
|
||||
var alwaysBounceHorizontal =
|
||||
|
|
|
@ -77,6 +77,12 @@ var View = React.createClass({
|
|||
},
|
||||
|
||||
propTypes: {
|
||||
/**
|
||||
* When false, indicates that the view should not be collapsed, even if it is
|
||||
* layout-only. Defaults to true.
|
||||
*/
|
||||
collapsible: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* When true, indicates that the view is an accessibility element. By default,
|
||||
* all the touchable elements are accessible.
|
||||
|
|
|
@ -43,6 +43,12 @@ var WebView = React.createClass({
|
|||
startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load
|
||||
style: View.propTypes.style,
|
||||
javaScriptEnabledAndroid: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Sets the JS to be injected when the webpage loads.
|
||||
*/
|
||||
injectedJavaScript: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Sets the user-agent for this WebView. The user-agent can also be set in native through
|
||||
* WebViewConfig, but this can and will overwrite that config.
|
||||
|
@ -96,6 +102,7 @@ var WebView = React.createClass({
|
|||
key="webViewKey"
|
||||
style={webViewStyles}
|
||||
url={this.props.url}
|
||||
injectedJavaScript={this.props.injectedJavaScript}
|
||||
userAgent={this.props.userAgent}
|
||||
javaScriptEnabledAndroid={this.props.javaScriptEnabledAndroid}
|
||||
contentInset={this.props.contentInset}
|
||||
|
@ -176,8 +183,9 @@ var WebView = React.createClass({
|
|||
|
||||
var RCTWebView = createReactNativeComponentClass({
|
||||
validAttributes: merge(ReactNativeViewAttributes.UIView, {
|
||||
url: true,
|
||||
injectedJavaScript: true,
|
||||
javaScriptEnabledAndroid: true,
|
||||
url: true,
|
||||
userAgent: true,
|
||||
}),
|
||||
uiViewClassName: 'RCTWebView',
|
||||
|
|
|
@ -98,9 +98,9 @@ var WebView = React.createClass({
|
|||
*/
|
||||
javaScriptEnabledAndroid: PropTypes.bool,
|
||||
/**
|
||||
* Used for iOS only, sets the JS to be injected when the webpage loads.
|
||||
* Sets the JS to be injected when the webpage loads.
|
||||
*/
|
||||
injectedJavascriptIOS: PropTypes.string,
|
||||
injectedJavaScript: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Used for iOS only, sets whether the webpage scales to fit the view and the
|
||||
|
@ -159,7 +159,7 @@ var WebView = React.createClass({
|
|||
style={webViewStyles}
|
||||
url={this.props.url}
|
||||
html={this.props.html}
|
||||
injectedJavascriptIOS={this.props.injectedJavascriptIOS}
|
||||
injectedJavaScript={this.props.injectedJavaScript}
|
||||
bounces={this.props.bounces}
|
||||
scrollEnabled={this.props.scrollEnabled}
|
||||
contentInset={this.props.contentInset}
|
||||
|
@ -179,15 +179,15 @@ var WebView = React.createClass({
|
|||
},
|
||||
|
||||
goForward: function() {
|
||||
RCTWebViewManager.goForward(this.getWebWiewHandle());
|
||||
RCTWebViewManager.goForward(this.getWebViewHandle());
|
||||
},
|
||||
|
||||
goBack: function() {
|
||||
RCTWebViewManager.goBack(this.getWebWiewHandle());
|
||||
RCTWebViewManager.goBack(this.getWebViewHandle());
|
||||
},
|
||||
|
||||
reload: function() {
|
||||
RCTWebViewManager.reload(this.getWebWiewHandle());
|
||||
RCTWebViewManager.reload(this.getWebViewHandle());
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -200,7 +200,7 @@ var WebView = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
getWebWiewHandle: function(): any {
|
||||
getWebViewHandle: function(): any {
|
||||
return React.findNodeHandle(this.refs[RCT_WEBVIEW_REF]);
|
||||
},
|
||||
|
||||
|
|
|
@ -29,14 +29,15 @@
|
|||
var ListViewDataSource = require('ListViewDataSource');
|
||||
var React = require('React');
|
||||
var RCTUIManager = require('NativeModules').UIManager;
|
||||
var RKScrollViewManager = require('NativeModules').ScrollViewManager;
|
||||
var ScrollView = require('ScrollView');
|
||||
var ScrollResponder = require('ScrollResponder');
|
||||
var StaticRenderer = require('StaticRenderer');
|
||||
var TimerMixin = require('react-timer-mixin');
|
||||
|
||||
var isEmpty = require('isEmpty');
|
||||
var logError = require('logError');
|
||||
var merge = require('merge');
|
||||
var isEmpty = require('isEmpty');
|
||||
|
||||
var PropTypes = React.PropTypes;
|
||||
|
||||
|
@ -173,6 +174,13 @@ var ListView = React.createClass({
|
|||
* header.
|
||||
*/
|
||||
renderSectionHeader: PropTypes.func,
|
||||
/**
|
||||
* (props) => renderable
|
||||
*
|
||||
* A function that returns the scrollable component in which the list rows
|
||||
* are rendered. Defaults to returning a ScrollView with the given props.
|
||||
*/
|
||||
renderScrollComponent: React.PropTypes.func.isRequired,
|
||||
/**
|
||||
* How early to start rendering rows before they come on screen, in
|
||||
* pixels.
|
||||
|
@ -228,6 +236,7 @@ var ListView = React.createClass({
|
|||
return {
|
||||
initialListSize: DEFAULT_INITIAL_ROWS,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
renderScrollComponent: props => <ScrollView {...props} />,
|
||||
scrollRenderAheadDistance: DEFAULT_SCROLL_RENDER_AHEAD,
|
||||
onEndReachedThreshold: DEFAULT_END_REACHED_THRESHOLD,
|
||||
};
|
||||
|
@ -365,23 +374,24 @@ var ListView = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
var props = merge(
|
||||
this.props, {
|
||||
onScroll: this._onScroll,
|
||||
stickyHeaderIndices: sectionHeaderIndices,
|
||||
}
|
||||
);
|
||||
var {
|
||||
renderScrollComponent,
|
||||
...props,
|
||||
} = this.props;
|
||||
if (!props.scrollEventThrottle) {
|
||||
props.scrollEventThrottle = DEFAULT_SCROLL_CALLBACK_THROTTLE;
|
||||
}
|
||||
return (
|
||||
<ScrollView {...props}
|
||||
ref={SCROLLVIEW_REF}>
|
||||
{header}
|
||||
{bodyComponents}
|
||||
{footer}
|
||||
</ScrollView>
|
||||
);
|
||||
Object.assign(props, {
|
||||
onScroll: this._onScroll,
|
||||
stickyHeaderIndices: sectionHeaderIndices,
|
||||
children: [header, bodyComponents, footer],
|
||||
});
|
||||
|
||||
// TODO(ide): Use function refs so we can compose with the scroll
|
||||
// component's original ref instead of clobbering it
|
||||
return React.cloneElement(renderScrollComponent(props), {
|
||||
ref: SCROLLVIEW_REF,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -400,6 +410,13 @@ var ListView = React.createClass({
|
|||
logError,
|
||||
this._setScrollVisibleHeight
|
||||
);
|
||||
|
||||
// RKScrollViewManager.calculateChildFrames not available on every platform
|
||||
RKScrollViewManager && RKScrollViewManager.calculateChildFrames &&
|
||||
RKScrollViewManager.calculateChildFrames(
|
||||
React.findNodeHandle(this.refs[SCROLLVIEW_REF]),
|
||||
this._updateChildFrames,
|
||||
);
|
||||
},
|
||||
|
||||
_setScrollContentHeight: function(left, top, width, height) {
|
||||
|
@ -412,6 +429,10 @@ var ListView = React.createClass({
|
|||
this._renderMoreRowsIfNeeded();
|
||||
},
|
||||
|
||||
_updateChildFrames: function(childFrames) {
|
||||
this._updateVisibleRows(childFrames);
|
||||
},
|
||||
|
||||
_renderMoreRowsIfNeeded: function() {
|
||||
if (this.scrollProperties.contentHeight === null ||
|
||||
this.scrollProperties.visibleHeight === null ||
|
||||
|
@ -449,11 +470,10 @@ var ListView = React.createClass({
|
|||
scrollProperties.offsetY;
|
||||
},
|
||||
|
||||
_updateVisibleRows: function(e) {
|
||||
_updateVisibleRows: function(updatedFrames) {
|
||||
if (!this.props.onChangeVisibleRows) {
|
||||
return; // No need to compute visible rows if there is no callback
|
||||
}
|
||||
var updatedFrames = e && e.nativeEvent.updatedChildFrames;
|
||||
if (updatedFrames) {
|
||||
updatedFrames.forEach((newFrame) => {
|
||||
this._childFrames[newFrame.index] = merge(newFrame);
|
||||
|
@ -522,7 +542,7 @@ var ListView = React.createClass({
|
|||
this.scrollProperties.visibleHeight = e.nativeEvent.layoutMeasurement.height;
|
||||
this.scrollProperties.contentHeight = e.nativeEvent.contentSize.height;
|
||||
this.scrollProperties.offsetY = e.nativeEvent.contentOffset.y;
|
||||
this._updateVisibleRows(e);
|
||||
this._updateVisibleRows(e.nativeEvent.updatedChildFrames);
|
||||
var nearEnd = this._getDistanceFromEnd(this.scrollProperties) < this.props.onEndReachedThreshold;
|
||||
if (nearEnd &&
|
||||
this.props.onEndReached &&
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* Copyright 2004-present Facebook. All Rights Reserved.
|
||||
*
|
||||
* @providesModule NavigationRouteStack
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var immutable = require('immutable');
|
||||
var invariant = require('invariant');
|
||||
|
||||
var {List} = immutable;
|
||||
|
||||
/**
|
||||
* The immutable routes stack.
|
||||
*/
|
||||
class RouteStack {
|
||||
_index: number;
|
||||
|
||||
_routes: List;
|
||||
|
||||
constructor(index: number, routes: List) {
|
||||
invariant(
|
||||
routes.size > 0,
|
||||
'size must not be empty'
|
||||
);
|
||||
|
||||
invariant(
|
||||
index > -1 && index <= routes.size - 1,
|
||||
'index out of bound'
|
||||
);
|
||||
|
||||
this._routes = routes;
|
||||
this._index = index;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this._routes.size;
|
||||
}
|
||||
|
||||
get index(): number {
|
||||
return this._index;
|
||||
}
|
||||
|
||||
toArray(): Array {
|
||||
return this._routes.toJS();
|
||||
}
|
||||
|
||||
get(index: number): any {
|
||||
if (index < 0 || index > this._routes.size - 1) {
|
||||
return null;
|
||||
}
|
||||
return this._routes.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new stack with the provided route appended,
|
||||
* starting at this stack size.
|
||||
*/
|
||||
push(route: any): RouteStack {
|
||||
invariant(
|
||||
route === 0 ||
|
||||
route === false ||
|
||||
!!route,
|
||||
'Must supply route to push'
|
||||
);
|
||||
|
||||
invariant(this._routes.indexOf(route) === -1, 'route must be unique');
|
||||
|
||||
// When pushing, removes the rest of the routes past the current index.
|
||||
var routes = this._routes.withMutations((list: List) => {
|
||||
list.slice(0, this._index + 1).push(route);
|
||||
});
|
||||
|
||||
return new RouteStack(routes.size - 1, routes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new stack a size ones less than this stack,
|
||||
* excluding the last index in this stack.
|
||||
*/
|
||||
pop(): RouteStack {
|
||||
invariant(this._routes.size > 1, 'shoud not pop routes stack to empty');
|
||||
|
||||
// When popping, removes the rest of the routes past the current index.
|
||||
var routes = this._routes.slice(0, this._index);
|
||||
return new RouteStack(routes.size - 1, routes);
|
||||
}
|
||||
|
||||
jumpToIndex(index: number): RouteStack {
|
||||
invariant(
|
||||
index > -1 && index < this._routes.size,
|
||||
'index out of bound'
|
||||
);
|
||||
|
||||
if (index === this._index) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return new RouteStack(index, this._routes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a route in the navigation stack.
|
||||
*
|
||||
* `index` specifies the route in the stack that should be replaced.
|
||||
* If it's negative, it counts from the back.
|
||||
*/
|
||||
replaceAtIndex(index: number, route: any): RouteStack {
|
||||
invariant(
|
||||
route === 0 ||
|
||||
route === false ||
|
||||
!!route,
|
||||
'Must supply route to replace'
|
||||
);
|
||||
|
||||
if (this.get(index) === route) {
|
||||
return this;
|
||||
}
|
||||
|
||||
invariant(this._routes.indexOf(route) === -1, 'route must be unique');
|
||||
|
||||
if (index < 0) {
|
||||
index += this._routes.size;
|
||||
}
|
||||
|
||||
invariant(
|
||||
index > -1 && index < this._routes.size,
|
||||
'index out of bound'
|
||||
);
|
||||
|
||||
var routes = this._routes.set(index, route);
|
||||
return new RouteStack(this._index, routes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The first class data structure for NavigationContext to manage the navigation
|
||||
* stack of routes.
|
||||
*/
|
||||
class NavigationRouteStack extends RouteStack {
|
||||
constructor(index: number, routes: Array) {
|
||||
// For now, `RouteStack` internally, uses an immutable `List` to keep
|
||||
// track of routes. Since using `List` is really just the implementation
|
||||
// detail, we don't want to accept `routes` as `list` from constructor
|
||||
// for developer.
|
||||
super(index, new List(routes));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NavigationRouteStack;
|
|
@ -0,0 +1,188 @@
|
|||
/**
|
||||
* Copyright (c) 2015, Facebook, Inc. All rights reserved.
|
||||
*
|
||||
* Facebook, Inc. (“Facebook”) owns all right, title and interest, including
|
||||
* all intellectual property and other proprietary rights, in and to the React
|
||||
* Native CustomComponents software (the “Software”). Subject to your
|
||||
* compliance with these terms, you are hereby granted a non-exclusive,
|
||||
* worldwide, royalty-free copyright license to (1) use and copy the Software;
|
||||
* and (2) reproduce and distribute the Software as part of your own software
|
||||
* (“Your Software”). Facebook reserves all rights not expressly granted to
|
||||
* you in this license agreement.
|
||||
*
|
||||
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
|
||||
* OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
|
||||
* EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
|
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest
|
||||
.dontMock('NavigationRouteStack')
|
||||
.dontMock('clamp')
|
||||
.dontMock('invariant')
|
||||
.dontMock('immutable');
|
||||
|
||||
var NavigationRouteStack = require('NavigationRouteStack');
|
||||
|
||||
describe('NavigationRouteStack:', () => {
|
||||
// Basic
|
||||
it('gets index', () => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b', 'c']);
|
||||
expect(stack.index).toEqual(1);
|
||||
});
|
||||
|
||||
it('gets size', () => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b', 'c']);
|
||||
expect(stack.size).toEqual(3);
|
||||
});
|
||||
|
||||
it('gets route', () => {
|
||||
var stack = new NavigationRouteStack(0, ['a', 'b', 'c']);
|
||||
expect(stack.get(2)).toEqual('c');
|
||||
});
|
||||
|
||||
it('converts to an array', () => {
|
||||
var stack = new NavigationRouteStack(0, ['a', 'b']);
|
||||
expect(stack.toArray()).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('creates a new stack after mutation', () => {
|
||||
var stack1 = new NavigationRouteStack(0, ['a', 'b']);
|
||||
var stack2 = stack1.push('c');
|
||||
expect(stack1).not.toEqual(stack2);
|
||||
});
|
||||
|
||||
it('throws at index out of bound', () => {
|
||||
expect(() => {
|
||||
new NavigationRouteStack(-1, ['a', 'b']);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
new NavigationRouteStack(100, ['a', 'b']);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
// Push
|
||||
it('pushes route', () => {
|
||||
var stack1 = new NavigationRouteStack(1, ['a', 'b']);
|
||||
var stack2 = stack1.push('c');
|
||||
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2.toArray()).toEqual(['a', 'b', 'c']);
|
||||
expect(stack2.index).toEqual(2);
|
||||
expect(stack2.size).toEqual(3);
|
||||
});
|
||||
|
||||
it('throws when pushing empty route', () => {
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
stack.push(null);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
stack.push('');
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
stack.push(undefined);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('replaces routes on push', () => {
|
||||
var stack1 = new NavigationRouteStack(1, ['a', 'b', 'c']);
|
||||
var stack2 = stack1.push('d');
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2.toArray()).toEqual(['a', 'b', 'd']);
|
||||
expect(stack2.index).toEqual(2);
|
||||
});
|
||||
|
||||
// Pop
|
||||
it('pops route', () => {
|
||||
var stack1 = new NavigationRouteStack(2, ['a', 'b', 'c']);
|
||||
var stack2 = stack1.pop();
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2.toArray()).toEqual(['a', 'b']);
|
||||
expect(stack2.index).toEqual(1);
|
||||
expect(stack2.size).toEqual(2);
|
||||
});
|
||||
|
||||
it('replaces routes on pop', () => {
|
||||
var stack1 = new NavigationRouteStack(1, ['a', 'b', 'c']);
|
||||
var stack2 = stack1.pop();
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2.toArray()).toEqual(['a']);
|
||||
expect(stack2.index).toEqual(0);
|
||||
});
|
||||
|
||||
it('throws when popping to empty stack', () => {
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(0, ['a']);
|
||||
stack.pop();
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
// Jump
|
||||
it('jumps to index', () => {
|
||||
var stack1 = new NavigationRouteStack(0, ['a', 'b', 'c']);
|
||||
var stack2 = stack1.jumpToIndex(2);
|
||||
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2.index).toEqual(2);
|
||||
});
|
||||
|
||||
it('throws then jumping to index out of bound', () => {
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
stack.jumpToIndex(2);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
stack.jumpToIndex(-1);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
// Replace
|
||||
it('replaces route at index', () => {
|
||||
var stack1 = new NavigationRouteStack(1, ['a', 'b']);
|
||||
var stack2 = stack1.replaceAtIndex(0, 'x');
|
||||
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2.toArray()).toEqual(['x', 'b']);
|
||||
expect(stack2.index).toEqual(1);
|
||||
});
|
||||
|
||||
it('replaces route at negative index', () => {
|
||||
var stack1 = new NavigationRouteStack(1, ['a', 'b']);
|
||||
var stack2 = stack1.replaceAtIndex(-1, 'x');
|
||||
|
||||
expect(stack2).not.toEqual(stack1);
|
||||
expect(stack2.toArray()).toEqual(['a', 'x']);
|
||||
expect(stack2.index).toEqual(1);
|
||||
});
|
||||
|
||||
|
||||
it('throws when replacing empty route', () => {
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
stack.replaceAtIndex(1, null);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('throws when replacing at index out of bound', () => {
|
||||
expect(() => {
|
||||
var stack = new NavigationRouteStack(1, ['a', 'b']);
|
||||
stack.replaceAtIndex(100, 'x');
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
|
@ -1002,7 +1002,8 @@ var Navigator = React.createClass({
|
|||
},
|
||||
|
||||
getCurrentRoutes: function() {
|
||||
return this.state.routeStack;
|
||||
// Clone before returning to avoid caller mutating the stack
|
||||
return this.state.routeStack.slice();
|
||||
},
|
||||
|
||||
_handleItemRef: function(itemId, route, ref) {
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
#import "RCTImageLoader.h"
|
||||
#import "RCTLog.h"
|
||||
#import "RCTUtils.h"
|
||||
|
||||
@implementation RCTCameraRollManager
|
||||
|
||||
|
@ -23,21 +24,20 @@ RCT_EXPORT_MODULE()
|
|||
|
||||
RCT_EXPORT_METHOD(saveImageWithTag:(NSString *)imageTag
|
||||
successCallback:(RCTResponseSenderBlock)successCallback
|
||||
errorCallback:(RCTResponseSenderBlock)errorCallback)
|
||||
errorCallback:(RCTResponseErrorBlock)errorCallback)
|
||||
{
|
||||
[RCTImageLoader loadImageWithTag:imageTag callback:^(NSError *loadError, UIImage *loadedImage) {
|
||||
if (loadError) {
|
||||
errorCallback(@[[loadError localizedDescription]]);
|
||||
errorCallback(loadError);
|
||||
return;
|
||||
}
|
||||
[[RCTImageLoader assetsLibrary] writeImageToSavedPhotosAlbum:[loadedImage CGImage] metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) {
|
||||
if (saveError) {
|
||||
NSString *errorMessage = [NSString stringWithFormat:@"Error saving cropped image: %@", saveError];
|
||||
RCTLogWarn(@"%@", errorMessage);
|
||||
errorCallback(@[errorMessage]);
|
||||
return;
|
||||
RCTLogWarn(@"Error saving cropped image: %@", saveError);
|
||||
errorCallback(saveError);
|
||||
} else {
|
||||
successCallback(@[[assetURL absoluteString]]);
|
||||
}
|
||||
successCallback(@[[assetURL absoluteString]]);
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ RCT_EXPORT_METHOD(saveImageWithTag:(NSString *)imageTag
|
|||
|
||||
RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
||||
callback:(RCTResponseSenderBlock)callback
|
||||
errorCallback:(RCTResponseSenderBlock)errorCallback)
|
||||
errorCallback:(RCTResponseErrorBlock)errorCallback)
|
||||
{
|
||||
NSUInteger first = [params[@"first"] integerValue];
|
||||
NSString *afterCursor = params[@"after"];
|
||||
|
@ -160,7 +160,7 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
|||
if (error.code != ALAssetsLibraryAccessUserDeniedError) {
|
||||
RCTLogError(@"Failure while iterating through asset groups %@", error);
|
||||
}
|
||||
errorCallback(@[error.description]);
|
||||
errorCallback(error);
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
|
@ -381,7 +381,7 @@ RCT_EXPORT_MODULE()
|
|||
|
||||
NSString *responseText = [[NSString alloc] initWithData:data encoding:encoding];
|
||||
if (!responseText && data.length) {
|
||||
RCTLogError(@"Received data was invalid.");
|
||||
RCTLogWarn(@"Received data was invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -154,7 +154,7 @@ class XMLHttpRequestBase {
|
|||
this._lowerCaseResponseHeaders =
|
||||
Object.keys(headers).reduce((lcaseHeaders, headerName) => {
|
||||
lcaseHeaders[headerName.toLowerCase()] = headers[headerName];
|
||||
return headers;
|
||||
return lcaseHeaders;
|
||||
}, {});
|
||||
}
|
||||
|
||||
|
|
|
@ -25,12 +25,12 @@ typedef NS_ENUM(NSInteger, RCTTestStatus) {
|
|||
/**
|
||||
* The snapshot test controller for this module.
|
||||
*/
|
||||
@property (nonatomic, weak) FBSnapshotTestController *controller;
|
||||
@property (nonatomic, strong) FBSnapshotTestController *controller;
|
||||
|
||||
/**
|
||||
* This is the view to be snapshotted.
|
||||
*/
|
||||
@property (nonatomic, weak) UIView *view;
|
||||
@property (nonatomic, strong) UIView *view;
|
||||
|
||||
/**
|
||||
* This is used to give meaningful names to snapshot image files.
|
||||
|
|
|
@ -16,7 +16,10 @@
|
|||
*
|
||||
* FB_REFERENCE_IMAGE_DIR="\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\""
|
||||
*/
|
||||
#define RCTInitRunnerForApp(app__) [[RCTTestRunner alloc] initWithApp:(app__) referenceDir:@FB_REFERENCE_IMAGE_DIR]
|
||||
#define RCTInitRunnerForApp(app__, moduleProvider__) \
|
||||
[[RCTTestRunner alloc] initWithApp:(app__) \
|
||||
referenceDirectory:@FB_REFERENCE_IMAGE_DIR \
|
||||
moduleProvider:(moduleProvider__)]
|
||||
|
||||
@interface RCTTestRunner : NSObject
|
||||
|
||||
|
@ -28,10 +31,12 @@
|
|||
* macro instead of calling this directly.
|
||||
*
|
||||
* @param app The path to the app bundle without suffixes, e.g. IntegrationTests/IntegrationTestsApp
|
||||
* @param referenceDir The path for snapshot references images. The RCTInitRunnerForApp macro uses
|
||||
* FB_REFERENCE_IMAGE_DIR for this automatically.
|
||||
* @param referenceDirectory The path for snapshot references images. The RCTInitRunnerForApp macro uses FB_REFERENCE_IMAGE_DIR for this automatically.
|
||||
* @param block A block that returns an array of extra modules to be used by the test runner.
|
||||
*/
|
||||
- (instancetype)initWithApp:(NSString *)app referenceDir:(NSString *)referenceDir NS_DESIGNATED_INITIALIZER;
|
||||
- (instancetype)initWithApp:(NSString *)app
|
||||
referenceDirectory:(NSString *)referenceDirectory
|
||||
moduleProvider:(NSArray *(^)(void))block NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
/**
|
||||
* Simplest runTest function simply mounts the specified JS module with no
|
||||
|
|
|
@ -27,20 +27,27 @@
|
|||
@implementation RCTTestRunner
|
||||
{
|
||||
FBSnapshotTestController *_testController;
|
||||
RCTBridgeModuleProviderBlock _moduleProvider;
|
||||
}
|
||||
|
||||
- (instancetype)initWithApp:(NSString *)app referenceDir:(NSString *)referenceDir
|
||||
- (instancetype)initWithApp:(NSString *)app
|
||||
referenceDirectory:(NSString *)referenceDirectory
|
||||
moduleProvider:(RCTBridgeModuleProviderBlock)block
|
||||
{
|
||||
RCTAssertParam(app);
|
||||
RCTAssertParam(referenceDir);
|
||||
RCTAssertParam(referenceDirectory);
|
||||
|
||||
if ((self = [super init])) {
|
||||
|
||||
NSString *sanitizedAppName = [app stringByReplacingOccurrencesOfString:@"/" withString:@"-"];
|
||||
sanitizedAppName = [sanitizedAppName stringByReplacingOccurrencesOfString:@"\\" withString:@"-"];
|
||||
_testController = [[FBSnapshotTestController alloc] initWithTestName:sanitizedAppName];
|
||||
_testController.referenceImagesDirectory = referenceDir;
|
||||
_testController.referenceImagesDirectory = referenceDirectory;
|
||||
_moduleProvider = [block copy];
|
||||
|
||||
#if RUNNING_ON_CI
|
||||
_scriptURL = [[NSBundle bundleForClass:[RCTBridge class]] URLForResource:@"main" withExtension:@"jsbundle"];
|
||||
RCTAssert(_scriptURL != nil, @"Could not locate main.jsBundle");
|
||||
#else
|
||||
_scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.includeRequire.runModule.bundle?dev=true", app]];
|
||||
#endif
|
||||
|
@ -73,16 +80,20 @@ RCT_NOT_IMPLEMENTED(-init)
|
|||
}];
|
||||
}
|
||||
|
||||
- (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock
|
||||
- (void)runTest:(SEL)test module:(NSString *)moduleName
|
||||
initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock
|
||||
{
|
||||
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:_scriptURL
|
||||
moduleName:moduleName
|
||||
launchOptions:nil];
|
||||
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:_scriptURL
|
||||
moduleProvider:_moduleProvider
|
||||
launchOptions:nil];
|
||||
|
||||
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName];
|
||||
rootView.initialProperties = initialProps;
|
||||
rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices
|
||||
|
||||
NSString *testModuleName = RCTBridgeModuleNameForClass([RCTTestModule class]);
|
||||
RCTTestModule *testModule = rootView.bridge.batchedBridge.modules[testModuleName];
|
||||
RCTAssert(_testController != nil, @"_testController should not be nil");
|
||||
testModule.controller = _testController;
|
||||
testModule.testSelector = test;
|
||||
testModule.view = rootView;
|
||||
|
@ -100,14 +111,11 @@ RCT_NOT_IMPLEMENTED(-init)
|
|||
}
|
||||
[rootView removeFromSuperview];
|
||||
|
||||
|
||||
NSArray *nonLayoutSubviews = [vc.view.subviews filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id subview, NSDictionary *bindings) {
|
||||
return ![NSStringFromClass([subview class]) isEqualToString:@"_UILayoutGuide"];
|
||||
}]];
|
||||
RCTAssert(nonLayoutSubviews.count == 0, @"There shouldn't be any other views: %@", nonLayoutSubviews);
|
||||
|
||||
|
||||
vc.view = nil;
|
||||
[[RCTRedBox sharedInstance] dismiss];
|
||||
if (expectErrorBlock) {
|
||||
RCTAssert(expectErrorBlock(error), @"Expected an error but nothing matched.");
|
||||
|
@ -116,7 +124,6 @@ RCT_NOT_IMPLEMENTED(-init)
|
|||
RCTAssert(testModule.status != RCTTestStatusPending, @"Test didn't finish within %d seconds", TIMEOUT_SECONDS);
|
||||
RCTAssert(testModule.status == RCTTestStatusPassed, @"Test failed");
|
||||
}
|
||||
RCTAssert(self.recordMode == NO, @"Don't forget to turn record mode back to NO before commit.");
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -24,6 +24,18 @@ ReactNativeViewAttributes.UIView = {
|
|||
onLayout: true,
|
||||
onAccessibilityTap: true,
|
||||
onMagicTap: true,
|
||||
collapsible: true,
|
||||
|
||||
// If any below are set, view should not be collapsible!
|
||||
onMoveShouldSetResponder: true,
|
||||
onResponderGrant: true,
|
||||
onResponderMove: true,
|
||||
onResponderReject: true,
|
||||
onResponderRelease: true,
|
||||
onResponderTerminate: true,
|
||||
onResponderTerminationRequest: true,
|
||||
onStartShouldSetResponder: true,
|
||||
onStartShouldSetResponderCapture: true,
|
||||
};
|
||||
|
||||
ReactNativeViewAttributes.RCTView = merge(
|
||||
|
|
|
@ -15,11 +15,6 @@
|
|||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
- (UIView *)view
|
||||
{
|
||||
return [[UIView alloc] init]; // TODO(#1102) Remove useless views.
|
||||
}
|
||||
|
||||
- (RCTShadowView *)shadowView
|
||||
{
|
||||
return [[RCTShadowRawText alloc] init];
|
||||
|
|
|
@ -20,6 +20,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
- (BOOL)isLayoutOnly
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
NSString *superDescription = super.description;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
#import "RCTShadowView.h"
|
||||
#import "RCTTextDecorationLineType.h"
|
||||
|
||||
extern NSString *const RCTIsHighlightedAttributeName;
|
||||
extern NSString *const RCTReactTagAttributeName;
|
||||
|
@ -26,6 +27,9 @@ extern NSString *const RCTReactTagAttributeName;
|
|||
@property (nonatomic, assign) CGSize shadowOffset;
|
||||
@property (nonatomic, assign) NSTextAlignment textAlign;
|
||||
@property (nonatomic, assign) NSWritingDirection writingDirection;
|
||||
@property (nonatomic, strong) UIColor *textDecorationColor;
|
||||
@property (nonatomic, assign) NSUnderlineStyle textDecorationStyle;
|
||||
@property (nonatomic, assign) RCTTextDecorationLineType textDecorationLine;
|
||||
|
||||
- (void)recomputeText;
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ static css_dim_t RCTMeasure(void *context, float width)
|
|||
_fontSize = NAN;
|
||||
_letterSpacing = NAN;
|
||||
_isHighlighted = NO;
|
||||
_textDecorationStyle = NSUnderlineStyleSingle;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
@ -252,6 +253,24 @@ static css_dim_t RCTMeasure(void *context, float width)
|
|||
value:paragraphStyle
|
||||
range:(NSRange){0, attributedString.length}];
|
||||
}
|
||||
|
||||
// Text decoration
|
||||
if(_textDecorationLine == RCTTextDecorationLineTypeUnderline ||
|
||||
_textDecorationLine == RCTTextDecorationLineTypeUnderlineStrikethrough) {
|
||||
[self _addAttribute:NSUnderlineStyleAttributeName withValue:@(_textDecorationStyle)
|
||||
toAttributedString:attributedString];
|
||||
}
|
||||
if(_textDecorationLine == RCTTextDecorationLineTypeStrikethrough ||
|
||||
_textDecorationLine == RCTTextDecorationLineTypeUnderlineStrikethrough){
|
||||
[self _addAttribute:NSStrikethroughStyleAttributeName withValue:@(_textDecorationStyle)
|
||||
toAttributedString:attributedString];
|
||||
}
|
||||
if(_textDecorationColor) {
|
||||
[self _addAttribute:NSStrikethroughColorAttributeName withValue:_textDecorationColor
|
||||
toAttributedString:attributedString];
|
||||
[self _addAttribute:NSUnderlineColorAttributeName withValue:_textDecorationColor
|
||||
toAttributedString:attributedString];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)fillCSSNode:(css_node_t *)node
|
||||
|
@ -297,6 +316,9 @@ RCT_TEXT_PROPERTY(LineHeight, _lineHeight, CGFloat)
|
|||
RCT_TEXT_PROPERTY(NumberOfLines, _numberOfLines, NSUInteger)
|
||||
RCT_TEXT_PROPERTY(ShadowOffset, _shadowOffset, CGSize)
|
||||
RCT_TEXT_PROPERTY(TextAlign, _textAlign, NSTextAlignment)
|
||||
RCT_TEXT_PROPERTY(TextDecorationColor, _textDecorationColor, UIColor *);
|
||||
RCT_TEXT_PROPERTY(TextDecorationLine, _textDecorationLine, RCTTextDecorationLineType);
|
||||
RCT_TEXT_PROPERTY(TextDecorationStyle, _textDecorationStyle, NSUnderlineStyle);
|
||||
RCT_TEXT_PROPERTY(WritingDirection, _writingDirection, NSWritingDirection)
|
||||
|
||||
@end
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
/* Begin PBXBuildFile section */
|
||||
131B6AC01AF0CD0600FFC3E0 /* RCTTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */; };
|
||||
131B6AC11AF0CD0600FFC3E0 /* RCTTextViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */; };
|
||||
1362F1001B4D51F400E06D8C /* RCTTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 1362F0FD1B4D51F400E06D8C /* RCTTextField.m */; };
|
||||
1362F1011B4D51F400E06D8C /* RCTTextFieldManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1362F0FF1B4D51F400E06D8C /* RCTTextFieldManager.m */; };
|
||||
58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */; };
|
||||
58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511C91A9E6C5C00147676 /* RCTShadowRawText.m */; };
|
||||
58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CB1A9E6C5C00147676 /* RCTShadowText.m */; };
|
||||
|
@ -33,6 +35,10 @@
|
|||
131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextView.m; sourceTree = "<group>"; };
|
||||
131B6ABE1AF0CD0600FFC3E0 /* RCTTextViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextViewManager.h; sourceTree = "<group>"; };
|
||||
131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextViewManager.m; sourceTree = "<group>"; };
|
||||
1362F0FC1B4D51F400E06D8C /* RCTTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextField.h; sourceTree = "<group>"; };
|
||||
1362F0FD1B4D51F400E06D8C /* RCTTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextField.m; sourceTree = "<group>"; };
|
||||
1362F0FE1B4D51F400E06D8C /* RCTTextFieldManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextFieldManager.h; sourceTree = "<group>"; };
|
||||
1362F0FF1B4D51F400E06D8C /* RCTTextFieldManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextFieldManager.m; sourceTree = "<group>"; };
|
||||
58B5119B1A9E6C1200147676 /* libRCTText.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTText.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
58B511C61A9E6C5C00147676 /* RCTRawTextManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRawTextManager.h; sourceTree = "<group>"; };
|
||||
58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRawTextManager.m; sourceTree = "<group>"; };
|
||||
|
@ -70,6 +76,10 @@
|
|||
58B512141A9E6EFF00147676 /* RCTText.m */,
|
||||
58B511CC1A9E6C5C00147676 /* RCTTextManager.h */,
|
||||
58B511CD1A9E6C5C00147676 /* RCTTextManager.m */,
|
||||
1362F0FC1B4D51F400E06D8C /* RCTTextField.h */,
|
||||
1362F0FD1B4D51F400E06D8C /* RCTTextField.m */,
|
||||
1362F0FE1B4D51F400E06D8C /* RCTTextFieldManager.h */,
|
||||
1362F0FF1B4D51F400E06D8C /* RCTTextFieldManager.m */,
|
||||
131B6ABC1AF0CD0600FFC3E0 /* RCTTextView.h */,
|
||||
131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */,
|
||||
131B6ABE1AF0CD0600FFC3E0 /* RCTTextViewManager.h */,
|
||||
|
@ -147,7 +157,9 @@
|
|||
58B511D11A9E6C5C00147676 /* RCTTextManager.m in Sources */,
|
||||
131B6AC01AF0CD0600FFC3E0 /* RCTTextView.m in Sources */,
|
||||
58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */,
|
||||
1362F1001B4D51F400E06D8C /* RCTTextField.m in Sources */,
|
||||
58B512161A9E6EFF00147676 /* RCTText.m in Sources */,
|
||||
1362F1011B4D51F400E06D8C /* RCTTextFieldManager.m in Sources */,
|
||||
131B6AC11AF0CD0600FFC3E0 /* RCTTextViewManager.m in Sources */,
|
||||
58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */,
|
||||
58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */,
|
||||
|
|
|
@ -34,7 +34,6 @@ RCT_EXPORT_MODULE()
|
|||
|
||||
#pragma mark - Shadow properties
|
||||
|
||||
RCT_EXPORT_SHADOW_PROPERTY(writingDirection, NSWritingDirection)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(color, UIColor)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(fontFamily, NSString)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(fontSize, CGFloat)
|
||||
|
@ -43,9 +42,13 @@ RCT_EXPORT_SHADOW_PROPERTY(fontStyle, NSString)
|
|||
RCT_EXPORT_SHADOW_PROPERTY(isHighlighted, BOOL)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(letterSpacing, CGFloat)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(lineHeight, CGFloat)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(numberOfLines, NSUInteger)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(shadowOffset, CGSize)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(textAlign, NSTextAlignment)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(numberOfLines, NSUInteger)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(textDecorationStyle, NSUnderlineStyle)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(textDecorationColor, UIColor)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(textDecorationLine, RCTTextDecorationLineType)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(writingDirection, NSWritingDirection)
|
||||
|
||||
- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry
|
||||
{
|
||||
|
|
|
@ -33,6 +33,13 @@ var TextStylePropTypes = Object.assign(Object.create(ViewStylePropTypes), {
|
|||
['auto' /*default*/, 'ltr', 'rtl']
|
||||
),
|
||||
letterSpacing: ReactPropTypes.number,
|
||||
textDecorationLine:ReactPropTypes.oneOf(
|
||||
['none' /*default*/, 'underline', 'line-through', 'underline line-through']
|
||||
),
|
||||
textDecorationStyle:ReactPropTypes.oneOf(
|
||||
['solid' /*default*/, 'double', 'dotted','dashed']
|
||||
),
|
||||
textDecorationColor: ReactPropTypes.string,
|
||||
});
|
||||
|
||||
// Text doesn't support padding correctly (#4841912)
|
||||
|
|
|
@ -45,11 +45,13 @@ var ReactNative = Object.assign(Object.create(require('React')), {
|
|||
ActionSheetIOS: require('ActionSheetIOS'),
|
||||
AdSupportIOS: require('AdSupportIOS'),
|
||||
AlertIOS: require('AlertIOS'),
|
||||
Animated: require('Animated'),
|
||||
AppRegistry: require('AppRegistry'),
|
||||
AppStateIOS: require('AppStateIOS'),
|
||||
AsyncStorage: require('AsyncStorage'),
|
||||
CameraRoll: require('CameraRoll'),
|
||||
Dimensions: require('Dimensions'),
|
||||
Easing: require('Easing'),
|
||||
ImagePickerIOS: require('ImagePickerIOS'),
|
||||
InteractionManager: require('InteractionManager'),
|
||||
LayoutAnimation: require('LayoutAnimation'),
|
||||
|
|
|
@ -19,6 +19,12 @@
|
|||
*/
|
||||
typedef void (^RCTResponseSenderBlock)(NSArray *response);
|
||||
|
||||
/**
|
||||
* The type of a block that is capable of sending an error response to a
|
||||
* bridged operation. Use this for returning error information to JS.
|
||||
*/
|
||||
typedef void (^RCTResponseErrorBlock)(NSError *error);
|
||||
|
||||
/**
|
||||
* Block that bridge modules use to resolve the JS promise waiting for a result.
|
||||
* Nil results are supported and are converted to JS's undefined value.
|
||||
|
|
|
@ -12,10 +12,12 @@
|
|||
|
||||
#import "Layout.h"
|
||||
#import "RCTAnimationType.h"
|
||||
#import "RCTTextDecorationLineType.h"
|
||||
#import "RCTDefines.h"
|
||||
#import "RCTLog.h"
|
||||
#import "RCTPointerEvents.h"
|
||||
|
||||
|
||||
/**
|
||||
* This class provides a collection of conversion functions for mapping
|
||||
* JSON objects to native types and classes. These are useful when writing
|
||||
|
@ -54,6 +56,7 @@ typedef NSURL RCTFileURL;
|
|||
+ (NSTimeInterval)NSTimeInterval:(id)json;
|
||||
|
||||
+ (NSTextAlignment)NSTextAlignment:(id)json;
|
||||
+ (NSUnderlineStyle)NSUnderlineStyle:(id)json;
|
||||
+ (NSWritingDirection)NSWritingDirection:(id)json;
|
||||
+ (UITextAutocapitalizationType)UITextAutocapitalizationType:(id)json;
|
||||
+ (UITextFieldViewMode)UITextFieldViewMode:(id)json;
|
||||
|
@ -126,6 +129,7 @@ typedef BOOL css_clip_t;
|
|||
|
||||
+ (RCTPointerEvents)RCTPointerEvents:(id)json;
|
||||
+ (RCTAnimationType)RCTAnimationType:(id)json;
|
||||
+ (RCTTextDecorationLineType)RCTTextDecorationLineType:(id)json;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -216,6 +216,20 @@ RCT_ENUM_CONVERTER(NSTextAlignment, (@{
|
|||
@"justify": @(NSTextAlignmentJustified),
|
||||
}), NSTextAlignmentNatural, integerValue)
|
||||
|
||||
RCT_ENUM_CONVERTER(NSUnderlineStyle, (@{
|
||||
@"solid": @(NSUnderlineStyleSingle),
|
||||
@"double": @(NSUnderlineStyleDouble),
|
||||
@"dotted": @(NSUnderlinePatternDot | NSUnderlineStyleSingle),
|
||||
@"dashed": @(NSUnderlinePatternDash | NSUnderlineStyleSingle),
|
||||
}), NSUnderlineStyleSingle, integerValue)
|
||||
|
||||
RCT_ENUM_CONVERTER(RCTTextDecorationLineType, (@{
|
||||
@"none": @(RCTTextDecorationLineTypeNone),
|
||||
@"underline": @(RCTTextDecorationLineTypeUnderline),
|
||||
@"line-through": @(RCTTextDecorationLineTypeStrikethrough),
|
||||
@"underline line-through": @(RCTTextDecorationLineTypeUnderlineStrikethrough),
|
||||
}), RCTTextDecorationLineTypeNone, integerValue)
|
||||
|
||||
RCT_ENUM_CONVERTER(NSWritingDirection, (@{
|
||||
@"auto": @(NSWritingDirectionNatural),
|
||||
@"ltr": @(NSWritingDirectionLeftToRight),
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
- (instancetype)initWithBridge:(RCTBridge *)bridge
|
||||
{
|
||||
RCTAssert(bridge, @"birdge parameter is required");
|
||||
RCTAssert(bridge, @"bridge parameter is required");
|
||||
|
||||
if ((self = [super init])) {
|
||||
_bridge = bridge;
|
||||
|
|
|
@ -174,6 +174,22 @@ case _value: { \
|
|||
}
|
||||
} else if ([argumentName isEqualToString:@"RCTResponseSenderBlock"]) {
|
||||
addBlockArgument();
|
||||
} else if ([argumentName isEqualToString:@"RCTResponseErrorBlock"]) {
|
||||
RCT_ARG_BLOCK(
|
||||
|
||||
if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) {
|
||||
RCTLogError(@"Argument %tu (%@) of %@.%@ should be a number", index,
|
||||
json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Marked as autoreleasing, because NSInvocation doesn't retain arguments
|
||||
__autoreleasing id value = (json ? ^(NSError *error) {
|
||||
[bridge _invokeAndProcessModule:@"BatchedBridge"
|
||||
method:@"invokeCallbackAndReturnFlushedQueue"
|
||||
arguments:@[json, @[RCTJSErrorFromNSError(error)]]];
|
||||
} : ^(__unused NSError *error) {});
|
||||
)
|
||||
} else if ([argumentName isEqualToString:@"RCTPromiseResolveBlock"]) {
|
||||
RCTAssert(i == numberOfArguments - 2,
|
||||
@"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]",
|
||||
|
|
|
@ -45,7 +45,6 @@ RCT_EXTERN BOOL RCTClassOverridesClassMethod(Class cls, SEL selector);
|
|||
RCT_EXTERN BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector);
|
||||
|
||||
// Creates a standardized error object
|
||||
// TODO(#6472857): create NSErrors and automatically convert them over the bridge.
|
||||
RCT_EXTERN NSDictionary *RCTMakeError(NSString *message, id toStringify, NSDictionary *extraData);
|
||||
RCT_EXTERN NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, NSDictionary *extraData);
|
||||
|
||||
|
@ -55,7 +54,7 @@ RCT_EXTERN BOOL RCTRunningInTestEnvironment(void);
|
|||
// Return YES if image has an alpha component
|
||||
RCT_EXTERN BOOL RCTImageHasAlpha(CGImageRef image);
|
||||
|
||||
// Create an NSError in the NCTErrorDomain
|
||||
// Create an NSError in the RCTErrorDomain
|
||||
RCT_EXTERN NSError *RCTErrorWithMessage(NSString *message);
|
||||
|
||||
// Convert nil values to NSNull, and vice-versa
|
||||
|
|
|
@ -177,12 +177,9 @@ void RCTSwapClassMethods(Class cls, SEL original, SEL replacement)
|
|||
IMP replacementImplementation = method_getImplementation(replacementMethod);
|
||||
const char *replacementArgTypes = method_getTypeEncoding(replacementMethod);
|
||||
|
||||
if (class_addMethod(cls, original, replacementImplementation, replacementArgTypes))
|
||||
{
|
||||
if (class_addMethod(cls, original, replacementImplementation, replacementArgTypes)) {
|
||||
class_replaceMethod(cls, replacement, originalImplementation, originalArgTypes);
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
method_exchangeImplementations(originalMethod, replacementMethod);
|
||||
}
|
||||
}
|
||||
|
@ -197,12 +194,9 @@ void RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement)
|
|||
IMP replacementImplementation = method_getImplementation(replacementMethod);
|
||||
const char *replacementArgTypes = method_getTypeEncoding(replacementMethod);
|
||||
|
||||
if (class_addMethod(cls, original, replacementImplementation, replacementArgTypes))
|
||||
{
|
||||
if (class_addMethod(cls, original, replacementImplementation, replacementArgTypes)) {
|
||||
class_replaceMethod(cls, replacement, originalImplementation, originalArgTypes);
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
method_exchangeImplementations(originalMethod, replacementMethod);
|
||||
}
|
||||
}
|
||||
|
@ -216,10 +210,8 @@ BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector)
|
|||
{
|
||||
unsigned int numberOfMethods;
|
||||
Method *methods = class_copyMethodList(cls, &numberOfMethods);
|
||||
for (unsigned int i = 0; i < numberOfMethods; i++)
|
||||
{
|
||||
if (method_getName(methods[i]) == selector)
|
||||
{
|
||||
for (unsigned int i = 0; i < numberOfMethods; i++) {
|
||||
if (method_getName(methods[i]) == selector) {
|
||||
free(methods);
|
||||
return YES;
|
||||
}
|
||||
|
@ -231,12 +223,11 @@ BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector)
|
|||
NSDictionary *RCTMakeError(NSString *message, id toStringify, NSDictionary *extraData)
|
||||
{
|
||||
if (toStringify) {
|
||||
message = [NSString stringWithFormat:@"%@%@", message, toStringify];
|
||||
}
|
||||
NSMutableDictionary *error = [@{@"message": message} mutableCopy];
|
||||
if (extraData) {
|
||||
[error addEntriesFromDictionary:extraData];
|
||||
message = [message stringByAppendingString:[toStringify description]];
|
||||
}
|
||||
|
||||
NSMutableDictionary *error = [NSMutableDictionary dictionaryWithDictionary:extraData];
|
||||
error[@"message"] = message;
|
||||
return error;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,15 +33,8 @@
|
|||
#import "RCTViewNodeProtocol.h"
|
||||
#import "UIView+React.h"
|
||||
|
||||
typedef void (^react_view_node_block_t)(id<RCTViewNodeProtocol>);
|
||||
|
||||
static void RCTTraverseViewNodes(id<RCTViewNodeProtocol> view, react_view_node_block_t block)
|
||||
{
|
||||
if (view.reactTag) block(view);
|
||||
for (id<RCTViewNodeProtocol> subview in view.reactSubviews) {
|
||||
RCTTraverseViewNodes(subview, block);
|
||||
}
|
||||
}
|
||||
static void RCTTraverseViewNodes(id<RCTViewNodeProtocol> view, void (^block)(id<RCTViewNodeProtocol>));
|
||||
static NSDictionary *RCTPropsMerge(NSDictionary *beforeProps, NSDictionary *newProps);
|
||||
|
||||
@interface RCTAnimation : NSObject
|
||||
|
||||
|
@ -140,10 +133,11 @@ static UIViewAnimationOptions UIViewAnimationOptionsFromRCTAnimationType(RCTAnim
|
|||
|
||||
@interface RCTLayoutAnimation : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSDictionary *config;
|
||||
@property (nonatomic, strong) RCTAnimation *createAnimation;
|
||||
@property (nonatomic, strong) RCTAnimation *updateAnimation;
|
||||
@property (nonatomic, strong) RCTAnimation *deleteAnimation;
|
||||
@property (nonatomic, strong) RCTResponseSenderBlock callback;
|
||||
@property (nonatomic, copy) RCTResponseSenderBlock callback;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -156,7 +150,7 @@ static UIViewAnimationOptions UIViewAnimationOptionsFromRCTAnimationType(RCTAnim
|
|||
}
|
||||
|
||||
if ((self = [super init])) {
|
||||
|
||||
_config = [config copy];
|
||||
NSTimeInterval duration = [RCTConvert NSTimeInterval:config[@"duration"]];
|
||||
if (duration > 0.0 && duration < 0.01) {
|
||||
RCTLogError(@"RCTLayoutAnimation expects timings to be in ms, not seconds.");
|
||||
|
@ -467,6 +461,24 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass)
|
|||
[rootShadowView collectRootUpdatedFrames:viewsWithNewFrames
|
||||
parentConstraint:(CGSize){CSS_UNDEFINED, CSS_UNDEFINED}];
|
||||
|
||||
NSSet *originalViewsWithNewFrames = [viewsWithNewFrames copy];
|
||||
NSMutableArray *viewsToCheck = [viewsWithNewFrames.allObjects mutableCopy];
|
||||
while (viewsToCheck.count > 0) {
|
||||
// Better to remove from the front and append to the end
|
||||
// because of how NSMutableArray is implemented.
|
||||
// (It's a "round" buffer with stored size and offset.)
|
||||
|
||||
RCTShadowView *viewToCheck = viewsToCheck.firstObject;
|
||||
[viewsToCheck removeObjectAtIndex:0];
|
||||
|
||||
if (viewToCheck.layoutOnly) {
|
||||
[viewsWithNewFrames removeObject:viewToCheck];
|
||||
[viewsToCheck addObjectsFromArray:[viewToCheck reactSubviews]];
|
||||
} else {
|
||||
[viewsWithNewFrames addObject:viewToCheck];
|
||||
}
|
||||
}
|
||||
|
||||
// Parallel arrays are built and then handed off to main thread
|
||||
NSMutableArray *frameReactTags = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count];
|
||||
NSMutableArray *frames = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count];
|
||||
|
@ -475,26 +487,30 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass)
|
|||
NSMutableArray *onLayoutEvents = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count];
|
||||
|
||||
for (RCTShadowView *shadowView in viewsWithNewFrames) {
|
||||
[frameReactTags addObject:shadowView.reactTag];
|
||||
[frames addObject:[NSValue valueWithCGRect:shadowView.frame]];
|
||||
CGRect frame = shadowView.adjustedFrame;
|
||||
NSNumber *reactTag = shadowView.reactTag;
|
||||
[frameReactTags addObject:reactTag];
|
||||
[frames addObject:[NSValue valueWithCGRect:frame]];
|
||||
[areNew addObject:@(shadowView.isNewView)];
|
||||
[parentsAreNew addObject:@(shadowView.superview.isNewView)];
|
||||
id event = (id)kCFNull;
|
||||
if (shadowView.hasOnLayout) {
|
||||
event = @{
|
||||
@"target": shadowView.reactTag,
|
||||
@"layout": @{
|
||||
@"x": @(shadowView.frame.origin.x),
|
||||
@"y": @(shadowView.frame.origin.y),
|
||||
@"width": @(shadowView.frame.size.width),
|
||||
@"height": @(shadowView.frame.size.height),
|
||||
},
|
||||
};
|
||||
|
||||
RCTShadowView *superview = shadowView;
|
||||
BOOL parentIsNew = NO;
|
||||
while (YES) {
|
||||
superview = superview.superview;
|
||||
parentIsNew = superview.isNewView;
|
||||
if (!superview.layoutOnly) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
[parentsAreNew addObject:@(parentIsNew)];
|
||||
|
||||
id event = shadowView.hasOnLayout
|
||||
? RCTShadowViewOnLayoutEventPayload(shadowView.reactTag, frame)
|
||||
: (id)kCFNull;
|
||||
[onLayoutEvents addObject:event];
|
||||
}
|
||||
|
||||
for (RCTShadowView *shadowView in viewsWithNewFrames) {
|
||||
for (RCTShadowView *shadowView in originalViewsWithNewFrames) {
|
||||
// We have to do this after we build the parentsAreNew array.
|
||||
shadowView.newView = NO;
|
||||
}
|
||||
|
@ -511,24 +527,28 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass)
|
|||
}
|
||||
|
||||
// Perform layout (possibly animated)
|
||||
return ^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
|
||||
RCTResponseSenderBlock callback = self->_layoutAnimation.callback;
|
||||
return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
|
||||
RCTResponseSenderBlock callback = uiManager->_layoutAnimation.callback;
|
||||
__block NSUInteger completionsCalled = 0;
|
||||
for (NSUInteger ii = 0; ii < frames.count; ii++) {
|
||||
NSNumber *reactTag = frameReactTags[ii];
|
||||
UIView *view = viewRegistry[reactTag];
|
||||
if (!view) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CGRect frame = [frames[ii] CGRectValue];
|
||||
id event = onLayoutEvents[ii];
|
||||
|
||||
BOOL isNew = [areNew[ii] boolValue];
|
||||
RCTAnimation *updateAnimation = isNew ? nil : _layoutAnimation.updateAnimation;
|
||||
RCTAnimation *updateAnimation = isNew ? nil : uiManager->_layoutAnimation.updateAnimation;
|
||||
BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue];
|
||||
RCTAnimation *createAnimation = shouldAnimateCreation ? _layoutAnimation.createAnimation : nil;
|
||||
RCTAnimation *createAnimation = shouldAnimateCreation ? uiManager->_layoutAnimation.createAnimation : nil;
|
||||
|
||||
void (^completion)(BOOL) = ^(BOOL finished) {
|
||||
completionsCalled++;
|
||||
if (event != (id)kCFNull) {
|
||||
[self.bridge.eventDispatcher sendInputEventWithName:@"topLayout" body:event];
|
||||
[uiManager.bridge.eventDispatcher sendInputEventWithName:@"topLayout" body:event];
|
||||
}
|
||||
if (callback && completionsCalled == frames.count - 1) {
|
||||
callback(@[@(finished)]);
|
||||
|
@ -540,13 +560,13 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass)
|
|||
[updateAnimation performAnimations:^{
|
||||
[view reactSetFrame:frame];
|
||||
for (RCTViewManagerUIBlock block in updateBlocks) {
|
||||
block(self, _viewRegistry);
|
||||
block(uiManager, viewRegistry);
|
||||
}
|
||||
} withCompletionBlock:completion];
|
||||
} else {
|
||||
[view reactSetFrame:frame];
|
||||
for (RCTViewManagerUIBlock block in updateBlocks) {
|
||||
block(self, _viewRegistry);
|
||||
block(uiManager, viewRegistry);
|
||||
}
|
||||
completion(YES);
|
||||
}
|
||||
|
@ -568,7 +588,7 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass)
|
|||
createAnimation.property);
|
||||
}
|
||||
for (RCTViewManagerUIBlock block in updateBlocks) {
|
||||
block(self, _viewRegistry);
|
||||
block(uiManager, viewRegistry);
|
||||
}
|
||||
} withCompletionBlock:nil];
|
||||
}
|
||||
|
@ -691,6 +711,135 @@ RCT_EXPORT_METHOD(replaceExistingNonRootView:(NSNumber *)reactTag withView:(NSNu
|
|||
removeAtIndices:removeAtIndices];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method modifies the indices received in manageChildren() to take into
|
||||
* account views that are layout only. For example, if JS tells native to insert
|
||||
* view with tag 12 at index 4, but view 12 is layout only, we would want to
|
||||
* insert its children's tags, tags 13 and 14, at indices 4 and 5 instead. This
|
||||
* causes us to have to shift the remaining indices to account for the new
|
||||
* views.
|
||||
*/
|
||||
- (void)modifyManageChildren:(NSNumber *)containerReactTag
|
||||
addChildReactTags:(NSMutableArray *)mutableAddChildReactTags
|
||||
addAtIndices:(NSMutableArray *)mutableAddAtIndices
|
||||
removeAtIndices:(NSMutableArray *)mutableRemoveAtIndices
|
||||
{
|
||||
NSUInteger i;
|
||||
NSMutableArray *containerSubviews = [[_shadowViewRegistry[containerReactTag] reactSubviews] mutableCopy];
|
||||
|
||||
i = 0;
|
||||
while (i < containerSubviews.count) {
|
||||
RCTShadowView *shadowView = containerSubviews[i];
|
||||
if (!shadowView.layoutOnly) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
[containerSubviews removeObjectAtIndex:i];
|
||||
|
||||
NSArray *subviews = [shadowView reactSubviews];
|
||||
NSUInteger subviewsCount = subviews.count;
|
||||
NSIndexSet *insertionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(i, subviewsCount)];
|
||||
[containerSubviews insertObjects:subviews atIndexes:insertionIndexes];
|
||||
|
||||
NSUInteger removalIndex = [mutableRemoveAtIndices indexOfObject:@(i)];
|
||||
if (removalIndex != NSNotFound) {
|
||||
[mutableRemoveAtIndices removeObjectAtIndex:removalIndex];
|
||||
}
|
||||
|
||||
if (subviewsCount != 1) {
|
||||
for (NSUInteger j = 0, count = mutableRemoveAtIndices.count; j < count; j++) {
|
||||
NSUInteger atIndex = [mutableRemoveAtIndices[j] unsignedIntegerValue];
|
||||
if (atIndex > i) {
|
||||
mutableRemoveAtIndices[j] = @(atIndex + subviewsCount - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removalIndex != NSNotFound) {
|
||||
for (NSUInteger j = 0; j < subviewsCount; j++) {
|
||||
[mutableRemoveAtIndices insertObject:@(i + j) atIndex:removalIndex + j];
|
||||
}
|
||||
}
|
||||
|
||||
if (removalIndex == NSNotFound && subviewsCount != 1) {
|
||||
for (NSUInteger j = 0, count = mutableAddAtIndices.count; j < count; j++) {
|
||||
NSUInteger atIndex = [mutableAddAtIndices[j] unsignedIntegerValue];
|
||||
if (atIndex > i) {
|
||||
mutableAddAtIndices[j] = @(atIndex + subviewsCount - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i = 0;
|
||||
while (i < mutableAddChildReactTags.count) {
|
||||
NSNumber *tag = mutableAddChildReactTags[i];
|
||||
NSNumber *index = mutableAddAtIndices[i];
|
||||
|
||||
RCTShadowView *shadowView = _shadowViewRegistry[tag];
|
||||
if (!shadowView.layoutOnly) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
NSArray *subviews = [shadowView reactSubviews];
|
||||
NSUInteger subviewsCount = subviews.count;
|
||||
[mutableAddAtIndices removeObjectAtIndex:i];
|
||||
[mutableAddChildReactTags removeObjectAtIndex:i];
|
||||
|
||||
for (NSUInteger j = 0; j < subviewsCount; j++) {
|
||||
[mutableAddChildReactTags insertObject:[subviews[j] reactTag] atIndex:i + j];
|
||||
[mutableAddAtIndices insertObject:@(index.unsignedIntegerValue + j) atIndex:i + j];
|
||||
}
|
||||
|
||||
for (NSUInteger j = i + subviewsCount, count = mutableAddAtIndices.count; j < count; j++) {
|
||||
NSUInteger atIndex = [mutableAddAtIndices[j] unsignedIntegerValue];
|
||||
mutableAddAtIndices[j] = @(atIndex + subviewsCount - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSNumber *)containerReactTag:(NSNumber *)containerReactTag offset:(inout NSUInteger *)offset
|
||||
{
|
||||
RCTShadowView *container = _shadowViewRegistry[containerReactTag];
|
||||
NSNumber *containerSuperviewReactTag = containerReactTag;
|
||||
RCTShadowView *superview = container;
|
||||
|
||||
while (superview.layoutOnly) {
|
||||
RCTShadowView *superviewSuperview = superview.superview;
|
||||
containerSuperviewReactTag = superviewSuperview.reactTag;
|
||||
NSMutableArray *reactSubviews = [[superviewSuperview reactSubviews] mutableCopy];
|
||||
NSUInteger superviewIndex = [reactSubviews indexOfObject:superview];
|
||||
|
||||
NSUInteger i = 0;
|
||||
while (i < superviewIndex) {
|
||||
RCTShadowView *child = reactSubviews[i];
|
||||
if (!child.layoutOnly) {
|
||||
if (offset) {
|
||||
(*offset)++;
|
||||
}
|
||||
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
[reactSubviews removeObjectAtIndex:i];
|
||||
|
||||
NSArray *subviews = [child reactSubviews];
|
||||
NSUInteger subviewsCount = subviews.count;
|
||||
NSIndexSet *insertionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(i, subviewsCount)];
|
||||
[reactSubviews insertObjects:subviews atIndexes:insertionIndexes];
|
||||
|
||||
superviewIndex += subviewsCount - 1;
|
||||
}
|
||||
|
||||
superview = superviewSuperview;
|
||||
}
|
||||
|
||||
return containerSuperviewReactTag;
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(manageChildren:(NSNumber *)containerReactTag
|
||||
moveFromIndices:(NSArray *)moveFromIndices
|
||||
moveToIndices:(NSArray *)moveToIndices
|
||||
|
@ -698,62 +847,109 @@ RCT_EXPORT_METHOD(manageChildren:(NSNumber *)containerReactTag
|
|||
addAtIndices:(NSArray *)addAtIndices
|
||||
removeAtIndices:(NSArray *)removeAtIndices)
|
||||
{
|
||||
RCTShadowView *container = _shadowViewRegistry[containerReactTag];
|
||||
NSUInteger offset = 0;
|
||||
NSNumber *containerSuperviewReactTag = [self containerReactTag:containerReactTag offset:&offset];
|
||||
|
||||
RCTAssert(moveFromIndices.count == moveToIndices.count, @"Invalid argument: moveFromIndices.count != moveToIndices.count");
|
||||
if (moveFromIndices.count > 0) {
|
||||
NSMutableArray *mutableAddChildReactTags = [addChildReactTags mutableCopy];
|
||||
NSMutableArray *mutableAddAtIndices = [addAtIndices mutableCopy];
|
||||
NSMutableArray *mutableRemoveAtIndices = [removeAtIndices mutableCopy];
|
||||
|
||||
NSArray *containerSubviews = [container reactSubviews];
|
||||
for (NSUInteger i = 0, count = moveFromIndices.count; i < count; i++) {
|
||||
NSNumber *from = moveFromIndices[i];
|
||||
NSNumber *to = moveToIndices[i];
|
||||
[mutableAddChildReactTags addObject:[containerSubviews[from.unsignedIntegerValue] reactTag]];
|
||||
[mutableAddAtIndices addObject:to];
|
||||
[mutableRemoveAtIndices addObject:from];
|
||||
}
|
||||
|
||||
addChildReactTags = mutableAddChildReactTags;
|
||||
addAtIndices = mutableAddAtIndices;
|
||||
removeAtIndices = mutableRemoveAtIndices;
|
||||
}
|
||||
|
||||
NSMutableArray *mutableAddChildReactTags;
|
||||
NSMutableArray *mutableAddAtIndices;
|
||||
NSMutableArray *mutableRemoveAtIndices;
|
||||
|
||||
if (containerSuperviewReactTag) {
|
||||
mutableAddChildReactTags = [addChildReactTags mutableCopy];
|
||||
mutableAddAtIndices = [addAtIndices mutableCopy];
|
||||
mutableRemoveAtIndices = [removeAtIndices mutableCopy];
|
||||
|
||||
[self modifyManageChildren:containerReactTag
|
||||
addChildReactTags:mutableAddChildReactTags
|
||||
addAtIndices:mutableAddAtIndices
|
||||
removeAtIndices:mutableRemoveAtIndices];
|
||||
|
||||
if (offset > 0) {
|
||||
NSUInteger count = MAX(mutableAddAtIndices.count, mutableRemoveAtIndices.count);
|
||||
for (NSUInteger i = 0; i < count; i++) {
|
||||
if (i < mutableAddAtIndices.count) {
|
||||
NSUInteger index = [mutableAddAtIndices[i] unsignedIntegerValue];
|
||||
mutableAddAtIndices[i] = @(index + offset);
|
||||
}
|
||||
|
||||
if (i < mutableRemoveAtIndices.count) {
|
||||
NSUInteger index = [mutableRemoveAtIndices[i] unsignedIntegerValue];
|
||||
mutableRemoveAtIndices[i] = @(index + offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[self _manageChildren:containerReactTag
|
||||
moveFromIndices:moveFromIndices
|
||||
moveToIndices:moveToIndices
|
||||
addChildReactTags:addChildReactTags
|
||||
addAtIndices:addAtIndices
|
||||
removeAtIndices:removeAtIndices
|
||||
registry:_shadowViewRegistry];
|
||||
|
||||
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
|
||||
[uiManager _manageChildren:containerReactTag
|
||||
moveFromIndices:moveFromIndices
|
||||
moveToIndices:moveToIndices
|
||||
addChildReactTags:addChildReactTags
|
||||
addAtIndices:addAtIndices
|
||||
removeAtIndices:removeAtIndices
|
||||
registry:viewRegistry];
|
||||
}];
|
||||
if (containerSuperviewReactTag) {
|
||||
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
|
||||
(void)(id []){containerReactTag, @(offset), addChildReactTags, addAtIndices, removeAtIndices};
|
||||
[uiManager _manageChildren:containerSuperviewReactTag
|
||||
addChildReactTags:mutableAddChildReactTags
|
||||
addAtIndices:mutableAddAtIndices
|
||||
removeAtIndices:mutableRemoveAtIndices
|
||||
registry:viewRegistry];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_manageChildren:(NSNumber *)containerReactTag
|
||||
moveFromIndices:(NSArray *)moveFromIndices
|
||||
moveToIndices:(NSArray *)moveToIndices
|
||||
addChildReactTags:(NSArray *)addChildReactTags
|
||||
addAtIndices:(NSArray *)addAtIndices
|
||||
removeAtIndices:(NSArray *)removeAtIndices
|
||||
registry:(RCTSparseArray *)registry
|
||||
{
|
||||
id<RCTViewNodeProtocol> container = registry[containerReactTag];
|
||||
RCTAssert(moveFromIndices.count == moveToIndices.count, @"moveFromIndices had size %tu, moveToIndices had size %tu", moveFromIndices.count, moveToIndices.count);
|
||||
RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one React child to add");
|
||||
RCTAssert(addChildReactTags.count == addAtIndices.count, @"Invalid arguments: addChildReactTags.count == addAtIndices.count");
|
||||
|
||||
// Removes (both permanent and temporary moves) are using "before" indices
|
||||
NSArray *permanentlyRemovedChildren = [self _childrenToRemoveFromContainer:container atIndices:removeAtIndices];
|
||||
NSArray *temporarilyRemovedChildren = [self _childrenToRemoveFromContainer:container atIndices:moveFromIndices];
|
||||
[self _removeChildren:permanentlyRemovedChildren fromContainer:container];
|
||||
[self _removeChildren:temporarilyRemovedChildren fromContainer:container];
|
||||
// Removes are using "before" indices
|
||||
NSArray *removedChildren = [self _childrenToRemoveFromContainer:container atIndices:removeAtIndices];
|
||||
[self _removeChildren:removedChildren fromContainer:container];
|
||||
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT reactTag in %@", addChildReactTags];
|
||||
NSArray *permanentlyRemovedChildren = [removedChildren filteredArrayUsingPredicate:predicate];
|
||||
[self _purgeChildren:permanentlyRemovedChildren fromRegistry:registry];
|
||||
|
||||
// TODO (#5906496): optimize all these loops - constantly calling array.count is not efficient
|
||||
|
||||
// Figure out what to insert - merge temporary inserts and adds
|
||||
NSMutableDictionary *destinationsToChildrenToAdd = [NSMutableDictionary dictionary];
|
||||
for (NSInteger index = 0, length = temporarilyRemovedChildren.count; index < length; index++) {
|
||||
destinationsToChildrenToAdd[moveToIndices[index]] = temporarilyRemovedChildren[index];
|
||||
}
|
||||
for (NSInteger index = 0, length = addAtIndices.count; index < length; index++) {
|
||||
id view = registry[addChildReactTags[index]];
|
||||
// Figure out what to insert
|
||||
NSMutableDictionary *childrenToAdd = [NSMutableDictionary dictionary];
|
||||
for (NSInteger index = 0, count = addAtIndices.count; index < count; index++) {
|
||||
id<RCTViewNodeProtocol> view = registry[addChildReactTags[index]];
|
||||
if (view) {
|
||||
destinationsToChildrenToAdd[addAtIndices[index]] = view;
|
||||
childrenToAdd[addAtIndices[index]] = view;
|
||||
}
|
||||
}
|
||||
|
||||
NSArray *sortedIndices = [[destinationsToChildrenToAdd allKeys] sortedArrayUsingSelector:@selector(compare:)];
|
||||
NSArray *sortedIndices = [[childrenToAdd allKeys] sortedArrayUsingSelector:@selector(compare:)];
|
||||
for (NSNumber *reactIndex in sortedIndices) {
|
||||
[container insertReactSubview:destinationsToChildrenToAdd[reactIndex] atIndex:reactIndex.integerValue];
|
||||
[container insertReactSubview:childrenToAdd[reactIndex] atIndex:reactIndex.integerValue];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -836,45 +1032,72 @@ RCT_EXPORT_METHOD(createView:(NSNumber *)reactTag
|
|||
// Set properties
|
||||
shadowView.viewName = viewName;
|
||||
shadowView.reactTag = reactTag;
|
||||
shadowView.allProps = props;
|
||||
RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[viewName], manager);
|
||||
}
|
||||
_shadowViewRegistry[reactTag] = shadowView;
|
||||
|
||||
// Shadow view is the source of truth for background color this is a little
|
||||
// bit counter-intuitive if people try to set background color when setting up
|
||||
// the view, but it's the only way that makes sense given our threading model
|
||||
UIColor *backgroundColor = shadowView.backgroundColor;
|
||||
if (!shadowView.layoutOnly) {
|
||||
// Shadow view is the source of truth for background color this is a little
|
||||
// bit counter-intuitive if people try to set background color when setting up
|
||||
// the view, but it's the only way that makes sense given our threading model
|
||||
UIColor *backgroundColor = shadowView.backgroundColor;
|
||||
[self addUIBlock:^(RCTUIManager *uiManager, __unused RCTSparseArray *viewRegistry) {
|
||||
[uiManager createView:reactTag viewName:viewName props:props withManager:manager backgroundColor:backgroundColor];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
|
||||
RCTAssertMainThread();
|
||||
- (UIView *)createView:(NSNumber *)reactTag viewName:(NSString *)viewName props:(NSDictionary *)props withManager:(RCTViewManager *)manager backgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
RCTAssertMainThread();
|
||||
UIView *view = [manager view];
|
||||
if (!view) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
UIView *view = [manager view];
|
||||
if (view) {
|
||||
// Generate default view, used for resetting default props
|
||||
if (!_defaultViews[viewName]) {
|
||||
// Note the default is setup after the props are read for the first time
|
||||
// ever for this className - this is ok because we only use the default
|
||||
// for restoring defaults, which never happens on first creation.
|
||||
_defaultViews[viewName] = [manager view];
|
||||
}
|
||||
|
||||
// Generate default view, used for resetting default props
|
||||
if (!uiManager->_defaultViews[viewName]) {
|
||||
// Note the default is setup after the props are read for the first time
|
||||
// ever for this className - this is ok because we only use the default
|
||||
// for restoring defaults, which never happens on first creation.
|
||||
uiManager->_defaultViews[viewName] = [manager view];
|
||||
}
|
||||
// Set properties
|
||||
view.reactTag = reactTag;
|
||||
view.backgroundColor = backgroundColor;
|
||||
if ([view isKindOfClass:[UIView class]]) {
|
||||
view.multipleTouchEnabled = YES;
|
||||
view.userInteractionEnabled = YES; // required for touch handling
|
||||
view.layer.allowsGroupOpacity = YES; // required for touch handling
|
||||
}
|
||||
RCTSetViewProps(props, view, _defaultViews[viewName], manager);
|
||||
|
||||
// Set properties
|
||||
view.reactTag = reactTag;
|
||||
view.backgroundColor = backgroundColor;
|
||||
if ([view isKindOfClass:[UIView class]]) {
|
||||
view.multipleTouchEnabled = YES;
|
||||
view.userInteractionEnabled = YES; // required for touch handling
|
||||
view.layer.allowsGroupOpacity = YES; // required for touch handling
|
||||
}
|
||||
RCTSetViewProps(props, view, uiManager->_defaultViews[viewName], manager);
|
||||
if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) {
|
||||
[_bridgeTransactionListeners addObject:view];
|
||||
}
|
||||
_viewRegistry[reactTag] = view;
|
||||
|
||||
if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) {
|
||||
[uiManager->_bridgeTransactionListeners addObject:view];
|
||||
}
|
||||
}
|
||||
viewRegistry[reactTag] = view;
|
||||
}];
|
||||
return view;
|
||||
}
|
||||
|
||||
NS_INLINE BOOL RCTRectIsDefined(CGRect frame)
|
||||
{
|
||||
return !(isnan(frame.origin.x) || isnan(frame.origin.y) || isnan(frame.size.width) || isnan(frame.size.height));
|
||||
}
|
||||
|
||||
NS_INLINE NSDictionary *RCTShadowViewOnLayoutEventPayload(NSNumber *reactTag, CGRect frame)
|
||||
{
|
||||
return @{
|
||||
@"target": reactTag,
|
||||
@"layout": @{
|
||||
@"x": @(frame.origin.x),
|
||||
@"y": @(frame.origin.y),
|
||||
@"width": @(frame.size.width),
|
||||
@"height": @(frame.size.height),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: remove viewName param as it isn't needed
|
||||
|
@ -888,10 +1111,100 @@ RCT_EXPORT_METHOD(updateView:(NSNumber *)reactTag
|
|||
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
|
||||
RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[viewName], viewManager);
|
||||
|
||||
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
|
||||
UIView *view = viewRegistry[reactTag];
|
||||
RCTSetViewProps(props, view, uiManager->_defaultViews[viewName], viewManager);
|
||||
}];
|
||||
const BOOL wasLayoutOnly = shadowView.layoutOnly;
|
||||
NSDictionary *newProps = RCTPropsMerge(shadowView.allProps, props);
|
||||
shadowView.allProps = newProps;
|
||||
|
||||
const BOOL isLayoutOnly = shadowView.layoutOnly;
|
||||
|
||||
if (wasLayoutOnly != isLayoutOnly) {
|
||||
// Add/remove node
|
||||
|
||||
if (isLayoutOnly) {
|
||||
[self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
|
||||
RCTAssertMainThread();
|
||||
|
||||
UIView *container = viewRegistry[reactTag];
|
||||
|
||||
const CGRect containerFrame = container.frame;
|
||||
const CGFloat deltaX = containerFrame.origin.x;
|
||||
const CGFloat deltaY = containerFrame.origin.y;
|
||||
|
||||
NSUInteger offset = [container.superview.subviews indexOfObject:container];
|
||||
[container.subviews enumerateObjectsUsingBlock:^(UIView *subview, NSUInteger idx, __unused BOOL *stop) {
|
||||
[container removeReactSubview:subview];
|
||||
|
||||
CGRect subviewFrame = subview.frame;
|
||||
subviewFrame.origin.x += deltaX;
|
||||
subviewFrame.origin.y += deltaY;
|
||||
subview.frame = subviewFrame;
|
||||
|
||||
[container.superview insertReactSubview:subview atIndex:idx + offset];
|
||||
}];
|
||||
|
||||
[container.superview removeReactSubview:container];
|
||||
if ([container conformsToProtocol:@protocol(RCTInvalidating)]) {
|
||||
[(id<RCTInvalidating>)container invalidate];
|
||||
}
|
||||
|
||||
viewRegistry[reactTag] = nil;
|
||||
}];
|
||||
} else {
|
||||
NSMutableArray *mutableAddChildReactTags = [[[shadowView reactSubviews] valueForKey:@"reactTag"] mutableCopy];
|
||||
NSMutableArray *mutableAddAtIndices = [NSMutableArray arrayWithCapacity:mutableAddChildReactTags.count];
|
||||
for (NSUInteger i = 0, count = mutableAddChildReactTags.count; i < count; i++) {
|
||||
[mutableAddAtIndices addObject:@(i)];
|
||||
}
|
||||
|
||||
[self modifyManageChildren:reactTag
|
||||
addChildReactTags:mutableAddChildReactTags
|
||||
addAtIndices:mutableAddAtIndices
|
||||
removeAtIndices:nil];
|
||||
|
||||
NSUInteger offset = [shadowView.superview.reactSubviews indexOfObject:shadowView];
|
||||
NSNumber *containerSuperviewReactTag = [self containerReactTag:shadowView.superview.reactTag offset:&offset];
|
||||
UIColor *backgroundColor = shadowView.backgroundColor;
|
||||
|
||||
CGRect shadowViewFrame = shadowView.adjustedFrame;
|
||||
NSMutableDictionary *newFrames = [NSMutableDictionary dictionaryWithCapacity:mutableAddChildReactTags.count];
|
||||
for (NSNumber *childTag in mutableAddChildReactTags) {
|
||||
RCTShadowView *child = _shadowViewRegistry[childTag];
|
||||
newFrames[childTag] = [NSValue valueWithCGRect:child.adjustedFrame];
|
||||
}
|
||||
|
||||
[self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
|
||||
RCTAssertMainThread();
|
||||
|
||||
UIView *containerSuperview = viewRegistry[containerSuperviewReactTag];
|
||||
UIView *container = [uiManager createView:reactTag viewName:viewName props:newProps withManager:viewManager backgroundColor:backgroundColor];
|
||||
|
||||
[containerSuperview insertReactSubview:container atIndex:offset];
|
||||
if (RCTRectIsDefined(shadowViewFrame)) {
|
||||
container.frame = shadowViewFrame;
|
||||
}
|
||||
|
||||
for (NSUInteger i = 0, count = mutableAddAtIndices.count; i < count; i++) {
|
||||
NSNumber *tag = mutableAddChildReactTags[i];
|
||||
UIView *subview = viewRegistry[tag];
|
||||
[containerSuperview removeReactSubview:subview];
|
||||
|
||||
NSUInteger atIndex = [mutableAddAtIndices[i] unsignedIntegerValue];
|
||||
[container insertReactSubview:subview atIndex:atIndex];
|
||||
|
||||
CGRect subviewFrame = [newFrames[tag] CGRectValue];
|
||||
if (RCTRectIsDefined(subviewFrame)) {
|
||||
subview.frame = subviewFrame;
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
} else if (!isLayoutOnly) {
|
||||
// Update node
|
||||
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
|
||||
UIView *view = viewRegistry[reactTag];
|
||||
RCTSetViewProps(props, view, uiManager->_defaultViews[viewName], viewManager);
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(focus:(NSNumber *)reactTag)
|
||||
|
@ -1227,12 +1540,16 @@ RCT_EXPORT_METHOD(zoomToRect:(NSNumber *)reactTag
|
|||
RCT_EXPORT_METHOD(setJSResponder:(NSNumber *)reactTag
|
||||
blockNativeResponder:(__unused BOOL)blockNativeResponder)
|
||||
{
|
||||
[self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
|
||||
_jsResponder = viewRegistry[reactTag];
|
||||
if (!_jsResponder) {
|
||||
RCTLogError(@"Invalid view set to be the JS responder - tag %zd", reactTag);
|
||||
}
|
||||
}];
|
||||
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
|
||||
if (!shadowView) {
|
||||
RCTLogError(@"Invalid view set to be the JS responder - tag %@", reactTag);
|
||||
} else if (shadowView.layoutOnly) {
|
||||
RCTLogError(@"Cannot set JS responder to layout-only view with tag %@ - %@, %@", reactTag, shadowView, shadowView.allProps);
|
||||
} else {
|
||||
[self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
|
||||
_jsResponder = viewRegistry[reactTag];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(clearJSResponder)
|
||||
|
@ -1461,8 +1778,8 @@ RCT_EXPORT_METHOD(configureNextLayoutAnimation:(NSDictionary *)config
|
|||
withCallback:(RCTResponseSenderBlock)callback
|
||||
errorCallback:(__unused RCTResponseSenderBlock)errorCallback)
|
||||
{
|
||||
if (_nextLayoutAnimation) {
|
||||
RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", _nextLayoutAnimation, config);
|
||||
if (_nextLayoutAnimation && ![config isEqualToDictionary:_nextLayoutAnimation.config]) {
|
||||
RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", _nextLayoutAnimation.config, config);
|
||||
}
|
||||
if (config[@"delete"] != nil) {
|
||||
RCTLogError(@"LayoutAnimation only supports create and update right now. Config: %@", config);
|
||||
|
@ -1488,3 +1805,27 @@ static UIView *_jsResponder;
|
|||
}
|
||||
|
||||
@end
|
||||
|
||||
static void RCTTraverseViewNodes(id<RCTViewNodeProtocol> view, void (^block)(id<RCTViewNodeProtocol>))
|
||||
{
|
||||
if (view.reactTag) block(view);
|
||||
for (id<RCTViewNodeProtocol> subview in view.reactSubviews) {
|
||||
RCTTraverseViewNodes(subview, block);
|
||||
}
|
||||
}
|
||||
|
||||
static NSDictionary *RCTPropsMerge(NSDictionary *beforeProps, NSDictionary *newProps)
|
||||
{
|
||||
NSMutableDictionary *afterProps = [NSMutableDictionary dictionaryWithDictionary:beforeProps];
|
||||
|
||||
// Can't use -addEntriesFromDictionary: because we want to remove keys with NSNull values.
|
||||
[newProps enumerateKeysAndObjectsUsingBlock:^(id key, id obj, __unused BOOL *stop) {
|
||||
if (obj == (id)kCFNull) {
|
||||
[afterProps removeObjectForKey:key];
|
||||
} else {
|
||||
afterProps[key] = obj;
|
||||
}
|
||||
}];
|
||||
|
||||
return afterProps;
|
||||
}
|
||||
|
|
|
@ -34,8 +34,6 @@
|
|||
13B0801B1A69489C00A75B9A /* RCTNavigatorManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B0800F1A69489C00A75B9A /* RCTNavigatorManager.m */; };
|
||||
13B0801C1A69489C00A75B9A /* RCTNavItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080111A69489C00A75B9A /* RCTNavItem.m */; };
|
||||
13B0801D1A69489C00A75B9A /* RCTNavItemManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080131A69489C00A75B9A /* RCTNavItemManager.m */; };
|
||||
13B0801E1A69489C00A75B9A /* RCTTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080151A69489C00A75B9A /* RCTTextField.m */; };
|
||||
13B0801F1A69489C00A75B9A /* RCTTextFieldManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080171A69489C00A75B9A /* RCTTextFieldManager.m */; };
|
||||
13B080201A69489C00A75B9A /* RCTActivityIndicatorViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */; };
|
||||
13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */; };
|
||||
13C156051AB1A2840079392D /* RCTWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156021AB1A2840079392D /* RCTWebView.m */; };
|
||||
|
@ -152,10 +150,6 @@
|
|||
13B080111A69489C00A75B9A /* RCTNavItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNavItem.m; sourceTree = "<group>"; };
|
||||
13B080121A69489C00A75B9A /* RCTNavItemManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNavItemManager.h; sourceTree = "<group>"; };
|
||||
13B080131A69489C00A75B9A /* RCTNavItemManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNavItemManager.m; sourceTree = "<group>"; };
|
||||
13B080141A69489C00A75B9A /* RCTTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextField.h; sourceTree = "<group>"; };
|
||||
13B080151A69489C00A75B9A /* RCTTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextField.m; sourceTree = "<group>"; };
|
||||
13B080161A69489C00A75B9A /* RCTTextFieldManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextFieldManager.h; sourceTree = "<group>"; };
|
||||
13B080171A69489C00A75B9A /* RCTTextFieldManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextFieldManager.m; sourceTree = "<group>"; };
|
||||
13B080181A69489C00A75B9A /* RCTActivityIndicatorViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTActivityIndicatorViewManager.h; sourceTree = "<group>"; };
|
||||
13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTActivityIndicatorViewManager.m; sourceTree = "<group>"; };
|
||||
13B080231A694A8400A75B9A /* RCTWrapperViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWrapperViewController.h; sourceTree = "<group>"; };
|
||||
|
@ -241,6 +235,7 @@
|
|||
83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTouchHandler.m; sourceTree = "<group>"; };
|
||||
83CBBACA1A6023D300E9B192 /* RCTConvert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTConvert.h; sourceTree = "<group>"; };
|
||||
83CBBACB1A6023D300E9B192 /* RCTConvert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert.m; sourceTree = "<group>"; };
|
||||
E3BBC8EB1ADE6F47001BBD81 /* RCTTextDecorationLineType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTTextDecorationLineType.h; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -360,10 +355,6 @@
|
|||
137327E41AA5CF210034F82E /* RCTTabBarItemManager.m */,
|
||||
137327E51AA5CF210034F82E /* RCTTabBarManager.h */,
|
||||
137327E61AA5CF210034F82E /* RCTTabBarManager.m */,
|
||||
13B080141A69489C00A75B9A /* RCTTextField.h */,
|
||||
13B080151A69489C00A75B9A /* RCTTextField.m */,
|
||||
13B080161A69489C00A75B9A /* RCTTextFieldManager.h */,
|
||||
13B080171A69489C00A75B9A /* RCTTextFieldManager.m */,
|
||||
13E0674F1A70F44B002CDEE1 /* RCTView.h */,
|
||||
13E067501A70F44B002CDEE1 /* RCTView.m */,
|
||||
13442BF41AA90E0B0037E5B0 /* RCTViewControllerProtocol.h */,
|
||||
|
@ -378,6 +369,7 @@
|
|||
13B080241A694A8400A75B9A /* RCTWrapperViewController.m */,
|
||||
13E067531A70F44B002CDEE1 /* UIView+React.h */,
|
||||
13E067541A70F44B002CDEE1 /* UIView+React.m */,
|
||||
E3BBC8EB1ADE6F47001BBD81 /* RCTTextDecorationLineType.h */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
@ -544,7 +536,6 @@
|
|||
13456E961ADAD482009F94A7 /* RCTConvert+MapKit.m in Sources */,
|
||||
13723B501A82FD3C00F88898 /* RCTStatusBarManager.m in Sources */,
|
||||
000E6CEB1AB0E980000CDF4D /* RCTSourceCode.m in Sources */,
|
||||
13B0801E1A69489C00A75B9A /* RCTTextField.m in Sources */,
|
||||
14C2CA761B3AC64F00E6CBB2 /* RCTFrameUpdate.m in Sources */,
|
||||
13B07FEF1A69327A00A75B9A /* RCTAlertManager.m in Sources */,
|
||||
83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */,
|
||||
|
@ -573,7 +564,6 @@
|
|||
13B080051A6947C200A75B9A /* RCTScrollView.m in Sources */,
|
||||
13B07FF21A69327A00A75B9A /* RCTTiming.m in Sources */,
|
||||
1372B70A1AB030C200659ED6 /* RCTAppState.m in Sources */,
|
||||
13B0801F1A69489C00A75B9A /* RCTTextFieldManager.m in Sources */,
|
||||
134FCB3D1A6E7F0800051CC8 /* RCTContextExecutor.m in Sources */,
|
||||
14C2CA781B3ACB0400E6CBB2 /* RCTBatchedBridge.m in Sources */,
|
||||
13E067591A70F44B002CDEE1 /* UIView+React.m in Sources */,
|
||||
|
|
|
@ -79,7 +79,7 @@ RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)
|
|||
}
|
||||
}
|
||||
|
||||
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(RCTPointAnnotation *)annotation
|
||||
- (MKAnnotationView *)mapView:(__unused MKMapView *)mapView viewForAnnotation:(RCTPointAnnotation *)annotation
|
||||
{
|
||||
if ([annotation isKindOfClass:[MKUserLocation class]]) {
|
||||
return nil;
|
||||
|
|
|
@ -357,6 +357,10 @@ RCT_NOT_IMPLEMENTED(-init)
|
|||
|
||||
@end
|
||||
|
||||
@interface RCTScrollView (Private)
|
||||
- (NSArray *)calculateChildFramesData;
|
||||
@end
|
||||
|
||||
@implementation RCTScrollView
|
||||
{
|
||||
RCTEventDispatcher *_eventDispatcher;
|
||||
|
@ -533,6 +537,23 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove)
|
|||
(_scrollEventThrottle > 0 && _scrollEventThrottle < (now - _lastScrollDispatchTime))) {
|
||||
|
||||
// Calculate changed frames
|
||||
NSArray *childFrames = [self calculateChildFramesData];
|
||||
|
||||
// Dispatch event
|
||||
[_eventDispatcher sendScrollEventWithType:RCTScrollEventTypeMove
|
||||
reactTag:self.reactTag
|
||||
scrollView:scrollView
|
||||
userData:@{@"updatedChildFrames": childFrames}];
|
||||
|
||||
// Update dispatch time
|
||||
_lastScrollDispatchTime = now;
|
||||
_allowNextScrollNoMatterWhat = NO;
|
||||
}
|
||||
RCT_FORWARD_SCROLL_EVENT(scrollViewDidScroll:scrollView);
|
||||
}
|
||||
|
||||
- (NSArray *)calculateChildFramesData
|
||||
{
|
||||
NSMutableArray *updatedChildFrames = [[NSMutableArray alloc] init];
|
||||
[[_contentView reactSubviews] enumerateObjectsUsingBlock:
|
||||
^(UIView *subview, NSUInteger idx, __unused BOOL *stop) {
|
||||
|
@ -558,26 +579,9 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove)
|
|||
@"height": @(newFrame.size.height),
|
||||
}];
|
||||
}
|
||||
|
||||
}];
|
||||
|
||||
// If there are new frames, add them to event data
|
||||
NSDictionary *userData = nil;
|
||||
if (updatedChildFrames.count > 0) {
|
||||
userData = @{@"updatedChildFrames": updatedChildFrames};
|
||||
}
|
||||
|
||||
// Dispatch event
|
||||
[_eventDispatcher sendScrollEventWithType:RCTScrollEventTypeMove
|
||||
reactTag:self.reactTag
|
||||
scrollView:scrollView
|
||||
userData:userData];
|
||||
|
||||
// Update dispatch time
|
||||
_lastScrollDispatchTime = now;
|
||||
_allowNextScrollNoMatterWhat = NO;
|
||||
}
|
||||
RCT_FORWARD_SCROLL_EVENT(scrollViewDidScroll:scrollView);
|
||||
return updatedChildFrames;
|
||||
}
|
||||
|
||||
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
#import "RCTSparseArray.h"
|
||||
#import "RCTUIManager.h"
|
||||
|
||||
@interface RCTScrollView (Private)
|
||||
- (NSArray *)calculateChildFramesData;
|
||||
@end
|
||||
|
||||
@implementation RCTConvert (UIScrollView)
|
||||
|
||||
RCT_ENUM_CONVERTER(UIScrollViewKeyboardDismissMode, (@{
|
||||
|
@ -91,4 +95,23 @@ RCT_EXPORT_METHOD(getContentSize:(NSNumber *)reactTag
|
|||
}];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(calculateChildFrames:(NSNumber *)reactTag
|
||||
callback:(RCTResponseSenderBlock)callback)
|
||||
{
|
||||
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
|
||||
|
||||
UIView *view = viewRegistry[reactTag];
|
||||
if (!view) {
|
||||
RCTLogError(@"Cannot find view with tag #%@", reactTag);
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray *childFrames = [((RCTScrollView *)view) calculateChildFramesData];
|
||||
|
||||
if (childFrames) {
|
||||
callback(@[childFrames]);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -41,6 +41,12 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *viewRegistry);
|
|||
@property (nonatomic, assign) RCTUpdateLifecycle layoutLifecycle;
|
||||
@property (nonatomic, assign) BOOL hasOnLayout;
|
||||
|
||||
@property (nonatomic, assign, readonly, getter=isLayoutOnly) BOOL layoutOnly;
|
||||
@property (nonatomic, copy) NSDictionary *allProps;
|
||||
|
||||
/// `frame` adjusted for recursive superview `layoutOnly` status.
|
||||
@property (nonatomic, assign, readonly) CGRect adjustedFrame;
|
||||
|
||||
/**
|
||||
* isNewView - Used to track the first time the view is introduced into the hierarchy. It is initialized YES, then is
|
||||
* set to NO in RCTUIManager after the layout pass is done and all frames have been extracted to be applied to the
|
||||
|
|
|
@ -367,8 +367,10 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st
|
|||
- (NSString *)description
|
||||
{
|
||||
NSString *description = super.description;
|
||||
description = [[description substringToIndex:description.length - 1] stringByAppendingFormat:@"; viewName: %@; reactTag: %@; frame: %@>", self.viewName, self.reactTag, NSStringFromCGRect(self.frame)];
|
||||
return description;
|
||||
if (self.layoutOnly) {
|
||||
description = [@"* " stringByAppendingString:description];
|
||||
}
|
||||
return [[description substringToIndex:description.length - 1] stringByAppendingFormat:@"; viewName: %@; reactTag: %@; frame: %@>", self.viewName, self.reactTag, NSStringFromCGRect(self.frame)];
|
||||
}
|
||||
|
||||
- (void)addRecursiveDescriptionToString:(NSMutableString *)string atLevel:(NSUInteger)level
|
||||
|
@ -392,6 +394,91 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st
|
|||
return description;
|
||||
}
|
||||
|
||||
- (BOOL)isLayoutOnly
|
||||
{
|
||||
if (![self.viewName isEqualToString:@"RCTView"]) {
|
||||
// For now, only `RCTView`s can be layout-only.
|
||||
return NO;
|
||||
}
|
||||
|
||||
// dispatch_once is unnecessary because this property SHOULD only be accessed
|
||||
// on the shadow queue
|
||||
static NSSet *layoutKeys;
|
||||
static NSSet *specialKeys;
|
||||
if (!layoutKeys || !specialKeys) {
|
||||
// Taken from LayoutPropTypes.js with the exception that borderWidth,
|
||||
// borderTopWidth, borderBottomWidth, borderLeftWidth, and borderRightWidth
|
||||
// were removed because black color is assumed
|
||||
static NSString *const layoutKeyStrings[] = {
|
||||
@"width",
|
||||
@"height",
|
||||
@"top",
|
||||
@"left",
|
||||
@"right",
|
||||
@"bottom",
|
||||
@"margin",
|
||||
@"marginVertical",
|
||||
@"marginHorizontal",
|
||||
@"marginTop",
|
||||
@"marginBottom",
|
||||
@"marginLeft",
|
||||
@"marginRight",
|
||||
@"padding",
|
||||
@"paddingVertical",
|
||||
@"paddingHorizontal",
|
||||
@"paddingTop",
|
||||
@"paddingBottom",
|
||||
@"paddingLeft",
|
||||
@"paddingRight",
|
||||
@"position",
|
||||
@"flexDirection",
|
||||
@"flexWrap",
|
||||
@"justifyContent",
|
||||
@"alignItems",
|
||||
@"alignSelf",
|
||||
@"flex",
|
||||
};
|
||||
layoutKeys = [NSSet setWithObjects:layoutKeyStrings count:sizeof(layoutKeyStrings)/sizeof(*layoutKeyStrings)];
|
||||
|
||||
static NSString *const specialKeyStrings[] = {
|
||||
@"accessible",
|
||||
@"collapsible",
|
||||
};
|
||||
specialKeys = [NSSet setWithObjects:specialKeyStrings count:sizeof(specialKeyStrings)/sizeof(*specialKeyStrings)];
|
||||
}
|
||||
|
||||
NSNumber *collapsible = self.allProps[@"collapsible"];
|
||||
if (collapsible && !collapsible.boolValue) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSNumber *accessible = self.allProps[@"accessible"];
|
||||
if (accessible && accessible.boolValue) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
for (NSString *key in self.allProps) {
|
||||
if (![specialKeys containsObject:key] && ![layoutKeys containsObject:key]) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (CGRect)adjustedFrame
|
||||
{
|
||||
CGRect frame = self.frame;
|
||||
RCTShadowView *superview = self;
|
||||
while ((superview = superview.superview) && superview.layoutOnly) {
|
||||
const CGRect superviewFrame = superview.frame;
|
||||
frame.origin.x += superviewFrame.origin.x;
|
||||
frame.origin.y += superviewFrame.origin.y;
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
// Margin
|
||||
|
||||
#define RCT_MARGIN_PROPERTY(prop, metaProp) \
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef NS_ENUM(NSInteger, RCTTextDecorationLineType) {
|
||||
RCTTextDecorationLineTypeNone = 0,
|
||||
RCTTextDecorationLineTypeUnderline,
|
||||
RCTTextDecorationLineTypeStrikethrough,
|
||||
RCTTextDecorationLineTypeUnderlineStrikethrough,
|
||||
};
|
|
@ -15,10 +15,11 @@
|
|||
@protocol RCTViewNodeProtocol <NSObject>
|
||||
|
||||
@property (nonatomic, copy) NSNumber *reactTag;
|
||||
@property (nonatomic, assign) CGRect frame;
|
||||
|
||||
- (void)insertReactSubview:(id<RCTViewNodeProtocol>)subview atIndex:(NSInteger)atIndex;
|
||||
- (void)removeReactSubview:(id<RCTViewNodeProtocol>)subview;
|
||||
- (NSMutableArray *)reactSubviews;
|
||||
- (NSArray *)reactSubviews;
|
||||
- (id<RCTViewNodeProtocol>)reactSuperview;
|
||||
- (NSNumber *)reactTagAtPoint:(CGPoint)point;
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ extern NSString *const RCTJSNavigationScheme;
|
|||
@property (nonatomic, strong) NSURL *URL;
|
||||
@property (nonatomic, assign) UIEdgeInsets contentInset;
|
||||
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
|
||||
@property (nonatomic, copy) NSString *injectedJavascriptIOS;
|
||||
@property (nonatomic, copy) NSString *injectedJavaScript;
|
||||
|
||||
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ NSString *const RCTJSNavigationScheme = @"react-js-navigation";
|
|||
{
|
||||
RCTEventDispatcher *_eventDispatcher;
|
||||
UIWebView *_webView;
|
||||
NSString *_injectedJavascriptIOS;
|
||||
NSString *_injectedJavaScript;
|
||||
}
|
||||
|
||||
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
|
||||
|
@ -126,19 +126,6 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
|
|||
return _webView.backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setinjectedJavascriptIOS:(NSString *)jsStr
|
||||
{
|
||||
if (_injectedJavascriptIOS == jsStr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ([_injectedJavascriptIOS isEqualToString:jsStr]) {
|
||||
return;
|
||||
}
|
||||
|
||||
_injectedJavascriptIOS = [jsStr copy];
|
||||
}
|
||||
|
||||
- (NSMutableDictionary *)baseEvent
|
||||
{
|
||||
NSURL *url = _webView.request.URL;
|
||||
|
@ -197,8 +184,8 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
|
|||
|
||||
- (void)webViewDidFinishLoad:(UIWebView *)webView
|
||||
{
|
||||
if (_injectedJavascriptIOS != nil) {
|
||||
[webView stringByEvaluatingJavaScriptFromString:_injectedJavascriptIOS];
|
||||
if (_injectedJavaScript != nil) {
|
||||
[webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript];
|
||||
}
|
||||
|
||||
// we only need the final 'finishLoad' call so only fire the event when we're actually done loading.
|
||||
|
|
|
@ -28,7 +28,7 @@ RCT_REMAP_VIEW_PROPERTY(html, HTML, NSString);
|
|||
RCT_REMAP_VIEW_PROPERTY(bounces, _webView.scrollView.bounces, BOOL);
|
||||
RCT_REMAP_VIEW_PROPERTY(scrollEnabled, _webView.scrollView.scrollEnabled, BOOL);
|
||||
RCT_REMAP_VIEW_PROPERTY(scalesPageToFit, _webView.scalesPageToFit, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(injectedJavascriptIOS, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets);
|
||||
RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL);
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ describe('setup Podfile', function() {
|
|||
|
||||
it('creates a Podfile if none exists', function() {
|
||||
try {
|
||||
fs.unlinkSync(path.resolve(__dirname, 'Podfile'));
|
||||
fs.unlinkSync(path.resolve(__dirname, '../Podfile'));
|
||||
} catch(e) {}
|
||||
|
||||
var setupPodfile = install.setupPodfile();
|
||||
|
@ -37,8 +37,11 @@ describe('setup Podfile', function() {
|
|||
expect(setupPodfile.podfileText).toContain(openingReactTag);
|
||||
expect(setupPodfile.podfileText).toContain(closingReactTag);
|
||||
|
||||
// cleanup
|
||||
try {
|
||||
fs.unlinkSync(path.resolve(__dirname, 'Podfile'));
|
||||
} catch(e) {}
|
||||
fs.unlinkSync(path.resolve(__dirname, '../Podfile'));
|
||||
} catch(e) {
|
||||
throw new Error('failed to cleanup Podfile', e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
"debug": "~2.1.0",
|
||||
"graceful-fs": "^3.0.6",
|
||||
"image-size": "0.3.5",
|
||||
"immutable": "^3.7.4",
|
||||
"joi": "~5.1.0",
|
||||
"jstransform": "11.0.1",
|
||||
"module-deps": "3.5.6",
|
||||
|
|