├── .editorconfig ├── .gitattributes ├── .gitignore ├── .yarn └── releases │ └── yarn-3.6.4.cjs ├── .yarnrc.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── android ├── .project ├── .settings │ └── org.eclipse.buildship.core.prefs ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── reactnativereanimatedtext │ │ ├── JBTextShadowNode.java │ │ ├── JBTextViewManager.java │ │ └── ReanimatedTextPackage.kt │ └── jni │ ├── CMakeLists.txt │ └── JBAnimatedText.h ├── babel.config.js ├── cpp └── react │ └── renderer │ └── components │ └── JBAnimatedText │ ├── BaseTextShadowNode.cpp │ ├── BaseTextShadowNode.h │ ├── ComponentDescriptors.h │ ├── ParagraphComponentDescriptor.h │ ├── ParagraphProps.cpp │ ├── ParagraphProps.h │ ├── ParagraphShadowNode.cpp │ └── ParagraphShadowNode.h ├── example ├── .gitignore ├── App.tsx ├── app.json ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png ├── babel.config.js ├── metro.config.js ├── package.json ├── react-native.config.js ├── tsconfig.json ├── yalc.lock └── yarn.lock ├── ios ├── JBAnimatedTextComponenetView.h ├── JBAnimatedTextComponenetView.mm ├── JBAnimatedTextManager.h ├── JBAnimatedTextManager.mm ├── JBTextShadowView.h ├── JBTextShadowView.mm └── ReanimatedText.xcodeproj │ └── project.pbxproj ├── package.json ├── react-native-animateable-text.podspec ├── react-native.config.js ├── src ├── AnimateableText.tsx ├── AnimateableText.web.tsx ├── TextProps.tsx ├── index.tsx └── specs │ └── JBAnimatedTextNativeComponent.tsx ├── tsconfig.build.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .idea 35 | .gradle 36 | local.properties 37 | android.iml 38 | 39 | # Cocoapods 40 | # 41 | example/ios/Pods 42 | 43 | # node.js 44 | # 45 | node_modules/ 46 | npm-debug.log 47 | yarn-debug.log 48 | yarn-error.log 49 | 50 | # BUCK 51 | buck-out/ 52 | \.buckd/ 53 | android/app/libs 54 | android/keystores/debug.keystore 55 | 56 | # Expo 57 | .expo/* 58 | 59 | # generated by bob 60 | lib/ 61 | .env 62 | 63 | 64 | .yarn/cache 65 | 66 | 67 | # Yarn 68 | .yarn/* -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-3.6.4.cjs 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. 4 | 5 | ## Development workflow 6 | 7 | To get started with the project, run `yarn bootstrap` in the root directory to install the required dependencies for each package: 8 | 9 | ```sh 10 | yarn bootstrap 11 | ``` 12 | 13 | While developing, you can run the [example app](/example/) to test your changes. 14 | 15 | To start the packager: 16 | 17 | ```sh 18 | yarn example start 19 | ``` 20 | 21 | To run the example app on Android: 22 | 23 | ```sh 24 | yarn example android 25 | ``` 26 | 27 | To run the example app on iOS: 28 | 29 | ```sh 30 | yarn example ios 31 | ``` 32 | 33 | Make sure your code passes TypeScript and ESLint. Run the following to verify: 34 | 35 | ```sh 36 | yarn typescript 37 | yarn lint 38 | ``` 39 | 40 | To fix formatting errors, run the following: 41 | 42 | ```sh 43 | yarn lint --fix 44 | ``` 45 | 46 | Remember to add tests for your change if possible. Run the unit tests by: 47 | 48 | ```sh 49 | yarn test 50 | ``` 51 | 52 | To edit the Objective-C files, open `example/ios/ReanimatedTextExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > react-native-animateable-text`. 53 | 54 | To edit the Kotlin files, open `example/android` in Android studio and find the source files at `reactnativereanimatedtext` under `Android`. 55 | 56 | ### Commit message convention 57 | 58 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 59 | 60 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 61 | - `feat`: new features, e.g. add new method to the module. 62 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 63 | - `docs`: changes into documentation, e.g. add usage example for the module.. 64 | - `test`: adding or updating tests, eg add integration tests using detox. 65 | - `chore`: tooling changes, e.g. change CI config. 66 | 67 | Our pre-commit hooks verify that your commit message matches this format when committing. 68 | 69 | ### Linting and tests 70 | 71 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 72 | 73 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 74 | 75 | Our pre-commit hooks verify that the linter and tests pass when committing. 76 | 77 | ### Scripts 78 | 79 | The `package.json` file contains various scripts for common tasks: 80 | 81 | - `yarn bootstrap`: setup project by installing all dependencies and pods. 82 | - `yarn typescript`: type-check files with TypeScript. 83 | - `yarn lint`: lint files with ESLint. 84 | - `yarn test`: run unit tests with Jest. 85 | - `yarn example start`: start the Metro server for the example app. 86 | - `yarn example android`: run the example app on Android. 87 | - `yarn example ios`: run the example app on iOS. 88 | 89 | ### Sending a pull request 90 | 91 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). 92 | 93 | When you're sending a pull request: 94 | 95 | - Prefer small pull requests focused on one change. 96 | - Verify that linters and tests are passing. 97 | - Review the documentation to make sure it looks good. 98 | - Follow the pull request template when opening a pull request. 99 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 100 | 101 | ## Code of Conduct 102 | 103 | ### Our Pledge 104 | 105 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 106 | 107 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 108 | 109 | ### Our Standards 110 | 111 | Examples of behavior that contributes to a positive environment for our community include: 112 | 113 | - Demonstrating empathy and kindness toward other people 114 | - Being respectful of differing opinions, viewpoints, and experiences 115 | - Giving and gracefully accepting constructive feedback 116 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 117 | - Focusing on what is best not just for us as individuals, but for the overall community 118 | 119 | Examples of unacceptable behavior include: 120 | 121 | - The use of sexualized language or imagery, and sexual attention or 122 | advances of any kind 123 | - Trolling, insulting or derogatory comments, and personal or political attacks 124 | - Public or private harassment 125 | - Publishing others' private information, such as a physical or email 126 | address, without their explicit permission 127 | - Other conduct which could reasonably be considered inappropriate in a 128 | professional setting 129 | 130 | ### Enforcement Responsibilities 131 | 132 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 133 | 134 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 135 | 136 | ### Scope 137 | 138 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 139 | 140 | ### Enforcement 141 | 142 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. 143 | 144 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 145 | 146 | ### Enforcement Guidelines 147 | 148 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 149 | 150 | #### 1. Correction 151 | 152 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 153 | 154 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 155 | 156 | #### 2. Warning 157 | 158 | **Community Impact**: A violation through a single incident or series of actions. 159 | 160 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 161 | 162 | #### 3. Temporary Ban 163 | 164 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 165 | 166 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 167 | 168 | #### 4. Permanent Ban 169 | 170 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 171 | 172 | **Consequence**: A permanent ban from any sort of public interaction within the community. 173 | 174 | ### Attribution 175 | 176 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 177 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 178 | 179 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 180 | 181 | [homepage]: https://www.contributor-covenant.org 182 | 183 | For answers to common questions about this code of conduct, see the FAQ at 184 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 185 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jonny Burger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-animateable-text 2 | 3 | A fork of React Native's `` component that supports Reanimated Shared Values as text! 4 | 5 | ## Compatibility 6 | (🚨 Make sure you use the correct version for your RN project) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
Animateable Text VersionRN VersionOld ArchNew Arch (Fabric)
^0.16.0-beta.0 ^0.79.0
^0.15.0 ^0.77.0
^0.14.2 ^0.76.0
^0.13.0 ^0.75.0🛑
^0.12.0 ^0.74.0🛑
^0.11.0 ^0.71.7🛑
^0.10.0 ^0.68🛑
^0.9.1 ^0.67🛑
^0.8.0 ^0.66🛑
^0.7.0 ^0.65🛑
^0.6.0 ^0.64🛑
^0.5.9 ^0.63🛑
87 | 88 | ## Installation 89 | 90 | First make sure you have reanimated already installed and linked from [here](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/) then run 91 | ```sh 92 | yarn add react-native-animateable-text 93 | ``` 94 | 95 | then for *Expo* managed projects you need to prebuild your project, and for *ReactNative* bare projects you should run 96 | ```sh 97 | npx pod-install 98 | ``` 99 | 100 | 101 | 102 | ## Usage (Reanimated 2/3) 103 | 104 | > Note about Reanimated 2: The library does not work with Alpha 9 until RC1. Make sure to update to RC2 or later! 105 | 106 | Use it the same as a `` component, except instead of passing the text as a child node, pass it using the `text` props. 107 | 108 | ```tsx 109 | import AnimateableText from 'react-native-animateable-text'; 110 | 111 | const Example: React.FC = () => { 112 | const reanimatedText = useSharedValue('World'); 113 | 114 | const animatedProps = useAnimatedProps(() => { 115 | return { 116 | text: reanimatedText.value, 117 | }; 118 | }); 119 | 120 | return ( 121 | props are also available 124 | />; 125 | }; 126 | ``` 127 | 128 | 129 | ## [OMG, why would you build this?](https://www.npmjs.com/package/react-native-reanimated/v/1.4.0#omg-why-would-you-build-this-motivation) 130 | 131 | We want to animate numbers based on gestures as fast as possible, for example for charts displaying financial data. Updating native state is too slow and not feasible for a smooth experience. Using `createAnimatedComponent` doesn't allow you to animate the text since the children of Text are separate nodes rather than just props. 132 | 133 | The best way so far has been to use the `` component from [react-native-redash](https://wcandillon.github.io/react-native-redash-v1-docs/strings#retext), which allows you to render a string from a Reanimated Text node. However, under the hood, it uses a `` and animates it's `value` prop. 134 | 135 | This naturally comes with a few edge cases, for example: 136 | 137 |
    138 | 139 |
  • 140 | *Flicker*: When changing values too fast, the text can be cut off and show an ellipsis. The problem gets worse the slower the device and the more congested the render queue is. Watch this GIF at 0.2x speed carefully:
    141 | 142 | 143 |
  • 144 |
  • 145 | *Inconsistent styling*: When styling a TextInput, you need to add more styles and spacing to make it align with the default Text styles. (Behavior in screenshot happens only on Android) 146 |

  • 147 | 148 |
  • 149 | *Lack of full capabilities*: Not all props are available. With Animateable Text, you can use props that you cannot use on a TextInput, such as selectable (Android), dataDetectorType or onTextLayout. 150 |
    151 | 152 |
  • 153 |
154 | 155 | ## Contributing 156 | 157 | See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. 158 | 159 | ## Credits 160 | 161 | Written by [Jonny Burger](https://jonny.io) for our needs at [Axelra](https://axelra.com). 162 | 163 | Thanks to Axelra for sponsoring my time to turn this into an open source project! 164 | 165 |
166 |
167 | We are a Swiss Agency specializing in React Native, caring even about the smallest of details. 168 | 169 | ## License 170 | 171 | MIT 172 | -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android_ 4 | Project android_ created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | 19 | 1729005394185 20 | 21 | 30 22 | 23 | org.eclipse.core.resources.regexFilterMatcher 24 | node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | arguments=--init-script /var/folders/9f/3mr21ntx3kv0cm900x84ds9w0000gn/T/db3b08fc4a9ef609cb16b96b200fa13e563f396e9bb1ed0905fdab7bc3bc513b.gradle --init-script /var/folders/9f/3mr21ntx3kv0cm900x84ds9w0000gn/T/52cde0cfcf3e28b8b7510e992210d9614505e0911af0c190bd590d7158574963.gradle 2 | auto.sync=false 3 | build.scans.enabled=false 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) 5 | connection.project.dir=../example/android 6 | eclipse.preferences.version=1 7 | gradle.user.home= 8 | java.home=/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home 9 | jvm.arguments= 10 | offline.mode=false 11 | override.workspace.settings=true 12 | show.console.view=true 13 | show.executions.view=true 14 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | // Buildscript is evaluated before everything else so we can't use getExtOrDefault 3 | def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['ReanimatedText_kotlinVersion'] 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.2.1' 12 | // noinspection DifferentKotlinGradleVersion 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | apply plugin: 'com.android.library' 18 | apply plugin: 'kotlin-android' 19 | 20 | def getExtOrDefault(name) { 21 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ReanimatedText_' + name] 22 | } 23 | 24 | def getExtOrIntegerDefault(name) { 25 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['ReanimatedText_' + name]).toInteger() 26 | } 27 | 28 | android { 29 | compileSdkVersion getExtOrIntegerDefault('compileSdkVersion') 30 | buildToolsVersion getExtOrDefault('buildToolsVersion') 31 | defaultConfig { 32 | minSdkVersion 21 33 | targetSdkVersion getExtOrIntegerDefault('targetSdkVersion') 34 | versionCode 1 35 | versionName "1.0" 36 | } 37 | 38 | buildTypes { 39 | release { 40 | minifyEnabled false 41 | } 42 | } 43 | lintOptions { 44 | disable 'GradleCompatible' 45 | } 46 | } 47 | 48 | repositories { 49 | mavenCentral() 50 | jcenter() 51 | google() 52 | 53 | def found = false 54 | def defaultDir = null 55 | def androidSourcesName = 'React Native sources' 56 | 57 | if (rootProject.ext.has('reactNativeAndroidRoot')) { 58 | defaultDir = rootProject.ext.get('reactNativeAndroidRoot') 59 | } else { 60 | defaultDir = new File( 61 | projectDir, 62 | '/../../../node_modules/react-native/android' 63 | ) 64 | } 65 | 66 | if (defaultDir.exists()) { 67 | maven { 68 | url defaultDir.toString() 69 | name androidSourcesName 70 | } 71 | 72 | logger.info(":${project.name}:reactNativeAndroidRoot ${defaultDir.canonicalPath}") 73 | found = true 74 | } else { 75 | def parentDir = rootProject.projectDir 76 | 77 | 1.upto(5, { 78 | if (found) return true 79 | parentDir = parentDir.parentFile 80 | 81 | def androidSourcesDir = new File( 82 | parentDir, 83 | 'node_modules/react-native' 84 | ) 85 | 86 | def androidPrebuiltBinaryDir = new File( 87 | parentDir, 88 | 'node_modules/react-native/android' 89 | ) 90 | 91 | if (androidPrebuiltBinaryDir.exists()) { 92 | maven { 93 | url androidPrebuiltBinaryDir.toString() 94 | name androidSourcesName 95 | } 96 | 97 | logger.info(":${project.name}:reactNativeAndroidRoot ${androidPrebuiltBinaryDir.canonicalPath}") 98 | found = true 99 | } else if (androidSourcesDir.exists()) { 100 | maven { 101 | url androidSourcesDir.toString() 102 | name androidSourcesName 103 | } 104 | 105 | logger.info(":${project.name}:reactNativeAndroidRoot ${androidSourcesDir.canonicalPath}") 106 | found = true 107 | } 108 | }) 109 | } 110 | 111 | if (!found) { 112 | throw new GradleException( 113 | "${project.name}: unable to locate React Native android sources. " + 114 | "Ensure you have you installed React Native as a dependency in your project and try again." 115 | ) 116 | } 117 | } 118 | 119 | def kotlin_version = getExtOrDefault('kotlinVersion') 120 | 121 | dependencies { 122 | // noinspection GradleDynamicVersion 123 | api 'com.facebook.react:react-native:+' 124 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 125 | } 126 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | ReanimatedText_kotlinVersion=1.6.10 2 | ReanimatedText_compileSdkVersion=28 3 | ReanimatedText_buildToolsVersion=28.0.3 4 | ReanimatedText_targetSdkVersion=28 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axelra-ag/react-native-animateable-text/6c565a74e91d2cbefc2484ad0b057a32cd7024c6/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Sep 13 17:40:18 CEST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativereanimatedtext/JBTextShadowNode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom - onBeforeLayout (forked) 3 | */ 4 | 5 | package com.reactnativereanimatedtext; 6 | 7 | import android.annotation.TargetApi; 8 | import android.os.Build; 9 | import com.facebook.react.uimanager.NativeViewHierarchyOptimizer; 10 | import com.facebook.react.uimanager.annotations.ReactProp; 11 | import com.facebook.react.views.text.ReactTextShadowNode; 12 | import com.facebook.react.views.text.ReactTextViewManagerCallback; 13 | 14 | import java.lang.reflect.Field; 15 | 16 | @TargetApi(Build.VERSION_CODES.M) 17 | public class JBTextShadowNode extends ReactTextShadowNode { 18 | protected String mText = ""; 19 | 20 | private static final Field mPreparedSpannableTextField; 21 | static { 22 | try { 23 | mPreparedSpannableTextField = ReactTextShadowNode.class.getDeclaredField("mPreparedSpannableText"); 24 | mPreparedSpannableTextField.setAccessible(true); 25 | } catch (NoSuchFieldException e) { 26 | throw new RuntimeException(e); 27 | } 28 | } 29 | 30 | public JBTextShadowNode() { 31 | this(null); 32 | } 33 | 34 | public JBTextShadowNode(ReactTextViewManagerCallback reactTextViewManagerCallback) { 35 | super(reactTextViewManagerCallback); 36 | } 37 | 38 | @Override 39 | public void onBeforeLayout(NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) { 40 | // ADDED 41 | try { 42 | // END ADDED 43 | mPreparedSpannableTextField.set(this, spannedFromShadowNode( 44 | this, 45 | /* text (e.g. from `value` prop): */ mText, // EDITED 46 | /* supportsInlineViews: */ true, 47 | nativeViewHierarchyOptimizer)); 48 | markUpdated(); 49 | // ADDED 50 | } catch (IllegalAccessException e) { 51 | throw new RuntimeException(e); 52 | } 53 | // END ADDED 54 | } 55 | 56 | @ReactProp(name = "text") 57 | public void setText(String newText) { 58 | mText = newText; 59 | markUpdated(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativereanimatedtext/JBTextViewManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom 3 | */ 4 | 5 | package com.reactnativereanimatedtext; 6 | 7 | import androidx.annotation.Nullable; 8 | import com.facebook.react.module.annotations.ReactModule; 9 | import com.facebook.react.views.text.ReactTextShadowNode; 10 | import com.facebook.react.views.text.ReactTextViewManager; 11 | import com.facebook.react.views.text.ReactTextViewManagerCallback; 12 | import androidx.annotation.NonNull; 13 | 14 | @ReactModule(name = JBTextViewManager.REACT_CLASS) 15 | public class JBTextViewManager 16 | extends ReactTextViewManager { 17 | 18 | public static final String REACT_CLASS = "JBAnimatedText"; 19 | 20 | @NonNull 21 | @Override 22 | public String getName() { 23 | return REACT_CLASS; 24 | } 25 | 26 | @Override 27 | public ReactTextShadowNode createShadowNodeInstance() { 28 | return new JBTextShadowNode(); 29 | } 30 | 31 | public JBTextShadowNode createShadowNodeInstance( 32 | @Nullable ReactTextViewManagerCallback reactTextViewManagerCallback) { 33 | return new JBTextShadowNode(reactTextViewManagerCallback); 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativereanimatedtext/ReanimatedTextPackage.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom 3 | */ 4 | 5 | package com.reactnativereanimatedtext 6 | 7 | import com.facebook.react.ReactPackage 8 | import com.facebook.react.bridge.NativeModule 9 | import com.facebook.react.bridge.ReactApplicationContext 10 | import com.facebook.react.uimanager.ViewManager 11 | import java.util.ArrayList 12 | 13 | class ReanimatedTextPackage : ReactPackage { 14 | override fun createViewManagers(reactContext: ReactApplicationContext): List> { 15 | val viewManagers: MutableList> = ArrayList() 16 | viewManagers.add(JBTextViewManager()) 17 | return viewManagers 18 | } 19 | 20 | override fun createNativeModules(reactContext: ReactApplicationContext): List { 21 | return emptyList() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /android/src/main/jni/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | set(CMAKE_VERBOSE_MAKEFILE ON) 3 | set(CMAKE_CXX_STANDARD 20) 4 | 5 | set(LIB_LITERAL JBAnimatedText) 6 | set(LIB_TARGET_NAME react_codegen_${LIB_LITERAL}) 7 | 8 | set(LIB_ANDROID_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../..) 9 | set(LIB_CPP_DIR ${LIB_ANDROID_DIR}/../cpp) 10 | set(LIB_CUSTOM_SOURCES_DIR ${LIB_CPP_DIR}/react/renderer/components/${LIB_LITERAL}) 11 | set(LIB_ANDROID_GENERATED_JNI_DIR ${LIB_ANDROID_DIR}/build/generated/source/codegen/jni) 12 | set(LIB_ANDROID_GENERATED_COMPONENTS_DIR ${LIB_ANDROID_GENERATED_JNI_DIR}/react/renderer/components/${LIB_LITERAL}) 13 | 14 | file(GLOB LIB_MODULE_SRCS CONFIGURE_DEPENDS *.cpp) 15 | file(GLOB LIB_CUSTOM_SRCS CONFIGURE_DEPENDS ${LIB_CUSTOM_SOURCES_DIR}/*.cpp) 16 | file(GLOB LIB_CODEGEN_SRCS CONFIGURE_DEPENDS ${LIB_ANDROID_GENERATED_COMPONENTS_DIR}/*.cpp) 17 | 18 | set(RN_DIR ${LIB_ANDROID_DIR}/../example/node_modules/react-native) 19 | 20 | add_library( 21 | ${LIB_TARGET_NAME} 22 | SHARED 23 | ${LIB_MODULE_SRCS} 24 | ${LIB_CUSTOM_SRCS} 25 | ${LIB_CODEGEN_SRCS} 26 | ) 27 | 28 | target_include_directories( 29 | ${LIB_TARGET_NAME} 30 | PUBLIC 31 | . 32 | ${LIB_ANDROID_GENERATED_JNI_DIR} 33 | ${LIB_ANDROID_GENERATED_COMPONENTS_DIR} 34 | ${LIB_CPP_DIR} 35 | ) 36 | 37 | find_package(fbjni REQUIRED CONFIG) 38 | find_package(ReactAndroid REQUIRED CONFIG) 39 | 40 | target_link_libraries( 41 | ${LIB_TARGET_NAME} 42 | fbjni::fbjni 43 | ReactAndroid::jsi 44 | ) 45 | 46 | if (ReactAndroid_VERSION_MINOR GREATER_EQUAL 76) 47 | target_link_libraries( 48 | ${LIB_TARGET_NAME} 49 | ReactAndroid::reactnative 50 | ) 51 | else () 52 | message(FATAL_ERROR "react-native-animateable-text requires react-native 0.76 or newer.") 53 | endif () 54 | 55 | target_compile_options( 56 | ${LIB_TARGET_NAME} 57 | PRIVATE 58 | -DLOG_TAG=\"ReactNative\" 59 | -fexceptions 60 | -frtti 61 | -Wall 62 | -std=c++20 63 | ) 64 | 65 | target_include_directories( 66 | ${CMAKE_PROJECT_NAME} 67 | PUBLIC 68 | ${CMAKE_CURRENT_SOURCE_DIR} 69 | ) 70 | -------------------------------------------------------------------------------- /android/src/main/jni/JBAnimatedText.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom 3 | */ 4 | 5 | #pragma once 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace facebook::react { 13 | 14 | JSI_EXPORT 15 | std::shared_ptr JBAnimatedText_ModuleProvider( 16 | const std::string &moduleName, 17 | const JavaTurboModule::InitParams ¶ms) { 18 | 19 | // Ensure moduleName matches the expected name 20 | return nullptr; 21 | } 22 | 23 | } // namespace facebook::react -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['module:react-native-builder-bob/babel-preset', { modules: 'commonjs' }], 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /cpp/react/renderer/components/JBAnimatedText/BaseTextShadowNode.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #include // ADDED 9 | 10 | #include "BaseTextShadowNode.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace facebook::react { 19 | 20 | inline ShadowView shadowViewFromShadowNode(const ShadowNode& shadowNode) { 21 | auto shadowView = ShadowView{shadowNode}; 22 | // Clearing `props` and `state` (which we don't use) allows avoiding retain 23 | // cycles. 24 | shadowView.props = nullptr; 25 | shadowView.state = nullptr; 26 | return shadowView; 27 | } 28 | 29 | void CBaseTextShadowNode::buildAttributedString( // EDITED 30 | const TextAttributes& baseTextAttributes, 31 | const ShadowNode& parentNode, 32 | AttributedString& outAttributedString, 33 | Attachments& outAttachments) { 34 | 35 | // ADDED 36 | auto paragraphParentNode = dynamic_cast(&parentNode); 37 | if (paragraphParentNode != nullptr) { 38 | auto fragment = AttributedString::Fragment{}; 39 | fragment.string = paragraphParentNode->getConcreteProps().text; 40 | fragment.textAttributes = baseTextAttributes; 41 | outAttributedString.appendFragment(std::move(fragment)); 42 | return; 43 | } 44 | // END ADDED 45 | bool lastFragmentWasRawText = false; 46 | for (const auto& childNode : parentNode.getChildren()) { 47 | // RawShadowNode 48 | auto rawTextShadowNode = 49 | dynamic_cast(childNode.get()); 50 | if (rawTextShadowNode != nullptr) { 51 | const auto& rawText = rawTextShadowNode->getConcreteProps().text; 52 | if (lastFragmentWasRawText) { 53 | outAttributedString.getFragments().back().string += rawText; 54 | } else { 55 | auto fragment = AttributedString::Fragment{}; 56 | fragment.string = rawText; 57 | fragment.textAttributes = baseTextAttributes; 58 | 59 | // Storing a retaining pointer to `ParagraphShadowNode` inside 60 | // `attributedString` causes a retain cycle (besides that fact that we 61 | // don't need it at all). Storing a `ShadowView` instance instead of 62 | // `ShadowNode` should properly fix this problem. 63 | fragment.parentShadowView = shadowViewFromShadowNode(parentNode); 64 | outAttributedString.appendFragment(std::move(fragment)); 65 | lastFragmentWasRawText = true; 66 | } 67 | continue; 68 | } 69 | 70 | lastFragmentWasRawText = false; 71 | 72 | // TextShadowNode 73 | auto textShadowNode = dynamic_cast(childNode.get()); 74 | if (textShadowNode != nullptr) { 75 | auto localTextAttributes = baseTextAttributes; 76 | localTextAttributes.apply( 77 | textShadowNode->getConcreteProps().textAttributes); 78 | buildAttributedString( 79 | localTextAttributes, 80 | *textShadowNode, 81 | outAttributedString, 82 | outAttachments); 83 | continue; 84 | } 85 | 86 | // Any *other* kind of ShadowNode 87 | auto fragment = AttributedString::Fragment{}; 88 | fragment.string = AttributedString::Fragment::AttachmentCharacter(); 89 | fragment.parentShadowView = shadowViewFromShadowNode(*childNode); 90 | fragment.textAttributes = baseTextAttributes; 91 | outAttributedString.appendFragment(std::move(fragment)); 92 | outAttachments.push_back(Attachment{ 93 | childNode.get(), outAttributedString.getFragments().size() - 1}); 94 | } 95 | } 96 | 97 | } // namespace facebook::react 98 | -------------------------------------------------------------------------------- /cpp/react/renderer/components/JBAnimatedText/BaseTextShadowNode.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | namespace facebook::react { 14 | 15 | /* 16 | * Base class (one of) for shadow nodes that represents attributed text, 17 | * such as Text and Paragraph (but not RawText). 18 | */ 19 | class CBaseTextShadowNode { // EDITED 20 | public: 21 | /* 22 | * Represents additional information associated with some fragments which 23 | * represent embedded into text component (such as an image or inline view). 24 | */ 25 | class Attachment final { 26 | public: 27 | /* 28 | * Unowning pointer to a `ShadowNode` that represents the attachment. 29 | * Cannot be `null`. 30 | */ 31 | const ShadowNode* shadowNode; 32 | 33 | /* 34 | * Index of the fragment in `AttributedString` that represents the 35 | * the attachment. 36 | */ 37 | size_t fragmentIndex; 38 | }; 39 | 40 | /* 41 | * A list of `Attachment`s. 42 | * Performance-wise, the prevailing case is when there are no attachments 43 | * at all, therefore we don't need an inline buffer (`small_vector`). 44 | */ 45 | using Attachments = std::vector; 46 | 47 | /* 48 | * Builds an `AttributedString` which represents text content of the node. 49 | * This is static so that both Paragraph (which subclasses BaseText) and 50 | * TextInput (which does not) can use this. 51 | * TODO T53299884: decide if this should be moved out and made a static 52 | * function, or if TextInput should inherit from BaseTextShadowNode. 53 | */ 54 | static void buildAttributedString( 55 | const TextAttributes& baseTextAttributes, 56 | const ShadowNode& parentNode, 57 | AttributedString& outAttributedString, 58 | Attachments& outAttachments); 59 | 60 | /** 61 | * Returns a character used to measure empty strings in native platforms. 62 | */ 63 | inline static std::string getEmptyPlaceholder() { 64 | return "I"; 65 | } 66 | }; 67 | 68 | } // namespace facebook::react 69 | -------------------------------------------------------------------------------- /cpp/react/renderer/components/JBAnimatedText/ComponentDescriptors.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom -> used by only by 3 | */ 4 | 5 | #include 6 | -------------------------------------------------------------------------------- /cpp/react/renderer/components/JBAnimatedText/ParagraphComponentDescriptor.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include // EDITED 11 | #include 12 | #include 13 | #include 14 | 15 | namespace facebook::react { 16 | 17 | /* 18 | * Descriptor for component. 19 | */ 20 | class CParagraphComponentDescriptor final // EDITED 21 | : public ConcreteComponentDescriptor { // EDITED 22 | public: 23 | CParagraphComponentDescriptor(const ComponentDescriptorParameters& parameters) // EDITED 24 | : ConcreteComponentDescriptor(parameters) { // EDITED 25 | // Every single `ParagraphShadowNode` will have a reference to 26 | // a shared `TextLayoutManager`. 27 | textLayoutManager_ = std::make_shared(contextContainer_); 28 | } 29 | 30 | protected: 31 | void adopt(ShadowNode& shadowNode) const override { 32 | ConcreteComponentDescriptor::adopt(shadowNode); 33 | 34 | auto& paragraphShadowNode = static_cast(shadowNode); // EDITED 35 | 36 | // `ParagraphShadowNode` uses `TextLayoutManager` to measure text content 37 | // and communicate text rendering metrics to mounting layer. 38 | paragraphShadowNode.setTextLayoutManager(textLayoutManager_); 39 | } 40 | 41 | private: 42 | std::shared_ptr textLayoutManager_; 43 | }; 44 | 45 | } // namespace facebook::react 46 | -------------------------------------------------------------------------------- /cpp/react/renderer/components/JBAnimatedText/ParagraphProps.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #include "ParagraphProps.h" 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | namespace facebook::react { 19 | 20 | CParagraphProps::CParagraphProps( // EDITED 21 | const PropsParserContext& context, 22 | const CParagraphProps& sourceProps, // EDITED 23 | const RawProps& rawProps) 24 | : ViewProps(context, sourceProps, rawProps), 25 | BaseTextProps(context, sourceProps, rawProps), 26 | paragraphAttributes( 27 | ReactNativeFeatureFlags::enableCppPropsIteratorSetter() 28 | ? sourceProps.paragraphAttributes 29 | : convertRawProp( 30 | context, 31 | rawProps, 32 | sourceProps.paragraphAttributes, 33 | {})), 34 | // ADDED 35 | text(convertRawProp(context, rawProps, "text", sourceProps.text, {})), 36 | // END ADDED 37 | isSelectable( 38 | ReactNativeFeatureFlags::enableCppPropsIteratorSetter() 39 | ? sourceProps.isSelectable 40 | : convertRawProp( 41 | context, 42 | rawProps, 43 | "selectable", 44 | sourceProps.isSelectable, 45 | false)), 46 | onTextLayout( 47 | ReactNativeFeatureFlags::enableCppPropsIteratorSetter() 48 | ? sourceProps.onTextLayout 49 | : convertRawProp( 50 | context, 51 | rawProps, 52 | "onTextLayout", 53 | sourceProps.onTextLayout, 54 | {})) { 55 | /* 56 | * These props are applied to `View`, therefore they must not be a part of 57 | * base text attributes. 58 | */ 59 | textAttributes.opacity = std::numeric_limits::quiet_NaN(); 60 | textAttributes.backgroundColor = {}; 61 | }; 62 | 63 | void CParagraphProps::setProp( // EDITED 64 | const PropsParserContext& context, 65 | RawPropsPropNameHash hash, 66 | const char* propName, 67 | const RawValue& value) { 68 | // All Props structs setProp methods must always, unconditionally, 69 | // call all super::setProp methods, since multiple structs may 70 | // reuse the same values. 71 | ViewProps::setProp(context, hash, propName, value); 72 | BaseTextProps::setProp(context, hash, propName, value); 73 | 74 | static auto defaults = CParagraphProps{}; // EDITED 75 | 76 | // ParagraphAttributes has its own switch statement - to keep all 77 | // of these fields together, and because there are some collisions between 78 | // propnames parsed here and outside of ParagraphAttributes. 79 | // This code is also duplicated in AndroidTextInput. 80 | static auto paDefaults = ParagraphAttributes{}; 81 | switch (hash) { 82 | REBUILD_FIELD_SWITCH_CASE( 83 | paDefaults, 84 | value, 85 | paragraphAttributes, 86 | maximumNumberOfLines, 87 | "numberOfLines"); 88 | REBUILD_FIELD_SWITCH_CASE( 89 | paDefaults, value, paragraphAttributes, ellipsizeMode, "ellipsizeMode"); 90 | REBUILD_FIELD_SWITCH_CASE( 91 | paDefaults, 92 | value, 93 | paragraphAttributes, 94 | textBreakStrategy, 95 | "textBreakStrategy"); 96 | REBUILD_FIELD_SWITCH_CASE( 97 | paDefaults, 98 | value, 99 | paragraphAttributes, 100 | adjustsFontSizeToFit, 101 | "adjustsFontSizeToFit"); 102 | REBUILD_FIELD_SWITCH_CASE( 103 | paDefaults, 104 | value, 105 | paragraphAttributes, 106 | minimumFontSize, 107 | "minimumFontSize"); 108 | REBUILD_FIELD_SWITCH_CASE( 109 | paDefaults, 110 | value, 111 | paragraphAttributes, 112 | maximumFontSize, 113 | "maximumFontSize"); 114 | REBUILD_FIELD_SWITCH_CASE( 115 | paDefaults, 116 | value, 117 | paragraphAttributes, 118 | includeFontPadding, 119 | "includeFontPadding"); 120 | REBUILD_FIELD_SWITCH_CASE( 121 | paDefaults, 122 | value, 123 | paragraphAttributes, 124 | android_hyphenationFrequency, 125 | "android_hyphenationFrequency"); 126 | } 127 | 128 | switch (hash) { 129 | RAW_SET_PROP_SWITCH_CASE_BASIC(isSelectable); 130 | RAW_SET_PROP_SWITCH_CASE_BASIC(onTextLayout); 131 | } 132 | 133 | /* 134 | * These props are applied to `View`, therefore they must not be a part of 135 | * base text attributes. 136 | */ 137 | textAttributes.opacity = std::numeric_limits::quiet_NaN(); 138 | textAttributes.backgroundColor = {}; 139 | } 140 | 141 | #pragma mark - DebugStringConvertible 142 | 143 | #if RN_DEBUG_STRING_CONVERTIBLE 144 | SharedDebugStringConvertibleList CParagraphProps::getDebugProps() const { // EDITED 145 | return ViewProps::getDebugProps() + BaseTextProps::getDebugProps() + 146 | paragraphAttributes.getDebugProps() + 147 | SharedDebugStringConvertibleList{ 148 | debugStringConvertibleItem("isSelectable", isSelectable)}; 149 | } 150 | #endif 151 | 152 | } // namespace facebook::react 153 | -------------------------------------------------------------------------------- /cpp/react/renderer/components/JBAnimatedText/ParagraphProps.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace facebook::react { 20 | 21 | /* 22 | * Props of component. 23 | * Most of the props are directly stored in composed `ParagraphAttributes` 24 | * object. 25 | */ 26 | class CParagraphProps : public ViewProps, public BaseTextProps { // EDITED 27 | public: 28 | CParagraphProps() = default; // EDITED 29 | CParagraphProps( // EDITED 30 | const PropsParserContext& context, 31 | const CParagraphProps& sourceProps, // EDITED 32 | const RawProps& rawProps); 33 | 34 | void setProp( 35 | const PropsParserContext& context, 36 | RawPropsPropNameHash hash, 37 | const char* propName, 38 | const RawValue& value); 39 | 40 | #pragma mark - Props 41 | 42 | /* 43 | * Contains all prop values that affect visual representation of the 44 | * paragraph. 45 | */ 46 | ParagraphAttributes paragraphAttributes{}; 47 | // ADDED 48 | std::string text{}; 49 | // END ADDED 50 | 51 | /* 52 | * Defines can the text be selected (and copied) or not. 53 | */ 54 | bool isSelectable{}; 55 | 56 | bool onTextLayout{}; 57 | 58 | #pragma mark - DebugStringConvertible 59 | 60 | #if RN_DEBUG_STRING_CONVERTIBLE 61 | SharedDebugStringConvertibleList getDebugProps() const override; 62 | #endif 63 | }; 64 | 65 | } // namespace facebook::react 66 | -------------------------------------------------------------------------------- /cpp/react/renderer/components/JBAnimatedText/ParagraphShadowNode.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #include "ParagraphShadowNode.h" 9 | 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include // EDITED 21 | 22 | namespace facebook::react { 23 | 24 | using Content = CParagraphShadowNode::Content; // EDITED 25 | 26 | const char JBAnimatedTextComponentName[] = "JBAnimatedText"; // EDITED 27 | 28 | CParagraphShadowNode::CParagraphShadowNode( // EDITED 29 | const ShadowNode& sourceShadowNode, 30 | const ShadowNodeFragment& fragment) 31 | : ConcreteViewShadowNode(sourceShadowNode, fragment) { 32 | auto& sourceParagraphShadowNode = 33 | static_cast(sourceShadowNode); // EDITED 34 | if (!fragment.children && !fragment.props && 35 | sourceParagraphShadowNode.getIsLayoutClean()) { 36 | // This ParagraphShadowNode was cloned but did not change 37 | // in a way that affects its layout. Let's mark it clean 38 | // to stop Yoga from traversing it. 39 | cleanLayout(); 40 | } 41 | } 42 | 43 | const Content& CParagraphShadowNode::getContent( // EDITED 44 | const LayoutContext& layoutContext) const { 45 | if (content_.has_value()) { 46 | return content_.value(); 47 | } 48 | 49 | ensureUnsealed(); 50 | 51 | auto textAttributes = TextAttributes::defaultTextAttributes(); 52 | textAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier; 53 | textAttributes.apply(getConcreteProps().textAttributes); 54 | textAttributes.layoutDirection = 55 | YGNodeLayoutGetDirection(&yogaNode_) == YGDirectionRTL 56 | ? LayoutDirection::RightToLeft 57 | : LayoutDirection::LeftToRight; 58 | auto attributedString = AttributedString{}; 59 | auto attachments = Attachments{}; 60 | buildAttributedString(textAttributes, *this, attributedString, attachments); 61 | attributedString.setBaseTextAttributes(textAttributes); 62 | 63 | content_ = Content{ 64 | attributedString, getConcreteProps().paragraphAttributes, attachments}; 65 | 66 | return content_.value(); 67 | } 68 | 69 | Content CParagraphShadowNode::getContentWithMeasuredAttachments( // EDITED 70 | const LayoutContext& layoutContext, 71 | const LayoutConstraints& layoutConstraints) const { 72 | auto content = getContent(layoutContext); 73 | 74 | if (content.attachments.empty()) { 75 | // Base case: No attachments, nothing to do. 76 | return content; 77 | } 78 | 79 | auto localLayoutConstraints = layoutConstraints; 80 | // Having enforced minimum size for text fragments doesn't make much sense. 81 | localLayoutConstraints.minimumSize = Size{0, 0}; 82 | 83 | auto& fragments = content.attributedString.getFragments(); 84 | 85 | for (const auto& attachment : content.attachments) { 86 | auto laytableShadowNode = 87 | dynamic_cast(attachment.shadowNode); 88 | 89 | if (laytableShadowNode == nullptr) { 90 | continue; 91 | } 92 | 93 | auto size = 94 | laytableShadowNode->measure(layoutContext, localLayoutConstraints); 95 | 96 | // Rounding to *next* value on the pixel grid. 97 | size.width += 0.01f; 98 | size.height += 0.01f; 99 | size = roundToPixel<&ceil>(size, layoutContext.pointScaleFactor); 100 | 101 | auto fragmentLayoutMetrics = LayoutMetrics{}; 102 | fragmentLayoutMetrics.pointScaleFactor = layoutContext.pointScaleFactor; 103 | fragmentLayoutMetrics.frame.size = size; 104 | fragments[attachment.fragmentIndex].parentShadowView.layoutMetrics = 105 | fragmentLayoutMetrics; 106 | } 107 | 108 | return content; 109 | } 110 | 111 | void CParagraphShadowNode::setTextLayoutManager( // EDITED 112 | std::shared_ptr textLayoutManager) { 113 | ensureUnsealed(); 114 | textLayoutManager_ = std::move(textLayoutManager); 115 | } 116 | 117 | void CParagraphShadowNode::updateStateIfNeeded(const Content& content) { // EDITED 118 | ensureUnsealed(); 119 | 120 | auto& state = getStateData(); 121 | 122 | react_native_assert(textLayoutManager_); 123 | 124 | if (state.attributedString == content.attributedString) { 125 | return; 126 | } 127 | 128 | setStateData(ParagraphState{ 129 | content.attributedString, 130 | content.paragraphAttributes, 131 | textLayoutManager_}); 132 | } 133 | 134 | #pragma mark - LayoutableShadowNode 135 | 136 | Size CParagraphShadowNode::measureContent( // EDITED 137 | const LayoutContext& layoutContext, 138 | const LayoutConstraints& layoutConstraints) const { 139 | auto content = 140 | getContentWithMeasuredAttachments(layoutContext, layoutConstraints); 141 | 142 | auto attributedString = content.attributedString; 143 | if (attributedString.isEmpty()) { 144 | // Note: `zero-width space` is insufficient in some cases (e.g. when we need 145 | // to measure the "height" of the font). 146 | // TODO T67606511: We will redefine the measurement of empty strings as part 147 | // of T67606511 148 | auto string = CBaseTextShadowNode::getEmptyPlaceholder(); // EDITED 149 | auto textAttributes = TextAttributes::defaultTextAttributes(); 150 | textAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier; 151 | textAttributes.apply(getConcreteProps().textAttributes); 152 | attributedString.appendFragment({string, textAttributes, {}}); 153 | } 154 | 155 | TextLayoutContext textLayoutContext{}; 156 | textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor; 157 | return textLayoutManager_ 158 | ->measure( 159 | AttributedStringBox{attributedString}, 160 | content.paragraphAttributes, 161 | textLayoutContext, 162 | layoutConstraints) 163 | .size; 164 | } 165 | 166 | Float CParagraphShadowNode::baseline( // EDITED 167 | const LayoutContext& layoutContext, 168 | Size size) const { 169 | auto layoutMetrics = getLayoutMetrics(); 170 | auto layoutConstraints = 171 | LayoutConstraints{size, size, layoutMetrics.layoutDirection}; 172 | auto content = 173 | getContentWithMeasuredAttachments(layoutContext, layoutConstraints); 174 | auto attributedString = content.attributedString; 175 | 176 | if (attributedString.isEmpty()) { 177 | // Note: `zero-width space` is insufficient in some cases (e.g. when we need 178 | // to measure the "height" of the font). 179 | // TODO T67606511: We will redefine the measurement of empty strings as part 180 | // of T67606511 181 | auto string = CBaseTextShadowNode::getEmptyPlaceholder(); // EDITED 182 | auto textAttributes = TextAttributes::defaultTextAttributes(); 183 | textAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier; 184 | textAttributes.apply(getConcreteProps().textAttributes); 185 | attributedString.appendFragment({string, textAttributes, {}}); 186 | } 187 | 188 | AttributedStringBox attributedStringBox{attributedString}; 189 | return textLayoutManager_->baseline( 190 | attributedStringBox, getConcreteProps().paragraphAttributes, size); 191 | } 192 | 193 | void CParagraphShadowNode::layout(LayoutContext layoutContext) { // EDITED 194 | ensureUnsealed(); 195 | 196 | auto layoutMetrics = getLayoutMetrics(); 197 | auto size = layoutMetrics.getContentFrame().size; 198 | 199 | auto layoutConstraints = 200 | LayoutConstraints{size, size, layoutMetrics.layoutDirection}; 201 | auto content = 202 | getContentWithMeasuredAttachments(layoutContext, layoutConstraints); 203 | 204 | updateStateIfNeeded(content); 205 | 206 | TextLayoutContext textLayoutContext{}; 207 | textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor; 208 | auto measurement = TextMeasurement{}; 209 | 210 | AttributedStringBox attributedStringBox{content.attributedString}; 211 | 212 | if (getConcreteProps().onTextLayout) { 213 | auto linesMeasurements = textLayoutManager_->measureLines( 214 | attributedStringBox, content.paragraphAttributes, size); 215 | getConcreteEventEmitter().onTextLayout(linesMeasurements); 216 | } 217 | 218 | if (content.attachments.empty()) { 219 | // No attachments to layout. 220 | return; 221 | } 222 | 223 | // Only measure if attachments are not empty. 224 | measurement = textLayoutManager_->measure( 225 | attributedStringBox, 226 | content.paragraphAttributes, 227 | textLayoutContext, 228 | layoutConstraints); 229 | 230 | // Iterating on attachments, we clone shadow nodes and moving 231 | // `paragraphShadowNode` that represents clones of `this` object. 232 | auto paragraphShadowNode = static_cast(this); // EDITED 233 | // `paragraphOwningShadowNode` is owning pointer to`paragraphShadowNode` 234 | // (besides the initial case when `paragraphShadowNode == this`), we need this 235 | // only to keep it in memory for a while. 236 | auto paragraphOwningShadowNode = ShadowNode::Unshared{}; 237 | 238 | react_native_assert( 239 | content.attachments.size() == measurement.attachments.size()); 240 | 241 | for (size_t i = 0; i < content.attachments.size(); i++) { 242 | auto& attachment = content.attachments.at(i); 243 | 244 | if (dynamic_cast(attachment.shadowNode) == 245 | nullptr) { 246 | // Not a layoutable `ShadowNode`, no need to lay it out. 247 | continue; 248 | } 249 | 250 | auto clonedShadowNode = ShadowNode::Unshared{}; 251 | 252 | paragraphOwningShadowNode = paragraphShadowNode->cloneTree( 253 | attachment.shadowNode->getFamily(), 254 | [&](const ShadowNode& oldShadowNode) { 255 | clonedShadowNode = oldShadowNode.clone({}); 256 | return clonedShadowNode; 257 | }); 258 | paragraphShadowNode = 259 | static_cast(paragraphOwningShadowNode.get()); // EDITED 260 | 261 | auto& layoutableShadowNode = 262 | dynamic_cast(*clonedShadowNode); 263 | 264 | auto attachmentFrame = measurement.attachments[i].frame; 265 | attachmentFrame.origin.x += layoutMetrics.contentInsets.left; 266 | attachmentFrame.origin.y += layoutMetrics.contentInsets.top; 267 | 268 | auto attachmentSize = roundToPixel<&ceil>( 269 | attachmentFrame.size, layoutMetrics.pointScaleFactor); 270 | auto attachmentOrigin = roundToPixel<&round>( 271 | attachmentFrame.origin, layoutMetrics.pointScaleFactor); 272 | auto attachmentLayoutContext = layoutContext; 273 | auto attachmentLayoutConstrains = LayoutConstraints{ 274 | attachmentSize, attachmentSize, layoutConstraints.layoutDirection}; 275 | 276 | // Laying out the `ShadowNode` and the subtree starting from it. 277 | layoutableShadowNode.layoutTree( 278 | attachmentLayoutContext, attachmentLayoutConstrains); 279 | 280 | // Altering the origin of the `ShadowNode` (which is defined by text layout, 281 | // not by internal styles and state). 282 | auto attachmentLayoutMetrics = layoutableShadowNode.getLayoutMetrics(); 283 | attachmentLayoutMetrics.frame.origin = attachmentOrigin; 284 | layoutableShadowNode.setLayoutMetrics(attachmentLayoutMetrics); 285 | } 286 | 287 | // If we ended up cloning something, we need to update the list of children to 288 | // reflect the changes that we made. 289 | if (paragraphShadowNode != this) { 290 | this->children_ = 291 | static_cast(paragraphShadowNode)->children_; // EDITED 292 | } 293 | } 294 | 295 | } // namespace facebook::react 296 | -------------------------------------------------------------------------------- /cpp/react/renderer/components/JBAnimatedText/ParagraphShadowNode.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include // EDITED 11 | #include 12 | #include // EDITED 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace facebook::react { 20 | 21 | extern const char JBAnimatedTextComponentName[]; // EDITED 22 | 23 | /* 24 | * `ShadowNode` for component, represents -like component 25 | * containing and displaying text. Text content is represented as nested 26 | * and components. 27 | */ 28 | class CParagraphShadowNode final : public ConcreteViewShadowNode< // EDITED 29 | JBAnimatedTextComponentName, // EDITED 30 | CParagraphProps, // EDITED 31 | ParagraphEventEmitter, 32 | ParagraphState, 33 | /* usesMapBufferForStateData */ true>, 34 | public CBaseTextShadowNode { // EDITED 35 | public: 36 | using ConcreteViewShadowNode::ConcreteViewShadowNode; 37 | 38 | CParagraphShadowNode( // EDITED 39 | const ShadowNode& sourceShadowNode, 40 | const ShadowNodeFragment& fragment); 41 | 42 | static ShadowNodeTraits BaseTraits() { 43 | auto traits = ConcreteViewShadowNode::BaseTraits(); 44 | traits.set(ShadowNodeTraits::Trait::LeafYogaNode); 45 | traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode); 46 | traits.set(ShadowNodeTraits::Trait::BaselineYogaNode); 47 | 48 | #ifdef ANDROID 49 | // Unsetting `FormsStackingContext` trait is essential on Android where we 50 | // can't mount views inside `TextView`. 51 | traits.unset(ShadowNodeTraits::Trait::FormsStackingContext); 52 | #endif 53 | 54 | return traits; 55 | } 56 | 57 | /* 58 | * Associates a shared TextLayoutManager with the node. 59 | * `ParagraphShadowNode` uses the manager to measure text content 60 | * and construct `ParagraphState` objects. 61 | */ 62 | void setTextLayoutManager( 63 | std::shared_ptr textLayoutManager); 64 | 65 | #pragma mark - LayoutableShadowNode 66 | 67 | void layout(LayoutContext layoutContext) override; 68 | 69 | Size measureContent( 70 | const LayoutContext& layoutContext, 71 | const LayoutConstraints& layoutConstraints) const override; 72 | 73 | Float baseline(const LayoutContext& layoutContext, Size size) const override; 74 | 75 | /* 76 | * Internal representation of the nested content of the node in a format 77 | * suitable for future processing. 78 | */ 79 | class Content final { 80 | public: 81 | AttributedString attributedString; 82 | ParagraphAttributes paragraphAttributes; 83 | Attachments attachments; 84 | }; 85 | 86 | private: 87 | /* 88 | * Builds (if needed) and returns a reference to a `Content` object. 89 | */ 90 | const Content& getContent(const LayoutContext& layoutContext) const; 91 | 92 | /* 93 | * Builds and returns a `Content` object with given `layoutConstraints`. 94 | */ 95 | Content getContentWithMeasuredAttachments( 96 | const LayoutContext& layoutContext, 97 | const LayoutConstraints& layoutConstraints) const; 98 | 99 | /* 100 | * Creates a `State` object (with `AttributedText` and 101 | * `TextLayoutManager`) if needed. 102 | */ 103 | void updateStateIfNeeded(const Content& content); 104 | 105 | std::shared_ptr textLayoutManager_; 106 | 107 | /* 108 | * Cached content of the subtree started from the node. 109 | */ 110 | mutable std::optional content_{}; 111 | }; 112 | 113 | } // namespace facebook::react 114 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | 37 | 38 | #example native directories 39 | /android/* 40 | /ios/* 41 | 42 | 43 | # Yarn 44 | .yarn/* 45 | !.yarn/patches 46 | !.yarn/plugins 47 | !.yarn/releases 48 | !.yarn/sdks 49 | !.yarn/versions 50 | 51 | /.yalc -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import AnimateableText from 'react-native-animateable-text'; 4 | import { useAnimatedProps, useSharedValue } from 'react-native-reanimated'; 5 | import { Slider } from '@miblanchard/react-native-slider'; 6 | 7 | const style = { 8 | fontSize: 30, 9 | fontWeight: 'bold' as const, 10 | color: '#7832C7' as const, 11 | paddingTop: 0, 12 | paddingBottom: 0, 13 | }; 14 | 15 | const styles = StyleSheet.create({ 16 | container: { 17 | flex: 1, 18 | alignItems: 'center', 19 | justifyContent: 'center', 20 | }, 21 | row: { flexDirection: 'row', paddingLeft: 20, paddingRight: 20 }, 22 | }); 23 | 24 | export default function App() { 25 | React.useEffect(() => { 26 | // checking if the app is in new arch or not 27 | // @ts-ignore 28 | const uiManager = global?.nativeFabricUIManager ? 'Fabric' : 'Paper'; 29 | console.log(`Using ${uiManager}`); 30 | }, []); 31 | 32 | const stringSharedValue = useSharedValue('1'); 33 | 34 | const animatedProps = useAnimatedProps(() => { 35 | return { 36 | text: stringSharedValue.value, 37 | }; 38 | }); 39 | 40 | return ( 41 | 42 | { 53 | stringSharedValue.value = `${value}`; 54 | }} 55 | /> 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "react-native-animateable-text-example", 4 | "slug": "animateable-text-example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "ios": { 15 | "supportsTablet": true, 16 | "bundleIdentifier": "com.animateable.text.example" 17 | }, 18 | "android": { 19 | "adaptiveIcon": { 20 | "foregroundImage": "./assets/adaptive-icon.png", 21 | "backgroundColor": "#ffffff" 22 | }, 23 | "package": "com.animateable.text.example" 24 | }, 25 | "web": { 26 | "favicon": "./assets/favicon.png" 27 | }, 28 | "plugins": [ 29 | [ 30 | "expo-build-properties", 31 | { 32 | "ios": { 33 | "newArchEnabled": true 34 | }, 35 | "android": { 36 | "newArchEnabled": true 37 | } 38 | } 39 | ] 40 | ], 41 | "install": { 42 | "exclude": [ 43 | "react-native@~0.74.5" 44 | ] 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axelra-ag/react-native-animateable-text/6c565a74e91d2cbefc2484ad0b057a32cd7024c6/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axelra-ag/react-native-animateable-text/6c565a74e91d2cbefc2484ad0b057a32cd7024c6/example/assets/favicon.png -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axelra-ag/react-native-animateable-text/6c565a74e91d2cbefc2484ad0b057a32cd7024c6/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axelra-ag/react-native-animateable-text/6c565a74e91d2cbefc2484ad0b057a32cd7024c6/example/assets/splash.png -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require('expo/metro-config'); 2 | const path = require('path'); 3 | 4 | const config = getDefaultConfig(__dirname); 5 | 6 | config.resolver.blockList = [ 7 | ...Array.from(config.resolver.blockList ?? []), 8 | new RegExp(path.resolve('..', 'node_modules', 'react-native')), 9 | ]; 10 | 11 | config.resolver.nodeModulesPaths = [ 12 | path.resolve(__dirname, './node_modules'), 13 | path.resolve(__dirname, '../node_modules'), 14 | ]; 15 | 16 | config.watchFolders = [path.resolve(__dirname, '..')]; 17 | 18 | // this is causing me some issues after build (white screen) 19 | // config.transformer.getTransformOptions = async () => ({ 20 | // transform: { 21 | // experimentalImportSupport: false, 22 | // inlineRequires: true, 23 | // }, 24 | // }); 25 | 26 | module.exports = config; -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-animateable-text-example", 3 | "version": "1.0.0", 4 | "main": "expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo run:android", 8 | "prebuild": "cd ../ && yalc publish && cd example && expo prebuild --clean && yarn", 9 | "ios": "expo run:ios", 10 | "web": "expo start --web", 11 | "generatecpp": "node node_modules/react-native/scripts/generate-codegen-artifacts.js -p ../ -o ../gcpp -t all" 12 | }, 13 | "dependencies": { 14 | "@expo/metro-runtime": "~4.0.0-preview.0", 15 | "@miblanchard/react-native-slider": "^2.6.0", 16 | "expo": "52.0.0-preview.3", 17 | "expo-build-properties": "~0.13.1", 18 | "expo-dev-client": "~5.0.0-preview.1", 19 | "expo-status-bar": "~2.0.0", 20 | "react": "18.3.1", 21 | "react-dom": "18.3.1", 22 | "react-native": "0.76.0", 23 | "react-native-animateable-text": "file:.yalc/react-native-animateable-text", 24 | "react-native-reanimated": "~3.16.1", 25 | "react-native-web": "~0.19.10" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.20.0", 29 | "@types/react": "~18.2.45", 30 | "typescript": "~5.3.3", 31 | "yalc": "^1.0.0-pre.53" 32 | }, 33 | "private": true 34 | } 35 | -------------------------------------------------------------------------------- /example/react-native.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | // linking the package to the example 5 | module.exports = { 6 | dependencies: { 7 | [pak.name]: { 8 | root: path.join(__dirname, '..'), 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "react-native-animateable-text": ["../src/index"], 7 | "react-native-animateable-text/*": ["../src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/yalc.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v1", 3 | "packages": { 4 | "react-native-animateable-text": { 5 | "signature": "959eaedf8cc6ead0a4d9bac9f35f6aa9", 6 | "file": true 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /ios/JBAnimatedTextComponenetView.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom 3 | */ 4 | 5 | #ifdef RCT_NEW_ARCH_ENABLED 6 | #import 7 | #import 8 | #import 9 | 10 | #ifndef JBAnimatedTextFabricViewNativeComponent_h 11 | #define JBAnimatedTextFabricViewNativeComponent_h 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface JBAnimatedText : RCTParagraphComponentView 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | 21 | #endif /* JBAnimatedTextFabricViewNativeComponent_h */ 22 | #endif /* RCT_NEW_ARCH_ENABLED */ 23 | -------------------------------------------------------------------------------- /ios/JBAnimatedTextComponenetView.mm: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom 3 | */ 4 | 5 | #ifdef RCT_NEW_ARCH_ENABLED 6 | 7 | #import "JBAnimatedTextComponenetView.h" 8 | 9 | #import 10 | #import 11 | 12 | #import "RCTFabricComponentsPlugins.h" 13 | 14 | using namespace facebook::react; 15 | 16 | @implementation JBAnimatedText 17 | 18 | + (ComponentDescriptorProvider)componentDescriptorProvider 19 | { 20 | return concreteComponentDescriptorProvider(); 21 | } 22 | 23 | Class JBAnimatedTextCls(void) 24 | { 25 | return JBAnimatedText.class; 26 | } 27 | 28 | @end 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /ios/JBAnimatedTextManager.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom 3 | */ 4 | 5 | #import 6 | 7 | 8 | @interface JBAnimatedTextManager : RCTBaseTextViewManager 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /ios/JBAnimatedTextManager.mm: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | 10 | #import 11 | #import 12 | #import 13 | #import 14 | #import 15 | 16 | #import 17 | #import 18 | 19 | // ADDED 20 | #import "JBTextShadowView.h" 21 | #import "JBAnimatedTextManager.h" 22 | // END ADDED 23 | 24 | @interface JBAnimatedTextManager () 25 | 26 | @end 27 | 28 | @implementation JBAnimatedTextManager { // EDITED 29 | NSHashTable *_shadowViews; 30 | } 31 | 32 | RCT_EXPORT_MODULE(JBAnimatedText) // EDITED 33 | 34 | RCT_REMAP_SHADOW_PROPERTY(numberOfLines, maximumNumberOfLines, NSInteger) 35 | RCT_REMAP_SHADOW_PROPERTY(ellipsizeMode, lineBreakMode, NSLineBreakMode) 36 | RCT_REMAP_SHADOW_PROPERTY(adjustsFontSizeToFit, adjustsFontSizeToFit, BOOL) 37 | RCT_REMAP_SHADOW_PROPERTY(minimumFontScale, minimumFontScale, CGFloat) 38 | 39 | // ADDED 40 | RCT_REMAP_SHADOW_PROPERTY(text, text, NSString) 41 | // END ADDED 42 | 43 | RCT_EXPORT_SHADOW_PROPERTY(onTextLayout, RCTDirectEventBlock) 44 | 45 | RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL) 46 | 47 | - (void)setBridge:(RCTBridge *)bridge 48 | { 49 | [super setBridge:bridge]; 50 | _shadowViews = [NSHashTable weakObjectsHashTable]; 51 | 52 | [bridge.uiManager.observerCoordinator addObserver:self]; 53 | 54 | [[NSNotificationCenter defaultCenter] addObserver:self 55 | selector:@selector(handleDidUpdateMultiplierNotification) 56 | name:@"RCTAccessibilityManagerDidUpdateMultiplierNotification" 57 | object:[bridge moduleForName:@"AccessibilityManager" 58 | lazilyLoadIfNecessary:YES]]; 59 | } 60 | 61 | - (UIView *)view 62 | { 63 | return [RCTTextView new]; 64 | } 65 | 66 | - (RCTShadowView *)shadowView 67 | { 68 | // ADDED - REPLACED 69 | RCTTextShadowView *shadowView = [[JBTextShadowView alloc] initWithBridge:self.bridge]; 70 | // END ADDED - REPLACED 71 | shadowView.textAttributes.fontSizeMultiplier = 72 | [[[self.bridge moduleForName:@"AccessibilityManager"] valueForKey:@"multiplier"] floatValue]; 73 | [_shadowViews addObject:shadowView]; 74 | return shadowView; 75 | } 76 | 77 | #pragma mark - RCTUIManagerObserver 78 | 79 | - (void)uiManagerWillPerformMounting:(__unused RCTUIManager *)uiManager 80 | { 81 | for (RCTTextShadowView *shadowView in _shadowViews) { 82 | [shadowView uiManagerWillPerformMounting]; 83 | } 84 | } 85 | 86 | #pragma mark - Font Size Multiplier 87 | 88 | - (void)handleDidUpdateMultiplierNotification 89 | { 90 | CGFloat fontSizeMultiplier = 91 | [[[self.bridge moduleForName:@"AccessibilityManager"] valueForKey:@"multiplier"] floatValue]; 92 | 93 | NSHashTable *shadowViews = _shadowViews; 94 | RCTExecuteOnUIManagerQueue(^{ 95 | for (RCTTextShadowView *shadowView in shadowViews) { 96 | shadowView.textAttributes.fontSizeMultiplier = fontSizeMultiplier; 97 | [shadowView dirtyLayout]; 98 | } 99 | 100 | [self.bridge.uiManager setNeedsLayout]; 101 | }); 102 | } 103 | 104 | @end 105 | -------------------------------------------------------------------------------- /ios/JBTextShadowView.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom 3 | */ 4 | 5 | 6 | #import 7 | 8 | NS_ASSUME_NONNULL_BEGIN 9 | 10 | @interface JBTextShadowView : RCTTextShadowView 11 | 12 | @property (nonatomic, copy) NSString* text; 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | 18 | -------------------------------------------------------------------------------- /ios/JBTextShadowView.mm: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom - attributedTextWithMeasuredAttachmentsThatFitSize (forked) 3 | */ 4 | 5 | #import "JBTextShadowView.h" 6 | 7 | #import 8 | #import 9 | #import 10 | #import 11 | 12 | #import 13 | #import 14 | 15 | 16 | #import "JBTextShadowView.h" 17 | 18 | @implementation JBTextShadowView 19 | 20 | - (NSAttributedString *)attributedTextWithMeasuredAttachmentsThatFitSize:(CGSize)size 21 | { 22 | static UIImage *placeholderImage; 23 | static dispatch_once_t onceToken; 24 | dispatch_once(&onceToken, ^{ 25 | placeholderImage = [UIImage new]; 26 | }); 27 | 28 | NSMutableAttributedString *attributedText = 29 | [[NSMutableAttributedString alloc] initWithAttributedString:[self attributedTextWithBaseTextAttributes:nil]]; 30 | 31 | // EDITED 32 | if (self.text.length) { 33 | NSAttributedString *propertyAttributedText = 34 | [[NSAttributedString alloc] initWithString:self.text attributes:self.textAttributes.effectiveTextAttributes]; 35 | [attributedText insertAttributedString:propertyAttributedText atIndex:0]; 36 | } 37 | // END EDITED 38 | 39 | [attributedText beginEditing]; 40 | 41 | [attributedText enumerateAttribute:RCTBaseTextShadowViewEmbeddedShadowViewAttributeName 42 | inRange:NSMakeRange(0, attributedText.length) 43 | options:0 44 | usingBlock:^(RCTShadowView *shadowView, NSRange range, __unused BOOL *stop) { 45 | if (!shadowView) { 46 | return; 47 | } 48 | 49 | CGSize fittingSize = [shadowView sizeThatFitsMinimumSize:CGSizeZero maximumSize:size]; 50 | NSTextAttachment *attachment = [NSTextAttachment new]; 51 | attachment.bounds = (CGRect){CGPointZero, fittingSize}; 52 | attachment.image = placeholderImage; 53 | [attributedText addAttribute:NSAttachmentAttributeName value:attachment range:range]; 54 | }]; 55 | 56 | [attributedText endEditing]; 57 | 58 | return [attributedText copy]; 59 | } 60 | 61 | @end 62 | 63 | 64 | -------------------------------------------------------------------------------- /ios/ReanimatedText.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 11 | 12 | 5E555C0D2413F4C50049A1A2 /* ReanimatedText.mm in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* ReanimatedText.mm */; }; 13 | /* End PBXBuildFile section */ 14 | 15 | /* Begin PBXCopyFilesBuildPhase section */ 16 | 58B511D91A9E6C8500147676 /* CopyFiles */ = { 17 | isa = PBXCopyFilesBuildPhase; 18 | buildActionMask = 2147483647; 19 | dstPath = "include/$(PRODUCT_NAME)"; 20 | dstSubfolderSpec = 16; 21 | files = ( 22 | ); 23 | runOnlyForDeploymentPostprocessing = 0; 24 | }; 25 | /* End PBXCopyFilesBuildPhase section */ 26 | 27 | /* Begin PBXFileReference section */ 28 | 134814201AA4EA6300B7C361 /* libReanimatedText.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReanimatedText.a; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | 30 | 31 | B3E7B5881CC2AC0600A0062D /* ReanimatedText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReanimatedText.h; sourceTree = ""; }; 32 | B3E7B5891CC2AC0600A0062D /* ReanimatedText.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ReanimatedText.mm; sourceTree = ""; }; 33 | 34 | /* End PBXFileReference section */ 35 | 36 | /* Begin PBXFrameworksBuildPhase section */ 37 | 58B511D81A9E6C8500147676 /* Frameworks */ = { 38 | isa = PBXFrameworksBuildPhase; 39 | buildActionMask = 2147483647; 40 | files = ( 41 | ); 42 | runOnlyForDeploymentPostprocessing = 0; 43 | }; 44 | /* End PBXFrameworksBuildPhase section */ 45 | 46 | /* Begin PBXGroup section */ 47 | 134814211AA4EA7D00B7C361 /* Products */ = { 48 | isa = PBXGroup; 49 | children = ( 50 | 134814201AA4EA6300B7C361 /* libReanimatedText.a */, 51 | ); 52 | name = Products; 53 | sourceTree = ""; 54 | }; 55 | 58B511D21A9E6C8500147676 = { 56 | isa = PBXGroup; 57 | children = ( 58 | 59 | 60 | B3E7B5881CC2AC0600A0062D /* ReanimatedText.h */, 61 | B3E7B5891CC2AC0600A0062D /* ReanimatedText.mm */, 62 | 63 | 134814211AA4EA7D00B7C361 /* Products */, 64 | ); 65 | sourceTree = ""; 66 | }; 67 | /* End PBXGroup section */ 68 | 69 | /* Begin PBXNativeTarget section */ 70 | 58B511DA1A9E6C8500147676 /* ReanimatedText */ = { 71 | isa = PBXNativeTarget; 72 | buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ReanimatedText" */; 73 | buildPhases = ( 74 | 58B511D71A9E6C8500147676 /* Sources */, 75 | 58B511D81A9E6C8500147676 /* Frameworks */, 76 | 58B511D91A9E6C8500147676 /* CopyFiles */, 77 | ); 78 | buildRules = ( 79 | ); 80 | dependencies = ( 81 | ); 82 | name = ReanimatedText; 83 | productName = RCTDataManager; 84 | productReference = 134814201AA4EA6300B7C361 /* libReanimatedText.a */; 85 | productType = "com.apple.product-type.library.static"; 86 | }; 87 | /* End PBXNativeTarget section */ 88 | 89 | /* Begin PBXProject section */ 90 | 58B511D31A9E6C8500147676 /* Project object */ = { 91 | isa = PBXProject; 92 | attributes = { 93 | LastUpgradeCheck = 0920; 94 | ORGANIZATIONNAME = Facebook; 95 | TargetAttributes = { 96 | 58B511DA1A9E6C8500147676 = { 97 | CreatedOnToolsVersion = 6.1.1; 98 | }; 99 | }; 100 | }; 101 | buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ReanimatedText" */; 102 | compatibilityVersion = "Xcode 3.2"; 103 | developmentRegion = English; 104 | hasScannedForEncodings = 0; 105 | knownRegions = ( 106 | English, 107 | en, 108 | ); 109 | mainGroup = 58B511D21A9E6C8500147676; 110 | productRefGroup = 58B511D21A9E6C8500147676; 111 | projectDirPath = ""; 112 | projectRoot = ""; 113 | targets = ( 114 | 58B511DA1A9E6C8500147676 /* ReanimatedText */, 115 | ); 116 | }; 117 | /* End PBXProject section */ 118 | 119 | /* Begin PBXSourcesBuildPhase section */ 120 | 58B511D71A9E6C8500147676 /* Sources */ = { 121 | isa = PBXSourcesBuildPhase; 122 | buildActionMask = 2147483647; 123 | files = ( 124 | 125 | 126 | 5E555C0D2413F4C50049A1A2 /* ReanimatedText.mm in Sources */, 127 | 128 | ); 129 | runOnlyForDeploymentPostprocessing = 0; 130 | }; 131 | /* End PBXSourcesBuildPhase section */ 132 | 133 | /* Begin XCBuildConfiguration section */ 134 | 58B511ED1A9E6C8500147676 /* Debug */ = { 135 | isa = XCBuildConfiguration; 136 | buildSettings = { 137 | ALWAYS_SEARCH_USER_PATHS = NO; 138 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 139 | CLANG_CXX_LIBRARY = "libc++"; 140 | CLANG_ENABLE_MODULES = YES; 141 | CLANG_ENABLE_OBJC_ARC = YES; 142 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 143 | CLANG_WARN_BOOL_CONVERSION = YES; 144 | CLANG_WARN_COMMA = YES; 145 | CLANG_WARN_CONSTANT_CONVERSION = YES; 146 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 147 | CLANG_WARN_EMPTY_BODY = YES; 148 | CLANG_WARN_ENUM_CONVERSION = YES; 149 | CLANG_WARN_INFINITE_RECURSION = YES; 150 | CLANG_WARN_INT_CONVERSION = YES; 151 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 152 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 153 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 154 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 155 | CLANG_WARN_STRICT_PROTOTYPES = YES; 156 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 157 | CLANG_WARN_UNREACHABLE_CODE = YES; 158 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 159 | COPY_PHASE_STRIP = NO; 160 | ENABLE_STRICT_OBJC_MSGSEND = YES; 161 | ENABLE_TESTABILITY = YES; 162 | GCC_C_LANGUAGE_STANDARD = gnu99; 163 | GCC_DYNAMIC_NO_PIC = NO; 164 | GCC_NO_COMMON_BLOCKS = YES; 165 | GCC_OPTIMIZATION_LEVEL = 0; 166 | GCC_PREPROCESSOR_DEFINITIONS = ( 167 | "DEBUG=1", 168 | "$(inherited)", 169 | ); 170 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 171 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 172 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 173 | GCC_WARN_UNDECLARED_SELECTOR = YES; 174 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 175 | GCC_WARN_UNUSED_FUNCTION = YES; 176 | GCC_WARN_UNUSED_VARIABLE = YES; 177 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 178 | MTL_ENABLE_DEBUG_INFO = YES; 179 | ONLY_ACTIVE_ARCH = YES; 180 | SDKROOT = iphoneos; 181 | }; 182 | name = Debug; 183 | }; 184 | 58B511EE1A9E6C8500147676 /* Release */ = { 185 | isa = XCBuildConfiguration; 186 | buildSettings = { 187 | ALWAYS_SEARCH_USER_PATHS = NO; 188 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 189 | CLANG_CXX_LIBRARY = "libc++"; 190 | CLANG_ENABLE_MODULES = YES; 191 | CLANG_ENABLE_OBJC_ARC = YES; 192 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 193 | CLANG_WARN_BOOL_CONVERSION = YES; 194 | CLANG_WARN_COMMA = YES; 195 | CLANG_WARN_CONSTANT_CONVERSION = YES; 196 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 197 | CLANG_WARN_EMPTY_BODY = YES; 198 | CLANG_WARN_ENUM_CONVERSION = YES; 199 | CLANG_WARN_INFINITE_RECURSION = YES; 200 | CLANG_WARN_INT_CONVERSION = YES; 201 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 202 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 203 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 204 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 205 | CLANG_WARN_STRICT_PROTOTYPES = YES; 206 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 207 | CLANG_WARN_UNREACHABLE_CODE = YES; 208 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 209 | COPY_PHASE_STRIP = YES; 210 | ENABLE_NS_ASSERTIONS = NO; 211 | ENABLE_STRICT_OBJC_MSGSEND = YES; 212 | GCC_C_LANGUAGE_STANDARD = gnu99; 213 | GCC_NO_COMMON_BLOCKS = YES; 214 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 215 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 216 | GCC_WARN_UNDECLARED_SELECTOR = YES; 217 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 218 | GCC_WARN_UNUSED_FUNCTION = YES; 219 | GCC_WARN_UNUSED_VARIABLE = YES; 220 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 221 | MTL_ENABLE_DEBUG_INFO = NO; 222 | SDKROOT = iphoneos; 223 | VALIDATE_PRODUCT = YES; 224 | }; 225 | name = Release; 226 | }; 227 | 58B511F01A9E6C8500147676 /* Debug */ = { 228 | isa = XCBuildConfiguration; 229 | buildSettings = { 230 | HEADER_SEARCH_PATHS = ( 231 | "$(inherited)", 232 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 233 | "$(SRCROOT)/../../../React/**", 234 | "$(SRCROOT)/../../react-native/React/**", 235 | ); 236 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 237 | OTHER_LDFLAGS = "-ObjC"; 238 | PRODUCT_NAME = ReanimatedText; 239 | SKIP_INSTALL = YES; 240 | 241 | }; 242 | name = Debug; 243 | }; 244 | 58B511F11A9E6C8500147676 /* Release */ = { 245 | isa = XCBuildConfiguration; 246 | buildSettings = { 247 | HEADER_SEARCH_PATHS = ( 248 | "$(inherited)", 249 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 250 | "$(SRCROOT)/../../../React/**", 251 | "$(SRCROOT)/../../react-native/React/**", 252 | ); 253 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 254 | OTHER_LDFLAGS = "-ObjC"; 255 | PRODUCT_NAME = ReanimatedText; 256 | SKIP_INSTALL = YES; 257 | 258 | }; 259 | name = Release; 260 | }; 261 | /* End XCBuildConfiguration section */ 262 | 263 | /* Begin XCConfigurationList section */ 264 | 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ReanimatedText" */ = { 265 | isa = XCConfigurationList; 266 | buildConfigurations = ( 267 | 58B511ED1A9E6C8500147676 /* Debug */, 268 | 58B511EE1A9E6C8500147676 /* Release */, 269 | ); 270 | defaultConfigurationIsVisible = 0; 271 | defaultConfigurationName = Release; 272 | }; 273 | 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ReanimatedText" */ = { 274 | isa = XCConfigurationList; 275 | buildConfigurations = ( 276 | 58B511F01A9E6C8500147676 /* Debug */, 277 | 58B511F11A9E6C8500147676 /* Release */, 278 | ); 279 | defaultConfigurationIsVisible = 0; 280 | defaultConfigurationName = Release; 281 | }; 282 | /* End XCConfigurationList section */ 283 | }; 284 | rootObject = 58B511D31A9E6C8500147676 /* Project object */; 285 | } 286 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-animateable-text", 3 | "version": "0.15.0", 4 | "description": "A fork of React Native's ` component that supports Reanimated Shared Values as text!", 5 | "main": "lib/commonjs/index", 6 | "module": "lib/module/index", 7 | "types": "lib/typescript/src/index.d.ts", 8 | "react-native": "src/index", 9 | "source": "src/index", 10 | "files": [ 11 | "src", 12 | "lib", 13 | "android", 14 | "ios", 15 | "cpp", 16 | "react-native-animateable-text.podspec", 17 | "!lib/typescript/example", 18 | "!**/__tests__", 19 | "!**/__fixtures__", 20 | "!**/__mocks__", 21 | "react-native.config.js" 22 | ], 23 | "scripts": { 24 | "test": "jest", 25 | "typescript": "tsc --noEmit", 26 | "lint": "eslint \"**/*.{js,ts,tsx}\"", 27 | "prepare": "bob build", 28 | "release": "dotenv release-it --", 29 | "example": "yarn --cwd example", 30 | "pods": "cd example && pod-install --quiet", 31 | "bootstrap": "yarn example && yarn && yarn pods" 32 | }, 33 | "keywords": [ 34 | "react-native", 35 | "ios", 36 | "android", 37 | "reanimated", 38 | "shared values", 39 | "animated", 40 | "text" 41 | ], 42 | "repository": "https://github.com/axelra-ag/react-native-animateable-text", 43 | "author": "Jonny Burger (https://github.com/JonnyBurger)", 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/axelra-ag/react-native-animateable-text/issues" 47 | }, 48 | "homepage": "https://github.com/axelra-ag/react-native-animateable-text#readme", 49 | "devDependencies": { 50 | "@commitlint/config-conventional": "^17.0.2", 51 | "@miblanchard/react-native-slider": "^2.6.0", 52 | "@react-native-community/eslint-config": "^2.0.0", 53 | "@release-it/conventional-changelog": "^5.0.0", 54 | "@types/react": "^18.3.11", 55 | "commitlint": "^8.3.5", 56 | "dotenv-cli": "^7.2.1", 57 | "eslint": "^7.2.0", 58 | "eslint-config-prettier": "^6.11.0", 59 | "eslint-plugin-prettier": "^3.1.3", 60 | "husky": "^4.2.5", 61 | "prettier": "^2.0.5", 62 | "react-native": "0.76.0", 63 | "react-native-builder-bob": "^0.20.0", 64 | "react-native-reanimated": "~3.16.1", 65 | "release-it": "^15.0.0", 66 | "typescript": "^5.0.2" 67 | }, 68 | "peerDependencies": { 69 | "react": "*", 70 | "react-native": ">=0.74.0", 71 | "react-native-reanimated": ">=3" 72 | }, 73 | "jest": { 74 | "preset": "react-native", 75 | "modulePathIgnorePatterns": [ 76 | "/example/node_modules", 77 | "/lib/" 78 | ] 79 | }, 80 | "commitlint": { 81 | "extends": [ 82 | "@commitlint/config-conventional" 83 | ] 84 | }, 85 | "release-it": { 86 | "git": { 87 | "commitMessage": "chore: release ${version}", 88 | "tagName": "v${version}" 89 | }, 90 | "npm": { 91 | "publish": true 92 | }, 93 | "github": { 94 | "release": false 95 | }, 96 | "plugins": { 97 | "@release-it/conventional-changelog": { 98 | "preset": "angular" 99 | } 100 | } 101 | }, 102 | "eslintConfig": { 103 | "extends": [ 104 | "@react-native-community", 105 | "prettier" 106 | ], 107 | "rules": { 108 | "prettier/prettier": [ 109 | "error", 110 | { 111 | "quoteProps": "consistent", 112 | "singleQuote": true, 113 | "tabWidth": 2, 114 | "trailingComma": "es5", 115 | "useTabs": false 116 | } 117 | ] 118 | } 119 | }, 120 | "eslintIgnore": [ 121 | "node_modules/", 122 | "lib/" 123 | ], 124 | "prettier": { 125 | "quoteProps": "consistent", 126 | "singleQuote": true, 127 | "tabWidth": 2, 128 | "trailingComma": "es5", 129 | "useTabs": false 130 | }, 131 | "react-native-builder-bob": { 132 | "source": "src", 133 | "output": "lib", 134 | "targets": [ 135 | "commonjs", 136 | "module", 137 | [ 138 | "typescript", 139 | { 140 | "project": "tsconfig.build.json" 141 | } 142 | ] 143 | ] 144 | }, 145 | "dependencies": { 146 | "deprecated-react-native-prop-types": "^2.3.0", 147 | "nullthrows": "^1.1.1" 148 | }, 149 | "codegenConfig": { 150 | "name": "JBAnimatedTextCodegen", 151 | "jsSrcsDir": "./src/specs", 152 | "type": "components" 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /react-native-animateable-text.podspec: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | react_native_node_modules_dir = ENV['REACT_NATIVE_NODE_MODULES_DIR'] || File.join(File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native/package.json')"`), '..') 4 | react_native_json = JSON.parse(File.read(File.join(react_native_node_modules_dir, 'react-native/package.json'))) 5 | react_native_minor_version = react_native_json['version'].split('.')[1].to_i 6 | 7 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 8 | folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' 9 | 10 | Pod::Spec.new do |s| 11 | s.name = "react-native-animateable-text" 12 | s.version = package["version"] 13 | s.summary = package["description"] 14 | s.homepage = package["homepage"] 15 | s.license = package["license"] 16 | s.authors = package["author"] 17 | 18 | s.platforms = { :ios => min_ios_version_supported } 19 | s.source = { :git => "https://github.com/JonnyBurger/react-native-animateable-text.git", :tag => "#{s.version}" } 20 | 21 | s.source_files = "{ios}/**/*.{h,m,mm,cpp}" 22 | 23 | s.dependency "hermes-engine" 24 | 25 | s.xcconfig = { 26 | "OTHER_CFLAGS" => "$(inherited) -DREACT_NATIVE_MINOR_VERSION=#{react_native_minor_version}" 27 | } 28 | 29 | install_modules_dependencies(s) 30 | 31 | if ENV['USE_FRAMEWORKS'] != nil && ENV['RCT_NEW_ARCH_ENABLED'] == '1' 32 | add_dependency(s, "React-FabricComponents", :additional_framework_paths => [ 33 | "react/renderer/textlayoutmanager/platform/ios", 34 | "react/renderer/components/text/platform/ios", 35 | 'react/renderer/components/text/BaseTextProps', 36 | 'react/renderer/components/JBAnimatedText' 37 | ]) 38 | end 39 | 40 | if ENV['RCT_NEW_ARCH_ENABLED'] == '1' 41 | s.subspec "newarch" do |ss| 42 | ss.source_files = "cpp/**/*.{cpp,h}" 43 | ss.header_dir = "JBAnimatedText" 44 | ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/cpp\"" } 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /react-native.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@react-native-community/cli-types').UserDependencyConfig} 3 | */ 4 | module.exports = { 5 | dependency: { 6 | platforms: { 7 | android: { 8 | libraryName: 'JBAnimatedText', 9 | componentDescriptors: ['CParagraphComponentDescriptor'], 10 | cmakeListsPath: '../android/src/main/jni/CMakeLists.txt', 11 | }, 12 | macos: null, 13 | windows: null, 14 | }, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/AnimateableText.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | * @format 9 | */ 10 | 11 | 'use strict'; 12 | const React = require('react'); 13 | const ReactNativeViewAttributes = require('react-native/Libraries/Components/View/ReactNativeViewAttributes'); 14 | const Touchable = require('react-native/Libraries/Components/Touchable/Touchable'); 15 | 16 | const createReactNativeComponentClass = require('react-native/Libraries/Renderer/shims/createReactNativeComponentClass').default; 17 | const nullthrows = require('nullthrows'); 18 | const processColor = require('react-native/Libraries/StyleSheet/processColor'); 19 | import type { 20 | Text as IText, 21 | GestureResponderEvent, 22 | HostComponent, 23 | } from 'react-native'; 24 | import type { AnimateableTextProps } from './TextProps'; 25 | 26 | type PressRetentionOffset = { 27 | top: number; 28 | left: number; 29 | bottom: number; 30 | right: number; 31 | }; 32 | 33 | type ResponseHandlers = { 34 | onStartShouldSetResponder: () => boolean; 35 | onResponderGrant: (event: GestureResponderEvent, dispatchID: string) => void; 36 | onResponderMove: (event: GestureResponderEvent) => void; 37 | onResponderRelease: (event: GestureResponderEvent) => void; 38 | onResponderTerminate: (event: GestureResponderEvent) => void; 39 | onResponderTerminationRequest: () => boolean; 40 | }; 41 | 42 | type State = { 43 | touchable: { 44 | touchState?: string; 45 | responderID?: number; 46 | }; 47 | isHighlighted: boolean; 48 | createResponderHandlers: () => ResponseHandlers; 49 | responseHandlers?: ResponseHandlers; 50 | }; 51 | 52 | const PRESS_RECT_OFFSET = { top: 20, left: 20, right: 20, bottom: 30 }; 53 | 54 | const viewConfig = { 55 | validAttributes: { 56 | ...ReactNativeViewAttributes.UIView, 57 | isHighlighted: true, 58 | isPressable: true, 59 | numberOfLines: true, 60 | ellipsizeMode: true, 61 | allowFontScaling: true, 62 | dynamicTypeRamp: true, 63 | maxFontSizeMultiplier: true, 64 | disabled: true, 65 | selectable: true, 66 | selectionColor: true, 67 | adjustsFontSizeToFit: true, 68 | minimumFontScale: true, 69 | textBreakStrategy: true, 70 | onTextLayout: true, 71 | onInlineViewLayout: true, 72 | dataDetectorType: true, 73 | android_hyphenationFrequency: true, 74 | lineBreakStrategyIOS: true, 75 | text: true, 76 | }, 77 | directEventTypes: { 78 | topTextLayout: { 79 | registrationName: 'onTextLayout', 80 | }, 81 | topInlineViewLayout: { 82 | registrationName: 'onInlineViewLayout', 83 | }, 84 | }, 85 | uiViewClassName: 'JBAnimatedText', 86 | }; 87 | 88 | /** 89 | * A React component for displaying text. 90 | * 91 | * See https://reactnative.dev/docs/text.html 92 | */ 93 | class TouchableText extends React.Component { 94 | static defaultProps = { 95 | accessible: true, 96 | allowFontScaling: true, 97 | ellipsizeMode: 'tail', 98 | }; 99 | 100 | touchableGetPressRectOffset?: () => PressRetentionOffset; 101 | touchableHandleActivePressIn?: () => void; 102 | touchableHandleActivePressOut?: () => void; 103 | touchableHandleLongPress?: (event: GestureResponderEvent) => void; 104 | touchableHandlePress?: (event: GestureResponderEvent) => void; 105 | touchableHandleResponderGrant?: (event: GestureResponderEvent) => void; 106 | touchableHandleResponderMove?: (event: GestureResponderEvent) => void; 107 | touchableHandleResponderRelease?: (event: GestureResponderEvent) => void; 108 | touchableHandleResponderTerminate?: (event: GestureResponderEvent) => void; 109 | touchableHandleResponderTerminationRequest?: () => boolean; 110 | 111 | state = { 112 | isHighlighted: false, 113 | createResponderHandlers: this._createResponseHandlers.bind(this), 114 | responseHandlers: null, 115 | }; 116 | 117 | static getDerivedStateFromProps( 118 | nextProps: AnimateableTextProps, 119 | prevState: State 120 | ): Partial | null { 121 | return prevState.responseHandlers == null && isTouchable(nextProps) 122 | ? { 123 | responseHandlers: prevState.createResponderHandlers(), 124 | } 125 | : null; 126 | } 127 | 128 | static viewConfig = viewConfig; 129 | 130 | render(): React.ReactNode { 131 | let props = this.props; 132 | if (isTouchable(props)) { 133 | props = { 134 | ...props, 135 | isHighlighted: this.state.isHighlighted, 136 | }; 137 | } 138 | if (props.selectionColor != null) { 139 | props = { 140 | ...props, 141 | selectionColor: processColor(props.selectionColor), 142 | }; 143 | } 144 | if (__DEV__) { 145 | if (Touchable.TOUCH_TARGET_DEBUG && props.onPress != null) { 146 | props = { 147 | ...props, 148 | style: [props.style, { color: 'magenta' }], 149 | }; 150 | } 151 | } 152 | return ; 153 | } 154 | 155 | _createResponseHandlers(): ResponseHandlers { 156 | return { 157 | onStartShouldSetResponder: (): boolean => { 158 | const { onStartShouldSetResponder } = this.props; 159 | const shouldSetResponder = 160 | (onStartShouldSetResponder == null 161 | ? false 162 | : onStartShouldSetResponder()) || isTouchable(this.props); 163 | 164 | if (shouldSetResponder) { 165 | this._attachTouchHandlers(); 166 | } 167 | return shouldSetResponder; 168 | }, 169 | onResponderGrant: (event: GestureResponderEvent): void => { 170 | nullthrows(this.touchableHandleResponderGrant)(event); 171 | if (this.props.onResponderGrant != null) { 172 | this.props.onResponderGrant.call(this, event); 173 | } 174 | }, 175 | onResponderMove: (event: GestureResponderEvent): void => { 176 | nullthrows(this.touchableHandleResponderMove)(event); 177 | if (this.props.onResponderMove != null) { 178 | this.props.onResponderMove.call(this, event); 179 | } 180 | }, 181 | onResponderRelease: (event: GestureResponderEvent): void => { 182 | nullthrows(this.touchableHandleResponderRelease)(event); 183 | if (this.props.onResponderRelease != null) { 184 | this.props.onResponderRelease.call(this, event); 185 | } 186 | }, 187 | onResponderTerminate: (event: GestureResponderEvent): void => { 188 | nullthrows(this.touchableHandleResponderTerminate)(event); 189 | if (this.props.onResponderTerminate != null) { 190 | this.props.onResponderTerminate.call(this, event); 191 | } 192 | }, 193 | onResponderTerminationRequest: (): boolean => { 194 | const { onResponderTerminationRequest } = this.props; 195 | if (!nullthrows(this.touchableHandleResponderTerminationRequest)()) { 196 | return false; 197 | } 198 | if (onResponderTerminationRequest == null) { 199 | return true; 200 | } 201 | return onResponderTerminationRequest(); 202 | }, 203 | }; 204 | } 205 | 206 | /** 207 | * Lazily attaches Touchable.Mixin handlers. 208 | */ 209 | _attachTouchHandlers(): void { 210 | if (this.touchableGetPressRectOffset != null) { 211 | return; 212 | } 213 | for (const key in Touchable.Mixin) { 214 | if (typeof Touchable.Mixin[key] === 'function') { 215 | this[key] = Touchable.Mixin[key].bind(this); 216 | } 217 | } 218 | this.touchableHandleActivePressIn = (): void => { 219 | if (!this.props.suppressHighlighting && isTouchable(this.props)) { 220 | this.setState({ isHighlighted: true }); 221 | } 222 | }; 223 | this.touchableHandleActivePressOut = (): void => { 224 | if (!this.props.suppressHighlighting && isTouchable(this.props)) { 225 | this.setState({ isHighlighted: false }); 226 | } 227 | }; 228 | this.touchableHandlePress = (event: GestureResponderEvent): void => { 229 | if (this.props.onPress != null) { 230 | this.props.onPress(event); 231 | } 232 | }; 233 | this.touchableHandleLongPress = (event: GestureResponderEvent): void => { 234 | if (this.props.onLongPress != null) { 235 | this.props.onLongPress(event); 236 | } 237 | }; 238 | this.touchableGetPressRectOffset = (): PressRetentionOffset => 239 | this.props.pressRetentionOffset == null 240 | ? PRESS_RECT_OFFSET 241 | : this.props.pressRetentionOffset; 242 | } 243 | } 244 | 245 | const isTouchable = (props: AnimateableTextProps): boolean => 246 | props.onPress != null || 247 | props.onLongPress != null || 248 | // @ts-expect-error 249 | props.onStartShouldSetResponder != null; 250 | 251 | const RCTText = createReactNativeComponentClass( 252 | viewConfig.uiViewClassName, 253 | () => viewConfig 254 | ); 255 | 256 | const Text = (props: AnimateableTextProps, forwardedRef?: React.Ref) => { 257 | // @ts-expect-error 258 | return ; 259 | }; 260 | const TextToExport = React.forwardRef(Text); 261 | TextToExport.displayName = 'Animateable'; 262 | 263 | // TODO: Deprecate this. 264 | /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an error 265 | * found when Flow v0.89 was deployed. To see the error, delete this comment 266 | * and run Flow. */ 267 | 268 | export const AnimateableText = TextToExport as React.ComponentClass< 269 | AnimateableTextProps, 270 | React.ElementRef> 271 | >; 272 | -------------------------------------------------------------------------------- /src/AnimateableText.web.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Text } from 'react-native'; 3 | import { AnimateableTextProps } from './TextProps'; 4 | 5 | export const AnimateableText = React.forwardRef( 6 | (props: AnimateableTextProps, ref) => { 7 | const [text, setText] = React.useState(props.text); 8 | const animatedTextRef = React.useRef(null); 9 | 10 | // just in case users tried to update the value without 11 | // a shared value 12 | React.useEffect(() => { 13 | if (props.text) { 14 | setText(props.text); 15 | } 16 | }, [props.text]); 17 | 18 | React.useImperativeHandle( 19 | ref, 20 | () => ({ 21 | setNativeProps: (nativeProps: AnimateableTextProps) => { 22 | if (animatedTextRef.current && nativeProps.text) { 23 | setText(nativeProps?.text); 24 | } 25 | }, 26 | }), 27 | [] 28 | ); 29 | 30 | return ( 31 | 32 | {text} 33 | 34 | ); 35 | } 36 | ); 37 | -------------------------------------------------------------------------------- /src/TextProps.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { TextProps as NativeTextProps, Text as IText } from 'react-native'; 3 | 4 | export type AnimateableTextProps = Omit & { 5 | forwardedRef?: React.Ref; 6 | text?: string; 7 | }; 8 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import Animated from 'react-native-reanimated'; 2 | import { AnimateableText as RawAnimateableText } from './AnimateableText'; 3 | 4 | Animated.addWhitelistedNativeProps({ text: true }); 5 | 6 | const AnimateableText = Animated.createAnimatedComponent(RawAnimateableText); 7 | 8 | export default AnimateableText; 9 | -------------------------------------------------------------------------------- /src/specs/JBAnimatedTextNativeComponent.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom -> used only for codegen 3 | */ 4 | 5 | import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; 6 | import type { ViewProps } from 'react-native'; 7 | 8 | interface NativeProps extends ViewProps { 9 | text?: string; 10 | } 11 | 12 | export default codegenNativeComponent('JBAnimatedText'); 13 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "paths": { 5 | "react-native-animateable-text": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "react", 12 | "lib": ["esnext"], 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitReturns": true, 17 | "noImplicitUseStrict": false, 18 | "noStrictGenericChecks": false, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "skipLibCheck": true, 23 | "strict": true, 24 | "target": "esnext" 25 | } 26 | } 27 | --------------------------------------------------------------------------------