diff --git a/.travis.yml b/.travis.yml index 603e924bb5b5cd..7da54bdf4076ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,28 @@ language: objective-c +osx_image: beta-xcode6.3 + cache: - directories: - - node_modules + directories: + - node_modules + - .nvm before_install: - brew update install: - - brew reinstall flow xctool + - brew reinstall flow xctool nvm + - mkdir -p .nvm + - cp $(brew --prefix nvm)/nvm-exec .nvm/ + - export NVM_DIR=.nvm + - source $(brew --prefix nvm)/nvm.sh + - nvm install v0.10 - npm config set spin=false - npm install script: - | + nvm use v0.10 + if [ "$TEST_TYPE" = objc ] then diff --git a/Examples/UIExplorer/BorderExample.js b/Examples/UIExplorer/BorderExample.js index 7789459f80bc8e..a9436108d651a7 100644 --- a/Examples/UIExplorer/BorderExample.js +++ b/Examples/UIExplorer/BorderExample.js @@ -81,11 +81,15 @@ var styles = StyleSheet.create({ borderTopLeftRadius: 100, }, border7: { - borderRadius: 20, + borderWidth: 10, + borderColor: 'rgba(255,0,0,0.5)', + borderRadius: 30, + overflow: 'hidden', }, border7_inner: { backgroundColor: 'blue', - flex: 1, + width: 100, + height: 100 }, }); diff --git a/Examples/UIExplorer/TextExample.ios.js b/Examples/UIExplorer/TextExample.ios.js index 10323c7a50ba7a..b2fd717adda4e3 100644 --- a/Examples/UIExplorer/TextExample.ios.js +++ b/Examples/UIExplorer/TextExample.ios.js @@ -300,17 +300,17 @@ exports.examples = [ title: 'containerBackgroundColor attribute', render: function() { return ( - - - - + + + + - + Default containerBackgroundColor (inherited) + backgroundColor wash + {marginBottom: 5, containerBackgroundColor: 'transparent'}]}> {"containerBackgroundColor: 'transparent' + backgroundColor wash"} @@ -322,13 +322,13 @@ exports.examples = [ return ( - Maximum of one line no matter now much I write here. If I keep writing it{"'"}ll just truncate after one line + Maximum of one line, no matter how much I write here. If I keep writing, it{"'"}ll just truncate after one line. - Maximum of two lines no matter now much I write here. If I keep writing it{"'"}ll just truncate after two lines + Maximum of two lines, no matter how much I write here. If I keep writing, it{"'"}ll just truncate after two lines. - No maximum lines specified no matter now much I write here. If I keep writing it{"'"}ll just keep going and going + No maximum lines specified, no matter how much I write here. If I keep writing, it{"'"}ll just keep going and going. ); @@ -337,7 +337,8 @@ exports.examples = [ var styles = StyleSheet.create({ backgroundColorText: { - left: 5, + margin: 5, + marginBottom: 0, backgroundColor: 'rgba(100, 100, 100, 0.3)' }, entity: { diff --git a/Examples/UIExplorer/UIExplorerBlock.js b/Examples/UIExplorer/UIExplorerBlock.js index 10a2b57cdc8a78..649b8994ff032f 100644 --- a/Examples/UIExplorer/UIExplorerBlock.js +++ b/Examples/UIExplorer/UIExplorerBlock.js @@ -77,13 +77,7 @@ var styles = StyleSheet.create({ paddingHorizontal: 10, paddingVertical: 5, }, - titleRow: { - flexDirection: 'row', - justifyContent: 'space-between', - backgroundColor: 'transparent', - }, titleText: { - backgroundColor: 'transparent', fontSize: 14, fontWeight: '500', }, @@ -101,8 +95,7 @@ var styles = StyleSheet.create({ height: 8, }, children: { - backgroundColor: 'transparent', - padding: 10, + margin: 10, } }); diff --git a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testLayoutExampleSnapshot_1@2x.png b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testLayoutExampleSnapshot_1@2x.png index b213ce9fb32bcd..bfa2091b38f525 100644 Binary files a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testLayoutExampleSnapshot_1@2x.png and b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testLayoutExampleSnapshot_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testSwitchExampleSnapshot_1@2x.png b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testSwitchExampleSnapshot_1@2x.png index 04395fa6b403c1..7cf2516faa0db8 100644 Binary files a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testSwitchExampleSnapshot_1@2x.png and b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testSwitchExampleSnapshot_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTextExampleSnapshot_1@2x.png b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTextExampleSnapshot_1@2x.png index 488db9a2d1f8ed..f869159a2c88ea 100644 Binary files a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTextExampleSnapshot_1@2x.png and b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTextExampleSnapshot_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testViewExampleSnapshot_1@2x.png b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testViewExampleSnapshot_1@2x.png index 9fa4e48b2a5634..d37b708794c43a 100644 Binary files a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testViewExampleSnapshot_1@2x.png and b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testViewExampleSnapshot_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m b/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m index 8ec59e5252c722..45df4e4179469d 100644 --- a/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m +++ b/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m @@ -38,7 +38,7 @@ - (void)setUp RCTAssert(!__LP64__, @"Snapshot tests should be run on 32-bit device simulators (e.g. iPhone 5)"); #endif NSString *version = [[UIDevice currentDevice] systemVersion]; - RCTAssert([version isEqualToString:@"8.1"], @"Snapshot tests should be run on iOS 8.1, found %@", version); + RCTAssert([version isEqualToString:@"8.3"], @"Snapshot tests should be run on iOS 8.3, found %@", version); _runner = RCTInitRunnerForApp(@"Examples/UIExplorer/UIExplorerApp"); // If tests have changes, set recordMode = YES below and run the affected diff --git a/Libraries/Animation/__tests__/AnimationUtils-test.js b/Libraries/Animation/__tests__/AnimationUtils-test.js deleted file mode 100644 index 32c73ec604dceb..00000000000000 --- a/Libraries/Animation/__tests__/AnimationUtils-test.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -'use strict'; - -jest.autoMockOff(); - -var AnimationUtils = require('AnimationUtils'); - -describe('AnimationUtils', function() { - var DURATION = 300; - - var Samples = { - easeInQuad: [0,0.0030864197530864196,0.012345679012345678,0.027777777777777776,0.04938271604938271,0.0771604938271605,0.1111111111111111,0.15123456790123457,0.19753086419753085,0.25,0.308641975308642,0.37345679012345684,0.4444444444444444,0.5216049382716049,0.6049382716049383,0.6944444444444445,0.7901234567901234,0.8919753086419753,1], - easeOutQuad: [0,0.10802469135802469,0.20987654320987653,0.3055555555555555,0.3950617283950617,0.47839506172839513,0.5555555555555556,0.6265432098765432,0.691358024691358,0.75,0.8024691358024691,0.8487654320987654,0.888888888888889,0.9228395061728394,0.9506172839506174,0.9722222222222221,0.9876543209876543,0.9969135802469136,1], - easeInOutQuad: [0,0.006172839506172839,0.024691358024691357,0.05555555555555555,0.09876543209876543,0.154320987654321,0.2222222222222222,0.30246913580246915,0.3950617283950617,0.5,0.6049382716049383,0.697530864197531,0.7777777777777777,0.845679012345679,0.9012345679012346,0.9444444444444444,0.9753086419753086,0.9938271604938271,1], - easeInCubic: [0,0.00017146776406035664,0.0013717421124828531,0.004629629629629629,0.010973936899862825,0.021433470507544586,0.037037037037037035,0.05881344307270234,0.0877914951989026,0.125,0.1714677640603567,0.22822359396433475,0.2962962962962963,0.37671467764060357,0.4705075445816187,0.5787037037037038,0.7023319615912208,0.8424211248285322,1], - easeOutCubic: [0,0.15757887517146785,0.2976680384087792,0.42129629629629617,0.5294924554183813,0.6232853223593964,0.7037037037037036,0.7717764060356652,0.8285322359396433,0.875,0.9122085048010974,0.9411865569272977,0.9629629629629629,0.9785665294924554,0.9890260631001372,0.9953703703703703,0.9986282578875172,0.9998285322359396,1], - easeInOutCubic: [0,0.0006858710562414266,0.0054869684499314125,0.018518518518518517,0.0438957475994513,0.08573388203017834,0.14814814814814814,0.23525377229080935,0.3511659807956104,0.5,0.6488340192043895,0.7647462277091908,0.8518518518518519,0.9142661179698217,0.9561042524005487,0.9814814814814815,0.9945130315500685,0.9993141289437586,1], - easeInQuart: [0,0.000009525986892242035,0.00015241579027587256,0.0007716049382716049,0.002438652644413961,0.005953741807651274,0.012345679012345678,0.02287189452827313,0.039018442310623375,0.0625,0.09525986892242039,0.1394699740893157,0.19753086419753085,0.2720717116293248,0.3659503124523701,0.48225308641975323,0.624295076969974,0.7956199512269471,1], - easeOutQuart: [0,0.20438004877305294,0.375704923030026,0.5177469135802468,0.6340496875476299,0.7279282883706752,0.802469135802469,0.8605300259106843,0.9047401310775796,0.9375,0.9609815576893767,0.9771281054717269,0.9876543209876543,0.9940462581923487,0.997561347355586,0.9992283950617284,0.9998475842097241,0.9999904740131078,1], - easeInOutQuart: [0,0.00007620789513793628,0.0012193263222069805,0.006172839506172839,0.019509221155311687,0.047629934461210194,0.09876543209876543,0.18297515622618504,0.312147538484987,0.5,0.687852461515013,0.8170248437738151,0.9012345679012346,0.9523700655387898,0.9804907788446883,0.9938271604938271,0.998780673677793,0.999923792104862,1], - easeInQuint: [0,5.292214940134463e-7,0.000016935087808430282,0.00012860082304526747,0.000541922809869769,0.0016538171687920206,0.004115226337448559,0.008894625649883995,0.01734152991583261,0.03125,0.05292214940134466,0.08523165083235959,0.1316872427983539,0.1964962361767346,0.28462802079628785,0.401877572016461,0.5549289573066435,0.75141884282545,1], - easeOutQuint: [0,0.24858115717454998,0.4450710426933565,0.598122427983539,0.7153719792037121,0.8035037638232654,0.868312757201646,0.9147683491676404,0.9470778505986553,0.96875,0.9826584700841674,0.991105374350116,0.9958847736625515,0.998346182831208,0.9994580771901302,0.9998713991769548,0.9999830649121916,0.999999470778506,1], - easeInOutQuint: [0,0.000008467543904215141,0.0002709614049348845,0.0020576131687242796,0.008670764957916305,0.02646107470067233,0.06584362139917695,0.14231401039814393,0.27746447865332174,0.5,0.7225355213466782,0.8576859896018563,0.934156378600823,0.9735389252993276,0.9913292350420837,0.9979423868312757,0.9997290385950651,0.9999915324560957,1], - easeInSine: [0,0.003805301908254455,0.01519224698779198,0.03407417371093169,0.06030737921409157,0.09369221296335006,0.1339745962155613,0.1808479557110082,0.233955556881022,0.2928932188134524,0.35721239031346064,0.42642356364895384,0.4999999999999999,0.5773817382593005,0.6579798566743311,0.7411809548974793,0.8263518223330696,0.9128442572523416,0.9999999999999999], - easeOutSine: [0,0.08715574274765817,0.17364817766693033,0.25881904510252074,0.3420201433256687,0.42261826174069944,0.49999999999999994,0.573576436351046,0.6427876096865393,0.7071067811865475,0.766044443118978,0.8191520442889918,0.8660254037844386,0.9063077870366499,0.9396926207859083,0.9659258262890683,0.984807753012208,0.9961946980917455,1], - easeInOutSine: [0,0.00759612349389599,0.030153689607045786,0.06698729810778065,0.116977778440511,0.17860619515673032,0.24999999999999994,0.32898992833716556,0.4131759111665348,0.49999999999999994,0.5868240888334652,0.6710100716628343,0.7499999999999999,0.8213938048432696,0.883022221559489,0.9330127018922194,0.9698463103929542,0.9924038765061041,1], - easeInExpo: [0,0.0014352875901128893,0.002109491677524035,0.0031003926796253885,0.004556754060844206,0.006697218616039631,0.009843133202303688,0.014466792379488908,0.021262343752724643,0.03125,0.045929202883612456,0.06750373368076916,0.09921256574801243,0.1458161299470146,0.2143109957132682,0.31498026247371835,0.46293735614364506,0.6803950000871883,1], - easeOutExpo: [0,0.31960499991281155,0.5370626438563548,0.6850197375262816,0.7856890042867318,0.8541838700529854,0.9007874342519875,0.9324962663192309,0.9540707971163875,0.96875,0.9787376562472754,0.9855332076205111,0.9901568667976963,0.9933027813839603,0.9954432459391558,0.9968996073203746,0.9978905083224759,0.9985647124098871,1], - easeInOutExpo: [0,0.0010547458387620175,0.002278377030422103,0.004921566601151844,0.010631171876362321,0.022964601441806228,0.049606282874006216,0.1071554978566341,0.23146867807182253,0.5,0.7685313219281775,0.892844502143366,0.9503937171259937,0.9770353985581938,0.9893688281236377,0.9950784333988482,0.9977216229695779,0.998945254161238,1], - easeInCirc: [0,0.0015444024660317135,0.006192010000093506,0.013986702816730645,0.025003956956430873,0.03935464078941209,0.057190958417936644,0.07871533601238889,0.10419358352238339,0.1339745962155614,0.1685205807169019,0.20845517506805522,0.2546440075000701,0.3083389112228482,0.37146063894529113,0.4472292016074334,0.5418771527091488,0.6713289009389102,1], - easeOutCirc: [0,0.3286710990610898,0.45812284729085123,0.5527707983925666,0.6285393610547089,0.6916610887771518,0.7453559924999298,0.7915448249319448,0.8314794192830981,0.8660254037844386,0.8958064164776166,0.9212846639876111,0.9428090415820634,0.9606453592105879,0.9749960430435691,0.9860132971832694,0.9938079899999065,0.9984555975339683,1], - easeInOutCirc: [0,0.003096005000046753,0.012501978478215436,0.028595479208968322,0.052096791761191696,0.08426029035845095,0.12732200375003505,0.18573031947264557,0.2709385763545744,0.5,0.7290614236454256,0.8142696805273546,0.8726779962499649,0.915739709641549,0.9479032082388084,0.9714045207910317,0.9874980215217846,0.9969039949999532,1], - easeInElastic: [0,0.0008570943160003016,0.0020526300563455885,0.0005383775388688477,-0.003807112477441741,-0.005595444524068916,0.0017092421431128787,0.014076838118604966,0.012696991251677569,-0.015625000000000045,-0.045618646044515744,-0.01936028903971309,0.07600123467884114,0.13030605320629246,-0.012461076179381799,-0.29598462833976175,-0.3176868895106366,0.2694906924487451,1], - easeOutElastic: [0,0.7305093075512543,1.3176868895106364,1.2959846283397618,1.0124610761793817,0.8696939467937076,0.9239987653211588,1.019360289039713,1.0456186460445158,1.015625,0.9873030087483224,0.9859231618813951,0.9982907578568871,1.005595444524069,1.0038071124774417,0.9994616224611311,0.9979473699436544,0.9991429056839997,1], - easeInOutElastic: [0,0.0010420781824747765,-0.0003083357248478688,-0.004888288728445655,0.0010292130059457788,0.022895545534212507,-0.0028843488305936938,-0.10707491183281304,0.004488485931276091,0.5,0.995511514068724,1.107074911832813,1.0028843488305939,0.9771044544657875,0.9989707869940542,1.0048882887284456,1.000308335724848,0.9989579218175252,1], - easeInBack: [0,-0.004788556241426612,-0.017301289437585736,-0.0347587962962963,-0.05438167352537723,-0.07339051783264748,-0.08900592592592595,-0.09844849451303156,-0.0989388203017833,-0.08769750000000004,-0.06194513031550073,-0.018902307956104283,0.044210370370370254,0.13017230795610413,0.2417629080932785,0.3817615740740742,0.5529477091906719,0.7581007167352535,0.9999999999999998], - easeOutBack: [2.220446049250313e-16,0.24189928326474652,0.44705229080932807,0.6182384259259258,0.7582370919067215,0.8698276920438959,0.9557896296296297,1.0189023079561044,1.0619451303155008,1.0876975,1.0989388203017834,1.0984484945130315,1.089005925925926,1.0733905178326475,1.0543816735253773,1.0347587962962963,1.0173012894375857,1.0047885562414267,1], - easeInOutBack: [0,-0.01355231550068587,-0.04434668449931412,-0.07758924074074074,-0.09848611796982167,-0.0922434499314129,-0.0440673703703704,0.060835986968449905,0.237260488340192,0.5,0.762739511659808,0.9391640130315503,1.0440673703703702,1.0922434499314129,1.0984861179698218,1.0775892407407408,1.0443466844993141,1.0135523155006858,1], - }; - - Object.keys(Samples).forEach(function(type) { - it('should interpolate ' + type, function() { - expect(AnimationUtils.evaluateEasingFunction(DURATION, type)) - .toEqual(Samples[type]); - }); - }); -}); diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index dc6d05eb92b714..03f374b67aa85e 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -407,15 +407,23 @@ var TextInput = React.createClass({ } }, + getChildContext: function(): Object { + return {isInAParentText: true}; + }, + + childContextTypes: { + isInAParentText: React.PropTypes.bool + }, + render: function() { if (Platform.OS === 'ios') { - return this._renderIOs(); + return this._renderIOS(); } else if (Platform.OS === 'android') { return this._renderAndroid(); } }, - _renderIOs: function() { + _renderIOS: function() { var textContainer; var autoCapitalize = autoCapitalizeConsts[this.props.autoCapitalize]; @@ -515,7 +523,8 @@ var TextInput = React.createClass({ return ( + rejectResponderTermination={true} + testID={this.props.testID}> {textContainer} ); @@ -523,6 +532,16 @@ var TextInput = React.createClass({ _renderAndroid: function() { var autoCapitalize = autoCapitalizeConsts[this.props.autoCapitalize]; + var children = this.props.children; + var childCount = 0; + ReactChildren.forEach(children, () => ++childCount); + invariant( + !(this.props.value && childCount), + 'Cannot specify both value and children.' + ); + if (childCount > 1) { + children = {children}; + } var textContainer = ; return ( diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index 1ebbc70aa0220e..f75430b7790ecd 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -549,11 +549,14 @@ var Navigator = React.createClass({ this.spring.getCurrentValue() ); } else if (this.state.activeGesture != null) { - this._transitionBetween( - this.state.presentedIndex, - this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture), - this.spring.getCurrentValue() - ); + var presentedToIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture); + if (presentedToIndex > -1) { + this._transitionBetween( + this.state.presentedIndex, + presentedToIndex, + this.spring.getCurrentValue() + ); + } } }, diff --git a/Libraries/ReactIOS/InspectorOverlay.js b/Libraries/ReactIOS/InspectorOverlay.js index a70d3b85d43f84..8b5c6c0cb2ab8e 100644 --- a/Libraries/ReactIOS/InspectorOverlay.js +++ b/Libraries/ReactIOS/InspectorOverlay.js @@ -75,7 +75,9 @@ var InspectorOverlay = React.createClass({ var ElementProperties = React.createClass({ render: function() { - var path = this.props.hierarchy.map((instance) => instance.getName()).join(' > '); + var path = this.props.hierarchy.map((instance) => { + return instance.getName ? instance.getName() : 'Unknown'; + }).join(' > '); return ( diff --git a/Libraries/ReactNative/ReactNativeStyleAttributes.js b/Libraries/ReactNative/ReactNativeStyleAttributes.js index 9adaf342adc55a..43b5902a64accc 100644 --- a/Libraries/ReactNative/ReactNativeStyleAttributes.js +++ b/Libraries/ReactNative/ReactNativeStyleAttributes.js @@ -29,4 +29,7 @@ var ReactNativeStyleAttributes = { ReactNativeStyleAttributes.transformMatrix = { diff: matricesDiffer }; ReactNativeStyleAttributes.shadowOffset = { diff: sizesDiffer }; +// Do not rely on this attribute. +ReactNativeStyleAttributes.decomposedMatrix = 'decomposedMatrix'; + module.exports = ReactNativeStyleAttributes; diff --git a/Libraries/Text/RCTShadowText.h b/Libraries/Text/RCTShadowText.h index c93f13184258cc..189bff79e4c109 100644 --- a/Libraries/Text/RCTShadowText.h +++ b/Libraries/Text/RCTShadowText.h @@ -29,5 +29,6 @@ extern NSString *const RCTReactTagAttributeName; @property (nonatomic, assign) NSWritingDirection writingDirection; - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width; +- (void)recomputeText; @end diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index dd880340daab70..7e1daf90855279 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -72,6 +72,12 @@ - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width return textStorage; } +- (void)recomputeText +{ + [self attributedString]; + [self setTextComputed]; +} + - (NSAttributedString *)attributedString { return [self _attributedStringWithFontFamily:nil @@ -125,13 +131,13 @@ - (NSAttributedString *)_attributedStringWithFontFamily:(NSString *)fontFamily } if (_color) { - [self _addAttribute:NSForegroundColorAttributeName withValue:self.color toAttributedString:attributedString]; + [self _addAttribute:NSForegroundColorAttributeName withValue:_color toAttributedString:attributedString]; } if (_isHighlighted) { [self _addAttribute:RCTIsHighlightedAttributeName withValue:@YES toAttributedString:attributedString]; } if (_textBackgroundColor) { - [self _addAttribute:NSBackgroundColorAttributeName withValue:self.textBackgroundColor toAttributedString:attributedString]; + [self _addAttribute:NSBackgroundColorAttributeName withValue:_textBackgroundColor toAttributedString:attributedString]; } UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily size:fontSize weight:fontWeight style:fontStyle]; diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index cdb09d4ff28d06..1ae432d90e4c99 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -16,12 +16,14 @@ @implementation RCTText { NSTextStorage *_textStorage; + NSMutableArray *_reactSubviews; } - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { _textStorage = [[NSTextStorage alloc] init]; + _reactSubviews = [NSMutableArray array]; self.isAccessibilityElement = YES; self.accessibilityTraits |= UIAccessibilityTraitStaticText; @@ -32,6 +34,30 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } +- (void)reactSetFrame:(CGRect)frame +{ + // Text looks super weird if its frame is animated. + // This disables the frame animation, without affecting opacity, etc. + [UIView performWithoutAnimation:^{ + [super reactSetFrame:frame]; + }]; +} + +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +{ + [_reactSubviews insertObject:subview atIndex:atIndex]; +} + +- (void)removeReactSubview:(UIView *)subview +{ + [_reactSubviews removeObject:subview]; +} + +- (NSMutableArray *)reactSubviews +{ + return _reactSubviews; +} + - (void)setTextStorage:(NSTextStorage *)textStorage { _textStorage = textStorage; diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index 3dbb2ed537af64..d9e547c773156c 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -34,6 +34,7 @@ - (RCTShadowView *)shadowView #pragma mark - View properties +RCT_IGNORE_VIEW_PROPERTY(backgroundColor); RCT_REMAP_VIEW_PROPERTY(containerBackgroundColor, backgroundColor, UIColor) #pragma mark - Shadow properties @@ -55,8 +56,6 @@ - (RCTShadowView *)shadowView - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry { - NSMutableArray *uiBlocks = [NSMutableArray new]; - for (RCTShadowView *rootView in shadowViewRegistry.allObjects) { if (![rootView isReactRootView]) { // This isn't a root view @@ -68,16 +67,13 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *) continue; } - RCTSparseArray *reactTaggedTextStorage = [[RCTSparseArray alloc] init]; NSMutableArray *queue = [NSMutableArray arrayWithObject:rootView]; for (NSInteger i = 0; i < [queue count]; i++) { RCTShadowView *shadowView = queue[i]; RCTAssert([shadowView isTextDirty], @"Don't process any nodes that don't have dirty text"); if ([shadowView isKindOfClass:[RCTShadowText class]]) { - RCTShadowText *shadowText = (RCTShadowText *)shadowView; - NSTextStorage *textStorage = [shadowText buildTextStorageForWidth:shadowView.frame.size.width]; - reactTaggedTextStorage[shadowText.reactTag] = textStorage; + [(RCTShadowText *)shadowView recomputeText]; } else if ([shadowView isKindOfClass:[RCTShadowRawText class]]) { RCTLogError(@"Raw text cannot be used outside of a tag. Not rendering string: '%@'", [(RCTShadowRawText *)shadowView text]); @@ -91,30 +87,21 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *) [shadowView setTextComputed]; } - - [uiBlocks addObject:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - [reactTaggedTextStorage enumerateObjectsUsingBlock:^(NSTextStorage *textStorage, NSNumber *reactTag, BOOL *stop) { - RCTText *text = viewRegistry[reactTag]; - text.textStorage = textStorage; - }]; - }]; } - return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - for (RCTViewManagerUIBlock shadowBlock in uiBlocks) { - shadowBlock(uiManager, viewRegistry); - } - }; + return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {}; } - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowText *)shadowView { NSNumber *reactTag = shadowView.reactTag; UIEdgeInsets padding = shadowView.paddingAsInsets; + NSTextStorage *textStorage = [shadowView buildTextStorageForWidth:shadowView.frame.size.width]; return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { RCTText *text = viewRegistry[reactTag]; text.contentInset = padding; + text.textStorage = textStorage; }; } diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js index 6e934a8e664e3c..7ff16c5d9ec270 100644 --- a/Libraries/Text/Text.js +++ b/Libraries/Text/Text.js @@ -12,7 +12,9 @@ 'use strict'; var NativeMethodsMixin = require('NativeMethodsMixin'); +var Platform = require('Platform'); var React = require('React'); +var ReactInstanceMap = require('ReactInstanceMap'); var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); var StyleSheetPropType = require('StyleSheetPropType'); var TextStylePropTypes = require('TextStylePropTypes'); @@ -177,6 +179,14 @@ var Text = React.createClass({ return PRESS_RECT_OFFSET; }, + getChildContext: function(): Object { + return {isInAParentText: true}; + }, + + childContextTypes: { + isInAParentText: React.PropTypes.bool + }, + render: function() { var props = {}; for (var key in this.props) { @@ -194,7 +204,14 @@ var Text = React.createClass({ props.onResponderMove = this.handleResponderMove; props.onResponderRelease = this.handleResponderRelease; props.onResponderTerminate = this.handleResponderTerminate; - return ; + + // TODO: Switch to use contextTypes and this.context after React upgrade + var context = ReactInstanceMap.get(this)._context; + if (context.isInAParentText) { + return ; + } else { + return ; + } }, }); @@ -208,5 +225,15 @@ type RectOffset = { var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; var RCTText = createReactNativeComponentClass(viewConfig); +var RCTVirtualText = RCTText; + +if (Platform.OS === 'android') { + RCTVirtualText = createReactNativeComponentClass({ + validAttributes: merge(ReactNativeViewAttributes.UIView, { + isHighlighted: true, + }), + uiViewClassName: 'RCTVirtualText', + }); +} module.exports = Text; diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index 0f73c28295df09..ee43c115977f54 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -109,8 +109,8 @@ typedef NSArray CGColorArray; typedef id NSPropertyList; + (NSPropertyList)NSPropertyList:(id)json; -typedef BOOL css_overflow; -+ (css_overflow)css_overflow:(id)json; +typedef BOOL css_clip_t; ++ (css_clip_t)css_clip_t:(id)json; + (css_flex_direction_t)css_flex_direction_t:(id)json; + (css_justify_t)css_justify_t:(id)json; + (css_align_t)css_align_t:(id)json; diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 0047f84616040e..3c9143c17d29ba 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -916,10 +916,10 @@ + (NSPropertyList)NSPropertyList:(id)json return RCTConvertPropertyListValue(json); } -RCT_ENUM_CONVERTER(css_overflow, (@{ - @"hidden": @NO, - @"visible": @YES -}), YES, boolValue) +RCT_ENUM_CONVERTER(css_clip_t, (@{ + @"hidden": @YES, + @"visible": @NO +}), NO, boolValue) RCT_ENUM_CONVERTER(css_flex_direction_t, (@{ @"row": @(CSS_FLEX_DIRECTION_ROW), diff --git a/React/Base/RCTRedBox.m b/React/Base/RCTRedBox.m index 9c5b6d3dd3bf0e..2d35dcd6fc2cd0 100644 --- a/React/Base/RCTRedBox.m +++ b/React/Base/RCTRedBox.m @@ -58,6 +58,7 @@ - (id)initWithFrame:(CGRect)frame [_rootView addSubview:_stackTraceTableView]; UIButton *dismissButton = [UIButton buttonWithType:UIButtonTypeCustom]; + dismissButton.accessibilityIdentifier = @"redbox-dismiss"; dismissButton.titleLabel.font = [UIFont systemFontOfSize:14]; [dismissButton setTitle:@"Dismiss (ESC)" forState:UIControlStateNormal]; [dismissButton setTitleColor:[[UIColor whiteColor] colorWithAlphaComponent:0.5] forState:UIControlStateNormal]; @@ -65,6 +66,7 @@ - (id)initWithFrame:(CGRect)frame [dismissButton addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchUpInside]; UIButton *reloadButton = [UIButton buttonWithType:UIButtonTypeCustom]; + reloadButton.accessibilityIdentifier = @"redbox-reload"; reloadButton.titleLabel.font = [UIFont systemFontOfSize:14]; [reloadButton setTitle:@"Reload JS (\u2318R)" forState:UIControlStateNormal]; [reloadButton setTitleColor:[[UIColor whiteColor] colorWithAlphaComponent:0.5] forState:UIControlStateNormal]; @@ -172,6 +174,7 @@ - (UITableViewCell *)reuseCell:(UITableViewCell *)cell forErrorMessage:(NSString { if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"msg-cell"]; + cell.textLabel.accessibilityIdentifier = @"redbox-error"; cell.textLabel.textColor = [UIColor whiteColor]; cell.textLabel.font = [UIFont boldSystemFontOfSize:16]; cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping; diff --git a/React/Base/RCTRootView.h b/React/Base/RCTRootView.h index d55094c3727328..dec81d2563bc1c 100644 --- a/React/Base/RCTRootView.h +++ b/React/Base/RCTRootView.h @@ -11,6 +11,18 @@ #import "RCTBridge.h" +/** + * This notification is sent when the first subviews are added to the root view + * after the application has loaded. This is used to hide the `loadingView`, and + * is a good indicator that the application is ready to use. + */ +extern NSString *const RCTContentDidAppearNotification; + +/** + * Native view used to host React-managed views within the app. Can be used just + * like any ordinary UIView. You can have multiple RCTRootViews on screen at + * once, all controlled by the same JavaScript application. + */ @interface RCTRootView : UIView /** @@ -67,4 +79,18 @@ */ @property (nonatomic, strong, readonly) UIView *contentView; +/** + * A view to display while the JavaScript is loading, so users aren't presented + * with a blank screen. By default this is nil, but you can override it with + * (for example) a UIActivityIndicatorView or a placeholder image. + */ +@property (nonatomic, strong) UIView *loadingView; + +/** + * Timings for hiding the loading view after the content has loaded. Both of + * these values default to 0.25 seconds. + */ +@property (nonatomic, assign) NSTimeInterval loadingViewFadeDelay; +@property (nonatomic, assign) NSTimeInterval loadingViewFadeDuration; + @end diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index e9a8170eb01ab1..b12cda4f212f82 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -25,6 +25,8 @@ #import "RCTWebViewExecutor.h" #import "UIView+React.h" +NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotification"; + @interface RCTBridge (RCTRootView) @property (nonatomic, weak, readonly) RCTBridge *batchedBridge; @@ -39,6 +41,8 @@ - (NSNumber *)allocateRootTag; @interface RCTRootContentView : RCTView +@property (nonatomic, readonly) BOOL contentHasAppeared; + - (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge; @end @@ -64,14 +68,23 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _bridge = bridge; _moduleName = moduleName; + _loadingViewFadeDelay = 0.25; + _loadingViewFadeDuration = 0.25; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(javaScriptDidLoad:) name:RCTJavaScriptDidLoadNotification object:_bridge]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(hideLoadingView) + name:RCTContentDidAppearNotification + object:self]; if (!_bridge.batchedBridge.isLoading) { [self bundleFinishedLoading:_bridge.batchedBridge]; } + + [self showLoadingView]; } return self; } @@ -106,6 +119,41 @@ - (BOOL)canBecomeFirstResponder RCT_IMPORT_METHOD(AppRegistry, runApplication) RCT_IMPORT_METHOD(ReactNative, unmountComponentAtNodeAndRemoveContainer) +- (void)setLoadingView:(UIView *)loadingView +{ + _loadingView = loadingView; + if (!_contentView.contentHasAppeared) { + [self showLoadingView]; + } +} + +- (void)showLoadingView +{ + if (_loadingView && !_contentView.contentHasAppeared) { + _loadingView.hidden = NO; + [self addSubview:_loadingView]; + } +} + +- (void)hideLoadingView +{ + if (_loadingView.superview == self && _contentView.contentHasAppeared) { + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_loadingViewFadeDelay * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + + [UIView transitionWithView:self + duration:_loadingViewFadeDuration + options:UIViewAnimationOptionTransitionCrossDissolve + animations:^{ + _loadingView.hidden = YES; + } completion:^(BOOL finished) { + [_loadingView removeFromSuperview]; + }]; + }); + } +} + - (void)javaScriptDidLoad:(NSNotification *)notification { RCTBridge *bridge = notification.userInfo[@"bridge"]; @@ -119,35 +167,31 @@ - (void)bundleFinishedLoading:(RCTBridge *)bridge return; } - /** - * Every root view that is created must have a unique React tag. - * Numbering of these tags goes from 1, 11, 21, 31, etc - * - * NOTE: Since the bridge persists, the RootViews might be reused, so now - * the React tag is assigned every time we load new content. - */ [_contentView removeFromSuperview]; _contentView = [[RCTRootContentView alloc] initWithFrame:self.bounds bridge:bridge]; _contentView.backgroundColor = self.backgroundColor; - [self addSubview:_contentView]; + [self insertSubview:_contentView atIndex:0]; NSString *moduleName = _moduleName ?: @""; NSDictionary *appParameters = @{ @"rootTag": _contentView.reactTag, @"initialProps": _initialProperties ?: @{}, }; + [bridge enqueueJSCall:@"AppRegistry.runApplication" - args:@[moduleName, appParameters]]; + args:@[moduleName, appParameters]]; }); } - (void)layoutSubviews { [super layoutSubviews]; - if (_contentView) { - _contentView.frame = self.bounds; - } + _contentView.frame = self.bounds; + _loadingView.center = (CGPoint){ + CGRectGetMidX(self.bounds), + CGRectGetMidY(self.bounds) + }; } - (NSNumber *)reactTag @@ -155,6 +199,13 @@ - (NSNumber *)reactTag return _contentView.reactTag; } +- (void)contentViewInvalidated +{ + [_contentView removeFromSuperview]; + _contentView = nil; + [self showLoadingView]; +} + - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; @@ -193,6 +244,18 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } +- (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex +{ + [super insertReactSubview:subview atIndex:atIndex]; + dispatch_async(dispatch_get_main_queue(), ^{ + if (!_contentHasAppeared) { + _contentHasAppeared = YES; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTContentDidAppearNotification + object:self.superview]; + } + }); +} + - (void)setFrame:(CGRect)frame { super.frame = frame; @@ -237,7 +300,7 @@ - (void)invalidate { if (self.isValid) { self.userInteractionEnabled = NO; - [self removeFromSuperview]; + [(RCTRootView *)self.superview contentViewInvalidated]; [_bridge enqueueJSCall:@"ReactNative.unmountComponentAtNodeAndRemoveContainer" args:@[self.reactTag]]; } diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 09cfd758f62a9e..f585edc10f283e 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -420,12 +420,12 @@ - (void)executeApplicationScript:(NSString *)script - (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block { - if ([NSThread currentThread] != _javaScriptThread) { - [self performSelector:@selector(executeBlockOnJavaScriptQueue:) - onThread:_javaScriptThread withObject:block waitUntilDone:NO]; - } else { - block(); - } + if ([NSThread currentThread] != _javaScriptThread) { + [self performSelector:@selector(executeBlockOnJavaScriptQueue:) + onThread:_javaScriptThread withObject:block waitUntilDone:NO]; + } else { + block(); + } } - (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 0edca85d9ed0e7..636df0406d7e65 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -913,6 +913,13 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView - (void)batchDidComplete { + // Gather blocks to be executed now that all view hierarchy manipulations have + // been completed (note that these may still take place before layout has finished) + for (RCTViewManager *manager in _viewManagers.allValues) { + RCTViewManagerUIBlock uiBlock = [manager uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry]; + [self addUIBlock:uiBlock]; + } + // Set up next layout animation if (_nextLayoutAnimation) { RCTLayoutAnimation *layoutAnimation = _nextLayoutAnimation; @@ -936,12 +943,6 @@ - (void)batchDidComplete _nextLayoutAnimation = nil; } - // Gather blocks to be executed now that layout is completed - for (RCTViewManager *manager in _viewManagers.allValues) { - RCTViewManagerUIBlock uiBlock = [manager uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry]; - [self addUIBlock:uiBlock]; - } - [self flushUIBlocks]; } diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index cf05f4e39c9c21..c7309989b4ca88 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */; }; 13C156051AB1A2840079392D /* RCTWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156021AB1A2840079392D /* RCTWebView.m */; }; 13C156061AB1A2840079392D /* RCTWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156041AB1A2840079392D /* RCTWebViewManager.m */; }; + 13CC8A821B17642100940AE7 /* RCTBorderDrawing.m in Sources */ = {isa = PBXBuildFile; fileRef = 13CC8A811B17642100940AE7 /* RCTBorderDrawing.m */; }; 13E0674A1A70F434002CDEE1 /* RCTUIManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067491A70F434002CDEE1 /* RCTUIManager.m */; }; 13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674C1A70F44B002CDEE1 /* RCTShadowView.m */; }; 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */; }; @@ -155,6 +156,8 @@ 13C325261AA63B6A0048765F /* RCTAutoInsetsProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAutoInsetsProtocol.h; sourceTree = ""; }; 13C325271AA63B6A0048765F /* RCTScrollableProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTScrollableProtocol.h; sourceTree = ""; }; 13C325281AA63B6A0048765F /* RCTViewNodeProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTViewNodeProtocol.h; sourceTree = ""; }; + 13CC8A801B17642100940AE7 /* RCTBorderDrawing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBorderDrawing.h; sourceTree = ""; }; + 13CC8A811B17642100940AE7 /* RCTBorderDrawing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBorderDrawing.m; 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 = ""; }; @@ -279,6 +282,8 @@ children = ( 13442BF21AA90E0B0037E5B0 /* RCTAnimationType.h */, 13C325261AA63B6A0048765F /* RCTAutoInsetsProtocol.h */, + 13CC8A801B17642100940AE7 /* RCTBorderDrawing.h */, + 13CC8A811B17642100940AE7 /* RCTBorderDrawing.m */, 58C571C01AA56C1900CDF9C8 /* RCTDatePickerManager.h */, 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */, 13456E911ADAD2DE009F94A7 /* RCTConvert+CoreLocation.h */, @@ -513,6 +518,7 @@ 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */, 13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */, 83CBBA5A1A601E9000E9B192 /* RCTRedBox.m in Sources */, + 13CC8A821B17642100940AE7 /* RCTBorderDrawing.m in Sources */, 83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */, 13AF20451AE707F9005F5298 /* RCTSlider.m in Sources */, 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */, diff --git a/React/Views/RCTBorderDrawing.h b/React/Views/RCTBorderDrawing.h new file mode 100644 index 00000000000000..b67074337e7a54 --- /dev/null +++ b/React/Views/RCTBorderDrawing.h @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +typedef struct { + CGFloat topLeft; + CGFloat topRight; + CGFloat bottomLeft; + CGFloat bottomRight; +} RCTCornerRadii; + +typedef struct { + CGSize topLeft; + CGSize topRight; + CGSize bottomLeft; + CGSize bottomRight; +} RCTCornerInsets; + +typedef struct { + CGColorRef top; + CGColorRef left; + CGColorRef bottom; + CGColorRef right; +} RCTBorderColors; + +/** + * Determine if the border widths, colors and radii are all equal. + */ +BOOL RCTBorderInsetsAreEqual(UIEdgeInsets borderInsets); +BOOL RCTCornerRadiiAreEqual(RCTCornerRadii cornerRadii); +BOOL RCTBorderColorsAreEqual(RCTBorderColors borderColors); + +/** + * Convert RCTCornerRadii to RCTCornerInsets by applying border insets. + */ +RCTCornerInsets RCTGetCornerInsets(RCTCornerRadii cornerRadii, + UIEdgeInsets borderInsets); + +/** + * Create a CGPath representing a rounded rectangle with the specified bounds + * and corner insets. Note that the CGPathRef must be released by the caller. + */ +CGPathRef RCTPathCreateWithRoundedRect(CGRect bounds, + RCTCornerInsets cornerInsets, + const CGAffineTransform *transform); + +/** + * Draw a CSS-compliant border as a scalable image. + */ +UIImage *RCTGetBorderImage(RCTCornerRadii cornerRadii, + UIEdgeInsets borderInsets, + RCTBorderColors borderColors, + CGColorRef backgroundColor, + BOOL drawToEdge); diff --git a/React/Views/RCTBorderDrawing.m b/React/Views/RCTBorderDrawing.m new file mode 100644 index 00000000000000..8d9e7c0d4e23d7 --- /dev/null +++ b/React/Views/RCTBorderDrawing.m @@ -0,0 +1,331 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTBorderDrawing.h" + +static const CGFloat RCTViewBorderThreshold = 0.001; + +BOOL RCTBorderInsetsAreEqual(UIEdgeInsets borderInsets) +{ + return + ABS(borderInsets.left - borderInsets.right) < RCTViewBorderThreshold && + ABS(borderInsets.left - borderInsets.bottom) < RCTViewBorderThreshold && + ABS(borderInsets.left - borderInsets.top) < RCTViewBorderThreshold; +} + +BOOL RCTCornerRadiiAreEqual(RCTCornerRadii cornerRadii) +{ + return + ABS(cornerRadii.topLeft - cornerRadii.topRight) < RCTViewBorderThreshold && + ABS(cornerRadii.topLeft - cornerRadii.bottomLeft) < RCTViewBorderThreshold && + ABS(cornerRadii.topLeft - cornerRadii.bottomRight) < RCTViewBorderThreshold; +} + +BOOL RCTBorderColorsAreEqual(RCTBorderColors borderColors) +{ + return + CGColorEqualToColor(borderColors.left, borderColors.right) && + CGColorEqualToColor(borderColors.left, borderColors.top) && + CGColorEqualToColor(borderColors.left, borderColors.bottom); +} + +RCTCornerInsets RCTGetCornerInsets(RCTCornerRadii cornerRadii, + UIEdgeInsets edgeInsets) +{ + return (RCTCornerInsets) { + { + MAX(0, cornerRadii.topLeft - edgeInsets.left), + MAX(0, cornerRadii.topLeft - edgeInsets.top), + }, + { + MAX(0, cornerRadii.topRight - edgeInsets.right), + MAX(0, cornerRadii.topRight - edgeInsets.top), + }, + { + MAX(0, cornerRadii.bottomLeft - edgeInsets.left), + MAX(0, cornerRadii.bottomLeft - edgeInsets.bottom), + }, + { + MAX(0, cornerRadii.bottomRight - edgeInsets.right), + MAX(0, cornerRadii.bottomRight - edgeInsets.bottom), + } + }; +} + +static void RCTPathAddEllipticArc(CGMutablePathRef path, + const CGAffineTransform *m, + CGPoint origin, + CGSize size, + CGFloat startAngle, + CGFloat endAngle, + BOOL clockwise) +{ + CGFloat xScale = 1, yScale = 1, radius = 0; + if (size.width != 0) { + xScale = 1; + yScale = size.height / size.width; + radius = size.width; + } else if (size.height != 0) { + xScale = size.width / size.height; + yScale = 1; + radius = size.height; + } + + CGAffineTransform t = CGAffineTransformMakeTranslation(origin.x, origin.y); + t = CGAffineTransformScale(t, xScale, yScale); + if (m != NULL) { + t = CGAffineTransformConcat(t, *m); + } + + CGPathAddArc(path, &t, 0, 0, radius, startAngle, endAngle, clockwise); +} + +CGPathRef RCTPathCreateWithRoundedRect(CGRect bounds, + RCTCornerInsets cornerInsets, + const CGAffineTransform *transform) +{ + const CGFloat minX = CGRectGetMinX(bounds); + const CGFloat minY = CGRectGetMinY(bounds); + const CGFloat maxX = CGRectGetMaxX(bounds); + const CGFloat maxY = CGRectGetMaxY(bounds); + + const CGSize topLeft = cornerInsets.topLeft; + const CGSize topRight = cornerInsets.topRight; + const CGSize bottomLeft = cornerInsets.bottomLeft; + const CGSize bottomRight = cornerInsets.bottomRight; + + CGMutablePathRef path = CGPathCreateMutable(); + RCTPathAddEllipticArc(path, transform, (CGPoint){ + minX + topLeft.width, minY + topLeft.height + }, topLeft, M_PI, 3 * M_PI_2, NO); + RCTPathAddEllipticArc(path, transform, (CGPoint){ + maxX - topRight.width, minY + topRight.height + }, topRight, 3 * M_PI_2, 0, NO); + RCTPathAddEllipticArc(path, transform, (CGPoint){ + maxX - bottomRight.width, maxY - bottomRight.height + }, bottomRight, 0, M_PI_2, NO); + RCTPathAddEllipticArc(path, transform, (CGPoint){ + minX + bottomLeft.width, maxY - bottomLeft.height + }, bottomLeft, M_PI_2, M_PI, NO); + CGPathCloseSubpath(path); + return path; +} + +static void RCTEllipseGetIntersectionsWithLine(CGRect ellipseBounds, + CGPoint lineStart, + CGPoint lineEnd, + CGPoint intersections[2]) +{ + const CGPoint ellipseCenter = { + CGRectGetMidX(ellipseBounds), + CGRectGetMidY(ellipseBounds) + }; + + lineStart.x -= ellipseCenter.x; + lineStart.y -= ellipseCenter.y; + lineEnd.x -= ellipseCenter.x; + lineEnd.y -= ellipseCenter.y; + + const CGFloat m = (lineEnd.y - lineStart.y) / (lineEnd.x - lineStart.x); + const CGFloat a = ellipseBounds.size.width / 2; + const CGFloat b = ellipseBounds.size.height / 2; + const CGFloat c = lineStart.y - m * lineStart.x; + const CGFloat A = (b * b + a * a * m * m); + const CGFloat B = 2 * a * a * c * m; + const CGFloat D = sqrt((a * a * (b * b - c * c)) / A + pow(B / (2 * A), 2)); + + const CGFloat x_ = -B / (2 * A); + const CGFloat x1 = x_ + D; + const CGFloat x2 = x_ - D; + const CGFloat y1 = m * x1 + c; + const CGFloat y2 = m * x2 + c; + + intersections[0] = (CGPoint){x1 + ellipseCenter.x, y1 + ellipseCenter.y}; + intersections[1] = (CGPoint){x2 + ellipseCenter.x, y2 + ellipseCenter.y}; +} + +UIImage *RCTGetBorderImage(RCTCornerRadii cornerRadii, + UIEdgeInsets borderInsets, + RCTBorderColors borderColors, + CGColorRef backgroundColor, + BOOL drawToEdge) +{ + const BOOL hasCornerRadii = + cornerRadii.topLeft > RCTViewBorderThreshold || + cornerRadii.topRight > RCTViewBorderThreshold || + cornerRadii.bottomLeft > RCTViewBorderThreshold || + cornerRadii.bottomRight > RCTViewBorderThreshold; + + const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, borderInsets); + + const UIEdgeInsets edgeInsets = (UIEdgeInsets){ + borderInsets.top + MAX(cornerInsets.topLeft.height, cornerInsets.topRight.height), + borderInsets.left + MAX(cornerInsets.topLeft.width, cornerInsets.bottomLeft.width), + borderInsets.bottom + MAX(cornerInsets.bottomLeft.height, cornerInsets.bottomRight.height), + borderInsets.right + MAX(cornerInsets.bottomRight.width, cornerInsets.topRight.width) + }; + + const CGSize size = (CGSize){ + edgeInsets.left + 1 + edgeInsets.right, + edgeInsets.top + 1 + edgeInsets.bottom + }; + + const CGFloat alpha = CGColorGetAlpha(backgroundColor); + const BOOL opaque = (drawToEdge || !hasCornerRadii) && alpha == 1.0; + UIGraphicsBeginImageContextWithOptions(size, opaque, 0.0); + + CGContextRef ctx = UIGraphicsGetCurrentContext(); + const CGRect rect = {.size = size}; + + CGPathRef path; + if (drawToEdge) { + path = CGPathCreateWithRect(rect, NULL); + } else { + path = RCTPathCreateWithRoundedRect(rect, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL); + } + + if (backgroundColor) { + CGContextSetFillColorWithColor(ctx, backgroundColor); + CGContextAddPath(ctx, path); + CGContextFillPath(ctx); + } + + CGContextAddPath(ctx, path); + CGPathRelease(path); + + CGPathRef insetPath = RCTPathCreateWithRoundedRect(UIEdgeInsetsInsetRect(rect, borderInsets), cornerInsets, NULL); + + CGContextAddPath(ctx, insetPath); + CGContextEOClip(ctx); + + BOOL hasEqualColors = RCTBorderColorsAreEqual(borderColors); + if ((drawToEdge || !hasCornerRadii) && hasEqualColors) { + + CGContextSetFillColorWithColor(ctx, borderColors.left); + CGContextAddRect(ctx, rect); + CGContextAddPath(ctx, insetPath); + CGContextEOFillPath(ctx); + + } else { + + CGPoint topLeft = (CGPoint){borderInsets.left, borderInsets.top}; + if (cornerInsets.topLeft.width > 0 && cornerInsets.topLeft.height > 0) { + CGPoint points[2]; + RCTEllipseGetIntersectionsWithLine((CGRect){ + topLeft, {2 * cornerInsets.topLeft.width, 2 * cornerInsets.topLeft.height} + }, CGPointZero, topLeft, points); + if (!isnan(points[1].x) && !isnan(points[1].y)) { + topLeft = points[1]; + } + } + + CGPoint bottomLeft = (CGPoint){borderInsets.left, size.height - borderInsets.bottom}; + if (cornerInsets.bottomLeft.width > 0 && cornerInsets.bottomLeft.height > 0) { + CGPoint points[2]; + RCTEllipseGetIntersectionsWithLine((CGRect){ + {bottomLeft.x, bottomLeft.y - 2 * cornerInsets.bottomLeft.height}, + {2 * cornerInsets.bottomLeft.width, 2 * cornerInsets.bottomLeft.height} + }, (CGPoint){0, size.height}, bottomLeft, points); + if (!isnan(points[1].x) && !isnan(points[1].y)) { + bottomLeft = points[1]; + } + } + + CGPoint topRight = (CGPoint){size.width - borderInsets.right, borderInsets.top}; + if (cornerInsets.topRight.width > 0 && cornerInsets.topRight.height > 0) { + CGPoint points[2]; + RCTEllipseGetIntersectionsWithLine((CGRect){ + {topRight.x - 2 * cornerInsets.topRight.width, topRight.y}, + {2 * cornerInsets.topRight.width, 2 * cornerInsets.topRight.height} + }, (CGPoint){size.width, 0}, topRight, points); + if (!isnan(points[0].x) && !isnan(points[0].y)) { + topRight = points[0]; + } + } + + CGPoint bottomRight = (CGPoint){size.width - borderInsets.right, size.height - borderInsets.bottom}; + if (cornerInsets.bottomRight.width > 0 && cornerInsets.bottomRight.height > 0) { + CGPoint points[2]; + RCTEllipseGetIntersectionsWithLine((CGRect){ + {bottomRight.x - 2 * cornerInsets.bottomRight.width, bottomRight.y - 2 * cornerInsets.bottomRight.height}, + {2 * cornerInsets.bottomRight.width, 2 * cornerInsets.bottomRight.height} + }, (CGPoint){size.width, size.height}, bottomRight, points); + if (!isnan(points[0].x) && !isnan(points[0].y)) { + bottomRight = points[0]; + } + } + + // RIGHT + if (borderInsets.right > 0) { + + const CGPoint points[] = { + (CGPoint){size.width, 0}, + topRight, + bottomRight, + (CGPoint){size.width, size.height}, + }; + + CGContextSetFillColorWithColor(ctx, borderColors.right); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + } + + // BOTTOM + if (borderInsets.bottom > 0) { + + const CGPoint points[] = { + (CGPoint){0, size.height}, + bottomLeft, + bottomRight, + (CGPoint){size.width, size.height}, + }; + + CGContextSetFillColorWithColor(ctx, borderColors.bottom); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + } + + // LEFT + if (borderInsets.left > 0) { + + const CGPoint points[] = { + CGPointZero, + topLeft, + bottomLeft, + (CGPoint){0, size.height}, + }; + + CGContextSetFillColorWithColor(ctx, borderColors.left); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + } + + // TOP + if (borderInsets.top > 0) { + + const CGPoint points[] = { + CGPointZero, + topLeft, + topRight, + (CGPoint){size.width, 0}, + }; + + CGContextSetFillColorWithColor(ctx, borderColors.top); + CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); + CGContextFillPath(ctx); + } + } + + CGPathRelease(insetPath); + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return [image resizableImageWithCapInsets:edgeInsets]; +} diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index 6fc7f58b129a09..b26193f24ca946 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -13,6 +13,7 @@ #import "RCTLog.h" #import "RCTSparseArray.h" #import "RCTUtils.h" +#import "UIView+React.h" typedef void (^RCTActionBlock)(RCTShadowView *shadowViewSelf, id value); typedef void (^RCTResetActionBlock)(RCTShadowView *shadowViewSelf); @@ -38,7 +39,6 @@ @implementation RCTShadowView NSMutableArray *_reactSubviews; BOOL _recomputePadding; BOOL _recomputeMargin; - BOOL _isBGColorExplicitlySet; float _paddingMetaProps[META_PROP_COUNT]; float _marginMetaProps[META_PROP_COUNT]; } @@ -167,22 +167,20 @@ - (void)applyLayoutNode:(css_node_t *)node viewsWithNewFrame:(NSMutableSet *)vie - (NSDictionary *)processBackgroundColor:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties { - if (!_isBGColorExplicitlySet) { + if (!_backgroundColor) { UIColor *parentBackgroundColor = parentProperties[RCTBackgroundColorProp]; - if (parentBackgroundColor && ![_backgroundColor isEqual:parentBackgroundColor]) { - _backgroundColor = parentBackgroundColor; + if (parentBackgroundColor) { [applierBlocks addObject:^(RCTSparseArray *viewRegistry) { UIView *view = viewRegistry[_reactTag]; - view.backgroundColor = parentBackgroundColor; + [view reactSetInheritedBackgroundColor:parentBackgroundColor]; }]; } - } - if (_isBGColorExplicitlySet) { + } else { // Update parent properties for children NSMutableDictionary *properties = [NSMutableDictionary dictionaryWithDictionary:parentProperties]; CGFloat alpha = CGColorGetAlpha(_backgroundColor.CGColor); if (alpha < 1.0) { - // If we see partial transparency, start propagating full transparency + // If bg is non-opaque, don't propagate further properties[RCTBackgroundColorProp] = [UIColor clearColor]; } else { properties[RCTBackgroundColorProp] = _backgroundColor; @@ -516,7 +514,6 @@ - (type)getProp \ - (void)setBackgroundColor:(UIColor *)color { _backgroundColor = color; - _isBGColorExplicitlySet = YES; [self dirtyPropagation]; } diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index d97eb5567cdc91..2cc03d874e5bd5 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -10,13 +10,12 @@ #import "RCTView.h" #import "RCTAutoInsetsProtocol.h" +#import "RCTBorderDrawing.h" #import "RCTConvert.h" #import "RCTLog.h" #import "RCTUtils.h" #import "UIView+React.h" -static const CGFloat RCTViewBorderThreshold = 0.001; - static UIView *RCTViewHitTest(UIView *view, CGPoint point, UIEvent *event) { for (UIView *subview in [view.subviews reverseObjectEnumerator]) { @@ -31,10 +30,6 @@ return nil; } -static BOOL RCTEllipseGetIntersectionsWithLine(CGRect ellipseBoundingRect, CGPoint p1, CGPoint p2, CGPoint intersections[2]); -static CGPathRef RCTPathCreateWithRoundedRect(CGRect rect, CGFloat topLeftRadiusX, CGFloat topLeftRadiusY, CGFloat topRightRadiusX, CGFloat topRightRadiusY, CGFloat bottomLeftRadiusX, CGFloat bottomLeftRadiusY, CGFloat bottomRightRadiusX, CGFloat bottomRightRadiusY, const CGAffineTransform *transform); -static void RCTPathAddEllipticArc(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y, CGFloat xRadius, CGFloat yRadius, CGFloat startAngle, CGFloat endAngle, bool clockwise); - @implementation UIView (RCTViewUnmounting) - (void)react_remountAllSubviews @@ -443,246 +438,88 @@ - (void)setBackgroundColor:(UIColor *)backgroundColor [self.layer setNeedsDisplay]; } -- (UIImage *)borderImage:(out CGRect *)contentsCenter +- (UIEdgeInsets)bordersAsInsets { - const CGFloat maxRadius = ({ - const CGRect bounds = self.bounds; - MIN(bounds.size.height, bounds.size.width); - }); - - const CGFloat radius = MAX(0, _borderRadius); - const CGFloat topLeftRadius = MIN(_borderTopLeftRadius >= 0 ? _borderTopLeftRadius : radius, maxRadius); - const CGFloat topRightRadius = MIN(_borderTopRightRadius >= 0 ? _borderTopRightRadius : radius, maxRadius); - const CGFloat bottomLeftRadius = MIN(_borderBottomLeftRadius >= 0 ? _borderBottomLeftRadius : radius, maxRadius); - const CGFloat bottomRightRadius = MIN(_borderBottomRightRadius >= 0 ? _borderBottomRightRadius : radius, maxRadius); - const CGFloat borderWidth = MAX(0, _borderWidth); - const CGFloat topWidth = _borderTopWidth >= 0 ? _borderTopWidth : borderWidth; - const CGFloat rightWidth = _borderRightWidth >= 0 ? _borderRightWidth : borderWidth; - const CGFloat bottomWidth = _borderBottomWidth >= 0 ? _borderBottomWidth : borderWidth; - const CGFloat leftWidth = _borderLeftWidth >= 0 ? _borderLeftWidth : borderWidth; - - const BOOL hasCornerRadii = - topLeftRadius > RCTViewBorderThreshold || - topRightRadius > RCTViewBorderThreshold || - bottomLeftRadius > RCTViewBorderThreshold || - bottomRightRadius > RCTViewBorderThreshold; - - const BOOL hasBorders = - topWidth > RCTViewBorderThreshold || - rightWidth > RCTViewBorderThreshold || - bottomWidth > RCTViewBorderThreshold || - leftWidth > RCTViewBorderThreshold; - - if (!hasCornerRadii && !hasBorders) { - return nil; - } - - const CGFloat innerTopLeftRadiusX = MAX(0, topLeftRadius - leftWidth); - const CGFloat innerTopLeftRadiusY = MAX(0, topLeftRadius - topWidth); - - const CGFloat innerTopRightRadiusX = MAX(0, topRightRadius - rightWidth); - const CGFloat innerTopRightRadiusY = MAX(0, topRightRadius - topWidth); - - const CGFloat innerBottomLeftRadiusX = MAX(0, bottomLeftRadius - leftWidth); - const CGFloat innerBottomLeftRadiusY = MAX(0, bottomLeftRadius - bottomWidth); - - const CGFloat innerBottomRightRadiusX = MAX(0, bottomRightRadius - rightWidth); - const CGFloat innerBottomRightRadiusY = MAX(0, bottomRightRadius - bottomWidth); - - const UIEdgeInsets edgeInsets = UIEdgeInsetsMake(topWidth + MAX(innerTopLeftRadiusY, innerTopRightRadiusY), leftWidth + MAX(innerTopLeftRadiusX, innerBottomLeftRadiusX), bottomWidth + MAX(innerBottomLeftRadiusY, innerBottomRightRadiusY), rightWidth + + MAX(innerBottomRightRadiusX, innerTopRightRadiusX)); - const CGSize size = CGSizeMake(edgeInsets.left + 1 + edgeInsets.right, edgeInsets.top + 1 + edgeInsets.bottom); - - const CGFloat alpha = CGColorGetAlpha(_backgroundColor.CGColor); - const BOOL opaque = (self.clipsToBounds || !hasCornerRadii) && alpha == 1.0; - UIGraphicsBeginImageContextWithOptions(size, opaque, 0.0); - - CGContextRef ctx = UIGraphicsGetCurrentContext(); - const CGRect rect = {.size = size}; - - CGPathRef path; - const BOOL hasClipping = self.clipsToBounds; - if (hasClipping) { - path = CGPathCreateWithRect(rect, NULL); - } else { - path = RCTPathCreateWithRoundedRect(rect, topLeftRadius, topLeftRadius, topRightRadius, topRightRadius, bottomLeftRadius, bottomLeftRadius, bottomRightRadius, bottomRightRadius, NULL); - } - - if (_backgroundColor) { - CGContextSaveGState(ctx); - - CGContextSetFillColorWithColor(ctx, _backgroundColor.CGColor); - CGContextAddPath(ctx, path); - CGContextFillPath(ctx); - - CGContextRestoreGState(ctx); - } - - CGContextAddPath(ctx, path); - CGPathRelease(path); - - const BOOL hasRadius = topLeftRadius > 0 || topRightRadius > 0 || bottomLeftRadius > 0 || bottomRightRadius > 0; - const UIEdgeInsets insetEdgeInsets = UIEdgeInsetsMake(topWidth, leftWidth, bottomWidth, rightWidth); - CGPathRef insetPath = RCTPathCreateWithRoundedRect(UIEdgeInsetsInsetRect(rect, insetEdgeInsets), innerTopLeftRadiusX, innerTopLeftRadiusY, innerTopRightRadiusX, innerTopRightRadiusY, innerBottomLeftRadiusX, innerBottomLeftRadiusY, innerBottomRightRadiusX, innerBottomRightRadiusY, NULL); - - CGContextAddPath(ctx, insetPath); - CGContextEOClip(ctx); - - BOOL hasEqualColor = !_borderTopColor && !_borderRightColor && !_borderBottomColor && !_borderLeftColor; - if ((hasClipping || !hasRadius) && hasEqualColor) { - CGContextSetFillColorWithColor(ctx, _borderColor); - CGContextAddRect(ctx, rect); - CGContextAddPath(ctx, insetPath); - CGContextEOFillPath(ctx); - } else { - BOOL didSet = NO; - CGPoint topLeft; - if (innerTopLeftRadiusX > 0 && innerTopLeftRadiusY > 0) { - CGPoint points[2]; - RCTEllipseGetIntersectionsWithLine(CGRectMake(leftWidth, topWidth, 2 * innerTopLeftRadiusX, 2 * innerTopLeftRadiusY), CGPointMake(0, 0), CGPointMake(leftWidth, topWidth), points); - if (!isnan(points[1].x) && !isnan(points[1].y)) { - topLeft = points[1]; - didSet = YES; - } - } - - if (!didSet) { - topLeft = CGPointMake(leftWidth, topWidth); - } - - didSet = NO; - CGPoint bottomLeft; - if (innerBottomLeftRadiusX > 0 && innerBottomLeftRadiusY > 0) { - CGPoint points[2]; - RCTEllipseGetIntersectionsWithLine(CGRectMake(leftWidth, (size.height - bottomWidth) - 2 * innerBottomLeftRadiusY, 2 * innerBottomLeftRadiusX, 2 * innerBottomLeftRadiusY), CGPointMake(0, size.height), CGPointMake(leftWidth, size.height - bottomWidth), points); - if (!isnan(points[1].x) && !isnan(points[1].y)) { - bottomLeft = points[1]; - didSet = YES; - } - } - - if (!didSet) { - bottomLeft = CGPointMake(leftWidth, size.height - bottomWidth); - } - - didSet = NO; - CGPoint topRight; - if (innerTopRightRadiusX > 0 && innerTopRightRadiusY > 0) { - CGPoint points[2]; - RCTEllipseGetIntersectionsWithLine(CGRectMake((size.width - rightWidth) - 2 * innerTopRightRadiusX, topWidth, 2 * innerTopRightRadiusX, 2 * innerTopRightRadiusY), CGPointMake(size.width, 0), CGPointMake(size.width - rightWidth, topWidth), points); - if (!isnan(points[0].x) && !isnan(points[0].y)) { - topRight = points[0]; - didSet = YES; - } - } - - if (!didSet) { - topRight = CGPointMake(size.width - rightWidth, topWidth); - } - - didSet = NO; - CGPoint bottomRight; - if (innerBottomRightRadiusX > 0 && innerBottomRightRadiusY > 0) { - CGPoint points[2]; - RCTEllipseGetIntersectionsWithLine(CGRectMake((size.width - rightWidth) - 2 * innerBottomRightRadiusX, (size.height - bottomWidth) - 2 * innerBottomRightRadiusY, 2 * innerBottomRightRadiusX, 2 * innerBottomRightRadiusY), CGPointMake(size.width, size.height), CGPointMake(size.width - rightWidth, size.height - bottomWidth), points); - if (!isnan(points[0].x) && !isnan(points[0].y)) { - bottomRight = points[0]; - didSet = YES; - } - } - - if (!didSet) { - bottomRight = CGPointMake(size.width - rightWidth, size.height - bottomWidth); - } - - // RIGHT - if (rightWidth > 0) { - CGContextSaveGState(ctx); - - const CGPoint points[] = { - CGPointMake(size.width, 0), - topRight, - bottomRight, - CGPointMake(size.width, size.height), - }; - - CGContextSetFillColorWithColor(ctx, _borderRightColor ?: _borderColor); - CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); - CGContextFillPath(ctx); - - CGContextRestoreGState(ctx); - } - // BOTTOM - if (bottomWidth > 0) { - CGContextSaveGState(ctx); - - const CGPoint points[] = { - CGPointMake(0, size.height), - bottomLeft, - bottomRight, - CGPointMake(size.width, size.height), - }; - - CGContextSetFillColorWithColor(ctx, _borderBottomColor ?: _borderColor); - CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); - CGContextFillPath(ctx); - - CGContextRestoreGState(ctx); - } - - // LEFT - if (leftWidth > 0) { - CGContextSaveGState(ctx); - - const CGPoint points[] = { - CGPointMake(0, 0), - topLeft, - bottomLeft, - CGPointMake(0, size.height), - }; - - CGContextSetFillColorWithColor(ctx, _borderLeftColor ?: _borderColor); - CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); - CGContextFillPath(ctx); - - CGContextRestoreGState(ctx); - } - - // TOP - if (topWidth > 0) { - CGContextSaveGState(ctx); - - const CGPoint points[] = { - CGPointMake(0, 0), - topLeft, - topRight, - CGPointMake(size.width, 0), - }; - - CGContextSetFillColorWithColor(ctx, _borderTopColor ?: _borderColor); - CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points)); - CGContextFillPath(ctx); - - CGContextRestoreGState(ctx); - } - } + return (UIEdgeInsets) { + _borderTopWidth >= 0 ? _borderTopWidth : borderWidth, + _borderLeftWidth >= 0 ? _borderLeftWidth : borderWidth, + _borderBottomWidth >= 0 ? _borderBottomWidth : borderWidth, + _borderRightWidth >= 0 ? _borderRightWidth : borderWidth, + }; +} - CGPathRelease(insetPath); +- (RCTCornerRadii)cornerRadii +{ + const CGRect bounds = self.bounds; + const CGFloat maxRadius = MIN(bounds.size.height, bounds.size.width); + const CGFloat radius = MAX(0, _borderRadius); - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); + return (RCTCornerRadii){ + MIN(_borderTopLeftRadius >= 0 ? _borderTopLeftRadius : radius, maxRadius), + MIN(_borderTopRightRadius >= 0 ? _borderTopRightRadius : radius, maxRadius), + MIN(_borderBottomLeftRadius >= 0 ? _borderBottomLeftRadius : radius, maxRadius), + MIN(_borderBottomRightRadius >= 0 ? _borderBottomRightRadius : radius, maxRadius), + }; +} - *contentsCenter = CGRectMake(edgeInsets.left / size.width, edgeInsets.top / size.height, 1.0 / size.width, 1.0 / size.height); - return [image resizableImageWithCapInsets:edgeInsets]; +- (RCTBorderColors)borderColors +{ + return (RCTBorderColors){ + _borderTopColor ?: _borderColor, + _borderLeftColor ?: _borderColor, + _borderBottomColor ?: _borderColor, + _borderRightColor ?: _borderColor + }; } - (void)displayLayer:(CALayer *)layer { - CGRect contentsCenter = {.size = {1, 1}}; - UIImage *image = [self borderImage:&contentsCenter]; + const RCTCornerRadii cornerRadii = [self cornerRadii]; + const UIEdgeInsets borderInsets = [self bordersAsInsets]; + const RCTBorderColors borderColors = [self borderColors]; + + BOOL useIOSBorderRendering = + !RCTRunningInTestEnvironment() && + RCTCornerRadiiAreEqual(cornerRadii) && + RCTBorderInsetsAreEqual(borderInsets) && + RCTBorderColorsAreEqual(borderColors); + + // TODO: A problem with this is that iOS draws borders in front of the content + // whereas CSS draws them behind the content. Also iOS clips to the outside of + // the border, but CSS clips to the inside. To solve this, we'll need to add + // a container view inside the main view to correctly clip the subviews. + + if (useIOSBorderRendering) { + layer.cornerRadius = cornerRadii.topLeft; + layer.borderColor = borderColors.left; + layer.borderWidth = borderInsets.left; + layer.backgroundColor = _backgroundColor.CGColor; + layer.contents = nil; + layer.needsDisplayOnBoundsChange = NO; + layer.mask = nil; + return; + } + + UIImage *image = RCTGetBorderImage([self cornerRadii], + [self bordersAsInsets], + [self borderColors], + _backgroundColor.CGColor, + self.clipsToBounds); + + const CGRect contentsCenter = ({ + CGSize size = image.size; + UIEdgeInsets insets = image.capInsets; + CGRectMake( + insets.left / size.width, + insets.top / size.height, + 1.0 / size.width, + 1.0 / size.height + ); + }); - if (image && RCTRunningInTestEnvironment()) { + if (RCTRunningInTestEnvironment()) { const CGSize size = self.bounds.size; UIGraphicsBeginImageContextWithOptions(size, NO, image.scale); [image drawInRect:(CGRect){CGPointZero, size}]; @@ -690,12 +527,12 @@ - (void)displayLayer:(CALayer *)layer UIGraphicsEndImageContext(); } - layer.backgroundColor = [image ? [UIColor clearColor] : _backgroundColor CGColor]; + layer.backgroundColor = NULL; layer.contents = (id)image.CGImage; layer.contentsCenter = contentsCenter; - layer.contentsScale = image.scale ?: 1.0; + layer.contentsScale = image.scale; layer.magnificationFilter = kCAFilterNearest; - layer.needsDisplayOnBoundsChange = image != nil; + layer.needsDisplayOnBoundsChange = YES; [self updateClippingForLayer:layer]; } @@ -706,30 +543,19 @@ - (void)updateClippingForLayer:(CALayer *)layer CGFloat cornerRadius = 0; if (self.clipsToBounds) { - if (_borderRadius > 0 && _borderTopLeftRadius < 0 && _borderTopRightRadius < 0 && _borderBottomLeftRadius < 0 && _borderBottomRightRadius < 0) { - cornerRadius = _borderRadius; - } else { - const CGRect bounds = layer.bounds; - const CGFloat maxRadius = MIN(bounds.size.height, bounds.size.width); - const CGFloat radius = MAX(0, _borderRadius); - const CGFloat topLeftRadius = MIN(_borderTopLeftRadius >= 0 ? _borderTopLeftRadius : radius, maxRadius); - const CGFloat topRightRadius = MIN(_borderTopRightRadius >= 0 ? _borderTopRightRadius : radius, maxRadius); - const CGFloat bottomLeftRadius = MIN(_borderBottomLeftRadius >= 0 ? _borderBottomLeftRadius : radius, maxRadius); - const CGFloat bottomRightRadius = MIN(_borderBottomRightRadius >= 0 ? _borderBottomRightRadius : radius, maxRadius); - - if (ABS(topLeftRadius - topRightRadius) < RCTViewBorderThreshold && - ABS(topLeftRadius - bottomLeftRadius) < RCTViewBorderThreshold && - ABS(topLeftRadius - bottomRightRadius) < RCTViewBorderThreshold) { - cornerRadius = topLeftRadius; - } else { - CAShapeLayer *shapeLayer = [CAShapeLayer layer]; - CGPathRef path = RCTPathCreateWithRoundedRect(bounds, topLeftRadius, topLeftRadius, topRightRadius, topRightRadius, bottomLeftRadius, bottomLeftRadius, bottomRightRadius, bottomRightRadius, NULL); - shapeLayer.path = path; - CGPathRelease(path); + const RCTCornerRadii cornerRadii = [self cornerRadii]; + if (RCTCornerRadiiAreEqual(cornerRadii)) { - mask = shapeLayer; - } + cornerRadius = cornerRadii.topLeft; + + } else { + + CAShapeLayer *shapeLayer = [CAShapeLayer layer]; + CGPathRef path = RCTPathCreateWithRoundedRect(self.bounds, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL); + shapeLayer.path = path; + CGPathRelease(path); + mask = shapeLayer; } } @@ -790,74 +616,3 @@ - (void)setBorder##side##Radius:(CGFloat)border##side##Radius \ setBorderRadius(BottomRight) @end - -static void RCTPathAddEllipticArc(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y, CGFloat xRadius, CGFloat yRadius, CGFloat startAngle, CGFloat endAngle, bool clockwise) -{ - CGFloat xScale = 1, yScale = 1, radius = 0; - if (xRadius != 0) { - xScale = 1; - yScale = yRadius / xRadius; - radius = xRadius; - } else if (yRadius != 0) { - xScale = xRadius / yRadius; - yScale = 1; - radius = yRadius; - } - - CGAffineTransform t = CGAffineTransformMakeTranslation(x, y); - t = CGAffineTransformScale(t, xScale, yScale); - if (m != NULL) { - t = CGAffineTransformConcat(t, *m); - } - - CGPathAddArc(path, &t, 0, 0, radius, startAngle, endAngle, clockwise); -} - -static CGPathRef RCTPathCreateWithRoundedRect(CGRect rect, CGFloat topLeftRadiusX, CGFloat topLeftRadiusY, CGFloat topRightRadiusX, CGFloat topRightRadiusY, CGFloat bottomLeftRadiusX, CGFloat bottomLeftRadiusY, CGFloat bottomRightRadiusX, CGFloat bottomRightRadiusY, const CGAffineTransform *transform) -{ - const CGFloat minX = CGRectGetMinX(rect); - const CGFloat minY = CGRectGetMinY(rect); - const CGFloat maxX = CGRectGetMaxX(rect); - const CGFloat maxY = CGRectGetMaxY(rect); - - CGMutablePathRef path = CGPathCreateMutable(); - RCTPathAddEllipticArc(path, transform, minX + topLeftRadiusX, minY + topLeftRadiusY, topLeftRadiusX, topLeftRadiusY, M_PI, 3 * M_PI_2, false); - RCTPathAddEllipticArc(path, transform, maxX - topRightRadiusX, minY + topRightRadiusY, topRightRadiusX, topRightRadiusY, 3 * M_PI_2, 0, false); - RCTPathAddEllipticArc(path, transform, maxX - bottomRightRadiusX, maxY - bottomRightRadiusY, bottomRightRadiusX, bottomRightRadiusY, 0, M_PI_2, false); - RCTPathAddEllipticArc(path, transform, minX + bottomLeftRadiusX, maxY - bottomLeftRadiusY, bottomLeftRadiusX, bottomLeftRadiusY, M_PI_2, M_PI, false); - CGPathCloseSubpath(path); - return path; -} - -static BOOL RCTEllipseGetIntersectionsWithLine(CGRect ellipseBoundingRect, CGPoint p1, CGPoint p2, CGPoint intersections[2]) -{ - const CGFloat ellipseCenterX = CGRectGetMidX(ellipseBoundingRect); - const CGFloat ellipseCenterY = CGRectGetMidY(ellipseBoundingRect); - - // ellipseBoundingRect.origin.x -= ellipseCenterX; - // ellipseBoundingRect.origin.y -= ellipseCenterY; - - p1.x -= ellipseCenterX; - p1.y -= ellipseCenterY; - - p2.x -= ellipseCenterX; - p2.y -= ellipseCenterY; - - const CGFloat m = (p2.y - p1.y) / (p2.x - p1.x); - const CGFloat a = ellipseBoundingRect.size.width / 2; - const CGFloat b = ellipseBoundingRect.size.height / 2; - const CGFloat c = p1.y - m * p1.x; - const CGFloat A = (b * b + a * a * m * m); - const CGFloat B = 2 * a * a * c * m; - const CGFloat D = sqrt((a * a * (b * b - c * c)) / A + pow(B / (2 * A), 2)); - - const CGFloat x_ = -B / (2 * A); - const CGFloat x1 = x_ + D; - const CGFloat x2 = x_ - D; - const CGFloat y1 = m * x1 + c; - const CGFloat y2 = m * x2 + c; - - intersections[0] = CGPointMake(x1 + ellipseCenterX, y1 + ellipseCenterY); - intersections[1] = CGPointMake(x2 + ellipseCenterX, y2 + ellipseCenterY); - return YES; -} diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 311167761090b4..d7a93b7e31fc7e 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -102,10 +102,7 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *) RCT_REMAP_VIEW_PROPERTY(shadowOpacity, layer.shadowOpacity, float) RCT_REMAP_VIEW_PROPERTY(shadowRadius, layer.shadowRadius, CGFloat) RCT_REMAP_VIEW_PROPERTY(transformMatrix, layer.transform, CATransform3D) -RCT_CUSTOM_VIEW_PROPERTY(overflow, css_overflow, RCTView) -{ - view.clipsToBounds = json ? ![RCTConvert css_overflow:json] : defaultView.clipsToBounds; -} +RCT_REMAP_VIEW_PROPERTY(overflow, clipsToBounds, css_clip_t) RCT_CUSTOM_VIEW_PROPERTY(pointerEvents, RCTPointerEvents, RCTView) { if ([view respondsToSelector:@selector(setPointerEvents:)]) { @@ -260,7 +257,7 @@ - (RCTViewEventHandler)eventHandlerWithName:(NSString *)eventName json:(id)json RCT_EXPORT_SHADOW_PROPERTY(paddingLeft, CGFloat) RCT_EXPORT_SHADOW_PROPERTY(paddingVertical, CGFloat) RCT_EXPORT_SHADOW_PROPERTY(paddingHorizontal, CGFloat) -RCT_EXPORT_SHADOW_PROPERTY(padding, CGFloat); +RCT_EXPORT_SHADOW_PROPERTY(padding, CGFloat) RCT_EXPORT_SHADOW_PROPERTY(flex, CGFloat) RCT_EXPORT_SHADOW_PROPERTY(flexDirection, css_flex_direction_t) diff --git a/React/Views/UIView+React.h b/React/Views/UIView+React.h index 28bca9db9eef36..cdd65ee960eeb7 100644 --- a/React/Views/UIView+React.h +++ b/React/Views/UIView+React.h @@ -21,6 +21,11 @@ */ - (void)reactSetFrame:(CGRect)frame; +/** + * Used to improve performance when compositing views with translucent content. + */ +- (void)reactSetInheritedBackgroundColor:(UIColor *)inheritedBackgroundColor; + /** * This method finds and returns the containing view controller for the view. */ diff --git a/React/Views/UIView+React.m b/React/Views/UIView+React.m index 6e4a8c45c1b921..a448559ca7a56f 100644 --- a/React/Views/UIView+React.m +++ b/React/Views/UIView+React.m @@ -83,6 +83,11 @@ - (void)reactSetFrame:(CGRect)frame self.layer.bounds = bounds; } +- (void)reactSetInheritedBackgroundColor:(UIColor *)inheritedBackgroundColor +{ + self.backgroundColor = inheritedBackgroundColor; +} + - (UIViewController *)backingViewController { id responder = [self nextResponder]; diff --git a/package.json b/package.json index 1f47a6d349fdb5..535d59a57300e8 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "react-native", "version": "0.4.4", "description": "A framework for building native apps using React", + "license": "BSD-3-Clause", "repository": { "type": "git", "url": "git@github.com:facebook/react-native.git" diff --git a/scripts/objc-test.sh b/scripts/objc-test.sh index e02f0fc2241e51..f70df62afc5149 100755 --- a/scripts/objc-test.sh +++ b/scripts/objc-test.sh @@ -25,5 +25,5 @@ trap cleanup EXIT xctool \ -project Examples/UIExplorer/UIExplorer.xcodeproj \ - -scheme UIExplorer -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 5,OS=8.1' \ + -scheme UIExplorer -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 5,OS=8.3' \ test