Many people know Spotify Wrapped which displays the listening activity for the past year. The last part of this story provides a summary about top songs, top artists, top genre and total minutes. Here the users can select between different styles of this summary that can be shared.
We want to show you how to implement this component without using any third-party libraries.
General functionality
In general there's just a horizontal scrolling container displaying different styles of this summary. When scrolling it's snapped that the selected summary is always centered.
Implementing the summary component
At first we create a basic summary component that is inspired by the design of Spotify. Here, an image is displayed at the top and some data is displayed in a table format below. Furthermore, we need to think about the size of this component, because it should not take the complete screen and should be responsive to fit different screen sizes. That's why we took 60% of the screen height and about 67% of the screen width.
const { width, height } = Dimensions.get("screen");
// calculating size of summary component
export const SUMMARY_HEIGHT = height * 0.6;
export const SUMMARY_WIDTH = width * 0.665;
Implementing the design is using some flex styling and implementing some rows and columns for displaying the data. In addition, props for updating the background and text color are added.
Below, you can find a snippet of this summary component. The complete implementation can be found here.
type Props = {
backgroundColor: string;
textColor: string;
};
export const Summary: FunctionComponent<Props> = ({
backgroundColor,
textColor,
}) => {
return (
<View
style={[
styles.container,
{
backgroundColor: backgroundColor,
height: SUMMARY_HEIGHT,
width: SUMMARY_WIDTH,
},
]}
>
<View style={styles.header}>
<View style={[styles.headerPlaceholder, { backgroundColor: backgroundColor }]}></View>
</View>
<View style={styles.body}>
<View style={styles.row}>
<View style={styles.column}>
<Text style={[styles.title, { color: textColor }]}>
{"Top songs"}
</Text>
<Text style={[styles.text, { color: textColor }]}>{"#1 Song"}</Text>
</View>
<View style={styles.column}>
<Text style={[styles.title, { color: textColor }]}>
{"Top categories"}
</Text>
</View>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
borderRadius: 8,
padding: 16,
},
header: {
flex: 1,
backgroundColor: "white",
marginBottom: 16,
justifyContent: "center",
alignItems: "center",
},
/** Further styles **/
});
Looks great so far.
Displaying multiple summaries
In the next step, we want to display multiple summaries next to each other. For achieving this, we can use the ScrollView with a horizontal orientation by setting the horizontal
property. In addition, we add a container around the summaries for adding some space between them.
export default function App() {
return (
<View style={styles.container}>
<View style={[styles.summaryScrollContainer, { height: SUMMARY_HEIGHT }]}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
>
<View style={styles.summaryContainer}>
<Summary backgroundColor="green" textColor="white" />
</View>
<View style={styles.summaryContainer}>
<Summary backgroundColor="red" textColor="white" />
</View>
<View style={styles.summaryContainer}>
<Summary backgroundColor="blue" textColor="white" />
</View>
</ScrollView>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "white",
alignItems: "center",
justifyContent: "center",
},
summaryScrollContainer: {
marginBottom: 12,
},
summaryContainer: {
paddingHorizontal: 8,
},
});
Furthermore we need to center the summaries in the ScrollView using the contentContainerStyle
property. For centering the first and last element, we need to add some padding horizontally.
This padding can be calculated easily with this formula
padding = (SCREEN_WIDTH - (SUMMARY_WIDTH + SUMMARY_PADDING)) / 2
Translated into code:
const screenWidth = Dimensions.get("screen").width;
// 16 is padding that is added horizontally around summary
const summaryWidth = SUMMARY_WIDTH + 16
// padding that is located on the left and right for centering the summary item
const horizontalPadding = (screenWidth - summaryWidth) / 2;
Now we got the summary items displayed in a centered and horizontal ScrollView.
Improving the scrolling
When inspecting the scrolling behavior in Spotify, some snapping can be found. We can implement this behavior in the ScrollView using snapToInterval
. This property helps to stop at a specific position. We can use our summaryWidth variable (containing SUMMARY_WIDTH and padding) for this.
Another useful property is decelerationRate
, which is used for defining speed for users fingers lift when scrolling.
return (
<ScrollView
ref={scrollViewRef}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: horizontalPadding }}
decelerationRate="fast"
// snap interval for width of summary items
snapToInterval={summaryWidth}
>
{/* summary components */}
</ScrollView>
)
Now it's already working fine.
Implement pagination dots
Next step is to implement the dots for displaying the current summary item. At first, we need to store the index of the current summary item in the state. And update this when a dot is pressed.
// further code
const [selected, setSelected] = useState(0);
const changePage = (index: number) => {
setSelected(index)
}
// further code
We place the dots below the scroll view and in a row. The dot itself is just a circle with a background color which is changed when it's selected. Furthermore it accepts a callback for changing the current summary when it's pressed.
<View style={styles.dotsContainer}>
<Dot selected={selected === 0} onPress={() => changePage(0)} />
<Dot selected={selected === 1} onPress={() => changePage(1)} />
<Dot selected={selected === 2} onPress={() => changePage(2)} />
</View>
Pressing the dot is working, but it's not connected to the ScrollView yet. For achieving this we need to do two things. First, creating a reference of the ScrollView that we can animate the scrolling when a dot is pressed. And second, we need to update the state and scrolling finished.
Animate scrolling when dot is pressed
When pressing a dot the position of the x axis needs to be calculated. This can be done easily by multiplying selected index with the summary width.
const scrollViewRef = useRef<ScrollView>(null)
const changePage = (index: number) => {
if (scrollViewRef.current) {
setSelected(index)
// calculate scroll position by multiplying selected index with with of summary item
scrollViewRef.current.scrollTo({ x: index * summaryWidth, animated: true })
}
}
return (
<ScrollView
ref={scrollViewRef}
>
{/* summary components */}
</ScrollView>
)
Updating state when scrolling finished
Updating the state can be done listening to onMomentumScrollEnd
property of ScrollView. It provides some information about the current scroll position when scrolling ended. We can calculate the current index by dividing the contentOffset for x axis with the summary width.
const onScrollEnd = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const item = Math.round(event.nativeEvent.contentOffset.x / summaryWidth)
setSelected(item)
}
return (
<ScrollView
// updating the index when scroll is ended
onMomentumScrollEnd={onScrollEnd}
>
{/* summary components */}
</ScrollView>
)
Result
The result looks pretty nice. We can just add the overScrollMode
property and set it to never. in the ScrollView. It removes some dragging visuals on Android.
Further thoughts
The same thing can be achieved as well with a FlatList instead of a ScrollView. Here you just need to create an array of items for rendering the different summary styles.
The only feature missing is the sharing functionality where you can share the selected summary style on social media.
Top comments (0)