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

Feature/progress indicator #43

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react';

import { CircularExample, LinearExample } from './ProgressIndicator';

export default {
title: 'components/ProgressIndicator',
component: LinearExample,
} as ComponentMeta<typeof LinearExample>;

export const Linear: ComponentStory<typeof LinearExample> = args => <LinearExample {...args} />;

Linear.args = {
progress: 0.45,
indeterminateDuration: 2000,
// to counteract align center by the parent
containerStyle: {
alignSelf: 'stretch',
},
};

Linear.parameters = {
docs: {
source: {
code: `
<ProgressIndicator.Linear progress={0.45} animating {...rest} />
`,
language: 'tsx',
type: 'auto',
},
},
};

export const Circular: ComponentStory<typeof CircularExample> = args => (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CircularExample has props

  • animatedValue
  • trackColor - not required for circular
  • visible - doesn't do anything

Also, What is the use for indeterminateMaxWidth in Circular indicator?

<CircularExample {...args} />
);

Circular.args = {
progress: 0.45,
};

Circular.parameters = {
docs: {
source: {
code: `
<ProgressIndicator.Linear color="colors.primary" size={30} animating {...rest} />
`,
language: 'tsx',
type: 'auto',
},
},
};
17 changes: 17 additions & 0 deletions example/stories/components/ProgressIndicator/ProgressIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
useMolecules,
CircularProgressIndicatorProps,
LinearProgressIndicatorProps,
} from 'bamboo-molecules';

export const CircularExample = (props: CircularProgressIndicatorProps) => {
const { ProgressIndicator } = useMolecules();

return <ProgressIndicator.Circular {...props} />;
};

export const LinearExample = (props: LinearProgressIndicatorProps) => {
const { ProgressIndicator } = useMolecules();

return <ProgressIndicator.Linear {...props} />;
};
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"react-native": "^0.70.1",
"react-native-builder-bob": "^0.18.3",
"react-native-document-picker": "^8.1.3",
"react-native-svg": "12.3.0",
"react-native-vector-icons": "^9.2.0",
"react-native-web": "^0.18.9",
"typescript": "~4.3.5"
Expand All @@ -89,6 +90,7 @@
"react-dom": "^16.8.0",
"react-native": "^0.60.0",
"react-native-document-picker": "^8.1.3",
"react-native-svg": "12.3.0",
"react-native-vector-icons": ">9.2.0"
},
"react-native-builder-bob": {
Expand Down
41 changes: 10 additions & 31 deletions src/components/ActivityIndicator/ActivityIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { Animated, Easing, Platform, StyleProp, StyleSheet, ViewStyle } from 'react-native';
import { Animated, Easing, Platform, StyleProp, ViewStyle } from 'react-native';
import type { ActivityIndicatorProps } from '@webbee/bamboo-atoms';

import type { ComponentStylePropWithVariants } from '../../types';
import { useComponentStyles, useMolecules } from '../../hooks';
import AnimatedSpinner from './AnimatedSpinner';
import { styles } from './utils';

export type Props = ActivityIndicatorProps & {
/**
Expand All @@ -23,11 +23,13 @@ export type Props = ActivityIndicatorProps & {
* Whether the indicator should hide when not animating.
*/
hidesWhenStopped?: boolean;
/**
* animation duration
*/
duration?: number;
style?: StyleProp<ViewStyle>;
};

const DURATION = 2400;

const mapIndicatorSize = (indicatorSize: 'small' | 'large' | number | undefined) => {
if (typeof indicatorSize === 'string') {
return indicatorSize === 'small' ? 24 : 48;
Expand Down Expand Up @@ -61,6 +63,7 @@ const ActivityIndicator = ({
hidesWhenStopped = true,
size: indicatorSize = 'small',
style: styleProp,
duration = 2400,
...rest
}: Props) => {
const { View } = useMolecules();
Expand Down Expand Up @@ -120,7 +123,7 @@ const ActivityIndicator = ({
if (rotation.current === undefined) {
// Circular animation in loop
rotation.current = Animated.timing(timer, {
duration: DURATION,
duration,
easing: Easing.linear,
// Animated.loop does not work if useNativeDriver is true on web
useNativeDriver: Platform.OS !== 'web',
Expand All @@ -146,7 +149,7 @@ const ActivityIndicator = ({
return () => {
if (animating) stopRotation();
};
}, [animating, fade, hidesWhenStopped, startRotation, animationScale, timer]);
}, [animating, fade, hidesWhenStopped, startRotation, animationScale, timer, duration]);

return (
<View
Expand All @@ -166,7 +169,7 @@ const ActivityIndicator = ({
color={color}
timer={timer}
styles={styles}
duration={DURATION}
duration={duration}
/>
);
})}
Expand All @@ -175,28 +178,4 @@ const ActivityIndicator = ({
);
};

const styles = {
container: {
justifyContent: 'center',
alignItems: 'center',
},

layer: {
...StyleSheet.absoluteFillObject,

justifyContent: 'center',
alignItems: 'center',
},
};

type CustomProps = {
color?: string;
animationScale?: string;
};

export const defaultStyles: ComponentStylePropWithVariants<ViewStyle, '', CustomProps> = {
color: 'colors.primary',
animationScale: 'animation.scale',
};

export default memo(ActivityIndicator);
112 changes: 52 additions & 60 deletions src/components/ActivityIndicator/AnimatedSpinner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,20 @@ const AnimatedSpinner = ({
color: string;
styles: StyleProp<any>;
}) => {
const containerStyle = useMemo(
() => ({
width: size,
height: size / 2,
overflow: 'hidden' as const,
}),
[size],
);
const offsetContainerStyles = useMemo(() => {
const offsetStyle = index ? { top: size / 2 } : null;

return [containerStyle, offsetStyle];
}, [containerStyle, index, size]);

const frames = (60 * duration) / 1000;
const easing = Easing.bezier(0.4, 0.0, 0.7, 1.0);
const inputRange = useMemo(
() => Array.from(new Array(frames), (_, frameIndex) => frameIndex / (frames - 1)),
[frames],
);
const outputRange = useMemo(
() =>
Array.from(new Array(frames), (_, frameIndex) => {
const { containerStyle, offsetContainerStyle, layerStyle, viewportStyle, lineStyle } =
useMemo(() => {
const _containerStyle = {
width: size,
height: size / 2,
overflow: 'hidden' as const,
};
const frames = (60 * duration) / 1000;
const easing = Easing.bezier(0.4, 0.0, 0.7, 1.0);
const inputRange = Array.from(
new Array(frames),
(_, frameIndex) => frameIndex / (frames - 1),
);
const outputRange = Array.from(new Array(frames), (_, frameIndex) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs to be broken down into a separate function and be memoized.
You are creating this range over and over again.. every time the component is rendered. or for example color changes..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for input range

let progress = (2 * frameIndex) / (frames - 1);
const rotation = index ? +(360 - 15) : -(180 - 15);

Expand All @@ -49,48 +40,49 @@ const AnimatedSpinner = ({
const direction = index ? -1 : +1;

return `${direction * (180 - 30) * easing(progress) + rotation}deg`;
}),
[easing, frames, index],
);

const layerStyle = {
width: size,
height: size,
transform: [
{
rotate: timer.interpolate({
inputRange: [0, 1],
outputRange: [`${0 + 30 + 15}deg`, `${2 * 360 + 30 + 15}deg`],
}),
},
],
};

const viewportStyle = {
width: size,
height: size,
transform: [
{
translateY: index ? -size / 2 : 0,
},
{
rotate: timer.interpolate({ inputRange, outputRange }),
},
],
};
});

const lineStyle = {
width: size,
height: size,
borderColor: color,
borderWidth: size / 10,
borderRadius: size / 2,
};
return {
containerStyle: _containerStyle,
offsetContainerStyle: [_containerStyle, index ? { top: size / 2 } : null],
layerStyle: {
width: size,
height: size,
transform: [
{
rotate: timer.interpolate({
inputRange: [0, 1],
outputRange: [`${0 + 30 + 15}deg`, `${2 * 360 + 30 + 15}deg`],
}),
},
],
},
viewportStyle: {
width: size,
height: size,
transform: [
{
translateY: index ? -size / 2 : 0,
},
{
rotate: timer.interpolate({ inputRange, outputRange }),
},
],
},
lineStyle: {
width: size,
height: size,
borderColor: color,
borderWidth: size / 10,
borderRadius: size / 2,
},
};
}, [color, duration, index, size, timer]);

return (
<Animated.View style={styles.layer}>
<Animated.View style={layerStyle}>
<Animated.View style={offsetContainerStyles} collapsable={false}>
<Animated.View style={offsetContainerStyle} collapsable={false}>
<Animated.View style={viewportStyle}>
<Animated.View style={containerStyle} collapsable={false}>
<Animated.View style={lineStyle} />
Expand Down
8 changes: 3 additions & 5 deletions src/components/ActivityIndicator/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
export {
default as ActivityIndicator,
Props as ActivityIndicatorProps,
defaultStyles as activityIndicatorStyles,
} from './ActivityIndicator';
export { default as ActivityIndicator, Props as ActivityIndicatorProps } from './ActivityIndicator';

export { activityIndicatorStyles } from './utils';
26 changes: 26 additions & 0 deletions src/components/ActivityIndicator/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { StyleSheet, ViewStyle } from 'react-native';
import type { ComponentStylePropWithVariants } from '../../types';

export const styles = {
container: {
justifyContent: 'center',
alignItems: 'center',
},

layer: {
...StyleSheet.absoluteFillObject,

justifyContent: 'center',
alignItems: 'center',
},
};

type CustomProps = {
color?: string;
animationScale?: string;
};

export const activityIndicatorStyles: ComponentStylePropWithVariants<ViewStyle, '', CustomProps> = {
color: 'colors.primary',
animationScale: 'animation.scale',
};
Loading