Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Improve text wrapping, add overflow property #25

Merged
merged 3 commits into from
Oct 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,23 +78,24 @@ const styles = StyleSheet.create({

The `ResponsiveText` component accepts all the properties available to the `Text`` component from React Native Skia, while also introducing additional features. These include the ability to adjust text alignment, set the number of lines, and the ellipsize mode, among others.

| Name | Type | Default | Required | Description |
| --------------------- | ------------------------------------------------------------------------------------------ | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| font | `SkFont` | - | yes | Font to use (React Native Skia doesn't require this property but `ResponsiveText` does in order to properly wrap text) |
| text | `string` | - | yes | Text to draw |
| x | `number` | 0 | no | Left position of the text |
| y | `number` | 0 | no | Top position the text |
| width | `number` | 0 | no | Width of the text component (isn't required but should be specified to properly render text) |
| height | `number` | 0 | no | Height of the text component (used only for the vertical alignment. Overflowing text is visible) |
| lineHeight | `number` | fontSize | no | Text line height |
| horizontalAlignment | `'center'` | `'center-left'` | `'center-right'` | `'left'` | `'right'` | `left` | no | Text alignment in the X axis |
| verticalAlignment | `'bottom'` | `'center'` | `'top'` | `top` | no | Text alignment in the Y axis |
| numberOfLines | `number` | - | no | Maximum number of text lines (the overflowing text will be cropped to occupy at most `numberOfLines` lines) |
| ellipsizeMode | `'clip'` | `'head'` | `'middle'` | `'tail'` | `'tail'` | no | Determines how the overflowing text will be truncated |
| color | `string` | `#000000` | no | The color of the text |
| backgroundColor | `string` | - | no | The color of the text background |
| animationSettings | `AnimationSettings`\* | - | no | Text lines transition animation settings (on alignment, lineHeight, text height changes) |
| animationProgress\*\* | `SharedValue<number>` | - | no | Custom animation progress (can be used instead of animationSettings) |
| Name | Type | Default | Required | Description |
| --------------------- | ------------------------------------------------------------------------------------------ | ---------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| font | `SkFont` | - | yes | Font to use (React Native Skia doesn't require this property but `ResponsiveText` does in order to properly wrap text) |
| text | `string` | - | yes | Text to draw |
| x | `number` | 0 | no | Left position of the text |
| y | `number` | 0 | no | Top position the text |
| width | `number` | text line width | no | Width of the text component (isn't required but should be specified to properly render text) |
| height | `number` | text line height | no | Height of the text component (used only for the vertical alignment. Overflowing text is visible) |
| lineHeight | `number` | fontSize | no | Text line height |
| overflow | `'hidden'` &#124; `'visible'` | `'hidden'` | no | By default, React Native `Text` component is cropped to dimensions of the container. The `ResponsiveText` behaves similarly and crops overflowing content |
| horizontalAlignment | `'center'` &#124; `'center-left'` &#124; `'center-right'` &#124; `'left'` &#124; `'right'` | `left` | no | Text alignment in the X axis |
| verticalAlignment | `'bottom'` &#124; `'center'` &#124; `'top'` | `top` | no | Text alignment in the Y axis |
| numberOfLines | `number` | - | no | Maximum number of text lines (the overflowing text will be cropped to occupy at most `numberOfLines` lines) |
| ellipsizeMode | `'clip'` &#124; `'head'` &#124; `'middle'` &#124; `'tail'` | `'tail'` | no | Determines how the overflowing text will be truncated |
| color | `string` | `#000000` | no | The color of the text |
| backgroundColor | `string` | - | no | The color of the text background |
| animationSettings | `AnimationSettings`\* | - | no | Text lines transition animation settings (on alignment, lineHeight, text height changes) |
| animationProgress\*\* | `SharedValue<number>` | - | no | Custom animation progress (can be used instead of animationSettings) |

\*`AnimationSettings` type contains the following properties:

Expand Down
2 changes: 0 additions & 2 deletions example/babel.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
const path = require('path');

console.log(__dirname);

const rootDir = path.resolve(__dirname, '..');
const rootPkg = require(path.join(rootDir, 'package.json'));

Expand Down
23 changes: 23 additions & 0 deletions example/src/components/StyleEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Animated, { FadeIn, useSharedValue } from 'react-native-reanimated';
import {
EllipsizeMode,
HorizontalAlignment,
TextOverflow,
VerticalAlignment
} from 'react-native-skia-responsive-text';
import { EASING } from 'src/constants';
Expand Down Expand Up @@ -58,6 +59,14 @@ const animationTypeOptions: Array<{
{ label: 'Timing-based', value: 'timing' }
];

const overflowOptions: Array<{
label: TextOverflow;
value: TextOverflow;
}> = [
{ label: 'hidden', value: 'hidden' },
{ label: 'visible', value: 'visible' }
];

const easingOptions: Array<{
label: keyof typeof EASING;
value: keyof typeof EASING;
Expand Down Expand Up @@ -90,6 +99,7 @@ export default function StyleEditor({
horizontalAlignmentValue,
lineHeight,
numberOfLines,
overflow,
setAnimationDuration,
setAnimationEasing,
setAnimationProgress,
Expand All @@ -101,6 +111,7 @@ export default function StyleEditor({
setLineHeight,
setNumberOfLines,
setText,
setTextOverflow,
setVerticalAlignment,
setWidth,
text,
Expand Down Expand Up @@ -133,6 +144,18 @@ export default function StyleEditor({
</View>
</View>

<View style={styles.section}>
<Text style={styles.sectionLabel}>Overflow</Text>
<View style={styles.sectionInput}>
<SelectInput
items={overflowOptions}
placeholder='Text overflow'
value={overflow}
onChange={setTextOverflow}
/>
</View>
</View>

<View style={styles.section}>
<Text style={styles.sectionLabel}>lineHeight</Text>
<View style={styles.sectionInput}>
Expand Down
2 changes: 2 additions & 0 deletions example/src/components/TextPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export default function TextPreview({
horizontalAlignmentValue,
lineHeight,
numberOfLines,
overflow,
text,
verticalAlignment,
verticalAlignmentValue,
Expand Down Expand Up @@ -117,6 +118,7 @@ export default function TextPreview({
horizontalAlignment={horizontalAlignmentValue}
lineHeight={lineHeight}
numberOfLines={numberOfLines}
overflow={overflow}
text={text}
verticalAlignment={verticalAlignmentValue}
width={width}
Expand Down
6 changes: 6 additions & 0 deletions example/src/context/StyleEditorContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SharedValue, useSharedValue } from 'react-native-reanimated';
import {
EllipsizeMode,
HorizontalAlignment,
TextOverflow,
VerticalAlignment
} from 'react-native-skia-responsive-text';
import { EASING } from 'src/constants';
Expand All @@ -22,6 +23,7 @@ type StyleEditorContextType = {
horizontalAlignmentValue: SharedValue<HorizontalAlignment>;
lineHeight: number | undefined;
numberOfLines: number | undefined;
overflow: TextOverflow | undefined;
setAnimationDuration: (animationDuration: number | undefined) => void;
setAnimationEasing: (
animationEasing: keyof typeof EASING | undefined
Expand All @@ -39,6 +41,7 @@ type StyleEditorContextType = {
setLineHeight: (lineHeight: number | undefined) => void;
setNumberOfLines: (numberOfLines: number | undefined) => void;
setText: (text: string | undefined) => void;
setTextOverflow: (overflow: TextOverflow | undefined) => void;
setVerticalAlignment: (
verticalAlignment: VerticalAlignment | undefined
) => void;
Expand Down Expand Up @@ -92,6 +95,7 @@ export function StyleEditorProvider({
const [animationProgress, setAnimationProgress] = useState<
SharedValue<number> | undefined
>();
const [overflow, setTextOverflow] = useState<TextOverflow | undefined>();

const horizontalAlignmentValue = useSharedValue<HorizontalAlignment>('left');
const verticalAlignmentValue = useSharedValue<VerticalAlignment>('top');
Expand All @@ -114,6 +118,7 @@ export function StyleEditorProvider({
horizontalAlignmentValue,
lineHeight,
numberOfLines,
overflow,
setAnimationDuration,
setAnimationEasing,
setAnimationProgress,
Expand All @@ -125,6 +130,7 @@ export function StyleEditorProvider({
setLineHeight,
setNumberOfLines,
setText: handleSetText,
setTextOverflow,
setVerticalAlignment,
setWidth,
text,
Expand Down
6 changes: 2 additions & 4 deletions example/src/examples/ReadmeExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
LinearGradient,
Rect,
RoundedRect,
Shadow,
useFont
} from '@shopify/react-native-skia';
import { useEffect, useState } from 'react';
Expand Down Expand Up @@ -103,9 +102,8 @@ export default function ReadmeExample() {
verticalAlignment={verticalAlignment}
width={size}
x={(width - size) / 2}
y={(height - size) / 2}>
<Shadow blur={5} color='rgba(0,0, 0, .25)' dx={0} dy={5} />
</ResponsiveText>
y={(height - size) / 2}
/>
</Canvas>
);
}
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@
},
"react-native": "src/index",
"repository": "git@github.com:MatiPl01/react-native-skia-responsive-text.git",
"resolutions": {
"@shopify/react-native-skia": "0.1.196",
"cliui": "8.0.1",
"react": "18.2.0",
"react-native": "0.72.6",
"react-native-reanimated": "3.3.0"
},
"scripts": {
"bootstrap": "yarn example && yarn",
"build": "yarn clean && bob build",
Expand Down
74 changes: 58 additions & 16 deletions src/components/ResponsiveText.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Group, Rect, SkFont, TextProps } from '@shopify/react-native-skia';
import {
Group,
Mask,
Rect,
SkFont,
TextProps
} from '@shopify/react-native-skia';
import { memo, useMemo } from 'react';
import { runOnJS, SharedValue, useDerivedValue } from 'react-native-reanimated';

Expand All @@ -10,6 +16,7 @@ import {
HorizontalAlignment,
PartialBy,
TextLineData,
TextOverflow,
VerticalAlignment
} from '../types';
import {
Expand All @@ -28,6 +35,7 @@ type ResponsiveTextProps = PartialBy<TextProps, 'x' | 'y'> & {
height?: number;
numberOfLines?: number;
onMeasure?: (width: number, height: number) => void;
overflow?: TextOverflow;
width?: number;
} & AnimatableProps<{
backgroundColor?: string;
Expand All @@ -51,19 +59,21 @@ function ResponsiveText({
children,
ellipsizeMode,
font,
height = 0,
height: heightProp,
horizontalAlignment: horizontalAlignmentProp = 'left',
lineHeight: lineHeightProp,
numberOfLines,
onMeasure,
overflow = 'hidden',
text = '',
verticalAlignment: verticalAlignmentProp = 'top',
width = 0,
width: widthProp,
x = 0,
y = 0,
...rest
}: ResponsiveTextPrivateProps) {
const fontSize = font.getSize();
const width = widthProp ?? font.getTextWidth(text);

// Update animation settings if they are provided
const animationSettings = useMemo(() => {
Expand All @@ -84,6 +94,7 @@ function ResponsiveText({
const verticalAlignment = useAnimatableValue(verticalAlignmentProp);

const textChunks = useMemo(() => getTextChunks(text), [text]);

// Divide text into lines
const textLines = useMemo<Array<TextLineData>>(
() => wrapText(textChunks, font, width, numberOfLines, ellipsizeMode),
Expand All @@ -110,32 +121,36 @@ function ResponsiveText({

return alignments;
}, [textLines]);

const textHeight = useDerivedValue(
() =>
textLines.length * lineHeight.value -
(lineHeight.value - LINE_HEIGHT_MULTIPLIER * fontSize)
);
const backgroundHeight = useDerivedValue(() =>
Math.max(textHeight.value, height)

const backgroundHeight = useDerivedValue(
() => heightProp ?? textHeight.value
);

const verticalAlignmentOffset = useDerivedValue(() =>
getVerticalAlignmentOffset(
textHeight.value,
height,
heightProp,
verticalAlignment.value
)
);

return (
<Group transform={[{ translateX: x }, { translateY: y }]}>
{backgroundColor && (
<Rect
color={backgroundColor}
height={backgroundHeight}
width={width}
y={0}
/>
)}
const backgroundComponent = backgroundColor && (
<Rect
color={backgroundColor}
height={backgroundHeight}
width={width}
y={0}
/>
);

const textComponent = (
<>
{textLines.map((line, i) => (
<TextLine
{...rest}
Expand All @@ -151,6 +166,33 @@ function ResponsiveText({
{children}
</TextLine>
))}
</>
);

const maskElement = useMemo(
() => <Rect color='white' height={backgroundHeight} width={width} />,
[backgroundHeight, width]
);

return (
<Group transform={[{ translateX: x }, { translateY: y }]}>
{overflow === 'hidden' ? (
<>
{backgroundComponent && (
<Mask mask={maskElement} mode='luminance'>
{backgroundComponent}
</Mask>
)}
<Mask mask={maskElement} mode='luminance'>
{textComponent}
</Mask>
</>
) : (
<>
{backgroundComponent}
{textComponent}
</>
)}
</Group>
);
}
Expand Down
5 changes: 4 additions & 1 deletion src/components/TextLine.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Text, TextProps } from '@shopify/react-native-skia';
import { memo } from 'react';
import {
interpolate,
SharedValue,
Expand All @@ -19,7 +20,7 @@ type TextLineProps = Omit<TextProps, 'x' | 'y'> & {
verticalAlignmentOffset: SharedValue<number>;
};

export default function TextLine({
function TextLine({
animationProgress,
animationSettings,
fontSize,
Expand Down Expand Up @@ -86,3 +87,5 @@ export default function TextLine({

return <Text {...rest} x={x} y={y} />;
}

export default memo(TextLine);
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type {
AnimationSettings,
EllipsizeMode,
HorizontalAlignment,
TextOverflow,
VerticalAlignment
} from './types';
export default ResponsiveText;
2 changes: 2 additions & 0 deletions src/types/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export type HorizontalAlignment =
| 'right';

export type VerticalAlignment = 'bottom' | 'center' | 'top';

export type TextOverflow = 'hidden' | 'visible';
Loading