├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .markdownlint.json ├── .npmignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── github │ └── amarcruz │ └── rntextsize │ ├── RNTextSizeConf.java │ ├── RNTextSizeModule.java │ ├── RNTextSizePackage.java │ └── RNTextSizeSpannedText.java ├── index.d.ts ├── index.js ├── index.js.flow ├── ios ├── RNTextSize.h ├── RNTextSize.m ├── RNTextSize.podspec └── RNTextSize.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ └── xcschemes │ └── RNTextSize.xcscheme └── package.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // avoid interference of any eslint config in parent directory 2 | const ON = 2 3 | 4 | module.exports = { 5 | root: true, 6 | parserOptions: { 7 | ecmaVersion: 2017, 8 | sourceType: 'module', 9 | ecmaFeatures: { 10 | experimentalObjectRestSpread: true, 11 | impliedStrict: true, 12 | }, 13 | }, 14 | env: { 15 | es6: true, 16 | }, 17 | extends: [ 18 | 'eslint:recommended', 19 | ], 20 | rules: { 21 | 'comma-dangle': [ON, 'always-multiline'], 22 | 'eqeqeq': [ON, 'smart'], 23 | 'semi': [ON, 'never'], 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !default.mode1v3 2 | !default.mode2v3 3 | !default.pbxuser 4 | !default.perspectivev3 5 | .* 6 | .bak/ 7 | .DS_Store 8 | .gradle 9 | .idea 10 | *.bak 11 | *.hmap 12 | *.iml 13 | *.ipa 14 | *.keystore 15 | *.log 16 | *.mode1v3 17 | *.mode2v3 18 | *.moved-aside 19 | *.pbxuser 20 | *.perspectivev3 21 | *.tgz 22 | *.xccheckout 23 | *.xcuserstate 24 | **/.vscode/.react/ 25 | \.buckd/ 26 | ~* 27 | android/build/ 28 | buck-out/ 29 | build/ 30 | DerivedData 31 | local.properties 32 | node_modules/ 33 | package-lock.json 34 | project.xcworkspace 35 | xcuserdata 36 | yarn.lock 37 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "line-length": false, 4 | "no-duplicate-header": { "siblings_only": true }, 5 | "no-inline-html": false 6 | } 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !default.mode1v3 2 | !default.mode2v3 3 | !default.pbxuser 4 | !default.perspectivev3 5 | .* 6 | .bak/ 7 | .DS_Store 8 | .gradle 9 | .idea 10 | *.bak 11 | *.hmap 12 | *.iml 13 | *.ipa 14 | *.keystore 15 | *.log 16 | *.mode1v3 17 | *.mode2v3 18 | *.moved-aside 19 | *.pbxuser 20 | *.perspectivev3 21 | *.tgz 22 | *.xccheckout 23 | *.xcuserstate 24 | **/.vscode/.react/ 25 | \.buckd/ 26 | ~* 27 | android/build/ 28 | buck-out/ 29 | build/ 30 | DerivedData 31 | local.properties 32 | node_modules/ 33 | package-lock.json 34 | project.xcworkspace 35 | xcuserdata 36 | yarn.lock 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 4 | 5 | ## \[Unreleased] 6 | 7 | ### Added 8 | 9 | - Updated README.md with example for flatHeights - Thanks to @donni106 10 | 11 | ### Changed 12 | 13 | - The dependency on android.support.annotations is replaced by javax.annotation 14 | 15 | ### Fixed 16 | 17 | - fix s.source_files regex to match RNTextSize source files: RNTextSize - Thanks to @abegehr 18 | 19 | ## \[3.0.0] - 2019-01-10 20 | 21 | ### Added 22 | 23 | - Support `textTransform:uppercase` for specsForTextStyles in Android. 24 | 25 | ### Changed 26 | 27 | - peerDependency on react-native to >=57.0 28 | - Update gradle files to be consistent with RN 0.57 29 | - Update License year. 30 | 31 | ### Removed 32 | 33 | - Unused 'example' directory. 34 | 35 | ## \[2.1.1] - 2019-01-10 36 | 37 | ### Added 38 | 39 | - Markdown lint rules. 40 | - `lineInfoForLine` option, to get information for a given line. 41 | 42 | ### Changed 43 | 44 | - Update Readme. 45 | - Limit peerDependency on RN to v0.56, rnTextSize v3 will support RN>=57 46 | 47 | ### Fixed 48 | 49 | - Linting errors in the markdown of the Changelog and Readme files. 50 | 51 | ## \[2.0.4] - 2018-09-14 52 | 53 | ### Changed 54 | 55 | - Patch to v2.0.3 published from wrong branch 56 | 57 | ## \[2.0.3] - 2018-09-14 58 | 59 | ### Changed 60 | 61 | - Updated README 62 | 63 | ### Fixed 64 | 65 | - PR #6 Fix podspec sysntax. Thanks to @Jyrno42 66 | 67 | ## \[2.0.2] - 2018-08-31 68 | 69 | ### Changed 70 | 71 | - iOS: Give `flatHeights` better performace by avoiding multiple creation of NSTextStorage. 72 | - Android: Now Studio can check versions in android/build.gradle 73 | 74 | ### Fixed 75 | 76 | - Closes #5: Inconsistency in flatHeights between Android and iOS. 77 | - Some error in the README and include note about unlink the previous version. 78 | 79 | ## \[2.0.1] - 2018-08-22 80 | 81 | ### Fixed 82 | 83 | - Silly typo error in last minute edition. 84 | 85 | ## \[2.0.0] - 2018-08-22 86 | 87 | Bump v2.0.0 :tada: 88 | 89 | ### Changed 90 | 91 | - Code cleanup, minor enhancements. 92 | 93 | ### Removed 94 | 95 | - Removed the `FontSize` constants. 96 | 97 | ## \[2.0.0-beta.6] - 2018-08-21 98 | 99 | ### Added 100 | 101 | - New function `flatHeights` to calculate the height of multiple strings at once, much faster than `measure`. 102 | - Revised Readme. Now it's clearer, part of its content moved to the Wiki. 103 | - Docummented the iOS-only properties `capHeight` and `xHeight` from the `fontFromSpecs` result. 104 | - New flag `usePreciseWidth` (default `false`) request the most accurate calculation of the width (Android) and the value of `lastWidth` (both), but its is a bit slower. 105 | 106 | ### Changed 107 | 108 | - iOS: The `specsForTextStyles` function returns fontSize amd letterSpacing with unscaled values, to allow its use with `allowFontScaling`. 109 | 110 | ### Fixed 111 | 112 | - Android: `Arguments.fromList` does not exists in RN 0.52, replaced with `Arguments.fromArray` 113 | - iOS: Fix errors in the sample App that prevented it from running in iOS. 114 | 115 | ### Removed 116 | 117 | - To avoid interfering with this changelog, the sample application was moved to its own repository. 118 | 119 | ## \[2.0.0-beta.4] - Unpublished 120 | 121 | **WARNING:** 122 | 123 | _The package id has changed, you must uninstall the previous version before using this one._ 124 | 125 | ### Added 126 | 127 | - Note on the README of the sample App, to test it with a different version of RN. 128 | - Adds flow typings (almost) working with Flow 0.61.x 129 | - Enhancements to the sample App with RN 0.56 and Flow 130 | - New functions `specsForTextStyles`, `fontFromSpecs`, and `fontFamilyNames` 131 | 132 | ### Changed 133 | 134 | - Complete rewrite of the sample app. 135 | - Simplify anroid/build.gradle and adjust the configuration. 136 | - `allowFontScaling` is handled in the native side, index.js was simplified a lot. 137 | - Migration of example to RN 0.52 and Typescript, enhanced features. 138 | - Minimum supported versions: RN 0.52.x, Android SDK 16, iOS 9.0 139 | - New fformat of the CHANGELOG to follow the _Keep a Changelog_ recommendations. 140 | - Package ID now is `com.github.amarcruz.rntextsize` for Android, and `RNTextSize` for iOS. 141 | - Fixes to typings. 142 | 143 | ### Removed 144 | 145 | - Drop support for iOS 8 and RN below 0.52 146 | 147 | ### Fixed 148 | 149 | - Tested on iOS 9 & 11 simulators, Android devices 5.1 & 7, simulator 4.4 & 11, all with RN 0.52.0 and 0.56.0 150 | - Improved measurement on both platforms, accuracy is more than 5 decimals. 151 | - The iOS API and behavior of `measure` now is the same as Android. 152 | - Makes the example works. 153 | 154 | ## \[1.0.0-beta.5] - Unpublished 155 | 156 | - Preliminary implementation for iOS (thanks to @xuhom) 157 | - The license is changed to BSD 2-Clause as [react-native-measure-text](https://github.com/airamrguez/react-native-measure-text), on which this library is based. 158 | 159 | ## \[1.0.0-beta.4] - Unpublished 160 | 161 | ### Changed 162 | 163 | - Now the SDK versions are compatible with global rootProject.ext for flexible configuration. 164 | 165 | ## [1.0.0-beta.3] - 2018-02-17 166 | 167 | Published to npm, tested with Android SDK 21. 168 | 169 | ### Changed 170 | 171 | - Updates compileSdkVersion, buildToolsVersion and targetSdkVersion. 172 | - Gradle plugin to 2.3.3 173 | 174 | ## \[1.0.0-beta.2] - Unpublished 175 | 176 | ### Changed 177 | 178 | - Using ES6 for index.js 179 | 180 | ### Fixed 181 | 182 | - Correction to the default export in the index 183 | 184 | ## \[1.0.0-alpha.10] - Unpublished 185 | 186 | ### Changed 187 | 188 | - compileSdkVersion 25, buildToolsVersion 25.0.3, targetSdkVersion 25 189 | 190 | ## \[1.0.0-alpha.9] - Unpublished 191 | 192 | ### Changed 193 | 194 | - Revision to README 195 | - targetSdkVersion from 22 to 23. 196 | 197 | ### Fixed 198 | 199 | - Fix error in android path 200 | - Android package id is 'io.amarcruz.rnmeasuretext' 201 | 202 | ## \[1.0.0-alpha.8] - Unpublished 203 | 204 | ### Added 205 | 206 | - Suppport for `includeFontPadding` 207 | 208 | ### Changed 209 | 210 | - Using gradle 2.2.3 211 | 212 | ## \[1.0.0-alpha.7] - Unpublished 213 | 214 | ### Added 215 | 216 | - Returned info includes `lineCount` with the number of lines and `lastLineWidth` with the last line width (Android). 217 | - Uses scaled `fontSize` (with `allowFontScaling`, automatic). 218 | 219 | ### Changed 220 | 221 | - Makes `width` property optional in parameters (Android). 222 | - Minor fixes, now working. 223 | - First commit, Android only. 224 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2018-2019 Alberto Martínez. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Text Size 2 | 3 | [![npm Version][npm-badge]][npm-url] 4 | [![License][license-badge]][license-url] 5 | 6 | Measure text accurately before laying it out and get font information from your App (Android and iOS). 7 | 8 | In my country (México), software developers are poorly paid, so I have had to look for another job to earn a living and I cannot dedicate more time to maintaining this and other repositories that over the years have never generated any money for me. If anyone is interested in maintaining this repository, I'd be happy to transfer it to them, along with the associated npm package. | 9 | :---: | 10 | En mi país (México), los desarrolladores de software somos pésimamente pagados, por lo que he tenido que buscar otro trabajo para ganarme la vida y no puedo dedicar más tiempo a mantener éste y otros repositorios que a través de los años nunca me generaron dinero. Si a alguien le interesa dar mantenimiento a este repositorio, con gusto se lo transferiré, así como el paquete de npm asociado. | 11 | 12 | There are two main functions: `flatHeights` to obtain the height of different blocks of text simultaneously, optimized for components such as [``][0] or [``][1]. 13 | 14 | The other one is `measure`, which gets detailed information about one block of text: 15 | 16 | - The width used by the text, with an option to calculate the real width of the largest line. 17 | - The height of the text, with or without paddings. 18 | - The number of lines. 19 | - The width of the last line. 20 | - Extended information of a given line. 21 | 22 | The width and height are practically the same as those received from the `onLayout` event of a `` component with the same properties. 23 | 24 | In both functions, the text to be measured is required, but the rest of the parameters are optional and work in the same way as with React Native: 25 | 26 | - `fontFamily` 27 | - `fontSize` 28 | - `fontWeight` 29 | - `fontStyle` 30 | - `fontVariant` (iOS) 31 | - `includeFontPadding` (Android) 32 | - `textBreakStrategy` (Android) 33 | - `letterSpacing` 34 | - `allowFontScaling` 35 | - `width`: Constraint for automatic line-break based on text-break strategy. 36 | 37 | In addition, the library includes functions to obtain information about the fonts visible to the App. 38 | 39 | If it has helped you, please support my work with a star ⭐️ or [ko-fi][kofi-url]. 40 | 41 | ## Installation 42 | 43 | Mostly automatic installation from npm 44 | 45 | ```bash 46 | yarn add react-native-text-size 47 | react-native link react-native-text-size 48 | ``` 49 | 50 | Change the `compile` directive to `implementation` in the dependencies block of the android/app/build.gradle file. 51 | 52 | **Requirements:** 53 | 54 | - React Native v0.57 or later. 55 | - Android API 16 or iOS 9.0 and above. 56 | 57 | For versions prior to 0.56 of React Native, please use [react-native-text-size v2.1.1](https://www.npmjs.com/package/react-native-text-size/v/2.1.1) 58 | 59 | See [Manual Installation][2] on the Wiki as an alternative if you have problems with automatic installation. 60 | 61 | ## API 62 | 63 | - [`measure`](#measure) 64 | 65 | - [`flatHeights`](#flatheights) 66 | 67 | - [`specsForTextStyles`](#specsfortextstyles) 68 | 69 | - [`fontFromSpecs`](#fontfromspecs) 70 | 71 | - [`fontFamilyNames`](#fontfamilynames) 72 | 73 | - [`fontNamesForFamilyName`](#fontnamesforfamilyname) 74 | 75 | ## measure 76 | 77 | ```ts 78 | measure(options: TSMeasureParams): Promise 79 | ``` 80 | 81 | This function measures the text as RN does and its result is consistent\* with that of `Text`'s [onLayout](https://facebook.github.io/react-native/docs/text#onlayout) event. It takes a subset of the properties used by [``][3] to describe the font and other options to use. 82 | 83 | If you provide `width`, the measurement will apply automatic wrapping in addition to the explicit line breaks. 84 | 85 | \* _There may be some inconsistencies in iOS, see this [Know Issue](#incorrent-height-ios) to know more._ 86 | 87 | **Note:** 88 | 89 | Although this function is accurate and provides complete information, it can be heavy if the text is a lot, like the one that can be displayed in a FlatList. For these cases, it is better to use [`flatHeights`](#flatheights), which is optimized for batch processing. 90 | 91 | ### TSMeasureParams 92 | 93 | Plain JS object with this properties (only `text` is required): 94 | 95 | Property | Type | Default | Notes 96 | ------------------ | ------ | -------- | ------ 97 | text | string | (none) | This is the only required parameter and may include _emojis_ or be empty, but it **must not be** `null`.
If this is an empty string the resulting `width` will be zero. 98 | fontFamily | string | OS dependent | The default is the same applied by React Native: Roboto in Android, San Francisco in iOS.
**Note:** Device manufacturer or custom ROM can change the default font. 99 | fontWeight | string | 'normal' | On android, numeric ranges has no granularity and '500' to '900' becomes 'bold', but you can use a `fontFamily` of specific weight ("sans-serif-thin", "sans-serif-medium", etc). 100 | fontSize | number | 14 | The default font size comes from RN. 101 | fontStyle | string | 'normal' | One of "normal" or "italic". 102 | fontVariant | array | (none) | _iOS only_ 103 | allowFontScaling | boolean | true | To respect the user' setting of large fonts (i.e. use SP units). 104 | letterSpacing | number | (none) | Additional spacing between characters (aka `tracking`).
**Note:** In iOS a zero cancels automatic kerning.
_All iOS, Android with API 21+_ 105 | includeFontPadding | boolean | true | Include additional top and bottom padding, to avoid clipping certain characters.
_Android only_ 106 | textBreakStrategy | string | 'highQuality' | One of 'simple', 'balanced', or 'highQuality'.
_Android only, with API 23+_ 107 | width | number | MAX_INT | Restrict the width. The resulting height will vary depending on the automatic flow of the text. 108 | usePreciseWidth | boolean | false | If `true`, the result will include an exact `width` and the `lastLineWidth` property.
You can see the effect of this flag in the [sample App][sample-app]. 109 | lineInfoForLine | number | (none) | If `>=0`, the result will include a [lineInfo](#lineinfo) property with information for the required line number. 110 | 111 | The [sample App][sample-app] shows interactively the effect of these parameters on the screen. 112 | 113 | ### TSMeasureResult 114 | 115 | `measure` returns a Promise that resolves to a JS object with this properties: 116 | 117 | Property | Type | Notes 118 | ------------- | ------ | ------ 119 | width | number | Total used width. It may be less or equal to the `width` option.
On Android, this value may vary depending on the `usePreciseWidth` flag. 120 | height | number | Total height, including top and bottom padding if `includingFontPadding` was set (the default). 121 | lastLineWidth | number | Width of the last line, without trailing blanks.
If `usePreciseWidth` is `false` (the default), this property is undefined. 122 | lineCount | number | Number of lines, taking into account hard and automatic line breaks. 123 | lineInfo | object | Line information.
If the `lineInfoForLine` option is not given, this property is undefined. 124 | 125 | #### lineInfo 126 | 127 | If the value of the `lineInfoForLine` is greater or equal than `lineCount`, this info is for the last line (i.e. `lineCount` - 1). 128 | 129 | Property | Type | Notes 130 | ------------- | ------ | ------ 131 | line | number | Line number of this info, base 0.
It can be less than the requested line number if `lineInfoForLine` is out of range. 132 | start | number | Text offset of the beginning of this line. 133 | end | number | Text offset after the last _visible_ character (so whitespace is not counted) on this line. 134 | bottom | number | The vertical position of the bottom of this line, including padding. 135 | width | number | Horizontal extent of this line, including leading margin indent, but excluding trailing whitespace.
Use `usePreciseWidth:true` to get an accurate value for this property. 136 | 137 | In case of error, the promise is rejected with an extended Error object with one of the following error codes, as a literal string: 138 | 139 | Code | Details 140 | -------------------- | ------- 141 | E_MISSING_PARAMETERS | `measure` requires an object with the parameters, which was not provided. 142 | E_MISSING_TEXT | The text to measure is `null` or was not provided. 143 | E_INVALID_FONT_SPEC | The font specification is not valid. It is unlikely that this will happen on Android. 144 | E_UNKNOWN_ERROR | Well... who knows? 145 | 146 | ### Example 147 | 148 | ```jsx 149 | //... 150 | import rnTextSize, { TSFontSpecs } from 'react-native-text-size' 151 | 152 | type Props = {} 153 | type State = { width: number, height: number } 154 | 155 | // On iOS 9+ will show 'San Francisco' and 'Roboto' on Android 156 | const fontSpecs: TSFontSpecs = { 157 | fontFamily = undefined, 158 | fontSize = 24, 159 | fontStyle = 'italic', 160 | fontWeight = 'bold', 161 | } 162 | const text = 'I ❤️ rnTextSize' 163 | 164 | class Test extends Component { 165 | state = { 166 | width: 0, 167 | height: 0, 168 | } 169 | 170 | async componentDidMount() { 171 | const width = Dimensions.get('window').width * 0.8 172 | const size = await rnTextSize.measure({ 173 | text, // text to measure, can include symbols 174 | width, // max-width of the "virtual" container 175 | ...fontSpecs, // RN font specification 176 | }) 177 | this.setState({ 178 | width: size.width, 179 | height: size.height 180 | }) 181 | } 182 | 183 | // The result is reversible 184 | render() { 185 | const { width, height } = this.state 186 | return ( 187 | 188 | 189 | {text} 190 | 191 | 192 | ) 193 | } 194 | } 195 | ``` 196 | 197 | ## flatHeights 198 | 199 | ```ts 200 | flatHeights(options: TSHeightsParams): Promise 201 | ``` 202 | 203 | Calculate the height of each of the strings in an array. 204 | 205 | This is an alternative to `measure` designed for cases in which you have to calculate the height of numerous text blocks with common characteristics (width, font, etc), a typical use case with `` or `` components. 206 | 207 | The measurement uses the same algorithm as `measure` but it returns only the height of each block and, by avoiding multiple steps through the bridge, it is faster... _much faster_ on Android! 208 | 209 | I did tests on 5,000 random text blocks and these were the results (ms): 210 | 211 |   | `measure` | `flatHeights` 212 | ------- | --------: | ----------: 213 | Android | 49,624 | 1,091 214 | iOS | 1,949 | 732 215 | 216 | In the future I will prepare an example of its use with FlatList and multiple styles on the same card. 217 | 218 | ### TSHeightsParams 219 | 220 | This is an object similar to the one you pass to `measure`, but the `text` option is an array of strings and the `usePreciseWidth` and `lineInfoForLine` options are ignored. 221 | 222 | Property | Type | Default 223 | ------------------- | -------- | -------- 224 | text | string[] | (none) 225 | width | number | Infinity 226 | fontFamily | string | OS dependent 227 | fontWeight | string | 'normal' 228 | fontSize | number | 14 229 | fontStyle | string | 'normal' 230 | fontVariant | array | (none) 231 | allowFontScaling | boolean | true 232 | letterSpacing | number | (none) 233 | includeFontPadding | boolean | true 234 | textBreakStrategy | string | 'highQuality' 235 | 236 | The result is a Promise that resolves to an array with the height of each block (in _SP_), in the same order in which the blocks were received. 237 | 238 | Unlike measure, `null` elements returns 0 without generating error, and empty strings returns the same height that RN assigns to empty `` components (the difference of the result between `null` and empty is intentional). 239 | 240 | ### Example 241 | 242 | ```jsx 243 | //... 244 | import rnTextSize, { TSFontSpecs } from 'react-native-text-size' 245 | 246 | type Props = { texts: string[] } 247 | type State = { heights: number[] } 248 | 249 | // On iOS 9+ will show 'San Francisco' and 'Roboto' on Android 250 | const fontSpecs: TSFontSpecs = { 251 | fontFamily = undefined, 252 | fontSize = 24, 253 | fontStyle = 'italic', 254 | fontWeight = 'bold', 255 | } 256 | const texts = ['I ❤️ rnTextSize', 'I ❤️ rnTextSize using flatHeights', 'Thx for flatHeights'] 257 | 258 | class Test extends Component { 259 | state = { 260 | heights: [], 261 | } 262 | 263 | async componentDidMount() { 264 | const { texts } = this.props 265 | const width = Dimensions.get('window').width * 0.8 266 | const heights = await rnTextSize.flatHeights({ 267 | text: texts, // array of texts to measure, can include symbols 268 | width, // max-width of the "virtual" container 269 | ...fontSpecs, // RN font specification 270 | }) 271 | this.setState({ 272 | heights 273 | }) 274 | } 275 | 276 | render() { 277 | const { texts } = this.props 278 | const { heights } = this.state 279 | 280 | return ( 281 | 282 | {texts.map( 283 | (text, index) => ( 284 | 285 | {text} 286 | 287 | ) 288 | )} 289 | 290 | ) 291 | } 292 | } 293 | ``` 294 | 295 | ## specsForTextStyles 296 | 297 | ```ts 298 | specsForTextStyles(): Promise<{ [key: string]: TSFontForStyle }> 299 | ``` 300 | 301 | Get system font information for the running OS. 302 | 303 | This is a wrapper for the iOS [`UIFont.preferredFontForTextStyle`][4] method and the current Android [Material Design Type Scale][5] styles. 304 | 305 | The result is a Promise that resolves to a JS object whose keys depend on the OS, but its values are in turn objects fully compatible with those used in the RN styles, so it can be used to stylize `` or `` components: 306 | 307 | ### TSFontForStyle 308 | 309 | Property | Type | Notes 310 | ------------- | ------------ |------ 311 | fontFamily | string | System family name or font face. 312 | fontSize | number | Font size in _SP_ (unscaled). 313 | fontStyle | TSFontStyle | Only if 'italic', undefined if the style is 'normal'. 314 | fontWeight | TSFontWeight | Only if 'bold', undefined if the weight is 'normal'. 315 | fontVariant | TSFontVariant[] or null | _iOS only_. Currently, no style includes this property. 316 | letterSpacing | number | Omitted if running on Android with RN lower than 0.55 317 | 318 | To know the key names, please see [Keys from specsForTextStyles][6] in the Wiki. 319 | 320 | I have not tried to normalize the keys of the result because, with the exception of two or three, they have a different interpretation in each OS, but you can use them to create custom styles according to your needs. 321 | 322 | ## fontFromSpecs 323 | 324 | ```ts 325 | fontFromSpecs(specs: TSFontSpecs): Promise 326 | ``` 327 | 328 | Returns the characteristics of the font obtained from the given specifications. 329 | 330 | ### TSFontSpecs 331 | 332 | This parameter is a subset of [`TSMeasureParams`](#tsmeasureparams), so the details are omitted here. 333 | 334 | Property | Type | Default 335 | ------------- | -------- | ------- 336 | fontFamily | string | iOS: 'San Francisco', Android: 'Roboto' 337 | fontWeight | string | 'normal' 338 | fontSize | number | 14 339 | fontStyle | string | 'normal' 340 | fontVariant | string[] | (none) 341 | letterSpacing | number | 0 342 | 343 | `fontFromSpecs` uses an implicit `allowsFontScaling:true` and, since this is not a measuring function, `includeFontPadding` has no meaning. 344 | 345 | ### TSFontInfo 346 | 347 | The result is a Promise that resolves to a JS object with info for the given font and size, units in [_SP_][7] in Android or points in iOS, using floating point numbers where applicable\*. 348 | 349 | Property | Type | Details 350 | ----------- | -------- | -------- 351 | fontFamily | string | In Android it is the same string passed as parameter. 352 | fontName | string |_iOS only_, always `undefined` in Android. 353 | fontSize | number | It may be different from the given parameter if the parameter includes decimals. 354 | fontStyle | string | 'normal' or 'italic'. 355 | fontWeight | string | 'normal' or 'bold', on iOS it can go from '100' to '900'. 356 | fontVariant | string[] | _iOS only_, always `undefined` in Android. 357 | ascender | number | The recommended distance above the baseline for singled spaced text. 358 | descender | number | The recommended distance below the baseline for singled spaced text. 359 | capHeight | number | _iOS only_ Height of capital characters. 360 | xHeight | number | _iOS only_ Height of lowercase "x". 361 | top | number | _Android only_. Maximum distance above the baseline for the tallest glyph in the font. 362 | bottom | number | _Android only_. Maximum distance below the baseline for the lowest glyph in the font. 363 | leading | number | The recommended additional space to add between lines of text. 364 | lineHeight | number | The recommended line height. It should be greater if text contain Unicode symbols, such as emojis. 365 | _hash | number | Hash code, may be useful for debugging. 366 | 367 | \* _Using floats is more accurate than integers and allows you to use your preferred rounding method, but consider no more than 5 digits of precision in this values. Also, remember RN doesn't work with subpixels in Android and will truncate this values._ 368 | 369 | See more in: 370 | 371 | [Understanding typography][8] at the Google Material Design site. 372 | 373 | [About Text Handling in iOS][9] for iOS. 374 | 375 | ## fontFamilyNames 376 | 377 | ```ts 378 | fontFamilyNames(): Promise 379 | ``` 380 | 381 | Returns a Promise for an array of font family names available on the system. 382 | 383 | On iOS, this uses the [`UIFont.familyNames`][10] method of the UIKit. 384 | 385 | On Android, the result is hard-coded for the system fonts and complemented dynamically with the fonts installed by your app, if any. 386 | 387 | See [About Android Fonts][11] and [Custom Fonts][12] in the Wiki to know more about this list. 388 | 389 | ## fontNamesForFamilyName 390 | 391 | ```ts 392 | fontNamesForFamilyName(fontFamily: string): Promise 393 | ``` 394 | 395 | Wrapper for the `UIFont.fontNamesForFamilyName` method of UIKit, returns an array of font names available in a particular font family. 396 | 397 | You can use the rnTextSize's `fontFamilyNames` function to get an array of the available font family names on the system. 398 | 399 | This is an **iOS only** function, on Android it always resolves to `null`. 400 | 401 | ## Known Issues 402 | 403 | ### Inconsistent width between platforms 404 | 405 | In iOS, the resulting width of both, `measure` and `flatHeights`, includes leading whitespace while in Android these are discarded. 406 | 407 | ### Incorrent height (iOS) 408 | 409 | On iOS, RN takes into account the absolute position on the screen to calculate the dimensions. rnTextSize can't do that and both, width and height, can have a difference of up to 1 pixel (not point). 410 | 411 | ### letterSpacing not scaling (iOS) 412 | 413 | RN does not support the [Dynamic Type Sizes][13], but does an excellent job imitating this feature through `allowFontScaling` ...except for `letterSpacing` that is not scaled. 414 | 415 | I hope that a future version of RN solves this issue. 416 | 417 | ### lineHeight Support 418 | 419 | Although rnTextSize provides the resulting `lineHeight` in some functions, it does not support it as a parameter because RN uses a non-standard algorithm to set it. I recommend you do not use `lineHeight` unless it is strictly necessary, but if you use it, try to make it 30% or more than the font size, or use rnTextSize [`fontFromSpecs`](#fontfromspecs) method if you want more precision. 420 | 421 | ### Nested Text 422 | 423 | Nested `` components (or with images inside) can be rasterized with dimensions different from those calculated, rnTextSize does not accept multiple sizes. 424 | 425 | ## TODO 426 | 427 | - [X] Normalized tracking or letter spacing in font info. 428 | - [ ] More testing, including Android and iOS TVs. 429 | - [ ] Learn the beautiful English, to make better docs. 430 | - [ ] Find something nice in the ugly Objective-C. 431 | 432 | ## Support my Work 433 | 434 | I'm a full-stack developer with more than 20 year of experience and I try to share most of my work for free and help others, but this takes a significant amount of time and effort so, if you like my work, please consider... 435 | 436 | [][kofi-url] 437 | 438 | Of course, feedback, PRs, and stars are also welcome 🙃 439 | 440 | Thanks for your support! 441 | 442 | ## License 443 | 444 | The [BSD 2-Clause](LICENSE) "Simplified" License. 445 | 446 | © 2018-2019, Alberto Martínez. All rights reserved. 447 | 448 | [npm-badge]: https://img.shields.io/npm/v/react-native-text-size.svg 449 | [npm-url]: https://www.npmjs.com/package/react-native-text-size 450 | [license-badge]: https://img.shields.io/badge/license-BSD%202--Clause-blue.svg 451 | [license-url]: https://github.com/aMarCruz/react-native-text-size/blob/master/LICENSE 452 | [kofi-url]: https://ko-fi.com/C0C7LF7I 453 | [sample-app]: https://github.com/aMarCruz/rn-text-size-sample-app 454 | [0]: https://facebook.github.io/react-native/docs/flatlist 455 | [1]: https://www.npmjs.com/package/recyclerlistview 456 | [2]: https://github.com/aMarCruz/react-native-text-size/wiki/Manual-Installation 457 | [3]: https://facebook.github.io/react-native/docs/text#props 458 | [4]: https://developer.apple.com/documentation/uikit/uifont/1619030-preferredfontfortextstyle 459 | [5]: https://material.io/design/typography/#type-scale 460 | [6]: https://github.com/aMarCruz/react-native-text-size/wiki/Keys-from-specsForTextStyles 461 | [7]: https://developer.android.com/guide/topics/resources/more-resources#Dimension 462 | [8]: https://material.io/design/typography/understanding-typography.html#type-properties 463 | [9]: https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/Introduction/Introduction.html#//apple_ref/doc/uid/TP40009542. 464 | [10]: https://developer.apple.com/documentation/uikit/uifont/1619040-familynames?language=objc 465 | [11]: https://github.com/aMarCruz/react-native-text-size/wiki/About-Android-Fonts 466 | [12]: https://github.com/aMarCruz/react-native-text-size/wiki/Custom-Fonts 467 | [13]: https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/typography#dynamic-type-sizes 468 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | import groovy.json.JsonSlurper 2 | 3 | def getPackageVersion() { 4 | return new JsonSlurper().parse(file('../package.json'), 'utf-8').version 5 | } 6 | 7 | def safeExtGet(prop, fallback) { 8 | return rootProject.ext.hasProperty(prop) ? rootProject.ext.get(prop) : fallback 9 | } 10 | 11 | def _buildToolsVersion = safeExtGet('buildToolsVersion', '28.0.3') 12 | def _compileSdkVersion = safeExtGet('compileSdkVersion', 28) 13 | def _targetSdkVersion = safeExtGet('targetSdkVersion', 28) 14 | def _minSdkVersion = safeExtGet('minSdkVersion', 16) 15 | 16 | buildscript { 17 | repositories { 18 | google() 19 | jcenter() 20 | } 21 | dependencies { 22 | classpath 'com.android.tools.build:gradle:3.4.1' 23 | } 24 | } 25 | 26 | apply plugin: 'com.android.library' 27 | 28 | android { 29 | compileSdkVersion _compileSdkVersion 30 | buildToolsVersion _buildToolsVersion 31 | 32 | defaultConfig { 33 | minSdkVersion _minSdkVersion 34 | targetSdkVersion _targetSdkVersion 35 | versionCode 4 36 | versionName getPackageVersion() 37 | } 38 | lintOptions { 39 | abortOnError false 40 | } 41 | } 42 | 43 | repositories { 44 | mavenLocal() 45 | google() 46 | jcenter() 47 | maven { 48 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 49 | url "$rootDir/../node_modules/react-native/android" 50 | } 51 | } 52 | 53 | dependencies { 54 | compileOnly 'com.facebook.react:react-native:+' 55 | } 56 | 57 | task customClean(type: Delete) { 58 | delete rootProject.buildDir 59 | } 60 | 61 | clean.dependsOn customClean 62 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aMarCruz/react-native-text-size/e8483f5fe8efc257ebfead5e8cad5250e7330153/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Mar 28 10:38:41 CST 2018 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-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/java/com/github/amarcruz/rntextsize/RNTextSizeConf.java: -------------------------------------------------------------------------------- 1 | package com.github.amarcruz.rntextsize; 2 | 3 | import android.graphics.Typeface; 4 | import android.os.Build; 5 | import android.text.Layout; 6 | import android.util.Log; 7 | 8 | import com.facebook.react.bridge.JSApplicationIllegalArgumentException; 9 | import com.facebook.react.bridge.ReactApplicationContext; 10 | import com.facebook.react.bridge.ReadableArray; 11 | import com.facebook.react.bridge.ReadableMap; 12 | import com.facebook.react.bridge.ReadableType; 13 | import com.facebook.react.uimanager.PixelUtil; 14 | import com.facebook.react.views.text.ReactFontManager; 15 | 16 | import java.util.Map; 17 | 18 | import javax.annotation.Nonnull; 19 | import javax.annotation.Nullable; 20 | 21 | @SuppressWarnings("SameParameterValue") 22 | final class RNTextSizeConf { 23 | 24 | private static final float DEF_FONTSIZE = 14.0f; 25 | private static final int reactNativeVersion; 26 | 27 | static { 28 | int version = 0; 29 | try { 30 | Class.forName("com.facebook.react.modules.systeminfo.ReactNativeVersion"); 31 | Map rnv = com.facebook.react.modules.systeminfo.ReactNativeVersion.VERSION; 32 | version = ((int) rnv.get("major") << 16) | (int) rnv.get("minor"); 33 | } catch (Exception ex) { 34 | Log.v("RNTextSize", "Cannot get RN version.", ex); 35 | } 36 | reactNativeVersion = version; 37 | } 38 | 39 | /** 40 | * Make a Typeface from the supplied font family and style. 41 | */ 42 | @Nonnull 43 | static Typeface getFont( 44 | @Nonnull final ReactApplicationContext context, 45 | @Nullable String family, 46 | final int style 47 | ) { 48 | final Typeface typeface = family != null 49 | ? ReactFontManager.getInstance().getTypeface(family, style, context.getAssets()) 50 | : null; 51 | 52 | return typeface != null ? typeface : Typeface.defaultFromStyle(style); 53 | } 54 | 55 | // letterSpacing is supported in RN 0.55+ 56 | static boolean supportLetterSpacing() { 57 | return reactNativeVersion >= 55; 58 | } 59 | 60 | static boolean supportUpperCaseTransform() { 61 | return reactNativeVersion >= 57; 62 | } 63 | 64 | private final ReadableMap mOpts; 65 | private final boolean allowFontScaling; 66 | 67 | final String fontFamily; 68 | final float fontSize; 69 | final int fontStyle; 70 | final boolean includeFontPadding; 71 | final float letterSpacing; 72 | 73 | /** 74 | * Proccess the user specs. Set both `allowFontScaling` & `includeFontPadding` to the user 75 | * value or the default `true` only if we have the `forText` flag. 76 | * 77 | * @param options User options 78 | * @param forText This will be used for measure text? 79 | */ 80 | RNTextSizeConf(@Nonnull final ReadableMap options, final boolean forText) { 81 | mOpts = options; 82 | 83 | allowFontScaling = forText && getBooleanOrTrue("allowFontScaling"); 84 | fontFamily = getString("fontFamily"); 85 | fontSize = getFontSizeOrDefault(); 86 | fontStyle = getFontStyle(); 87 | includeFontPadding = forText && getBooleanOrTrue("includeFontPadding"); 88 | 89 | // letterSpacing is supported in RN 0.55+ 90 | letterSpacing = supportLetterSpacing() ? getFloatOrNaN("letterSpacing") : Float.NaN; 91 | } 92 | 93 | boolean has(@Nonnull final String name) { 94 | return mOpts.hasKey(name); 95 | } 96 | 97 | boolean getBooleanOrTrue(@Nonnull final String name) { 98 | return !mOpts.hasKey(name) || mOpts.getBoolean(name); 99 | } 100 | 101 | Integer getIntOrNull(@Nonnull final String name) { 102 | return mOpts.hasKey(name) 103 | ? mOpts.getInt(name) : null; 104 | } 105 | 106 | @Nullable 107 | String getString(@Nonnull final String name) { 108 | return mOpts.hasKey(name) 109 | ? mOpts.getString(name) : null; 110 | } 111 | 112 | @Nullable 113 | ReadableArray getArray(@Nonnull final String name) { 114 | return mOpts.hasKey(name) && mOpts.getType(name) == ReadableType.Array 115 | ? mOpts.getArray(name) : null; 116 | } 117 | 118 | float scale(final float measure) { 119 | return allowFontScaling 120 | ? PixelUtil.toPixelFromSP(measure) 121 | : PixelUtil.toPixelFromDIP(measure); 122 | } 123 | 124 | float getWidth(final float density) { 125 | float width = getFloatOrNaN("width"); 126 | if (!Float.isNaN(width) && width > 0) { 127 | return width * density; // always DIP 128 | } else { 129 | return Float.MAX_VALUE; 130 | } 131 | } 132 | 133 | int getTextBreakStrategy() { 134 | if (Build.VERSION.SDK_INT < 23) { 135 | return 0; 136 | } 137 | 138 | final String textBreakStrategy = getString("textBreakStrategy"); 139 | 140 | if (textBreakStrategy != null) { 141 | switch (textBreakStrategy) { 142 | case "balanced": 143 | return Layout.BREAK_STRATEGY_BALANCED; 144 | case "highQuality": 145 | return Layout.BREAK_STRATEGY_HIGH_QUALITY; 146 | case "simple": 147 | return Layout.BREAK_STRATEGY_SIMPLE; 148 | default: 149 | throw new JSApplicationIllegalArgumentException( 150 | "Invalid textBreakStrategy: " + textBreakStrategy); 151 | } 152 | } 153 | return Layout.BREAK_STRATEGY_HIGH_QUALITY; 154 | } 155 | 156 | private float getFloatOrNaN(@Nonnull final String name) { 157 | return mOpts.hasKey(name) ? (float) mOpts.getDouble(name) : Float.NaN; 158 | } 159 | 160 | private float getFontSizeOrDefault() { 161 | if (mOpts.hasKey("fontSize")) { 162 | final float num = (float) mOpts.getDouble("fontSize"); 163 | 164 | if (num > 0f) { 165 | return num; 166 | } 167 | } 168 | return DEF_FONTSIZE; 169 | } 170 | 171 | private int getFontStyle() { 172 | int style = "italic".equals(getString("fontStyle")) ? Typeface.ITALIC : Typeface.NORMAL; 173 | 174 | final String weight = getString("fontWeight"); 175 | if (weight != null) { 176 | switch (weight) { 177 | case "bold": 178 | case "900": 179 | case "800": 180 | case "700": 181 | case "600": 182 | case "500": 183 | style |= Typeface.BOLD; 184 | break; 185 | } 186 | } 187 | return style; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /android/src/main/java/com/github/amarcruz/rntextsize/RNTextSizeModule.java: -------------------------------------------------------------------------------- 1 | package com.github.amarcruz.rntextsize; 2 | 3 | import android.content.res.AssetManager; 4 | import android.graphics.Paint; 5 | import android.graphics.Typeface; 6 | import android.os.Build; 7 | import android.text.BoringLayout; 8 | import android.text.Layout; 9 | import android.text.SpannableString; 10 | import android.text.SpannableStringBuilder; 11 | import android.text.StaticLayout; 12 | import android.text.TextPaint; 13 | 14 | import com.facebook.react.bridge.Arguments; 15 | import com.facebook.react.bridge.Promise; 16 | import com.facebook.react.bridge.ReactApplicationContext; 17 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 18 | import com.facebook.react.bridge.ReactMethod; 19 | import com.facebook.react.bridge.ReadableArray; 20 | import com.facebook.react.bridge.ReadableMap; 21 | import com.facebook.react.bridge.ReadableType; 22 | import com.facebook.react.bridge.WritableArray; 23 | import com.facebook.react.bridge.WritableMap; 24 | import com.facebook.react.uimanager.DisplayMetricsHolder; 25 | 26 | import java.io.IOException; 27 | import java.util.ArrayList; 28 | import java.util.Collections; 29 | import java.util.List; 30 | 31 | import javax.annotation.Nonnull; 32 | import javax.annotation.Nullable; 33 | 34 | class RNTextSizeModule extends ReactContextBaseJavaModule { 35 | private static final String TAG = "RNTextSize"; 36 | private static final float SPACING_ADDITION = 0f; 37 | private static final float SPACING_MULTIPLIER = 1f; 38 | 39 | private static final String E_MISSING_TEXT = "E_MISSING_TEXT"; 40 | private static final String E_MISSING_PARAMETER = "E_MISSING_PARAMETER"; 41 | private static final String E_UNKNOWN_ERROR = "E_UNKNOWN_ERROR"; 42 | 43 | // It's important to pass the ANTI_ALIAS_FLAG flag to the constructor rather than setting it 44 | // later by calling setFlags. This is because the latter approach triggers a bug on Android 4.4.2. 45 | // The bug is that unicode emoticons aren't measured properly which causes text to be clipped. 46 | private static final TextPaint sTextPaintInstance = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); 47 | 48 | private final ReactApplicationContext mReactContext; 49 | 50 | RNTextSizeModule(ReactApplicationContext reactContext) { 51 | super(reactContext); 52 | mReactContext = reactContext; 53 | } 54 | 55 | @Override 56 | public String getName() { 57 | return TAG; 58 | } 59 | 60 | /** 61 | * Based on ReactTextShadowNode.java 62 | */ 63 | @SuppressWarnings("unused") 64 | @ReactMethod 65 | public void measure(@Nullable final ReadableMap specs, final Promise promise) { 66 | final RNTextSizeConf conf = getConf(specs, promise, true); 67 | if (conf == null) { 68 | return; 69 | } 70 | 71 | final String _text = conf.getString("text"); 72 | if (_text == null) { 73 | promise.reject(E_MISSING_TEXT, "Missing required text."); 74 | return; 75 | } 76 | 77 | final float density = getCurrentDensity(); 78 | final float width = conf.getWidth(density); 79 | final boolean includeFontPadding = conf.includeFontPadding; 80 | 81 | final WritableMap result = Arguments.createMap(); 82 | if (_text.isEmpty()) { 83 | result.putInt("width", 0); 84 | result.putDouble("height", minimalHeight(density, includeFontPadding)); 85 | result.putInt("lastLineWidth", 0); 86 | result.putInt("lineCount", 0); 87 | promise.resolve(result); 88 | return; 89 | } 90 | 91 | final SpannableString text = (SpannableString) RNTextSizeSpannedText 92 | .spannedFromSpecsAndText(mReactContext, conf, new SpannableString(_text)); 93 | 94 | final TextPaint textPaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); 95 | Layout layout = null; 96 | try { 97 | final BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint); 98 | int hintWidth = (int) width; 99 | 100 | if (boring == null) { 101 | // Not boring, ie. the text is multiline or contains unicode characters. 102 | final float desiredWidth = Layout.getDesiredWidth(text, textPaint); 103 | if (desiredWidth <= width) { 104 | hintWidth = (int) Math.ceil(desiredWidth); 105 | } 106 | } else if (boring.width <= width) { 107 | // Single-line and width unknown or bigger than the width of the text. 108 | layout = BoringLayout.make( 109 | text, 110 | textPaint, 111 | boring.width, 112 | Layout.Alignment.ALIGN_NORMAL, 113 | SPACING_MULTIPLIER, 114 | SPACING_ADDITION, 115 | boring, 116 | includeFontPadding); 117 | } 118 | 119 | if (layout == null) { 120 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 121 | layout = StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, hintWidth) 122 | .setAlignment(Layout.Alignment.ALIGN_NORMAL) 123 | .setBreakStrategy(conf.getTextBreakStrategy()) 124 | .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) 125 | .setIncludePad(includeFontPadding) 126 | .setLineSpacing(SPACING_ADDITION, SPACING_MULTIPLIER) 127 | .build(); 128 | } else { 129 | layout = new StaticLayout( 130 | text, 131 | textPaint, 132 | hintWidth, 133 | Layout.Alignment.ALIGN_NORMAL, 134 | SPACING_MULTIPLIER, 135 | SPACING_ADDITION, 136 | includeFontPadding 137 | ); 138 | } 139 | } 140 | 141 | final int lineCount = layout.getLineCount(); 142 | float rectWidth; 143 | 144 | if (conf.getBooleanOrTrue("usePreciseWidth")) { 145 | float lastWidth = 0f; 146 | // Layout.getWidth() returns the configured max width, we must 147 | // go slow to get the used one (and with the text trimmed). 148 | rectWidth = 0f; 149 | for (int i = 0; i < lineCount; i++) { 150 | lastWidth = layout.getLineMax(i); 151 | if (lastWidth > rectWidth) { 152 | rectWidth = lastWidth; 153 | } 154 | } 155 | result.putDouble("lastLineWidth", lastWidth / density); 156 | } else { 157 | rectWidth = layout.getWidth(); 158 | } 159 | 160 | result.putDouble("width", Math.min(rectWidth / density, width)); 161 | result.putDouble("height", layout.getHeight() / density); 162 | result.putInt("lineCount", lineCount); 163 | 164 | Integer lineInfoForLine = conf.getIntOrNull("lineInfoForLine"); 165 | if (lineInfoForLine != null && lineInfoForLine >= 0) { 166 | final int line = Math.min(lineInfoForLine, lineCount); 167 | final WritableMap info = Arguments.createMap(); 168 | info.putInt("line", line); 169 | info.putInt("start", layout.getLineStart(line)); 170 | info.putInt("end", layout.getLineVisibleEnd(line)); 171 | info.putDouble("bottom", layout.getLineBottom(line) / density); 172 | info.putDouble("width", layout.getLineMax(line) / density); 173 | result.putMap("lineInfo", info); 174 | } 175 | 176 | promise.resolve(result); 177 | } catch (Exception e) { 178 | promise.reject(E_UNKNOWN_ERROR, e); 179 | } 180 | } 181 | 182 | // https://stackoverflow.com/questions/3654321/measuring-text-height-to-be-drawn-on-canvas-android 183 | @SuppressWarnings("unused") 184 | @ReactMethod 185 | public void flatHeights(@Nullable final ReadableMap specs, final Promise promise) { 186 | final RNTextSizeConf conf = getConf(specs, promise, true); 187 | if (conf == null) { 188 | return; 189 | } 190 | 191 | final ReadableArray texts = conf.getArray("text"); 192 | if (texts == null) { 193 | promise.reject(E_MISSING_TEXT, "Missing required text, must be an array."); 194 | return; 195 | } 196 | 197 | final float density = getCurrentDensity(); 198 | final float width = conf.getWidth(density); 199 | final boolean includeFontPadding = conf.includeFontPadding; 200 | final int textBreakStrategy = conf.getTextBreakStrategy(); 201 | 202 | final WritableArray result = Arguments.createArray(); 203 | 204 | final SpannableStringBuilder sb = new SpannableStringBuilder(" "); 205 | RNTextSizeSpannedText.spannedFromSpecsAndText(mReactContext, conf, sb); 206 | 207 | final TextPaint textPaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); 208 | Layout layout; 209 | try { 210 | 211 | for (int ix = 0; ix < texts.size(); ix++) { 212 | 213 | // If this element is `null` or another type, return zero 214 | if (texts.getType(ix) != ReadableType.String) { 215 | result.pushInt(0); 216 | continue; 217 | } 218 | 219 | final String text = texts.getString(ix); 220 | 221 | // If empty, return the minimum height of components 222 | if (text.isEmpty()) { 223 | result.pushDouble(minimalHeight(density, includeFontPadding)); 224 | continue; 225 | } 226 | 227 | // Reset the SB text, the attrs will expand to its full length 228 | sb.replace(0, sb.length(), text); 229 | 230 | if (Build.VERSION.SDK_INT >= 23) { 231 | layout = StaticLayout.Builder.obtain(sb, 0, sb.length(), textPaint, (int) width) 232 | .setAlignment(Layout.Alignment.ALIGN_NORMAL) 233 | .setBreakStrategy(textBreakStrategy) 234 | .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) 235 | .setIncludePad(includeFontPadding) 236 | .setLineSpacing(SPACING_ADDITION, SPACING_MULTIPLIER) 237 | .build(); 238 | } else { 239 | layout = new StaticLayout( 240 | sb, 241 | textPaint, 242 | (int) width, 243 | Layout.Alignment.ALIGN_NORMAL, 244 | SPACING_MULTIPLIER, 245 | SPACING_ADDITION, 246 | includeFontPadding 247 | ); 248 | } 249 | 250 | result.pushDouble(layout.getHeight() / density); 251 | } 252 | 253 | promise.resolve(result); 254 | } catch (Exception e) { 255 | promise.reject(E_UNKNOWN_ERROR, e); 256 | } 257 | } 258 | 259 | /** 260 | * See https://material.io/design/typography/#type-scale 261 | */ 262 | @SuppressWarnings("unused") 263 | @ReactMethod 264 | public void specsForTextStyles(final Promise promise) { 265 | WritableMap result = Arguments.createMap(); 266 | 267 | result.putMap("h1", makeFontSpecs("-light", 96, -1.5)); 268 | result.putMap("h2", makeFontSpecs("-light", 60, -0.5)); 269 | result.putMap("h3", makeFontSpecs(null, 48, 0)); 270 | result.putMap("h4", makeFontSpecs(null, 34, 0.25)); 271 | result.putMap("h5", makeFontSpecs(null, 24, 0)); 272 | result.putMap("h6", makeFontSpecs("-medium", 20, 0.15)); 273 | result.putMap("subtitle1", makeFontSpecs(null, 16, 0.15)); 274 | result.putMap("subtitle2", makeFontSpecs("-medium", 14, 0.1)); 275 | result.putMap("body1", makeFontSpecs(null, 16, 0.5)); 276 | result.putMap("body2", makeFontSpecs(null, 14, 0.25)); 277 | result.putMap("button", makeFontSpecs("-medium", 14, 0.75, true)); 278 | result.putMap("caption", makeFontSpecs(null, 12, 0.4)); 279 | result.putMap("overline", makeFontSpecs(null, 10, 1.5, true)); 280 | 281 | promise.resolve(result); 282 | } 283 | 284 | /** 285 | * https://stackoverflow.com/questions/27631736 286 | * /meaning-of-top-ascent-baseline-descent-bottom-and-leading-in-androids-font 287 | */ 288 | @SuppressWarnings("unused") 289 | @ReactMethod 290 | public void fontFromSpecs(@Nullable final ReadableMap specs, final Promise promise) { 291 | final RNTextSizeConf conf = getConf(specs, promise); 292 | if (conf == null) { 293 | return; 294 | } 295 | final Typeface typeface = RNTextSizeConf.getFont(mReactContext, conf.fontFamily, conf.fontStyle); 296 | final TextPaint textPaint = sTextPaintInstance; 297 | final int fontSize = (int) Math.ceil(conf.scale(conf.fontSize)); 298 | 299 | textPaint.reset(); 300 | textPaint.setTypeface(typeface); 301 | textPaint.setTextSize(fontSize); 302 | 303 | promise.resolve(fontInfoFromTypeface(textPaint, typeface, conf)); 304 | } 305 | 306 | @SuppressWarnings("unused") 307 | @ReactMethod 308 | public void fontFamilyNames(final Promise promise) { 309 | final boolean lollipop = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; 310 | final WritableArray names = Arguments.createArray(); 311 | 312 | names.pushString("sans-serif"); 313 | names.pushString("sans-serif-condensed"); 314 | if (lollipop) { 315 | names.pushString("sans-serif-thin"); 316 | names.pushString("sans-serif-light"); 317 | names.pushString("sans-serif-medium"); 318 | names.pushString("sans-serif-black"); 319 | names.pushString("sans-serif-smallcaps"); 320 | names.pushString("sans-serif-condensed-light"); 321 | } else { 322 | // SDK 16 323 | names.pushString("sans-serif-light"); 324 | } 325 | names.pushString("serif"); 326 | names.pushString("monospace"); 327 | if (lollipop) { 328 | names.pushString("serif-monospace"); 329 | names.pushString("casual"); 330 | names.pushString("cursive"); 331 | } 332 | 333 | getFontsInAssets(names); 334 | promise.resolve(names); 335 | } 336 | 337 | /** 338 | * Android does not have font name info. 339 | */ 340 | @SuppressWarnings("unused") 341 | @ReactMethod 342 | public void fontNamesForFamilyName(final String ignored, final Promise promise) { 343 | promise.resolve(null); 344 | } 345 | 346 | // ============================================================================ 347 | // 348 | // Non-exposed instance & static methods 349 | // 350 | // ============================================================================ 351 | 352 | @Nullable 353 | private RNTextSizeConf getConf(final ReadableMap specs, final Promise promise, boolean forText) { 354 | if (specs == null) { 355 | promise.reject(E_MISSING_PARAMETER, "Missing parameter object."); 356 | return null; 357 | } 358 | return new RNTextSizeConf(specs, forText); 359 | } 360 | 361 | @Nullable 362 | private RNTextSizeConf getConf(final ReadableMap specs, final Promise promise) { 363 | return getConf(specs, promise, false); 364 | } 365 | 366 | /** 367 | * RN consistently sets the height at 14dp divided by the density 368 | * plus 1 if includeFontPadding when text is empty, so we do the same. 369 | */ 370 | private double minimalHeight(final float density, final boolean includeFontPadding) { 371 | final double height = 14.0 / density; 372 | return includeFontPadding ? height + 1.0 : height; 373 | } 374 | 375 | /** 376 | * This is for 'fontFromFontStyle', makes the minimal info required. 377 | * @param suffix The font variant 378 | * @param fontSize Font size in SP 379 | * @param letterSpacing Sugest this to user 380 | * @return map with specs 381 | */ 382 | private WritableMap makeFontSpecs(String suffix, int fontSize, double letterSpacing, boolean upcase) { 383 | final WritableMap map = Arguments.createMap(); 384 | final String roboto = "sans-serif"; 385 | 386 | // In Android, the fontFamily determines the weight 387 | map.putString("fontFamily", suffix != null ? (roboto + suffix) : roboto); 388 | map.putInt("fontSize", fontSize); 389 | 390 | if (RNTextSizeConf.supportLetterSpacing()) { 391 | map.putDouble("letterSpacing", letterSpacing); 392 | } 393 | 394 | if (upcase && RNTextSizeConf.supportUpperCaseTransform()) { 395 | map.putString("textTransform", "uppercase"); 396 | } 397 | 398 | return map; 399 | } 400 | 401 | private WritableMap makeFontSpecs(String suffix, int fontSize, double letterSpacing) { 402 | return makeFontSpecs(suffix, fontSize, letterSpacing, false); 403 | } 404 | 405 | @Nonnull 406 | private WritableMap fontInfoFromTypeface( 407 | @Nonnull final TextPaint textPaint, 408 | @Nonnull final Typeface typeface, 409 | @Nonnull final RNTextSizeConf conf 410 | ) { 411 | // Info is always in unscaled values 412 | final float density = getCurrentDensity(); 413 | final Paint.FontMetrics metrics = new Paint.FontMetrics(); 414 | final float lineHeight = textPaint.getFontMetrics(metrics); 415 | 416 | final WritableMap info = Arguments.createMap(); 417 | info.putString("fontFamily", conf.getString("fontFamily")); 418 | info.putString("fontWeight", typeface.isBold() ? "bold" : "normal"); 419 | info.putString("fontStyle", typeface.isItalic() ? "italic" : "normal"); 420 | info.putDouble("fontSize", textPaint.getTextSize() / density); 421 | info.putDouble("leading", metrics.leading / density); 422 | info.putDouble("ascender", metrics.ascent / density); 423 | info.putDouble("descender", metrics.descent / density); 424 | info.putDouble("top", metrics.top / density); 425 | info.putDouble("bottom", metrics.bottom / density); 426 | info.putDouble("lineHeight", lineHeight / density); 427 | info.putInt("_hash", typeface.hashCode()); 428 | return info; 429 | } 430 | 431 | /** 432 | * Retuns the current density. 433 | */ 434 | @SuppressWarnings("deprecation") 435 | private float getCurrentDensity() { 436 | return DisplayMetricsHolder.getWindowDisplayMetrics().density; 437 | } 438 | 439 | private static final String[] FILE_EXTENSIONS = {".ttf", ".otf"}; 440 | private static final String FONTS_ASSET_PATH = "fonts"; 441 | 442 | private String[] fontsInAssets = null; 443 | 444 | /** 445 | * Set the font names in assets/fonts into the target array. 446 | * @param destArr Target 447 | */ 448 | private void getFontsInAssets(@Nonnull WritableArray destArr) { 449 | String[] srcArr = fontsInAssets; 450 | 451 | if (srcArr == null) { 452 | final AssetManager assetManager = mReactContext.getAssets(); 453 | ArrayList tmpArr = new ArrayList<>(); 454 | 455 | if (assetManager != null) { 456 | try { 457 | String[] list = assetManager.list(FONTS_ASSET_PATH); 458 | 459 | if (list != null) { 460 | for (String spec : list) { 461 | addFamilyToArray(tmpArr, spec); 462 | } 463 | } 464 | } catch (IOException ex) { 465 | ex.printStackTrace(); 466 | } 467 | } 468 | 469 | Collections.sort(tmpArr, String.CASE_INSENSITIVE_ORDER); 470 | fontsInAssets = srcArr = tmpArr.toArray(new String[0]); 471 | } 472 | 473 | for (String name : srcArr) { 474 | destArr.pushString(name); 475 | } 476 | } 477 | 478 | private void addFamilyToArray( 479 | @Nonnull final List outArr, 480 | @Nonnull final String spec 481 | ) { 482 | for (String ext : FILE_EXTENSIONS) { 483 | if (spec.endsWith(ext)) { 484 | final String name = spec.substring(0, spec.length() - ext.length()); 485 | 486 | if (!outArr.contains(name)) { 487 | outArr.add(name); 488 | } 489 | break; 490 | } 491 | } 492 | } 493 | } 494 | -------------------------------------------------------------------------------- /android/src/main/java/com/github/amarcruz/rntextsize/RNTextSizePackage.java: -------------------------------------------------------------------------------- 1 | package com.github.amarcruz.rntextsize; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.facebook.react.bridge.JavaScriptModule; 5 | import com.facebook.react.bridge.NativeModule; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.react.uimanager.ViewManager; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | @SuppressWarnings("unused") 14 | public class RNTextSizePackage implements ReactPackage { 15 | 16 | @Override 17 | public List createNativeModules(ReactApplicationContext reactContext) { 18 | List modules = new ArrayList<>(); 19 | modules.add(new RNTextSizeModule(reactContext)); 20 | return modules; 21 | } 22 | 23 | public List> createJSModules() { 24 | return null; 25 | } 26 | 27 | @Override 28 | public List createViewManagers(ReactApplicationContext reactContext) { 29 | return Collections.emptyList(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /android/src/main/java/com/github/amarcruz/rntextsize/RNTextSizeSpannedText.java: -------------------------------------------------------------------------------- 1 | package com.github.amarcruz.rntextsize; 2 | 3 | import android.annotation.TargetApi; 4 | import android.graphics.Typeface; 5 | import android.os.Build; 6 | import android.text.Spannable; 7 | import android.text.TextPaint; 8 | import android.text.style.AbsoluteSizeSpan; 9 | import android.text.style.MetricAffectingSpan; 10 | 11 | import com.facebook.react.bridge.ReactApplicationContext; 12 | 13 | import javax.annotation.Nonnull; 14 | 15 | final class RNTextSizeSpannedText { 16 | 17 | RNTextSizeSpannedText() {} 18 | 19 | static Spannable spannedFromSpecsAndText( 20 | @Nonnull final ReactApplicationContext context, 21 | @Nonnull final RNTextSizeConf conf, 22 | @Nonnull final Spannable text 23 | ) { 24 | 25 | //final SpannableString str = new SpannableString(text); 26 | final int end = text.length(); 27 | 28 | // Actual order of calling {@code execute} does NOT matter, 29 | // but the {@code priority} DOES matter (higher numbers go first). 30 | int priority = -1; 31 | 32 | // Lowest priority 33 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 34 | if (!Float.isNaN(conf.letterSpacing)) { 35 | final float letterSpacing = conf.scale(conf.letterSpacing); 36 | priority++; 37 | setSpanOperation(text, end, priority, 38 | new CustomLetterSpacingSpan(letterSpacing)); 39 | } 40 | } 41 | 42 | final int fontSize = (int) Math.ceil(conf.scale(conf.fontSize)); 43 | priority++; 44 | setSpanOperation(text, end, priority, new AbsoluteSizeSpan(fontSize)); 45 | 46 | if (conf.fontFamily != null || conf.has("fontStyle") || conf.has("fontWeight")) { 47 | priority++; 48 | setSpanOperation(text, end, priority, 49 | new CustomStyleSpan(RNTextSizeConf.getFont(context, conf.fontFamily, conf.fontStyle))); 50 | } 51 | 52 | return text; 53 | } 54 | 55 | private static void setSpanOperation( 56 | Spannable str, 57 | int end, 58 | int priority, 59 | Object span 60 | ) { 61 | // Here all spans will automatically extend from the start to the end of the text. 62 | int spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE; 63 | spanFlags |= (priority << Spannable.SPAN_PRIORITY_SHIFT) & Spannable.SPAN_PRIORITY; 64 | 65 | str.setSpan(span, 0, end, spanFlags); 66 | } 67 | 68 | /** 69 | * A {@link MetricAffectingSpan} that allows to set the letter spacing 70 | * on the selected text span. 71 | * 72 | * The letter spacing is specified in pixels, which are converted to 73 | * ems at paint time; this span must therefore be applied after any 74 | * spans affecting font size. 75 | */ 76 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 77 | private static class CustomLetterSpacingSpan extends MetricAffectingSpan { 78 | 79 | private final float mLetterSpacing; 80 | 81 | CustomLetterSpacingSpan(float letterSpacing) { 82 | mLetterSpacing = letterSpacing; 83 | } 84 | 85 | @Override 86 | public void updateDrawState(TextPaint paint) { 87 | apply(paint); 88 | } 89 | 90 | @Override 91 | public void updateMeasureState(@Nonnull TextPaint paint) { 92 | apply(paint); 93 | } 94 | 95 | private void apply(TextPaint paint) { 96 | paint.setLetterSpacing(mLetterSpacing / paint.getTextSize()); 97 | } 98 | } 99 | 100 | /** 101 | * Try to load the fontFamily with the right style and weight from the 102 | * assets. 103 | */ 104 | private static class CustomStyleSpan extends MetricAffectingSpan { 105 | 106 | private final Typeface mTypeface; 107 | 108 | CustomStyleSpan(final Typeface typeface) { 109 | mTypeface = typeface; 110 | } 111 | 112 | @Override 113 | public void updateDrawState(TextPaint ds) { 114 | apply(ds, mTypeface); 115 | } 116 | 117 | @Override 118 | public void updateMeasureState(@Nonnull TextPaint paint) { 119 | apply(paint, mTypeface); 120 | } 121 | 122 | private static void apply(TextPaint paint, final Typeface typeface) { 123 | paint.setTypeface(typeface); 124 | } 125 | 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-native-text-size" { 2 | 3 | export type TSFontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' 4 | export type TSFontStyle = 'normal' | 'italic' 5 | export type TSFontVariant = 'small-caps' | 'oldstyle-nums' | 'lining-nums' | 'tabular-nums' | 'proportional-nums' 6 | export type TSTextBreakStrategy = 'simple' | 'highQuality' | 'balanced' 7 | 8 | export type TSFontSize = { 9 | readonly default: number, 10 | readonly button: number, 11 | readonly label: number, 12 | readonly smallSystem: number, 13 | readonly system: number, 14 | } 15 | 16 | export type TSMDStyleSpec = 17 | | 'h1' 18 | | 'h2' 19 | | 'h3' 20 | | 'h4' 21 | | 'h5' 22 | | 'h6' 23 | | 'subtitle1' 24 | | 'subtitle2' 25 | | 'body1' 26 | | 'body2' 27 | | 'button' 28 | | 'caption' 29 | | 'overline' 30 | 31 | export type TSTextStyle = 32 | | 'body' 33 | | 'callout' 34 | | 'caption1' 35 | | 'caption2' 36 | | 'footnote' 37 | | 'headline' 38 | | 'subheadline' 39 | | 'largeTitle' 40 | | 'title1' 41 | | 'title2' 42 | | 'title3' 43 | 44 | export type TSFontInfo = { 45 | fontFamily: string | null, 46 | fontName?: string | null, 47 | fontWeight: TSFontWeight, 48 | fontSize: number, 49 | fontStyle: TSFontStyle, 50 | fontVariant?: TSFontVariant | null, 51 | ascender: number, 52 | descender: number, 53 | capHeight?: number, 54 | xHeight?: number, 55 | top?: number, 56 | bottom?: number, 57 | leading: number, 58 | lineHeight: number, 59 | _hash: number, 60 | } 61 | 62 | export interface TSFontSpecs { 63 | fontFamily?: string; 64 | fontSize?: number; 65 | fontStyle?: TSFontStyle; 66 | fontWeight?: TSFontWeight; 67 | /** @platform ios */ 68 | fontVariant?: Array; 69 | /** iOS all, Android SDK 21+ with RN 0.55+ */ 70 | letterSpacing?: number; 71 | /** @platform android */ 72 | includeFontPadding?: boolean; 73 | /** @platform android (SDK 23+) */ 74 | textBreakStrategy?: TSTextBreakStrategy; 75 | } 76 | 77 | export type TSFontForStyle = { 78 | fontFamily: string, 79 | /** Unscaled font size, untits are SP in Android, points in iOS */ 80 | fontSize: number, 81 | /** fontStyle is omitted if it is "normal" */ 82 | fontStyle?: TSFontStyle, 83 | /** fontWeight is omitted if it is "normal" */ 84 | fontWeight?: TSFontWeight, 85 | /** @platform ios */ 86 | fontVariant?: Array | null, 87 | /** iOS all, Android SDK 21+ with RN 0.55+ */ 88 | letterSpacing?: number, 89 | } 90 | 91 | export interface TSHeightsParams extends TSFontSpecs { 92 | /** The required text to measure. */ 93 | text: Array; 94 | /** Maximum width of the area to display the text. @default MAX_INT */ 95 | width?: number; 96 | /** @default true */ 97 | allowFontScaling?: boolean; 98 | } 99 | 100 | export interface TSMeasureParams extends TSFontSpecs { 101 | /** 102 | * This is the only required parameter and may include _emojis_ or be empty, 103 | * but it **must not be** `null`. 104 | * 105 | * If this is an empty string the resulting `width` will be zero. 106 | */ 107 | text: string; 108 | /** 109 | * Restrict the width. The resulting height will vary depending on the 110 | * automatic flow of the text. 111 | * @default MAX_INT 112 | */ 113 | width?: number; 114 | /** 115 | * To respect the user' setting of large fonts (i.e. use SP units). 116 | * @default true 117 | */ 118 | allowFontScaling?: boolean; 119 | /** 120 | * If `true`, the result will include an exact `width` and the 121 | * `lastLineWidth` property. 122 | * @default false 123 | */ 124 | usePreciseWidth?: boolean; 125 | /** 126 | * If `>=0`, the result will include a `lineInfo` property with information 127 | * for the required line number. 128 | */ 129 | lineInfoForLine?: number; 130 | } 131 | 132 | export type TSMeasureResult = { 133 | /** 134 | * Total used width. It may be less or equal to the `width` option. 135 | * 136 | * On Android, this value may vary depending on the `usePreciseWidth` flag. 137 | */ 138 | width: number; 139 | /** 140 | * Total height, including top and bottom padding if `includingFontPadding` 141 | * was set (the default). 142 | */ 143 | height: number; 144 | /** 145 | * Width of the last line, without trailing blanks. 146 | * 147 | * If `usePreciseWidth` is `false` (the default), this field is undefined. 148 | */ 149 | lastLineWidth?: number; 150 | /** 151 | * Number of lines, taking into account hard and automatic line breaks. 152 | */ 153 | lineCount: number; 154 | /** 155 | * Line information, if the `lineInfoForLine` option is given. 156 | */ 157 | lineInfo?: { 158 | /** Line number of this info, base 0. 159 | * 160 | * It can be less than the requested line number if `lineInfoForLine` is out of range. 161 | */ 162 | line: number; 163 | /** Text offset of the beginning of this line. */ 164 | start: number; 165 | /** Text offset after the last _visible_ character (so whitespace is not counted) on this line. */ 166 | end: number; 167 | /** The vertical position of the bottom of this line, including padding. */ 168 | bottom: number; 169 | /** Horizontal extent of this line, including leading margin indent, but excluding trailing whitespace. */ 170 | width: number; 171 | }; 172 | } 173 | 174 | interface TextSizeStatic { 175 | measure(params: TSMeasureParams): Promise; 176 | flatHeights(params: TSHeightsParams): Promise; 177 | specsForTextStyles(): Promise<{ [key: string]: TSFontForStyle }>; 178 | fontFromSpecs(specs?: TSFontSpecs): Promise; 179 | fontFamilyNames(): Promise; 180 | } 181 | 182 | const TextSize: TextSizeStatic; 183 | export default TextSize; 184 | } 185 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { NativeModules } from 'react-native' 2 | export default NativeModules.RNTextSize 3 | -------------------------------------------------------------------------------- /index.js.flow: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export type TSFontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' 4 | export type TSFontStyle = 'normal' | 'italic' 5 | export type TSFontVariant = 'small-caps' | 'oldstyle-nums' | 'lining-nums' | 'tabular-nums' | 'proportional-nums' 6 | export type TSTextBreakStrategy = 'simple' | 'highQuality' | 'balanced' 7 | 8 | export type TSFontSize = {| 9 | +default: number, 10 | +button: number, 11 | +label: number, 12 | +smallSystem: number, 13 | +system: number, 14 | |} 15 | 16 | export type TSMDStyleSpec = 17 | | 'h1' 18 | | 'h2' 19 | | 'h3' 20 | | 'h4' 21 | | 'h5' 22 | | 'h6' 23 | | 'subtitle1' 24 | | 'subtitle2' 25 | | 'body1' 26 | | 'body2' 27 | | 'button' 28 | | 'caption' 29 | | 'overline' 30 | 31 | export type TSTextStyle = 32 | | 'body' 33 | | 'callout' 34 | | 'caption1' 35 | | 'caption2' 36 | | 'footnote' 37 | | 'headline' 38 | | 'subheadline' 39 | | 'largeTitle' 40 | | 'title1' 41 | | 'title2' 42 | | 'title3' 43 | 44 | export type TSFontInfo = { 45 | fontFamily: string | null, 46 | fontName: string | null, 47 | fontWeight: TSFontWeight, 48 | fontSize: number, 49 | fontStyle: TSFontStyle, 50 | fontVariant?: TSFontVariant | null, 51 | ascender: number, 52 | descender: number, 53 | capHeight?: number, 54 | xHeight?: number, 55 | top?: number, 56 | bottom?: number, 57 | leading: number, 58 | lineHeight: number, 59 | _hash: number, 60 | } 61 | 62 | export type TSFontSpecs = { 63 | fontFamily?: string, 64 | fontSize?: number, 65 | fontStyle?: TSFontStyle, 66 | fontWeight?: TSFontWeight, 67 | /** @platform ios */ 68 | fontVariant?: Array, 69 | /** iOS all, Android SDK 21+ with RN 0.55+ */ 70 | letterSpacing?: number, 71 | /** @platform android */ 72 | includeFontPadding?: boolean, 73 | /** @platform android (SDK 23+) */ 74 | textBreakStrategy?: TSTextBreakStrategy, 75 | } 76 | 77 | export type TSFontForStyle = { 78 | fontFamily: string, 79 | fontSize: number, 80 | /** fontStyle is omitted if it is "normal" */ 81 | fontStyle?: TSFontStyle, 82 | /** fontWeight is omitted if it is "normal" */ 83 | fontWeight?: TSFontWeight, 84 | /** @platform ios */ 85 | fontVariant?: Array | null, 86 | /** iOS all, Android SDK 21+ with RN 0.55+ */ 87 | letterSpacing?: number, 88 | } 89 | 90 | export type TSHeightsParams = TSFontSpecs & { 91 | /** The required text to measure. */ 92 | text: Array, 93 | /** Maximum width of the area to display the text. @default MAX_INT */ 94 | width?: number, 95 | /** @default true */ 96 | allowFontScaling?: boolean, 97 | } 98 | 99 | export type TSMeasureParams = TSFontSpecs & { 100 | /** 101 | * This is the only required parameter and may include _emojis_ or be empty, 102 | * but it **must not be** `null`. 103 | * 104 | * If this is an empty string the resulting `width` will be zero. 105 | */ 106 | text: string, 107 | /** 108 | * To respect the user' setting of large fonts (i.e. use SP units). 109 | * @default true 110 | */ 111 | allowFontScaling?: boolean, 112 | /** 113 | * Restrict the width. The resulting height will vary depending on the 114 | * automatic flow of the text. 115 | * @default MAX_INT 116 | */ 117 | width?: number, 118 | /** 119 | * If `true`, the result will include an exact `width` and the 120 | * `lastLineWidth` property. 121 | * @default false 122 | */ 123 | usePreciseWidth?: boolean, 124 | /** 125 | * If `>=0`, the result will include a `lineInfo` property with information 126 | * for the required line number. 127 | */ 128 | lineInfoForLine?: number, 129 | } 130 | 131 | export interface TSMeasureResult { 132 | /** 133 | * Total used width. It may be less or equal to the `width` option. 134 | * 135 | * On Android, this value may vary depending on the `usePreciseWidth` flag. 136 | */ 137 | width: number; 138 | /** 139 | * Total height, including top and bottom padding if `includingFontPadding` 140 | * was set (the default). 141 | */ 142 | height: number; 143 | /** 144 | * Width of the last line, without trailing blanks. 145 | * 146 | * If `usePreciseWidth` is `false` (the default), this field is undefined. 147 | */ 148 | lastLineWidth?: number; 149 | /** 150 | * Number of lines, taking into account hard and automatic line breaks. 151 | */ 152 | lineCount: number; 153 | /** 154 | * Line information, if the `lineInfoForLine` option is given. 155 | */ 156 | lineInfo?: { 157 | /** Line number of this info, base 0. 158 | * 159 | * It can be less than the requested line number if `lineInfoForLine` is out of range. 160 | */ 161 | line: number; 162 | /** Text offset of the beginning of this line. */ 163 | start: number; 164 | /** Text offset after the last _visible_ character (so whitespace is not counted) on this line. */ 165 | end: number; 166 | /** The vertical position of the bottom of this line, including padding. */ 167 | bottom: number; 168 | /** Horizontal extent of this line, including leading margin indent, but excluding trailing whitespace. */ 169 | width: number; 170 | } 171 | } 172 | 173 | declare interface TextSizeStatic { 174 | measure(params: TSMeasureParams): Promise; 175 | flatHeights(params: TSHeightsParams): Promise; 176 | specsForTextStyles(): Promise<{ [string]: TSFontForStyle }>; 177 | fontFromSpecs(specs: TSFontSpecs): Promise; 178 | fontFamilyNames(): Promise; 179 | } 180 | 181 | declare var TextSize: TextSizeStatic; 182 | export default TextSize; 183 | -------------------------------------------------------------------------------- /ios/RNTextSize.h: -------------------------------------------------------------------------------- 1 | #if __has_include() 2 | #import 3 | #import 4 | #else 5 | #import "React/RCTBridgeModule.h" 6 | #import "React/RCTAccessibilityManager.h" 7 | #endif 8 | 9 | @interface RNTextSize : NSObject 10 | @end 11 | -------------------------------------------------------------------------------- /ios/RNTextSize.m: -------------------------------------------------------------------------------- 1 | #import "RNTextSize.h" 2 | 3 | #if __has_include() 4 | #import 5 | #import 6 | #import 7 | #else 8 | #import "React/RCTConvert.h" // Required when used as a Pod in a Swift project 9 | #import "React/RCTFont.h" 10 | #import "React/RCTUtils.h" 11 | #endif 12 | 13 | #import 14 | 15 | static NSString *const E_MISSING_TEXT = @"E_MISSING_TEXT"; 16 | static NSString *const E_INVALID_FONT_SPEC = @"E_INVALID_FONT_SPEC"; 17 | static NSString *const E_INVALID_TEXTSTYLE = @"E_INVALID_TEXTSTYLE"; 18 | static NSString *const E_INVALID_FONTFAMILY = @"E_INVALID_FONTFAMILY"; 19 | 20 | static inline BOOL isNull(id str) { 21 | return !str || str == (id) kCFNull; 22 | } 23 | 24 | static inline CGFloat CGFloatValueFrom(NSNumber * _Nullable num) { 25 | #if CGFLOAT_IS_DOUBLE 26 | return num ? num.doubleValue : NAN; 27 | #else 28 | return num ? num.floatValue : NAN; 29 | #endif 30 | } 31 | 32 | #define A_SIZE(x) (sizeof (x)/sizeof (x)[0]) 33 | 34 | /* 35 | * 2018-08-14 by aMarCruz: First working version, tested in RN 0.56 36 | */ 37 | @implementation RNTextSize 38 | 39 | RCT_EXPORT_MODULE(); 40 | 41 | @synthesize bridge = _bridge; 42 | 43 | // Because the exported constants 44 | + (BOOL)requiresMainQueueSetup 45 | { 46 | return YES; 47 | } 48 | 49 | - (dispatch_queue_t)methodQueue { 50 | return dispatch_get_main_queue(); 51 | } 52 | 53 | /** 54 | * Gets the width, height, line count and last line width for the provided text 55 | * font specifications. 56 | * Based on `RCTTextShadowViewMeasure` of Libraries/Text/Text/RCTTextShadowView.m 57 | */ 58 | RCT_EXPORT_METHOD(measure:(NSDictionary * _Nullable)options 59 | resolver:(RCTPromiseResolveBlock)resolve 60 | rejecter:(RCTPromiseRejectBlock)reject) 61 | { 62 | // RCTConvert will return nil if the `options` object was not received. 63 | NSString *const _Nullable text = [RCTConvert NSString:options[@"text"]]; 64 | if (isNull(text)) { 65 | reject(E_MISSING_TEXT, @"Missing required text.", nil); 66 | return; 67 | } 68 | 69 | // Allow empty text without generating error 70 | // ~~TODO~~: Return the same height as RN. @completed(v2.0.1) 71 | if (!text.length) { 72 | resolve(@{ 73 | @"width": @0, 74 | @"height": @14, 75 | @"lastLineWidth": @0, 76 | @"lineCount": @0, 77 | }); 78 | return; 79 | } 80 | 81 | // We cann't use RCTConvert since it does not handle font scaling and RN 82 | // does not scale the font if a custom delegate has been defined to create. 83 | UIFont *const _Nullable font = [self scaledUIFontFromUserSpecs:options]; 84 | if (!font) { 85 | reject(E_INVALID_FONT_SPEC, @"Invalid font specification.", nil); 86 | return; 87 | } 88 | 89 | // Allow the user to specify the width or height (both optionals). 90 | const CGFloat optWidth = CGFloatValueFrom(options[@"width"]); 91 | const CGFloat maxWidth = isnan(optWidth) || isinf(optWidth) ? CGFLOAT_MAX : optWidth; 92 | const CGSize maxSize = CGSizeMake(maxWidth, CGFLOAT_MAX); 93 | 94 | // Create attributes for the font and the optional letter spacing. 95 | const CGFloat letterSpacing = CGFloatValueFrom(options[@"letterSpacing"]); 96 | NSDictionary *const attributes = isnan(letterSpacing) 97 | ? @{NSFontAttributeName: font} 98 | : @{NSFontAttributeName: font, NSKernAttributeName: @(letterSpacing)}; 99 | 100 | NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:maxSize]; 101 | textContainer.lineFragmentPadding = 0.0; 102 | textContainer.lineBreakMode = NSLineBreakByClipping; // no maxlines support 103 | 104 | NSLayoutManager *layoutManager = [NSLayoutManager new]; 105 | [layoutManager addTextContainer:textContainer]; 106 | layoutManager.allowsNonContiguousLayout = YES; // 'cause lastLineWidth 107 | 108 | NSTextStorage *textStorage = [[NSTextStorage alloc] initWithString:text attributes:attributes]; 109 | [textStorage addLayoutManager:layoutManager]; 110 | 111 | [layoutManager ensureLayoutForTextContainer:textContainer]; 112 | CGSize size = [layoutManager usedRectForTextContainer:textContainer].size; 113 | if (!isnan(letterSpacing) && letterSpacing < 0) { 114 | size.width -= letterSpacing; 115 | } 116 | 117 | const CGFloat epsilon = 0.001; 118 | const CGFloat width = MIN(RCTCeilPixelValue(size.width + epsilon), maxSize.width); 119 | const CGFloat height = MIN(RCTCeilPixelValue(size.height + epsilon), maxSize.height); 120 | const NSInteger lineCount = [self getLineCount:layoutManager]; 121 | 122 | NSMutableDictionary *result = [[NSMutableDictionary alloc] 123 | initWithObjectsAndKeys:@(width), @"width", 124 | @(height), @"height", 125 | @(lineCount), @"lineCount", 126 | nil]; 127 | 128 | if ([options[@"usePreciseWidth"] boolValue]) { 129 | const CGFloat lastIndex = layoutManager.numberOfGlyphs - 1; 130 | const CGSize lastSize = [layoutManager lineFragmentUsedRectForGlyphAtIndex:lastIndex 131 | effectiveRange:nil].size; 132 | [result setValue:@(lastSize.width) forKey:@"lastLineWidth"]; 133 | } 134 | 135 | const CGFloat optLine = CGFloatValueFrom(options[@"lineInfoForLine"]); 136 | if (!isnan(optLine) && optLine >= 0) { 137 | const NSInteger line = MIN((NSInteger) optLine, lineCount); 138 | NSDictionary *lineInfo = [self getLineInfo:layoutManager str:text lineNo:line]; 139 | if (lineInfo) { 140 | [result setValue:lineInfo forKey:@"lineInfo"]; 141 | } 142 | } 143 | 144 | resolve(result); 145 | } 146 | 147 | /** 148 | * Gets the width, height, line count and last line width for the provided text 149 | * font specifications. 150 | * Based on `RCTTextShadowViewMeasure` of Libraries/Text/Text/RCTTextShadowView.m 151 | */ 152 | RCT_EXPORT_METHOD(flatHeights:(NSDictionary * _Nullable)options 153 | resolver:(RCTPromiseResolveBlock)resolve 154 | rejecter:(RCTPromiseRejectBlock)reject) 155 | { 156 | // Don't use NSStringArray, we are handling nulls 157 | NSArray *const _Nullable texts = [RCTConvert NSArray:options[@"text"]]; 158 | if (isNull(texts)) { 159 | reject(E_MISSING_TEXT, @"Missing required text, must be an array.", nil); 160 | return; 161 | } 162 | 163 | UIFont *const _Nullable font = [self scaledUIFontFromUserSpecs:options]; 164 | if (!font) { 165 | reject(E_INVALID_FONT_SPEC, @"Invalid font specification.", nil); 166 | return; 167 | } 168 | 169 | const CGFloat optWidth = CGFloatValueFrom(options[@"width"]); 170 | const CGFloat maxWidth = isnan(optWidth) || isinf(optWidth) ? CGFLOAT_MAX : optWidth; 171 | const CGSize maxSize = CGSizeMake(maxWidth, CGFLOAT_MAX); 172 | 173 | // Create attributes for the font and the optional letter spacing. 174 | const CGFloat letterSpacing = CGFloatValueFrom(options[@"letterSpacing"]); 175 | NSDictionary *const attributes = isnan(letterSpacing) 176 | ? @{NSFontAttributeName: font} 177 | : @{NSFontAttributeName: font, NSKernAttributeName: @(letterSpacing)}; 178 | 179 | NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:maxSize]; 180 | textContainer.lineFragmentPadding = 0.0; 181 | textContainer.lineBreakMode = NSLineBreakByClipping; // no maxlines support 182 | 183 | NSLayoutManager *layoutManager = [NSLayoutManager new]; 184 | [layoutManager addTextContainer:textContainer]; 185 | 186 | NSTextStorage *textStorage = [[NSTextStorage alloc] initWithString:@" " attributes:attributes]; 187 | [textStorage addLayoutManager:layoutManager]; 188 | 189 | NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:texts.count]; 190 | const CGFloat epsilon = 0.001; 191 | 192 | for (int ix = 0; ix < texts.count; ix++) { 193 | NSString *text = texts[ix]; 194 | 195 | // If this element is `null` or another type, return zero 196 | if (![text isKindOfClass:[NSString class]]) { 197 | result[ix] = @0; 198 | continue; 199 | } 200 | 201 | // If empty, return the minimum height of components 202 | if (!text.length) { 203 | result[ix] = @14; 204 | continue; 205 | } 206 | 207 | // Reset the textStorage, the attrs will expand to its new length 208 | NSRange range = NSMakeRange(0, textStorage.length); 209 | [textStorage replaceCharactersInRange:range withString:text]; 210 | CGSize size = [layoutManager usedRectForTextContainer:textContainer].size; 211 | 212 | const CGFloat height = MIN(RCTCeilPixelValue(size.height + epsilon), maxSize.height); 213 | result[ix] = @(height); 214 | } 215 | 216 | resolve(result); 217 | } 218 | 219 | /** 220 | * Resolve with an object with info about a font built with the parameters provided by 221 | * the user. Rejects if the parameters are falsy or the font could not be created. 222 | */ 223 | RCT_EXPORT_METHOD(fontFromSpecs:(NSDictionary *)specs 224 | resolver:(RCTPromiseResolveBlock)resolve 225 | rejecter:(RCTPromiseRejectBlock)reject) 226 | { 227 | if (isNull(specs)) { 228 | reject(E_INVALID_FONT_SPEC, @"Missing font specification.", nil); 229 | } else { 230 | UIFont * _Nullable font = [self UIFontFromUserSpecs:specs withScale:1.0]; 231 | if (font) { 232 | resolve([self fontInfoFromUIFont:font]); 233 | } else { 234 | reject(E_INVALID_FONT_SPEC, @"Invalid font specification.", nil); 235 | } 236 | } 237 | } 238 | 239 | /** 240 | * Resolves with an array of font info for the predefined iOS Text Styles. 241 | * The returned size is "Large", the default following the iOS HIG. 242 | * 243 | * NOTE: The info includes unscaled fontSize and letterSpacing. fontSize is managed by 244 | * by RN `allowFontScaling`, but letterSpacing is not. 245 | * 246 | * Altough the technique used to get create the result is complicated to maintain, 247 | * it simplifies things a lot. 248 | */ 249 | RCT_EXPORT_METHOD(specsForTextStyles:(RCTPromiseResolveBlock)resolve 250 | rejecter:(RCTPromiseRejectBlock)reject) 251 | { 252 | // From https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/typography/ 253 | // These are the predefined kerning (1/1000em) to convert into letterSpacing (points) 254 | static const int T_OFFSET = 10; // tracking start with fontSize 10 255 | static const char trackings[] = { 256 | 12, 6, 0, -6, -11, -16, -20, -24, -25, -26, 257 | 19, 17, 16, 16, 15, 14, 14, 13, 13, 13, 258 | 12, 12, 12, 11, 11, 259 | }; 260 | // These are the names of the properties to return 261 | static char *keys[] = { 262 | "title1", "title2", "title3", "headline", 263 | "body", "callout", "subheadline", 264 | "footnote", "caption1", "caption2", 265 | "largeTitle", 266 | }; 267 | // These are the predefined fontSize values of the "Large" size set 268 | static char sizes[] = { 269 | 28, 22, 20, 17, 270 | 17, 16, 15, 271 | 13, 12, 11, 272 | 34, 273 | }; 274 | 275 | // The largeTitle style is avaiable from iOS 11 only 276 | UIFontTextStyle textStyleLargeTitle; 277 | int length = A_SIZE(keys); 278 | if (@available(iOS 11.0, *)) { 279 | textStyleLargeTitle = UIFontTextStyleLargeTitle; 280 | } else { 281 | textStyleLargeTitle = (id) [NSNull null]; 282 | length--; 283 | } 284 | 285 | // These are the keys for getting the info with UIKit's preferredFontForTextStyle 286 | // (I really don't know if we can use NSString* in static arrays). 287 | NSArray *textStyles = 288 | @[ 289 | UIFontTextStyleTitle1, UIFontTextStyleTitle2, UIFontTextStyleTitle3, UIFontTextStyleHeadline, 290 | UIFontTextStyleBody, UIFontTextStyleCallout, UIFontTextStyleSubheadline, 291 | UIFontTextStyleFootnote, UIFontTextStyleCaption1, UIFontTextStyleCaption2, 292 | textStyleLargeTitle, 293 | ]; 294 | 295 | // ...and with all in place, we are ready to create our result 296 | NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:[textStyles count]]; 297 | 298 | for (int ix = 0; ix < length; ix++) { 299 | const UIFontTextStyle textStyle = textStyles[ix]; 300 | 301 | const UIFont *font = [UIFont preferredFontForTextStyle:textStyle]; 302 | const UIFontDescriptor *descriptor = font.fontDescriptor; 303 | const NSDictionary *traits = [descriptor objectForKey:UIFontDescriptorTraitsAttribute]; 304 | 305 | const NSString *fontFamily = font.familyName ?: font.fontName ?: (id) [NSNull null]; 306 | const NSArray *fontVariant = [self fontVariantFromDescriptor:descriptor]; 307 | const NSString *fontStyle = [self fontStyleFromTraits:traits]; 308 | const NSString *fontWeight = [self fontWeightFromTraits:traits]; 309 | 310 | // The standard font size for this style is also used to calculate letterSpacing 311 | const int fontSize = sizes[ix]; 312 | const int index = fontSize - T_OFFSET; 313 | const int tracking = index >= 0 && index < A_SIZE(trackings) ? trackings[index] : 0; 314 | const CGFloat letterSpacing = fontSize * tracking / 1000.0; 315 | 316 | NSMutableDictionary *value = [[NSMutableDictionary alloc] 317 | initWithObjectsAndKeys:fontFamily, @"fontFamily", 318 | @(fontSize), @"fontSize", 319 | @(letterSpacing), @"letterSpacing", 320 | nil]; 321 | if (![fontWeight isEqualToString:@"normal"]) { 322 | [value setValue:fontWeight forKey:@"fontWeight"]; 323 | } 324 | if (![fontStyle isEqualToString:@"normal"]) { 325 | [value setValue:fontStyle forKey:@"fontStyle"]; 326 | } 327 | if (fontVariant) { 328 | [value setValue:fontVariant forKey:@"fontVariant"]; 329 | } 330 | 331 | [result setValue:value forKey:@(keys[ix])]; 332 | } 333 | 334 | resolve(result); 335 | } 336 | 337 | /** 338 | * Resolve with an array of font family names available on the system. 339 | */ 340 | RCT_EXPORT_METHOD(fontFamilyNames:(RCTPromiseResolveBlock)resolve 341 | rejecter:(RCTPromiseRejectBlock)reject) 342 | { 343 | NSArray *fonts = [UIFont.familyNames 344 | sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; 345 | resolve(fonts); 346 | } 347 | 348 | /** 349 | * Resolve with an array of font names available in a particular font family. 350 | * Reject if the name is falsy or the names could not be obtain. 351 | */ 352 | RCT_EXPORT_METHOD(fontNamesForFamilyName:(NSString * _Nullable)fontFamily 353 | resolver:(RCTPromiseResolveBlock)resolve 354 | rejecter:(RCTPromiseRejectBlock)reject) 355 | { 356 | if (isNull(fontFamily)) { 357 | reject(E_INVALID_FONTFAMILY, @"Missing fontFamily name.", nil); 358 | } else { 359 | NSArray *fontNames = [UIFont fontNamesForFamilyName:fontFamily]; 360 | if (fontNames) { 361 | resolve(UIFont.familyNames); 362 | } else { 363 | reject(E_INVALID_FONTFAMILY, @"Invalid fontFamily name.", nil); 364 | } 365 | } 366 | } 367 | 368 | // 369 | // ============================================================================ 370 | // Non-exposed instance & static methods 371 | // ============================================================================ 372 | // 373 | 374 | /** 375 | * Get extended info for a given line number. 376 | * @since v2.1.0 377 | */ 378 | - (NSInteger)getLineCount:(NSLayoutManager *)layoutManager { 379 | NSRange lineRange; 380 | NSUInteger glyphCount = layoutManager.numberOfGlyphs; 381 | NSInteger lineCount = 0; 382 | 383 | for (NSUInteger index = 0; index < glyphCount; lineCount++) { 384 | [layoutManager 385 | lineFragmentUsedRectForGlyphAtIndex:index effectiveRange:&lineRange withoutAdditionalLayout:YES]; 386 | index = NSMaxRange(lineRange); 387 | } 388 | 389 | return lineCount; 390 | } 391 | 392 | /** 393 | * Get extended info for a given line number. 394 | * @since v2.1.0 395 | */ 396 | - (NSDictionary *)getLineInfo:(NSLayoutManager *)layoutManager str:(NSString *)str lineNo:(NSInteger)line { 397 | CGRect lineRect = CGRectZero; 398 | NSRange lineRange; 399 | NSUInteger glyphCount = layoutManager.numberOfGlyphs; 400 | NSInteger lineCount = 0; 401 | 402 | for (NSUInteger index = 0; index < glyphCount; lineCount++) { 403 | lineRect = [layoutManager 404 | lineFragmentUsedRectForGlyphAtIndex:index 405 | effectiveRange:&lineRange 406 | withoutAdditionalLayout:YES]; 407 | index = NSMaxRange(lineRange); 408 | 409 | if (line == lineCount) { 410 | NSCharacterSet *ws = NSCharacterSet.whitespaceAndNewlineCharacterSet; 411 | NSRange charRange = [layoutManager characterRangeForGlyphRange:lineRange actualGlyphRange:nil]; 412 | NSUInteger start = charRange.location; 413 | index = NSMaxRange(charRange); 414 | /* 415 | Get the trimmed range of chars for the glyph range, to be consistent 416 | w/android, but the width here will include the trailing whitespace. 417 | */ 418 | while (index > start && [ws characterIsMember:[str characterAtIndex:index - 1]]) { 419 | index--; 420 | } 421 | return @{ 422 | @"line": @(line), 423 | @"start": @(start), 424 | @"end": @(index), 425 | @"bottom": @(lineRect.origin.y + lineRect.size.height), 426 | @"width": @(lineRect.size.width) 427 | }; 428 | } 429 | } 430 | 431 | return nil; 432 | } 433 | 434 | /** 435 | * Create a scaled font based on the given specs. 436 | * 437 | * TODO: 438 | * This method is used instead of [RCTConvert UIFont] to support the omission 439 | * of scaling when a custom delegate has been defined for font's creation. 440 | */ 441 | - (UIFont * _Nullable)scaledUIFontFromUserSpecs:(const NSDictionary *)specs 442 | { 443 | const id allowFontScalingSrc = specs[@"allowFontScaling"]; 444 | const BOOL allowFontScaling = allowFontScalingSrc ? [allowFontScalingSrc boolValue] : YES; 445 | const CGFloat scaleMultiplier = 446 | allowFontScaling && _bridge ? _bridge.accessibilityManager.multiplier : 1.0; 447 | 448 | return [self UIFontFromUserSpecs:specs withScale:scaleMultiplier]; 449 | } 450 | 451 | /** 452 | * Create a font based on the given specs. 453 | */ 454 | - (UIFont * _Nullable)UIFontFromUserSpecs:(const NSDictionary *)specs 455 | withScale:(CGFloat)scaleMultiplier 456 | { 457 | return [RCTFont updateFont:nil 458 | withFamily:[RCTConvert NSString:specs[@"fontFamily"]] 459 | size:[RCTConvert NSNumber:specs[@"fontSize"]] 460 | weight:[RCTConvert NSString:specs[@"fontWeight"]] 461 | style:[RCTConvert NSString:specs[@"fontStyle"]] 462 | variant:[RCTConvert NSStringArray:specs[@"fontVariant"]] 463 | scaleMultiplier:scaleMultiplier]; 464 | } 465 | 466 | /** 467 | * Create the font info that will be returned by other methods. 468 | * The keys in the returned dictionary are a superset of the RN Text styles 469 | * so the format is not fully compatible. 470 | */ 471 | - (NSDictionary *)fontInfoFromUIFont:(const UIFont *)font 472 | { 473 | const UIFontDescriptor *descriptor = font.fontDescriptor; 474 | const NSDictionary *traits = [descriptor objectForKey:UIFontDescriptorTraitsAttribute]; 475 | const NSArray *fontVariant = [self fontVariantFromDescriptor:descriptor]; 476 | 477 | return @{ 478 | @"fontFamily": RCTNullIfNil(font.familyName), 479 | @"fontName": RCTNullIfNil(font.fontName), 480 | @"fontSize": @(font.pointSize), 481 | @"fontStyle": [self fontStyleFromTraits:traits], 482 | @"fontWeight": [self fontWeightFromTraits:traits], 483 | @"fontVariant": RCTNullIfNil(fontVariant), 484 | @"ascender": @(font.ascender), 485 | @"descender": @(font.descender), 486 | @"capHeight": @(font.capHeight), // height of capital characters 487 | @"xHeight": @(font.xHeight), // height of lowercase "x" 488 | @"leading": @(font.leading), // additional space between lines 489 | @"lineHeight": @(font.lineHeight), 490 | @"_hash": @(font.hash), 491 | }; 492 | } 493 | 494 | /** 495 | * Reads the font weight of a trait and returns a string with the representation 496 | * of the weight in multiples of "100", as expected by RN, or one of the words 497 | * "bold" or "normal" if appropiate. 498 | * 499 | * @param trais NSDictionary with the traits of the font. 500 | * @return NSString with the weight of the font. 501 | */ 502 | - (NSString *)fontWeightFromTraits:(const NSDictionary *)traits 503 | { 504 | // Use a small tolerance to avoid rounding problems 505 | const CGFloat weight = CGFloatValueFrom(traits[UIFontWeightTrait]) + 0.01; 506 | 507 | return (weight >= UIFontWeightBlack) ? @"900" 508 | : (weight >= UIFontWeightHeavy) ? @"800" 509 | : (weight >= UIFontWeightBold) ? @"bold" 510 | : (weight >= UIFontWeightSemibold) ? @"600" 511 | : (weight >= UIFontWeightMedium) ? @"500" 512 | : (weight >= UIFontWeightRegular) ? @"normal" 513 | : (weight >= UIFontWeightLight) ? @"300" 514 | : (weight >= UIFontWeightThin) ? @"200" : @"100"; // UIFontWeightUltraLight 515 | } 516 | 517 | /** 518 | * Returns a string with the style found in the trait, either "normal" or "italic". 519 | * 520 | * @param trais NSDictionary with the traits of the font. 521 | * @return NSString with the style. 522 | */ 523 | - (NSString *)fontStyleFromTraits:(const NSDictionary *)traits 524 | { 525 | const UIFontDescriptorSymbolicTraits symbolicTrais = [traits[UIFontSymbolicTrait] unsignedIntValue]; 526 | const BOOL isItalic = (symbolicTrais & UIFontDescriptorTraitItalic) != 0; 527 | 528 | return isItalic ? @"italic" : @"normal"; 529 | } 530 | 531 | /** 532 | * Parses a font descriptor and returns a fontVariant array as expected by RN. 533 | * 534 | * @param descriptor with the features of the font 535 | * @return NSArray of NSString with variants, or nil if none was found. 536 | * 537 | * FIXME: 538 | * kNumberCase variants are not being recognized... RN bug? 539 | */ 540 | - (NSArray * _Nullable)fontVariantFromDescriptor:(const UIFontDescriptor *)descriptor 541 | { 542 | const NSArray *features = descriptor.fontAttributes[UIFontDescriptorFeatureSettingsAttribute]; 543 | if (isNull(features)) { 544 | return nil; 545 | } 546 | 547 | // Use a C array to store the result temporarily 548 | const NSString *outArr[features.count]; 549 | NSUInteger count = 0; 550 | 551 | for (NSDictionary *item in features) { 552 | const NSNumber *type = item[UIFontFeatureTypeIdentifierKey]; 553 | if (type) { 554 | const int value = (int) [item[UIFontFeatureSelectorIdentifierKey] longValue]; 555 | 556 | switch (type.integerValue) { 557 | case kLowerCaseType: 558 | if (value == kLowerCaseSmallCapsSelector) { 559 | outArr[count++] = @"small-caps"; 560 | } 561 | break; 562 | case kNumberCaseType: 563 | if (value == kLowerCaseNumbersSelector) { 564 | outArr[count++] = @"oldstyle-nums"; 565 | } else if (value == kUpperCaseNumbersSelector) { 566 | outArr[count++] = @"lining-nums"; 567 | } 568 | break; 569 | case kNumberSpacingType: 570 | if (value == kMonospacedNumbersSelector) { 571 | outArr[count++] = @"tabular-nums"; 572 | } else if (value == kProportionalNumbersSelector) { 573 | outArr[count++] = @"proportional-nums"; 574 | } 575 | break; 576 | } 577 | } 578 | } 579 | 580 | // Returns an array only if found variants, to preserve memory 581 | return count ? [NSArray arrayWithObjects:outArr count:count] : nil; 582 | } 583 | 584 | @end 585 | -------------------------------------------------------------------------------- /ios/RNTextSize.podspec: -------------------------------------------------------------------------------- 1 | require 'json' 2 | package = JSON.parse(File.read('../package.json')) 3 | 4 | Pod::Spec.new do |s| 5 | s.name = 'RNTextSize' 6 | s.version = package['version'] 7 | s.summary = package['description'] 8 | s.description = <<-DESC 9 | React Native library to measure blocks of text before laying it on screen and get fonts info, 10 | based originally on Airam's react-native-measure-text (support iOS and Android). 11 | DESC 12 | s.homepage = package['homepage'] 13 | s.license = package['license'] 14 | s.author = package['author'] 15 | s.platform = :ios, '9.0' 16 | s.source = { :git => package['repository'], :tag => "v#{s.version}" } 17 | s.source_files = '*.{h,m}' 18 | s.requires_arc = true 19 | 20 | s.dependency 'React' 21 | end 22 | 23 | -------------------------------------------------------------------------------- /ios/RNTextSize.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B3E7B58A1CC2AC0600A0062D /* RNTextSize.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* RNTextSize.m */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXCopyFilesBuildPhase section */ 14 | 58B511D91A9E6C8500147676 /* CopyFiles */ = { 15 | isa = PBXCopyFilesBuildPhase; 16 | buildActionMask = 2147483647; 17 | dstPath = "include/$(PRODUCT_NAME)"; 18 | dstSubfolderSpec = 16; 19 | files = ( 20 | ); 21 | runOnlyForDeploymentPostprocessing = 0; 22 | }; 23 | /* End PBXCopyFilesBuildPhase section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 134814201AA4EA6300B7C361 /* libRNTextSize.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNTextSize.a; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | B3E7B5881CC2AC0600A0062D /* RNTextSize.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNTextSize.h; sourceTree = ""; }; 28 | B3E7B5891CC2AC0600A0062D /* RNTextSize.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNTextSize.m; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 58B511D81A9E6C8500147676 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 134814211AA4EA7D00B7C361 /* Products */ = { 43 | isa = PBXGroup; 44 | children = ( 45 | 134814201AA4EA6300B7C361 /* libRNTextSize.a */, 46 | ); 47 | name = Products; 48 | sourceTree = ""; 49 | }; 50 | 58B511D21A9E6C8500147676 = { 51 | isa = PBXGroup; 52 | children = ( 53 | B3E7B5881CC2AC0600A0062D /* RNTextSize.h */, 54 | B3E7B5891CC2AC0600A0062D /* RNTextSize.m */, 55 | 134814211AA4EA7D00B7C361 /* Products */, 56 | ); 57 | sourceTree = ""; 58 | }; 59 | /* End PBXGroup section */ 60 | 61 | /* Begin PBXNativeTarget section */ 62 | 58B511DA1A9E6C8500147676 /* RNTextSize */ = { 63 | isa = PBXNativeTarget; 64 | buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNTextSize" */; 65 | buildPhases = ( 66 | 58B511D71A9E6C8500147676 /* Sources */, 67 | 58B511D81A9E6C8500147676 /* Frameworks */, 68 | 58B511D91A9E6C8500147676 /* CopyFiles */, 69 | ); 70 | buildRules = ( 71 | ); 72 | dependencies = ( 73 | ); 74 | name = RNTextSize; 75 | productName = RCTDataManager; 76 | productReference = 134814201AA4EA6300B7C361 /* libRNTextSize.a */; 77 | productType = "com.apple.product-type.library.static"; 78 | }; 79 | /* End PBXNativeTarget section */ 80 | 81 | /* Begin PBXProject section */ 82 | 58B511D31A9E6C8500147676 /* Project object */ = { 83 | isa = PBXProject; 84 | attributes = { 85 | LastUpgradeCheck = 0940; 86 | ORGANIZATIONNAME = Facebook; 87 | TargetAttributes = { 88 | 58B511DA1A9E6C8500147676 = { 89 | CreatedOnToolsVersion = 6.1.1; 90 | }; 91 | }; 92 | }; 93 | buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNTextSize" */; 94 | compatibilityVersion = "Xcode 8.0"; 95 | developmentRegion = English; 96 | hasScannedForEncodings = 0; 97 | knownRegions = ( 98 | en, 99 | ); 100 | mainGroup = 58B511D21A9E6C8500147676; 101 | productRefGroup = 58B511D21A9E6C8500147676; 102 | projectDirPath = ""; 103 | projectRoot = ""; 104 | targets = ( 105 | 58B511DA1A9E6C8500147676 /* RNTextSize */, 106 | ); 107 | }; 108 | /* End PBXProject section */ 109 | 110 | /* Begin PBXSourcesBuildPhase section */ 111 | 58B511D71A9E6C8500147676 /* Sources */ = { 112 | isa = PBXSourcesBuildPhase; 113 | buildActionMask = 2147483647; 114 | files = ( 115 | B3E7B58A1CC2AC0600A0062D /* RNTextSize.m in Sources */, 116 | ); 117 | runOnlyForDeploymentPostprocessing = 0; 118 | }; 119 | /* End PBXSourcesBuildPhase section */ 120 | 121 | /* Begin XCBuildConfiguration section */ 122 | 58B511ED1A9E6C8500147676 /* Debug */ = { 123 | isa = XCBuildConfiguration; 124 | buildSettings = { 125 | ALWAYS_SEARCH_USER_PATHS = NO; 126 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 127 | CLANG_CXX_LIBRARY = "libc++"; 128 | CLANG_ENABLE_MODULES = YES; 129 | CLANG_ENABLE_OBJC_ARC = YES; 130 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 131 | CLANG_WARN_BOOL_CONVERSION = YES; 132 | CLANG_WARN_COMMA = YES; 133 | CLANG_WARN_CONSTANT_CONVERSION = YES; 134 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 135 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 136 | CLANG_WARN_EMPTY_BODY = YES; 137 | CLANG_WARN_ENUM_CONVERSION = YES; 138 | CLANG_WARN_INFINITE_RECURSION = YES; 139 | CLANG_WARN_INT_CONVERSION = YES; 140 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 141 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 142 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 143 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 144 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 145 | CLANG_WARN_STRICT_PROTOTYPES = YES; 146 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 147 | CLANG_WARN_UNREACHABLE_CODE = YES; 148 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 149 | COPY_PHASE_STRIP = NO; 150 | ENABLE_STRICT_OBJC_MSGSEND = YES; 151 | ENABLE_TESTABILITY = YES; 152 | GCC_C_LANGUAGE_STANDARD = gnu99; 153 | GCC_DYNAMIC_NO_PIC = NO; 154 | GCC_NO_COMMON_BLOCKS = YES; 155 | GCC_OPTIMIZATION_LEVEL = 0; 156 | GCC_PREPROCESSOR_DEFINITIONS = ( 157 | "DEBUG=1", 158 | "$(inherited)", 159 | ); 160 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 161 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 162 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 163 | GCC_WARN_UNDECLARED_SELECTOR = YES; 164 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 165 | GCC_WARN_UNUSED_FUNCTION = YES; 166 | GCC_WARN_UNUSED_VARIABLE = YES; 167 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 168 | MTL_ENABLE_DEBUG_INFO = YES; 169 | ONLY_ACTIVE_ARCH = YES; 170 | SDKROOT = iphoneos; 171 | }; 172 | name = Debug; 173 | }; 174 | 58B511EE1A9E6C8500147676 /* Release */ = { 175 | isa = XCBuildConfiguration; 176 | buildSettings = { 177 | ALWAYS_SEARCH_USER_PATHS = NO; 178 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 179 | CLANG_CXX_LIBRARY = "libc++"; 180 | CLANG_ENABLE_MODULES = YES; 181 | CLANG_ENABLE_OBJC_ARC = YES; 182 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 183 | CLANG_WARN_BOOL_CONVERSION = YES; 184 | CLANG_WARN_COMMA = YES; 185 | CLANG_WARN_CONSTANT_CONVERSION = YES; 186 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 187 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 188 | CLANG_WARN_EMPTY_BODY = YES; 189 | CLANG_WARN_ENUM_CONVERSION = YES; 190 | CLANG_WARN_INFINITE_RECURSION = YES; 191 | CLANG_WARN_INT_CONVERSION = YES; 192 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 193 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 194 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 195 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 196 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 197 | CLANG_WARN_STRICT_PROTOTYPES = YES; 198 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 199 | CLANG_WARN_UNREACHABLE_CODE = YES; 200 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 201 | COPY_PHASE_STRIP = YES; 202 | ENABLE_NS_ASSERTIONS = NO; 203 | ENABLE_STRICT_OBJC_MSGSEND = YES; 204 | GCC_C_LANGUAGE_STANDARD = gnu99; 205 | GCC_NO_COMMON_BLOCKS = YES; 206 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 207 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 208 | GCC_WARN_UNDECLARED_SELECTOR = YES; 209 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 210 | GCC_WARN_UNUSED_FUNCTION = YES; 211 | GCC_WARN_UNUSED_VARIABLE = YES; 212 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 213 | MTL_ENABLE_DEBUG_INFO = NO; 214 | SDKROOT = iphoneos; 215 | VALIDATE_PRODUCT = YES; 216 | }; 217 | name = Release; 218 | }; 219 | 58B511F01A9E6C8500147676 /* Debug */ = { 220 | isa = XCBuildConfiguration; 221 | buildSettings = { 222 | HEADER_SEARCH_PATHS = ( 223 | "$(inherited)", 224 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 225 | "$(SRCROOT)/../../react-native/React/**", 226 | "$(SRCROOT)/../node_modules/react-native/React/**", 227 | ); 228 | OTHER_LDFLAGS = "-ObjC"; 229 | PRODUCT_NAME = RNTextSize; 230 | SKIP_INSTALL = YES; 231 | }; 232 | name = Debug; 233 | }; 234 | 58B511F11A9E6C8500147676 /* Release */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | HEADER_SEARCH_PATHS = ( 238 | "$(inherited)", 239 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 240 | "$(SRCROOT)/../../react-native/React/**", 241 | "$(SRCROOT)/../node_modules/react-native/React/**", 242 | ); 243 | OTHER_LDFLAGS = "-ObjC"; 244 | PRODUCT_NAME = RNTextSize; 245 | SKIP_INSTALL = YES; 246 | }; 247 | name = Release; 248 | }; 249 | /* End XCBuildConfiguration section */ 250 | 251 | /* Begin XCConfigurationList section */ 252 | 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNTextSize" */ = { 253 | isa = XCConfigurationList; 254 | buildConfigurations = ( 255 | 58B511ED1A9E6C8500147676 /* Debug */, 256 | 58B511EE1A9E6C8500147676 /* Release */, 257 | ); 258 | defaultConfigurationIsVisible = 0; 259 | defaultConfigurationName = Release; 260 | }; 261 | 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNTextSize" */ = { 262 | isa = XCConfigurationList; 263 | buildConfigurations = ( 264 | 58B511F01A9E6C8500147676 /* Debug */, 265 | 58B511F11A9E6C8500147676 /* Release */, 266 | ); 267 | defaultConfigurationIsVisible = 0; 268 | defaultConfigurationName = Release; 269 | }; 270 | /* End XCConfigurationList section */ 271 | }; 272 | rootObject = 58B511D31A9E6C8500147676 /* Project object */; 273 | } 274 | -------------------------------------------------------------------------------- /ios/RNTextSize.xcodeproj/xcshareddata/xcschemes/RNTextSize.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 53 | 54 | 55 | 56 | 57 | 58 | 68 | 69 | 75 | 76 | 77 | 78 | 79 | 80 | 86 | 87 | 93 | 94 | 95 | 96 | 98 | 99 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-text-size", 3 | "version": "4.0.0-rc.1", 4 | "description": "Measure text accurately before laying it out and get font information from your App", 5 | "main": "index.js", 6 | "keywords": [ 7 | "react-native", 8 | "measure", 9 | "text", 10 | "size", 11 | "height", 12 | "width", 13 | "precompute", 14 | "block", 15 | "layout", 16 | "multiline", 17 | "fonts" 18 | ], 19 | "repository": "https://github.com/aMarCruz/react-native-text-size.git", 20 | "bugs": "https://github.com/aMarCruz/react-native-text-size/issues", 21 | "homepage": "https://github.com/aMarCruz/react-native-text-size", 22 | "author": "aMarCruz ", 23 | "license": "BSD-2-Clause", 24 | "scripts": { 25 | "clean": "rm -rf $TMPDIR/react-* && watchman watch-del-all && cd android && ./gradlew clean" 26 | }, 27 | "peerDependencies": { 28 | "react-native": ">=0.59.0" 29 | } 30 | } 31 | --------------------------------------------------------------------------------