Skip to content

Commit b472eb9

Browse files
adids1221ethanshar
andauthored
Feat/marquee 2 (#2311)
* New Marquee component, Marquee example screen * Fixed exmaple screen * Refactor the component code, changed the example screen * Code refactor, added duration logic * removed loadsh import * Fixed vertical number of lines * Fixed review notes * Marquee docs api.json file * Fixed review notes, using isHorizontal var Co-authored-by: Ethan Sharabi <[email protected]>
1 parent a03155d commit b472eb9

File tree

7 files changed

+285
-1
lines changed

7 files changed

+285
-1
lines changed

demo/src/screens/MenuStructure.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export const navigationData = {
4343
screen: 'unicorn.components.SharedTransitionScreen'
4444
},
4545
{title: 'Stack Aggregator', tags: 'stack aggregator', screen: 'unicorn.components.StackAggregatorScreen'},
46-
{title: 'Wheel Picker Dialog', tags: 'wheel picker dialog', screen: 'unicorn.components.WheelPickerDialogScreen'}
46+
{title: 'Wheel Picker Dialog', tags: 'wheel picker dialog', screen: 'unicorn.components.WheelPickerDialogScreen'},
47+
{title: 'Marquee', tags: 'sliding text', screen: 'unicorn.components.MarqueeScreen'}
4748
]
4849
},
4950
Form: {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import _ from 'lodash';
2+
import React, {Component} from 'react';
3+
import {StyleSheet} from 'react-native';
4+
import {Marquee, MarqueeDirections, Text, View, Spacings} from 'react-native-ui-lib';
5+
import {renderBooleanOption, renderMultipleSegmentOptions} from '../ExampleScreenPresenter';
6+
7+
export default class MarqueeScreen extends Component<{}> {
8+
state = {
9+
duration: 5000,
10+
directionHorizontal: true,
11+
directionVertical: true,
12+
numOfReps: -1
13+
};
14+
15+
renderHorizontalSection = () => {
16+
const {directionHorizontal, numOfReps, duration} = this.state;
17+
return (
18+
<View marginV-s3 center>
19+
<Text h2 marginB-s2 $textDefault>
20+
Horizontal
21+
</Text>
22+
<Text h3 marginV-s2 $textDefault>
23+
Marquee: {directionHorizontal ? MarqueeDirections.LEFT : MarqueeDirections.RIGHT}
24+
</Text>
25+
<Marquee
26+
key={`${directionHorizontal}-${duration}-${numOfReps}`}
27+
label={'Hey there, this is the new Marquee component from UILIB!'}
28+
direction={directionHorizontal ? MarqueeDirections.LEFT : MarqueeDirections.RIGHT}
29+
duration={duration}
30+
numberOfReps={numOfReps}
31+
containerStyle={[styles.containerHorizontal, styles.horizontal]}
32+
/>
33+
</View>
34+
);
35+
};
36+
37+
renderVerticalSection = () => {
38+
const {directionVertical, numOfReps, duration} = this.state;
39+
return (
40+
<View marginV-s3 center>
41+
<Text h2 marginB-s2 $textDefault>
42+
Vertical
43+
</Text>
44+
<Text h3 marginV-s2 $textDefault>
45+
Marquee: {directionVertical ? MarqueeDirections.UP : MarqueeDirections.DOWN}
46+
</Text>
47+
<Marquee
48+
label={
49+
'Hey there, this is the new Marquee! Hey there, this is the new Marquee! Hey there, this is the new Marquee! Hey there, this is the new Marquee!'
50+
}
51+
labelStyle={styles.label}
52+
key={`${directionVertical}-${duration}-${numOfReps}`}
53+
direction={directionVertical ? MarqueeDirections.UP : MarqueeDirections.DOWN}
54+
duration={duration}
55+
numberOfReps={numOfReps}
56+
containerStyle={[styles.containerHorizontal, styles.vertical]}
57+
/>
58+
</View>
59+
);
60+
};
61+
62+
render() {
63+
return (
64+
<View flex padding-page>
65+
<Text h1 center margin-20 $textDefault>
66+
Marquee
67+
</Text>
68+
<View>
69+
{renderMultipleSegmentOptions.call(this, 'Duration (optional)', 'duration', [
70+
{label: '3000', value: 3000},
71+
{label: '5000', value: 5000},
72+
{label: '10000', value: 10000}
73+
])}
74+
{renderMultipleSegmentOptions.call(this, 'Number Of Reps', 'numOfReps', [
75+
{label: 'Infinite', value: -1},
76+
{label: '1', value: 1},
77+
{label: '3', value: 3},
78+
{label: '5', value: 5}
79+
])}
80+
<View marginV-s2>
81+
{renderBooleanOption.call(this, 'Direction Horizontal: Left To Right/Right To Left', 'directionHorizontal')}
82+
{renderBooleanOption.call(this, 'Direction Vertical: Bottom To Up/Up To Bottom', 'directionVertical')}
83+
</View>
84+
</View>
85+
{this.renderHorizontalSection()}
86+
{this.renderVerticalSection()}
87+
</View>
88+
);
89+
}
90+
}
91+
92+
const styles = StyleSheet.create({
93+
containerHorizontal: {
94+
borderWidth: 1,
95+
borderColor: 'black',
96+
marginVertical: Spacings.s2
97+
},
98+
horizontal: {width: 200},
99+
vertical: {width: 250, height: 100, alignItems: 'center'},
100+
containerVertical: {borderWidth: 1, borderColor: 'black', marginVertical: Spacings.s2},
101+
label: {alignSelf: 'center'}
102+
});

demo/src/screens/componentScreens/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export function registerScreens(registrar) {
3030
registrar('unicorn.components.GridViewScreen', () => require('./GridViewScreen').default);
3131
registrar('unicorn.components.KeyboardAwareScrollViewScreen', () => require('./KeyboardAwareScrollViewScreen').default);
3232
registrar('unicorn.components.MaskedInputScreen', () => require('./MaskedInputScreen').default);
33+
registrar('unicorn.components.MarqueeScreen', () => require('./MarqueeScreen').default);
3334
registrar('unicorn.components.OverlaysScreen', () => require('./OverlaysScreen').default);
3435
registrar('unicorn.components.PageControlScreen', () => require('./PageControlScreen').default);
3536
registrar('unicorn.components.PanDismissibleScreen', () => require('./PanDismissibleScreen').default);

src/components/marquee/index.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React, {useCallback, useEffect, useState} from 'react';
2+
import {LayoutChangeEvent, LayoutRectangle, StyleSheet} from 'react-native';
3+
import {useSharedValue, useAnimatedStyle, withTiming, withRepeat, Easing} from 'react-native-reanimated';
4+
import View from '../view';
5+
import Text from '../text';
6+
import {MarqueeDirections, MarqueeProps} from './types';
7+
8+
const DEFAULT_DURATION = 3000;
9+
const DEFAULT_DURATION_PER_WORD = 250;
10+
11+
function Marquee(props: MarqueeProps) {
12+
const {label, labelStyle, direction = MarqueeDirections.LEFT, duration, numberOfReps = -1, containerStyle} = props;
13+
14+
const calcDuration = () => {
15+
const numOfWords = label.split(' ').length;
16+
return DEFAULT_DURATION + DEFAULT_DURATION_PER_WORD * numOfWords;
17+
};
18+
19+
const isHorizontal = direction === MarqueeDirections.LEFT || direction === MarqueeDirections.RIGHT;
20+
const fixedDuration = duration || (isHorizontal ? calcDuration() : DEFAULT_DURATION);
21+
22+
const [viewLayout, setViewLayout] = useState<LayoutRectangle | undefined>(undefined);
23+
const [textLayout, setTextLayout] = useState<LayoutRectangle | undefined>(undefined);
24+
25+
const offset = useSharedValue<number | undefined>(undefined);
26+
27+
let initialOffset = 0;
28+
let axisX = false;
29+
let axisY = false;
30+
31+
if (isHorizontal) {
32+
axisX = true;
33+
} else {
34+
axisY = true;
35+
}
36+
37+
const onLayoutView = useCallback((event: LayoutChangeEvent) => {
38+
setViewLayout(event.nativeEvent.layout);
39+
}, []);
40+
41+
const onLayoutText = useCallback((event: LayoutChangeEvent) => {
42+
setTextLayout(event.nativeEvent.layout);
43+
}, []);
44+
45+
const startAnimation = (fromValue: number, toValue: number, backToValue: number) => {
46+
initialOffset = fromValue;
47+
offset.value = initialOffset;
48+
49+
offset.value = withRepeat(withTiming(toValue, {duration: fixedDuration, easing: Easing.linear}),
50+
numberOfReps,
51+
false,
52+
finished => {
53+
if (finished) {
54+
offset.value = initialOffset;
55+
offset.value = withTiming(backToValue, {duration: fixedDuration, easing: Easing.linear});
56+
}
57+
});
58+
};
59+
60+
useEffect(() => {
61+
if (viewLayout && textLayout) {
62+
switch (direction) {
63+
case MarqueeDirections.RIGHT:
64+
startAnimation(-textLayout.width, viewLayout.width, 0);
65+
break;
66+
case MarqueeDirections.LEFT:
67+
startAnimation(viewLayout?.width, -textLayout.width, viewLayout.width - textLayout.width);
68+
break;
69+
case MarqueeDirections.UP:
70+
startAnimation(viewLayout.height, -textLayout.height, viewLayout.height - textLayout.height);
71+
break;
72+
case MarqueeDirections.DOWN:
73+
startAnimation(-textLayout.height, viewLayout.height, 0);
74+
break;
75+
}
76+
}
77+
}, [viewLayout, textLayout]);
78+
79+
const translateStyle = useAnimatedStyle(() => {
80+
if (offset.value) {
81+
return {
82+
transform: [{translateX: axisX ? offset.value : 0}, {translateY: axisY ? offset.value : 0}],
83+
position: 'absolute',
84+
width: !isHorizontal || textLayout?.width ? textLayout?.width : '400%'
85+
};
86+
}
87+
return {position: 'absolute', width: !isHorizontal || textLayout?.width ? textLayout?.width : '400%'};
88+
});
89+
90+
return (
91+
<View style={[styles.container, containerStyle]} onLayout={onLayoutView}>
92+
<View reanimated style={[translateStyle]}>
93+
<Text style={[styles.text, labelStyle]} onLayout={onLayoutText}>
94+
{label}
95+
</Text>
96+
</View>
97+
<Text style={[styles.text, labelStyle, styles.hiddenText]} numberOfLines={1}>
98+
{label}
99+
</Text>
100+
</View>
101+
);
102+
}
103+
104+
export {MarqueeProps, MarqueeDirections};
105+
106+
export default Marquee;
107+
108+
const styles = StyleSheet.create({
109+
container: {overflow: 'hidden'},
110+
text: {alignSelf: 'center'},
111+
hiddenText: {color: 'transparent'}
112+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "Marquee",
3+
"category": "media",
4+
"description": "Marquee component for sliding text",
5+
"extends": [""],
6+
"example": "",
7+
"props": [
8+
{"name": "label", "type": "string", "description": "Marquee label"},
9+
{"name": "labelStyle", "type": "TextProps['style']", "description": "Marquee label style"},
10+
{"name": "direction", "type": "MarqueeDirections", "description": "Marquee direction", "default": "LEFT"},
11+
{"name": "duration", "type": "number", "description": "Marquee animation duration", "default": "3 secondes"},
12+
{
13+
"name": "numberOfReps",
14+
"type": "number",
15+
"description": "Marquee animation number of repetitions",
16+
"default": "infinite"
17+
},
18+
{"name": "containerStyle", "type": "ViewProps['style']", "description": "Custom container style"}
19+
],
20+
"snippet": [
21+
"<Marquee",
22+
"key={`${directionHorizontal}-${duration}-${numOfReps}`}",
23+
"label={'Hey there, this is the new Marquee component from UILIB!'}",
24+
"direction={directionHorizontal ? MarqueeDirections.LEFT : MarqueeDirections.RIGHT}",
25+
"duration={duration}",
26+
"numberOfReps={numOfReps}",
27+
"containerStyle={[styles.containerHorizontal, styles.horizontal]}",
28+
"/>"
29+
]
30+
}

src/components/marquee/types.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type {PropsWithChildren} from 'react';
2+
import {TextProps} from '../text';
3+
import {ViewProps} from '../view';
4+
5+
export enum MarqueeDirections {
6+
RIGHT = 'RIGHT', //LTR
7+
LEFT = 'LEFT', //RTL
8+
UP = 'UP', //Bottom Up
9+
DOWN = 'DOWN' //Up To Bottom
10+
}
11+
12+
export type MarqueeProps = PropsWithChildren<{
13+
/**
14+
* Marquee label
15+
*/
16+
label: string;
17+
/**
18+
* Marquee label style
19+
*/
20+
labelStyle?: TextProps['style'];
21+
/**
22+
* Marquee direction
23+
*/
24+
direction?: MarqueeDirections;
25+
/**
26+
* Marquee animation duration
27+
*/
28+
duration?: number;
29+
/**
30+
* Marquee animation number of repetitions
31+
*/
32+
numberOfReps?: number;
33+
/**
34+
* Custom container style
35+
*/
36+
containerStyle?: ViewProps['style'];
37+
}>;

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export {default as KeyboardAwareFlatList} from './components/KeyboardAwareScroll
8888
export {default as ListItem, ListItemProps} from './components/listItem';
8989
export {default as LoaderScreen, LoaderScreenProps} from './components/loaderScreen';
9090
export {default as MaskedInput, MaskedInputProps} from './components/maskedInput';
91+
export {default as Marquee, MarqueeDirections, MarqueeProps} from './components/marquee';
9192
export {default as Modal, ModalProps, ModalTopBarProps} from './components/modal';
9293
export {default as Overlay, OverlayTypes} from './components/overlay';
9394
export {default as PageControl, PageControlProps} from './components/pageControl';

0 commit comments

Comments
 (0)