├── .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 | Animateable Text Version |
10 | RN Version |
11 | Old Arch |
12 | New Arch (Fabric) |
13 |
14 |
15 | ^0.16.0-beta.0 |
16 | ^0.79.0 |
17 | ✅ |
18 | ✅ |
19 |
20 |
21 | ^0.15.0 |
22 | ^0.77.0 |
23 | ✅ |
24 | ✅ |
25 |
26 |
27 | ^0.14.2 |
28 | ^0.76.0 |
29 | ✅ |
30 | ✅ |
31 |
32 |
33 | ^0.13.0 |
34 | ^0.75.0 |
35 | ✅ |
36 | 🛑 |
37 |
38 |
39 | ^0.12.0 |
40 | ^0.74.0 |
41 | ✅ |
42 | 🛑 |
43 |
44 |
45 | ^0.11.0 |
46 | ^0.71.7 |
47 | ✅ |
48 | 🛑 |
49 |
50 |
51 | ^0.10.0 |
52 | ^0.68 |
53 | ✅ |
54 | 🛑 |
55 |
56 |
57 | ^0.9.1 |
58 | ^0.67 |
59 | ✅ |
60 | 🛑 |
61 |
62 |
63 | ^0.8.0 |
64 | ^0.66 |
65 | ✅ |
66 | 🛑 |
67 |
68 |
69 | ^0.7.0 |
70 | ^0.65 |
71 | ✅ |
72 | 🛑 |
73 |
74 |
75 | ^0.6.0 |
76 | ^0.64 |
77 | ✅ |
78 | 🛑 |
79 |
80 |
81 | ^0.5.9 |
82 | ^0.63 |
83 | ✅ |
84 | 🛑 |
85 |
86 |
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 |
--------------------------------------------------------------------------------