Skip to content

Commit

Permalink
Merge pull request #580 from cocrafts/ltminhthu/highlights-animation
Browse files Browse the repository at this point in the history
[MS-303] [wallet] update smooth animation in new explorer UI
  • Loading branch information
tanlethanh authored Jun 29, 2024
2 parents d183abe + a2a8568 commit a47b066
Show file tree
Hide file tree
Showing 11 changed files with 337 additions and 284 deletions.
111 changes: 111 additions & 0 deletions apps/wallet/src/features/Explorer/Highlights/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { type FC, useEffect } from 'react';
import { StyleSheet } from 'react-native';
import type { SharedValue, WithTimingConfig } from 'react-native-reanimated';
import Animated, {
interpolate,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import type { WidgetDocument } from '@walless/store';

import HighlightItem from './HighlightItem';
import { MAX_X_OFFSET } from './shared';

interface Props {
widget: WidgetDocument;
index: number;
currentIndex: number;
dataLength: number;
dragXOffset: SharedValue<number>;
}

const timingConfig: WithTimingConfig = { duration: 650 };

const Card: FC<Props> = ({
widget,
index,
currentIndex,
dataLength,
dragXOffset,
}) => {
const xOffset = useSharedValue(0);
const scale = useSharedValue(1);
const opacity = useSharedValue(1);

const animatedStyle = useAnimatedStyle(() => {
const addingTranslateX = interpolate(
index - currentIndex,
[dataLength - currentIndex, 0],
[0, dragXOffset.value],
);

const addingScale = interpolate(
dragXOffset.value,
[-MAX_X_OFFSET, MAX_X_OFFSET],
[0.08, -0.08],
);

return {
opacity: opacity.value,
transform: [
{ translateX: xOffset.value + addingTranslateX },
{ scale: scale.value + addingScale },
],
};
}, [xOffset, scale, opacity, dragXOffset, currentIndex]);

const handleSwipe = () => {
if (index >= currentIndex) {
const newScale = interpolate(
index - currentIndex,
[0, dataLength],
[1, 0.08],
);
scale.value = withTiming(newScale, timingConfig);
} else if (currentIndex - index === 1) {
scale.value = withTiming(scale.value + 0.15, timingConfig);
}

const isPopped = index < currentIndex;
xOffset.value = withTiming(
isPopped ? -200 : (index - currentIndex) * 34,
timingConfig,
);

const toCurrent = index === currentIndex;
const toPopped = index < currentIndex;
const toHidden = toPopped || index - currentIndex > 2;
if (toCurrent) {
opacity.value = withTiming(1, timingConfig);
} else if (toHidden) {
opacity.value = withTiming(0, timingConfig);
} else {
opacity.value = 1;
}
};

const containerStyle = { zIndex: -index };

useEffect(handleSwipe, [currentIndex]);

return (
<Animated.View
style={[
containerStyle,
animatedStyle,
index !== currentIndex && styles.absolute,
]}
>
<HighlightItem widget={widget} />
</Animated.View>
);
};

export default Card;

const styles = StyleSheet.create({
absolute: {
position: 'absolute',
},
});
99 changes: 99 additions & 0 deletions apps/wallet/src/features/Explorer/Highlights/CardCarousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { type FC, useEffect, useRef } from 'react';
import { StyleSheet, View } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { runOnJS, useSharedValue } from 'react-native-reanimated';
import type { WidgetDocument } from '@walless/store';

import Card from './Card';
import { MAX_X_OFFSET, SWIPE_THRESHOLD } from './shared';

interface Props {
widgets: WidgetDocument[];
currentIndex: number;
onChangeCurrentIndex: (index: number) => void;
}

const CardCarousel: FC<Props> = ({
widgets,
currentIndex,
onChangeCurrentIndex,
}) => {
const pressed = useRef(false);
const autoSwipeDirection = useRef(-1);
const xOffset = useSharedValue(0);

const handleSwipeLeft = () => {
onChangeCurrentIndex(currentIndex + 1);
};

const handleSwipeRight = () => {
onChangeCurrentIndex(currentIndex - 1);
};

const pan = Gesture.Pan()
.onUpdate((event) => {
pressed.current = true;
if (currentIndex === widgets.length - 1 && event.translationX < 0) return;
if (currentIndex === 0 && event.translationX > 0) return;

if (Math.abs(event.translationX) < MAX_X_OFFSET)
xOffset.value = event.translationX;
})
.onFinalize(() => {
pressed.current = false;
if (
xOffset.value < -SWIPE_THRESHOLD &&
currentIndex < widgets.length - 1
) {
runOnJS(handleSwipeLeft)();
} else if (xOffset.value > SWIPE_THRESHOLD && currentIndex > 0) {
runOnJS(handleSwipeRight)();
}

xOffset.value = 0;
});

useEffect(() => {
const timer = setTimeout(() => {
if (pressed.current) return;
if (currentIndex == widgets.length - 1) {
autoSwipeDirection.current = -1;
} else if (currentIndex === 0) {
autoSwipeDirection.current = 1;
}

onChangeCurrentIndex(currentIndex + autoSwipeDirection.current);
}, 2000);

return () => clearTimeout(timer);
}, [currentIndex, pressed]);

return (
<GestureDetector gesture={pan}>
<View style={styles.container}>
{widgets.map((card, index) => {
return (
<Card
key={card._id}
widget={card}
index={index}
currentIndex={currentIndex}
dataLength={widgets.length}
dragXOffset={xOffset}
/>
);
})}
</View>
</GestureDetector>
);
};

export default CardCarousel;

const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 20,
cursor: 'pointer',
},
});
27 changes: 11 additions & 16 deletions apps/wallet/src/features/Explorer/Highlights/HighlightIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,28 @@
import type { FC } from 'react';
import { useMemo } from 'react';
import { StyleSheet } from 'react-native';
import type { SharedValue } from 'react-native-reanimated';
import { View } from '@walless/gui';

import IndicatorDot from './IndicatorDot';

interface HighlightIndicatorProps {
currentIndex: number;
dataLength: number;
onSelectItem: (index: number) => void;
currentIndex: SharedValue<number>;
animatedValue: SharedValue<number>;
}

const HighlightIndicator: FC<HighlightIndicatorProps> = ({
currentIndex,
dataLength,
onSelectItem,
animatedValue,
}) => {
const data = Array.from({ length: dataLength }, (_, i) => i);
const indexes = useMemo(() => {
return Array.from({ length: dataLength }, (_, i) => i);
}, [dataLength]);

return (
<View style={styles.container}>
{data.map((item) => {
{indexes.map((index) => {
return (
<IndicatorDot
key={item}
index={item}
data={data}
onPress={onSelectItem}
animatedValue={animatedValue}
/>
<IndicatorDot key={index} index={index} currentIndex={currentIndex} />
);
})}
</View>
Expand All @@ -40,6 +33,8 @@ export default HighlightIndicator;

const styles = StyleSheet.create({
container: {
alignSelf: 'center',
flexDirection: 'row',
justifyContent: 'center',
gap: 7,
},
});
Loading

0 comments on commit a47b066

Please sign in to comment.