-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Start writing main library code * Finish adding library files
- Loading branch information
Showing
19 changed files
with
642 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
"version": "1.0.0", | ||
"author": "Mateusz Łopaciński <[email protected]>", | ||
"devDependencies": { | ||
"@shopify/react-native-skia": "^0.1.214", | ||
"eslint": "^8.52.0", | ||
"eslint-config-react-native-matipl01": "^1.0.2", | ||
"husky": "^8.0.0", | ||
|
@@ -11,6 +12,7 @@ | |
"prettier": "^3.0.3", | ||
"react": "^18.2.0", | ||
"react-native": "^0.72.5", | ||
"react-native-reanimated": "^3.5.4", | ||
"syncpack": "^11.2.1", | ||
"typescript": "^5.2.2" | ||
}, | ||
|
@@ -26,6 +28,12 @@ | |
] | ||
}, | ||
"main": "index.js", | ||
"peerDependencies": { | ||
"@shopify/react-native-skia": ">=0.1.75", | ||
"react": "*", | ||
"react-native": "*", | ||
"react-native-reanimated": ">=2.0.0" | ||
}, | ||
"private": true, | ||
"publishConfig": { | ||
"access": "public" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import { Group, Rect, SkFont, TextProps } from '@shopify/react-native-skia'; | ||
import { memo, useMemo } from 'react'; | ||
import { runOnJS, SharedValue, useDerivedValue } from 'react-native-reanimated'; | ||
|
||
import { DEFAULT_FONT } from '@/constants'; | ||
import { useAnimatableValue } from '@/hooks'; | ||
import { | ||
AnimatableProps, | ||
AnimationSettings, | ||
EllipsizeMode, | ||
HorizontalAlignment, | ||
PartialBy, | ||
TextLineData, | ||
VerticalAlignment | ||
} from '@/types'; | ||
import { | ||
getTextLinesAlignment, | ||
getVerticalAlignmentOffset, | ||
wrapText | ||
} from '@/utils'; | ||
|
||
import TextLine from './TextLine'; | ||
|
||
type ResponsiveTextProps = PartialBy<TextProps, 'x' | 'y'> & { | ||
ellipsizeMode?: EllipsizeMode; | ||
height?: number; | ||
numberOfLines?: number; | ||
onMeasure?: (width: number, height: number) => void; | ||
width?: number; | ||
} & AnimatableProps<{ | ||
backgroundColor?: string; | ||
font: SkFont; | ||
horizontalAlignment?: HorizontalAlignment; | ||
lineHeight?: number; | ||
verticalAlignment?: VerticalAlignment; | ||
}> & | ||
( | ||
| { animationProgress?: SharedValue<number> } | ||
| { animationSettings?: AnimationSettings } | ||
); | ||
|
||
function ResponsiveText({ | ||
backgroundColor: backgroundColorProp = 'transparent', | ||
children, | ||
ellipsizeMode, | ||
font = DEFAULT_FONT, | ||
height = 0, | ||
horizontalAlignment: horizontalAlignmentProp = 'left', | ||
lineHeight: lineHeightProp, | ||
numberOfLines, | ||
onMeasure, | ||
text = '', | ||
verticalAlignment: verticalAlignmentProp = 'top', | ||
width = 0, | ||
x = 0, | ||
y = 0, | ||
...rest | ||
}: ResponsiveTextProps) { | ||
const fontSize = font.getSize(); | ||
// Create shared values from animatable props | ||
const backgroundColor = useAnimatableValue(backgroundColorProp); | ||
|
||
const lineHeight = useAnimatableValue(lineHeightProp ?? fontSize); | ||
const horizontalAlignment = useAnimatableValue(horizontalAlignmentProp); | ||
const verticalAlignment = useAnimatableValue(verticalAlignmentProp); | ||
|
||
// Divide text into lines | ||
const textLines = useMemo<Array<TextLineData>>( | ||
() => wrapText(text, font, width, numberOfLines, ellipsizeMode), | ||
[text, font, width, numberOfLines, ellipsizeMode] | ||
); | ||
|
||
// Calculate remaining values | ||
const horizontalAlignmentOffsets = useDerivedValue(() => { | ||
const alignments = getTextLinesAlignment( | ||
textLines, | ||
width, | ||
horizontalAlignment.value | ||
); | ||
|
||
const textWidth = textLines.reduce( | ||
(acc, { width: w }) => Math.max(acc, w), | ||
0 | ||
); | ||
const textHeight = textLines.length * lineHeight.value; | ||
|
||
if (onMeasure) { | ||
runOnJS(onMeasure)(textWidth, textHeight); | ||
} | ||
|
||
return alignments; | ||
}, [textLines]); | ||
const textHeight = useDerivedValue( | ||
() => | ||
textLines.length * lineHeight.value - (lineHeight.value - 1.5 * fontSize) | ||
); | ||
const backgroundHeight = useDerivedValue(() => | ||
Math.max(textHeight.value, height) | ||
); | ||
const verticalAlignmentOffset = useDerivedValue(() => | ||
getVerticalAlignmentOffset( | ||
textHeight.value, | ||
height, | ||
verticalAlignment.value | ||
) | ||
); | ||
|
||
return ( | ||
<Group transform={[{ translateX: x }, { translateY: y }]}> | ||
{backgroundColor && ( | ||
<Rect | ||
color={backgroundColor} | ||
height={backgroundHeight} | ||
width={width} | ||
y={0} | ||
/> | ||
)} | ||
{textLines.map((line, i) => ( | ||
<TextLine | ||
{...rest} | ||
font={font} | ||
fontSize={fontSize} | ||
horizontalAlignmentOffsets={horizontalAlignmentOffsets} | ||
index={i} | ||
key={i} | ||
lineHeight={lineHeight} | ||
text={line.text} | ||
verticalAlignmentOffset={verticalAlignmentOffset}> | ||
{children} | ||
</TextLine> | ||
))} | ||
</Group> | ||
); | ||
} | ||
|
||
export default memo(ResponsiveText); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { Text, TextProps } from '@shopify/react-native-skia'; | ||
import { | ||
interpolate, | ||
SharedValue, | ||
useAnimatedReaction, | ||
useSharedValue, | ||
withTiming | ||
} from 'react-native-reanimated'; | ||
|
||
import { AnimationSettings } from '@/types'; | ||
|
||
type TextLineProps = Omit<TextProps, 'x' | 'y'> & { | ||
animationProgress?: SharedValue<number>; | ||
animationSettings?: AnimationSettings; | ||
fontSize: number; | ||
horizontalAlignmentOffsets: SharedValue<Array<number>>; | ||
index: number; | ||
lineHeight: SharedValue<number>; | ||
verticalAlignmentOffset: SharedValue<number>; | ||
}; | ||
|
||
export default function TextLine({ | ||
animationProgress, | ||
animationSettings, | ||
fontSize, | ||
horizontalAlignmentOffsets, | ||
index, | ||
lineHeight, | ||
verticalAlignmentOffset, | ||
...rest | ||
}: TextLineProps) { | ||
const getCurrentX = () => { | ||
'worklet'; | ||
return horizontalAlignmentOffsets.value[index] ?? 0; | ||
}; | ||
const getCurrentY = () => { | ||
'worklet'; | ||
return verticalAlignmentOffset.value + index * lineHeight.value + fontSize; | ||
}; | ||
|
||
const startX = useSharedValue(getCurrentX()); | ||
const startY = useSharedValue(getCurrentY()); | ||
const prevTargetX = useSharedValue(getCurrentX()); | ||
const prevTargetY = useSharedValue(getCurrentY()); | ||
const x = useSharedValue(getCurrentX()); | ||
const y = useSharedValue(getCurrentY()); | ||
|
||
useAnimatedReaction( | ||
() => ({ | ||
x: horizontalAlignmentOffsets.value[index] ?? 0, | ||
y: verticalAlignmentOffset.value + index * lineHeight.value + fontSize | ||
}), | ||
target => { | ||
if (animationSettings) { | ||
const { onComplete, ...animation } = animationSettings; | ||
x.value = withTiming(target.x, animation, onComplete); | ||
y.value = withTiming(target.y, animation, onComplete); | ||
} else if (!animationProgress) { | ||
x.value = target.x; | ||
y.value = target.y; | ||
} | ||
} | ||
); | ||
|
||
useAnimatedReaction( | ||
() => animationProgress?.value ?? null, | ||
progress => { | ||
if (progress === null) return; | ||
|
||
const targetX = getCurrentX(); | ||
const targetY = getCurrentY(); | ||
|
||
if (targetX !== prevTargetX.value || targetY !== prevTargetY.value) { | ||
startX.value = x.value; | ||
startY.value = y.value; | ||
prevTargetX.value = targetX; | ||
prevTargetY.value = targetY; | ||
} | ||
|
||
x.value = interpolate(progress, [0, 1], [startX.value, targetX]); | ||
y.value = interpolate(progress, [0, 1], [startY.value, targetY]); | ||
} | ||
); | ||
|
||
return <Text {...rest} x={x} y={y} />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as ResponsiveText } from './ResponsiveText'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/* eslint-disable new-cap */ | ||
import { FontStyle, Skia } from '@shopify/react-native-skia'; | ||
import { Platform } from 'react-native'; | ||
|
||
const familyName = Platform.select({ default: 'serif', ios: 'Helvetica' }); | ||
const fontSize = 32; | ||
|
||
const fontMgr = Skia.FontMgr.System(); | ||
|
||
const typeface = fontMgr.matchFamilyStyle(familyName, FontStyle.Bold); | ||
|
||
export const DEFAULT_FONT = Skia.Font(typeface, fontSize); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './font'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as useAnimatableValue } from './useAnimatableValue'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { | ||
isSharedValue, | ||
SharedValue, | ||
useDerivedValue | ||
} from 'react-native-reanimated'; | ||
|
||
import { AnimatableValue } from '@/types'; | ||
|
||
export default function useAnimatableValue<T>( | ||
value: AnimatableValue<T> | ||
): SharedValue<T> { | ||
return useDerivedValue( | ||
() => (isSharedValue<T>(value) ? value.value : (value as T)), | ||
[value] | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export { ResponsiveText } from './components'; | ||
export type { | ||
AnimatableProps, | ||
AnimatableValue, | ||
AnimationSettings, | ||
EllipsizeMode, | ||
HorizontalAlignment, | ||
VerticalAlignment | ||
} from './types'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { EasingFunction } from 'react-native'; | ||
import { EasingFunctionFactory, SharedValue } from 'react-native-reanimated'; | ||
|
||
type AnimationEasing = EasingFunction | EasingFunctionFactory; | ||
|
||
export type AnimationSettings = { | ||
duration?: number; | ||
easing?: AnimationEasing; | ||
onComplete?: (finished?: boolean) => void; | ||
}; | ||
|
||
export type AnimatableValue<T> = T extends infer U | undefined | ||
? U extends undefined | ||
? never | ||
: SharedValue<U> | U | ||
: SharedValue<T> | T; | ||
|
||
export type AnimatableProps<T extends object> = { | ||
[K in keyof T]: AnimatableValue<T[K]>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export * from './animations'; | ||
export * from './layout'; | ||
export * from './text'; | ||
export * from './utils'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export type HorizontalAlignment = | ||
| 'center' | ||
| 'center-left' | ||
| 'center-right' | ||
| 'left' | ||
| 'right'; | ||
|
||
export type VerticalAlignment = 'bottom' | 'center' | 'top'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export type EllipsizeMode = 'clip' | 'head' | 'middle' | 'tail'; | ||
|
||
export type TextLineData = { | ||
text: string; | ||
width: number; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { HorizontalAlignment, TextLineData, VerticalAlignment } from '@/types'; | ||
|
||
export const getTextLinesAlignment = ( | ||
lines: Array<TextLineData>, | ||
width: number, | ||
alignment: HorizontalAlignment = 'left' | ||
): Array<number> => { | ||
'worklet'; | ||
switch (alignment) { | ||
case 'right': | ||
return lines.map(line => width - line.width); | ||
case 'center': | ||
return lines.map(line => (width - line.width) / 2); | ||
case 'center-left': | ||
case 'center-right': | ||
const longestLineWidth = Math.max(...lines.map(line => line.width)); | ||
return lines.map(line => | ||
alignment === 'center-left' | ||
? (width - longestLineWidth) / 2 | ||
: (width + longestLineWidth) / 2 - line.width | ||
); | ||
case 'left': | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
return lines.map(_ => 0); | ||
} | ||
}; | ||
|
||
export const getVerticalAlignmentOffset = ( | ||
componentHeight: number, | ||
parentHeight = 0, | ||
verticalAlignment: VerticalAlignment = 'top' | ||
): number => { | ||
'worklet'; | ||
switch (verticalAlignment) { | ||
case 'top': | ||
return 0; | ||
case 'bottom': | ||
return parentHeight - componentHeight; | ||
case 'center': | ||
return (parentHeight - componentHeight) / 2; | ||
} | ||
}; |
Oops, something went wrong.