From fb475dd0c4100c5b3ab1461c09a82b41bcfde7a5 Mon Sep 17 00:00:00 2001 From: Alex Akers Date: Tue, 10 Mar 2015 14:23:03 -0700 Subject: [PATCH] [React Native] Add preliminary animation API --- Examples/2048/2048.xcodeproj/project.pbxproj | 357 ++++++++++++++++++ Examples/2048/AppDelegate.h | 9 + Examples/2048/AppDelegate.m | 44 +++ Examples/2048/Base.lproj/LaunchScreen.xib | 41 ++ Examples/2048/Game2048.js | 298 +++++++++++++++ Examples/2048/GameBoard.js | 189 ++++++++++ .../AppIcon.appiconset/Contents.json | 38 ++ Examples/2048/Info.plist | 38 ++ Examples/2048/main.m | 11 + Libraries/Animation/Animation.js | 34 ++ Libraries/Animation/AnimationMixin.js | 45 +++ Libraries/Animation/AnimationUtils.js | 226 +++++++++++ .../Components/Touchable/TouchableBounce.js | 124 ++++++ Libraries/react-native/react-native.js | 1 + ReactKit/Base/RCTAssert.h | 4 +- ReactKit/Base/RCTConvert.h | 6 + ReactKit/Base/RCTConvert.m | 108 ++++-- ReactKit/Modules/RCTAnimationManager.h | 9 + ReactKit/Modules/RCTAnimationManager.m | 203 ++++++++++ ReactKit/Modules/RCTUIManager.m | 7 +- ReactKit/ReactKit.xcodeproj/project.pbxproj | 6 + 21 files changed, 1771 insertions(+), 27 deletions(-) create mode 100644 Examples/2048/2048.xcodeproj/project.pbxproj create mode 100644 Examples/2048/AppDelegate.h create mode 100644 Examples/2048/AppDelegate.m create mode 100644 Examples/2048/Base.lproj/LaunchScreen.xib create mode 100644 Examples/2048/Game2048.js create mode 100644 Examples/2048/GameBoard.js create mode 100644 Examples/2048/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Examples/2048/Info.plist create mode 100644 Examples/2048/main.m create mode 100644 Libraries/Animation/Animation.js create mode 100644 Libraries/Animation/AnimationMixin.js create mode 100644 Libraries/Animation/AnimationUtils.js create mode 100644 Libraries/Components/Touchable/TouchableBounce.js create mode 100644 ReactKit/Modules/RCTAnimationManager.h create mode 100644 ReactKit/Modules/RCTAnimationManager.m diff --git a/Examples/2048/2048.xcodeproj/project.pbxproj b/Examples/2048/2048.xcodeproj/project.pbxproj new file mode 100644 index 000000000..256cd7aef --- /dev/null +++ b/Examples/2048/2048.xcodeproj/project.pbxproj @@ -0,0 +1,357 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; + 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; + 8323482C1A77B59500B55238 /* libReactKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832348291A77B50100B55238 /* libReactKit.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5119B1A9E6C1200147676; + remoteInfo = RCTText; + }; + 832348281A77B50100B55238 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 834D32361A76971A00F38302 /* ReactKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; + remoteInfo = ReactKit; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 13B07F961A680F5B00A75B9A /* 2048.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = 2048.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../../Libraries/Text/RCTText.xcodeproj; sourceTree = ""; }; + 834D32361A76971A00F38302 /* ReactKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactKit.xcodeproj; path = ../../ReactKit/ReactKit.xcodeproj; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8323482C1A77B59500B55238 /* libReactKit.a in Frameworks */, + 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 13B07FAE1A68108700A75B9A /* 2048 */ = { + isa = PBXGroup; + children = ( + 13B07FAF1A68108700A75B9A /* AppDelegate.h */, + 13B07FB01A68108700A75B9A /* AppDelegate.m */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, + 13B07FB71A68108700A75B9A /* main.m */, + ); + name = 2048; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + 834D32361A76971A00F38302 /* ReactKit.xcodeproj */, + 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, + ); + name = Libraries; + sourceTree = ""; + }; + 832341B11AAA6A8300B99B32 /* Products */ = { + isa = PBXGroup; + children = ( + 832341B51AAA6A8300B99B32 /* libRCTText.a */, + ); + name = Products; + sourceTree = ""; + }; + 832348241A77B50100B55238 /* Products */ = { + isa = PBXGroup; + children = ( + 832348291A77B50100B55238 /* libReactKit.a */, + ); + name = Products; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* 2048 */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 83CBBA001A601CBA00E9B192 /* Products */, + ); + sourceTree = ""; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* 2048.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 13B07F861A680F5B00A75B9A /* 2048 */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "2048" */; + buildPhases = ( + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = 2048; + productName = "Hello World"; + productReference = 13B07F961A680F5B00A75B9A /* 2048.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "2048" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 832341B11AAA6A8300B99B32 /* Products */; + ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; + }, + { + ProductGroup = 832348241A77B50100B55238 /* Products */; + ProjectRef = 834D32361A76971A00F38302 /* ReactKit.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* 2048 */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + 832341B51AAA6A8300B99B32 /* libRCTText.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTText.a; + remoteRef = 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 832348291A77B50100B55238 /* libReactKit.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libReactKit.a; + remoteRef = 832348281A77B50100B55238 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, + 13B07FC11A68108700A75B9A /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { + isa = PBXVariantGroup; + children = ( + 13B07FB21A68108700A75B9A /* Base */, + ); + name = LaunchScreen.xib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = 2048; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = 2048; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "2048" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "2048" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/Examples/2048/AppDelegate.h b/Examples/2048/AppDelegate.h new file mode 100644 index 000000000..062fb99c0 --- /dev/null +++ b/Examples/2048/AppDelegate.h @@ -0,0 +1,9 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +@interface AppDelegate : UIResponder + +@property (nonatomic, strong) UIWindow *window; + +@end diff --git a/Examples/2048/AppDelegate.m b/Examples/2048/AppDelegate.m new file mode 100644 index 000000000..32d9dd85c --- /dev/null +++ b/Examples/2048/AppDelegate.m @@ -0,0 +1,44 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "AppDelegate.h" + +#import "RCTRootView.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + NSURL *jsCodeLocation; + RCTRootView *rootView = [[RCTRootView alloc] init]; + + // Loading JavaScript code - uncomment the one you want. + + // OPTION 1 + // Load from development server. Start the server from the repository root: + // + // $ npm start + // + // To run on device, change `localhost` to the IP address of your computer, and make sure your computer and + // iOS device are on the same Wi-Fi network. + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/2048/Game2048.includeRequire.runModule.bundle"]; + + // OPTION 2 + // Load from pre-bundled file on disk. To re-generate the static bundle, run + // + // $ curl http://localhost:8081/Examples/2048/Game2048.includeRequire.runModule.bundle -o main.jsbundle + // + // and uncomment the next following line + // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + + rootView.scriptURL = jsCodeLocation; + rootView.moduleName = @"Game2048"; + + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UIViewController *rootViewController = [[UIViewController alloc] init]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/Examples/2048/Base.lproj/LaunchScreen.xib b/Examples/2048/Base.lproj/LaunchScreen.xib new file mode 100644 index 000000000..351e21c59 --- /dev/null +++ b/Examples/2048/Base.lproj/LaunchScreen.xib @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/2048/Game2048.js b/Examples/2048/Game2048.js new file mode 100644 index 000000000..4337aee91 --- /dev/null +++ b/Examples/2048/Game2048.js @@ -0,0 +1,298 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Game2048 + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + Animation, + AppRegistry, + StyleSheet, + Text, + View, +} = React; + +var GameBoard = require('./GameBoard'); +var TouchableBounce = require('TouchableBounce'); + +var BOARD_PADDING = 3; +var CELL_MARGIN = 4; +var CELL_SIZE = 60; + +var Cell = React.createClass({ + render: function() { + return ; + } +}); + +var Board = React.createClass({ + render() { + return ( + + + + + + {this.props.children} + + ); + } +}); + +var Tile = React.createClass({ + mixins: [Animation.Mixin], + + calculateOffset() { + var tile = this.props.tile; + + var pos = (i) => { + return BOARD_PADDING + (i * (CELL_SIZE + CELL_MARGIN * 2) + CELL_MARGIN); + }; + + var animationPosition = (i) => { + return pos(i) + (CELL_SIZE / 2); + }; + + var offset = { + top: pos(tile.toRow()), + left: pos(tile.toColumn()), + }; + + if (tile.isNew()) { + offset.opacity = 0; + } else { + var point = [ + animationPosition(tile.toColumn()), + animationPosition(tile.toRow()), + ]; + this.startAnimation('this', 100, 0, 'easeInOutQuad', {position: point}); + } + + return offset; + }, + + componentDidMount() { + setTimeout(() => { + this.startAnimation('this', 300, 0, 'easeInOutQuad', {scaleXY: [1, 1]}); + this.startAnimation('this', 100, 0, 'easeInOutQuad', {opacity: 1}); + }, 0); + }, + + render() { + var tile = this.props.tile; + + var tileStyles = [ + styles.tile, + styles['tile' + tile.value], + this.calculateOffset() + ]; + + var textStyles = [ + styles.value, + tile.value > 4 && styles.whiteText, + tile.value > 100 && styles.threeDigits, + tile.value > 1000 && styles.fourDigits, + ]; + + return ( + + {tile.value} + + ); + } +}); + +var GameEndOverlay = React.createClass({ + render() { + var board = this.props.board; + + if (!board.hasWon() && !board.hasLost()) { + return ; + } + + var message = board.hasWon() ? + 'Good Job!' : 'Game Over'; + + return ( + + {message} + + + Try Again? + + + + ); + } +}); + +var Game2048 = React.createClass({ + getInitialState() { + return { board: new GameBoard() }; + }, + + restartGame() { + this.setState(this.getInitialState()); + }, + + handleTouchStart(event) { + if (this.state.board.hasWon()) { + return; + } + + this.startX = event.nativeEvent.pageX; + this.startY = event.nativeEvent.pageY; + }, + + handleTouchEnd(event) { + if (this.state.board.hasWon()) { + return; + } + + var deltaX = event.nativeEvent.pageX - this.startX; + var deltaY = event.nativeEvent.pageY - this.startY; + + var direction = -1; + if (Math.abs(deltaX) > 3 * Math.abs(deltaY) && Math.abs(deltaX) > 30) { + direction = deltaX > 0 ? 2 : 0; + } else if (Math.abs(deltaY) > 3 * Math.abs(deltaX) && Math.abs(deltaY) > 30) { + direction = deltaY > 0 ? 3 : 1; + } + + if (direction !== -1) { + this.setState({board: this.state.board.move(direction)}); + } + }, + + render() { + var tiles = this.state.board.tiles + .filter((tile) => tile.value) + .map((tile) => ); + + return ( + + + {tiles} + + + + ); + } +}); + +var styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + board: { + padding: BOARD_PADDING, + backgroundColor: '#bbaaaa', + borderRadius: 5, + }, + overlay: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + backgroundColor: 'rgba(221, 221, 221, 0.5)', + flex: 1, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }, + overlayMessage: { + fontSize: 40, + marginBottom: 20, + }, + tryAgain: { + backgroundColor: '#887766', + padding: 20, + borderRadius: 5, + }, + tryAgainText: { + color: '#ffffff', + fontSize: 20, + fontWeight: 'bold', + }, + cell: { + width: CELL_SIZE, + height: CELL_SIZE, + borderRadius: 5, + backgroundColor: '#ddccbb', + margin: CELL_MARGIN, + }, + row: { + flexDirection: 'row', + }, + tile: { + position: 'absolute', + width: CELL_SIZE, + height: CELL_SIZE, + backgroundColor: '#ddccbb', + borderRadius: 5, + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + value: { + fontSize: 24, + color: '#776666', + fontFamily: 'Verdana', + fontWeight: 'bold', + }, + tile2: { + backgroundColor: '#eeeeee', + }, + tile4: { + backgroundColor: '#eeeecc', + }, + tile8: { + backgroundColor: '#ffbb88', + }, + tile16: { + backgroundColor: '#ff9966', + }, + tile32: { + backgroundColor: '#ff7755', + }, + tile64: { + backgroundColor: '#ff5533', + }, + tile128: { + backgroundColor: '#eecc77', + }, + tile256: { + backgroundColor: '#eecc66', + }, + tile512: { + backgroundColor: '#eecc55', + }, + tile1024: { + backgroundColor: '#eecc33', + }, + tile2048: { + backgroundColor: '#eecc22', + }, + whiteText: { + color: '#ffffff', + }, + threeDigits: { + fontSize: 20, + }, + fourDigits: { + fontSize: 18, + }, +}); + +AppRegistry.registerComponent('Game2048', () => Game2048); + +module.exports = Game2048; diff --git a/Examples/2048/GameBoard.js b/Examples/2048/GameBoard.js new file mode 100644 index 000000000..8db899116 --- /dev/null +++ b/Examples/2048/GameBoard.js @@ -0,0 +1,189 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule GameBoard + */ +'use strict'; + +// NB: Taken straight from: https://github.com/IvanVergiliev/2048-react/blob/master/src/board.js +// with no modificiation except to format it for CommonJS and fix lint errors + +var rotateLeft = function (matrix) { + var rows = matrix.length; + var columns = matrix[0].length; + var res = []; + for (var row = 0; row < rows; ++row) { + res.push([]); + for (var column = 0; column < columns; ++column) { + res[row][column] = matrix[column][columns - row - 1]; + } + } + return res; +}; + +var Tile = function (value, row, column) { + this.value = value || 0; + this.row = row || -1; + this.column = column || -1; + this.oldRow = -1; + this.oldColumn = -1; + this.markForDeletion = false; + this.mergedInto = null; + this.id = Tile.id++; +}; + +Tile.id = 0; + +Tile.prototype.moveTo = function (row, column) { + this.oldRow = this.row; + this.oldColumn = this.column; + this.row = row; + this.column = column; +}; + +Tile.prototype.isNew = function () { + return this.oldRow === -1 && !this.mergedInto; +}; + +Tile.prototype.hasMoved = function () { + return (this.fromRow() !== -1 && (this.fromRow() !== this.toRow() || this.fromColumn() !== this.toColumn())) || + this.mergedInto; +}; + +Tile.prototype.fromRow = function () { + return this.mergedInto ? this.row : this.oldRow; +}; + +Tile.prototype.fromColumn = function () { + return this.mergedInto ? this.column : this.oldColumn; +}; + +Tile.prototype.toRow = function () { + return this.mergedInto ? this.mergedInto.row : this.row; +}; + +Tile.prototype.toColumn = function () { + return this.mergedInto ? this.mergedInto.column : this.column; +}; + +var Board = function () { + this.tiles = []; + this.cells = []; + for (var i = 0; i < Board.size; ++i) { + this.cells[i] = [this.addTile(), this.addTile(), this.addTile(), this.addTile()]; + } + this.addRandomTile(); + this.setPositions(); + this.won = false; +}; + +Board.prototype.addTile = function () { + var res = new Tile(); + Tile.apply(res, arguments); + this.tiles.push(res); + return res; +}; + +Board.size = 4; + +Board.prototype.moveLeft = function () { + var hasChanged = false; + for (var row = 0; row < Board.size; ++row) { + var currentRow = this.cells[row].filter(function (tile) { return tile.value !== 0; }); + var resultRow = []; + for (var target = 0; target < Board.size; ++target) { + var targetTile = currentRow.length ? currentRow.shift() : this.addTile(); + if (currentRow.length > 0 && currentRow[0].value === targetTile.value) { + var tile1 = targetTile; + targetTile = this.addTile(targetTile.value); + tile1.mergedInto = targetTile; + var tile2 = currentRow.shift(); + tile2.mergedInto = targetTile; + targetTile.value += tile2.value; + } + resultRow[target] = targetTile; + this.won |= (targetTile.value === 2048); + hasChanged |= (targetTile.value !== this.cells[row][target].value); + } + this.cells[row] = resultRow; + } + return hasChanged; +}; + +Board.prototype.setPositions = function () { + this.cells.forEach(function (row, rowIndex) { + row.forEach(function (tile, columnIndex) { + tile.oldRow = tile.row; + tile.oldColumn = tile.column; + tile.row = rowIndex; + tile.column = columnIndex; + tile.markForDeletion = false; + }); + }); +}; + +Board.fourProbability = 0.1; + +Board.prototype.addRandomTile = function () { + var emptyCells = []; + for (var r = 0; r < Board.size; ++r) { + for (var c = 0; c < Board.size; ++c) { + if (this.cells[r][c].value === 0) { + emptyCells.push({r: r, c: c}); + } + } + } + var index = Math.floor(Math.random() * emptyCells.length); + var cell = emptyCells[index]; + var newValue = Math.random() < Board.fourProbability ? 4 : 2; + this.cells[cell.r][cell.c] = this.addTile(newValue); +}; + +Board.prototype.move = function (direction) { + // 0 -> left, 1 -> up, 2 -> right, 3 -> down + this.clearOldTiles(); + for (var i = 0; i < direction; ++i) { + this.cells = rotateLeft(this.cells); + } + var hasChanged = this.moveLeft(); + for (var i = direction; i < 4; ++i) { + this.cells = rotateLeft(this.cells); + } + if (hasChanged) { + this.addRandomTile(); + } + this.setPositions(); + return this; +}; + +Board.prototype.clearOldTiles = function () { + this.tiles = this.tiles.filter(function (tile) { return tile.markForDeletion === false; }); + this.tiles.forEach(function (tile) { tile.markForDeletion = true; }); +}; + +Board.prototype.hasWon = function () { + return this.won; +}; + +Board.deltaX = [-1, 0, 1, 0]; +Board.deltaY = [0, -1, 0, 1]; + +Board.prototype.hasLost = function () { + var canMove = false; + for (var row = 0; row < Board.size; ++row) { + for (var column = 0; column < Board.size; ++column) { + canMove |= (this.cells[row][column].value === 0); + for (var dir = 0; dir < 4; ++dir) { + var newRow = row + Board.deltaX[dir]; + var newColumn = column + Board.deltaY[dir]; + if (newRow < 0 || newRow >= Board.size || newColumn < 0 || newColumn >= Board.size) { + continue; + } + canMove |= (this.cells[row][column].value === this.cells[newRow][newColumn].value); + } + } + } + return !canMove; +}; + +module.exports = Board; diff --git a/Examples/2048/Images.xcassets/AppIcon.appiconset/Contents.json b/Examples/2048/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..118c98f74 --- /dev/null +++ b/Examples/2048/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Examples/2048/Info.plist b/Examples/2048/Info.plist new file mode 100644 index 000000000..1c298405a --- /dev/null +++ b/Examples/2048/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Examples/2048/main.m b/Examples/2048/main.m new file mode 100644 index 000000000..a43b55738 --- /dev/null +++ b/Examples/2048/main.m @@ -0,0 +1,11 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/Libraries/Animation/Animation.js b/Libraries/Animation/Animation.js new file mode 100644 index 000000000..80029f148 --- /dev/null +++ b/Libraries/Animation/Animation.js @@ -0,0 +1,34 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Animation + * @flow + */ +'use strict'; + +var { RCTAnimationManager } = require('NativeModules'); +var AnimationUtils = require('AnimationUtils'); + +type EasingFunction = (t: number) => number; + +var Animation = { + Mixin: require('AnimationMixin'), + + startAnimation: function( + node: any, + duration: number, + delay: number, + easing: (string | EasingFunction), + properties: {[key: string]: any} + ): number { + var nodeHandle = +node.getNodeHandle(); + var easingSample = AnimationUtils.evaluateEasingFunction(duration, easing); + RCTAnimationManager.startAnimation(nodeHandle, AnimationUtils.allocateTag(), duration, delay, easingSample, properties); + }, + + stopAnimation: function(tag) { + RCTAnimationManager.stopAnimation(tag); + }, +}; + +module.exports = Animation; diff --git a/Libraries/Animation/AnimationMixin.js b/Libraries/Animation/AnimationMixin.js new file mode 100644 index 000000000..ff29e2735 --- /dev/null +++ b/Libraries/Animation/AnimationMixin.js @@ -0,0 +1,45 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule AnimationMixin + * @flow + */ +'use strict'; + +var AnimationUtils = require('AnimationUtils'); +var { RCTAnimationManager } = require('NativeModules'); + +var invariant = require('invariant'); + +type EasingFunction = (t: number) => number; + +var AnimationMixin = { + getInitialState: function(): Object { + return {}; + }, + + startAnimation: function( + refKey: string, + duration: number, + delay: number, + easing: (string | EasingFunction), + properties: {[key: string]: any} + ): number { + var ref = this.refs[refKey]; + invariant( + ref, + 'Invalid refKey ' + refKey + '; ' + + 'valid refs: ' + JSON.stringify(Object.keys(this.refs)) + ); + + var nodeHandle = +ref.getNodeHandle(); + var easingSample = AnimationUtils.evaluateEasingFunction(duration, easing); + RCTAnimationManager.startAnimation(nodeHandle, AnimationUtils.allocateTag(), duration, delay, easingSample, properties); + }, + + stopAnimation: function(tag: number) { + RCTAnimationManager.stopAnimation(tag); + }, +}; + +module.exports = AnimationMixin; diff --git a/Libraries/Animation/AnimationUtils.js b/Libraries/Animation/AnimationUtils.js new file mode 100644 index 000000000..cf986abe8 --- /dev/null +++ b/Libraries/Animation/AnimationUtils.js @@ -0,0 +1,226 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule AnimationUtils + * @flow + */ +'use strict'; + +type EasingFunction = (t: number) => number; + +var b = 0, + c = 1, + d = 1; +var defaults = { + easeInQuad: function(t) { + return c * (t /= 1) * t + b; + }, + easeOutQuad: function(t) { + return -c * (t /= d) * (t - 2) + b; + }, + easeInOutQuad: function(t) { + if ((t /= d / 2) < 1) { + return c / 2 * t * t + b; + } + return -c / 2 * ((--t) * (t - 2) - 1) + b; + }, + easeInCubic: function(t) { + return c * (t /= d) * t * t + b; + }, + easeOutCubic: function(t) { + return c * ((t = t / d - 1) * t * t + 1) + b; + }, + easeInOutCubic: function(t) { + if ((t /= d / 2) < 1) { + return c / 2 * t * t * t + b; + } + return c / 2 * ((t -= 2) * t * t + 2) + b; + }, + easeInQuart: function(t) { + return c * (t /= d) * t * t * t + b; + }, + easeOutQuart: function(t) { + return -c * ((t = t / d - 1) * t * t * t - 1) + b; + }, + easeInOutQuart: function(t) { + if ((t /= d / 2) < 1) { + return c / 2 * t * t * t * t + b; + } + return -c / 2 * ((t -= 2) * t * t * t - 2) + b; + }, + easeInQuint: function(t) { + return c * (t /= d) * t * t * t * t + b; + }, + easeOutQuint: function(t) { + return c * ((t = t / d - 1) * t * t * t * t + 1) + b; + }, + easeInOutQuint: function(t) { + if ((t /= d / 2) < 1) { + return c / 2 * t * t * t * t * t + b; + } + return c / 2 * ((t -= 2) * t * t * t * t + 2) + b; + }, + easeInSine: function(t) { + return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; + }, + easeOutSine: function(t) { + return c * Math.sin(t / d * (Math.PI / 2)) + b; + }, + easeInOutSine: function(t) { + return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b; + }, + easeInExpo: function(t) { + return (t === 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b; + }, + easeOutExpo: function(t) { + return (t === d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b; + }, + easeInOutExpo: function(t) { + if (t === 0) { + return b; + } + if (t === d) { + return b + c; + } + if ((t /= d / 2) < 1) { + return c / 2 * Math.pow(2, 10 * (t - 1)) + b; + } + return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b; + }, + easeInCirc: function(t) { + return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b; + }, + easeOutCirc: function(t) { + return c * Math.sqrt(1 - (t = t / d - 1) * t) + b; + }, + easeInOutCirc: function(t) { + if ((t /= d / 2) < 1) { + return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b; + } + return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b; + }, + easeInElastic: function(t) { + var s = 1.70158; + var p = 0; + var a = c; + if (t === 0) { + return b; + } + if ((t /= d) === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + if (a < Math.abs(c)) { + a = c; + var s = p / 4; + } else { + var s = p / (2 * Math.PI) * Math.asin(c / a); + } + return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b; + }, + easeOutElastic: function(t) { + var s = 1.70158; + var p = 0; + var a = c; + if (t === 0) { + return b; + } + if ((t /= d) === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + if (a < Math.abs(c)) { + a = c; + var s = p / 4; + } else { + var s = p / (2 * Math.PI) * Math.asin(c / a); + } + return a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b; + }, + easeInOutElastic: function(t) { + var s = 1.70158; + var p = 0; + var a = c; + if (t === 0) { + return b; + } + if ((t /= d / 2) === 2) { + return b + c; + } + if (!p) { + p = d * (0.3 * 1.5); + } + if (a < Math.abs(c)) { + a = c; + var s = p / 4; + } else { + var s = p / (2 * Math.PI) * Math.asin(c / a); + } + if (t < 1) { + return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b; + } + return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * 0.5 + c + b; + }, + easeInBack: function(t) { + var s = 1.70158; + return c * (t /= d) * t * ((s + 1) * t - s) + b; + }, + easeOutBack: function(t) { + var s = 1.70158; + return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; + }, + easeInOutBack: function(t) { + var s = 1.70158; + if ((t /= d / 2) < 1) { + return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; + } + return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; + }, + easeInBounce: function(t) { + return c - this.easeOutBounce(d - t) + b; + }, + easeOutBounce: function(t) { + if ((t /= d) < (1 / 2.75)) { + return c * (7.5625 * t * t) + b; + } else if (t < (2 / 2.75)) { + return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b; + } else if (t < (2.5 / 2.75)) { + return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b; + } else { + return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b; + } + }, + easeInOutBounce: function(t) { + if (t < d / 2) { + return this.easeInBounce(t * 2) * 0.5 + b; + } + return this.easeOutBounce(t * 2 - d) * 0.5 + c * 0.5 + b; + }, +}; + +var ticksPerSecond = 60; +var lastUsedTag = 0; + +module.exports = { + allocateTag: function(): number { + return ++lastUsedTag; + }, + + evaluateEasingFunction: function(duration: number, easing: string | EasingFunction): Array { + if (typeof easing === 'string') { + easing = defaults[easing] || defaults.easeOutQuad; + } + + var tickCount = Math.round(duration * ticksPerSecond / 1000); + var sample = []; + for (var i = 0; i <= tickCount; i++) { + sample.push(easing(i / tickCount)); + } + + return sample; + }, +}; diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js new file mode 100644 index 000000000..2eb052fd8 --- /dev/null +++ b/Libraries/Components/Touchable/TouchableBounce.js @@ -0,0 +1,124 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule TouchableBounce + */ +'use strict'; + +var NativeMethodsMixin = require('NativeMethodsMixin'); +var React = require('React'); +var POPAnimation = require('POPAnimation'); +var Animation = require('Animation'); +var Touchable = require('Touchable'); + +var merge = require('merge'); +var copyProperties = require('copyProperties'); +var onlyChild = require('onlyChild'); + +/** + * 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}; +/** + * Example of using the `TouchableMixin` to play well with other responder + * locking views including `ScrollView`. `TouchableMixin` provides touchable + * hooks (`this.touchableHandle*`) that we forward events to. In turn, + * `TouchableMixin` expects us to implement some abstract methods to handle + * interesting interactions such as `handleTouchablePress`. + */ +var TouchableBounce = React.createClass({ + mixins: [Touchable.Mixin, NativeMethodsMixin], + + propTypes: { + onPress: React.PropTypes.func, + // The function passed takes a callback to start the animation which should + // be run after this onPress handler is done. You can use this (for example) + // to update UI before starting the animation. + onPressWithCompletion: React.PropTypes.func, + // the function passed is called after the animation is complete + onPressAnimationComplete: React.PropTypes.func, + }, + + getInitialState: function() { + return merge(this.touchableGetInitialState(), {animationID: null}); + }, + + bounceTo: function(value, velocity, bounciness, fromValue, callback) { + if (POPAnimation) { + this.state.animationID && this.removeAnimation(this.state.animationID); + var anim = { + property: POPAnimation.Properties.scaleXY, + dynamicsTension: 0, + toValue: [value, value], + velocity: [velocity, velocity], + springBounciness: bounciness, + }; + if (fromValue) { + anim.fromValue = [fromValue, fromValue]; + } + this.state.animationID = POPAnimation.createSpringAnimation(anim); + this.addAnimation(this.state.animationID, callback); + } else { + Animation.startAnimation(this, 300, 0, 'easeOutBack', {scaleXY: [value, value]}); + if (fromValue && typeof fromValue === 'function') { + callback = fromValue; + } + if (callback) { + setTimeout(callback, 300); + } + } + }, + + /** + * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are + * defined on your component. + */ + touchableHandleActivePressIn: function() { + this.bounceTo(0.93, 0.1, 0); + }, + + touchableHandleActivePressOut: function() { + this.bounceTo(1, 0.4, 0); + }, + + touchableHandlePress: function() { + if (this.props.onPressWithCompletion) { + this.props.onPressWithCompletion( + this.bounceTo.bind(this, 1, 10, 10, 0.93, this.props.onPressAnimationComplete) + ); + return; + } + + this.bounceTo(1, 10, 10, undefined, this.props.onPressAnimationComplete); + 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() { + // Note(vjeux): use cloneWithProps once React has been upgraded + var child = onlyChild(this.props.children); + copyProperties(child.props, { + accessible: true, + testID: this.props.testID, + onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, + onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, + onResponderGrant: this.touchableHandleResponderGrant, + onResponderMove: this.touchableHandleResponderMove, + onResponderRelease: this.touchableHandleResponderRelease, + onResponderTerminate: this.touchableHandleResponderTerminate + }); + return child; + } +}); + +module.exports = TouchableBounce; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index c01a59b68..4d4eee73d 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -7,6 +7,7 @@ var ReactNative = { ...require('React'), + Animation: require('Animation'), AppRegistry: require('AppRegistry'), CameraRoll: require('CameraRoll'), DatePickerIOS: require('DatePickerIOS'), diff --git a/ReactKit/Base/RCTAssert.h b/ReactKit/Base/RCTAssert.h index 66f670430..0aaf176f0 100644 --- a/ReactKit/Base/RCTAssert.h +++ b/ReactKit/Base/RCTAssert.h @@ -4,8 +4,8 @@ #define RCTErrorDomain @"RCTErrorDomain" -#define RCTAssert(condition, message, ...) _RCTAssert(condition, message, ##__VA_ARGS__) -#define RCTCAssert(condition, message, ...) _RCTCAssert(condition, message, ##__VA_ARGS__) +#define RCTAssert(condition, message, ...) _RCTAssert((condition) != 0, message, ##__VA_ARGS__) +#define RCTCAssert(condition, message, ...) _RCTCAssert((condition) != 0, message, ##__VA_ARGS__) typedef void (^RCTAssertFunction)(BOOL condition, NSString *message, ...); diff --git a/ReactKit/Base/RCTConvert.h b/ReactKit/Base/RCTConvert.h index 16cd6967b..709410f17 100644 --- a/ReactKit/Base/RCTConvert.h +++ b/ReactKit/Base/RCTConvert.h @@ -88,6 +88,12 @@ BOOL RCTSetProperty(id target, NSString *keypath, id json); */ BOOL RCTCopyProperty(id target, id source, NSString *keypath); +/** + * This function attempts to convert a JSON value to an object that can be used + * in KVC with the specific target and key path. + */ +id RCTConvertValue(id target, NSString *keypath, id json); + #ifdef __cplusplus } #endif diff --git a/ReactKit/Base/RCTConvert.m b/ReactKit/Base/RCTConvert.m index 760f8284d..a2dcea511 100644 --- a/ReactKit/Base/RCTConvert.m +++ b/ReactKit/Base/RCTConvert.m @@ -608,7 +608,7 @@ static NSString *RCTGuessTypeEncoding(id target, NSString *key, id value, NSStri return nil; } -static NSDictionary *RCTConvertValue(id value, NSString *encoding) +static id RCTConvertValueWithEncoding(id value, NSString *encoding) { static NSDictionary *converters = nil; static dispatch_once_t onceToken; @@ -690,18 +690,7 @@ static NSDictionary *RCTConvertValue(id value, NSString *encoding) return converter ? converter(value) : value; } -BOOL RCTSetProperty(id target, NSString *keypath, id value) -{ - // Split keypath - NSArray *parts = [keypath componentsSeparatedByString:@"."]; - NSString *key = [parts lastObject]; - for (NSUInteger i = 0; i < parts.count - 1; i++) { - target = [target valueForKey:parts[i]]; - if (!target) { - return NO; - } - } - +static NSString *RCTPropertyEncoding(id target, NSString *key, id value) { // Check target class for property definition NSString *encoding = nil; objc_property_t property = class_getProperty([target class], [key UTF8String]); @@ -720,7 +709,7 @@ BOOL RCTSetProperty(id target, NSString *keypath, id value) [key substringFromIndex:1]]); if (![target respondsToSelector:setter]) { - return NO; + return nil; } // Get type of first method argument @@ -730,17 +719,92 @@ BOOL RCTSetProperty(id target, NSString *keypath, id value) encoding = @(typeEncoding); free(typeEncoding); } + + if (encoding.length == 0 || [encoding isEqualToString:@(@encode(id))]) { + // Not enough info about the type encoding to be useful, so + // try to guess the type from the value and property name + encoding = RCTGuessTypeEncoding(target, key, value, encoding); + } + } - if (encoding.length == 0 || [encoding isEqualToString:@(@encode(id))]) { - // Not enough info about the type encoding to be useful, so - // try to guess the type from the value and property name - encoding = RCTGuessTypeEncoding(target, key, value, encoding); + return encoding; +} + +static id RCTConvertValueWithExplicitEncoding(id target, NSString *key, id json, NSString *encoding) { + if (!encoding) return nil; + + // Special case for numeric encodings, which may be enums + if ([json isKindOfClass:[NSString class]] && + [@"iIsSlLqQ" rangeOfString:[encoding substringToIndex:1]].length) { + + /** + * NOTE: the property names below may seem weird, but it's + * because they are tested as case-sensitive suffixes, so + * "apitalizationType" will match any of the following + * + * - capitalizationType + * - autocapitalizationType + * - autoCapitalizationType + * - titleCapitalizationType + * - etc. + */ + static NSDictionary *converters = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + converters = + @{ + @"apitalizationType": ^(id val) { + return [RCTConvert UITextAutocapitalizationType:val]; + }, + @"eyboardType": ^(id val) { + return [RCTConvert UIKeyboardType:val]; + }, + @"extAlignment": ^(id val) { + return [RCTConvert NSTextAlignment:val]; + }, + @"ointerEvents": ^(id val) { + return [RCTConvert RCTPointerEvents:val]; + }, + }; + }); + for (NSString *subkey in converters) { + if ([key hasSuffix:subkey]) { + NSInteger (^converter)(NSString *) = converters[subkey]; + json = @(converter(json)); + break; + } + } } + return RCTConvertValueWithEncoding(json, encoding); +} + +id RCTConvertValue(id target, NSString *key, id json) { + NSString *encoding = RCTPropertyEncoding(target, key, json); + return RCTConvertValueWithExplicitEncoding(target, key, json, encoding); +} + +BOOL RCTSetProperty(id target, NSString *keypath, id value) +{ + // Split keypath + NSArray *parts = [keypath componentsSeparatedByString:@"."]; + NSString *key = [parts lastObject]; + for (NSUInteger i = 0; i < parts.count - 1; i++) { + target = [target valueForKey:parts[i]]; + if (!target) { + return NO; + } + } + + NSString *encoding = RCTPropertyEncoding(target, key, value); + if (!encoding) return NO; + + value = RCTConvertValueWithExplicitEncoding(target, keypath, value, encoding); + // Special case for numeric encodings, which may be enums if ([value isKindOfClass:[NSString class]] && - [@"iIsSlLqQ" rangeOfString:[encoding substringToIndex:1]].length) { + [@"iIsSlLqQ" rangeOfString:[encoding substringToIndex:1]].location != NSNotFound) { /** * NOTE: the property names below may seem weird, but it's @@ -798,15 +862,15 @@ BOOL RCTSetProperty(id target, NSString *keypath, id value) }); void (^block)(UITextField *f, NSInteger v) = specialCases[key]; - if (block) - { + if (block) { block(target, [value integerValue]); return YES; } } // Set converted value - [target setValue:RCTConvertValue(value, encoding) forKey:key]; + [target setValue:value forKey:key]; + return YES; } diff --git a/ReactKit/Modules/RCTAnimationManager.h b/ReactKit/Modules/RCTAnimationManager.h new file mode 100644 index 000000000..9f2391514 --- /dev/null +++ b/ReactKit/Modules/RCTAnimationManager.h @@ -0,0 +1,9 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +#import "RCTBridgeModule.h" + +@interface RCTAnimationManager : NSObject + +@end diff --git a/ReactKit/Modules/RCTAnimationManager.m b/ReactKit/Modules/RCTAnimationManager.m new file mode 100644 index 000000000..a5de9aa29 --- /dev/null +++ b/ReactKit/Modules/RCTAnimationManager.m @@ -0,0 +1,203 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTAnimationManager.h" + +#import +#import + +#import "RCTSparseArray.h" +#import "RCTUIManager.h" + +#if CGFLOAT_IS_DOUBLE + #define CG_APPEND(PREFIX, SUFFIX_F, SUFFIX_D) PREFIX##SUFFIX_D +#else + #define CG_APPEND(PREFIX, SUFFIX_F, SUFFIX_D) PREFIX##SUFFIX_F +#endif + +@implementation RCTAnimationManager +{ + RCTSparseArray *_animationRegistry; // Main thread only; animation tag -> view tag +} + +@synthesize bridge = _bridge; + +- (instancetype)init +{ + if ((self = [super init])) { + _animationRegistry = [[RCTSparseArray alloc] init]; + } + + return self; +} + +- (id (^)(CGFloat))interpolateFrom:(CGFloat[])fromArray to:(CGFloat[])toArray count:(NSUInteger)count typeName:(const char *)typeName +{ + if (count == 1) { + CGFloat from = *fromArray, to = *toArray, delta = to - from; + return ^(CGFloat t) { + return @(from + t * delta); + }; + } + + CG_APPEND(vDSP_vsub,,D)(fromArray, 1, toArray, 1, toArray, 1, count); + + const size_t size = count * sizeof(CGFloat); + NSData *deltaData = [NSData dataWithBytes:toArray length:size]; + NSData *fromData = [NSData dataWithBytes:fromArray length:size]; + + return ^(CGFloat t) { + const CGFloat *delta = deltaData.bytes; + const CGFloat *fromArray = fromData.bytes; + + CGFloat value[count]; + CG_APPEND(vDSP_vma,,D)(delta, 1, &t, 0, fromArray, 1, value, 1, count); + return [NSValue valueWithBytes:value objCType:typeName]; + }; +} + +- (void)startAnimationForTag:(NSNumber *)reactTag animationTag:(NSNumber *)animationTag duration:(double)duration delay:(double)delay easingSample:(NSArray *)easingSample properties:(NSDictionary *)properties +{ + RCT_EXPORT(startAnimation); + + __weak RCTAnimationManager *weakSelf = self; + [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + RCTAnimationManager *strongSelf = weakSelf; + + UIView *view = viewRegistry[reactTag]; + if (!view) { + RCTLogWarn(@"React tag %@ is not registered with the view registry", reactTag); + return; + } + + [properties enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { + NSValue *toValue = nil; + if ([key isEqualToString:@"scaleXY"]) { + key = @"transform.scale"; + toValue = obj[0]; + } else if ([obj respondsToSelector:@selector(count)]) { + switch ([obj count]) { + case 2: + if ([obj respondsToSelector:@selector(objectForKey:)] && [obj objectForKey:@"w"]) { + toValue = [NSValue valueWithCGSize:[RCTConvert CGSize:obj]]; + } else { + toValue = [NSValue valueWithCGPoint:[RCTConvert CGPoint:obj]]; + } + break; + case 4: + toValue = [NSValue valueWithCGRect:[RCTConvert CGRect:obj]]; + break; + case 16: + toValue = [NSValue valueWithCGAffineTransform:[RCTConvert CGAffineTransform:obj]]; + break; + } + } + + if (!toValue) toValue = obj; + + const char *typeName = toValue.objCType; + + size_t count; + switch (typeName[0]) { + case 'i': + case 'I': + case 's': + case 'S': + case 'l': + case 'L': + case 'q': + case 'Q': + count = 1; + break; + + default: { + NSUInteger size; + NSGetSizeAndAlignment(typeName, &size, NULL); + count = size / sizeof(CGFloat); + break; + } + } + + CGFloat toFields[count]; + + switch (typeName[0]) { +#define CASE(encoding, type) \ + case encoding: { \ + type value; \ + [toValue getValue:&value]; \ + toFields[0] = value; \ + break; \ + } + + CASE('i', int) + CASE('I', unsigned int) + CASE('s', short) + CASE('S', unsigned short) + CASE('l', long) + CASE('L', unsigned long) + CASE('q', long long) + CASE('Q', unsigned long long) + +#undef CASE + + default: + [toValue getValue:toFields]; + break; + } + + NSValue *fromValue = [view.layer.presentationLayer valueForKeyPath:key]; + CGFloat fromFields[count]; + [fromValue getValue:fromFields]; + + id (^interpolationBlock)(CGFloat t) = [strongSelf interpolateFrom:fromFields to:toFields count:count typeName:typeName]; + + NSMutableArray *sampledValues = [NSMutableArray arrayWithCapacity:easingSample.count]; + for (NSNumber *sample in easingSample) { + CGFloat t = sample.CG_APPEND(, floatValue, doubleValue); + [sampledValues addObject:interpolationBlock(t)]; + } + + CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:key]; + animation.beginTime = CACurrentMediaTime() + delay / 1000.0; + animation.duration = duration / 1000.0; + animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; + animation.values = sampledValues; + + [view.layer setValue:toValue forKey:key]; + + NSString *animationKey = [NSString stringWithFormat:@"RCT.%@.%@", animationTag, key]; + [view.layer addAnimation:animation forKey:animationKey]; + }]; + + strongSelf->_animationRegistry[animationTag] = reactTag; + }]; +} + +- (void)stopAnimation:(NSNumber *)animationTag +{ + RCT_EXPORT(stopAnimation); + + __weak RCTAnimationManager *weakSelf = self; + [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + RCTAnimationManager *strongSelf = weakSelf; + + NSNumber *reactTag = strongSelf->_animationRegistry[animationTag]; + if (!reactTag) return; + + UIView *view = viewRegistry[reactTag]; + for (NSString *animationKey in view.layer.animationKeys) { + if ([animationKey hasPrefix:@"RCT"]) { + NSRange periodLocation = [animationKey rangeOfString:@"." options:0 range:NSMakeRange(3, animationKey.length - 3)]; + if (periodLocation.location != NSNotFound) { + NSInteger integerTag = [[animationKey substringWithRange:NSMakeRange(3, periodLocation.location)] integerValue]; + if (animationTag.integerValue == integerTag) { + [view.layer removeAnimationForKey:animationKey]; + } + } + } + } + + strongSelf->_animationRegistry[animationTag] = nil; + }]; +} + +@end diff --git a/ReactKit/Modules/RCTUIManager.m b/ReactKit/Modules/RCTUIManager.m index a22f24540..ae69890d3 100644 --- a/ReactKit/Modules/RCTUIManager.m +++ b/ReactKit/Modules/RCTUIManager.m @@ -2,24 +2,25 @@ #import "RCTUIManager.h" -#import #import +#import + #import "Layout.h" #import "RCTAnimationType.h" #import "RCTAssert.h" #import "RCTBridge.h" #import "RCTConvert.h" -#import "RCTRootView.h" #import "RCTLog.h" #import "RCTNavigator.h" +#import "RCTRootView.h" #import "RCTScrollableProtocol.h" #import "RCTShadowView.h" #import "RCTSparseArray.h" #import "RCTUtils.h" #import "RCTView.h" -#import "RCTViewNodeProtocol.h" #import "RCTViewManager.h" +#import "RCTViewNodeProtocol.h" #import "UIView+ReactKit.h" typedef void (^react_view_node_block_t)(id); diff --git a/ReactKit/ReactKit.xcodeproj/project.pbxproj b/ReactKit/ReactKit.xcodeproj/project.pbxproj index c2efa2125..fc8e49ac3 100644 --- a/ReactKit/ReactKit.xcodeproj/project.pbxproj +++ b/ReactKit/ReactKit.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; }; 830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 830BA4541A8E3BDA00D53203 /* RCTCache.m */; }; 832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; }; + 83C911101AAE6521001323A3 /* RCTAnimationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 83C9110F1AAE6521001323A3 /* RCTAnimationManager.m */; }; 83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA4B1A601E3B00E9B192 /* RCTAssert.m */; }; 83CBBA521A601E3B00E9B192 /* RCTLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA4E1A601E3B00E9B192 /* RCTLog.m */; }; 83CBBA531A601E3B00E9B192 /* RCTUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA501A601E3B00E9B192 /* RCTUtils.m */; }; @@ -153,6 +154,8 @@ 830BA4541A8E3BDA00D53203 /* RCTCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCache.m; sourceTree = ""; }; 83BEE46C1A6D19BC00B5863B /* RCTSparseArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSparseArray.h; sourceTree = ""; }; 83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSparseArray.m; sourceTree = ""; }; + 83C9110E1AAE6521001323A3 /* RCTAnimationManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationManager.h; sourceTree = ""; }; + 83C9110F1AAE6521001323A3 /* RCTAnimationManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimationManager.m; sourceTree = ""; }; 83CBBA2E1A601D0E00E9B192 /* libReactKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReactKit.a; sourceTree = BUILT_PRODUCTS_DIR; }; 83CBBA4A1A601E3B00E9B192 /* RCTAssert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAssert.h; sourceTree = ""; }; 83CBBA4B1A601E3B00E9B192 /* RCTAssert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAssert.m; sourceTree = ""; }; @@ -210,6 +213,8 @@ children = ( 13B07FE71A69327A00A75B9A /* RCTAlertManager.h */, 13B07FE81A69327A00A75B9A /* RCTAlertManager.m */, + 83C9110E1AAE6521001323A3 /* RCTAnimationManager.h */, + 83C9110F1AAE6521001323A3 /* RCTAnimationManager.m */, 13B07FE91A69327A00A75B9A /* RCTExceptionsManager.h */, 13B07FEA1A69327A00A75B9A /* RCTExceptionsManager.m */, 5F5F0D971A9E456B001279FA /* RCTLocationObserver.h */, @@ -458,6 +463,7 @@ 13A1F71E1A75392D00D3D453 /* RCTKeyCommands.m in Sources */, 83CBBA531A601E3B00E9B192 /* RCTUtils.m in Sources */, 14435CE61AAC4AE100FC20F4 /* RCTMapManager.m in Sources */, + 83C911101AAE6521001323A3 /* RCTAnimationManager.m in Sources */, 83CBBA601A601EAA00E9B192 /* RCTBridge.m in Sources */, 58114A161AAE854800E7D092 /* RCTPicker.m in Sources */, 137327E81AA5CF210034F82E /* RCTTabBarItem.m in Sources */,