From ef842c285bb237ee381f7e2dc926c58891ab7e4c Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Wed, 18 Feb 2015 17:39:09 -0800 Subject: [PATCH] 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 --- Examples/UIExplorer/ListViewPagingExample.js | 250 +++++++++++ Examples/UIExplorer/UIExplorerList.js | 1 + Libraries/Animation/LayoutAnimation.js | 89 ++++ Libraries/Animation/POPAnimationMixin.js | 250 +++++++++++ .../BatchedBridgedModules/POPAnimation.js | 166 +++++++ .../Components/ScrollView/ScrollView.ios.js | 22 +- .../Components/Touchable/TouchableOpacity.js | 144 ++++++ Libraries/ReactIOS/NativeMethodsMixin.js | 7 +- Libraries/ReactIOS/ReactIOSEventEmitter.js | 16 +- Libraries/Utilities/RCTRenderingPerf.js | 33 +- Libraries/react-native/react-native.js | 2 + ReactKit/Base/RCTAnimationType.h | 11 + ReactKit/Base/RCTBridge.m | 38 +- ReactKit/Base/RCTConvert.h | 2 + ReactKit/Base/RCTConvert.m | 37 +- ReactKit/Modules/RCTUIManager.h | 4 - ReactKit/Modules/RCTUIManager.m | 413 ++++++++++++------ ReactKit/ReactKit.xcodeproj/project.pbxproj | 2 + ReactKit/Views/RCTShadowView.h | 17 +- ReactKit/Views/RCTShadowView.m | 59 ++- ReactKit/Views/RCTTextManager.m | 12 +- .../src/DependencyResolver/haste/index.js | 6 +- .../haste/polyfills}/console.js | 0 .../haste/polyfills}/error-guard.js | 0 24 files changed, 1344 insertions(+), 237 deletions(-) create mode 100644 Examples/UIExplorer/ListViewPagingExample.js create mode 100644 Libraries/Animation/LayoutAnimation.js create mode 100644 Libraries/Animation/POPAnimationMixin.js create mode 100644 Libraries/BatchedBridge/BatchedBridgedModules/POPAnimation.js create mode 100644 Libraries/Components/Touchable/TouchableOpacity.js create mode 100644 ReactKit/Base/RCTAnimationType.h rename packager/{polyfill => react-packager/src/DependencyResolver/haste/polyfills}/console.js (100%) rename packager/{polyfill => react-packager/src/DependencyResolver/haste/polyfills}/error-guard.js (100%) diff --git a/Examples/UIExplorer/ListViewPagingExample.js b/Examples/UIExplorer/ListViewPagingExample.js new file mode 100644 index 000000000..4818da998 --- /dev/null +++ b/Examples/UIExplorer/ListViewPagingExample.js @@ -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 ( + + + + + + {this.state.dir === 'column' ? + + 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. + : + + } + + + ); + } +}); + +var ListViewPagingExample = React.createClass({ + statics: { + title: ' - 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 (); + }, + + renderSectionHeader: function(sectionData, sectionID) { + return ( + + + {sectionData} + + + ); + }, + + renderHeader: function() { + var headerLikeText = this.state.headerPressCount % 2 ? + 1 Like : + null; + return ( + + + {headerLikeText} + + + Table Header (click me) + + + + + ); + }, + + renderFooter: function() { + return ( + + console.log('Footer!')} style={styles.text}> + Table Footer + + + ); + }, + + render: function() { + return ( + 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; diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index af31617e3..581f34b36 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -24,6 +24,7 @@ var EXAMPLES = [ require('./ExpandingTextExample'), require('./ImageExample'), require('./ListViewSimpleExample'), + require('./ListViewPagingExample'), require('./NavigatorIOSExample'), require('./StatusBarIOSExample'), require('./PointerEventsExample'), diff --git a/Libraries/Animation/LayoutAnimation.js b/Libraries/Animation/LayoutAnimation.js new file mode 100644 index 000000000..5ce5648b3 --- /dev/null +++ b/Libraries/Animation/LayoutAnimation.js @@ -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; diff --git a/Libraries/Animation/POPAnimationMixin.js b/Libraries/Animation/POPAnimationMixin.js new file mode 100644 index 000000000..a3f4b7def --- /dev/null +++ b/Libraries/Animation/POPAnimationMixin.js @@ -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, + 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; + +} diff --git a/Libraries/BatchedBridge/BatchedBridgedModules/POPAnimation.js b/Libraries/BatchedBridge/BatchedBridgedModules/POPAnimation.js new file mode 100644 index 000000000..8ec0bd62f --- /dev/null +++ b/Libraries/BatchedBridge/BatchedBridgedModules/POPAnimation.js @@ -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; + +} diff --git a/Libraries/Components/ScrollView/ScrollView.ios.js b/Libraries/Components/ScrollView/ScrollView.ios.js index 24e06b367..3e425162a 100644 --- a/Libraries/Components/ScrollView/ScrollView.ios.js +++ b/Libraries/Components/ScrollView/ScrollView.ios.js @@ -25,7 +25,6 @@ var invariant = require('invariant'); var merge = require('merge'); var nativePropType = require('nativePropType'); var validAttributesFromPropTypes = require('validAttributesFromPropTypes'); -var warning = require('warning'); var PropTypes = React.PropTypes; @@ -194,14 +193,19 @@ var ScrollView = React.createClass({ ); } if (__DEV__) { - warning( - this.props.onScroll && !this.props.throttleScrollCallbackMS, - 'You specified `onScroll` on a but not ' + - '`throttleScrollCallbackMS`. You will only receive one event. ' + - 'Using `16` you get all the events but be aware that it may cause ' + - 'frame drops, use a bigger number if you don\'t need as much ' + - 'precision.' - ); + if (this.props.onScroll && !this.props.throttleScrollCallbackMS) { + var onScroll = this.props.onScroll; + this.props.onScroll = function() { + console.log( + 'You specified `onScroll` on a but not ' + + '`throttleScrollCallbackMS`. You will only receive one event. ' + + 'Using `16` you get all the events but be aware that it may ' + + 'cause frame drops, use a bigger number if you don\'t need as ' + + 'much precision.' + ); + onScroll.apply(this, arguments); + }; + } } var contentContainer = diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js new file mode 100644 index 000000000..a1bd8f4e3 --- /dev/null +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -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 ( + * + * + * + * ); + * }, + * + * 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; diff --git a/Libraries/ReactIOS/NativeMethodsMixin.js b/Libraries/ReactIOS/NativeMethodsMixin.js index c67575a85..ebfc246f0 100644 --- a/Libraries/ReactIOS/NativeMethodsMixin.js +++ b/Libraries/ReactIOS/NativeMethodsMixin.js @@ -9,6 +9,7 @@ var NativeModules = require('NativeModules'); var NativeModulesDeprecated = require('NativeModulesDeprecated'); var RKUIManager = NativeModules.RKUIManager; var RKUIManagerDeprecated = NativeModulesDeprecated.RKUIManager; +var RKPOPAnimationManagerDeprecated = NativeModulesDeprecated.RKPOPAnimationManager; var TextInputState = require('TextInputState'); var flattenStyle = require('flattenStyle'); @@ -19,19 +20,19 @@ var animationIDInvariant = function(funcName, anim) { invariant( anim, funcName + ' must be called with a valid animation ID returned from' + - ' ReactIOSAnimation.createAnimation, received: "' + anim + '"' + ' POPAnimation.createAnimation, received: "' + anim + '"' ); }; var NativeMethodsMixin = { addAnimation: function(anim, callback) { animationIDInvariant('addAnimation', anim); - RKUIManagerDeprecated.addAnimation(this.getNodeHandle(), anim, callback); + RKPOPAnimationManagerDeprecated.addAnimation(this.getNodeHandle(), anim, callback); }, removeAnimation: function(anim) { animationIDInvariant('removeAnimation', anim); - RKUIManagerDeprecated.removeAnimation(this.getNodeHandle(), anim); + RKPOPAnimationManagerDeprecated.removeAnimation(this.getNodeHandle(), anim); }, measure: function(callback) { diff --git a/Libraries/ReactIOS/ReactIOSEventEmitter.js b/Libraries/ReactIOS/ReactIOSEventEmitter.js index b539ff06e..ab768c927 100644 --- a/Libraries/ReactIOS/ReactIOSEventEmitter.js +++ b/Libraries/ReactIOS/ReactIOSEventEmitter.js @@ -169,20 +169,12 @@ var ReactIOSEventEmitter = merge(ReactEventEmitterMixin, { var target = nativeEvent.target; if (target !== null && target !== undefined) { 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 (jj === 0) { - warning( - false, - 'A view is reporting that a touch occured on tag zero.' - ); - } + warning( + false, + 'A view is reporting that a touch occured on tag zero.' + ); } - continue; } else { rootNodeID = NodeHandle.getRootNodeID(target); } diff --git a/Libraries/Utilities/RCTRenderingPerf.js b/Libraries/Utilities/RCTRenderingPerf.js index b61d79f89..cdc44aaa6 100644 --- a/Libraries/Utilities/RCTRenderingPerf.js +++ b/Libraries/Utilities/RCTRenderingPerf.js @@ -9,20 +9,35 @@ var ReactDefaultPerf = require('ReactDefaultPerf'); var ReactPerf = require('ReactPerf'); var invariant = require('invariant'); + var perfModules = []; +var enabled = false; var RCTRenderingPerf = { + // Once perf is enabled, it stays enabled toggle: function() { - if (ReactPerf.enableMeasure) { - ReactDefaultPerf.stop(); - ReactDefaultPerf.printInclusive(); - ReactDefaultPerf.printWasted(); - perfModules.forEach((module) => module.stop()); - } else { - ReactDefaultPerf.start(); - console.log('Render perfomance measurements started'); - perfModules.forEach((module) => module.start()); + 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.printInclusive(); + ReactDefaultPerf.printWasted(); + perfModules.forEach((module) => module.stop()); }, register: function(module) { diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 74dd38283..7b4765c97 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -10,6 +10,7 @@ var ReactNative = { Bundler: require('Bundler'), ExpandingText: require('ExpandingText'), Image: require('Image'), + LayoutAnimation: require('LayoutAnimation'), ListView: require('ListView'), ListViewDataSource: require('ListViewDataSource'), NavigatorIOS: require('NavigatorIOS'), @@ -22,6 +23,7 @@ var ReactNative = { TextInput: require('TextInput'), TimerMixin: require('TimerMixin'), TouchableHighlight: require('TouchableHighlight'), + TouchableOpacity: require('TouchableOpacity'), TouchableWithoutFeedback: require('TouchableWithoutFeedback'), View: require('View'), invariant: require('invariant'), diff --git a/ReactKit/Base/RCTAnimationType.h b/ReactKit/Base/RCTAnimationType.h new file mode 100644 index 000000000..dae90b845 --- /dev/null +++ b/ReactKit/Base/RCTAnimationType.h @@ -0,0 +1,11 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +typedef NS_ENUM(NSInteger, RCTAnimationType) { + RCTAnimationTypeSpring = 0, + RCTAnimationTypeLinear, + RCTAnimationTypeEaseIn, + RCTAnimationTypeEaseOut, + RCTAnimationTypeEaseInEaseOut, +}; diff --git a/ReactKit/Base/RCTBridge.m b/ReactKit/Base/RCTBridge.m index 20acd088a..9bd0dae35 100644 --- a/ReactKit/Base/RCTBridge.m +++ b/ReactKit/Base/RCTBridge.m @@ -240,15 +240,15 @@ static NSDictionary *RCTRemoteModulesConfig() NSMutableDictionary *methods = [NSMutableDictionary dictionaryWithCapacity:rawMethods.count]; [rawMethods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger methodID, BOOL *stop) { methods[method.JSMethodName] = @{ - @"methodID": @(methodID), - @"type": @"remote", - }; + @"methodID": @(methodID), + @"type": @"remote", + }; }]; NSDictionary *module = @{ - @"moduleID": @(remoteModules.count), - @"methods": methods - }; + @"moduleID": @(remoteModules.count), + @"methods": methods + }; Class cls = RCTBridgeModuleClasses()[moduleName]; if (RCTClassOverridesClassMethod(cls, @selector(constantsToExport))) { @@ -302,10 +302,10 @@ static NSDictionary *RCTLocalModulesConfig() // Add globally used methods [JSMethods addObjectsFromArray:@[ - @"Bundler.runApplication", - @"RCTEventEmitter.receiveEvent", - @"RCTEventEmitter.receiveTouches", - ]]; + @"Bundler.runApplication", + @"RCTEventEmitter.receiveEvent", + @"RCTEventEmitter.receiveTouches", + ]]; // NOTE: these methods are currently unused in the OSS project // @"Dimensions.set", @@ -331,9 +331,9 @@ static NSDictionary *RCTLocalModulesConfig() NSDictionary *module = localModules[moduleName]; if (!module) { module = @{ - @"moduleID": @(localModules.count), - @"methods": [[NSMutableDictionary alloc] init] - }; + @"moduleID": @(localModules.count), + @"methods": [[NSMutableDictionary alloc] init] + }; localModules[moduleName] = module; } @@ -342,9 +342,9 @@ static NSDictionary *RCTLocalModulesConfig() NSMutableDictionary *methods = module[@"methods"]; if (!methods[methodName]) { methods[methodName] = @{ - @"methodID": @(methods.count), - @"type": @"local" - }; + @"methodID": @(methods.count), + @"type": @"local" + }; } // Add module and method lookup @@ -386,9 +386,9 @@ static id _latestJSExecutor; // Inject module data into JS context NSString *configJSON = RCTJSONStringify(@{ - @"remoteModuleConfig": RCTRemoteModulesConfig(), - @"localModulesConfig": RCTLocalModulesConfig() - }, NULL); + @"remoteModuleConfig": RCTRemoteModulesConfig(), + @"localModulesConfig": RCTLocalModulesConfig() + }, NULL); dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [_javaScriptExecutor injectJSONText:configJSON asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback:^(id err) { dispatch_semaphore_signal(semaphore); diff --git a/ReactKit/Base/RCTConvert.h b/ReactKit/Base/RCTConvert.h index ed7327017..51a6b76ce 100644 --- a/ReactKit/Base/RCTConvert.h +++ b/ReactKit/Base/RCTConvert.h @@ -5,6 +5,7 @@ #import "Layout.h" #import "RCTPointerEvents.h" +#import "RCTAnimationType.h" /** * This class provides a collection of conversion functions for mapping @@ -64,6 +65,7 @@ + (css_wrap_type_t)css_wrap_type_t:(id)json; + (RCTPointerEvents)RCTPointerEvents:(id)json; ++ (RCTAnimationType)RCTAnimationType:(id)json; @end diff --git a/ReactKit/Base/RCTConvert.m b/ReactKit/Base/RCTConvert.m index 40519d812..b1b461b83 100644 --- a/ReactKit/Base/RCTConvert.m +++ b/ReactKit/Base/RCTConvert.m @@ -20,7 +20,7 @@ NSString *const RCTBoldFontWeight = @"bold"; return code; \ } \ @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 = nil; \ return code; \ @@ -45,15 +45,15 @@ RCT_CONVERTER_CUSTOM(type, name, [json getter]) if ([[mapping allValues] containsObject:json] || [json getter] == default) { \ 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; \ } \ 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]; \ 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; \ } @@ -72,7 +72,7 @@ RCT_CONVERTER_CUSTOM(type, name, [json getter]) type result; \ if ([json isKindOfClass:[NSArray class]]) { \ 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 { \ for (NSUInteger i = 0; i < count; i++) { \ ((CGFloat *)&result)[i] = [json[i] doubleValue]; \ @@ -80,7 +80,7 @@ RCT_CONVERTER_CUSTOM(type, name, [json getter]) } \ } else { \ 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 { \ for (NSUInteger i = 0; i < count; i++) { \ ((CGFloat *)&result)[i] = [json[fields[i]] doubleValue]; \ @@ -90,7 +90,7 @@ RCT_CONVERTER_CUSTOM(type, name, [json getter]) return result; \ } \ @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; \ return result; \ } \ @@ -111,7 +111,7 @@ RCT_CONVERTER_CUSTOM(NSUInteger, NSUInteger, [json unsignedIntegerValue]) + (NSURL *)NSURL:(id)json { 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; } @@ -376,10 +376,10 @@ RCT_STRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"] } else if ([colorString hasPrefix:@"rgb("]) { sscanf([colorString UTF8String], "rgb(%zd,%zd,%zd)", &red, &green, &blue); } 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) { - RCTLogMustFix(@"Invalid color string '%@'", colorString); + RCTLogError(@"Invalid color string '%@'", colorString); } else { 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) { - 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 { @@ -409,7 +409,7 @@ RCT_STRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"] } 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 @@ -509,7 +509,7 @@ RCT_STRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"] + (UIImage *)UIImage:(id)json { 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; } @@ -657,6 +657,14 @@ RCT_ENUM_CONVERTER(RCTPointerEvents, (@{ @"boxnone": @(RCTPointerEventsBoxNone) }), RCTPointerEventsUnspecified, integerValue) +RCT_ENUM_CONVERTER(RCTAnimationType, (@{ + @"spring": @(RCTAnimationTypeSpring), + @"linear": @(RCTAnimationTypeLinear), + @"easeIn": @(RCTAnimationTypeEaseIn), + @"easeOut": @(RCTAnimationTypeEaseOut), + @"easeInEaseOut": @(RCTAnimationTypeEaseInEaseOut), +}), RCTAnimationTypeEaseInEaseOut, integerValue) + @end 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) { return [RCTConvert NSTextAlignment:val]; }, + @"ointerEvents": ^(id val) { + return [RCTConvert RCTPointerEvents:val]; + }, }; }); for (NSString *subkey in converters) { diff --git a/ReactKit/Modules/RCTUIManager.h b/ReactKit/Modules/RCTUIManager.h index 96ea15d28..87e91118a 100644 --- a/ReactKit/Modules/RCTUIManager.h +++ b/ReactKit/Modules/RCTUIManager.h @@ -6,15 +6,11 @@ #import "RCTInvalidating.h" @class RCTRootView; -@class RCTShadowView; -@class RCTSparseArray; @protocol RCTScrollableProtocol; @interface RCTUIManager : NSObject -@property (nonatomic, strong) RCTSparseArray *shadowViewRegistry; -@property (nonatomic, strong) RCTSparseArray *viewRegistry; @property (nonatomic, weak) id mainScrollView; /** diff --git a/ReactKit/Modules/RCTUIManager.m b/ReactKit/Modules/RCTUIManager.m index 63a22d223..a86c4f718 100644 --- a/ReactKit/Modules/RCTUIManager.m +++ b/ReactKit/Modules/RCTUIManager.m @@ -4,9 +4,9 @@ #import #import -#import #import "Layout.h" +#import "RCTAnimationType.h" #import "RCTAssert.h" #import "RCTBridge.h" #import "RCTConvert.h" @@ -22,8 +22,6 @@ #import "RCTViewManager.h" #import "UIView+ReactKit.h" -@class RCTAnimationConfig; - typedef void (^react_view_node_block_t)(id); static void RCTTraverseViewNodes(id view, react_view_node_block_t block) @@ -75,40 +73,151 @@ static NSDictionary *RCTViewModuleClasses(void) 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 { // Root views are only mutated on the shadow queue - NSDictionary *_viewManagers; NSMutableSet *_rootViewTags; NSMutableArray *_pendingUIBlocks; + NSLock *_pendingUIBlocksLock; - pthread_mutex_t _pendingUIBlocksMutex; - NSDictionary *_nextLayoutAnimationConfig; // RCT thread only - RCTResponseSenderBlock _nextLayoutAnimationCallback; // RCT thread only - RCTResponseSenderBlock _layoutAnimationCallbackMT; // Main thread only + // Animation + RCTLayoutAnimation *_nextLayoutAnimation; // RCT thread only + RCTLayoutAnimation *_layoutAnimation; // Main thread only + + // Keyed by moduleName + NSMutableDictionary *_defaultShadowViews; // RCT thread only + NSMutableDictionary *_defaultViews; // Main thread only + NSDictionary *_viewManagers; - NSMutableDictionary *_defaultShadowViews; - NSMutableDictionary *_defaultViews; + // Keyed by React tag + RCTSparseArray *_viewManagerRegistry; // RCT thread only + RCTSparseArray *_shadowViewRegistry; // RCT thread only + RCTSparseArray *_viewRegistry; // Main thread only __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 { if ((self = [super init])) { _bridge = bridge; - pthread_mutex_init(&_pendingUIBlocksMutex, NULL); + _pendingUIBlocksLock = [[NSLock alloc] init]; // Instantiate view managers NSMutableDictionary *viewManagers = [[NSMutableDictionary alloc] init]; @@ -116,16 +225,16 @@ static NSDictionary *RCTViewModuleClasses(void) viewManagers[moduleName] = [[moduleClass alloc] initWithEventDispatcher:_bridge.eventDispatcher]; }]; _viewManagers = viewManagers; + _defaultShadowViews = [[NSMutableDictionary alloc] init]; + _defaultViews = [[NSMutableDictionary alloc] init]; - _viewRegistry = [[RCTSparseArray alloc] init]; + _viewManagerRegistry = [[RCTSparseArray alloc] init]; _shadowViewRegistry = [[RCTSparseArray alloc] init]; + _viewRegistry = [[RCTSparseArray alloc] init]; // Internal resources _pendingUIBlocks = [[NSMutableArray alloc] init]; _rootViewTags = [[NSMutableSet alloc] init]; - - _defaultShadowViews = [[NSMutableDictionary alloc] init]; - _defaultViews = [[NSMutableDictionary alloc] init]; } return self; } @@ -138,7 +247,6 @@ static NSDictionary *RCTViewModuleClasses(void) - (void)dealloc { RCTAssert(!self.valid, @"must call -invalidate before -dealloc"); - pthread_mutex_destroy(&_pendingUIBlocksMutex); } - (BOOL)isValid @@ -153,9 +261,9 @@ static NSDictionary *RCTViewModuleClasses(void) _viewRegistry = nil; _shadowViewRegistry = nil; - pthread_mutex_lock(&_pendingUIBlocksMutex); + [_pendingUIBlocksLock lock]; _pendingUIBlocks = nil; - pthread_mutex_unlock(&_pendingUIBlocksMutex); + [_pendingUIBlocksLock unlock]; } - (void)registerRootView:(RCTRootView *)rootView; @@ -171,6 +279,9 @@ static NSDictionary *RCTViewModuleClasses(void) _viewRegistry[reactTag] = rootView; CGRect frame = rootView.frame; + // Register manager (TODO: should we do this, or leave it nil?) + _viewManagerRegistry[reactTag] = _viewManagers[[RCTViewManager moduleName]]; + // Register shadow view dispatch_async(_bridge.shadowQueue, ^{ @@ -192,7 +303,7 @@ static NSDictionary *RCTViewModuleClasses(void) { for (id child in children) { RCTTraverseViewNodes(registry[child.reactTag], ^(id subview) { - RCTAssert(![subview isReactRootView], @"Host views should not be unregistered"); + RCTAssert(![subview isReactRootView], @"Root views should not be unregistered"); if ([subview conformsToProtocol:@protocol(RCTInvalidating)]) { [(id)subview invalidate]; } @@ -203,8 +314,8 @@ static NSDictionary *RCTViewModuleClasses(void) - (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"); + __weak RCTUIManager *weakViewManager = self; __weak RCTSparseArray *weakViewRegistry = _viewRegistry; dispatch_block_t outerBlock = ^{ @@ -215,34 +326,12 @@ static NSDictionary *RCTViewModuleClasses(void) } }; - pthread_mutex_lock(&_pendingUIBlocksMutex); - [_pendingUIBlocks addObject:[outerBlock copy]]; - pthread_mutex_unlock(&_pendingUIBlocksMutex); + [_pendingUIBlocksLock lock]; + [_pendingUIBlocks addObject:outerBlock]; + [_pendingUIBlocksLock unlock]; } -- (void)setViewLayout:(UIView *)view withAnchorPoint:(CGPoint)anchorPoint position:(CGPoint)position bounds:(CGRect)bounds config:(RCTAnimationConfig *)config completion:(void (^)(BOOL finished))completion -{ - 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 `` 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 +- (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)rootShadowView { 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 // 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 - // the main thread. To help protect against mutation, we alias the variable to - // a threadsafe `NSArray`, however the `NSArray` doesn't guarantee deep - // 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)]; + // the main thread. + [rootShadowView collectRootUpdatedFrames:viewsWithNewFrames parentConstraint:(CGSize){CSS_UNDEFINED, CSS_UNDEFINED}]; // Parallel arrays NSMutableArray *frameReactTags = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; @@ -269,37 +354,75 @@ static NSDictionary *RCTViewModuleClasses(void) [areNew addObject:@(shadowView.isNewView)]; [parentsAreNew addObject:@(shadowView.superview.isNewView)]; } + for (RCTShadowView *shadowView in viewsWithNewFrames) { // We have to do this after we build the parentsAreNew array. shadowView.newView = NO; } - NSArray *immutableFrameReactTags = frameReactTags; - NSArray *immutableFrames = frames; - - NSNumber *rootViewTag = hostShadowView.reactTag; - return ^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { - for (NSUInteger ii = 0; ii < immutableFrames.count; ii++) { - NSNumber *reactTag = immutableFrameReactTags[ii]; + // Perform layout (possibly animated) + NSNumber *rootViewTag = rootShadowView.reactTag; + return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + for (NSUInteger ii = 0; ii < frames.count; ii++) { + NSNumber *reactTag = frameReactTags[ii]; 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 // views are anchorPoint = center for easier scale and rotation animations. // Convert the frame so it works with anchorPoint = center. - __weak RCTUIManager *weakSelf = self; - [self setViewLayout:view - withAnchorPoint:CGPointMake(0.5, 0.5) - position:CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame)) - bounds:CGRectMake(0, 0, frame.size.width, frame.size.height) - config:/*!isNew ? _layoutAnimationConfigMT.updateConfig : */nil // TODO: !!! - completion:^(BOOL finished) { - __strong RCTUIManager *strongSelf = weakSelf; - if (strongSelf->_layoutAnimationCallbackMT) { - strongSelf->_layoutAnimationCallbackMT(@[@(finished)]); - } - }]; + CGPoint position = {CGRectGetMidX(frame), CGRectGetMidY(frame)}; + CGRect bounds = {0, 0, frame.size}; + + // Avoid crashes due to nan coords + 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)); + continue; + } + + 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]; RCTTraverseViewNodes(rootView, ^(id view) { if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) { @@ -314,7 +437,7 @@ static NSDictionary *RCTViewModuleClasses(void) NSMutableSet *applierBlocks = [NSMutableSet setWithCapacity:1]; [topView collectUpdatedProperties:applierBlocks parentProperties:@{}]; - [self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { for (RCTApplierBlock block in applierBlocks) { block(viewRegistry); } @@ -391,10 +514,10 @@ static NSDictionary *RCTViewModuleClasses(void) [self _purgeChildren:@[rootShadowView] fromRegistry:_shadowViewRegistry]; [_rootViewTags removeObject:rootReactTag]; - [self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){ + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ RCTCAssertMainThread(); UIView *rootView = viewRegistry[rootReactTag]; - [viewManager _purgeChildren:@[rootView] fromRegistry:viewRegistry]; + [uiManager _purgeChildren:@[rootView] fromRegistry:viewRegistry]; }]; } @@ -437,15 +560,15 @@ static NSDictionary *RCTViewModuleClasses(void) removeAtIndices:removeAtIndices registry:_shadowViewRegistry]; - [self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){ - RCTCAssertMainThread(); - [viewManager _manageChildren:containerReactTag - moveFromIndices:moveFromIndices - moveToIndices:moveToIndices - addChildReactTags:addChildReactTags - addAtIndices:addAtIndices - removeAtIndices:removeAtIndices - registry:viewRegistry]; + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ + + [uiManager _manageChildren:containerReactTag + moveFromIndices:moveFromIndices + moveToIndices:moveToIndices + addChildReactTags:addChildReactTags + addAtIndices:addAtIndices + removeAtIndices:removeAtIndices + registry:viewRegistry]; }]; } @@ -549,8 +672,12 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView { 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 if (!_defaultShadowViews[moduleName]) { _defaultShadowViews[moduleName] = [manager shadowView]; @@ -565,6 +692,9 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ RCTCAssertMainThread(); + // Register manager (TODO: should we do this, or leave it nil?) + uiManager->_viewManagerRegistry[reactTag] = manager; + // Generate default view, used for resetting default props if (!uiManager->_defaultViews[moduleName]) { // 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]; if (view) { + // Set required properties view.reactTag = reactTag; 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(); + RCTViewManager *viewManager = _viewManagerRegistry[reactTag]; + NSString *moduleName = [[viewManager class] moduleName]; + RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; - RCTViewManager *manager = [self _managerInstanceForViewWithModuleName:moduleName]; - RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[moduleName], manager); + RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[moduleName], viewManager); [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - RCTCAssertMainThread(); - UIView *view = viewRegistry[reactTag]; - RCTSetViewProps(props, view, uiManager->_defaultViews[moduleName], manager); + UIView *view = uiManager->_viewRegistry[reactTag]; + RCTSetViewProps(props, view, uiManager->_defaultViews[moduleName], viewManager); }]; } @@ -608,7 +741,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView RCT_EXPORT(focus); if (!reactTag) return; - [self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { UIView *newResponder = viewRegistry[reactTag]; [newResponder becomeFirstResponder]; }]; @@ -619,7 +752,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView RCT_EXPORT(blur); if (!reactTag) return; - [self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){ + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ UIView *currentResponder = viewRegistry[reactTag]; [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 // pending blocks to a new array. This guards against mutation while // processing the pending blocks in another thread. - for (RCTViewManager *manager in _viewManagers.allValues) { RCTViewManagerUIBlock uiBlock = [manager uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry]; - if (uiBlock != nil) { + if (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) { RCTShadowView *rootView = _shadowViewRegistry[reactTag]; [self addUIBlock:[self uiBlockWithLayoutUpdateForRootView: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; _pendingUIBlocks = [[NSMutableArray alloc] init]; - pthread_mutex_unlock(&_pendingUIBlocksMutex); + [_pendingUIBlocksLock unlock]; dispatch_async(dispatch_get_main_queue(), ^{ 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 { RCT_EXPORT(); @@ -675,7 +814,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView return; } - [self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { UIView *view = viewRegistry[reactTag]; if (!view) { RCTLogError(@"measure cannot find view with tag %zd", reactTag); @@ -705,7 +844,6 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView }]; } - - (void)requestSchedulingJavaScriptNavigation:(NSNumber *)reactTag errorCallback:(RCTResponseSenderBlock)errorCallback callback:(RCTResponseSenderBlock)callback @@ -716,7 +854,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView RCTLogError(@"Callback not provided for navigation scheduling."); return; } - [self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){ + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ if (reactTag) { //TODO: This is nasty - why is RCTNavigator hard-coded? id rkObject = viewRegistry[reactTag]; @@ -870,24 +1008,24 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView { 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 "`nativeMainScrollDelegate`" // - The one designated main scroll view should have the one designated // `nativeMainScrollDelegate` set as its `nativeMainScrollDelegate`. - if (viewManager.mainScrollView) { - viewManager.mainScrollView.nativeMainScrollDelegate = nil; + if (uiManager.mainScrollView) { + uiManager.mainScrollView.nativeMainScrollDelegate = nil; } if (reactTag) { id rkObject = viewRegistry[reactTag]; if ([rkObject conformsToProtocol:@protocol(RCTScrollableProtocol)]) { - viewManager.mainScrollView = (id)rkObject; - ((id)rkObject).nativeMainScrollDelegate = viewManager.nativeMainScrollDelegate; + uiManager.mainScrollView = (id)rkObject; + ((id)rkObject).nativeMainScrollDelegate = uiManager.nativeMainScrollDelegate; } else { RCTCAssert(NO, @"Tag %@ does not conform to RCTScrollableProtocol", reactTag); } } else { - viewManager.mainScrollView = nil; + uiManager.mainScrollView = nil; } }]; } @@ -896,7 +1034,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView { RCT_EXPORT(scrollTo); - [self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){ + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ UIView *view = viewRegistry[reactTag]; if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { [(id)view scrollToOffset:CGPointMake([offsetX floatValue], [offsetY floatValue])]; @@ -910,7 +1048,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView { RCT_EXPORT(zoomToRect); - [self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){ + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ UIView *view = viewRegistry[reactTag]; if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { [(id)view zoomToRect:[RCTConvert CGRect:rectDict] animated:YES]; @@ -924,7 +1062,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView { RCT_EXPORT(); - [self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { UIView *view = viewRegistry[reactTag]; if (!view) { NSString *error = [[NSString alloc] initWithFormat:@"cannot find view with tag %@", reactTag]; @@ -947,7 +1085,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView { RCT_EXPORT(); - [self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { _jsResponder = viewRegistry[reactTag]; if (!_jsResponder) { 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(); - [self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { _jsResponder = nil; }]; } @@ -1189,18 +1327,19 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView return allJSConstants; } -- (void)configureNextLayoutAnimation:(NSDictionary *)config withCallback:(RCTResponseSenderBlock)callback errorCallback:(RCTResponseSenderBlock)errorCallback +- (void)configureNextLayoutAnimation:(NSDictionary *)config + withCallback:(RCTResponseSenderBlock)callback + errorCallback:(RCTResponseSenderBlock)errorCallback { RCT_EXPORT(); - if (_nextLayoutAnimationCallback || _nextLayoutAnimationConfig) { - RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", _nextLayoutAnimationConfig, config); + if (_nextLayoutAnimation) { + RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", _nextLayoutAnimation, config); } if (config[@"delete"] != nil) { RCTLogError(@"LayoutAnimation only supports create and update right now. Config: %@", config); } - _nextLayoutAnimationConfig = config; - _nextLayoutAnimationCallback = callback; + _nextLayoutAnimation = [[RCTLayoutAnimation alloc] initWithDictionary:config callback:callback]; } static UIView *_jsResponder; diff --git a/ReactKit/ReactKit.xcodeproj/project.pbxproj b/ReactKit/ReactKit.xcodeproj/project.pbxproj index 483e4bc55..42ca15dd3 100644 --- a/ReactKit/ReactKit.xcodeproj/project.pbxproj +++ b/ReactKit/ReactKit.xcodeproj/project.pbxproj @@ -124,6 +124,7 @@ 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWrapperViewController.m; sourceTree = ""; }; 13B080271A694C4900A75B9A /* RCTDataManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDataManager.h; sourceTree = ""; }; 13B080281A694C4900A75B9A /* RCTDataManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDataManager.m; sourceTree = ""; }; + 13DB9D681A8CC58200429C20 /* RCTAnimationType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationType.h; sourceTree = ""; }; 13E067481A70F434002CDEE1 /* RCTUIManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUIManager.h; sourceTree = ""; }; 13E067491A70F434002CDEE1 /* RCTUIManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIManager.m; sourceTree = ""; }; 13E0674B1A70F44B002CDEE1 /* RCTShadowView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowView.h; sourceTree = ""; }; @@ -330,6 +331,7 @@ 137029521A69923600575408 /* RCTImageDownloader.m */, 13B07FCD1A683B5F00A75B9A /* RCTScrollableProtocol.h */, 13ED13891A80C9D40050A8F9 /* RCTPointerEvents.h */, + 13DB9D681A8CC58200429C20 /* RCTAnimationType.h */, ); path = Base; sourceTree = ""; diff --git a/ReactKit/Views/RCTShadowView.h b/ReactKit/Views/RCTShadowView.h index afe1d4cf8..1a75fdb08 100644 --- a/ReactKit/Views/RCTShadowView.h +++ b/ReactKit/Views/RCTShadowView.h @@ -14,7 +14,7 @@ typedef NS_ENUM(NSUInteger, RCTLayoutLifecycle) { RCTLayoutLifecycleDirtied, }; -// TODO: is this redundact now? +// TODO: is this still needed? typedef NS_ENUM(NSUInteger, RCTPropagationLifecycle) { RCTPropagationLifecycleUninitialized = 0, RCTPropagationLifecycleComputed, @@ -72,7 +72,8 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *); @property (nonatomic, assign) CGFloat height; @property (nonatomic, assign) CGRect frame; -- (void)updateShadowViewLayout; +- (void)setTopLeft:(CGPoint)topLeft; +- (void)setSize:(CGSize)size; /** * 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 - (void)dirtyLayout; -- (void)dirtyPropagation; +- (BOOL)isLayoutDirty; +// TODO: is this still needed? +- (void)dirtyPropagation; +- (BOOL)isPropagationDirty; + +// TODO: move this to text node? - (void)dirtyText; - (BOOL)isTextDirty; - (void)setTextComputed; +/** + * Triggers a recalculation of the shadow view's layout. + */ +- (void)updateShadowViewLayout; + /** * 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 diff --git a/ReactKit/Views/RCTShadowView.m b/ReactKit/Views/RCTShadowView.m index 84ce9ab0d..3872570b1 100644 --- a/ReactKit/Views/RCTShadowView.m +++ b/ReactKit/Views/RCTShadowView.m @@ -103,7 +103,7 @@ static css_node_t *RCTGetChild(void *context, int i) static bool RCTIsDirty(void *context) { RCTShadowView *shadowView = (__bridge RCTShadowView *)context; - return shadowView.layoutLifecycle != RCTLayoutLifecycleComputed; + return [shadowView isLayoutDirty]; } // 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 { 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 { if (_textLifecycle != RCTTextLifecycleDirtied) { @@ -391,23 +401,6 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st 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 #define RCT_MARGIN_PROPERTY(prop, metaProp) \ @@ -503,6 +496,20 @@ RCT_POSITION_PROPERTY(Left, left, LEFT) [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 #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]; } +- (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 diff --git a/ReactKit/Views/RCTTextManager.m b/ReactKit/Views/RCTTextManager.m index 97944e34e..fe1ca6820 100644 --- a/ReactKit/Views/RCTTextManager.m +++ b/ReactKit/Views/RCTTextManager.m @@ -80,12 +80,12 @@ RCT_REMAP_VIEW_PROPERTY(containerBackgroundColor, backgroundColor) // layout to copy its properties across? - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry { - NSMutableArray *shadowBlocks = [NSMutableArray new]; + NSMutableArray *uiBlocks = [NSMutableArray new]; // TODO: are modules global, or specific to a given rootView? for (RCTShadowView *rootView in shadowViewRegistry.allObjects) { if (![rootView isReactRootView]) { - // This isn't a host view + // This isn't a root view continue; } @@ -117,7 +117,7 @@ RCT_REMAP_VIEW_PROPERTY(containerBackgroundColor, backgroundColor) [shadowView setTextComputed]; } - [shadowBlocks addObject:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { + [uiBlocks addObject:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { [reactTaggedAttributedStrings enumerateObjectsUsingBlock:^(NSAttributedString *attributedString, NSNumber *reactTag, BOOL *stop) { RCTText *text = viewRegistry[reactTag]; text.attributedText = attributedString; @@ -125,9 +125,9 @@ RCT_REMAP_VIEW_PROPERTY(containerBackgroundColor, backgroundColor) }]; } - return ^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) { - for (RCTViewManagerUIBlock shadowBlock in shadowBlocks) { - shadowBlock(viewManager, viewRegistry); + return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + for (RCTViewManagerUIBlock shadowBlock in uiBlocks) { + shadowBlock(uiManager, viewRegistry); } }; } diff --git a/packager/react-packager/src/DependencyResolver/haste/index.js b/packager/react-packager/src/DependencyResolver/haste/index.js index 211da3655..13525bdab 100644 --- a/packager/react-packager/src/DependencyResolver/haste/index.js +++ b/packager/react-packager/src/DependencyResolver/haste/index.js @@ -5,10 +5,6 @@ var FileWatcher = require('../../FileWatcher'); var DependencyGraph = require('./DependencyGraph'); var ModuleDescriptor = require('../ModuleDescriptor'); -var DEFAULT_POLYFILLS = [ - -]; - var DEFINE_MODULE_CODE = '__d(' + '\'_moduleName_\',' + @@ -39,6 +35,8 @@ function HasteDependencyResolver(config) { : path.join(__dirname, 'polyfills/prelude.js'), path.join(__dirname, 'polyfills/require.js'), path.join(__dirname, 'polyfills/polyfills.js'), + path.join(__dirname, 'polyfills/console.js'), + path.join(__dirname, 'polyfills/error-guard.js'), ].concat( config.polyfillModuleNames || [] ); diff --git a/packager/polyfill/console.js b/packager/react-packager/src/DependencyResolver/haste/polyfills/console.js similarity index 100% rename from packager/polyfill/console.js rename to packager/react-packager/src/DependencyResolver/haste/polyfills/console.js diff --git a/packager/polyfill/error-guard.js b/packager/react-packager/src/DependencyResolver/haste/polyfills/error-guard.js similarity index 100% rename from packager/polyfill/error-guard.js rename to packager/react-packager/src/DependencyResolver/haste/polyfills/error-guard.js