react-native-text-measure 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -0
- package/android/README.md +14 -0
- package/android/build.gradle +148 -0
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/com/reactlibrary/ReactNativeTextMeasureModule.java +292 -0
- package/android/src/main/java/com/reactlibrary/ReactNativeTextMeasurePackage.java +44 -0
- package/index.js +115 -0
- package/ios/ReactNativeTextMeasure.h +19 -0
- package/ios/ReactNativeTextMeasure.m +204 -0
- package/ios/ReactNativeTextMeasure.xcodeproj/project.pbxproj +281 -0
- package/ios/ReactNativeTextMeasure.xcworkspace/contents.xcworkspacedata +7 -0
- package/package.json +28 -0
- package/react-native-text-measure.podspec +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# react-native-text-measure
|
|
2
|
+
|
|
3
|
+
> Accurately measure text `width`, `height`, and `lineCount` using native layout engines — no view rendered.
|
|
4
|
+
|
|
5
|
+
| Platform | Engine | Accuracy |
|
|
6
|
+
|----------|--------|----------|
|
|
7
|
+
| iOS | `NSLayoutManager` (TextKit) | ✅ Identical to `UILabel` |
|
|
8
|
+
| Android | `StaticLayout` | ✅ Identical to `TextView` |
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install react-native-text-measure
|
|
16
|
+
# or
|
|
17
|
+
yarn add react-native-text-measure
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### iOS
|
|
21
|
+
```sh
|
|
22
|
+
cd ios && pod install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Android
|
|
26
|
+
Auto-linking handles it on RN >= 0.60. For manual linking see [`android/README.md`](android/README.md).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import { measureText, measureTextSync } from 'react-native-text-measure';
|
|
34
|
+
|
|
35
|
+
// ── Async (recommended) ───────────────────────────────────────────────────────
|
|
36
|
+
const { width, height, lineCount } = await measureText('Hello world', {
|
|
37
|
+
fontSize: 20,
|
|
38
|
+
fontFamily: 'Helvetica',
|
|
39
|
+
fontWeight: 'normal',
|
|
40
|
+
maxWidth: 300,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ── Sync (use sparingly — blocks JS thread) ───────────────────────────────────
|
|
44
|
+
const result = measureTextSync('Hello', { fontSize: 16 });
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## API
|
|
50
|
+
|
|
51
|
+
### `measureText(text, options)` → `Promise<Result>`
|
|
52
|
+
### `measureTextSync(text, options)` → `Result`
|
|
53
|
+
|
|
54
|
+
#### Options
|
|
55
|
+
|
|
56
|
+
| Option | Type | Default | Description |
|
|
57
|
+
|--------|------|---------|-------------|
|
|
58
|
+
| `fontSize` | `number` | `14` | Font size in dp/pt |
|
|
59
|
+
| `fontFamily` | `string` | system | Font family name |
|
|
60
|
+
| `fontWeight` | `string` | `'normal'` | `'normal'`, `'bold'`, `'100'`–`'900'` |
|
|
61
|
+
| `fontStyle` | `string` | `'normal'` | `'normal'` or `'italic'` |
|
|
62
|
+
| `letterSpacing` | `number` | `0` | Extra letter spacing in dp/pt |
|
|
63
|
+
| `lineHeight` | `number` | `0` | Explicit line height. `0` = natural |
|
|
64
|
+
| `maxWidth` | `number` | `0` | Wrap width. `0` = no wrap (single line) |
|
|
65
|
+
| `maxHeight` | `number` | `0` | Clamp returned height. `0` = unclamped |
|
|
66
|
+
| `numberOfLines` | `number` | `0` | Max lines. `0` = unlimited |
|
|
67
|
+
| `includeFontPadding` | `boolean` | `true` | **Android only.** Match your `<Text>` setting |
|
|
68
|
+
|
|
69
|
+
#### Result
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
{
|
|
73
|
+
width: number; // dp (Android) / pt (iOS)
|
|
74
|
+
height: number; // dp (Android) / pt (iOS)
|
|
75
|
+
lineCount: number;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Accuracy notes
|
|
82
|
+
|
|
83
|
+
- Results are **not estimates** — both engines perform a full layout pass.
|
|
84
|
+
- `letterSpacing` unit differs between platforms (see inline comments).
|
|
85
|
+
- `includeFontPadding` must match your `<Text>` component on Android.
|
|
86
|
+
- If `fontFamily` is not found, the system font is used silently — measurement will be accurate but for the wrong font.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README
|
|
2
|
+
======
|
|
3
|
+
|
|
4
|
+
If you want to publish the lib as a maven dependency, follow these steps before publishing a new version to npm:
|
|
5
|
+
|
|
6
|
+
1. Be sure to have the Android [SDK](https://developer.android.com/studio/index.html) and [NDK](https://developer.android.com/ndk/guides/index.html) installed
|
|
7
|
+
2. Be sure to have a `local.properties` file in this folder that points to the Android SDK and NDK
|
|
8
|
+
```
|
|
9
|
+
ndk.dir=/Users/{username}/Library/Android/sdk/ndk-bundle
|
|
10
|
+
sdk.dir=/Users/{username}/Library/Android/sdk
|
|
11
|
+
```
|
|
12
|
+
3. Delete the `maven` folder
|
|
13
|
+
4. Run `./gradlew installArchives`
|
|
14
|
+
5. Verify that latest set of generated files is in the maven folder with the correct version number
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// android/build.gradle
|
|
2
|
+
|
|
3
|
+
// based on:
|
|
4
|
+
//
|
|
5
|
+
// * https://github.com/facebook/react-native/blob/0.60-stable/template/android/build.gradle
|
|
6
|
+
// previous location:
|
|
7
|
+
// - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/build.gradle
|
|
8
|
+
//
|
|
9
|
+
// * https://github.com/facebook/react-native/blob/0.60-stable/template/android/app/build.gradle
|
|
10
|
+
// previous location:
|
|
11
|
+
// - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/app/build.gradle
|
|
12
|
+
|
|
13
|
+
// These defaults should reflect the SDK versions used by
|
|
14
|
+
// the minimum React Native version supported.
|
|
15
|
+
def DEFAULT_COMPILE_SDK_VERSION = 28
|
|
16
|
+
def DEFAULT_BUILD_TOOLS_VERSION = '28.0.3'
|
|
17
|
+
def DEFAULT_MIN_SDK_VERSION = 16
|
|
18
|
+
def DEFAULT_TARGET_SDK_VERSION = 28
|
|
19
|
+
|
|
20
|
+
def safeExtGet(prop, fallback) {
|
|
21
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
apply plugin: 'com.android.library'
|
|
25
|
+
apply plugin: 'maven'
|
|
26
|
+
|
|
27
|
+
buildscript {
|
|
28
|
+
// The Android Gradle plugin is only required when opening the android folder stand-alone.
|
|
29
|
+
// This avoids unnecessary downloads and potential conflicts when the library is included as a
|
|
30
|
+
// module dependency in an application project.
|
|
31
|
+
// ref: https://docs.gradle.org/current/userguide/tutorial_using_tasks.html#sec:build_script_external_dependencies
|
|
32
|
+
if (project == rootProject) {
|
|
33
|
+
repositories {
|
|
34
|
+
google()
|
|
35
|
+
}
|
|
36
|
+
dependencies {
|
|
37
|
+
// This should reflect the Gradle plugin version used by
|
|
38
|
+
// the minimum React Native version supported.
|
|
39
|
+
classpath 'com.android.tools.build:gradle:3.4.1'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
android {
|
|
45
|
+
compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION)
|
|
46
|
+
buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION)
|
|
47
|
+
defaultConfig {
|
|
48
|
+
minSdkVersion safeExtGet('minSdkVersion', DEFAULT_MIN_SDK_VERSION)
|
|
49
|
+
targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION)
|
|
50
|
+
versionCode 1
|
|
51
|
+
versionName "1.0"
|
|
52
|
+
}
|
|
53
|
+
lintOptions {
|
|
54
|
+
abortOnError false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
repositories {
|
|
59
|
+
// ref: https://www.baeldung.com/maven-local-repository
|
|
60
|
+
mavenLocal()
|
|
61
|
+
maven {
|
|
62
|
+
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
|
63
|
+
url "$rootDir/../node_modules/react-native/android"
|
|
64
|
+
}
|
|
65
|
+
maven {
|
|
66
|
+
// Android JSC is installed from npm
|
|
67
|
+
url "$rootDir/../node_modules/jsc-android/dist"
|
|
68
|
+
}
|
|
69
|
+
google()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
dependencies {
|
|
73
|
+
//noinspection GradleDynamicVersion
|
|
74
|
+
implementation 'com.facebook.react:react-native:+' // From node_modules
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def configureReactNativePom(def pom) {
|
|
78
|
+
def packageJson = new groovy.json.JsonSlurper().parseText(file('../package.json').text)
|
|
79
|
+
|
|
80
|
+
pom.project {
|
|
81
|
+
name packageJson.title
|
|
82
|
+
artifactId packageJson.name
|
|
83
|
+
version = packageJson.version
|
|
84
|
+
group = "com.reactlibrary"
|
|
85
|
+
description packageJson.description
|
|
86
|
+
url packageJson.repository.baseUrl
|
|
87
|
+
|
|
88
|
+
licenses {
|
|
89
|
+
license {
|
|
90
|
+
name packageJson.license
|
|
91
|
+
url packageJson.repository.baseUrl + '/blob/master/' + packageJson.licenseFilename
|
|
92
|
+
distribution 'repo'
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
developers {
|
|
97
|
+
developer {
|
|
98
|
+
id packageJson.author.username
|
|
99
|
+
name packageJson.author.name
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
afterEvaluate { project ->
|
|
106
|
+
// some Gradle build hooks ref:
|
|
107
|
+
// https://www.oreilly.com/library/view/gradle-beyond-the/9781449373801/ch03.html
|
|
108
|
+
task androidJavadoc(type: Javadoc) {
|
|
109
|
+
source = android.sourceSets.main.java.srcDirs
|
|
110
|
+
classpath += files(android.bootClasspath)
|
|
111
|
+
classpath += files(project.getConfigurations().getByName('compile').asList())
|
|
112
|
+
include '**/*.java'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) {
|
|
116
|
+
classifier = 'javadoc'
|
|
117
|
+
from androidJavadoc.destinationDir
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
task androidSourcesJar(type: Jar) {
|
|
121
|
+
classifier = 'sources'
|
|
122
|
+
from android.sourceSets.main.java.srcDirs
|
|
123
|
+
include '**/*.java'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
android.libraryVariants.all { variant ->
|
|
127
|
+
def name = variant.name.capitalize()
|
|
128
|
+
def javaCompileTask = variant.javaCompileProvider.get()
|
|
129
|
+
|
|
130
|
+
task "jar${name}"(type: Jar, dependsOn: javaCompileTask) {
|
|
131
|
+
from javaCompileTask.destinationDir
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
artifacts {
|
|
136
|
+
archives androidSourcesJar
|
|
137
|
+
archives androidJavadocJar
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
task installArchives(type: Upload) {
|
|
141
|
+
configuration = configurations.archives
|
|
142
|
+
repositories.mavenDeployer {
|
|
143
|
+
// Deploy to react-native-event-bridge/maven, ready to publish to npm
|
|
144
|
+
repository url: "file://${projectDir}/../android/maven"
|
|
145
|
+
configureReactNativePom pom
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
package com.reactlibrary;
|
|
2
|
+
|
|
3
|
+
import android.graphics.Paint;
|
|
4
|
+
import android.graphics.Typeface;
|
|
5
|
+
import android.os.Build;
|
|
6
|
+
import android.text.Layout;
|
|
7
|
+
import android.text.StaticLayout;
|
|
8
|
+
import android.text.TextPaint;
|
|
9
|
+
import android.text.TextUtils;
|
|
10
|
+
|
|
11
|
+
import com.facebook.react.bridge.Arguments;
|
|
12
|
+
import com.facebook.react.bridge.Promise;
|
|
13
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
14
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
15
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
16
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
17
|
+
import com.facebook.react.bridge.WritableMap;
|
|
18
|
+
|
|
19
|
+
import androidx.annotation.NonNull;
|
|
20
|
+
import androidx.annotation.Nullable;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* ReactNativeTextMeasureModule
|
|
24
|
+
*
|
|
25
|
+
* Measures text bounding dimensions (width, height, lineCount) using Android's
|
|
26
|
+
* StaticLayout — the same engine that TextView uses. No View is created or rendered.
|
|
27
|
+
*
|
|
28
|
+
* All input values are in density-independent pixels (dp).
|
|
29
|
+
* All output values are also in dp, matching React Native's coordinate system.
|
|
30
|
+
*
|
|
31
|
+
* JS API:
|
|
32
|
+
* measureText(text, options) → Promise<{ width, height, lineCount }>
|
|
33
|
+
* measureTextSync(text, options) → { width, height, lineCount }
|
|
34
|
+
*/
|
|
35
|
+
public class ReactNativeTextMeasureModule extends ReactContextBaseJavaModule {
|
|
36
|
+
|
|
37
|
+
private final ReactApplicationContext reactContext;
|
|
38
|
+
|
|
39
|
+
public ReactNativeTextMeasureModule(ReactApplicationContext context) {
|
|
40
|
+
super(context);
|
|
41
|
+
this.reactContext = context;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@NonNull
|
|
45
|
+
@Override
|
|
46
|
+
public String getName() {
|
|
47
|
+
// Must match NativeModules.ReactNativeTextMeasure on the JS side
|
|
48
|
+
return "ReactNativeTextMeasure";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
52
|
+
// JS-callable: async (recommended)
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Async measurement. Resolves with { width, height, lineCount }.
|
|
57
|
+
* Runs on the React Native background thread pool — does not block JS.
|
|
58
|
+
*/
|
|
59
|
+
@ReactMethod
|
|
60
|
+
public void measureText(String text, ReadableMap options, Promise promise) {
|
|
61
|
+
try {
|
|
62
|
+
promise.resolve(performMeasure(text, options));
|
|
63
|
+
} catch (Exception e) {
|
|
64
|
+
promise.reject("MEASURE_ERROR", e.getMessage(), e);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
69
|
+
// JS-callable: synchronous
|
|
70
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Synchronous measurement. Returns { width, height, lineCount } directly.
|
|
74
|
+
*
|
|
75
|
+
* ⚠️ Runs on the JS thread. Use only when async is not an option.
|
|
76
|
+
* For old architecture: isBlockingSynchronousMethod = true is required.
|
|
77
|
+
* For new architecture (TurboModules): implement NativeReactNativeTextMeasureSpec.
|
|
78
|
+
*/
|
|
79
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
80
|
+
public WritableMap measureTextSync(String text, ReadableMap options) {
|
|
81
|
+
try {
|
|
82
|
+
return performMeasure(text, options);
|
|
83
|
+
} catch (Exception e) {
|
|
84
|
+
WritableMap error = Arguments.createMap();
|
|
85
|
+
error.putString("error", e.getMessage());
|
|
86
|
+
return error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
91
|
+
// Core measurement — called by both async and sync entry points
|
|
92
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
private WritableMap performMeasure(@Nullable String text, @Nullable ReadableMap options) {
|
|
95
|
+
if (text == null) text = "";
|
|
96
|
+
if (options == null) options = Arguments.createMap();
|
|
97
|
+
|
|
98
|
+
// ── 1. Parse options ─────────────────────────────────────────────────
|
|
99
|
+
float fontSize = getFloat(options, "fontSize", 14f);
|
|
100
|
+
String fontFamily = getString(options, "fontFamily", null);
|
|
101
|
+
String fontWeight = getString(options, "fontWeight", "normal");
|
|
102
|
+
String fontStyle = getString(options, "fontStyle", "normal");
|
|
103
|
+
float letterSpacing = getFloat(options, "letterSpacing", 0f);
|
|
104
|
+
float lineHeight = getFloat(options, "lineHeight", 0f);
|
|
105
|
+
float maxWidth = getFloat(options, "maxWidth", 0f);
|
|
106
|
+
float maxHeight = getFloat(options, "maxHeight", 0f);
|
|
107
|
+
int numberOfLines = getInt(options, "numberOfLines", 0);
|
|
108
|
+
boolean includeFontPad = getBool(options, "includeFontPadding", true);
|
|
109
|
+
|
|
110
|
+
// ── 2. dp → px conversion ─────────────────────────────────────────────
|
|
111
|
+
// React Native uses dp everywhere; Android Paint/StaticLayout needs px.
|
|
112
|
+
float density = reactContext.getResources().getDisplayMetrics().density;
|
|
113
|
+
float fontSizePx = fontSize * density;
|
|
114
|
+
float lineHeightPx = lineHeight * density;
|
|
115
|
+
int maxWidthPx = (maxWidth > 0) ? Math.round(maxWidth * density) : Integer.MAX_VALUE;
|
|
116
|
+
int maxHeightPx = (maxHeight > 0) ? Math.round(maxHeight * density) : Integer.MAX_VALUE;
|
|
117
|
+
|
|
118
|
+
// ── 3. Resolve Typeface ───────────────────────────────────────────────
|
|
119
|
+
boolean isBold = "bold".equalsIgnoreCase(fontWeight) || numericWeightIsBold(fontWeight);
|
|
120
|
+
boolean isItalic = "italic".equalsIgnoreCase(fontStyle);
|
|
121
|
+
|
|
122
|
+
int typefaceStyle;
|
|
123
|
+
if (isBold && isItalic) typefaceStyle = Typeface.BOLD_ITALIC;
|
|
124
|
+
else if (isBold) typefaceStyle = Typeface.BOLD;
|
|
125
|
+
else if (isItalic) typefaceStyle = Typeface.ITALIC;
|
|
126
|
+
else typefaceStyle = Typeface.NORMAL;
|
|
127
|
+
|
|
128
|
+
Typeface typeface = resolveTypeface(fontFamily, typefaceStyle);
|
|
129
|
+
|
|
130
|
+
// ── 4. Build TextPaint ────────────────────────────────────────────────
|
|
131
|
+
TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
|
|
132
|
+
paint.setTextSize(fontSizePx);
|
|
133
|
+
paint.setTypeface(typeface);
|
|
134
|
+
|
|
135
|
+
// letterSpacing: Android uses EM fractions; convert from dp.
|
|
136
|
+
// letterSpacing_em = letterSpacing_px / fontSizePx
|
|
137
|
+
// We treat the user's value as dp, so first dp→px, then →em.
|
|
138
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && letterSpacing != 0f) {
|
|
139
|
+
float letterSpacingPx = letterSpacing * density;
|
|
140
|
+
paint.setLetterSpacing(letterSpacingPx / fontSizePx);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── 5. Build StaticLayout ─────────────────────────────────────────────
|
|
144
|
+
//
|
|
145
|
+
// StaticLayout is the exact engine TextView uses for static (non-editable) text.
|
|
146
|
+
// It performs full Unicode line-breaking, glyph shaping, and produces the real
|
|
147
|
+
// pixel-accurate dimensions — not an estimate.
|
|
148
|
+
//
|
|
149
|
+
StaticLayout layout = buildStaticLayout(
|
|
150
|
+
text, paint, maxWidthPx,
|
|
151
|
+
lineHeightPx, numberOfLines, includeFontPad
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// ── 6. Extract dimensions ─────────────────────────────────────────────
|
|
155
|
+
int lineCount = layout.getLineCount();
|
|
156
|
+
float measuredHeight = layout.getHeight();
|
|
157
|
+
float measuredWidth = 0f;
|
|
158
|
+
|
|
159
|
+
for (int i = 0; i < lineCount; i++) {
|
|
160
|
+
float lineWidth = layout.getLineWidth(i);
|
|
161
|
+
if (lineWidth > measuredWidth) measuredWidth = lineWidth;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Clamp height if maxHeight was provided
|
|
165
|
+
if (maxHeightPx != Integer.MAX_VALUE && measuredHeight > maxHeightPx) {
|
|
166
|
+
measuredHeight = maxHeightPx;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── 7. px → dp for output ─────────────────────────────────────────────
|
|
170
|
+
WritableMap result = Arguments.createMap();
|
|
171
|
+
result.putDouble("width", measuredWidth / density);
|
|
172
|
+
result.putDouble("height", measuredHeight / density);
|
|
173
|
+
result.putInt( "lineCount", lineCount);
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
178
|
+
// StaticLayout builder — handles API version differences
|
|
179
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
@SuppressWarnings("deprecation")
|
|
182
|
+
private StaticLayout buildStaticLayout(
|
|
183
|
+
String text,
|
|
184
|
+
TextPaint paint,
|
|
185
|
+
int maxWidthPx,
|
|
186
|
+
float lineHeightPx,
|
|
187
|
+
int numberOfLines,
|
|
188
|
+
boolean includeFontPad
|
|
189
|
+
) {
|
|
190
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
191
|
+
// API 23+ — use the Builder (clean, full-featured)
|
|
192
|
+
StaticLayout.Builder builder = StaticLayout.Builder
|
|
193
|
+
.obtain(text, 0, text.length(), paint, maxWidthPx)
|
|
194
|
+
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
|
|
195
|
+
.setIncludePad(includeFontPad)
|
|
196
|
+
.setMaxLines(numberOfLines > 0 ? numberOfLines : Integer.MAX_VALUE)
|
|
197
|
+
.setEllipsize(numberOfLines > 0 ? TextUtils.TruncateAt.END : null);
|
|
198
|
+
|
|
199
|
+
if (lineHeightPx > 0) {
|
|
200
|
+
// Android line spacing: total_line_height = naturalLineHeight * mult + add
|
|
201
|
+
// We want total = lineHeightPx, so: add = lineHeightPx - natural, mult = 1
|
|
202
|
+
float naturalLineHeight = -paint.ascent() + paint.descent();
|
|
203
|
+
builder.setLineSpacing(lineHeightPx - naturalLineHeight, 1f);
|
|
204
|
+
} else {
|
|
205
|
+
builder.setLineSpacing(0f, 1f);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return builder.build();
|
|
209
|
+
|
|
210
|
+
} else {
|
|
211
|
+
// API 21–22 fallback — deprecated constructor but still accurate
|
|
212
|
+
float spacingAdd = 0f;
|
|
213
|
+
float spacingMult = 1f;
|
|
214
|
+
|
|
215
|
+
if (lineHeightPx > 0) {
|
|
216
|
+
float naturalLineHeight = -paint.ascent() + paint.descent();
|
|
217
|
+
spacingAdd = lineHeightPx - naturalLineHeight;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return new StaticLayout(
|
|
221
|
+
text, 0, text.length(),
|
|
222
|
+
paint, maxWidthPx,
|
|
223
|
+
Layout.Alignment.ALIGN_NORMAL,
|
|
224
|
+
spacingMult, spacingAdd,
|
|
225
|
+
includeFontPad,
|
|
226
|
+
numberOfLines > 0 ? TextUtils.TruncateAt.END : null,
|
|
227
|
+
maxWidthPx
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
233
|
+
// Typeface resolution
|
|
234
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
private Typeface resolveTypeface(@Nullable String fontFamily, int style) {
|
|
237
|
+
if (fontFamily != null && !fontFamily.isEmpty()) {
|
|
238
|
+
// 1. Try assets/fonts/<fontFamily>.ttf (common RN convention)
|
|
239
|
+
try {
|
|
240
|
+
Typeface fromAssets = Typeface.createFromAsset(
|
|
241
|
+
reactContext.getAssets(),
|
|
242
|
+
"fonts/" + fontFamily + ".ttf"
|
|
243
|
+
);
|
|
244
|
+
if (fromAssets != null) {
|
|
245
|
+
return Typeface.create(fromAssets, style);
|
|
246
|
+
}
|
|
247
|
+
} catch (Exception ignored) { /* not in assets */ }
|
|
248
|
+
|
|
249
|
+
// 2. Try as a system font name (e.g. "sans-serif", "monospace", "Roboto")
|
|
250
|
+
try {
|
|
251
|
+
Typeface systemFont = Typeface.create(fontFamily, style);
|
|
252
|
+
if (systemFont != null) return systemFont;
|
|
253
|
+
} catch (Exception ignored) { /* unknown family */ }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 3. Default system font with the requested style
|
|
257
|
+
return Typeface.create((String) null, style);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
261
|
+
// ReadableMap helpers (null-safe)
|
|
262
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
private float getFloat(ReadableMap map, String key, float def) {
|
|
265
|
+
if (map.hasKey(key) && !map.isNull(key)) return (float) map.getDouble(key);
|
|
266
|
+
return def;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private int getInt(ReadableMap map, String key, int def) {
|
|
270
|
+
if (map.hasKey(key) && !map.isNull(key)) return map.getInt(key);
|
|
271
|
+
return def;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private boolean getBool(ReadableMap map, String key, boolean def) {
|
|
275
|
+
if (map.hasKey(key) && !map.isNull(key)) return map.getBoolean(key);
|
|
276
|
+
return def;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@Nullable
|
|
280
|
+
private String getString(ReadableMap map, String key, @Nullable String def) {
|
|
281
|
+
if (map.hasKey(key) && !map.isNull(key)) return map.getString(key);
|
|
282
|
+
return def;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private boolean numericWeightIsBold(String weight) {
|
|
286
|
+
try {
|
|
287
|
+
return Integer.parseInt(weight.trim()) >= 600;
|
|
288
|
+
} catch (NumberFormatException e) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package com.reactlibrary;
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage;
|
|
4
|
+
import com.facebook.react.bridge.NativeModule;
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
7
|
+
|
|
8
|
+
import java.util.Arrays;
|
|
9
|
+
import java.util.Collections;
|
|
10
|
+
import java.util.List;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ReactNativeTextMeasurePackage
|
|
14
|
+
*
|
|
15
|
+
* Registers ReactNativeTextMeasureModule with the React Native bridge.
|
|
16
|
+
*
|
|
17
|
+
* Add to your MainApplication.java:
|
|
18
|
+
*
|
|
19
|
+
* @Override
|
|
20
|
+
* protected List<ReactPackage> getPackages() {
|
|
21
|
+
* return Arrays.<ReactPackage>asList(
|
|
22
|
+
* new MainReactPackage(),
|
|
23
|
+
* new ReactNativeTextMeasurePackage() // ← add this line
|
|
24
|
+
* );
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* If you are on RN >= 0.60 with auto-linking enabled, you do NOT need to
|
|
28
|
+
* manually add the package — it will be registered automatically via
|
|
29
|
+
* react-native.config.js.
|
|
30
|
+
*/
|
|
31
|
+
public class ReactNativeTextMeasurePackage implements ReactPackage {
|
|
32
|
+
|
|
33
|
+
@Override
|
|
34
|
+
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
|
35
|
+
return Arrays.<NativeModule>asList(
|
|
36
|
+
new ReactNativeTextMeasureModule(reactContext)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Override
|
|
41
|
+
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
|
42
|
+
return Collections.emptyList();
|
|
43
|
+
}
|
|
44
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* react-native-text-measure
|
|
3
|
+
*
|
|
4
|
+
* Accurately measures text dimensions using native layout engines.
|
|
5
|
+
* iOS → NSLayoutManager (TextKit) — identical to UILabel
|
|
6
|
+
* Android → StaticLayout — identical to TextView
|
|
7
|
+
*
|
|
8
|
+
* No view is rendered. Measurement is pure native computation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { NativeModules } from "react-native";
|
|
12
|
+
|
|
13
|
+
const LINKING_ERROR =
|
|
14
|
+
`react-native-text-measure: Native module not found.\n\n` +
|
|
15
|
+
`iOS: Make sure you ran \`pod install\` and the .m file is in your Xcode project.\n` +
|
|
16
|
+
`Android: Make sure ReactNativeTextMeasurePackage is registered in MainApplication.\n\n` +
|
|
17
|
+
`If you are using React Native >= 0.60, auto-linking should handle this.\n` +
|
|
18
|
+
`Try rebuilding the app.`;
|
|
19
|
+
|
|
20
|
+
const NativeTextMeasure = NativeModules.ReactNativeTextMeasure
|
|
21
|
+
? NativeModules.ReactNativeTextMeasure
|
|
22
|
+
: new Proxy(
|
|
23
|
+
{},
|
|
24
|
+
{
|
|
25
|
+
get() {
|
|
26
|
+
throw new Error(LINKING_ERROR);
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// JSDoc typedef (works in plain JS + IDEs; see index.d.ts for TypeScript)
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {Object} TextMeasureOptions
|
|
37
|
+
* @property {number} [fontSize=14] - Font size in dp (Android) / pt (iOS).
|
|
38
|
+
* @property {string} [fontFamily] - Font family name. Falls back to system font.
|
|
39
|
+
* @property {string} [fontWeight='normal']- 'normal' | 'bold' | '100'–'900'.
|
|
40
|
+
* @property {string} [fontStyle='normal'] - 'normal' | 'italic'.
|
|
41
|
+
* @property {number} [letterSpacing=0] - Extra letter spacing in dp/pt.
|
|
42
|
+
* @property {number} [lineHeight=0] - Explicit line height. 0 = natural.
|
|
43
|
+
* @property {number} [maxWidth=0] - Wrap width. 0 = no wrapping.
|
|
44
|
+
* @property {number} [maxHeight=0] - Clamp height. 0 = unclamped.
|
|
45
|
+
* @property {number} [numberOfLines=0] - Max lines. 0 = unlimited.
|
|
46
|
+
* @property {boolean} [includeFontPadding=true] - Android only: match RN Text default.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} TextMeasureResult
|
|
51
|
+
* @property {number} width - Measured width in dp (Android) / pt (iOS).
|
|
52
|
+
* @property {number} height - Measured height in dp (Android) / pt (iOS).
|
|
53
|
+
* @property {number} lineCount - Number of lines the text was laid out into.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
// Public API
|
|
58
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Asynchronously measures the bounding box of `text` with the given style options.
|
|
62
|
+
* Runs off the JS thread — safe to call frequently.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} text
|
|
65
|
+
* @param {TextMeasureOptions} [options={}]
|
|
66
|
+
* @returns {Promise<TextMeasureResult>}
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* const { width, height, lineCount } = await measureText('Hello world', {
|
|
70
|
+
* fontSize: 20,
|
|
71
|
+
* fontFamily: 'Helvetica',
|
|
72
|
+
* fontWeight: 'normal',
|
|
73
|
+
* maxWidth: 200,
|
|
74
|
+
* });
|
|
75
|
+
*/
|
|
76
|
+
export function measureText(text, options = {}) {
|
|
77
|
+
return NativeTextMeasure.measureText(String(text), sanitize(options));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Synchronously measures text. Runs on the JS thread.
|
|
82
|
+
* ⚠️ Prefer `measureText` (async). Use this only when you cannot await
|
|
83
|
+
* (e.g., inside a synchronous layout calculation).
|
|
84
|
+
*
|
|
85
|
+
* @param {string} text
|
|
86
|
+
* @param {TextMeasureOptions} [options={}]
|
|
87
|
+
* @returns {TextMeasureResult}
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* const { height } = measureTextSync('Hello', { fontSize: 16 });
|
|
91
|
+
*/
|
|
92
|
+
export function measureTextSync(text, options = {}) {
|
|
93
|
+
return NativeTextMeasure.measureTextSync(String(text), sanitize(options));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
97
|
+
// Helpers
|
|
98
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Remove undefined keys so the native side receives a clean map.
|
|
102
|
+
* @param {TextMeasureOptions} options
|
|
103
|
+
* @returns {Object}
|
|
104
|
+
*/
|
|
105
|
+
function sanitize(options) {
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [key, value] of Object.entries(options)) {
|
|
108
|
+
if (value !== undefined && value !== null) {
|
|
109
|
+
out[key] = value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default { measureText, measureTextSync };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ReactNativeTextMeasure
|
|
7
|
+
*
|
|
8
|
+
* Native module that measures text bounding dimensions (width, height, lineCount)
|
|
9
|
+
* using Apple's TextKit layout engine — identical to UILabel / UITextView rendering.
|
|
10
|
+
* No view is created or rendered.
|
|
11
|
+
*
|
|
12
|
+
* Exposes two JS methods:
|
|
13
|
+
* measureText(text, options) → Promise (async, background thread)
|
|
14
|
+
* measureTextSync(text, options) → Object (sync, JS thread)
|
|
15
|
+
*/
|
|
16
|
+
@interface ReactNativeTextMeasure : NSObject <RCTBridgeModule>
|
|
17
|
+
@end
|
|
18
|
+
|
|
19
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#import "ReactNativeTextMeasure.h"
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Private helper — builds the measurement result dict from a string + options.
|
|
6
|
+
// Extracted so both the async and sync entry-points share identical logic.
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
static NSDictionary *RNTMPerformMeasure(NSString *text, NSDictionary *options) {
|
|
9
|
+
|
|
10
|
+
// ── 1. Parse options ───────────────────────────────────────────────────────
|
|
11
|
+
CGFloat fontSize = options[@"fontSize"] ? [options[@"fontSize"] doubleValue] : 14.0;
|
|
12
|
+
NSString *fontFamily = options[@"fontFamily"];
|
|
13
|
+
NSString *fontWeight = options[@"fontWeight"] ?: @"normal";
|
|
14
|
+
NSString *fontStyle = options[@"fontStyle"] ?: @"normal";
|
|
15
|
+
CGFloat letterSpacing = options[@"letterSpacing"] ? [options[@"letterSpacing"] doubleValue] : 0.0;
|
|
16
|
+
CGFloat lineHeight = options[@"lineHeight"] ? [options[@"lineHeight"] doubleValue] : 0.0;
|
|
17
|
+
CGFloat maxWidth = options[@"maxWidth"] ? [options[@"maxWidth"] doubleValue] : 0.0;
|
|
18
|
+
CGFloat maxHeight = options[@"maxHeight"] ? [options[@"maxHeight"] doubleValue] : 0.0;
|
|
19
|
+
NSInteger maxLines = options[@"numberOfLines"] ? [options[@"numberOfLines"] integerValue]: 0;
|
|
20
|
+
|
|
21
|
+
if (maxWidth <= 0) maxWidth = CGFLOAT_MAX;
|
|
22
|
+
if (maxHeight <= 0) maxHeight = CGFLOAT_MAX;
|
|
23
|
+
|
|
24
|
+
// ── 2. Resolve UIFontWeight ────────────────────────────────────────────────
|
|
25
|
+
UIFontWeight uiFontWeight = UIFontWeightRegular;
|
|
26
|
+
NSDictionary *weightMap = @{
|
|
27
|
+
@"100": @(UIFontWeightUltraLight),
|
|
28
|
+
@"200": @(UIFontWeightThin),
|
|
29
|
+
@"300": @(UIFontWeightLight),
|
|
30
|
+
@"400": @(UIFontWeightRegular),
|
|
31
|
+
@"500": @(UIFontWeightMedium),
|
|
32
|
+
@"600": @(UIFontWeightSemibold),
|
|
33
|
+
@"700": @(UIFontWeightBold),
|
|
34
|
+
@"800": @(UIFontWeightHeavy),
|
|
35
|
+
@"900": @(UIFontWeightBlack),
|
|
36
|
+
@"bold": @(UIFontWeightBold),
|
|
37
|
+
@"normal": @(UIFontWeightRegular),
|
|
38
|
+
};
|
|
39
|
+
if (weightMap[fontWeight]) {
|
|
40
|
+
uiFontWeight = [weightMap[fontWeight] doubleValue];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
BOOL isItalic = [fontStyle isEqualToString:@"italic"];
|
|
44
|
+
|
|
45
|
+
// ── 3. Build UIFont ────────────────────────────────────────────────────────
|
|
46
|
+
UIFont *font = nil;
|
|
47
|
+
|
|
48
|
+
if (fontFamily.length > 0) {
|
|
49
|
+
// Build a descriptor for the requested family so we can apply traits
|
|
50
|
+
UIFontDescriptor *descriptor = [UIFontDescriptor fontDescriptorWithName:fontFamily size:fontSize];
|
|
51
|
+
|
|
52
|
+
UIFontDescriptorSymbolicTraits traits = 0;
|
|
53
|
+
if (uiFontWeight >= UIFontWeightBold) traits |= UIFontDescriptorTraitBold;
|
|
54
|
+
if (isItalic) traits |= UIFontDescriptorTraitItalic;
|
|
55
|
+
|
|
56
|
+
if (traits != 0) {
|
|
57
|
+
UIFontDescriptor *traitDescriptor = [descriptor fontDescriptorWithSymbolicTraits:traits];
|
|
58
|
+
// fontDescriptorWithSymbolicTraits returns nil if the traits can't be satisfied
|
|
59
|
+
if (traitDescriptor) descriptor = traitDescriptor;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
font = [UIFont fontWithDescriptor:descriptor size:fontSize];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Fallback: system font (always available)
|
|
66
|
+
if (!font) {
|
|
67
|
+
if (isItalic) {
|
|
68
|
+
UIFontDescriptor *sysDesc = [[UIFont systemFontOfSize:fontSize weight:uiFontWeight] fontDescriptor];
|
|
69
|
+
UIFontDescriptor *italicDesc = [sysDesc fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitItalic];
|
|
70
|
+
font = italicDesc ? [UIFont fontWithDescriptor:italicDesc size:fontSize]
|
|
71
|
+
: [UIFont systemFontOfSize:fontSize weight:uiFontWeight];
|
|
72
|
+
} else {
|
|
73
|
+
font = [UIFont systemFontOfSize:fontSize weight:uiFontWeight];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Ultimate fallback
|
|
78
|
+
if (!font) font = [UIFont systemFontOfSize:fontSize];
|
|
79
|
+
|
|
80
|
+
// ── 4. Build paragraph style ───────────────────────────────────────────────
|
|
81
|
+
NSMutableParagraphStyle *paraStyle = [[NSMutableParagraphStyle alloc] init];
|
|
82
|
+
paraStyle.lineBreakMode = NSLineBreakByWordWrapping;
|
|
83
|
+
|
|
84
|
+
if (lineHeight > 0.0) {
|
|
85
|
+
// Setting both min and max to the same value gives an exact line height,
|
|
86
|
+
// which matches React Native's lineHeight prop behaviour.
|
|
87
|
+
paraStyle.minimumLineHeight = lineHeight;
|
|
88
|
+
paraStyle.maximumLineHeight = lineHeight;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── 5. Build attributed string ─────────────────────────────────────────────
|
|
92
|
+
NSMutableDictionary *attrs = [@{
|
|
93
|
+
NSFontAttributeName: font,
|
|
94
|
+
NSParagraphStyleAttributeName: paraStyle,
|
|
95
|
+
} mutableCopy];
|
|
96
|
+
|
|
97
|
+
if (letterSpacing != 0.0) {
|
|
98
|
+
// NSKernAttributeName is in points — same unit as fontSize on iOS.
|
|
99
|
+
attrs[NSKernAttributeName] = @(letterSpacing);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
NSString *safeText = (text.length > 0) ? text : @"";
|
|
103
|
+
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:safeText
|
|
104
|
+
attributes:attrs];
|
|
105
|
+
|
|
106
|
+
// ── 6. TextKit layout ──────────────────────────────────────────────────────
|
|
107
|
+
//
|
|
108
|
+
// We use the full TextKit stack (NSTextStorage → NSLayoutManager → NSTextContainer)
|
|
109
|
+
// rather than boundingRectWithSize:options:context: because:
|
|
110
|
+
// • It correctly handles numberOfLines (via maximumNumberOfLines).
|
|
111
|
+
// • It gives us a real lineCount via enumerateLineFragments.
|
|
112
|
+
// • It is the same engine UILabel uses internally.
|
|
113
|
+
//
|
|
114
|
+
NSTextStorage *storage = [[NSTextStorage alloc] initWithAttributedString:attrString];
|
|
115
|
+
NSLayoutManager *manager = [[NSLayoutManager alloc] init];
|
|
116
|
+
NSTextContainer *container = [[NSTextContainer alloc] initWithSize:CGSizeMake(maxWidth, maxHeight)];
|
|
117
|
+
|
|
118
|
+
container.lineFragmentPadding = 0.0; // no extra horizontal padding
|
|
119
|
+
container.lineBreakMode = NSLineBreakByWordWrapping;
|
|
120
|
+
if (maxLines > 0) container.maximumNumberOfLines = (NSUInteger)maxLines;
|
|
121
|
+
|
|
122
|
+
[manager addTextContainer:container];
|
|
123
|
+
[storage addLayoutManager:manager];
|
|
124
|
+
|
|
125
|
+
// Force complete layout pass
|
|
126
|
+
[manager ensureLayoutForTextContainer:container];
|
|
127
|
+
|
|
128
|
+
CGRect usedRect = [manager usedRectForTextContainer:container];
|
|
129
|
+
|
|
130
|
+
// Count lines by enumerating actual line fragments
|
|
131
|
+
__block NSInteger lineCount = 0;
|
|
132
|
+
NSRange glyphRange = [manager glyphRangeForTextContainer:container];
|
|
133
|
+
[manager enumerateLineFragmentsForGlyphRange:glyphRange
|
|
134
|
+
usingBlock:^(CGRect rect,
|
|
135
|
+
CGRect usedLineRect,
|
|
136
|
+
NSTextContainer *tc,
|
|
137
|
+
NSRange glyphRng,
|
|
138
|
+
BOOL *stop) {
|
|
139
|
+
lineCount++;
|
|
140
|
+
}];
|
|
141
|
+
|
|
142
|
+
// ceil to avoid sub-pixel gaps when the caller uses these values for layout
|
|
143
|
+
CGFloat measuredWidth = ceil(usedRect.size.width);
|
|
144
|
+
CGFloat measuredHeight = ceil(usedRect.size.height);
|
|
145
|
+
|
|
146
|
+
return @{
|
|
147
|
+
@"width": @(measuredWidth),
|
|
148
|
+
@"height": @(measuredHeight),
|
|
149
|
+
@"lineCount": @(lineCount),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
155
|
+
// Module
|
|
156
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
@implementation ReactNativeTextMeasure
|
|
159
|
+
|
|
160
|
+
RCT_EXPORT_MODULE()
|
|
161
|
+
|
|
162
|
+
// ── Async (recommended) ───────────────────────────────────────────────────────
|
|
163
|
+
/**
|
|
164
|
+
* measureText(text, options) → Promise<{ width, height, lineCount }>
|
|
165
|
+
*
|
|
166
|
+
* Runs on a background serial queue — does NOT block the JS thread.
|
|
167
|
+
*/
|
|
168
|
+
RCT_EXPORT_METHOD(measureText:(NSString *)text
|
|
169
|
+
options:(NSDictionary *)options
|
|
170
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
171
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
172
|
+
{
|
|
173
|
+
// Capture to avoid potential mutation
|
|
174
|
+
NSString *capturedText = [text copy];
|
|
175
|
+
NSDictionary *capturedOptions = [options copy];
|
|
176
|
+
|
|
177
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
178
|
+
@try {
|
|
179
|
+
NSDictionary *result = RNTMPerformMeasure(capturedText, capturedOptions ?: @{});
|
|
180
|
+
resolve(result);
|
|
181
|
+
} @catch (NSException *exception) {
|
|
182
|
+
reject(@"MEASURE_ERROR", exception.reason, nil);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Synchronous (use sparingly) ───────────────────────────────────────────────
|
|
188
|
+
/**
|
|
189
|
+
* measureTextSync(text, options) → { width, height, lineCount }
|
|
190
|
+
*
|
|
191
|
+
* Runs on the JS thread. Safe for use in synchronous layout code but
|
|
192
|
+
* will block JS execution for the duration of the measurement.
|
|
193
|
+
*/
|
|
194
|
+
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(measureTextSync:(NSString *)text
|
|
195
|
+
options:(NSDictionary *)options)
|
|
196
|
+
{
|
|
197
|
+
@try {
|
|
198
|
+
return RNTMPerformMeasure(text ?: @"", options ?: @{});
|
|
199
|
+
} @catch (NSException *exception) {
|
|
200
|
+
return @{ @"error": exception.reason ?: @"Unknown error" };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@end
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// !$*UTF8*$!
|
|
2
|
+
{
|
|
3
|
+
archiveVersion = 1;
|
|
4
|
+
classes = {
|
|
5
|
+
};
|
|
6
|
+
objectVersion = 46;
|
|
7
|
+
objects = {
|
|
8
|
+
|
|
9
|
+
/* Begin PBXCopyFilesBuildPhase section */
|
|
10
|
+
58B511D91A9E6C8500147676 /* CopyFiles */ = {
|
|
11
|
+
isa = PBXCopyFilesBuildPhase;
|
|
12
|
+
buildActionMask = 2147483647;
|
|
13
|
+
dstPath = "include/$(PRODUCT_NAME)";
|
|
14
|
+
dstSubfolderSpec = 16;
|
|
15
|
+
files = (
|
|
16
|
+
);
|
|
17
|
+
runOnlyForDeploymentPostprocessing = 0;
|
|
18
|
+
};
|
|
19
|
+
/* End PBXCopyFilesBuildPhase section */
|
|
20
|
+
|
|
21
|
+
/* Begin PBXFileReference section */
|
|
22
|
+
134814201AA4EA6300B7C361 /* libReactNativeTextMeasure.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReactNativeTextMeasure.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
23
|
+
/* End PBXFileReference section */
|
|
24
|
+
|
|
25
|
+
/* Begin PBXFrameworksBuildPhase section */
|
|
26
|
+
58B511D81A9E6C8500147676 /* Frameworks */ = {
|
|
27
|
+
isa = PBXFrameworksBuildPhase;
|
|
28
|
+
buildActionMask = 2147483647;
|
|
29
|
+
files = (
|
|
30
|
+
);
|
|
31
|
+
runOnlyForDeploymentPostprocessing = 0;
|
|
32
|
+
};
|
|
33
|
+
/* End PBXFrameworksBuildPhase section */
|
|
34
|
+
|
|
35
|
+
/* Begin PBXGroup section */
|
|
36
|
+
134814211AA4EA7D00B7C361 /* Products */ = {
|
|
37
|
+
isa = PBXGroup;
|
|
38
|
+
children = (
|
|
39
|
+
134814201AA4EA6300B7C361 /* libReactNativeTextMeasure.a */,
|
|
40
|
+
);
|
|
41
|
+
name = Products;
|
|
42
|
+
sourceTree = "<group>";
|
|
43
|
+
};
|
|
44
|
+
58B511D21A9E6C8500147676 = {
|
|
45
|
+
isa = PBXGroup;
|
|
46
|
+
children = (
|
|
47
|
+
134814211AA4EA7D00B7C361 /* Products */,
|
|
48
|
+
);
|
|
49
|
+
sourceTree = "<group>";
|
|
50
|
+
};
|
|
51
|
+
/* End PBXGroup section */
|
|
52
|
+
|
|
53
|
+
/* Begin PBXNativeTarget section */
|
|
54
|
+
58B511DA1A9E6C8500147676 /* ReactNativeTextMeasure */ = {
|
|
55
|
+
isa = PBXNativeTarget;
|
|
56
|
+
buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ReactNativeTextMeasure" */;
|
|
57
|
+
buildPhases = (
|
|
58
|
+
58B511D71A9E6C8500147676 /* Sources */,
|
|
59
|
+
58B511D81A9E6C8500147676 /* Frameworks */,
|
|
60
|
+
58B511D91A9E6C8500147676 /* CopyFiles */,
|
|
61
|
+
);
|
|
62
|
+
buildRules = (
|
|
63
|
+
);
|
|
64
|
+
dependencies = (
|
|
65
|
+
);
|
|
66
|
+
name = ReactNativeTextMeasure;
|
|
67
|
+
productName = RCTDataManager;
|
|
68
|
+
productReference = 134814201AA4EA6300B7C361 /* libReactNativeTextMeasure.a */;
|
|
69
|
+
productType = "com.apple.product-type.library.static";
|
|
70
|
+
};
|
|
71
|
+
/* End PBXNativeTarget section */
|
|
72
|
+
|
|
73
|
+
/* Begin PBXProject section */
|
|
74
|
+
58B511D31A9E6C8500147676 /* Project object */ = {
|
|
75
|
+
isa = PBXProject;
|
|
76
|
+
attributes = {
|
|
77
|
+
LastUpgradeCheck = 0920;
|
|
78
|
+
ORGANIZATIONNAME = Facebook;
|
|
79
|
+
TargetAttributes = {
|
|
80
|
+
58B511DA1A9E6C8500147676 = {
|
|
81
|
+
CreatedOnToolsVersion = 6.1.1;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ReactNativeTextMeasure" */;
|
|
86
|
+
compatibilityVersion = "Xcode 3.2";
|
|
87
|
+
developmentRegion = en;
|
|
88
|
+
hasScannedForEncodings = 0;
|
|
89
|
+
knownRegions = (
|
|
90
|
+
en,
|
|
91
|
+
Base,
|
|
92
|
+
);
|
|
93
|
+
mainGroup = 58B511D21A9E6C8500147676;
|
|
94
|
+
productRefGroup = 58B511D21A9E6C8500147676;
|
|
95
|
+
projectDirPath = "";
|
|
96
|
+
projectRoot = "";
|
|
97
|
+
targets = (
|
|
98
|
+
58B511DA1A9E6C8500147676 /* ReactNativeTextMeasure */,
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
/* End PBXProject section */
|
|
102
|
+
|
|
103
|
+
/* Begin PBXSourcesBuildPhase section */
|
|
104
|
+
58B511D71A9E6C8500147676 /* Sources */ = {
|
|
105
|
+
isa = PBXSourcesBuildPhase;
|
|
106
|
+
buildActionMask = 2147483647;
|
|
107
|
+
files = (
|
|
108
|
+
);
|
|
109
|
+
runOnlyForDeploymentPostprocessing = 0;
|
|
110
|
+
};
|
|
111
|
+
/* End PBXSourcesBuildPhase section */
|
|
112
|
+
|
|
113
|
+
/* Begin XCBuildConfiguration section */
|
|
114
|
+
58B511ED1A9E6C8500147676 /* Debug */ = {
|
|
115
|
+
isa = XCBuildConfiguration;
|
|
116
|
+
buildSettings = {
|
|
117
|
+
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
118
|
+
CLANG_ANALYZER_NONNULL = YES;
|
|
119
|
+
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
|
120
|
+
CLANG_CXX_LIBRARY = "libc++";
|
|
121
|
+
CLANG_ENABLE_MODULES = YES;
|
|
122
|
+
CLANG_ENABLE_OBJC_ARC = YES;
|
|
123
|
+
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
124
|
+
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
125
|
+
CLANG_WARN_COMMA = YES;
|
|
126
|
+
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
127
|
+
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
128
|
+
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
129
|
+
CLANG_WARN_EMPTY_BODY = YES;
|
|
130
|
+
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
131
|
+
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
132
|
+
CLANG_WARN_INT_CONVERSION = YES;
|
|
133
|
+
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
134
|
+
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
135
|
+
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
136
|
+
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
137
|
+
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
138
|
+
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
139
|
+
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
140
|
+
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
141
|
+
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
142
|
+
COPY_PHASE_STRIP = NO;
|
|
143
|
+
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
144
|
+
ENABLE_TESTABILITY = YES;
|
|
145
|
+
GCC_C_LANGUAGE_STANDARD = gnu99;
|
|
146
|
+
GCC_DYNAMIC_NO_PIC = NO;
|
|
147
|
+
GCC_NO_COMMON_BLOCKS = YES;
|
|
148
|
+
GCC_OPTIMIZATION_LEVEL = 0;
|
|
149
|
+
GCC_PREPROCESSOR_DEFINITIONS = (
|
|
150
|
+
"DEBUG=1",
|
|
151
|
+
"$(inherited)",
|
|
152
|
+
);
|
|
153
|
+
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
|
154
|
+
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
155
|
+
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
156
|
+
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
157
|
+
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
158
|
+
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
159
|
+
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
160
|
+
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
|
161
|
+
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
|
162
|
+
LIBRARY_SEARCH_PATHS = (
|
|
163
|
+
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
|
|
164
|
+
"\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"",
|
|
165
|
+
"\"$(inherited)\"",
|
|
166
|
+
);
|
|
167
|
+
MTL_ENABLE_DEBUG_INFO = YES;
|
|
168
|
+
ONLY_ACTIVE_ARCH = YES;
|
|
169
|
+
SDKROOT = iphoneos;
|
|
170
|
+
};
|
|
171
|
+
name = Debug;
|
|
172
|
+
};
|
|
173
|
+
58B511EE1A9E6C8500147676 /* Release */ = {
|
|
174
|
+
isa = XCBuildConfiguration;
|
|
175
|
+
buildSettings = {
|
|
176
|
+
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
177
|
+
CLANG_ANALYZER_NONNULL = YES;
|
|
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
|
+
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
|
214
|
+
LIBRARY_SEARCH_PATHS = (
|
|
215
|
+
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
|
|
216
|
+
"\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"",
|
|
217
|
+
"\"$(inherited)\"",
|
|
218
|
+
);
|
|
219
|
+
MTL_ENABLE_DEBUG_INFO = NO;
|
|
220
|
+
SDKROOT = iphoneos;
|
|
221
|
+
VALIDATE_PRODUCT = YES;
|
|
222
|
+
};
|
|
223
|
+
name = Release;
|
|
224
|
+
};
|
|
225
|
+
58B511F01A9E6C8500147676 /* Debug */ = {
|
|
226
|
+
isa = XCBuildConfiguration;
|
|
227
|
+
buildSettings = {
|
|
228
|
+
HEADER_SEARCH_PATHS = (
|
|
229
|
+
"$(inherited)",
|
|
230
|
+
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
|
231
|
+
"$(SRCROOT)/../../../React/**",
|
|
232
|
+
"$(SRCROOT)/../../react-native/React/**",
|
|
233
|
+
);
|
|
234
|
+
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
|
235
|
+
OTHER_LDFLAGS = "-ObjC";
|
|
236
|
+
PRODUCT_NAME = ReactNativeTextMeasure;
|
|
237
|
+
SKIP_INSTALL = YES;
|
|
238
|
+
};
|
|
239
|
+
name = Debug;
|
|
240
|
+
};
|
|
241
|
+
58B511F11A9E6C8500147676 /* Release */ = {
|
|
242
|
+
isa = XCBuildConfiguration;
|
|
243
|
+
buildSettings = {
|
|
244
|
+
HEADER_SEARCH_PATHS = (
|
|
245
|
+
"$(inherited)",
|
|
246
|
+
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
|
247
|
+
"$(SRCROOT)/../../../React/**",
|
|
248
|
+
"$(SRCROOT)/../../react-native/React/**",
|
|
249
|
+
);
|
|
250
|
+
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
|
251
|
+
OTHER_LDFLAGS = "-ObjC";
|
|
252
|
+
PRODUCT_NAME = ReactNativeTextMeasure;
|
|
253
|
+
SKIP_INSTALL = YES;
|
|
254
|
+
};
|
|
255
|
+
name = Release;
|
|
256
|
+
};
|
|
257
|
+
/* End XCBuildConfiguration section */
|
|
258
|
+
|
|
259
|
+
/* Begin XCConfigurationList section */
|
|
260
|
+
58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ReactNativeTextMeasure" */ = {
|
|
261
|
+
isa = XCConfigurationList;
|
|
262
|
+
buildConfigurations = (
|
|
263
|
+
58B511ED1A9E6C8500147676 /* Debug */,
|
|
264
|
+
58B511EE1A9E6C8500147676 /* Release */,
|
|
265
|
+
);
|
|
266
|
+
defaultConfigurationIsVisible = 0;
|
|
267
|
+
defaultConfigurationName = Release;
|
|
268
|
+
};
|
|
269
|
+
58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ReactNativeTextMeasure" */ = {
|
|
270
|
+
isa = XCConfigurationList;
|
|
271
|
+
buildConfigurations = (
|
|
272
|
+
58B511F01A9E6C8500147676 /* Debug */,
|
|
273
|
+
58B511F11A9E6C8500147676 /* Release */,
|
|
274
|
+
);
|
|
275
|
+
defaultConfigurationIsVisible = 0;
|
|
276
|
+
defaultConfigurationName = Release;
|
|
277
|
+
};
|
|
278
|
+
/* End XCConfigurationList section */
|
|
279
|
+
};
|
|
280
|
+
rootObject = 58B511D31A9E6C8500147676 /* Project object */;
|
|
281
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-text-measure",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Accurately measure text dimensions (width, height, lineCount) using native text layout engines — no view rendered.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/AminAllahham/react-native-text-measure.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"react-native",
|
|
13
|
+
"text",
|
|
14
|
+
"measure",
|
|
15
|
+
"height",
|
|
16
|
+
"width",
|
|
17
|
+
"layout",
|
|
18
|
+
"native-module"
|
|
19
|
+
],
|
|
20
|
+
"author": "Amin Allahham <amin.allahham9@gmail.com>",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"react-native": ">=0.60.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"react-native": "^0.73.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# react-native-text-measure.podspec
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
6
|
+
|
|
7
|
+
Pod::Spec.new do |s|
|
|
8
|
+
s.name = "react-native-text-measure"
|
|
9
|
+
s.version = package["version"]
|
|
10
|
+
s.summary = package["description"]
|
|
11
|
+
s.description = <<-DESC
|
|
12
|
+
react-native-text-measure
|
|
13
|
+
DESC
|
|
14
|
+
s.homepage = "https://github.com/github_account/react-native-text-measure"
|
|
15
|
+
# brief license entry:
|
|
16
|
+
s.license = "MIT"
|
|
17
|
+
# optional - use expanded license entry instead:
|
|
18
|
+
# s.license = { :type => "MIT", :file => "LICENSE" }
|
|
19
|
+
s.authors = { "Your Name" => "yourname@email.com" }
|
|
20
|
+
s.platforms = { :ios => "9.0" }
|
|
21
|
+
s.source = { :git => "https://github.com/github_account/react-native-text-measure.git", :tag => "#{s.version}" }
|
|
22
|
+
|
|
23
|
+
s.source_files = "ios/**/*.{h,c,cc,cpp,m,mm,swift}"
|
|
24
|
+
s.requires_arc = true
|
|
25
|
+
|
|
26
|
+
s.dependency "React"
|
|
27
|
+
# ...
|
|
28
|
+
# s.dependency "..."
|
|
29
|
+
end
|
|
30
|
+
|