Updates from Fri Feb 13

- [ReactNative] Fix throttle warning and warn in callback instead of render | Christopher Chedeau
- [react-packager][streamline oss] Remove react-page-middleware | Amjad Masad
- [ReactNative] Turn on perf measurement around a group feed load | Jing Chen
- Implemented Layout animations | Nick Lockwood
- [ReactNative] Revert D1815137 - avoid dropping touch start on missing target | Eric Vicenti
- Moved RKPOPAnimationManager into FBReactKitComponents | Nick Lockwood
- Extracted RKAnimationManager | Nick Lockwood
This commit is contained in:
Spencer Ahrens 2015-02-18 17:39:09 -08:00
parent 472c287cd3
commit ef842c285b
24 changed files with 1344 additions and 237 deletions

View File

@ -0,0 +1,250 @@
/**
* Copyright 2004-present Facebook. All Rights Reserved.
*
* @providesModule ListViewPagingExample
* @flow
*/
'use strict';
var React = require('react-native');
var {
Image,
LayoutAnimation,
ListView,
ListViewDataSource,
StyleSheet,
Text,
TouchableOpacity,
View,
} = React;
var PAGE_SIZE = 4;
var THUMB_URLS = ['https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-ash3/t39.1997/p128x128/851549_767334479959628_274486868_n.png', 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851561_767334496626293_1958532586_n.png', 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-ash3/t39.1997/p128x128/851579_767334503292959_179092627_n.png', 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851589_767334513292958_1747022277_n.png', 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851563_767334559959620_1193692107_n.png', 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851593_767334566626286_1953955109_n.png', 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851591_767334523292957_797560749_n.png', 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851567_767334529959623_843148472_n.png', 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851548_767334489959627_794462220_n.png', 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851575_767334539959622_441598241_n.png', 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-ash3/t39.1997/p128x128/851573_767334549959621_534583464_n.png', 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851583_767334573292952_1519550680_n.png'];
var NUM_SECTIONS = 100;
var NUM_ROWS_PER_SECTION = 10;
var Thumb = React.createClass({
getInitialState: function() {
return {thumbIndex: this._getThumbIdx(), dir: 'row'};
},
_getThumbIdx: function() {
return Math.floor(Math.random() * THUMB_URLS.length);
},
_onPressThumb: function() {
var config = layoutAnimationConfigs[this.state.thumbIndex % layoutAnimationConfigs.length];
LayoutAnimation.configureNext(config);
this.setState({
thumbIndex: this._getThumbIdx(),
dir: this.state.dir === 'row' ? 'column' : 'row',
});
},
render: function() {
return (
<TouchableOpacity onPress={this._onPressThumb}>
<View style={[styles.buttonContents, {flexDirection: this.state.dir}]}>
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
{this.state.dir === 'column' ?
<Text>
Oooo, look at this new text! So awesome it may just be crazy.
Let me keep typing here so it wraps at least one line.
</Text> :
<Text />
}
</View>
</TouchableOpacity>
);
}
});
var ListViewPagingExample = React.createClass({
statics: {
title: '<ListView> - Paging',
description: 'Floating headers & layout animations.'
},
getInitialState: function() {
var getSectionData = (dataBlob, sectionID) => {
return dataBlob[sectionID];
};
var getRowData = (dataBlob, sectionID, rowID) => {
return dataBlob[rowID];
};
var dataSource = new ListViewDataSource({
getRowData: getRowData,
getSectionHeaderData: getSectionData,
rowHasChanged: (row1, row2) => row1 !== row2,
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
});
var dataBlob = {};
var sectionIDs = [];
var rowIDs = [];
for (var ii = 0; ii < NUM_SECTIONS; ii++) {
var sectionName = 'Section ' + ii;
sectionIDs.push(sectionName);
dataBlob[sectionName] = sectionName;
rowIDs[ii] = [];
for (var jj = 0; jj < NUM_ROWS_PER_SECTION; jj++) {
var rowName = 'S' + ii + ', R' + jj;
rowIDs[ii].push(rowName);
dataBlob[rowName] = rowName;
}
}
return {
dataSource: dataSource.cloneWithRowsAndSections(dataBlob, sectionIDs, rowIDs),
headerPressCount: 0,
};
},
renderRow: function(rowData, sectionID, rowID) {
return (<Thumb text={rowData}/>);
},
renderSectionHeader: function(sectionData, sectionID) {
return (
<View style={styles.section}>
<Text style={styles.text}>
{sectionData}
</Text>
</View>
);
},
renderHeader: function() {
var headerLikeText = this.state.headerPressCount % 2 ?
<View><Text style={styles.text}>1 Like</Text></View> :
null;
return (
<TouchableOpacity onPress={this._onPressHeader}>
<View style={styles.header}>
{headerLikeText}
<View>
<Text style={styles.text}>
Table Header (click me)
</Text>
</View>
</View>
</TouchableOpacity>
);
},
renderFooter: function() {
return (
<View style={styles.header}>
<Text onPress={() => console.log('Footer!')} style={styles.text}>
Table Footer
</Text>
</View>
);
},
render: function() {
return (
<ListView
style={styles.listview}
dataSource={this.state.dataSource}
onChangeVisibleRows={(visibleRows, changedRows) => console.log({visibleRows, changedRows})}
renderHeader={this.renderHeader}
renderFooter={this.renderFooter}
renderSectionHeader={this.renderSectionHeader}
renderRow={this.renderRow}
initialListSize={10}
pageSize={PAGE_SIZE}
scrollRenderAheadDistance={2000}
/>
);
},
_onPressHeader: function() {
var config = layoutAnimationConfigs[Math.floor(this.state.headerPressCount / 2) % layoutAnimationConfigs.length];
LayoutAnimation.configureNext(config);
this.setState({headerPressCount: this.state.headerPressCount + 1});
},
});
var styles = StyleSheet.create({
listview: {
backgroundColor: '#B0C4DE',
},
header: {
height: 40,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#3B5998',
flexDirection: 'row',
},
text: {
color: 'white',
paddingHorizontal: 8,
},
rowText: {
color: '#888888',
},
thumbText: {
fontSize: 20,
color: '#888888',
},
buttonContents: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 5,
marginVertical: 3,
padding: 5,
backgroundColor: '#EAEAEA',
borderRadius: 3,
paddingVertical: 10,
},
img: {
width: 64,
height: 64,
marginHorizontal: 10,
},
section: {
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'flex-start',
padding: 6,
backgroundColor: '#5890ff',
},
});
var animations = {
layout: {
spring: {
duration: 0.75,
create: {
duration: 0.3,
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
},
update: {
type: LayoutAnimation.Types.spring,
springDamping: 0.4,
},
},
easeInEaseOut: {
duration: 0.3,
create: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.scaleXY,
},
update: {
delay: 0.1,
type: LayoutAnimation.Types.easeInEaseOut,
},
},
},
};
var layoutAnimationConfigs = [
animations.layout.spring,
animations.layout.easeInEaseOut,
];
module.exports = ListViewPagingExample;

View File

@ -24,6 +24,7 @@ var EXAMPLES = [
require('./ExpandingTextExample'), require('./ExpandingTextExample'),
require('./ImageExample'), require('./ImageExample'),
require('./ListViewSimpleExample'), require('./ListViewSimpleExample'),
require('./ListViewPagingExample'),
require('./NavigatorIOSExample'), require('./NavigatorIOSExample'),
require('./StatusBarIOSExample'), require('./StatusBarIOSExample'),
require('./PointerEventsExample'), require('./PointerEventsExample'),

View File

@ -0,0 +1,89 @@
/**
* Copyright 2004-present Facebook. All Rights Reserved.
*
* @providesModule LayoutAnimation
*/
'use strict';
var PropTypes = require('ReactPropTypes');
var RKUIManager = require('NativeModules').RKUIManager;
var createStrictShapeTypeChecker = require('createStrictShapeTypeChecker');
var keyMirror = require('keyMirror');
var Types = keyMirror({
spring: true,
linear: true,
easeInEaseOut: true,
easeIn: true,
easeOut: true,
});
var Properties = keyMirror({
opacity: true,
scaleXY: true,
});
var animChecker = createStrictShapeTypeChecker({
duration: PropTypes.number,
delay: PropTypes.number,
springDamping: PropTypes.number,
initialVelocity: PropTypes.number,
type: PropTypes.oneOf(
Object.keys(Types)
),
property: PropTypes.oneOf( // Only applies to create/delete
Object.keys(Properties)
),
});
var configChecker = createStrictShapeTypeChecker({
duration: PropTypes.number.isRequired,
create: animChecker,
update: animChecker,
delete: animChecker,
});
var LayoutAnimation = {
configureNext(config, onAnimationDidEnd, onError) {
configChecker({config}, 'config', 'LayoutAnimation.configureNext');
RKUIManager.configureNextLayoutAnimation(config, onAnimationDidEnd, onError);
},
create(duration, type, creationProp) {
return {
duration,
create: {
type,
property: creationProp,
},
update: {
type,
},
};
},
Types: Types,
Properties: Properties,
configChecker: configChecker,
};
LayoutAnimation.Presets = {
easeInEaseOut: LayoutAnimation.create(
0.3, Types.easeInEaseOut, Properties.opacity
),
linear: LayoutAnimation.create(
0.5, Types.linear, Properties.opacity
),
spring: {
duration: 0.7,
create: {
type: Types.linear,
property: Properties.opacity,
},
update: {
type: Types.spring,
springDamping: 0.4,
},
},
};
module.exports = LayoutAnimation;

View File

@ -0,0 +1,250 @@
/**
* Copyright 2004-present Facebook. All Rights Reserved.
*
* @providesModule POPAnimationMixin
* @flow
*/
'use strict';
var POPAnimation = require('POPAnimation');
if (!POPAnimation) {
// POP animation isn't available in the OSS fork - this is a temporary
// workaround to enable its availability to be determined at runtime.
module.exports = null;
} else {
var invariant = require('invariant');
var warning = require('warning');
var POPAnimationMixin = {
/**
* Different ways to interpolate between beginning and end states
* of properties during animation, such as spring, linear, and decay.
*/
AnimationTypes: POPAnimation.Types,
AnimationProperties: POPAnimation.Properties,
getInitialState: function(): Object {
return {
_currentAnimationsByNodeHandle: {},
};
},
_ensureBookkeepingSetup: function(nodeHandle: any) {
if (!this.state._currentAnimationsByNodeHandle[nodeHandle]) {
this.state._currentAnimationsByNodeHandle[nodeHandle] = [];
}
},
/**
* Start animating the View with ref `refKey`.
*
* @param {key} refKey The key to reference the View to be animated.
*
* @param {number|Object} anim Either the identifier returned by
* POPAnimation.create* or an object defining all the necessary
* properties of the animation you wish to start (including type, matching
* an entry in AnimationTypes).
*
* @param {func} doneCallback A callback fired when the animation is done, and
* is passed a `finished` param that indicates whether the animation
* completely finished, or was interrupted.
*/
startAnimation: function(
refKey: string,
anim: number | {type: number; property: number;},
doneCallback: (finished: bool) => void
) {
var animID: number = 0;
if (typeof anim === 'number') {
animID = anim;
} else {
invariant(
anim instanceof Object &&
anim.type !== undefined &&
anim.property !== undefined,
'Animation definitions must specify a type of animation and a ' +
'property to animate.'
);
animID = POPAnimation.createAnimation(anim.type, anim);
}
invariant(
this.refs[refKey],
'Invalid refKey ' + refKey + ' for anim:\n' + JSON.stringify(anim) +
'\nvalid refs: ' + JSON.stringify(Object.keys(this.refs))
);
var refNodeHandle = this.refs[refKey].getNodeHandle();
this.startAnimationWithNodeHandle(refNodeHandle, animID, doneCallback);
},
/**
* Starts an animation on a native node.
*
* @param {NodeHandle} nodeHandle Handle to underlying native node.
* @see `startAnimation`.
*/
startAnimationWithNodeHandle: function(
nodeHandle: any,
animID: number,
doneCallback: (finished: bool) => void
) {
this._ensureBookkeepingSetup(nodeHandle);
var animations = this.state._currentAnimationsByNodeHandle[nodeHandle];
var animIndex = animations.length;
animations.push(animID);
var cleanupWrapper = (finished) => {
if (!this.isMounted()) {
return;
}
animations[animIndex] = 0; // zero it out so we don't try to stop it
var allDone = true;
for (var ii = 0; ii < animations.length; ii++) {
if (animations[ii]) {
allDone = false;
break;
}
}
if (allDone) {
this.state._currentAnimationsByNodeHandle[nodeHandle] = undefined;
}
doneCallback && doneCallback(finished);
};
POPAnimation.addAnimation(nodeHandle, animID, cleanupWrapper);
},
/**
* Starts multiple animations with one shared callback that is called when all
* animations complete.
*
* @param {Array(Object} animations Array of objects defining all the
* animations to start, each with shape `{ref|nodeHandle, anim}`.
* @param {func} onSuccess A callback fired when all animations have returned,
* and is passed a finished arg that is true if all animations finished
* completely.
* @param {func} onFailure Not supported yet.
*/
startAnimations: function(
animations: Array<Object>,
onSuccess: (finished: boolean) => void,
onFailure: () => void
) {
var numReturned = 0;
var numFinished = 0;
var numAnimations = animations.length;
var metaCallback = (finished) => {
if (finished) {
++numFinished;
}
if (++numReturned === numAnimations) {
onSuccess && onSuccess(numFinished === numAnimations);
}
};
animations.forEach((anim) => {
warning(
anim.ref != null || anim.nodeHandle != null &&
!anim.ref !== !anim.nodeHandle,
'Animations must be specified with either ref xor nodeHandle'
);
if (anim.ref) {
this.startAnimation(anim.ref, anim.anim, metaCallback);
} else if (anim.nodeHandle) {
this.startAnimationWithNodeHandle(anim.nodeHandle, anim.anim, metaCallback);
}
});
},
/**
* Stop any and all animations operating on the View with native node handle
* `nodeHandle`.
*
* @param {NodeHandle} component The instance to stop animations
* on. Do not pass a composite component.
*/
stopNodeHandleAnimations: function(nodeHandle: any) {
if (!this.state._currentAnimationsByNodeHandle[nodeHandle]) {
return;
}
var anims = this.state._currentAnimationsByNodeHandle[nodeHandle];
for (var i = 0; i < anims.length; i++) {
var anim = anims[i];
if (anim) {
// Note: Converting the string key to a number `nodeHandle`.
POPAnimation.removeAnimation(+nodeHandle, anim);
}
}
this.state._currentAnimationsByNodeHandle[nodeHandle] = undefined;
},
/**
* Stop any and all animations operating on the View with ref `refKey`.
*
* @param {key} refKey The key to reference the View to be animated.
*/
stopAnimations: function(refKey: string) {
invariant(this.refs[refKey], 'invalid ref');
this.stopNodeHandleAnimations(this.refs[refKey].getNodeHandle());
},
/**
* Stop any and all animations created by this component on itself and
* subviews.
*/
stopAllAnimations: function() {
for (var nodeHandle in this.state._currentAnimationsByNodeHandle) {
this.stopNodeHandleAnimations(nodeHandle);
}
},
/**
* Animates size and position of a view referenced by `refKey` to a specific
* frame.
*
* @param {key} refKey ref key for view to animate.
* @param {Object} frame The frame to animate the view to, specified as {left,
* top, width, height}.
* @param {const} type What type of interpolation to use, selected from
* `inperpolationTypes`.
* @param {Object} event Event encapsulating synthetic and native data that
* may have triggered this animation. Velocity is extracted from it if
* possible and applied to the animation.
* @param {func} doneCallback A callback fired when the animation is done, and
* is passed a `finished` param that indicates whether the animation
* completely finished, or was interrupted.
*/
animateToFrame: function(
refKey: string,
frame: {left: number; top: number; width: number; height: number;},
type: number,
velocity: number,
doneCallback: (finished: boolean) => void
) {
var animFrame = { // Animations use a centered coordinate system.
x: frame.left + frame.width / 2,
y: frame.top + frame.height / 2,
w: frame.width,
h: frame.height
};
frame = undefined;
var velocity = velocity || [0, 0];
var posAnim = POPAnimation.createAnimation(type, {
property: POPAnimation.Properties.position,
toValue: [animFrame.x, animFrame.y],
velocity: velocity,
});
var sizeAnim = POPAnimation.createAnimation(type, {
property: POPAnimation.Properties.size,
toValue: [animFrame.w, animFrame.h]
});
this.startAnimation(refKey, posAnim, doneCallback);
this.startAnimation(refKey, sizeAnim);
},
// Cleanup any potentially leaked animations.
componentWillUnmount: function() {
this.stopAllAnimations();
}
};
module.exports = POPAnimationMixin;
}

View File

@ -0,0 +1,166 @@
/**
* Copyright 2004-present Facebook. All Rights Reserved.
*
* @providesModule POPAnimation
*/
'use strict';
var RKPOPAnimationManager = require('NativeModulesDeprecated').RKPOPAnimationManager;
if (!RKPOPAnimationManager) {
// POP animation isn't available in the OSS fork - this is a temporary
// workaround to enable its availability to be determined at runtime.
module.exports = null;
} else {
var ReactPropTypes = require('ReactPropTypes');
var createStrictShapeTypeChecker = require('createStrictShapeTypeChecker');
var getObjectValues = require('getObjectValues');
var invariant = require('invariant');
var merge = require('merge');
var RKTypes = RKPOPAnimationManager.Types;
var RKProperties = RKPOPAnimationManager.Properties;
var Properties = {
bounds: RKProperties.bounds,
opacity: RKProperties.opacity,
position: RKProperties.position,
positionX: RKProperties.positionX,
positionY: RKProperties.positionY,
zPosition: RKProperties.zPosition,
rotation: RKProperties.rotation,
rotationX: RKProperties.rotationX,
rotationY: RKProperties.rotationY,
scaleX: RKProperties.scaleX,
scaleXY: RKProperties.scaleXY,
scaleY: RKProperties.scaleY,
shadowColor: RKProperties.shadowColor,
shadowOffset: RKProperties.shadowOffset,
shadowOpacity: RKProperties.shadowOpacity,
shadowRadius: RKProperties.shadowRadius,
size: RKProperties.size,
subscaleXY: RKProperties.subscaleXY,
subtranslationX: RKProperties.subtranslationX,
subtranslationXY: RKProperties.subtranslationXY,
subtranslationY: RKProperties.subtranslationY,
subtranslationZ: RKProperties.subtranslationZ,
translationX: RKProperties.translationX,
translationXY: RKProperties.translationXY,
translationY: RKProperties.translationY,
translationZ: RKProperties.translationZ,
};
var Types = {
decay: RKTypes.decay,
easeIn: RKTypes.easeIn,
easeInEaseOut: RKTypes.easeInEaseOut,
easeOut: RKTypes.easeOut,
linear: RKTypes.linear,
spring: RKTypes.spring,
};
var POPAnimation = {
Types: Types,
Properties: Properties,
attributeChecker: createStrictShapeTypeChecker({
type: ReactPropTypes.oneOf(getObjectValues(Types)),
property: ReactPropTypes.oneOf(getObjectValues(Properties)),
fromValue: ReactPropTypes.any,
toValue: ReactPropTypes.any,
duration: ReactPropTypes.any,
velocity: ReactPropTypes.any,
deceleration: ReactPropTypes.any,
springBounciness: ReactPropTypes.any,
dynamicsFriction: ReactPropTypes.any,
dynamicsMass: ReactPropTypes.any,
dynamicsTension: ReactPropTypes.any,
}),
lastUsedTag: 0,
allocateTagForAnimation: function() {
return ++this.lastUsedTag;
},
createAnimation: function(typeName, attrs) {
var tag = this.allocateTagForAnimation();
if (__DEV__) {
POPAnimation.attributeChecker(
{attrs},
'attrs',
'POPAnimation.createAnimation'
);
POPAnimation.attributeChecker(
{attrs: {type: typeName}},
'attrs',
'POPAnimation.createAnimation'
);
}
RKPOPAnimationManager.createAnimationInternal(tag, typeName, attrs);
return tag;
},
createSpringAnimation: function(attrs) {
return this.createAnimation(this.Types.spring, attrs);
},
createDecayAnimation: function(attrs) {
return this.createAnimation(this.Types.decay, attrs);
},
createLinearAnimation: function(attrs) {
return this.createAnimation(this.Types.linear, attrs);
},
createEaseInAnimation: function(attrs) {
return this.createAnimation(this.Types.easeIn, attrs);
},
createEaseOutAnimation: function(attrs) {
return this.createAnimation(this.Types.easeOut, attrs);
},
createEaseInEaseOutAnimation: function(attrs) {
return this.createAnimation(this.Types.easeInEaseOut, attrs);
},
addAnimation: function(nodeHandle, anim, callback) {
RKPOPAnimationManager.addAnimation(nodeHandle, anim, callback);
},
removeAnimation: function(nodeHandle, anim) {
RKPOPAnimationManager.removeAnimation(nodeHandle, anim);
},
};
// Make sure that we correctly propagate RKPOPAnimationManager constants
// to POPAnimation
if (__DEV__) {
var allProperties = merge(
RKPOPAnimationManager.Properties,
RKPOPAnimationManager.Properties
);
for (var key in allProperties) {
invariant(
POPAnimation.Properties[key] === RKPOPAnimationManager.Properties[key],
'POPAnimation doesn\'t copy property ' + key + ' correctly'
);
}
var allTypes = merge(
RKPOPAnimationManager.Types,
RKPOPAnimationManager.Types
);
for (var key in allTypes) {
invariant(
POPAnimation.Types[key] === RKPOPAnimationManager.Types[key],
'POPAnimation doesn\'t copy type ' + key + ' correctly'
);
}
}
module.exports = POPAnimation;
}

View File

@ -25,7 +25,6 @@ var invariant = require('invariant');
var merge = require('merge'); var merge = require('merge');
var nativePropType = require('nativePropType'); var nativePropType = require('nativePropType');
var validAttributesFromPropTypes = require('validAttributesFromPropTypes'); var validAttributesFromPropTypes = require('validAttributesFromPropTypes');
var warning = require('warning');
var PropTypes = React.PropTypes; var PropTypes = React.PropTypes;
@ -194,14 +193,19 @@ var ScrollView = React.createClass({
); );
} }
if (__DEV__) { if (__DEV__) {
warning( if (this.props.onScroll && !this.props.throttleScrollCallbackMS) {
this.props.onScroll && !this.props.throttleScrollCallbackMS, var onScroll = this.props.onScroll;
this.props.onScroll = function() {
console.log(
'You specified `onScroll` on a <ScrollView> but not ' + 'You specified `onScroll` on a <ScrollView> but not ' +
'`throttleScrollCallbackMS`. You will only receive one event. ' + '`throttleScrollCallbackMS`. You will only receive one event. ' +
'Using `16` you get all the events but be aware that it may cause ' + 'Using `16` you get all the events but be aware that it may ' +
'frame drops, use a bigger number if you don\'t need as much ' + 'cause frame drops, use a bigger number if you don\'t need as ' +
'precision.' 'much precision.'
); );
onScroll.apply(this, arguments);
};
}
} }
var contentContainer = var contentContainer =

View File

@ -0,0 +1,144 @@
/**
* Copyright 2004-present Facebook. All Rights Reserved.
*
* @providesModule TouchableOpacity
*/
'use strict';
var NativeMethodsMixin = require('NativeMethodsMixin');
var POPAnimationMixin = require('POPAnimationMixin');
var React = require('React');
var Touchable = require('Touchable');
var cloneWithProps = require('cloneWithProps');
var ensureComponentIsNative = require('ensureComponentIsNative');
var keyOf = require('keyOf');
var onlyChild = require('onlyChild');
/**
* TouchableOpacity - A wrapper for making views respond properly to touches.
* On press down, the opacity of the wrapped view is decreased, dimming it.
* This is done without actually changing the view hierarchy, and in general is
* easy to add to an app without weird side-effects. Example:
*
* renderButton: function() {
* return (
* <TouchableOpacity onPress={this._onPressButton}>
* <Image
* style={styles.button}
* source={ix('myButton')}
* />
* </View>
* );
* },
*
* More example code in TouchableExample.js, and more in-depth discussion in
* Touchable.js. See also TouchableHighlight.js and
* TouchableWithoutFeedback.js.
*/
var TouchableOpacity = React.createClass({
mixins: [Touchable.Mixin, NativeMethodsMixin, POPAnimationMixin],
propTypes: {
/**
* Called when the touch is released, but not if cancelled (e.g. by
* a scroll that steals the responder lock).
*/
onPress: React.PropTypes.func,
/**
* Determines what the opacity of the wrapped view should be when touch is
* active.
*/
activeOpacity: React.PropTypes.number,
},
getDefaultProps: function() {
return {
activeOpacity: 0.5,
};
},
getInitialState: function() {
return this.touchableGetInitialState();
},
componentDidMount: function() {
ensureComponentIsNative(this.refs[CHILD_REF]);
},
componentDidUpdate: function() {
ensureComponentIsNative(this.refs[CHILD_REF]);
},
setOpacityTo: function(value) {
if (POPAnimationMixin) {
// Reset with animation if POP is available
this.stopAllAnimations();
var anim = {
type: this.AnimationTypes.linear,
property: this.AnimationProperties.opacity,
toValue: value,
};
this.startAnimation(CHILD_REF, anim);
} else {
// Reset immediately if POP is unavailable
this.refs[CHILD_REF].setNativeProps({
opacity: value
});
}
},
/**
* `Touchable.Mixin` self callbacks. The mixin will invoke these if they are
* defined on your component.
*/
touchableHandleActivePressIn: function() {
this.refs[CHILD_REF].setNativeProps({
opacity: this.props.activeOpacity
});
},
touchableHandleActivePressOut: function() {
this.setOpacityTo(1.0);
},
touchableHandlePress: function() {
this.setOpacityTo(1.0);
this.props.onPress && this.props.onPress();
},
touchableGetPressRectOffset: function() {
return PRESS_RECT_OFFSET; // Always make sure to predeclare a constant!
},
touchableGetHighlightDelayMS: function() {
return 0;
},
render: function() {
return cloneWithProps(onlyChild(this.props.children), {
ref: CHILD_REF,
accessible: true,
testID: this.props.testID,
onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder,
onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest,
onResponderGrant: this.touchableHandleResponderGrant,
onResponderMove: this.touchableHandleResponderMove,
onResponderRelease: this.touchableHandleResponderRelease,
onResponderTerminate: this.touchableHandleResponderTerminate,
});
},
});
/**
* When the scroll view is disabled, this defines how far your touch may move
* off of the button, before deactivating the button. Once deactivated, try
* moving it back and you'll see that the button is once again reactivated!
* Move it back and forth several times while the scroll view is disabled.
*/
var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
var CHILD_REF = keyOf({childRef: null});
module.exports = TouchableOpacity;

View File

@ -9,6 +9,7 @@ var NativeModules = require('NativeModules');
var NativeModulesDeprecated = require('NativeModulesDeprecated'); var NativeModulesDeprecated = require('NativeModulesDeprecated');
var RKUIManager = NativeModules.RKUIManager; var RKUIManager = NativeModules.RKUIManager;
var RKUIManagerDeprecated = NativeModulesDeprecated.RKUIManager; var RKUIManagerDeprecated = NativeModulesDeprecated.RKUIManager;
var RKPOPAnimationManagerDeprecated = NativeModulesDeprecated.RKPOPAnimationManager;
var TextInputState = require('TextInputState'); var TextInputState = require('TextInputState');
var flattenStyle = require('flattenStyle'); var flattenStyle = require('flattenStyle');
@ -19,19 +20,19 @@ var animationIDInvariant = function(funcName, anim) {
invariant( invariant(
anim, anim,
funcName + ' must be called with a valid animation ID returned from' + funcName + ' must be called with a valid animation ID returned from' +
' ReactIOSAnimation.createAnimation, received: "' + anim + '"' ' POPAnimation.createAnimation, received: "' + anim + '"'
); );
}; };
var NativeMethodsMixin = { var NativeMethodsMixin = {
addAnimation: function(anim, callback) { addAnimation: function(anim, callback) {
animationIDInvariant('addAnimation', anim); animationIDInvariant('addAnimation', anim);
RKUIManagerDeprecated.addAnimation(this.getNodeHandle(), anim, callback); RKPOPAnimationManagerDeprecated.addAnimation(this.getNodeHandle(), anim, callback);
}, },
removeAnimation: function(anim) { removeAnimation: function(anim) {
animationIDInvariant('removeAnimation', anim); animationIDInvariant('removeAnimation', anim);
RKUIManagerDeprecated.removeAnimation(this.getNodeHandle(), anim); RKPOPAnimationManagerDeprecated.removeAnimation(this.getNodeHandle(), anim);
}, },
measure: function(callback) { measure: function(callback) {

View File

@ -169,20 +169,12 @@ var ReactIOSEventEmitter = merge(ReactEventEmitterMixin, {
var target = nativeEvent.target; var target = nativeEvent.target;
if (target !== null && target !== undefined) { if (target !== null && target !== undefined) {
if (target < ReactIOSTagHandles.tagsStartAt) { if (target < ReactIOSTagHandles.tagsStartAt) {
// When we get multiple touches at the same time, only the first touch
// actually has a view attached to it. The rest of the touches do not.
// This is presumably because iOS doesn't want to send touch events to
// two views for a single multi touch. Therefore this warning is only
// appropriate when it happens to the first touch. (hence jj === 0)
if (__DEV__) { if (__DEV__) {
if (jj === 0) {
warning( warning(
false, false,
'A view is reporting that a touch occured on tag zero.' 'A view is reporting that a touch occured on tag zero.'
); );
} }
}
continue;
} else { } else {
rootNodeID = NodeHandle.getRootNodeID(target); rootNodeID = NodeHandle.getRootNodeID(target);
} }

View File

@ -9,20 +9,35 @@ var ReactDefaultPerf = require('ReactDefaultPerf');
var ReactPerf = require('ReactPerf'); var ReactPerf = require('ReactPerf');
var invariant = require('invariant'); var invariant = require('invariant');
var perfModules = []; var perfModules = [];
var enabled = false;
var RCTRenderingPerf = { var RCTRenderingPerf = {
// Once perf is enabled, it stays enabled
toggle: function() { toggle: function() {
if (ReactPerf.enableMeasure) { console.log('Render perfomance measurements enabled');
enabled = true;
},
start: function() {
if (!enabled) {
return;
}
ReactDefaultPerf.start();
perfModules.forEach((module) => module.start());
},
stop: function() {
if (!enabled) {
return;
}
ReactDefaultPerf.stop(); ReactDefaultPerf.stop();
ReactDefaultPerf.printInclusive(); ReactDefaultPerf.printInclusive();
ReactDefaultPerf.printWasted(); ReactDefaultPerf.printWasted();
perfModules.forEach((module) => module.stop()); perfModules.forEach((module) => module.stop());
} else {
ReactDefaultPerf.start();
console.log('Render perfomance measurements started');
perfModules.forEach((module) => module.start());
}
}, },
register: function(module) { register: function(module) {

View File

@ -10,6 +10,7 @@ var ReactNative = {
Bundler: require('Bundler'), Bundler: require('Bundler'),
ExpandingText: require('ExpandingText'), ExpandingText: require('ExpandingText'),
Image: require('Image'), Image: require('Image'),
LayoutAnimation: require('LayoutAnimation'),
ListView: require('ListView'), ListView: require('ListView'),
ListViewDataSource: require('ListViewDataSource'), ListViewDataSource: require('ListViewDataSource'),
NavigatorIOS: require('NavigatorIOS'), NavigatorIOS: require('NavigatorIOS'),
@ -22,6 +23,7 @@ var ReactNative = {
TextInput: require('TextInput'), TextInput: require('TextInput'),
TimerMixin: require('TimerMixin'), TimerMixin: require('TimerMixin'),
TouchableHighlight: require('TouchableHighlight'), TouchableHighlight: require('TouchableHighlight'),
TouchableOpacity: require('TouchableOpacity'),
TouchableWithoutFeedback: require('TouchableWithoutFeedback'), TouchableWithoutFeedback: require('TouchableWithoutFeedback'),
View: require('View'), View: require('View'),
invariant: require('invariant'), invariant: require('invariant'),

View File

@ -0,0 +1,11 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSInteger, RCTAnimationType) {
RCTAnimationTypeSpring = 0,
RCTAnimationTypeLinear,
RCTAnimationTypeEaseIn,
RCTAnimationTypeEaseOut,
RCTAnimationTypeEaseInEaseOut,
};

View File

@ -5,6 +5,7 @@
#import "Layout.h" #import "Layout.h"
#import "RCTPointerEvents.h" #import "RCTPointerEvents.h"
#import "RCTAnimationType.h"
/** /**
* This class provides a collection of conversion functions for mapping * This class provides a collection of conversion functions for mapping
@ -64,6 +65,7 @@
+ (css_wrap_type_t)css_wrap_type_t:(id)json; + (css_wrap_type_t)css_wrap_type_t:(id)json;
+ (RCTPointerEvents)RCTPointerEvents:(id)json; + (RCTPointerEvents)RCTPointerEvents:(id)json;
+ (RCTAnimationType)RCTAnimationType:(id)json;
@end @end

View File

@ -20,7 +20,7 @@ NSString *const RCTBoldFontWeight = @"bold";
return code; \ return code; \
} \ } \
@catch (__unused NSException *e) { \ @catch (__unused NSException *e) { \
RCTLogMustFix(@"JSON value '%@' of type '%@' cannot be converted to '%s'", \ RCTLogError(@"JSON value '%@' of type '%@' cannot be converted to '%s'", \
json, [json class], #type); \ json, [json class], #type); \
json = nil; \ json = nil; \
return code; \ return code; \
@ -45,15 +45,15 @@ RCT_CONVERTER_CUSTOM(type, name, [json getter])
if ([[mapping allValues] containsObject:json] || [json getter] == default) { \ if ([[mapping allValues] containsObject:json] || [json getter] == default) { \
return [json getter]; \ return [json getter]; \
} \ } \
RCTLogMustFix(@"Invalid %s '%@'. should be one of: %@", #type, json, [mapping allValues]); \ RCTLogError(@"Invalid %s '%@'. should be one of: %@", #type, json, [mapping allValues]); \
return default; \ return default; \
} \ } \
if (![json isKindOfClass:[NSString class]]) { \ if (![json isKindOfClass:[NSString class]]) { \
RCTLogMustFix(@"Expected NSNumber or NSString for %s, received %@: %@", #type, [json class], json); \ RCTLogError(@"Expected NSNumber or NSString for %s, received %@: %@", #type, [json class], json); \
} \ } \
id value = mapping[json]; \ id value = mapping[json]; \
if(!value && [json description].length > 0) { \ if(!value && [json description].length > 0) { \
RCTLogMustFix(@"Invalid %s '%@'. should be one of: %@", #type, json, [mapping allKeys]); \ RCTLogError(@"Invalid %s '%@'. should be one of: %@", #type, json, [mapping allKeys]); \
} \ } \
return value ? [value getter] : default; \ return value ? [value getter] : default; \
} }
@ -72,7 +72,7 @@ RCT_CONVERTER_CUSTOM(type, name, [json getter])
type result; \ type result; \
if ([json isKindOfClass:[NSArray class]]) { \ if ([json isKindOfClass:[NSArray class]]) { \
if ([json count] != count) { \ if ([json count] != count) { \
RCTLogMustFix(@"Expected array with count %zd, but count is %zd: %@", count, [json count], json); \ RCTLogError(@"Expected array with count %zd, but count is %zd: %@", count, [json count], json); \
} else { \ } else { \
for (NSUInteger i = 0; i < count; i++) { \ for (NSUInteger i = 0; i < count; i++) { \
((CGFloat *)&result)[i] = [json[i] doubleValue]; \ ((CGFloat *)&result)[i] = [json[i] doubleValue]; \
@ -80,7 +80,7 @@ RCT_CONVERTER_CUSTOM(type, name, [json getter])
} \ } \
} else { \ } else { \
if (![json isKindOfClass:[NSDictionary class]]) { \ if (![json isKindOfClass:[NSDictionary class]]) { \
RCTLogMustFix(@"Expected NSArray or NSDictionary for %s, received %@: %@", #type, [json class], json); \ RCTLogError(@"Expected NSArray or NSDictionary for %s, received %@: %@", #type, [json class], json); \
} else { \ } else { \
for (NSUInteger i = 0; i < count; i++) { \ for (NSUInteger i = 0; i < count; i++) { \
((CGFloat *)&result)[i] = [json[fields[i]] doubleValue]; \ ((CGFloat *)&result)[i] = [json[fields[i]] doubleValue]; \
@ -90,7 +90,7 @@ RCT_CONVERTER_CUSTOM(type, name, [json getter])
return result; \ return result; \
} \ } \
@catch (__unused NSException *e) { \ @catch (__unused NSException *e) { \
RCTLogMustFix(@"JSON value '%@' cannot be converted to '%s'", json, #type); \ RCTLogError(@"JSON value '%@' cannot be converted to '%s'", json, #type); \
type result; \ type result; \
return result; \ return result; \
} \ } \
@ -111,7 +111,7 @@ RCT_CONVERTER_CUSTOM(NSUInteger, NSUInteger, [json unsignedIntegerValue])
+ (NSURL *)NSURL:(id)json + (NSURL *)NSURL:(id)json
{ {
if (![json isKindOfClass:[NSString class]]) { if (![json isKindOfClass:[NSString class]]) {
RCTLogMustFix(@"Expected NSString for NSURL, received %@: %@", [json class], json); RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json class], json);
return nil; return nil;
} }
@ -376,10 +376,10 @@ RCT_STRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"]
} else if ([colorString hasPrefix:@"rgb("]) { } else if ([colorString hasPrefix:@"rgb("]) {
sscanf([colorString UTF8String], "rgb(%zd,%zd,%zd)", &red, &green, &blue); sscanf([colorString UTF8String], "rgb(%zd,%zd,%zd)", &red, &green, &blue);
} else { } else {
RCTLogMustFix(@"Unrecognized color format '%@', must be one of #hex|rgba|rgb", colorString); RCTLogError(@"Unrecognized color format '%@', must be one of #hex|rgba|rgb", colorString);
} }
if (red == -1 || green == -1 || blue == -1 || alpha > 1.0 || alpha < 0.0) { if (red == -1 || green == -1 || blue == -1 || alpha > 1.0 || alpha < 0.0) {
RCTLogMustFix(@"Invalid color string '%@'", colorString); RCTLogError(@"Invalid color string '%@'", colorString);
} else { } else {
color = [UIColor colorWithRed:red / 255.0 green:green / 255.0 blue:blue / 255.0 alpha:alpha]; color = [UIColor colorWithRed:red / 255.0 green:green / 255.0 blue:blue / 255.0 alpha:alpha];
} }
@ -388,7 +388,7 @@ RCT_STRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"]
if ([json count] < 3 || [json count] > 4) { if ([json count] < 3 || [json count] > 4) {
RCTLogMustFix(@"Expected array with count 3 or 4, but count is %zd: %@", [json count], json); RCTLogError(@"Expected array with count 3 or 4, but count is %zd: %@", [json count], json);
} else { } else {
@ -409,7 +409,7 @@ RCT_STRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"]
} else if (json && ![json isKindOfClass:[NSNull class]]) { } else if (json && ![json isKindOfClass:[NSNull class]]) {
RCTLogMustFix(@"Expected NSArray, NSDictionary or NSString for UIColor, received %@: %@", [json class], json); RCTLogError(@"Expected NSArray, NSDictionary or NSString for UIColor, received %@: %@", [json class], json);
} }
// Default color // Default color
@ -509,7 +509,7 @@ RCT_STRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"]
+ (UIImage *)UIImage:(id)json + (UIImage *)UIImage:(id)json
{ {
if (![json isKindOfClass:[NSString class]]) { if (![json isKindOfClass:[NSString class]]) {
RCTLogMustFix(@"Expected NSString for UIImage, received %@: %@", [json class], json); RCTLogError(@"Expected NSString for UIImage, received %@: %@", [json class], json);
return nil; return nil;
} }
@ -657,6 +657,14 @@ RCT_ENUM_CONVERTER(RCTPointerEvents, (@{
@"boxnone": @(RCTPointerEventsBoxNone) @"boxnone": @(RCTPointerEventsBoxNone)
}), RCTPointerEventsUnspecified, integerValue) }), RCTPointerEventsUnspecified, integerValue)
RCT_ENUM_CONVERTER(RCTAnimationType, (@{
@"spring": @(RCTAnimationTypeSpring),
@"linear": @(RCTAnimationTypeLinear),
@"easeIn": @(RCTAnimationTypeEaseIn),
@"easeOut": @(RCTAnimationTypeEaseOut),
@"easeInEaseOut": @(RCTAnimationTypeEaseInEaseOut),
}), RCTAnimationTypeEaseInEaseOut, integerValue)
@end @end
static NSString *RCTGuessTypeEncoding(id target, NSString *key, id value, NSString *encoding) static NSString *RCTGuessTypeEncoding(id target, NSString *key, id value, NSString *encoding)
@ -832,6 +840,9 @@ BOOL RCTSetProperty(id target, NSString *keypath, id value)
@"extAlignment": ^(id val) { @"extAlignment": ^(id val) {
return [RCTConvert NSTextAlignment:val]; return [RCTConvert NSTextAlignment:val];
}, },
@"ointerEvents": ^(id val) {
return [RCTConvert RCTPointerEvents:val];
},
}; };
}); });
for (NSString *subkey in converters) { for (NSString *subkey in converters) {

View File

@ -6,15 +6,11 @@
#import "RCTInvalidating.h" #import "RCTInvalidating.h"
@class RCTRootView; @class RCTRootView;
@class RCTShadowView;
@class RCTSparseArray;
@protocol RCTScrollableProtocol; @protocol RCTScrollableProtocol;
@interface RCTUIManager : NSObject <RCTBridgeModule, RCTInvalidating> @interface RCTUIManager : NSObject <RCTBridgeModule, RCTInvalidating>
@property (nonatomic, strong) RCTSparseArray *shadowViewRegistry;
@property (nonatomic, strong) RCTSparseArray *viewRegistry;
@property (nonatomic, weak) id<RCTScrollableProtocol> mainScrollView; @property (nonatomic, weak) id<RCTScrollableProtocol> mainScrollView;
/** /**

View File

@ -4,9 +4,9 @@
#import <AVFoundation/AVFoundation.h> #import <AVFoundation/AVFoundation.h>
#import <objc/message.h> #import <objc/message.h>
#import <pthread.h>
#import "Layout.h" #import "Layout.h"
#import "RCTAnimationType.h"
#import "RCTAssert.h" #import "RCTAssert.h"
#import "RCTBridge.h" #import "RCTBridge.h"
#import "RCTConvert.h" #import "RCTConvert.h"
@ -22,8 +22,6 @@
#import "RCTViewManager.h" #import "RCTViewManager.h"
#import "UIView+ReactKit.h" #import "UIView+ReactKit.h"
@class RCTAnimationConfig;
typedef void (^react_view_node_block_t)(id<RCTViewNodeProtocol>); typedef void (^react_view_node_block_t)(id<RCTViewNodeProtocol>);
static void RCTTraverseViewNodes(id<RCTViewNodeProtocol> view, react_view_node_block_t block) static void RCTTraverseViewNodes(id<RCTViewNodeProtocol> view, react_view_node_block_t block)
@ -75,40 +73,151 @@ static NSDictionary *RCTViewModuleClasses(void)
return modules; return modules;
} }
@interface RCTAnimation : NSObject
@property (nonatomic, readonly) NSTimeInterval duration;
@property (nonatomic, readonly) NSTimeInterval delay;
@property (nonatomic, readonly, copy) NSString *property;
@property (nonatomic, readonly) id fromValue;
@property (nonatomic, readonly) id toValue;
@property (nonatomic, readonly) CGFloat springDamping;
@property (nonatomic, readonly) CGFloat initialVelocity;
@property (nonatomic, readonly) RCTAnimationType animationType;
@end
@implementation RCTAnimation
UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType type)
{
switch (type) {
case RCTAnimationTypeLinear:
return UIViewAnimationCurveLinear;
case RCTAnimationTypeEaseIn:
return UIViewAnimationCurveEaseIn;
case RCTAnimationTypeEaseOut:
return UIViewAnimationCurveEaseOut;
case RCTAnimationTypeEaseInEaseOut:
return UIViewAnimationCurveEaseInOut;
default:
RCTCAssert(NO, @"Unsupported animation type %zd", type);
return UIViewAnimationCurveEaseInOut;
}
}
- (instancetype)initWithDuration:(NSTimeInterval)duration dictionary:(NSDictionary *)config
{
if (!config) {
return nil;
}
if ((self = [super init])) {
_property = [RCTConvert NSString:config[@"property"]];
// TODO: this should be provided in ms, not seconds
_duration = [RCTConvert NSTimeInterval:config[@"duration"]] ?: duration;
_delay = [RCTConvert NSTimeInterval:config[@"delay"]];
_animationType = [RCTConvert RCTAnimationType:config[@"type"]];
if (_animationType == RCTAnimationTypeSpring) {
_springDamping = [RCTConvert CGFloat:config[@"springDamping"]];
_initialVelocity = [RCTConvert CGFloat:config[@"initialVelocity"]];
}
_fromValue = config[@"fromValue"];
_toValue = config[@"toValue"];
}
return self;
}
- (void)performAnimations:(void (^)(void))animations
withCompletionBlock:(void (^)(BOOL completed))completionBlock
{
if (_animationType == RCTAnimationTypeSpring) {
[UIView animateWithDuration:_duration
delay:_delay
usingSpringWithDamping:_springDamping
initialSpringVelocity:_initialVelocity
options:UIViewAnimationOptionBeginFromCurrentState
animations:animations
completion:completionBlock];
} else {
UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState |
UIViewAnimationCurveFromRCTAnimationType(_animationType);
[UIView animateWithDuration:_duration
delay:_delay
options:options
animations:animations
completion:completionBlock];
}
}
@end
@interface RCTLayoutAnimation : NSObject
@property (nonatomic, strong) RCTAnimation *createAnimation;
@property (nonatomic, strong) RCTAnimation *updateAnimation;
@property (nonatomic, strong) RCTAnimation *deleteAnimation;
@property (nonatomic, strong) RCTResponseSenderBlock callback;
@end
@implementation RCTLayoutAnimation
- (instancetype)initWithDictionary:(NSDictionary *)config callback:(RCTResponseSenderBlock)callback
{
if (!config) {
return nil;
}
if ((self = [super init])) {
// TODO: this should be provided in ms, not seconds
NSTimeInterval duration = [RCTConvert NSTimeInterval:config[@"duration"]];
_createAnimation = [[RCTAnimation alloc] initWithDuration:duration dictionary:config[@"create"]];
_updateAnimation = [[RCTAnimation alloc] initWithDuration:duration dictionary:config[@"update"]];
_deleteAnimation = [[RCTAnimation alloc] initWithDuration:duration dictionary:config[@"delete"]];
_callback = callback;
}
return self;
}
@end
@implementation RCTUIManager @implementation RCTUIManager
{ {
// Root views are only mutated on the shadow queue // Root views are only mutated on the shadow queue
NSDictionary *_viewManagers;
NSMutableSet *_rootViewTags; NSMutableSet *_rootViewTags;
NSMutableArray *_pendingUIBlocks; NSMutableArray *_pendingUIBlocks;
NSLock *_pendingUIBlocksLock;
pthread_mutex_t _pendingUIBlocksMutex; // Animation
NSDictionary *_nextLayoutAnimationConfig; // RCT thread only RCTLayoutAnimation *_nextLayoutAnimation; // RCT thread only
RCTResponseSenderBlock _nextLayoutAnimationCallback; // RCT thread only RCTLayoutAnimation *_layoutAnimation; // Main thread only
RCTResponseSenderBlock _layoutAnimationCallbackMT; // Main thread only
NSMutableDictionary *_defaultShadowViews; // Keyed by moduleName
NSMutableDictionary *_defaultViews; NSMutableDictionary *_defaultShadowViews; // RCT thread only
NSMutableDictionary *_defaultViews; // Main thread only
NSDictionary *_viewManagers;
// Keyed by React tag
RCTSparseArray *_viewManagerRegistry; // RCT thread only
RCTSparseArray *_shadowViewRegistry; // RCT thread only
RCTSparseArray *_viewRegistry; // Main thread only
__weak RCTBridge *_bridge; __weak RCTBridge *_bridge;
} }
- (RCTViewManager *)_managerInstanceForViewWithModuleName:(NSString *)moduleName
{
RCTViewManager *managerInstance = _viewManagers[moduleName];
if (managerInstance == nil) {
RCTLogWarn(@"No manager class found for view with module name \"%@\"", moduleName);
managerInstance = [[RCTViewManager alloc] init];
}
return managerInstance;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge - (instancetype)initWithBridge:(RCTBridge *)bridge
{ {
if ((self = [super init])) { if ((self = [super init])) {
_bridge = bridge; _bridge = bridge;
pthread_mutex_init(&_pendingUIBlocksMutex, NULL); _pendingUIBlocksLock = [[NSLock alloc] init];
// Instantiate view managers // Instantiate view managers
NSMutableDictionary *viewManagers = [[NSMutableDictionary alloc] init]; NSMutableDictionary *viewManagers = [[NSMutableDictionary alloc] init];
@ -116,16 +225,16 @@ static NSDictionary *RCTViewModuleClasses(void)
viewManagers[moduleName] = [[moduleClass alloc] initWithEventDispatcher:_bridge.eventDispatcher]; viewManagers[moduleName] = [[moduleClass alloc] initWithEventDispatcher:_bridge.eventDispatcher];
}]; }];
_viewManagers = viewManagers; _viewManagers = viewManagers;
_defaultShadowViews = [[NSMutableDictionary alloc] init];
_defaultViews = [[NSMutableDictionary alloc] init];
_viewRegistry = [[RCTSparseArray alloc] init]; _viewManagerRegistry = [[RCTSparseArray alloc] init];
_shadowViewRegistry = [[RCTSparseArray alloc] init]; _shadowViewRegistry = [[RCTSparseArray alloc] init];
_viewRegistry = [[RCTSparseArray alloc] init];
// Internal resources // Internal resources
_pendingUIBlocks = [[NSMutableArray alloc] init]; _pendingUIBlocks = [[NSMutableArray alloc] init];
_rootViewTags = [[NSMutableSet alloc] init]; _rootViewTags = [[NSMutableSet alloc] init];
_defaultShadowViews = [[NSMutableDictionary alloc] init];
_defaultViews = [[NSMutableDictionary alloc] init];
} }
return self; return self;
} }
@ -138,7 +247,6 @@ static NSDictionary *RCTViewModuleClasses(void)
- (void)dealloc - (void)dealloc
{ {
RCTAssert(!self.valid, @"must call -invalidate before -dealloc"); RCTAssert(!self.valid, @"must call -invalidate before -dealloc");
pthread_mutex_destroy(&_pendingUIBlocksMutex);
} }
- (BOOL)isValid - (BOOL)isValid
@ -153,9 +261,9 @@ static NSDictionary *RCTViewModuleClasses(void)
_viewRegistry = nil; _viewRegistry = nil;
_shadowViewRegistry = nil; _shadowViewRegistry = nil;
pthread_mutex_lock(&_pendingUIBlocksMutex); [_pendingUIBlocksLock lock];
_pendingUIBlocks = nil; _pendingUIBlocks = nil;
pthread_mutex_unlock(&_pendingUIBlocksMutex); [_pendingUIBlocksLock unlock];
} }
- (void)registerRootView:(RCTRootView *)rootView; - (void)registerRootView:(RCTRootView *)rootView;
@ -171,6 +279,9 @@ static NSDictionary *RCTViewModuleClasses(void)
_viewRegistry[reactTag] = rootView; _viewRegistry[reactTag] = rootView;
CGRect frame = rootView.frame; CGRect frame = rootView.frame;
// Register manager (TODO: should we do this, or leave it nil?)
_viewManagerRegistry[reactTag] = _viewManagers[[RCTViewManager moduleName]];
// Register shadow view // Register shadow view
dispatch_async(_bridge.shadowQueue, ^{ dispatch_async(_bridge.shadowQueue, ^{
@ -192,7 +303,7 @@ static NSDictionary *RCTViewModuleClasses(void)
{ {
for (id<RCTViewNodeProtocol> child in children) { for (id<RCTViewNodeProtocol> child in children) {
RCTTraverseViewNodes(registry[child.reactTag], ^(id<RCTViewNodeProtocol> subview) { RCTTraverseViewNodes(registry[child.reactTag], ^(id<RCTViewNodeProtocol> subview) {
RCTAssert(![subview isReactRootView], @"Host views should not be unregistered"); RCTAssert(![subview isReactRootView], @"Root views should not be unregistered");
if ([subview conformsToProtocol:@protocol(RCTInvalidating)]) { if ([subview conformsToProtocol:@protocol(RCTInvalidating)]) {
[(id<RCTInvalidating>)subview invalidate]; [(id<RCTInvalidating>)subview invalidate];
} }
@ -203,8 +314,8 @@ static NSDictionary *RCTViewModuleClasses(void)
- (void)addUIBlock:(RCTViewManagerUIBlock)block - (void)addUIBlock:(RCTViewManagerUIBlock)block
{ {
// This assert is fragile. This is temporary pending t4698600
RCTAssert(![NSThread isMainThread], @"This method should only be called on the shadow thread"); RCTAssert(![NSThread isMainThread], @"This method should only be called on the shadow thread");
__weak RCTUIManager *weakViewManager = self; __weak RCTUIManager *weakViewManager = self;
__weak RCTSparseArray *weakViewRegistry = _viewRegistry; __weak RCTSparseArray *weakViewRegistry = _viewRegistry;
dispatch_block_t outerBlock = ^{ dispatch_block_t outerBlock = ^{
@ -215,34 +326,12 @@ static NSDictionary *RCTViewModuleClasses(void)
} }
}; };
pthread_mutex_lock(&_pendingUIBlocksMutex); [_pendingUIBlocksLock lock];
[_pendingUIBlocks addObject:[outerBlock copy]]; [_pendingUIBlocks addObject:outerBlock];
pthread_mutex_unlock(&_pendingUIBlocksMutex); [_pendingUIBlocksLock unlock];
} }
- (void)setViewLayout:(UIView *)view withAnchorPoint:(CGPoint)anchorPoint position:(CGPoint)position bounds:(CGRect)bounds config:(RCTAnimationConfig *)config completion:(void (^)(BOOL finished))completion - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)rootShadowView
{
if (isnan(position.x) || isnan(position.y) ||
isnan(bounds.origin.x) || isnan(bounds.origin.y) ||
isnan(bounds.size.width) || isnan(bounds.size.height)) {
RCTLogError(@"Invalid layout for (%zd)%@. position: %@. bounds: %@", [view reactTag], self, NSStringFromCGPoint(position), NSStringFromCGRect(bounds));
return;
}
view.layer.anchorPoint = anchorPoint;
view.layer.position = position;
view.layer.bounds = bounds;
completion(YES);
}
/**
* TODO: `RCTBridge` has first class knowledge of this method. We should either:
* 1. Require that the JS trigger this after a batch - almost like a flush.
* 2. Build in support to the `<BatchedExports>` protocol so that each module
* may return values to JS via a third callback function passed in, but can
* return a tuple that is `(UIThreadBlocks, JSThreadBlocks)`.
*/
- (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)hostShadowView
{ {
NSMutableSet *viewsWithNewFrames = [NSMutableSet setWithCapacity:1]; NSMutableSet *viewsWithNewFrames = [NSMutableSet setWithCapacity:1];
@ -250,12 +339,8 @@ static NSDictionary *RCTViewModuleClasses(void)
// `frameTags`/`frames` that is created/mutated in the JS thread. We access // `frameTags`/`frames` that is created/mutated in the JS thread. We access
// these structures in the UI-thread block. `NSMutableArray` is not thread // these structures in the UI-thread block. `NSMutableArray` is not thread
// safe so we rely on the fact that we never mutate it after it's passed to // safe so we rely on the fact that we never mutate it after it's passed to
// the main thread. To help protect against mutation, we alias the variable to // the main thread.
// a threadsafe `NSArray`, however the `NSArray` doesn't guarantee deep [rootShadowView collectRootUpdatedFrames:viewsWithNewFrames parentConstraint:(CGSize){CSS_UNDEFINED, CSS_UNDEFINED}];
// immutability so we must be very careful.
// https://developer.apple.com/library/mac/documentation/Cocoa/
// Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html
[hostShadowView collectRootUpdatedFrames:viewsWithNewFrames parentConstraint:CGSizeMake(CSS_UNDEFINED, CSS_UNDEFINED)];
// Parallel arrays // Parallel arrays
NSMutableArray *frameReactTags = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableArray *frameReactTags = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count];
@ -269,37 +354,75 @@ static NSDictionary *RCTViewModuleClasses(void)
[areNew addObject:@(shadowView.isNewView)]; [areNew addObject:@(shadowView.isNewView)];
[parentsAreNew addObject:@(shadowView.superview.isNewView)]; [parentsAreNew addObject:@(shadowView.superview.isNewView)];
} }
for (RCTShadowView *shadowView in viewsWithNewFrames) { for (RCTShadowView *shadowView in viewsWithNewFrames) {
// We have to do this after we build the parentsAreNew array. // We have to do this after we build the parentsAreNew array.
shadowView.newView = NO; shadowView.newView = NO;
} }
NSArray *immutableFrameReactTags = frameReactTags; // Perform layout (possibly animated)
NSArray *immutableFrames = frames; NSNumber *rootViewTag = rootShadowView.reactTag;
return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
NSNumber *rootViewTag = hostShadowView.reactTag; for (NSUInteger ii = 0; ii < frames.count; ii++) {
return ^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { NSNumber *reactTag = frameReactTags[ii];
for (NSUInteger ii = 0; ii < immutableFrames.count; ii++) {
NSNumber *reactTag = immutableFrameReactTags[ii];
UIView *view = viewRegistry[reactTag]; UIView *view = viewRegistry[reactTag];
CGRect frame = [immutableFrames[ii] CGRectValue]; CGRect frame = [frames[ii] CGRectValue];
// These frames are in terms of anchorPoint = topLeft, but internally the // These frames are in terms of anchorPoint = topLeft, but internally the
// views are anchorPoint = center for easier scale and rotation animations. // views are anchorPoint = center for easier scale and rotation animations.
// Convert the frame so it works with anchorPoint = center. // Convert the frame so it works with anchorPoint = center.
__weak RCTUIManager *weakSelf = self; CGPoint position = {CGRectGetMidX(frame), CGRectGetMidY(frame)};
[self setViewLayout:view CGRect bounds = {0, 0, frame.size};
withAnchorPoint:CGPointMake(0.5, 0.5)
position:CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame)) // Avoid crashes due to nan coords
bounds:CGRectMake(0, 0, frame.size.width, frame.size.height) if (isnan(position.x) || isnan(position.y) ||
config:/*!isNew ? _layoutAnimationConfigMT.updateConfig : */nil // TODO: !!! isnan(bounds.origin.x) || isnan(bounds.origin.y) ||
completion:^(BOOL finished) { isnan(bounds.size.width) || isnan(bounds.size.height)) {
__strong RCTUIManager *strongSelf = weakSelf; RCTLogError(@"Invalid layout for (%zd)%@. position: %@. bounds: %@", [view reactTag], self, NSStringFromCGPoint(position), NSStringFromCGRect(bounds));
if (strongSelf->_layoutAnimationCallbackMT) { continue;
strongSelf->_layoutAnimationCallbackMT(@[@(finished)]);
} }
}];
void (^completion)(BOOL finished) = ^(BOOL finished) {
if (self->_layoutAnimation.callback) {
self->_layoutAnimation.callback(@[@(finished)]);
} }
};
// Animate view update
BOOL isNew = [areNew[ii] boolValue];
RCTAnimation *updateAnimation = isNew ? nil: _layoutAnimation.updateAnimation;
if (updateAnimation) {
[updateAnimation performAnimations:^{
view.layer.position = position;
view.layer.bounds = bounds;
} withCompletionBlock:completion];
} else {
view.layer.position = position;
view.layer.bounds = bounds;
completion(YES);
}
// Animate view creations
BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue];
RCTAnimation *createAnimation = _layoutAnimation.createAnimation;
if (shouldAnimateCreation && createAnimation) {
if ([createAnimation.property isEqualToString:@"scaleXY"]) {
view.layer.transform = CATransform3DMakeScale(0, 0, 0);
} else if ([createAnimation.property isEqualToString:@"opacity"]) {
view.layer.opacity = 0.0;
}
[createAnimation performAnimations:^{
if ([createAnimation.property isEqual:@"scaleXY"]) {
view.layer.transform = CATransform3DIdentity;
} else if ([createAnimation.property isEqual:@"opacity"]) {
view.layer.opacity = 1.0;
} else {
RCTLogError(@"Unsupported layout animation createConfig property %@", createAnimation.property);
}
} withCompletionBlock:nil];
}
}
RCTRootView *rootView = _viewRegistry[rootViewTag]; RCTRootView *rootView = _viewRegistry[rootViewTag];
RCTTraverseViewNodes(rootView, ^(id<RCTViewNodeProtocol> view) { RCTTraverseViewNodes(rootView, ^(id<RCTViewNodeProtocol> view) {
if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) { if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) {
@ -314,7 +437,7 @@ static NSDictionary *RCTViewModuleClasses(void)
NSMutableSet *applierBlocks = [NSMutableSet setWithCapacity:1]; NSMutableSet *applierBlocks = [NSMutableSet setWithCapacity:1];
[topView collectUpdatedProperties:applierBlocks parentProperties:@{}]; [topView collectUpdatedProperties:applierBlocks parentProperties:@{}];
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
for (RCTApplierBlock block in applierBlocks) { for (RCTApplierBlock block in applierBlocks) {
block(viewRegistry); block(viewRegistry);
} }
@ -391,10 +514,10 @@ static NSDictionary *RCTViewModuleClasses(void)
[self _purgeChildren:@[rootShadowView] fromRegistry:_shadowViewRegistry]; [self _purgeChildren:@[rootShadowView] fromRegistry:_shadowViewRegistry];
[_rootViewTags removeObject:rootReactTag]; [_rootViewTags removeObject:rootReactTag];
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){ [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
RCTCAssertMainThread(); RCTCAssertMainThread();
UIView *rootView = viewRegistry[rootReactTag]; UIView *rootView = viewRegistry[rootReactTag];
[viewManager _purgeChildren:@[rootView] fromRegistry:viewRegistry]; [uiManager _purgeChildren:@[rootView] fromRegistry:viewRegistry];
}]; }];
} }
@ -437,9 +560,9 @@ static NSDictionary *RCTViewModuleClasses(void)
removeAtIndices:removeAtIndices removeAtIndices:removeAtIndices
registry:_shadowViewRegistry]; registry:_shadowViewRegistry];
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){ [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
RCTCAssertMainThread();
[viewManager _manageChildren:containerReactTag [uiManager _manageChildren:containerReactTag
moveFromIndices:moveFromIndices moveFromIndices:moveFromIndices
moveToIndices:moveToIndices moveToIndices:moveToIndices
addChildReactTags:addChildReactTags addChildReactTags:addChildReactTags
@ -549,7 +672,11 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
{ {
RCT_EXPORT(createView); RCT_EXPORT(createView);
RCTViewManager *manager = [self _managerInstanceForViewWithModuleName:moduleName]; RCTViewManager *manager = _viewManagers[moduleName];
if (manager == nil) {
RCTLogWarn(@"No manager class found for view with module name \"%@\"", moduleName);
manager = [[RCTViewManager alloc] init];
}
// Generate default view, used for resetting default props // Generate default view, used for resetting default props
if (!_defaultShadowViews[moduleName]) { if (!_defaultShadowViews[moduleName]) {
@ -565,6 +692,9 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
RCTCAssertMainThread(); RCTCAssertMainThread();
// Register manager (TODO: should we do this, or leave it nil?)
uiManager->_viewManagerRegistry[reactTag] = manager;
// Generate default view, used for resetting default props // Generate default view, used for resetting default props
if (!uiManager->_defaultViews[moduleName]) { if (!uiManager->_defaultViews[moduleName]) {
// Note the default is setup after the props are read for the first time ever // Note the default is setup after the props are read for the first time ever
@ -575,6 +705,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
UIView *view = [manager view]; UIView *view = [manager view];
if (view) { if (view) {
// Set required properties // Set required properties
view.reactTag = reactTag; view.reactTag = reactTag;
view.multipleTouchEnabled = YES; view.multipleTouchEnabled = YES;
@ -588,18 +719,20 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
}]; }];
} }
- (void)updateView:(NSNumber *)reactTag moduleName:(NSString *)moduleName props:(NSDictionary *)props // TODO: remove moduleName param as it isn't needed
- (void)updateView:(NSNumber *)reactTag moduleName:(__unused NSString *)_ props:(NSDictionary *)props
{ {
RCT_EXPORT(); RCT_EXPORT();
RCTViewManager *viewManager = _viewManagerRegistry[reactTag];
NSString *moduleName = [[viewManager class] moduleName];
RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
RCTViewManager *manager = [self _managerInstanceForViewWithModuleName:moduleName]; RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[moduleName], viewManager);
RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[moduleName], manager);
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
RCTCAssertMainThread(); UIView *view = uiManager->_viewRegistry[reactTag];
UIView *view = viewRegistry[reactTag]; RCTSetViewProps(props, view, uiManager->_defaultViews[moduleName], viewManager);
RCTSetViewProps(props, view, uiManager->_defaultViews[moduleName], manager);
}]; }];
} }
@ -608,7 +741,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
RCT_EXPORT(focus); RCT_EXPORT(focus);
if (!reactTag) return; if (!reactTag) return;
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
UIView *newResponder = viewRegistry[reactTag]; UIView *newResponder = viewRegistry[reactTag];
[newResponder becomeFirstResponder]; [newResponder becomeFirstResponder];
}]; }];
@ -619,7 +752,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
RCT_EXPORT(blur); RCT_EXPORT(blur);
if (!reactTag) return; if (!reactTag) return;
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){ [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
UIView *currentResponder = viewRegistry[reactTag]; UIView *currentResponder = viewRegistry[reactTag];
[currentResponder resignFirstResponder]; [currentResponder resignFirstResponder];
}]; }];
@ -630,24 +763,40 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
// First copy the previous blocks into a temporary variable, then reset the // First copy the previous blocks into a temporary variable, then reset the
// pending blocks to a new array. This guards against mutation while // pending blocks to a new array. This guards against mutation while
// processing the pending blocks in another thread. // processing the pending blocks in another thread.
for (RCTViewManager *manager in _viewManagers.allValues) { for (RCTViewManager *manager in _viewManagers.allValues) {
RCTViewManagerUIBlock uiBlock = [manager uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry]; RCTViewManagerUIBlock uiBlock = [manager uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry];
if (uiBlock != nil) { if (uiBlock) {
[self addUIBlock:uiBlock]; [self addUIBlock:uiBlock];
} }
} }
// Set up next layout animation
if (_nextLayoutAnimation) {
RCTLayoutAnimation *layoutAnimation = _nextLayoutAnimation;
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
uiManager->_layoutAnimation = layoutAnimation;
}];
}
// Perform layout
for (NSNumber *reactTag in _rootViewTags) { for (NSNumber *reactTag in _rootViewTags) {
RCTShadowView *rootView = _shadowViewRegistry[reactTag]; RCTShadowView *rootView = _shadowViewRegistry[reactTag];
[self addUIBlock:[self uiBlockWithLayoutUpdateForRootView:rootView]]; [self addUIBlock:[self uiBlockWithLayoutUpdateForRootView:rootView]];
[self _amendPendingUIBlocksWithStylePropagationUpdateForRootView:rootView]; [self _amendPendingUIBlocksWithStylePropagationUpdateForRootView:rootView];
} }
pthread_mutex_lock(&_pendingUIBlocksMutex); // Clear layout animations
if (_nextLayoutAnimation) {
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
uiManager->_layoutAnimation = nil;
}];
_nextLayoutAnimation = nil;
}
[_pendingUIBlocksLock lock];
NSArray *previousPendingUIBlocks = _pendingUIBlocks; NSArray *previousPendingUIBlocks = _pendingUIBlocks;
_pendingUIBlocks = [[NSMutableArray alloc] init]; _pendingUIBlocks = [[NSMutableArray alloc] init];
pthread_mutex_unlock(&_pendingUIBlocksMutex); [_pendingUIBlocksLock unlock];
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
for (dispatch_block_t block in previousPendingUIBlocks) { for (dispatch_block_t block in previousPendingUIBlocks) {
@ -656,16 +805,6 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
}); });
} }
- (void)layoutRootShadowView:(RCTShadowView *)rootShadowView
{
RCTViewManagerUIBlock uiBlock = [self uiBlockWithLayoutUpdateForRootView:rootShadowView];
__weak RCTUIManager *weakViewManager = self;
__weak RCTSparseArray *weakViewRegistry = _viewRegistry;
dispatch_async(dispatch_get_main_queue(), ^{
uiBlock(weakViewManager, weakViewRegistry);
});
}
- (void)measure:(NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback - (void)measure:(NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback
{ {
RCT_EXPORT(); RCT_EXPORT();
@ -675,7 +814,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
return; return;
} }
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
UIView *view = viewRegistry[reactTag]; UIView *view = viewRegistry[reactTag];
if (!view) { if (!view) {
RCTLogError(@"measure cannot find view with tag %zd", reactTag); RCTLogError(@"measure cannot find view with tag %zd", reactTag);
@ -705,7 +844,6 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
}]; }];
} }
- (void)requestSchedulingJavaScriptNavigation:(NSNumber *)reactTag - (void)requestSchedulingJavaScriptNavigation:(NSNumber *)reactTag
errorCallback:(RCTResponseSenderBlock)errorCallback errorCallback:(RCTResponseSenderBlock)errorCallback
callback:(RCTResponseSenderBlock)callback callback:(RCTResponseSenderBlock)callback
@ -716,7 +854,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
RCTLogError(@"Callback not provided for navigation scheduling."); RCTLogError(@"Callback not provided for navigation scheduling.");
return; return;
} }
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){ [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
if (reactTag) { if (reactTag) {
//TODO: This is nasty - why is RCTNavigator hard-coded? //TODO: This is nasty - why is RCTNavigator hard-coded?
id rkObject = viewRegistry[reactTag]; id rkObject = viewRegistry[reactTag];
@ -870,24 +1008,24 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
{ {
RCT_EXPORT(); RCT_EXPORT();
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){ [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
// - There should be at most one designated "main scroll view" // - There should be at most one designated "main scroll view"
// - There should be at most one designated "`nativeMainScrollDelegate`" // - There should be at most one designated "`nativeMainScrollDelegate`"
// - The one designated main scroll view should have the one designated // - The one designated main scroll view should have the one designated
// `nativeMainScrollDelegate` set as its `nativeMainScrollDelegate`. // `nativeMainScrollDelegate` set as its `nativeMainScrollDelegate`.
if (viewManager.mainScrollView) { if (uiManager.mainScrollView) {
viewManager.mainScrollView.nativeMainScrollDelegate = nil; uiManager.mainScrollView.nativeMainScrollDelegate = nil;
} }
if (reactTag) { if (reactTag) {
id rkObject = viewRegistry[reactTag]; id rkObject = viewRegistry[reactTag];
if ([rkObject conformsToProtocol:@protocol(RCTScrollableProtocol)]) { if ([rkObject conformsToProtocol:@protocol(RCTScrollableProtocol)]) {
viewManager.mainScrollView = (id<RCTScrollableProtocol>)rkObject; uiManager.mainScrollView = (id<RCTScrollableProtocol>)rkObject;
((id<RCTScrollableProtocol>)rkObject).nativeMainScrollDelegate = viewManager.nativeMainScrollDelegate; ((id<RCTScrollableProtocol>)rkObject).nativeMainScrollDelegate = uiManager.nativeMainScrollDelegate;
} else { } else {
RCTCAssert(NO, @"Tag %@ does not conform to RCTScrollableProtocol", reactTag); RCTCAssert(NO, @"Tag %@ does not conform to RCTScrollableProtocol", reactTag);
} }
} else { } else {
viewManager.mainScrollView = nil; uiManager.mainScrollView = nil;
} }
}]; }];
} }
@ -896,7 +1034,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
{ {
RCT_EXPORT(scrollTo); RCT_EXPORT(scrollTo);
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){ [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
UIView *view = viewRegistry[reactTag]; UIView *view = viewRegistry[reactTag];
if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) {
[(id<RCTScrollableProtocol>)view scrollToOffset:CGPointMake([offsetX floatValue], [offsetY floatValue])]; [(id<RCTScrollableProtocol>)view scrollToOffset:CGPointMake([offsetX floatValue], [offsetY floatValue])];
@ -910,7 +1048,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
{ {
RCT_EXPORT(zoomToRect); RCT_EXPORT(zoomToRect);
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){ [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
UIView *view = viewRegistry[reactTag]; UIView *view = viewRegistry[reactTag];
if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) {
[(id<RCTScrollableProtocol>)view zoomToRect:[RCTConvert CGRect:rectDict] animated:YES]; [(id<RCTScrollableProtocol>)view zoomToRect:[RCTConvert CGRect:rectDict] animated:YES];
@ -924,7 +1062,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
{ {
RCT_EXPORT(); RCT_EXPORT();
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
UIView *view = viewRegistry[reactTag]; UIView *view = viewRegistry[reactTag];
if (!view) { if (!view) {
NSString *error = [[NSString alloc] initWithFormat:@"cannot find view with tag %@", reactTag]; NSString *error = [[NSString alloc] initWithFormat:@"cannot find view with tag %@", reactTag];
@ -947,7 +1085,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
{ {
RCT_EXPORT(); RCT_EXPORT();
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
_jsResponder = viewRegistry[reactTag]; _jsResponder = viewRegistry[reactTag];
if (!_jsResponder) { if (!_jsResponder) {
RCTLogMustFix(@"Invalid view set to be the JS responder - tag %zd", reactTag); RCTLogMustFix(@"Invalid view set to be the JS responder - tag %zd", reactTag);
@ -959,7 +1097,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
{ {
RCT_EXPORT(); RCT_EXPORT();
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
_jsResponder = nil; _jsResponder = nil;
}]; }];
} }
@ -1189,18 +1327,19 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView
return allJSConstants; return allJSConstants;
} }
- (void)configureNextLayoutAnimation:(NSDictionary *)config withCallback:(RCTResponseSenderBlock)callback errorCallback:(RCTResponseSenderBlock)errorCallback - (void)configureNextLayoutAnimation:(NSDictionary *)config
withCallback:(RCTResponseSenderBlock)callback
errorCallback:(RCTResponseSenderBlock)errorCallback
{ {
RCT_EXPORT(); RCT_EXPORT();
if (_nextLayoutAnimationCallback || _nextLayoutAnimationConfig) { if (_nextLayoutAnimation) {
RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", _nextLayoutAnimationConfig, config); RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", _nextLayoutAnimation, config);
} }
if (config[@"delete"] != nil) { if (config[@"delete"] != nil) {
RCTLogError(@"LayoutAnimation only supports create and update right now. Config: %@", config); RCTLogError(@"LayoutAnimation only supports create and update right now. Config: %@", config);
} }
_nextLayoutAnimationConfig = config; _nextLayoutAnimation = [[RCTLayoutAnimation alloc] initWithDictionary:config callback:callback];
_nextLayoutAnimationCallback = callback;
} }
static UIView *_jsResponder; static UIView *_jsResponder;

View File

@ -124,6 +124,7 @@
13B080241A694A8400A75B9A /* RCTWrapperViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWrapperViewController.m; sourceTree = "<group>"; }; 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWrapperViewController.m; sourceTree = "<group>"; };
13B080271A694C4900A75B9A /* RCTDataManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDataManager.h; sourceTree = "<group>"; }; 13B080271A694C4900A75B9A /* RCTDataManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDataManager.h; sourceTree = "<group>"; };
13B080281A694C4900A75B9A /* RCTDataManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDataManager.m; sourceTree = "<group>"; }; 13B080281A694C4900A75B9A /* RCTDataManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDataManager.m; sourceTree = "<group>"; };
13DB9D681A8CC58200429C20 /* RCTAnimationType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationType.h; sourceTree = "<group>"; };
13E067481A70F434002CDEE1 /* RCTUIManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUIManager.h; sourceTree = "<group>"; }; 13E067481A70F434002CDEE1 /* RCTUIManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUIManager.h; sourceTree = "<group>"; };
13E067491A70F434002CDEE1 /* RCTUIManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIManager.m; sourceTree = "<group>"; }; 13E067491A70F434002CDEE1 /* RCTUIManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIManager.m; sourceTree = "<group>"; };
13E0674B1A70F44B002CDEE1 /* RCTShadowView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowView.h; sourceTree = "<group>"; }; 13E0674B1A70F44B002CDEE1 /* RCTShadowView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowView.h; sourceTree = "<group>"; };
@ -330,6 +331,7 @@
137029521A69923600575408 /* RCTImageDownloader.m */, 137029521A69923600575408 /* RCTImageDownloader.m */,
13B07FCD1A683B5F00A75B9A /* RCTScrollableProtocol.h */, 13B07FCD1A683B5F00A75B9A /* RCTScrollableProtocol.h */,
13ED13891A80C9D40050A8F9 /* RCTPointerEvents.h */, 13ED13891A80C9D40050A8F9 /* RCTPointerEvents.h */,
13DB9D681A8CC58200429C20 /* RCTAnimationType.h */,
); );
path = Base; path = Base;
sourceTree = "<group>"; sourceTree = "<group>";

View File

@ -14,7 +14,7 @@ typedef NS_ENUM(NSUInteger, RCTLayoutLifecycle) {
RCTLayoutLifecycleDirtied, RCTLayoutLifecycleDirtied,
}; };
// TODO: is this redundact now? // TODO: is this still needed?
typedef NS_ENUM(NSUInteger, RCTPropagationLifecycle) { typedef NS_ENUM(NSUInteger, RCTPropagationLifecycle) {
RCTPropagationLifecycleUninitialized = 0, RCTPropagationLifecycleUninitialized = 0,
RCTPropagationLifecycleComputed, RCTPropagationLifecycleComputed,
@ -72,7 +72,8 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *);
@property (nonatomic, assign) CGFloat height; @property (nonatomic, assign) CGFloat height;
@property (nonatomic, assign) CGRect frame; @property (nonatomic, assign) CGRect frame;
- (void)updateShadowViewLayout; - (void)setTopLeft:(CGPoint)topLeft;
- (void)setSize:(CGSize)size;
/** /**
* Border. Defaults to 0. * Border. Defaults to 0.
@ -110,12 +111,22 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *);
// The following are implementation details exposed to subclasses. Do not call them directly // The following are implementation details exposed to subclasses. Do not call them directly
- (void)dirtyLayout; - (void)dirtyLayout;
- (void)dirtyPropagation; - (BOOL)isLayoutDirty;
// TODO: is this still needed?
- (void)dirtyPropagation;
- (BOOL)isPropagationDirty;
// TODO: move this to text node?
- (void)dirtyText; - (void)dirtyText;
- (BOOL)isTextDirty; - (BOOL)isTextDirty;
- (void)setTextComputed; - (void)setTextComputed;
/**
* Triggers a recalculation of the shadow view's layout.
*/
- (void)updateShadowViewLayout;
/** /**
* Computes the recursive offset, meaning the sum of all descendant offsets - * Computes the recursive offset, meaning the sum of all descendant offsets -
* this is the sum of all positions inset from parents. This is not merely the * this is the sum of all positions inset from parents. This is not merely the

View File

@ -103,7 +103,7 @@ static css_node_t *RCTGetChild(void *context, int i)
static bool RCTIsDirty(void *context) static bool RCTIsDirty(void *context)
{ {
RCTShadowView *shadowView = (__bridge RCTShadowView *)context; RCTShadowView *shadowView = (__bridge RCTShadowView *)context;
return shadowView.layoutLifecycle != RCTLayoutLifecycleComputed; return [shadowView isLayoutDirty];
} }
// Enforces precedence rules, e.g. marginLeft > marginHorizontal > margin. // Enforces precedence rules, e.g. marginLeft > marginHorizontal > margin.
@ -325,6 +325,11 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st
} }
} }
- (BOOL)isLayoutDirty
{
return _layoutLifecycle != RCTLayoutLifecycleComputed;
}
- (void)dirtyPropagation - (void)dirtyPropagation
{ {
if (_propagationLifecycle != RCTPropagationLifecycleDirtied) { if (_propagationLifecycle != RCTPropagationLifecycleDirtied) {
@ -333,6 +338,11 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st
} }
} }
- (BOOL)isPropagationDirty
{
return _propagationLifecycle != RCTLayoutLifecycleComputed;
}
- (void)dirtyText - (void)dirtyText
{ {
if (_textLifecycle != RCTTextLifecycleDirtied) { if (_textLifecycle != RCTTextLifecycleDirtied) {
@ -391,23 +401,6 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st
return self.reactTag; return self.reactTag;
} }
- (void)updateShadowViewLayout
{
if (_recomputePadding) {
RCTProcessMetaProps(_paddingMetaProps, _cssNode->style.padding);
}
if (_recomputeMargin) {
RCTProcessMetaProps(_marginMetaProps, _cssNode->style.margin);
}
if (_recomputePadding || _recomputeMargin) {
[self dirtyLayout];
}
[self fillCSSNode:_cssNode];
_recomputeMargin = NO;
_recomputePadding = NO;
}
// Margin // Margin
#define RCT_MARGIN_PROPERTY(prop, metaProp) \ #define RCT_MARGIN_PROPERTY(prop, metaProp) \
@ -503,6 +496,20 @@ RCT_POSITION_PROPERTY(Left, left, LEFT)
[self dirtyLayout]; [self dirtyLayout];
} }
- (void)setTopLeft:(CGPoint)topLeft
{
_cssNode->style.position[CSS_LEFT] = topLeft.x;
_cssNode->style.position[CSS_TOP] = topLeft.y;
[self dirtyLayout];
}
- (void)setSize:(CGSize)size
{
_cssNode->style.dimensions[CSS_WIDTH] = size.width;
_cssNode->style.dimensions[CSS_HEIGHT] = size.height;
[self dirtyLayout];
}
// Flex // Flex
#define RCT_STYLE_PROPERTY(setProp, getProp, cssProp, type) \ #define RCT_STYLE_PROPERTY(setProp, getProp, cssProp, type) \
@ -530,4 +537,20 @@ RCT_STYLE_PROPERTY(FlexWrap, flexWrap, flex_wrap, css_wrap_type_t)
[self dirtyPropagation]; [self dirtyPropagation];
} }
- (void)updateShadowViewLayout
{
if (_recomputePadding) {
RCTProcessMetaProps(_paddingMetaProps, _cssNode->style.padding);
}
if (_recomputeMargin) {
RCTProcessMetaProps(_marginMetaProps, _cssNode->style.margin);
}
if (_recomputePadding || _recomputeMargin) {
[self dirtyLayout];
}
[self fillCSSNode:_cssNode];
_recomputeMargin = NO;
_recomputePadding = NO;
}
@end @end

View File

@ -80,12 +80,12 @@ RCT_REMAP_VIEW_PROPERTY(containerBackgroundColor, backgroundColor)
// layout to copy its properties across? // layout to copy its properties across?
- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry
{ {
NSMutableArray *shadowBlocks = [NSMutableArray new]; NSMutableArray *uiBlocks = [NSMutableArray new];
// TODO: are modules global, or specific to a given rootView? // TODO: are modules global, or specific to a given rootView?
for (RCTShadowView *rootView in shadowViewRegistry.allObjects) { for (RCTShadowView *rootView in shadowViewRegistry.allObjects) {
if (![rootView isReactRootView]) { if (![rootView isReactRootView]) {
// This isn't a host view // This isn't a root view
continue; continue;
} }
@ -117,7 +117,7 @@ RCT_REMAP_VIEW_PROPERTY(containerBackgroundColor, backgroundColor)
[shadowView setTextComputed]; [shadowView setTextComputed];
} }
[shadowBlocks addObject:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { [uiBlocks addObject:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
[reactTaggedAttributedStrings enumerateObjectsUsingBlock:^(NSAttributedString *attributedString, NSNumber *reactTag, BOOL *stop) { [reactTaggedAttributedStrings enumerateObjectsUsingBlock:^(NSAttributedString *attributedString, NSNumber *reactTag, BOOL *stop) {
RCTText *text = viewRegistry[reactTag]; RCTText *text = viewRegistry[reactTag];
text.attributedText = attributedString; text.attributedText = attributedString;
@ -125,9 +125,9 @@ RCT_REMAP_VIEW_PROPERTY(containerBackgroundColor, backgroundColor)
}]; }];
} }
return ^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
for (RCTViewManagerUIBlock shadowBlock in shadowBlocks) { for (RCTViewManagerUIBlock shadowBlock in uiBlocks) {
shadowBlock(viewManager, viewRegistry); shadowBlock(uiManager, viewRegistry);
} }
}; };
} }

View File

@ -5,10 +5,6 @@ var FileWatcher = require('../../FileWatcher');
var DependencyGraph = require('./DependencyGraph'); var DependencyGraph = require('./DependencyGraph');
var ModuleDescriptor = require('../ModuleDescriptor'); var ModuleDescriptor = require('../ModuleDescriptor');
var DEFAULT_POLYFILLS = [
];
var DEFINE_MODULE_CODE = var DEFINE_MODULE_CODE =
'__d(' + '__d(' +
'\'_moduleName_\',' + '\'_moduleName_\',' +
@ -39,6 +35,8 @@ function HasteDependencyResolver(config) {
: path.join(__dirname, 'polyfills/prelude.js'), : path.join(__dirname, 'polyfills/prelude.js'),
path.join(__dirname, 'polyfills/require.js'), path.join(__dirname, 'polyfills/require.js'),
path.join(__dirname, 'polyfills/polyfills.js'), path.join(__dirname, 'polyfills/polyfills.js'),
path.join(__dirname, 'polyfills/console.js'),
path.join(__dirname, 'polyfills/error-guard.js'),
].concat( ].concat(
config.polyfillModuleNames || [] config.polyfillModuleNames || []
); );