Skip to content

Commit

Permalink
add lineInfoForLine feature (android)
Browse files Browse the repository at this point in the history
  • Loading branch information
aMarCruz committed Jan 10, 2019
1 parent f2f9a31 commit 805714c
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 96 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
### Added

- Markdown lint rules.
- `lineInfoForLine` option, to get information for a given line.

### Changed

Expand Down
140 changes: 73 additions & 67 deletions README.md
Expand Up @@ -5,18 +5,19 @@

Measure text accurately before laying it out and get font information from your App (Android and iOS).

There are two main functions: `flatHeights` to obtain the height of different blocks of text simultaneously, optimized for components such as `<FlatList>` or `<RecyclerListView>`.
There are two main functions: `flatHeights` to obtain the height of different blocks of text simultaneously, optimized for components such as [`<FlatList>`][0] or [`<RecyclerListView>`][1].

The other one is `measure`, which gets detailed information about one block of text:

- The width used by the text, with an option to calculate the real width of the largest line.
- Height, with or without paddings.
- The height of the text, with or without paddings.
- The number of lines.
- The width of the last line, if required, useful to save space with "See more..." style labels or time stamps.
- The width of the last line.
- Extended information of a given line.

The width and height are practically the same as those received from the `onLayout` event of a `<Text>` component with the same properties.

In both functions, the text to be measured is required, but the rest of the parameters supported are optional and work in the same way as with React Native:
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:

- `fontFamily`
- `fontSize`
Expand All @@ -31,31 +32,22 @@ In both functions, the text to be measured is required, but the rest of the para

In addition, the library includes functions to obtain information about the fonts visible to the App.

rnTextSize is WIP, but if it has helped you, please support my work with a star :star2: or [buy me a coffee][bmc-url].

---
**IMPORTANT:**

**rnTextSize (react-native-text-size) v2.0 is a complete refactoring, before using it, please unlink the previous version.**

**If `react-native unlink` fails, please reverse the changes described in [Manual Installation][2].**

---

**Requirements:**

- React Native v0.52.0 or later
- Targets Android API 16 and iOS 9.0

The [sample App][1] uses RN v0.52.0, the minimum supported version but, to take advantage of features such as `letterSpacing` and better support for the most modern devices, use RN v0.55 or above.
If it has helped you, please support my work with a star ⭐️ or [ko-fi][kofi-url].

## Installation

Mostly automatic installation from npm

```bash
yarn add react-native-text-size
react-native link react-native-text-size
```

**Requirements:**

- React Native v0.52.0 or later.
- Android API 16 or iOS 9.0 and above.

If you are using Gradle 4 or later, don't forget to change the `compile` directive to `implementation` in the dependencies block of the android/app/build.gradle file.

See [Manual Installation][2] on the Wiki as an alternative if you have problems with automatic installation.
Expand All @@ -76,60 +68,74 @@ See [Manual Installation][2] on the Wiki as an alternative if you have problems

## measure

```js
```ts
measure(options: TSMeasureParams): Promise<TSMeasureResult>
```

This function measures the text as RN does and its result is consistent\* with that of `Text`'s onLayout event. It takes a subset of the properties used by [`<Text>`][3] to describe the font and other options to use.
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 [`<Text>`][3] to describe the font and other options to use.

If you provide the `width`, the measurement will apply automatic wrapping in addition to the explicit line breaks.
If you provide `width`, the measurement will apply automatic wrapping in addition to the explicit line breaks.

\* _There may be some inconsistencies in iOS, see this [Know Issue](#incorrent-height-ios) to know more._

**NOTE:**
**Note:**

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.

### TSMeasureParams

Plain JS object with this properties:

Property | Type | Default | Notes
---------- | ------ | -------- | ------
text | string | (none) | This is the only required parameter and may include _emojis_ or be empty, but it **must not be** `null`.<br>If this is an empty string the resulting `width` will be zero.
width | number | Infinity | Restrict the width. The resulting height will vary depending on the automatic flow of the text.
usePreciseWidth | boolean | false | If `true`, the result will include an exact `width` and the `lastLineWidth` property.<br>You can see the effect of this flag in the [sample App][1].
fontFamily | string | OS dependent | The default is the same applied by React Native: Roboto in Android, San Francisco in iOS.<br>**Note:** Device manufacturer or custom ROM can change the default font.
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).
fontSize | number | 14 | The default font size comes from RN.
fontStyle | string | 'normal' | One of "normal" or "italic".
fontVariant | array | (none) | _iOS only_
allowFontScaling | boolean | true | To respect the user' setting of large fonts (i.e. use SP units).
letterSpacing | number | (none) | Additional spacing between characters (aka `tracking`).<br>**Note:** In iOS a zero cancels automatic kerning.<br>_All iOS, Android with API 21+ and RN 0.55+_
includeFontPadding | boolean | true | Include additional top and bottom padding, to avoid clipping certain characters.<br>_Android only_
textBreakStrategy | string | 'highQuality' | One of 'simple', 'balanced', or 'highQuality'.<br>_Android only, with API 23+_

The [sample App][1] shows interactively the effect of these parameters on the screen.
Plain JS object with this properties (only `text` is required):

Property | Type | Default | Notes
------------------ | ------ | -------- | ------
text | string | (none) | This is the only required parameter and may include _emojis_ or be empty, but it **must not be** `null`.<br>If this is an empty string the resulting `width` will be zero.
fontFamily | string | OS dependent | The default is the same applied by React Native: Roboto in Android, San Francisco in iOS.<br>**Note:** Device manufacturer or custom ROM can change the default font.
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).
fontSize | number | 14 | The default font size comes from RN.
fontStyle | string | 'normal' | One of "normal" or "italic".
fontVariant | array | (none) | _iOS only_
allowFontScaling | boolean | true | To respect the user' setting of large fonts (i.e. use SP units).
letterSpacing | number | (none) | Additional spacing between characters (aka `tracking`).<br>**Note:** In iOS a zero cancels automatic kerning.<br>_All iOS, Android with API 21+ and RN 0.55+_
includeFontPadding | boolean | true | Include additional top and bottom padding, to avoid clipping certain characters.<br>_Android only_
textBreakStrategy | string | 'highQuality' | One of 'simple', 'balanced', or 'highQuality'.<br>_Android only, with API 23+_
width | number | MAX_INT | Restrict the width. The resulting height will vary depending on the automatic flow of the text.
usePreciseWidth | boolean | false | If `true`, the result will include an exact `width` and the `lastLineWidth` property.<br>You can see the effect of this flag in the [sample App][sample-app].
lineInfoForLine | number | (none) | If `>=0`, the result will include a [lineInfo](#lineinfo) property with information for the required line number.

The [sample App][sample-app] shows interactively the effect of these parameters on the screen.

### TSMeasureResult

`measure` returns a Promise that resolves to a plain JS object with this properties:
`measure` returns a Promise that resolves to a JS object with this properties:

Property | Type | Notes
------------- | ------ | ------
width | number | Total used width. It may be less or equal to the `width` option.<br>On Android, this value may vary depending on the `usePreciseWidth` flag.
height | number | Total height, including top and bottom padding if `includingFontPadding` was set (the default).
lastLineWidth | number | Width of the last line, without trailing blanks.<br>If `usePreciseWidth` is `false` (the default), this property is undefined.
lineCount | number | Number of lines, taking into account hard and automatic line breaks.
lineInfo | object | Line information.<br>If the `lineInfoForLine` option is not given, this property is undefined.

#### lineInfo

If the value of the `lineInfoForLine` is greater or equal than `lineCount`, this info is for the last line (i.e. `lineCount` - 1).

Property | Type | Notes
--------- | ------ | ------
width | number | Total used width. It may be less or equal to the given width.<br>On Android this value may vary depending on the `usePreciseWidth` flag.
height | number | Total height, including top and bottom padding if `includingFontPadding` was set (the default).
lastLineWidth | number | Width of the last line, without trailing blanks.<br>If `usePreciseWidth` is `false` (the default), this field is undefined.
lineCount | number | Number of lines, taking into account hard and automatic line breaks.
Property | Type | Notes
------------- | ------ | ------
line | number | Line number of this info, base 0.<br>It can be less than the requested line number if `lineInfoForLine` is out of range.
start | number | Text offset of the beginning of this line.
end | number | Text offset after the last _visible_ character (so whitespace is not counted) on this line.
bottom | number | The vertical position of the bottom of this line, including padding.
width | number | Horizontal extent of this line, including leading margin indent, but excluding trailing whitespace.<br>Use `usePreciseWidth:true` to get an accurate value for this property.

In case of error, the promise is rejected with an extended Error object with one of the following error codes, as a literal string:

Code | Details
---- | -------
Code | Details
-------------------- | -------
E_MISSING_PARAMETERS | `measure` requires an object with the parameters, which was not provided.
E_MISSING_TEXT | The text to measure is `null` or was not provided.
E_INVALID_FONT_SPEC | The font specification is not valid. It is unlikely that this will happen on Android.
E_UNKNOWN_ERROR | Well... who knows?
E_MISSING_TEXT | The text to measure is `null` or was not provided.
E_INVALID_FONT_SPEC | The font specification is not valid. It is unlikely that this will happen on Android.
E_UNKNOWN_ERROR | Well... who knows?

### Example

Expand Down Expand Up @@ -205,7 +211,7 @@ In the future I will prepare an example of its use with FlatList and multiple st

### TSHeightsParams

This is an object similar to the one you pass to `measure`, but the `text` property is an array of strings and the `usePreciseWidth` property is ignored.
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.

Property | Type | Default
------------------- | -------- | --------
Expand Down Expand Up @@ -248,9 +254,9 @@ fontWeight | TSFontWeight | Only if 'bold', undefined if the weight is 'norma
fontVariant | TSFontVariant[] or null | _iOS only_. Currently, no style includes this property.
letterSpacing | number | Omitted if running on Android with RN lower than 0.55

To know the key names, please see [Keys from specsForTextStyles][6] the Wiki.
To know the key names, please see [Keys from specsForTextStyles][6] in the Wiki.

I have not tried to normalize these keys since, with the exception of 2 or 3, they have a different interpretation in each OS. You will know how to use them to create custom styles according to your needs.
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.

## fontFromSpecs

Expand Down Expand Up @@ -283,7 +289,7 @@ Property | Type | Details
----------- | -------- | --------
fontFamily | string | In Android it is the same string passed as parameter.
fontName | string |_iOS only_, always `undefined` in Android.
fontSize | number | It may be different from the given parameter if it includes decimals.
fontSize | number | It may be different from the given parameter if the parameter includes decimals.
fontStyle | string | 'normal' or 'italic'.
fontWeight | string | 'normal' or 'bold', on iOS it can go from '100' to '900'.
fontVariant | string[] | _iOS only_, always `undefined` in Android.
Expand All @@ -295,7 +301,7 @@ top | number | _Android only_. Maximum distance above the baseline for
bottom | number | _Android only_. Maximum distance below the baseline for the lowest glyph in the font.
leading | number | The recommended additional space to add between lines of text.
lineHeight | number | The recommended line height. It should be greater if text contain Unicode symbols, such as emojis.
_hash | number | Hash code, maybe useful for debugging.
_hash | number | Hash code, may be useful for debugging.

\* _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._

Expand Down Expand Up @@ -329,7 +335,7 @@ Wrapper for the `UIFont.fontNamesForFamilyName` method of UIKit, returns an arra

You can use the rnTextSize's `fontFamilyNames` function to get an array of the available font family names on the system.

**iOS only**, on Android this function always resolves to `null`.
This is an **iOS only** function, on Android it always resolves to `null`.

## Known Issues

Expand All @@ -347,18 +353,16 @@ I hope that a future version of RN solves this issue.

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.

### Nexted Text
### Nested Text

Nested `<Text>` components (or with images inside) can be rasterized with dimensions different from those calculated, rnTextSize does not accept multiple sizes in the text.
Nested `<Text>` components (or with images inside) can be rasterized with dimensions different from those calculated, rnTextSize does not accept multiple sizes.

## TODO

- [X] Normalized tracking or letter spacing in font info.
- [ ] More testing, including Android and iOS TVs.
- [ ] Learn the beautiful English, to make better docs.
- [ ] Learn the ugly Objective-C, after almost a month of studying I don't find it pretty.
- [ ] And a lot of more things.
- [ ] Ahh a... lot of money, of course. I need a Mac 😆 so...
- [ ] Find something nice in the ugly Objective-C.

## Support my Work

Expand All @@ -381,7 +385,9 @@ The [BSD 2-Clause](LICENSE) "Simplified" License.
[license-badge]: https://img.shields.io/badge/license-BSD%202--Clause-blue.svg
[license-url]: https://github.com/aMarCruz/react-native-text-size/blob/master/LICENSE
[kofi-url]: https://ko-fi.com/C0C7LF7I
[1]: https://github.com/aMarCruz/rn-text-size-sample-app
[sample-app]: https://github.com/aMarCruz/rn-text-size-sample-app
[0]: https://facebook.github.io/react-native/docs/flatlist
[1]: https://www.npmjs.com/package/recyclerlistview
[2]: https://github.com/aMarCruz/react-native-text-size/wiki/Manual-Installation
[3]: https://facebook.github.io/react-native/docs/text#props
[4]: https://developer.apple.com/documentation/uikit/uifont/1619030-preferredfontfortextstyle
Expand Down
Expand Up @@ -35,7 +35,6 @@ final class RNTextSizeConf {
reactNativeVersion = version;
}


/**
* Make a Typeface from the supplied font family and style.
*/
Expand Down
Expand Up @@ -140,7 +140,6 @@ public void measure(@Nullable final ReadableMap specs, final Promise promise) {
final int lineCount = layout.getLineCount();
float rectWidth;

// go more faster?
if (conf.getBooleanOrTrue("usePreciseWidth")) {
float lastWidth = 0f;
// Layout.getWidth() returns the configured max width, we must
Expand All @@ -161,9 +160,16 @@ public void measure(@Nullable final ReadableMap specs, final Promise promise) {
result.putDouble("height", layout.getHeight() / density);
result.putInt("lineCount", lineCount);

Integer lineEndForLineNo = conf.getIntOrNull("lineEndForLineNo");
if (lineEndForLineNo != null) {
result.putInt("lineEnd", layout.getLineVisibleEnd(lineEndForLineNo));
Integer lineInfoForLine = conf.getIntOrNull("lineInfoForLine");
if (lineInfoForLine != null && lineInfoForLine >= 0) {
final int line = Math.max(lineInfoForLine, lineCount);
final WritableMap info = Arguments.createMap();
info.putInt("line", line);
info.putInt("start", layout.getLineStart(line));
info.putInt("end", layout.getLineVisibleEnd(line));
info.putDouble("bottom", layout.getLineBottom(line) / density);
info.putDouble("width", layout.getLineMax(line) / density);
result.putMap("lineInfo", info);
}

promise.resolve(result);
Expand Down Expand Up @@ -332,8 +338,7 @@ public void fontFamilyNames(final Promise promise) {
}

/**
* TODO:
* Maybe some day?
* Android does not have font name info.
*/
@SuppressWarnings("unused")
@ReactMethod
Expand Down Expand Up @@ -446,8 +451,10 @@ private void getFontsInAssets(@NonNull WritableArray destArr) {
try {
String[] list = assetManager.list(FONTS_ASSET_PATH);

for (String spec : list) {
addFamilyToArray(tmpArr, spec);
if (list != null) {
for (String spec : list) {
addFamilyToArray(tmpArr, spec);
}
}
} catch (IOException ex) {
ex.printStackTrace();
Expand Down

0 comments on commit 805714c

Please sign in to comment.